Skip to content

spec(AST-107): public gesture hooks — usePanGesture, useDragToSnapPoints, useAnimator - #8600

Merged
cixzhang merged 25 commits into
mainfrom
spec/public-gesture-hooks
Oct 27, 2026
Merged

cixzhang merged 25 commits into
mainfrom
spec/public-gesture-hooks

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Decided (Cindy, 2026-10-26): products get a public hook layer for gestures and motion ("option B"), with docs and a playing-cards example built on the hooks.

User impact

Product teams can build drags, snapping, and single-element motion that feel like Core's, with keyboard paths and Reduce Motion handled, without copying Core's internals. Core keeps its gesture machinery internal, so it can evolve without breaking products.

Record: spec:AST-107 public gesture hooks

  • FR1 usePanGesture(ref, {axis?, onPan}): onPan({state, translation, velocity}) with drag values only, never a recognizer object, and no press or long press.

  • FR2 useDragToSnapPoints(ref, {axis, snapPoints, flickThreshold?, onSettle}) → {settleTo(index)}: snapPoints are pixel offsets along axis, not BottomSheet's viewport fractions. The element settles on the nearest snap point; a release past flickThreshold (px/s, default 1008, the sheet's measured flick speed) carries to the next, and a release moving back returns to the start (spec:AST-094 FR37). settleTo is the keyboard path.

  • FR3 useAnimator() → {ref, animator}: animator is a public AnimatorHandle over x, y, scale, rotate, and opacity, with animate, decay, set, get, getVelocity, and stop signatures. It's distinct from the internal animator module. Reduce Motion is planned. animate plays springs.stateChange (the role modeled on the platform's default animation), which follows the theme's tempo and character (spec:AST-108 FR3); decay is the fixed momentum decay (AST-108 FR4). It merges after spec(AST-108): theme motion is three knobs — tempo, character, reducedMotion #8613 and spec(AST-103): optional positions() for grid and fan drops, and attach() for plain elements #8601.

  • FR4 one hook per job (architecture:public-component-api INV21):

    • usePanGesture does no presses; useLongPress owns those.
    • useAnimator animates one element; useAnimatedLayout (spec:AST-105) animates an area.
    • BottomSheet's snapPoints and Carousel's hasSnap stay the component path.
    • Everything else stays internal.
  • FR5 done criterion:

    • Each public API, including AST-103's positions (spec(AST-103): optional positions() for grid and fan drops, and attach() for plain elements #8601), ships with docsite docs: usage, options, the keyboard and AT path, Reduce Motion, a "use the component prop instead when…" note, and a live example.
    • The flagship example is a playing-cards demo built only on the public hooks: flick and snap back, drag-to-reorder plus keyboard reorder mode, swipe to discard, and a fanned hand with custom positions.
  • DEC-1 records the decision.

  • FR6 (Cindy 20:38): non-React factories createPanGesture(element, {axis?, onPan}) → {destroy}, createDragToSnapPoints(element, {…}) → {settleTo, destroy}, and createAnimator(element) → {animate, decay, set, get, getVelocity, stop, destroy}. Each hook is a thin wrapper that delegates entirely to its factory (INV21: one job, two layers). destroy removes every listener, and importing does no DOM work, so it's SSR-safe.

  • Name clash: Core's internal createAnimator either becomes the public factory (if its shape matches) or is renamed. The public name stays createAnimator, and no internal factory shares a public name.

  • FR7 (Cindy 20:39), dogfood: Core and Lab use the public layer wherever it covers the job (factories and hooks, createReorderable, useAnimatedLayout). Internals are allowed only for listed public-API gaps: recognizer relationships, the presentation hold, the input hold, press feedback, lift-from-trigger, and action-group tracking.

  • FR5's docs cover factories and hooks, and the playing-cards demo ships in plain JS (factories) and in React (hooks).

  • architecture:react-component-runtime INV13 (Cindy 20:39): the recognizer family (base, arena, host) is the one internal place using classes, with a kind field instead of instanceof. Everything else is create… factories.

  • DEC-2 records the 20:38/20:39 decisions.

  • FR8 (Cindy 20:46): a public headless createContextMenu(element, {items | render, onOpenChange?, presentation?}) → {open(at?), close(), destroy()}, which opens on the context request (long press, right click, Shift+F10), plus a thin useContextMenu(ref, opts) → {open, close}. <ContextMenu> is built on it (INV21, dogfood). DEC-3.

