diff --git a/.changeset/build-stylex-cross-file-consts.md b/.changeset/build-stylex-cross-file-consts.md
new file mode 100644
index 0000000000000..736b20da4327d
--- /dev/null
+++ b/.changeset/build-stylex-cross-file-consts.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/build': patch
+---
+
+[fix] `astryxStylex` resolves `defineConsts` values read from another file. With the `{stylexOptions}` form (the one Storybook uses), a file that read only constants from a `.stylex.ts` module lost that import once StyleX compiled it, so the constants never reached the CSS: a constant used as a media-query key failed the build ("Invalid empty selector") and one used as a value shipped as a custom property nothing declares. In either form, a constant defined on the other side of the layer split, or in an Astryx package (compiled with the library class-name prefix) and read from an app file (compiled with its own), stayed unresolved.
+
+@vjeux
diff --git a/.changeset/contextmenu-glass-morph.md b/.changeset/contextmenu-glass-morph.md
new file mode 100644
index 0000000000000..cb0e131fcc9a6
--- /dev/null
+++ b/.changeset/contextmenu-glass-morph.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[feat] Under a finger, on every touch device, when a held finger opens a ContextMenu, the menu now grows out of the finger as iOS 26's does: it starts small at the press point and grows into its place beside the lifted trigger, overshooting slightly, as it fades in. Only its position, scale and opacity move, on the compositor. Under Reduce Motion the menu cross-fades in where it is instead, in 200 ms: nothing moves or grows.
+
+@vjeux
diff --git a/.changeset/contextmenu-lift-presentation.md b/.changeset/contextmenu-lift-presentation.md
new file mode 100644
index 0000000000000..25de19d5cf964
--- /dev/null
+++ b/.changeset/contextmenu-lift-presentation.md
@@ -0,0 +1,10 @@
+---
+'@astryxdesign/core': patch
+'@astryxdesign/cli': patch
+---
+
+[feat] Under a finger, on every touch device (iOS and Android alike), a ContextMenu opened by a held finger now presents as iOS 26's does. A block trigger itself lifts out of the page, with no copy and with its spot in the page held: it grows on from the press to 26 px larger on its longer side (15% at most), with the theme's high shadow fading in under it, over the theme's overlay, and it stays inside the layer runtime's viewport gutter. A tap on the tint closes the menu; while the lift is still growing, a tap or a drag closes nothing and reaches nothing under it, and the menu finishes opening (keys still work). The menu sits 16 px from the lifted trigger, below it when it fits and above it otherwise, centered on it or flush with the edge it is held against (right-to-left pages included), and fades in, with no row lit until the finger moves onto one. Closing returns the trigger to its place. Under Reduce Motion the animator cross-fades everything in place instead, in 200 ms, as the reduced-motion design record asks: nothing travels or grows. Inline triggers, pointer opens and the bottom sheet are unchanged. A fine pointer keeps today's menu at the cursor.
+
+The tint is the theme's modal overlay (`--color-overlay`).
+
+@vjeux
diff --git a/.changeset/contextmenu-presentation-popover.md b/.changeset/contextmenu-presentation-popover.md
new file mode 100644
index 0000000000000..b5ee8f739742e
--- /dev/null
+++ b/.changeset/contextmenu-presentation-popover.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[feat] Under a finger, a ContextMenu whose `presentation` is omitted now opens from a held finger on iOS's timeline (the touch model). Pass `presentation="popover"` to keep today's menu on every device: the menu at the pointer, opened by a finger held for 500 ms (the long press). Where the primary pointer is fine (a mouse or trackpad), a finger keeps that same long press.
+
+@vjeux
diff --git a/.changeset/contextmenu-press-constants.md b/.changeset/contextmenu-press-constants.md
new file mode 100644
index 0000000000000..a598482bd7bc7
--- /dev/null
+++ b/.changeset/contextmenu-press-constants.md
@@ -0,0 +1,8 @@
+---
+'@astryxdesign/core': patch
+'@astryxdesign/cli': patch
+---
+
+[chore] The UIKit constants table gains ContextMenu's touch timeline as measured on iOS 26.2: the press feedback starts at 150 ms, the press commits at 400 ms, and the menu displays at 735 ms. It also records the drift tolerance, and the press feedback's growth, spring and display latency. A closed-form press-feedback curve, checked against UIKit's per-frame presentation, comes with them. ContextMenu's touch timeline uses them.
+
+@vjeux
diff --git a/.changeset/contextmenu-touch-timeline.md b/.changeset/contextmenu-touch-timeline.md
new file mode 100644
index 0000000000000..2a279da741343
--- /dev/null
+++ b/.changeset/contextmenu-touch-timeline.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[feat] ContextMenu opens from a held finger on iOS 26's measured timeline, on every touch device (a coarse primary pointer, iOS and Android alike). The trigger starts to grow at 150 ms (the onset of core's one press machine), following UIKit's measured press curve on the compositor, and the hold runs on useLongPress's detector with UIKit's measured moments. At 400 ms the press takes the finger: its lift no longer sends a click, and the page cannot scroll under it. The menu opens at 735 ms if the finger is still down, or as soon as it lifts after 400 ms. A finger that lifts before 400 ms, or drifts more than 11.5 px on either axis before the menu opens, gets no menu, and the trigger springs back. The page stays still while the menu is up, and the trigger returns to its size when the menu closes. A touch open lights no row: the menu takes the focus, and a row lights only under the finger, as in UIKit (keyboard and pointer opens still focus the first item). A second finger cancels the press, and Android's own long-press menu no longer opens a second menu. A fine pointer (a mouse) keeps the right-click menu, and the touch recognizer stays off there.
+
+@vjeux
diff --git a/.changeset/gesture-arena-moving-cancel.md b/.changeset/gesture-arena-moving-cancel.md
new file mode 100644
index 0000000000000..5b7e52cad131a
--- /dev/null
+++ b/.changeset/gesture-arena-moving-cancel.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: a gesture that cancels itself on a finger's move (a long press dragged past its tolerance) keeps that finger claimed until it lifts, so the page never takes the drag over midway; a resting finger is still given back when its gesture is cancelled. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/gesture-browser-negotiation.md b/.changeset/gesture-browser-negotiation.md
new file mode 100644
index 0000000000000..1930e34496d7e
--- /dev/null
+++ b/.changeset/gesture-browser-negotiation.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: touch-action derivation, the native scroll probe and haptics for the gesture system. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/gesture-hook-fixes.md b/.changeset/gesture-hook-fixes.md
new file mode 100644
index 0000000000000..87f08a0948e31
--- /dev/null
+++ b/.changeset/gesture-hook-fixes.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: the gesture hooks call nothing back after unmount; `usePressTracker` brakes with the host by default and keeps one tracker, so an option change or a new ref never drops a press in progress; the recognizer's arena plumbing is `protected`. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/gesture-host-fixes.md b/.changeset/gesture-host-fixes.md
new file mode 100644
index 0000000000000..2fc0915c974a6
--- /dev/null
+++ b/.changeset/gesture-host-fixes.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: gesture host fixes from review. A page scroll under a still finger keeps the press and its click; watchers and touches nothing can claim get passive listeners only; a claimed pen holds only its own stylus touches; an element's own `touch-action` is never widened; slotted content scrolls with its shadow-tree scroller. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/gesture-host-owner.md b/.changeset/gesture-host-owner.md
new file mode 100644
index 0000000000000..151a939aff001
--- /dev/null
+++ b/.changeset/gesture-host-owner.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: the gesture host becomes the one document-level touch owner: a scroll lock's page hold, finger scrolls, the finger count, touch gates, and a kept press for continuous-tracking controls, whose element gets `touch-action: manipulation` where the pointer is coarse. A second finger fails a one-finger pan that has not claimed, so the page can pinch. A scroll-edge pan holds a lone finger from the first move that pulls away from an edge every scroller under it is at, so those moves are cancelled before it begins; with a second finger anywhere on the screen the page keeps its pinch. Where the primary pointer is fine, the host attaches nothing and registers no listener, following the pointer when it changes; a recognizer that reads each event's pointer itself opts out with `perEventPointer`, and a press tracker with it paints a finger's press feedback only, with no control event and no mouse or pen press. A press tracker whose hold acts (a stepper's hold-repeat) can bring a selection lock for its presses, as a recognizer can. Internal, for the touch components that adopt them; not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/gesture-host.md b/.changeset/gesture-host.md
new file mode 100644
index 0000000000000..eb9f2fec42db2
--- /dev/null
+++ b/.changeset/gesture-host.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: the gesture system's DOM host (claims, click suppression, the browser's takeover, the pointer behind a contact). Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/gesture-pan.md b/.changeset/gesture-pan.md
index 12eb02fc42391..908da1b367575 100644
--- a/.changeset/gesture-pan.md
+++ b/.changeset/gesture-pan.md
@@ -2,6 +2,6 @@
'@astryxdesign/core': patch
---
-[chore] Internal: the pan gesture recognizer, with UIKit's measured hysteresis and velocity filter. Not exported from the package entry point; no component uses it yet.
+[chore] Internal: the pan gesture recognizer, with UIKit's measured hysteresis and velocity filter. Not exported from the package entry point.
@vjeux
diff --git a/.changeset/gesture-press-tracking.md b/.changeset/gesture-press-tracking.md
new file mode 100644
index 0000000000000..1c66836c74243
--- /dev/null
+++ b/.changeset/gesture-press-tracking.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: UIControl press tracking with control and cell profiles and the gesture hooks, with every press painter writing `data-astryx-press` through one internal writer in `utils/pressFeedback.ts`. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/gesture-readme.md b/.changeset/gesture-readme.md
new file mode 100644
index 0000000000000..9a6293b811b68
--- /dev/null
+++ b/.changeset/gesture-readme.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: the gesture system's README for component authors, with its browser proof in Chromium and WebKit.
+
+@vjeux
diff --git a/.changeset/gesture-recognizer-core.md b/.changeset/gesture-recognizer-core.md
index 0b0bfc389235f..28caef0d5579c 100644
--- a/.changeset/gesture-recognizer-core.md
+++ b/.changeset/gesture-recognizer-core.md
@@ -2,6 +2,6 @@
'@astryxdesign/core': patch
---
-[chore] Internal: the gesture recognizer core for touch presentations: UIKit's recognizer state machine and the arena that settles which recognizer owns a touch. Not exported from the package entry point; no component uses it yet.
+[chore] Internal: the gesture recognizer core for touch presentations: UIKit's recognizer state machine and the arena that settles which recognizer owns a touch. Not exported from the package entry point.
@vjeux
diff --git a/.changeset/gesture-swipe-pinch-rotation-edge-hover.md b/.changeset/gesture-swipe-pinch-rotation-edge-hover.md
new file mode 100644
index 0000000000000..ffeadde679ef5
--- /dev/null
+++ b/.changeset/gesture-swipe-pinch-rotation-edge-hover.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: swipe, pinch, rotation, screen-edge pan and hover gesture recognizers. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/gesture-tap-long-press.md b/.changeset/gesture-tap-long-press.md
index 9ef786da65842..f1efef8d29014 100644
--- a/.changeset/gesture-tap-long-press.md
+++ b/.changeset/gesture-tap-long-press.md
@@ -2,6 +2,6 @@
'@astryxdesign/core': patch
---
-[chore] Internal: tap and long-press gesture recognizers with UIKit's measured thresholds. Not exported from the package entry point; no component uses them yet.
+[chore] Internal: tap and long-press gesture recognizers with UIKit's measured thresholds. Not exported from the package entry point.
@vjeux
diff --git a/.changeset/gesture-watcher.md b/.changeset/gesture-watcher.md
new file mode 100644
index 0000000000000..bec7cd286e49d
--- /dev/null
+++ b/.changeset/gesture-watcher.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: a recognizer created with `claimsTouches: false` is a watcher: passive listeners only, simultaneous with every recognizer, and it follows a touch the browser takes for a scroll to the lift. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/layer-exit-copy.md b/.changeset/layer-exit-copy.md
new file mode 100644
index 0000000000000..441788643f6d2
--- /dev/null
+++ b/.changeset/layer-exit-copy.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: the layer runtime gains one exit copy for touch presentations that animate a popover out: a static, inert copy of the closing popover, shown only in the top layer where the popover was drawn, for the close to play on.
+
+@vjeux
diff --git a/.changeset/layer-input-hold.md b/.changeset/layer-input-hold.md
new file mode 100644
index 0000000000000..89eb007172d37
--- /dev/null
+++ b/.changeset/layer-input-hold.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: one input hold for every layer (`Layer/inputHold`). While a layer presents, a touch, a press or a click anywhere reaches nothing (keys and assistive technology still act, and a pinch still zooms the page); while it dismisses, the same, and the layer's content is inert. The gesture host holds the layer's touches through its touch gates, and `isInputShield` lets a hit test see past a hold's shield. Not exported from the package entry point; ContextMenu's touch lift uses it.
+
+@vjeux
diff --git a/.changeset/lift-module.md b/.changeset/lift-module.md
new file mode 100644
index 0000000000000..de42637a5b97a
--- /dev/null
+++ b/.changeset/lift-module.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: one opt-in lift-from-trigger presentation for touch surfaces. The trigger itself lifts into the top layer above a tinted backdrop; a surface grows out of the press beside it on the shared rect morph and shrinks back into it on the layer runtime's exit copy; the trigger returns to its place. Translate, scale and opacity only; nothing mounted at rest, four elements while lifted. Not exported from the package entry point; ContextMenu's touch lift uses it.
+
+@vjeux
diff --git a/.changeset/motion-progress.md b/.changeset/motion-progress.md
new file mode 100644
index 0000000000000..6f5882f7f6e02
--- /dev/null
+++ b/.changeset/motion-progress.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: one progress module for the motion foundation. `progressCurve` gives a spring's progress from 0 to 1 as a closed-form curve, `mapProgress` maps any curve from another (a companion's dim following a sheet, a clamped range), `mapRange` is the clamped linear map they share, and `presentedProgressVelocity` reads the velocity an interrupted motion continues from. One velocity unit throughout: progress per second. Nothing public changes; ContextMenu's touch lift uses it.
+
+@vjeux
diff --git a/.changeset/motion-rect-morph.md b/.changeset/motion-rect-morph.md
new file mode 100644
index 0000000000000..8598e899b7e10
--- /dev/null
+++ b/.changeset/motion-rect-morph.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: the rect morph, one way for a touch presentation to grow out of a source rect and shrink back into it. `lerpPath` (a straight line between two rects) and `edgeGrowPath` (UIKit's popover, growing out of a point on its edge) name the rect an element shows at each progress; `morphCurves` turns a path into compositor curves (`translate`, `scale` and a companion `opacity`); `createRectMorph` presents an element along a path and dismisses it back on a copy or on itself, each continuing the other from the rect on screen with its velocity, with Reduced Motion left to the animator. It runs on new progress helpers (`progressCurve`, `mapProgress`, `mapRange`, `presentedProgressVelocity`). Not exported from the package entry point; ContextMenu's touch lift uses it.
+
+@vjeux
diff --git a/.changeset/presentation-resolver.md b/.changeset/presentation-resolver.md
new file mode 100644
index 0000000000000..decee65fa0585
--- /dev/null
+++ b/.changeset/presentation-resolver.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] An internal `usePresentation` / `readPresentation` resolves a component's `presentation` prop into the surface to render and whether its touch input modes apply, keeping an omitted value distinct from an explicit `'popover'` so a caller's choice always wins over a touch default. ContextMenu reads it; its touch behavior has its own entry.
+
+@vjeux
diff --git a/.changeset/reduced-motion-record.md b/.changeset/reduced-motion-record.md
new file mode 100644
index 0000000000000..2ec44f4c1f9c8
--- /dev/null
+++ b/.changeset/reduced-motion-record.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: Reduce Motion for the touch presentations follows design:reduced-motion beyond the animator too. `reducedMotionFade()` gives code beside the animator the animator's own fade (an opacity-only 200 ms cross-fade on a coarse pointer or after a touch release, at once on any other pointer), `animator.reducedMotion()` reports which form a change takes for feedback that plays no fade of its own, and `reducedMotion.stylex.ts` holds the fade's constants for StyleX transitions. The fade's length is not a theme motion role. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/touch-selection-lock-gestures.md b/.changeset/touch-selection-lock-gestures.md
new file mode 100644
index 0000000000000..d1f3ab7c2b7fc
--- /dev/null
+++ b/.changeset/touch-selection-lock-gestures.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: the gesture host applies a recognizer's own `selectionLock` to that recognizer's contact and imports no lock itself; a long press brings one only when it has a hold (a `minimumPressDuration` above 0), and `selectionLock: null` opts out. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/touch-selection-lock.md b/.changeset/touch-selection-lock.md
new file mode 100644
index 0000000000000..e5285afc1f8ad
--- /dev/null
+++ b/.changeset/touch-selection-lock.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[fix] Where the primary pointer is fine (a mouse, a trackpad, a touchscreen laptop, a desktop pen tablet), the press feedback controller no longer registers a touch listener: it reads the primary pointer at the first finger or pen press and keeps its `touchcancel` listener only while that pointer is coarse.
+
+@vjeux
diff --git a/.changeset/uikit-constants-table.md b/.changeset/uikit-constants-table.md
index 01fd41a182b0b..2c063cdb50bc7 100644
--- a/.changeset/uikit-constants-table.md
+++ b/.changeset/uikit-constants-table.md
@@ -2,6 +2,6 @@
'@astryxdesign/core': patch
---
-[chore] Internal: the UIKit constants table for touch presentations (springs, scroll physics, timing curves and navigation values), each value with its source and a checked list of the simulator measurements still owed. Not exported from the package entry point; no component reads it yet.
+[chore] Internal: the UIKit constants table for touch presentations (springs, scroll physics, timing curves and navigation values), each value with its source and a checked list of the simulator measurements still owed. Not exported from the package entry point.
@vjeux
diff --git a/.changeset/uikit-motion-animator-pseudo-element.md b/.changeset/uikit-motion-animator-pseudo-element.md
new file mode 100644
index 0000000000000..7ccb10bab35da
--- /dev/null
+++ b/.changeset/uikit-motion-animator-pseudo-element.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: the touch presentations' animator drives a pseudo-element such as a dialog's ::backdrop: a static filling animation holds its value after a curve ends and after `set`, a custom property on its host promotes it, and a Reduce Motion dip borrows and hands back the pseudo-element's own opacity. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/uikit-motion-animator.md b/.changeset/uikit-motion-animator.md
index d489e3022e6db..6b23ce1985186 100644
--- a/.changeset/uikit-motion-animator.md
+++ b/.changeset/uikit-motion-animator.md
@@ -2,6 +2,6 @@
'@astryxdesign/core': patch
---
-[chore] Internal: a compositor animator for touch presentations. It plays UIKit's springs, and scroll coasts (built by a separate coast module, so a surface that never coasts does not carry them; `decay` plays momentum, and sends a spring back from past an edge through the Reduce Motion policy: a cross-fade on a coarse pointer or after a touch release, at once elsewhere) as Web Animations on translate, rotate, scale and opacity, so they keep running while JavaScript is busy, and reads values and velocities back from the curves for interruption, retargeting and Reduce Motion cross-fades. Not exported from the package entry point; no component uses it yet.
+[chore] Internal: a compositor animator for touch presentations. It plays UIKit's springs, and scroll coasts (built by a separate coast module, so a surface that never coasts does not carry them; `decay` plays momentum, and sends a spring back from past an edge through the Reduce Motion policy: a cross-fade on a coarse pointer or after a touch release, at once elsewhere) as Web Animations on translate, rotate, scale and opacity, so they keep running while JavaScript is busy, and reads values and velocities back from the curves for interruption, retargeting and Reduce Motion cross-fades. Not exported from the package entry point.
@vjeux
diff --git a/.changeset/uikit-motion-math.md b/.changeset/uikit-motion-math.md
index 7af3d73a92a98..cbb9399568857 100644
--- a/.changeset/uikit-motion-math.md
+++ b/.changeset/uikit-motion-math.md
@@ -2,6 +2,6 @@
'@astryxdesign/core': patch
---
-[chore] Internal: the spring, decay, rubber-band and projection math of UIKit for touch presentations, as pure functions checked against an independent integration and against values read from UIKit. Not exported from the package entry point; no component uses it yet.
+[chore] Internal: the spring, decay, rubber-band and projection math of UIKit for touch presentations, as pure functions checked against an independent integration and against values read from UIKit. Not exported from the package entry point.
@vjeux
diff --git a/.changeset/uikit-motion-use-animator.md b/.changeset/uikit-motion-use-animator.md
new file mode 100644
index 0000000000000..b234a4e987752
--- /dev/null
+++ b/.changeset/uikit-motion-use-animator.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[chore] Internal: useAnimator, a React wrapper around the touch presentations' animator that never re-renders per frame, and browser proofs on the interaction harness that the animator's motion keeps running through a blocked main thread in Chromium and WebKit. Not exported from the package entry point.
+
+@vjeux
diff --git a/.changeset/uselongpress-stages.md b/.changeset/uselongpress-stages.md
new file mode 100644
index 0000000000000..a6c0bb3c92218
--- /dev/null
+++ b/.changeset/uselongpress-stages.md
@@ -0,0 +1,7 @@
+---
+'@astryxdesign/core': patch
+---
+
+[feat] useLongPress takes `stages`: later moments of the same hold, each fired once the finger has stayed that long from the start and still within `moveCancelPx`. ContextMenu claims the finger at its long press and shows its menu at a later stage. `start` also takes the press's start time, so the deadlines can count from a touch's own timestamp.
+
+@vjeux
diff --git a/apps/storybook/stories/ContextMenu.stories.tsx b/apps/storybook/stories/ContextMenu.stories.tsx
index 5c144b8bf45a9..9deadaa00db79 100644
--- a/apps/storybook/stories/ContextMenu.stories.tsx
+++ b/apps/storybook/stories/ContextMenu.stories.tsx
@@ -589,3 +589,72 @@ export const InScrollContainer: Story = {
),
};
+
+const touchStyles = stylex.create({
+ card: {
+ width: '200px',
+ height: '120px',
+ display: 'grid',
+ placeItems: 'center',
+ borderRadius: '12px',
+ backgroundColor: '#e5e7eb',
+ color: '#374151',
+ userSelect: 'none',
+ },
+});
+
+export const TouchLongPress: Story = {
+ name: 'Touch / long press',
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'On a touch screen (iOS or Android), hold a finger on the card. As on iOS, it starts to grow after 150 ms, a haptic plays at 400 ms where the device can play one, and the menu opens at 735 ms, or as soon as the finger lifts after 400 ms. A finger that lifts before 400 ms, or drifts more than 11.5 px on either axis before the menu opens, gets no menu, and the card springs back. While the menu is up the page stays still, and the card returns to its size when the menu closes.',
+ },
+ },
+ },
+ render: () => (
+ {}},
+ {label: 'Share', icon: ShareIcon, onClick: () => {}},
+ {label: 'Save to Files', icon: ArrowDownTrayIcon, onClick: () => {}},
+ {type: 'divider'},
+ {
+ label: 'Delete',
+ icon: TrashIcon,
+ variant: 'destructive',
+ onClick: () => {},
+ },
+ ]}>
+
Hold for actions
+
+ ),
+};
+
+export const TouchLongPressPopover: Story = {
+ name: 'Touch / long press, presentation="popover"',
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'With `presentation="popover"`, ContextMenu keeps today’s menu on every device: a finger held for 500 ms opens it at the finger, with no growth and no iOS timeline. A finger on a device whose primary pointer is fine (a mouse or a trackpad) gets the same long press.',
+ },
+ },
+ },
+ render: () => (
+ {}},
+ {label: 'Share', icon: ShareIcon, onClick: () => {}},
+ {label: 'Save to Files', icon: ArrowDownTrayIcon, onClick: () => {}},
+ ]}>
+