Repository navigation
feat(core): createDragToSnapPoints and useDragToSnapPoints settle an element on snap points - #8658
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
30ce9c7 to
ffa0821
Compare
|
Posted by Cindy's agent on her behalf. Thanks for the public snap-points hook. One structure ask: this PR has its own release rule ( |
|
Done in a671989: |
There was a problem hiding this comment.
This snap-point drag can't be approved yet: createDragToSnapPoints and useDragToSnapPoints rely on spec:AST-107, which isn't on main (it is proposed in #8600), and the current specs still keep the gesture and motion layer and release thresholds internal. An engineering owner needs to provide or identify the governing spec for these exports, for example by landing #8600, and record that decision here by commenting /approve-spec followed by the full head SHA; the author may do so as an engineering owner. Whatever that decision, these changes are still needed:
- Rebase once the stacked pull requests land, so this diff holds only the snap-point change (about 1,500 changed lines instead of 20,493).
- Make
useDragToSnapPointsattach to an element that mounts or is replaced after the first render, asusePanGesturedoes. - Start a re-created factory from the element's current offset, so changing
snapPoints,axisorflickThresholddoesn't make the next drag jump. - Reject an empty
snapPointsin the hook as the factory does, and add a test that fails if the settle ignores the release velocity.
[Automated review]
| useEffect(() => { | ||
| const element = ref.current; | ||
| if (element == null) { | ||
| return undefined; | ||
| } | ||
| const created = createDragToSnapPoints(element, { | ||
| axis, | ||
| snapPoints: points.split(',').map(Number), | ||
| flickThreshold, | ||
| onSettle: index => onSettleRef.current(index), | ||
| }); | ||
| snapRef.current = created; | ||
| return () => { | ||
| created.destroy(); | ||
| if (snapRef.current === created) { | ||
| snapRef.current = null; | ||
| } | ||
| }; | ||
| }, [ref, axis, points, flickThreshold]); |
There was a problem hiding this comment.
This effect only re-runs when ref, axis, points or flickThreshold change, so an element that mounts after the first commit (for example {open && <div ref={ref} />}) or a replaced node never gets the drag, and settleTo stays a no-op. usePanGesture in this stack re-checks the node on every commit; doing the same here, or taking a callback ref as useAnimator does, would handle attachment and replacement as architecture:react-component-runtime INV6 asks.
| } | ||
| const created = createDragToSnapPoints(element, { | ||
| axis, | ||
| snapPoints: points.split(',').map(Number), |
There was a problem hiding this comment.
[].join(',') is '', and ''.split(',').map(Number) is [0], so an empty snapPoints passed to the hook becomes one snap point at 0 instead of throwing as createDragToSnapPoints does.
| const animator = createMotionAnimator(element); | ||
| // The snap point the element rests on, or is settling toward. | ||
| let index = resolveSnapPoint({ | ||
| points: snapPoints, | ||
| position: 0, | ||
| velocity: 0, | ||
| flickThreshold, | ||
| }).index; |
There was a problem hiding this comment.
When the hook re-creates the factory after snapPoints, axis or flickThreshold change, this starts from offset 0 and the snap point nearest 0, but the previous animator's inline translate stays on the element, so the next drag jumps from where the element is drawn to the finger's travel from 0. Seeding the animator's initial and the starting index from the element's current offset would keep them in step.
There was a problem hiding this comment.
This snap-point drag can't be approved yet: createDragToSnapPoints and useDragToSnapPoints rely on spec:AST-107, which isn't on main (it is proposed in #8600), and the current specs still keep the gesture and motion layer and release thresholds internal. An engineering owner needs to provide or identify the governing spec for these exports, for example by landing #8600, and record that decision here by commenting /approve-spec followed by the full head SHA; the author may do so as an engineering owner. Whatever that decision, these changes are still needed:
- Rebase once the stacked pull requests land, so this diff holds only the snap-point change (roughly 1,550 changed lines instead of 20,574).
- Make
useDragToSnapPointsattach to an element that mounts or is replaced after the first render, asusePanGesturedoes. - Start a re-created factory from the element's current offset, so changing
snapPoints,axisorflickThresholddoesn't send the next drag back toward the first snap point. - Mirror the templates' snap points in right-to-left layouts, where the card now moves away from its next place and off-screen.
- Add a test that fails if the settle ignores the release velocity.
[Automated review]
There was a problem hiding this comment.
Approval waits on an engineering owner deciding which spec governs the new public createDragToSnapPoints and useDragToSnapPoints (with flickThreshold): spec:AST-107 exists only in open #8600, while current spec:AST-017 FR21, architecture:react-component-runtime INV13 and spec:AST-100 FR3 keep gesture modules and release thresholds internal. Any engineering owner, the author included if they are one, can record that decision by commenting /approve-spec 21f071817cf2ef629e0e2cfa8713f02377a0a57e on this pull request, or can point to another current record that governs these exports. Whatever the decision, these still need changes:
- The diff carries 20,168 authored lines from eleven other open pull requests, so rebase onto the landed dependencies until it holds only the snap-point drag.
- A mouse or pen drag never starts where the primary pointer is fine (
GestureHost.ts:580-583), though both doc pages and the templates' grab cursor promise one. useDragToSnapPointsnever attaches to an element that mounts after its first render, and aftersnapPointschange the next drag starts from 0 (useDragToSnapPoints.ts:67-85,createDragToSnapPoints.ts:124-129).- The templates' card moves the wrong way in right-to-left layouts (
DragToSnapPointsCard.tsx:15-24), and no test fails when the settle ignores the release velocity (spec:AST-094FR38).
[Automated review]
| }); | ||
| }; | ||
|
|
||
| const pan = createPanGesture(element, { |
There was a problem hiding this comment.
On a desktop whose primary pointer is fine, the document's gesture host installs no listener (GestureHost.ts:580-583), so no mouse or pen drag reaches this pan and only settleTo moves the element, while both doc pages describe the settle "after a mouse or pen drag" and the templates show a grab cursor. Either give the public pan a mouse and pen path on its own element under a fine pointer (the host already does this for action groups, GestureHost.ts:476-487), or document that the drag needs a coarse primary pointer and drop the grab cursor.
|
|
||
| const PLACES = ['Hand', 'Table', 'Discard pile']; | ||
| /** Where each place sits, in px from the card's resting position. */ | ||
| const SNAP_POINTS = [0, 110, 220]; |
There was a problem hiding this comment.
These offsets are physical, but the card and the place buttons sit in rows that mirror: in a right-to-left layout Table is to the card's left, a drag toward it settles back on Hand, and Table's button slides the card 110 px right, past the inline-start edge (design:bidirectionality DR13). Mirror the offsets when the layout is right-to-left, mapping onSettle's sorted index back to its place; DragToSnapPointsCardVanilla.tsx needs the same change.
| snap.destroy(); | ||
| }); | ||
|
|
||
| it('a quick flick carries to the next snap point', async () => { |
There was a problem hiding this comment.
This flick test asserts only onSettle, so it still passes when the settle drops the release velocity (createDragToSnapPoints.ts:147); spec:AST-094 FR38's verification row needs a test that fails then. Assert the recorded settle's opening keyframes, or the animator's velocity right after the release, against the release velocity.
PR Analysis Report🔎 Preview ·
|
| Component | Size | LoC |
|---|---|---|
| DateInput | 167.0 kB (+18 B) · 16.1 kB CSS (0) | — |
| PowerSearch | 263.9 kB (+17 B) · 22.9 kB CSS (0) | — |
| richtext/RichTextView | 270.9 kB (+5 B) · 2.3 kB CSS (0) | — |
21 more components
| Component | Size | LoC |
|---|---|---|
| lab/NavigationScreen | 75.0 kB (+2 B) · 1.1 kB CSS (0) | — |
| charts/ChartTooltip | 151.0 kB (+1 B) · 423 B CSS (0) | — |
| lab/NavigationStack | 75.0 kB (+1 B) · 1.1 kB CSS (0) | — |
| Calendar | 62.4 kB (0) · 9.9 kB CSS (0) | — |
| DateRangeInput | 81.3 kB (0) · 11.8 kB CSS (0) | — |
| DateTimeInput | 193.3 kB (0) · 18.0 kB CSS (0) | — |
| lab/ChatEmojiPicker | 74.6 kB (0) · 773 B CSS (0) | — |
| lab/ChatReactionBar | 75.1 kB (0) · 1.1 kB CSS (0) | — |
| lab/ChatTypingIndicator | 25.1 kB (0) · 612 B CSS (0) | — |
| lab/CommentComposer | 115.3 kB (0) · 1.9 kB CSS (0) | — |
| lab/CommentPin | 52.6 kB (0) · 1.2 kB CSS (0) | — |
| lab/DiffViewer | 77.9 kB (0) · 2.1 kB CSS (0) | — |
| lab/EmojiPicker | 77.5 kB (0) · 751 B CSS (0) | — |
| lab/ListInput | 90.9 kB (0) · 1.6 kB CSS (0) | — |
| lab/ThreeDChart | 5.0 kB (0) · 20 B CSS (0) | — |
| lab/TransferList | 85.3 kB (0) · 1.7 kB CSS (0) | — |
| lab/TransferListSelector | 92.3 kB (0) · 1.9 kB CSS (0) | — |
| richtext/RichTextEditor | 290.6 kB (0) · 2.6 kB CSS (0) | — |
| charts/Chart | 173.6 kB (−9 B) · 494 B CSS (0) | — |
| lab/Comments | 281.6 kB (−41 B) · 2.5 kB CSS (0) | — |
| lab/CommentThread | 275.8 kB (−49 B) · 2.4 kB CSS (0) | — |
Package size:
- @astryxdesign/cli 3.39 MB packed (+2.3 kB); no new dependencies
- @astryxdesign/core 6.69 MB packed (+9.7 kB); no new dependencies
API changes: core +9 · ~0 · −0
Committed API report behind this build: cli 19 rows, core 21 rows; run pnpm api:report.
9 API changes (advisory)
| Package | Where | Signature | Usage | |
|---|---|---|---|---|
| + | core |
. · Hooks · hook useDragToSnapPoints |
hook useDragToSnapPoints: (ref: RefObject<HTMLElement | null>, options: DragToSnapPointsOptions) => UseDragToSnapPointsResult |
import {useDragToSnapPoints} from '@astryxdesign/core'; useDragToSnapPoints(…) |
| + | core |
. · Functions · function createDragToSnapPoints |
function createDragToSnapPoints: (element: HTMLElement, options: DragToSnapPointsOptions) => DragToSnapPoints |
import {createDragToSnapPoints} from '@astryxdesign/core'; createDragToSnapPoints(…) |
| + | core |
. · Types · interface DragToSnapPoints |
interface DragToSnapPoints |
import type {DragToSnapPoints} from '@astryxdesign/core' |
| + | core |
. · Types · interface DragToSnapPointsOptions |
interface DragToSnapPointsOptions |
import type {DragToSnapPointsOptions} from '@astryxdesign/core' |
| + | core |
. · Types · interface UseDragToSnapPointsResult |
interface UseDragToSnapPointsResult |
import type {UseDragToSnapPointsResult} from '@astryxdesign/core' |
| + | core |
./hooks · Also exported from an earlier entry point · DragToSnapPoints |
DragToSnapPoints (.) |
import {DragToSnapPoints} from '@astryxdesign/core/hooks' |
| + | core |
./hooks · Also exported from an earlier entry point · DragToSnapPointsOptions |
DragToSnapPointsOptions (.) |
import {DragToSnapPointsOptions} from '@astryxdesign/core/hooks' |
| + | core |
./hooks · Also exported from an earlier entry point · UseDragToSnapPointsResult |
UseDragToSnapPointsResult (.) |
import {UseDragToSnapPointsResult} from '@astryxdesign/core/hooks' |
| + | core |
./hooks · Also exported from an earlier entry point · useDragToSnapPoints |
useDragToSnapPoints (.) |
import {useDragToSnapPoints} from '@astryxdesign/core/hooks' |
Not run on this PR: add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.
Generated by PR Enrichment workflow | View full report
There was a problem hiding this comment.
Approval waits on an engineering owner deciding the architecture:theme-authoring-contract INV15 that this branch carries from #8661: its clause that "an explicit token anywhere in the lineage still wins" over a child theme's tempo and character is not stated by spec:AST-108 FR5 and changes the record's own base-then-generated precedence. Any engineering owner, the author included if they are one, can record that decision by commenting /approve-spec d504154424d4488fde7f96499f099505c5a2ab66 on this pull request or ask for the clause to change, and rebasing the theme work out of this branch also removes it. Whatever the decision, these still need changes:
- A mouse or pen drag never starts where the primary pointer is fine (
GestureHost.ts:580-583), thoughspec:AST-107FR1 and FR2 require every pointer and both doc pages and templates promise one. - The diff carries 22,498 authored lines from thirteen other open pull requests, so rebase onto the landed dependencies until it holds only the snap-point drag.
useDragToSnapPointsnever attaches to an element that mounts after its first render, and after an option change the next drag starts from 0 (useDragToSnapPoints.ts:67-85,createDragToSnapPoints.ts:122-129).- The templates' card moves the wrong way in right-to-left layouts (
DragToSnapPointsCard.tsx:15), and no test fails when the settle ignores the release velocity (spec:AST-094FR38).
[Automated review]
| are the defaults. A duration scale's durations win over `tempo`, and | ||
| explicit tokens win over both. An extending theme inherits motion field by | ||
| field: a duration-scale field or knob the child leaves unset keeps its | ||
| base's value, and an explicit token anywhere in the lineage still wins. Each duration's |
There was a problem hiding this comment.
This clause makes a base theme's explicit motion token outrank a child theme's tempo and character. spec:AST-108 FR5 says explicit tokens win over the knobs, but not that a base theme's do, and this record's own order (resolved base, then generated values, then explicit tokens) lets a child's generated durations replace its base's tokens. Either an engineering owner approves this as the reading, or the clause follows the existing order.
…test Main's lint asks every browser spec to import test from @astryxdesign/a11y-spec/playwright, whose contexts stay off remote hosts.
There was a problem hiding this comment.
Changes are needed before this can merge, though the snap-point drag itself follows spec:AST-107 FR2, FR5 and FR6, and its unit and browser tests pass on this head.
- Regression: the
Stack: #8655 → #8658, #9928line no longer holds because #9928's head does not contain #8655's current head, so this branch is judged on its own and carries #8655's 1,799-line public pan beside the drag (4,232 authored lines), which the behavior-module declaration does not cover; bring #9928 onto #8655's current head (or list only the pull requests that contain the one below them), or land #8655 first.
Non-blocking: the plain-JavaScript example's place buttons render unstyled under the Astryx reset, both hooks write their latest callback into a ref during render, and the description's notes on the stack (no merge commits, #8655's head) are out of date.
[Automated review]
| const {axis, snapPoints, flickThreshold} = options; | ||
| const onSettleRef = useRef(options.onSettle); | ||
| // The drag calls the latest onSettle. | ||
| onSettleRef.current = options.onSettle; |
There was a problem hiding this comment.
Non-blocking: this writes the latest onSettle into a ref during render. A render React discards can then leave a callback from a render that never committed. Writing it in a layout Effect keeps it to committed renders.
There was a problem hiding this comment.
Fixed in a42cd3f: the hook writes onSettle into the ref in a layout Effect, so a render React throws away leaves the committed one in place, and a new test shows it. Core has no shared latest-ref helper, and useGestureRecognizer writes its listener's ref the same way.
There was a problem hiding this comment.
Changes are needed before this can merge, though the snap-point drag itself follows spec:AST-107 FR2, FR5 and FR6 and needs only a rename.
- Regression:
packages/core/src/dragToSnapPoints/createDragToSnapPoints.ts(line 24) still importscreateMotionAnimatorfrom../motion/animator, whichmainhas since renamed tocreateMotionEngine, so at this head Core's type check and build fail and everycreateDragToSnapPointscall throwsTypeError: createMotionAnimator is not a function(33 of the drag's 43 unit tests fail); rename the import and the call at line 135, and the factory test's mock at lines 36-39, tocreateMotionEngine, after which all 43 tests and the type check pass.
Non-blocking: the plain-JavaScript example's place buttons render unstyled under the Astryx reset, and useDragToSnapPoints writes its latest onSettle into a ref during render, as usePanGesture does on main.
[Automated review]
| createPanGesture, | ||
| type PanGestureEvent, | ||
| } from '../gestures/createPanGesture'; | ||
| import {createMotionAnimator} from '../motion/animator'; |
There was a problem hiding this comment.
Regression: main has renamed this export to createMotionEngine, so this import no longer resolves. Core's type check fails here (TS2305), the core build stops on it, and the call at line 135 throws TypeError: createMotionAnimator is not a function for every caller. Rename it here and at line 135, and in the factory test's mock (createDragToSnapPoints.test.ts lines 36-39).
There was a problem hiding this comment.
Fixed in e33f7e7: the import, the call and the factory test's mock now use createMotionEngine.
useDragToSnapPoints wrote the latest onSettle into a ref during render, so a render that React throws away could leave its callback behind. It now writes it in a layout effect, as the other hooks do. The plain-JavaScript example's place buttons rendered unstyled under the Astryx reset. They now take the theme's secondary button look from its tokens.
Main renamed createMotionAnimator to createMotionEngine.
3f70145 to
e33f7e7
Compare
There was a problem hiding this comment.
Recommending approval. Approved pending CI: CI has not run on this draft head yet. Once the pull request is marked ready for review, a maintainer adds the ci:run label so CI runs on this fork branch.
createDragToSnapPoints and useDragToSnapPoints match the public gesture-hooks spec (spec:AST-107 FR2, FR5 and FR6). The createMotionEngine rename fixes the build break, the hook now hands the drag only a committed onSettle, and the plain-JavaScript example's buttons take the secondary button look; the new tests for both changes fail on the previous head and pass here.
[Automated review]
…ntries plus this PR's four, in the generator's path order)
There was a problem hiding this comment.
Recommending approval. Approved pending CI: CI has not run on this draft head. Once the pull request is marked ready for review, a maintainer adds the ci:run label so CI runs.
createDragToSnapPoints and useDragToSnapPoints match spec:AST-107 FR2, FR5 and FR6, and their unit tests, template tests and Chromium browser spec pass on this head.
One optional suggestion:
- Regression:
mainnow listscreatePanGesture,usePanGesture,createAnimatoranduseAnimatorunder the "Motion & gestures" hook category, while both new docs still saycategory: 'interaction'(createDragToSnapPoints.doc.mjs:112,useDragToSnapPoints.doc.mjs:101);'motion & gestures'would list the drag beside the hooks it builds on.
[Automated review]
| relatedComponents: ['BottomSheet', 'Carousel'], | ||
| relatedHooks: ['useDragToSnapPoints', 'createPanGesture', 'createAnimator'], | ||
| importPath: '@astryxdesign/core', | ||
| category: 'interaction', |
There was a problem hiding this comment.
Optional (Regression): main now files createPanGesture and createAnimator under category: 'motion & gestures'. Using the same category here keeps the drag listed beside the pan and animator it builds on.
category: 'motion & gestures',| relatedComponents: ['BottomSheet', 'Carousel'], | ||
| relatedHooks: ['usePanGesture', 'useAnimator'], | ||
| importPath: '@astryxdesign/core/hooks', | ||
| category: 'interaction', |
There was a problem hiding this comment.
Optional (Regression): usePanGesture and useAnimator are now under category: 'motion & gestures' on main, so astryx hook --list shows this hook apart from them under "Interaction".
category: 'motion & gestures',There was a problem hiding this comment.
Recommending approval. Approved pending CI: on this head CI's build and test jobs stopped with "CI is full" before running, so a maintainer needs to re-run all jobs before merge.
createDragToSnapPoints and useDragToSnapPoints match spec:AST-107 FR2, FR5 and FR6, and their unit tests, template tests and Chromium browser spec pass on this head.
One optional suggestion:
- Regression:
mainlistscreatePanGesture,usePanGesture,createAnimatoranduseAnimatorunder the "Motion & gestures" hook category, while both new docs still saycategory: 'interaction'(createDragToSnapPoints.doc.mjs:112,useDragToSnapPoints.doc.mjs:101);'motion & gestures'would list the drag beside the hooks it builds on.
[Automated review]
There was a problem hiding this comment.
Recommending approval. Approved pending CI: on this head CI's build and test jobs stopped with "CI is full" before running, so all jobs need a re-run.
createDragToSnapPoints and useDragToSnapPoints follow spec:AST-107 (FR2, FR5, FR6): the release settles on the nearest snap point, carries one further past flickThreshold, and returns to its start when the release moves back; settleTo gives keyboard users the same positions. The drag's unit, hook and template tests and its browser spec pass on this head with main's latest motion changes merged in.
- Optional: both new docs use
category: 'interaction'(createDragToSnapPoints.doc.mjs:112,useDragToSnapPoints.doc.mjs:101), while the pan and animator they build on are now under'motion & gestures'; matching them keeps the three listed together.
[Automated review]
There was a problem hiding this comment.
Recommending approval.
createDragToSnapPoints and useDragToSnapPoints follow spec:AST-107 (FR2, FR5, FR6): the release settles on the nearest snap point, carries one further past flickThreshold, and returns to its start when the release moves back; settleTo gives keyboard users the same positions. The drag's unit, hook and template tests and its browser spec pass on this head with main's latest changes merged in, and CI is green.
- Optional: both new docs use
category: 'interaction'(createDragToSnapPoints.doc.mjs:112,useDragToSnapPoints.doc.mjs:101), while the pan and animator they build on are under'motion & gestures'; matching them keeps the three listed together.
[Automated review]
#8655 has merged.
The merge worker merged
mainin after #8655's squash (vjeux@f127a5587c), so the diff againstmainis only this pull request's own commits: the snap drag (vjeux@83c69ca2f2), the shared Playwright test import (vjeux@a00b8a8240), a committedonSettleand styled example buttons (vjeux@a42cd3f3b2), and the motion engine's new name (vjeux@e33f7e7a1d).Why
A product's drawer, card or panel that rests on a few positions wants iOS's release behavior: the nearest position after a slow release, the next one after a flick, and back where it started when the finger moves back. Without a public API, each product rebuilds this from raw pointer events.
What led to it
This comes from the work to make Astryx's touch components behave as native iOS ones do.
spec:AST-107FR2 asks for it over the public pan (#8655) and animator (#8653), with a thin hook (FR6) and Core's release policy (resolveSnapPoint), so drawers and BottomSheet release alike.Options and tradeoffs
snapPointsare, or pixel offsets from the resting position: fractions suit sheets, offsets suit any element.Why this choice
The spec picks pixel offsets and a public
flickThreshold(default 1008 px/s, UIKit's measured switch). New options create the drag again where the element is, which stops a settle in flight there: the hook adds no behavior of its own (FR6).settleTo(index)gives keyboard users the same positions.What changes
createDragToSnapPoints(element, {axis, snapPoints, flickThreshold?, onSettle})from the package root only, anduseDragToSnapPoints(ref, options)from@astryxdesign/core/hooks. The hook keeps one drag for the element behind its ref through Core's shareduseAttachmenthook (hooks/useAttachment), keyed on the axis, the snap points and the flick speed, so a node mounted or replaced later is driven too and new options create the drag again.spec:AST-094FR43). The settle playssprings.settlefrom the release velocity (FR38); under Reduce Motion the release cross-fades after a touch drag or on a coarse primary pointer, and lands at once elsewhere.destroyremoves every listener.Behavior module:
createDragToSnapPointswith its hookuseDragToSnapPoints.Pull request size. This pull request's own commit is 2,453 authored lines with tests, and no part of it can stack apart.
spec:AST-107FR5 reads "Each hook ships with docs and a playing-cards example", so the two examples, their test and their registrations (390 lines) ship with the hook. The rest is the factory and the hook with their docs, changeset and API report rows (664 lines), and the tests that cover them (1,410 lines), which cannot land apart from the code they test.How to check it
useDragToSnapPointsexample (a card on Hand, Table and Discard pile), drag past halfway toward Table and release: it settles there. Flick: it carries one place further. Drag toward Table and back: it returns to Hand.Evidence
flickThresholdthrough the factory), fine pointer, hook (late element, options mid-use, and its imports:createDragToSnapPointsand./useAttachment, nothing frommotion/orgestures/), examples (right to left), entry.settleTo. The browser spec now reads the settles once the panel is at rest, not after a fixed time, so a second settle would show: with every animation at a quarter speed, the old 1,200 ms wait fails 8 of 8 and the new read passes 12 of 12 (not-for-landing arms fceac0b and 2bc739b).main8c0e95b: build, docsite generate (route lock unchanged), type check,CI=truelint and Prettier pass; the drag, hook and gesture unit tests pass (868), and so do the doc inputs and touch bundle tests (18); the snap spec's 4 tests pass in Chromium and in the WebKit container; changeset coverage passes.useDragToSnapPointswrites its latestonSettlein a layout effect, and a new test shows a render React throws away leaves the committed one in place (red before the fix). Core has no shared latest-ref helper.useGestureRecognizer,PickerWheelandMarkdowneach write their listener's ref in a layout Effect, and this follows that pattern.review:preflags such an Effect as a copy into a ref (INV3); here the copy at commit is the fix.useEffectEventneeds React 19.2, and Core supports 19.0. The plain-JavaScript example's place buttons take the secondary button's tokens, and a new test checks them (red before the change).main859df5f): install, build, docsite generate (route lock unchanged), Storybook, type check,CI=truelint,test:ui(19,744), the node suites (16,263), the docsite tests, knowledge, changesets and coverage pass. The snap spec passes 10 of 10 in Chromium and in the CI WebKit container, and interaction passes 541 in Chromium and 480 in WebKit (at d6efeb3, which differs only in the hook's ref write and two comments and test lines).api:report --checkasks only for rowsmainlacks (--duration-exit, CheckboxListisOptional, CodeBlockstartLineNumber), in the same 8 reports as at feat(core): createPanGesture and usePanGesture report a drag as values (spec:AST-107 FR1, FR6) #8655's head.Scope