Implementation PRs follow once this record lands; Changesets follow the release policy (public docsite).

Round-2 record fixes

Writing rubric results

  • R1 present tense: pass. R2 research: none in the record.
  • R3 verification: a row for each of FR1–FR5.
  • R4 rejected alternatives: one line in DEC-1.
  • R5 change references: none in the record. R6 open questions: none.

@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 27, 2026 8:33pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 27, 2026, 9:04 PM UTC

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec fed2e1c

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Previews ready: Storybook and Sandbox were built with this PR's docsite. CI evidence will be added when available.

View Storybook for this PR · View Sandbox for this PR

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 26, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: the direction in spec:AST-107 is approved, but four places in the record need fixing before it can govern an implementation.

  • FR2 names the internal function resolveSnapPoint, which a requirement may not prescribe and which does not exist on main; state the release rule as behavior and give flickThreshold's default and unit.
  • FR3's "default motion role" matches no defined motion role; name the role it means.
  • snapPoints reuses BottomSheet's option name, whose numbers are viewport fractions, without stating its own unit.
  • FR3 returns an animator that the FR4 verification row says must fail as an exported animator object; give the public object its own name and signatures.

The positions docs in FR5 need #8601 to land first, and after these edits the new head needs a fresh /approve-spec.

[Automated review]


const {settleTo} = useDragToSnapPoints(ref, {
axis: 'y',
snapPoints: [0, 240, 480],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snapPoints shares BottomSheet's public option name, where a number is a viewport fraction in (0, 1], so 0, 240 and 480 would all be ignored there. Could FR2 state this option's unit and origin, or use a different name?

- **FR2 — `useDragToSnapPoints` settles an element on snap points.**
`useDragToSnapPoints(ref, {axis, snapPoints, flickThreshold?, onSettle})`
lets a drag move the element along `axis`, and at the release settles it on
the snap point the release resolves to (`resolveSnapPoint`): a release faster

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveSnapPoint is an internal function, and architecture:knowledge-contracts INV18 keeps internal function names out of requirements unless they are public contracts (the Non-goals keep the resolver internal). It is also not on main yet. Suggest stating the rule as behavior, citing spec:AST-094 FR37 for the moving-back cancel, and giving flickThreshold's default and unit.

`{ref, animator}`, where `animator` has `animate`, `decay`, `set`, `get`,
`getVelocity`, and `stop` for the one element `ref` attaches to. Reduce
Motion is planned for every animation (`design:reduced-motion` DR1–DR3).
Every animation plays on the default motion role; choosing a spring or curve

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No current record or code defines a "default motion role": spec:AST-094 FR4 names each role for its use, and none is a default. Which role should useAnimator play? The FR3 verification row depends on the answer.

| FR1 | Unit and browser tests of `usePanGesture` | a drag on each axis, a cancel, a press, a long press | a recognizer in the result, a missing state, a drag off its axis, or a press or long press reported as a pan fails |
| FR2 | Unit and browser tests of `useDragToSnapPoints` | a slow release, a flick past `flickThreshold`, a reversed drag, `settleTo` from a key | a settle on the wrong snap point, a reversed drag that settles elsewhere, or a missing `onSettle` fails |
| FR3 | Unit and browser tests of `useAnimator` | each method, Reduce Motion, an unattached ref | a missing method, a spring other than the default role, or motion that ignores Reduce Motion fails |
| FR4 | Export check of `@astryxdesign/core` and its hooks | the public exports | a recognizer, arena, animator object, or resolver reachable from a public export fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under architecture:public-component-api INV12, the animator that useAnimator() returns is reachable from a public export, so this row fails FR3 as written. Suggest giving the public object its own name, with signatures for decay, set, get, getVelocity and stop, and rewording this row and the Non-goal to mean the internal animator module.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 92becf1

…speed; animate follows the theme's motion knobs
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 1d9b484

@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

A naming question on FR3, facts first. The experimental touch layer in #8076 already has an internal hook named useAnimator (packages/core/src/motion/useAnimator.ts): useAnimator(options?) returns {ref, animator} with the full internal Animator. Open Lab pull requests import it in 44 places, and under architecture:react-component-runtime INV13 it would reach Lab through internal/motion. This spec's public useAnimator() in @astryxdesign/core/hooks has the same name and the same {ref, animator} shape, with a narrower AnimatorHandle: Promise-returning animate and decay, a uniform scale, get() of every channel, and stop(). Two hooks named useAnimator at two paths, returning different handles, would be easy to confuse. One way out: the public hook wraps the internal one and narrows its handle, and the internal hook takes a distinct name (for example useMotionAnimator) before it is exported through internal/motion. Which name should the internal hook have, or should it keep useAnimator?

@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

A question on FR1 and FR4 before code. AST-107 gives a product a pan as values, but no way for that pan to order itself against a component's own recognizers. Two cases it leaves open:

  • A horizontal pan a product puts inside a BottomSheet wins over the sheet's drag, because the innermost recognizer wins (spec:AST-094 FR15), and the product cannot make it wait for the sheet.
  • A product cannot stop a component's gesture, such as a navigation back swipe while a form has unsaved changes.

UIKit covers both with require(toFail:) and a delegate's gestureRecognizerShouldBegin. Should usePanGesture take a relationship option, for example one naming a component gesture to wait for, plus a shouldBegin callback, with a matching way to gate a component's own gesture? Core already has an internal mechanism that could sit behind such an option: component recognizers published by name, with outside delegates the arena consults (#8261). It stays internal under FR4 either way.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: the direction in spec:AST-107 is approved for this head, but several claims rest on records that are not on main yet or conflict with current ones. Rebase once #8613 and #8601 land, or drop the clauses that depend on them, resolve the flickThreshold conflict, and then have an engineering owner re-approve the new head with /approve-spec.

  • FR3's theme-following springs and fixed decay cite spec:AST-108, which is not on main, while spec:AST-094 FR4 and DEC-11 still say a theme cannot change a motion role.
  • FR2's public flickThreshold conflicts with spec:AST-100 FR3 and spec:AST-094 FR4, which keep gesture thresholds internal, so either drop it or amend those records with the spec:AST-002 FR5 admission argument.
  • The default "1008, the sheet's measured flick speed" has no source on main, and FR5 cites spec:AST-103 for a positions option that record does not define yet.
  • The FR3 verification row tests an unattached ref, but FR3 never says what happens then.

[Automated review]

Comment on lines +101 to +103
`springs.stateChange` motion role (`spec:AST-094` FR4), which follows the
theme's tempo and character (`spec:AST-108` FR3), and `decay` on the fixed
momentum decay (`spec:AST-108` FR4); choosing any other spring or curve is

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-108 is not on main yet, and current spec:AST-094 FR4 says "The roles are internal until the public motion-token set is decided, so a theme cannot override them yet (DEC-11)". Could this land after #8613, which adds AST-094 DEC-15, or defer spring choice to the public motion-token set as the earlier wording did?

Comment on lines +85 to +86
except that a release faster than `flickThreshold`, in pixels per second
(default 1008, the sheet's measured flick speed), carries to the next snap point in its direction, and a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-100 FR3 keeps flick thresholds as "per-component tuning that stays internal (spec:AST-002 FR3)", and spec:AST-094 FR4 says gesture thresholds "are not public concepts". Either drop the option and use the measured flick speed internally, or amend those records here with the AST-002 FR5 cases. Separately, main has no sheet flick speed of 1008: the sheet projects the throw with UIKit's 0.998 deceleration rate. The default therefore needs a source, as AST-094 FR2 requires.

motion modules stays internal (`architecture:public-component-api` INV21).

- **FR5 — Each hook ships with docs and a playing-cards example.** Each public
API here, and `createReorderable`'s `positions` (`spec:AST-103`), ships with

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-103 FR1 on main lists only onReorder and isReorderable as createReorderable options, and positions arrives with #8601. Land that first, or drop this clause and the fanned hand until it does.

| -------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| FR1 | Unit and browser tests of `usePanGesture` | a drag on each axis, a cancel, a press, a long press | a recognizer in the result, a missing state, a drag off its axis, or a press or long press reported as a pan fails |
| FR2 | Unit and browser tests of `useDragToSnapPoints` | a slow release, a flick past `flickThreshold` and the default flick speed, a release moving back, `settleTo` from a key | a settle on the wrong snap point, a reversed drag that settles elsewhere, or a missing `onSettle` fails |
| FR3 | Unit and browser tests of `useAnimator` | each method, Reduce Motion, an unattached ref | a missing method or channel, an `animate` off `springs.stateChange`, or motion that ignores Reduce Motion fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FR3 does not say what an unattached ref does, or what an interrupted animate promise does, so this row can neither pass nor fail. spec:AST-002 DEC-8 asks a public primitive's contract to name its failure behavior.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 1f22c68

@github-actions github-actions Bot removed the spec-auto-merge Auto-merge was enabled by the spec owner gate label Oct 27, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec ab539aa

@github-actions

Copy link
Copy Markdown
Contributor

That /approve-spec comment did not change spec-owner-approval: it named 1f22c68, which is not the current head.

The gate only accepts the exact current head, so post this instead:

/approve-spec ab539aac7d5db94dd5ca625b64871c3a1673c6fb

A new commit invalidates the command, so repeat it after any push.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 687ec4c

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: an engineering owner approved the direction of spec:AST-107 for this revision, but three clauses still rest on records main does not have yet. Please land #8613, #8646 and #8601 first and rebase (or drop the clauses that depend on them), make the small record edits below, and have an engineering owner re-approve the new revision with /approve-spec.

  • FR3's theme-following spring cites spec:AST-108, while spec:AST-094 FR4 on main still says a theme cannot override a motion role (DEC-11).
  • FR1 and FR2's mouse-and-pen scope cites AST-094 FR43, which main lacks, while the INV13 text this pull request keeps attaches no recognizer under a fine primary pointer.
  • FR5 and FR7 cite createReorderable's positions and attach path, which spec:AST-103 does not define yet.
  • The Public contract should name entry points for createContextMenu and useContextMenu, FR2 should say which order a sorted snapPoints index counts in, and FR8 should state focus and arrow-key behavior in the factory's own menu.

[Automated review]

createPanGesture,
createDragToSnapPoints,
createAnimator,
} from '@astryxdesign/core';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Public contract has no entry point for createContextMenu and useContextMenu (FR8, lines 178–190). Add them here so FR4's export check knows where they belong.

`usePanGesture(ref, {axis?, onPan})` calls `onPan` with
`{state, translation, velocity}` as a drag on the element begins, changes,
ends, or is cancelled. `axis` limits the drag to `'x'` or `'y'`. A finger,
a mouse, and a pen all drive it (`spec:AST-094` FR43). It never

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AST-094 on main ends at FR41 (#8646 proposes FR43), and the INV13 text this pull request keeps says "Where the primary pointer is fine, no recognizer attaches and no touch listener is registered". Land #8646 first and rebase, or scope FR1 and FR2 to what INV13 and AST-094 FR19 allow today and drop the FR43 citation.

to different snap points, which no motion role or
theme knob expresses (`spec:AST-002` FR5); Core components keep theirs
internal (`spec:AST-100` FR3). Under reduced motion the element goes to its snap point
without travel (`design:reduced-motion` DR1–DR3). `snapPoints` is sorted before use; an empty list,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorting before use means an unsorted list's indexes differ from the caller's. Please say whether onSettle(index) and settleTo(index) count in the given order or the sorted one.

`stop()` halts them where they are. Reduce Motion is planned for every
animation (`design:reduced-motion` DR1–DR3). Every `animate` plays on the
`springs.stateChange` motion role (`spec:AST-094` FR4), which follows the
theme's tempo and character (`spec:AST-108` FR3), and every `decay` plays on

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-108 is not on main yet (#8613 is open), and spec:AST-094 FR4 there still says "so a theme cannot override them yet (DEC-11)". Land #8613 first and rebase, or have animate play springs.stateChange as AST-094 FR4 defines it, cite AST-094 FR3's momentum coast for decay, and drop the AST-108 citations until then.

use it rather than the internals beneath it. A component reaches internals
only for a job the public layer does not expose; those jobs are listed as
known public-API gaps in `architecture:react-component-runtime` INV13.
`createReorderable`'s plain-element attach path (`spec:AST-103`) covers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-103 FR1 on main gives createReorderable only onReorder and isReorderable, applied through a collection's reorderable prop; the plain-element attach path (and FR5's positions, line 192) arrive with #8601. Land it first, or drop both clauses until then.

- **FR8 — A headless context menu.** `createContextMenu(element, {items | render, label?, isDisabled?,
onOpenChange?, presentation?}) → {open(at?), close(), destroy()}`
attaches a context menu to any element: it opens on the element's context
request (a long press, a right click, or Shift+F10), at `at` when given; `render(container)`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FR8 says how the menu opens and that label names it, but not where focus starts, how arrow keys move through the factory's own items rows, or where focus returns; component:ContextMenu, which the options defer to, is draft. Please state it, or cite the current record that defines it.

| FR3 | Unit and browser tests of `useAnimator` | each method, Reduce Motion, an unattached ref, an unknown channel, a call after `destroy` | a missing method or channel, an `animate` off `springs.stateChange`, or motion that ignores Reduce Motion or a throw on an unknown channel or after `destroy` fails |
| FR4 | Export check of `@astryxdesign/core` and its hooks | the public exports | a recognizer, the arena, the internal animator module, or the resolver reachable from a public export fails |
| FR6 | Unit tests of each factory and its hook, server-render and import checks, and listener counts after `destroy` | each factory without React; each hook; a server import; `destroy` mid-drag | a hook with behavior its factory lacks, a listener left after `destroy`, or DOM work at import fails |
| FR7 | Source boundary check over Core and Lab against the gap table | every Core and Lab component that drags, snaps, animates one element, reorders, or animates layout | a component reaching internals for a job the public layer covers, or an internal use whose job is not in the gap table, fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV13 lists the known public-API gaps as prose (react-component-runtime.md, INV13); there is no gap table. Name INV13's list here.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 32814b9

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: an engineering owner approved the direction of spec:AST-107 for this revision, but three clauses still rest on records main does not have yet. Please land #8613, #8646 and #8601 first and rebase (or drop the clauses that depend on them), then have an engineering owner re-approve the new revision with /approve-spec.

  • FR3's theme-following spring cites spec:AST-108, while spec:AST-094 FR4 on main still says a theme cannot override a motion role (DEC-11).
  • FR1 and FR2's mouse-and-pen scope cites AST-094 FR43, which main lacks, while the INV13 text this pull request keeps attaches no recognizer under a fine primary pointer.
  • FR5 and FR7 cite createReorderable's positions and attach path, which spec:AST-103 does not define yet.

[Automated review]

`usePanGesture(ref, {axis?, onPan})` calls `onPan` with
`{state, translation, velocity}` as a drag on the element begins, changes,
ends, or is cancelled. `axis` limits the drag to `'x'` or `'y'`. A finger,
a mouse, and a pen all drive it (`spec:AST-094` FR43). It never

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-094 has no FR43 on main (#8646 adds it), and the INV13 text this pull request keeps says "Where the primary pointer is fine, no recognizer attaches and no touch listener is registered". Land #8646 first and rebase, or scope FR1 and FR2 to what INV13 allows today and drop the FR43 citation.

`stop()` halts them where they are. Reduce Motion is planned for every
animation (`design:reduced-motion` DR1–DR3). Every `animate` plays on the
`springs.stateChange` motion role (`spec:AST-094` FR4), which follows the
theme's tempo and character (`spec:AST-108` FR3), and every `decay` plays on

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-108 is not on main yet (#8613 is open), and spec:AST-094 FR4 there still says "a theme cannot override them yet (DEC-11)". Land #8613 first, or have animate play springs.stateChange as AST-094 FR4 defines it and cite AST-094 FR3's momentum coast for decay.

use it rather than the internals beneath it. A component reaches internals
only for a job the public layer does not expose; those jobs are listed as
known public-API gaps in `architecture:react-component-runtime` INV13.
`createReorderable`'s plain-element attach path (`spec:AST-103`) covers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-103 FR1 on main gives createReorderable only onReorder and isReorderable, applied through a collection's reorderable prop; this attach path, and FR5's positions, arrive with #8601. Land it first, or drop both clauses until then.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 738ca2c

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: an engineering owner approved spec:AST-107 for this head, and the FR43 and positions dependencies are now resolved on main, but two clauses still contradict current records. Please either land #8613 first and update this branch, or have FR3's animate play springs.stateChange as spec:AST-094 FR4 defines it and drop the spec:AST-108 citations. Then carry the flickThreshold exception into spec:AST-100's Non-goals and FR3 verification row, and have an engineering owner re-approve the new head with /approve-spec.

  • FR3 cites spec:AST-108, which is not on main, while spec:AST-094 FR4 still says a theme cannot override a motion role yet (DEC-11).
  • spec:AST-100 still says every threshold stays internal, and its FR3 row fails "a threshold reachable from public API".

[Automated review]

Comment on lines +146 to +148
`springs.stateChange` motion role (`spec:AST-094` FR4), which follows the
theme's tempo and character (`spec:AST-108` FR3), and every `decay` plays on
the fixed momentum decay (`spec:AST-108` FR4); choosing any other spring or

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-108 is not on main yet (#8613 is open), and spec:AST-094 FR4 there still says "a theme cannot override them yet (DEC-11)". Please land #8613 first and update this branch, or have animate play springs.stateChange as AST-094 FR4 defines it and cite AST-094 FR3's momentum coast for decay.

Comment on lines +77 to +78
stays internal (`spec:AST-002` FR3), except the public `flickThreshold` a
product passes to `spec:AST-107`'s snap-point drag; a component never derives a second

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same exception is missing from this record's Non-goals (line 51: "every threshold stay internal") and from the FR3 verification row (line 124), which fails on "a threshold reachable from public API". Could both carry the flickThreshold exception too?

| FR3 | Unit and browser tests of `useAnimator` | each method, Reduce Motion, an unattached ref, an unknown channel, a call after `destroy` | a missing method or channel, an `animate` off `springs.stateChange`, or motion that ignores Reduce Motion or a throw on an unknown channel or after `destroy` fails |
| FR4 | Export check of `@astryxdesign/core` and its hooks | the public exports | a recognizer, the arena, the internal animator module, or the resolver reachable from a public export fails |
| FR6 | Unit tests of each factory and its hook, server-render and import checks, and listener counts after `destroy` | each factory without React; each hook; a server import; `destroy` mid-drag | a hook with behavior its factory lacks, a listener left after `destroy`, or DOM work at import fails |
| FR7 | Source boundary check over Core and Lab against the known public-API gap list in `architecture:react-component-runtime` INV13 | every Core and Lab component that drags, snaps, animates one element, reorders, or animates layout | a component reaching internals for a job the public layer covers, or an internal use whose job is not in the gap table, fails |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INV13 holds a prose list, not a table, so the failure signal should say "gap list", as the Verification cell does.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec c635592

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: an engineering owner approved spec:AST-107 for this head and the flickThreshold exception is carried through spec:AST-094 and spec:AST-100, but FR3 still contradicts a current record. Please either land #8613 first and update this branch, or have FR3's animate play springs.stateChange as spec:AST-094 FR4 defines it, cite AST-094 FR3's momentum coast for decay, and drop the spec:AST-108 citations, the Non-goal's included. Then have an engineering owner re-approve the new head with /approve-spec.

  • FR3 says animate follows the theme's tempo and character (spec:AST-108 FR3), but spec:AST-108 is not on main, and spec:AST-094 FR4 still says a theme cannot override a motion role yet (DEC-11).

[Automated review]

Comment on lines +146 to +148
`springs.stateChange` motion role (`spec:AST-094` FR4), which follows the
theme's tempo and character (`spec:AST-108` FR3), and every `decay` plays on
the fixed momentum decay (`spec:AST-108` FR4); choosing any other spring or

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spec:AST-108 is not on main yet (#8613 is open), and spec:AST-094 FR4 there still says “a theme cannot override them yet (DEC-11)”. Please land #8613 first and update this branch, or have animate play springs.stateChange as AST-094 FR4 defines it, cite AST-094 FR3's momentum coast for decay, and drop the spec:AST-108 citations here and in the Non-goal at line 58.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec c635592

…ooks

# Conflicts:
#	docs/specs/AST-094-touch-presentations/spec.md
@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec e2455c8

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving: an engineering owner approved spec:AST-107 for this head, and with spec:AST-108 now on main, every current record the spec relies on exists, so FR3's theme-following animate no longer conflicts with spec:AST-094 FR4. Two optional clarifications would keep the first implementation from settling details of a stable API by accident:

  • FR1 and FR2: say whether axis: 'x' offsets and the flick direction follow the inline direction under right-to-left (design:bidirectionality DR13).
  • FR1: name the values of onPan's state and whether translation and velocity are points or scalars along axis.

[Automated review]


- **FR1 — `usePanGesture` reports a drag as values.**
`usePanGesture(ref, {axis?, onPan})` calls `onPan` with
`{state, translation, velocity}` as a drag on the element begins, changes,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: could FR1 name state's values and say whether translation and velocity are points or scalars along axis? With no _experimental suffix, the first implementation would otherwise fix them for a stable API.

`useDragToSnapPoints(ref, {axis, snapPoints, flickThreshold?, onSettle})`
lets a drag move the element along `axis`, under
every pointer, as FR1's does. `snapPoints` are offsets in
pixels along `axis` from the element's resting position, in ascending order;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: under right-to-left, do axis: 'x' offsets and the flick direction follow the inline direction, or stay physical with the caller flipping them? design:bidirectionality DR13 governs a horizontal gesture's outcome, and spec:AST-057 FR8 and spec:AST-100 FR5 state theirs.

`animate(target, {velocity?}) → Promise<void>` springs the given channels to
`target`; `decay(velocity) → Promise<void>` coasts them from a velocity;
`set(values)` jumps them; `get()` and `getVelocity()` read them; and
`stop()` halts them where they are. Reduce Motion is planned for every

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: “Reduce Motion is planned for every animation” can read as future work; “Under reduced motion every animation follows design:reduced-motion DR1–DR3” states the same rule in the present tense.

@cixzhang
cixzhang merged commit a5cb073 into main Oct 27, 2026
68 checks passed
@github-actions
github-actions Bot deleted the spec/public-gesture-hooks branch October 28, 2026 06:17

This branch was successfully deployed

1 active deployment
Preview — e2455c8c Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants