@unrulysystems/native-motion 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +55 -0
  4. package/android/build.gradle +24 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/unrulysystems/nativemotion/GestureExclusionModule.kt +55 -0
  7. package/android/src/main/java/com/unrulysystems/nativemotion/NativeMotionPackage.kt +27 -0
  8. package/package.json +57 -0
  9. package/react-native.config.js +14 -0
  10. package/src/native/driver/instantWindow.ts +31 -0
  11. package/src/native/driver/strictModeReplay.ts +20 -0
  12. package/src/native/driver/uiLayoutEngine.ts +845 -0
  13. package/src/native/driver/uiLayoutGraph.ts +338 -0
  14. package/src/native/driver/uiValueChannel.ts +4640 -0
  15. package/src/native/driver/workletDriver.ts +3860 -0
  16. package/src/native/motion/AnimatePresence.tsx +1605 -0
  17. package/src/native/motion/LayoutGroup.tsx +163 -0
  18. package/src/native/motion/MotionConfig.tsx +400 -0
  19. package/src/native/motion/MotionRoot.tsx +250 -0
  20. package/src/native/motion/MotionView.tsx +6761 -0
  21. package/src/native/motion/addScaleCorrector.ts +97 -0
  22. package/src/native/motion/colorEndpointUiFeed.ts +32 -0
  23. package/src/native/motion/colorProjection.ts +39 -0
  24. package/src/native/motion/composeTransform.ts +271 -0
  25. package/src/native/motion/constraintMeasure.ts +157 -0
  26. package/src/native/motion/deferredPendingSnapshots.ts +25 -0
  27. package/src/native/motion/discreteProjection.ts +36 -0
  28. package/src/native/motion/dragAncestorPanContext.ts +28 -0
  29. package/src/native/motion/dragControls.ts +152 -0
  30. package/src/native/motion/dragGestureWiring.ts +1168 -0
  31. package/src/native/motion/dragHandoffBinding.ts +179 -0
  32. package/src/native/motion/dragHubStream.ts +756 -0
  33. package/src/native/motion/dragPropagationLock.ts +57 -0
  34. package/src/native/motion/driverValueChannel.ts +3288 -0
  35. package/src/native/motion/externalDragDriver.ts +214 -0
  36. package/src/native/motion/frameData.ts +31 -0
  37. package/src/native/motion/gestureBinding.ts +107 -0
  38. package/src/native/motion/gestureStateGate.ts +563 -0
  39. package/src/native/motion/gestureStateResolver.ts +286 -0
  40. package/src/native/motion/identityValueChannelControllerAdapter.ts +2043 -0
  41. package/src/native/motion/identityValueChannelControllerReconciliation.ts +176 -0
  42. package/src/native/motion/identityValueChannelLaneMarker.ts +14 -0
  43. package/src/native/motion/imperativeAnimate.ts +816 -0
  44. package/src/native/motion/keyframeTiming.ts +7 -0
  45. package/src/native/motion/layoutIdBinding.ts +261 -0
  46. package/src/native/motion/layoutIdFlightConfig.ts +51 -0
  47. package/src/native/motion/layoutProjection.ts +89 -0
  48. package/src/native/motion/layoutScroll.ts +204 -0
  49. package/src/native/motion/layoutTransition.ts +1302 -0
  50. package/src/native/motion/lengthLayoutContext.tsx +100 -0
  51. package/src/native/motion/lengthLayoutHost.ts +44 -0
  52. package/src/native/motion/mappedKeys.ts +184 -0
  53. package/src/native/motion/motionViewController.ts +3180 -0
  54. package/src/native/motion/nativeHostMarker.ts +22 -0
  55. package/src/native/motion/panSessionWiring.ts +127 -0
  56. package/src/native/motion/pathTransition.ts +101 -0
  57. package/src/native/motion/popLayout.ts +176 -0
  58. package/src/native/motion/presenceBinding.ts +382 -0
  59. package/src/native/motion/scaleCorrectorRegistry.ts +123 -0
  60. package/src/native/motion/scrollValues.ts +260 -0
  61. package/src/native/motion/serializablePayload.ts +151 -0
  62. package/src/native/motion/severity.ts +16 -0
  63. package/src/native/motion/shippedSurface.ts +843 -0
  64. package/src/native/motion/staticLengthGate.ts +86 -0
  65. package/src/native/motion/styleBaseGate.ts +72 -0
  66. package/src/native/motion/styleValueBinding.ts +657 -0
  67. package/src/native/motion/systemGestureExclusion.ts +61 -0
  68. package/src/native/motion/tapGestureWiring.ts +215 -0
  69. package/src/native/motion/transformOrder.ts +38 -0
  70. package/src/native/motion/transformStringBinding.ts +200 -0
  71. package/src/native/motion/transformTemplateGate.ts +45 -0
  72. package/src/native/motion/transitionGate.ts +435 -0
  73. package/src/native/motion/useAnimate.ts +52 -0
  74. package/src/native/motion/useCycle.ts +44 -0
  75. package/src/native/motion/useInstantTransition.ts +68 -0
  76. package/src/native/motion/useReducedMotion.ts +58 -0
  77. package/src/native/motion/useScroll.ts +162 -0
  78. package/src/native/motion/useViewportScroll.ts +29 -0
  79. package/src/native/motion/valueChannel.ts +7121 -0
  80. package/src/native/motion/valueHooks.ts +901 -0
  81. package/src/native/motion/variantChildRegistry.ts +67 -0
  82. package/src/native/motion/variantContext.tsx +172 -0
  83. package/src/native/motion/variantProps.ts +598 -0
  84. package/src/native.ts +159 -0
  85. package/src/verification/harnessMetrics.ts +78 -0
  86. package/src/verification/probe/LayoutIdentityWorkletProbe.tsx +251 -0
  87. package/src/verification/probe/WorkletParityProbe.tsx +162 -0
  88. package/src/verification/probe/layoutIdentityProbeEngine.ts +429 -0
  89. package/src/verification/screens/ArcPathChecksScreen.tsx +273 -0
  90. package/src/verification/screens/BooleanAnimateChecksScreen.tsx +358 -0
  91. package/src/verification/screens/ChoreographyGalleryScreen.tsx +1010 -0
  92. package/src/verification/screens/ColorBindingChecksScreen.tsx +568 -0
  93. package/src/verification/screens/CompletionChecksScreen.tsx +294 -0
  94. package/src/verification/screens/ConformanceScreen.tsx +573 -0
  95. package/src/verification/screens/ContentionProbeScreen.tsx +224 -0
  96. package/src/verification/screens/DriverSmokeScreen.tsx +99 -0
  97. package/src/verification/screens/DurationOnlyTweenChecksScreen.tsx +294 -0
  98. package/src/verification/screens/DynamicDragConfigChecksScreen.tsx +237 -0
  99. package/src/verification/screens/FrameDataChecksScreen.tsx +252 -0
  100. package/src/verification/screens/GestureChecksScreen.tsx +685 -0
  101. package/src/verification/screens/InstantTransitionChecksScreen.tsx +839 -0
  102. package/src/verification/screens/LayoutAnimationStartChecksScreen.tsx +648 -0
  103. package/src/verification/screens/LayoutChecksScreen.tsx +822 -0
  104. package/src/verification/screens/LayoutCommitSpikeScreen.tsx +142 -0
  105. package/src/verification/screens/MotionViewChecksScreen.tsx +930 -0
  106. package/src/verification/screens/PresenceChecksScreen.tsx +614 -0
  107. package/src/verification/screens/ReducedMotionChecksScreen.tsx +614 -0
  108. package/src/verification/screens/RestThresholdChecksScreen.tsx +634 -0
  109. package/src/verification/screens/ScaleCorrectorChecksScreen.tsx +425 -0
  110. package/src/verification/screens/SharedLayoutContinuityChecksScreen.tsx +338 -0
  111. package/src/verification/screens/SharedLayoutCrossfadeChecksScreen.tsx +425 -0
  112. package/src/verification/screens/TransitionDefaultSelectionChecksScreen.tsx +514 -0
  113. package/src/verification/screens/ViewportScrollAliasChecksScreen.tsx +303 -0
  114. package/src/verification/screens/conformanceBanner.ts +21 -0
  115. package/src/verification/screens/proofConsoleTap.ts +18 -0
  116. package/src/verification.ts +47 -0
  117. package/src/web.ts +98 -0
@@ -0,0 +1,3860 @@
1
+ // The real native driver (specs/M2.1-BUILD-PACKET.md, Milestone 3): core's `Driver` seam implemented
2
+ // over the Reanimated UI runtime. The substrate meets the graph exactly at this seam — commands cross
3
+ // JS→UI once each (O(1), REQ-DRIVER-013), the frame loop advances everything UI-side with core's own
4
+ // `stepProp` + generators (REQ-DRIVER-003: no forked math — the crossing chain is worklet-tagged core
5
+ // dist, see packages/core/scripts/tag-worklet-dist.mjs), and each element's props mirror into shared
6
+ // values that animated styles read.
7
+ //
8
+ // Frame source (REQ-DRIVER-011): the LIVE loop is a single driver-owned `useFrameCallback` — real
9
+ // vsync (Choreographer / CADisplayLink) — owned by the `useWorkletDriver` hook. It is deliberately
10
+ // NOT a UI-runtime `requestAnimationFrame` chain: that is a software poll (animationQueuePollingRate),
11
+ // wrong on 120Hz and drift-prone — review cycle 1 rejected exactly that. Settle-cancel
12
+ // (REQ-DRIVER-020): when every element quiesces, the UI side notifies JS ONCE (settleNotified guard)
13
+ // and JS deactivates the frame callback after a synchronous re-check (a command racing the settle edge
14
+ // must win); a deactivated callback costs zero per-frame work — the M2.0 JS-thread driver burned CPU
15
+ // forever and is deleted.
16
+ //
17
+ // Command→trajectory resolution is split across the runtimes on plain-data lines: the JS side converts
18
+ // the public `Transition` with the same COMPONENT converters the reference driver uses
19
+ // (`toSpringConfig` / `toTimingConfig`), and the UI side CONSTRUCTS the generator closure where it
20
+ // runs — a closure cannot cross runtimes (packet §Grounding), plain configs can.
21
+
22
+ import { useEffect, useRef } from 'react'
23
+ import {
24
+ makeMutable,
25
+ useFrameCallback,
26
+ type FrameInfo,
27
+ type SharedValue,
28
+ } from 'react-native-reanimated'
29
+ import { executeOnUIRuntimeSync, scheduleOnRN, scheduleOnUI } from 'react-native-worklets'
30
+ import {
31
+ buildRepeatedGenerator,
32
+ delayGenerator,
33
+ inertiaGenerator,
34
+ resolveSpring,
35
+ resolveSpringGenerator,
36
+ toDelayMs,
37
+ toRepeatFoldOptions,
38
+ retargetSpring,
39
+ stepProp,
40
+ timingGenerator,
41
+ toKeyframesConfig,
42
+ toSpringConfig,
43
+ type Driver,
44
+ type ElementHandle,
45
+ type Generator,
46
+ type InertiaTransition,
47
+ type RepeatFoldOptions,
48
+ type RepeatLeg,
49
+ type KeyframesConfig,
50
+ type LivePropState,
51
+ type PropStep,
52
+ type PropTargetTo,
53
+ type PropTransition,
54
+ type SpringConfig,
55
+ type TimingConfig,
56
+ type Transition,
57
+ } from '@unrulysystems/native-motion-core'
58
+ import {
59
+ adaptPreparedDriver,
60
+ applyPathProgressInto,
61
+ instantFinalKeyframe,
62
+ instantGenerator,
63
+ isDurationZeroInstantTransition,
64
+ keyframeOffsetAt,
65
+ keyframeSegmentEase,
66
+ isRepeatFoldRefusalIdentity,
67
+ isTimingLaneTransition,
68
+ iterationMeasurementLane,
69
+ repeatRefusalMessage,
70
+ toTimingLaneConfig,
71
+ REPEAT_REFUSAL_KEY,
72
+ REPEAT_REFUSAL_NAME,
73
+ type ArcScratch,
74
+ type PathChannelGeometry,
75
+ type PathProgressSample,
76
+ type PreparedDriver,
77
+ type PreparedPropTarget,
78
+ } from '@unrulysystems/native-motion-core/internal-driver'
79
+ // Type-only: the runtime registry/session enter the UI runtime inside uiLayoutEngine, which
80
+ // imports them from the file-tagged worklet-layout subpath (the root-entry import above never
81
+ // reaches a UI worklet from this file).
82
+ import type {
83
+ LayoutIdentityRole,
84
+ LayoutIdToken,
85
+ WindowSpaceRect,
86
+ } from '@unrulysystems/native-motion-core/worklet-layout'
87
+ import { assertSetupNotReplayedOnDisposed } from './strictModeReplay'
88
+ import { isInstantWindowOpen } from './instantWindow'
89
+ import {
90
+ createUiLayoutState,
91
+ uiLayoutAttach,
92
+ uiLayoutCommit,
93
+ uiLayoutCouple,
94
+ uiLayoutDetach,
95
+ uiLayoutOpacityWitness,
96
+ uiLayoutStep,
97
+ uiLayoutUncouple,
98
+ uiProjectionAttach,
99
+ uiProjectionDetach,
100
+ uiProjectionSetProjection,
101
+ uiSetProjectionRadiusFeed,
102
+ type UiLayoutHolderOutputs,
103
+ type UiProjectionOutputs,
104
+ type UiLayoutState,
105
+ } from './uiLayoutEngine'
106
+
107
+ // A transition resolved JS-side to plain data so it can cross to the UI runtime. Mirrors the reference
108
+ // driver's start/retarget selection (spring is the fallback; tween goes through TIMING; REQ-TIMING-006:
109
+ // an authored duration-ONLY bag with no `type` also resolves 'tween', the pin's keyframes generator).
110
+ // `keyframes`
111
+ // carries the multi-keyframe timing config (REQ-API-033) resolved through the SAME COMPONENT converter
112
+ // (`toKeyframesConfig`) the reference driver uses, so an ARRAY target selects it UI-side with zero
113
+ // resolution skew between the two driver implementations — the target shape, not `kind`, selects it.
114
+ // `kind` is the SCALAR generator selector (spring is the fallback — an omitted type WITHOUT
115
+ // `duration` resolves to 'spring'; an omitted type WITH `duration` resolves to 'tween',
116
+ // REQ-TIMING-006). `explicitSpring` preserves whether the type was LITERALLY 'spring' (vs defaulted), which
117
+ // the keyframe-ARRAY branch needs: an array's default is a keyframes tween, so only an EXPLICIT spring
118
+ // routes an array to spring physics — collapsing omitted→spring for arrays skews vs the reference
119
+ // driver (review major 8). Tween is never a spring, so its `explicitSpring` is the literal `false`.
120
+ type PlainTransition =
121
+ | {
122
+ readonly kind: 'spring'
123
+ readonly config: SpringConfig
124
+ readonly keyframes: KeyframesConfig
125
+ readonly explicitSpring: boolean
126
+ // T18-a (REQ-API-049): the repeat fold's options, resolved JS-side to plain scalars, or null
127
+ // when the fold is inert. An authored `Transition` never crosses the marshal.
128
+ readonly repeat: RepeatFoldOptions | null
129
+ // T18-b (REQ-TIMING-004): the delay rebase's shift in MS, resolved JS-side through the same
130
+ // `toDelayMs` seconds→ms crossing the reference driver uses. Plain scalar — the authored
131
+ // seconds never cross. (Inertia is command-layer-only and carries no authored delay, so its
132
+ // arm omits this — the same totality law as `repeat`.)
133
+ readonly delayMs: number
134
+ }
135
+ | {
136
+ readonly kind: 'tween'
137
+ readonly config: TimingConfig
138
+ readonly keyframes: KeyframesConfig
139
+ readonly explicitSpring: false
140
+ readonly repeat: RepeatFoldOptions | null
141
+ readonly delayMs: number
142
+ }
143
+ | {
144
+ // U7a + transition-default-selection F5: the instant lane (the pin's makeAnimationInstant,
145
+ // motion-dom@12.42.2 motion-value.ts:89-98,122-136) — the guard is type-independent:
146
+ // `type: false` (U7a) OR `duration === 0 && !repeatDelay` (F5). NO trajectory generator —
147
+ // the final keyframe commits on the driver's next update tick (never synchronously at
148
+ // command time), and the authored delay is honored through the shared rebase (only the
149
+ // pin's GLOBAL instant flag zeroes delay — 7b's window, not this lane). The start/retarget
150
+ // seams never fold it, so it carries no repeat/keyframes fields.
151
+ readonly kind: 'instant'
152
+ readonly delayMs: number
153
+ }
154
+ | {
155
+ // R16 (REQ-GESTURE-023): the gesture free-drag release's two-phase inertia descriptor.
156
+ // Command-layer only (never authored); plain scalar data so the marshal rides verbatim.
157
+ readonly kind: 'inertia'
158
+ readonly config: PlainInertiaConfig
159
+ readonly explicitSpring: false
160
+ }
161
+
162
+ // The inertia descriptor as plain worklet-marshalable data. The bounds ride verbatim from the
163
+ // prepared descriptor: `undefined` for an absent bound (no constraints on the release, or the
164
+ // side omitted) — an undefined-valued optional DOES cross the marshal, benign since the UI side
165
+ // treats it as an absent bound; the ±Infinity sentinel for the open side of a resolved
166
+ // constraint set (the drag-constraints law); or a finite number.
167
+ type PlainInertiaConfig = {
168
+ readonly power: number
169
+ readonly timeConstant: number
170
+ readonly bounceStiffness: number
171
+ readonly bounceDamping: number
172
+ readonly restDelta: number
173
+ readonly restSpeed: number
174
+ readonly min: number | undefined
175
+ readonly max: number | undefined
176
+ }
177
+
178
+ type PlainCommand =
179
+ | {
180
+ readonly kind: 'start'
181
+ // `velocity` rides the marshal verbatim (plain data): the REQ-DRIVER-023 generator seed. `to` is a
182
+ // scalar destination OR a keyframe array the value interpolates THROUGH (REQ-API-033, R8). The
183
+ // prepared per-prop `transition` is marshaled to PlainTransition JS-side (REQ-DRIVER-030).
184
+ readonly targets: Readonly<Record<string, PlainStartTarget>>
185
+ // The gesture RELEASE edge (review r8 major 24): true ⇒ this start ends the hold on its targets;
186
+ // false ⇒ a declarative start that CONTENDS on a gesture-held prop (drifts the live value
187
+ // without clearing the hold, REQ-DRIVER-026). Plain boolean, marshaled.
188
+ readonly releasesGesture: boolean
189
+ readonly path?: {
190
+ readonly fromX: number
191
+ readonly fromY: number
192
+ readonly toX: number
193
+ readonly toY: number
194
+ readonly peak: number
195
+ readonly signedStrength: number
196
+ readonly rotationScale: number
197
+ readonly transition: PlainTransition
198
+ }
199
+ }
200
+ | {
201
+ readonly kind: 'retarget'
202
+ readonly targets: Readonly<Record<string, number>>
203
+ readonly transition: PlainTransition
204
+ readonly targetTransitions?: Readonly<Record<string, PlainTransition>>
205
+ }
206
+ | { readonly kind: 'stop' }
207
+
208
+ // The per-target payload crossing to the UI runtime: `to`/`from`/`velocity` plain data, plus the
209
+ // OPTIONAL per-prop transition ALREADY marshaled to PlainTransition JS-side (R8 M2 per-prop defaults).
210
+ type PlainStartTarget = {
211
+ readonly to: PropTargetTo
212
+ readonly from: number
213
+ readonly velocity: number
214
+ readonly transition: PlainTransition
215
+ }
216
+
217
+ // Marshal an already-complete REQ-DRIVER-030 target snapshot to UI-runtime data. Caller accessors,
218
+ // mutation detachment, velocity defaulting, and transition fallback were all handled once by core's
219
+ // adapter; this backend performs only the host-specific config conversion.
220
+ function marshalStartTargets(
221
+ targets: Readonly<Record<string, PreparedPropTarget>>,
222
+ marshalTransition: (transition: PropTransition) => PlainTransition,
223
+ ): Readonly<Record<string, PlainStartTarget>> {
224
+ const out: Record<string, PlainStartTarget> = {}
225
+ for (const key of Object.keys(targets)) {
226
+ const t = targets[key]!
227
+ // defineProperty preserves an own `__proto__` key without changing this plain object's prototype;
228
+ // the worklets serializer receives the same ordinary-object shape as before REQ-DRIVER-030.
229
+ Object.defineProperty(out, key, {
230
+ enumerable: true,
231
+ value: Object.freeze({
232
+ to: t.to,
233
+ from: t.from,
234
+ velocity: t.velocity,
235
+ transition: marshalTransition(t.transition),
236
+ }),
237
+ })
238
+ }
239
+ return Object.freeze(out)
240
+ }
241
+
242
+ // Core deliberately prepares target maps with a null prototype so an own `__proto__` key remains data.
243
+ // Worklets 0.10.2 serializes an ordinary record only when its prototype is Object.prototype, so rebuild
244
+ // that map at the host crossing without using assignment (which would invoke the legacy __proto__ setter).
245
+ export function marshalRetargetTargets(
246
+ targets: Readonly<Record<string, number>>,
247
+ ): Readonly<Record<string, number>> {
248
+ const out: Record<string, number> = {}
249
+ for (const key of Object.keys(targets)) {
250
+ Object.defineProperty(out, key, { enumerable: true, value: targets[key]! })
251
+ }
252
+ return Object.freeze(out)
253
+ }
254
+
255
+ export function marshalRetargetTransitions(
256
+ transitions: Readonly<Record<string, PropTransition>> | undefined,
257
+ marshalTransition: (transition: PropTransition) => PlainTransition,
258
+ ): Readonly<Record<string, PlainTransition>> | undefined {
259
+ if (transitions === undefined) return undefined
260
+ const out: Record<string, PlainTransition> = {}
261
+ for (const key of Object.keys(transitions)) {
262
+ Object.defineProperty(out, key, {
263
+ enumerable: true,
264
+ value: marshalTransition(transitions[key]!),
265
+ })
266
+ }
267
+ return Object.freeze(out)
268
+ }
269
+
270
+ // One element's live UI-side state. Plain records (not Map) — everything here is created and mutated on
271
+ // the UI runtime only; `shared` are the Reanimated shared values animated styles bind to.
272
+ interface UiElementState {
273
+ active: boolean
274
+ // Per-prop gesture hold count (REQ-DRIVER-024, G-INV-7, review r7 major 23): the number of props a
275
+ // gesture holds. A `write` establishes a hold; only the MATCHING release/stop command clears it —
276
+ // an unrelated command is a PARTIAL overlay and must not release another prop's hold. While any
277
+ // prop is held the element is UNSETTLED (a finger is down, REQ-GESTURE-002) — the live analog of
278
+ // the M1 session's ActivityHandle — so the frame loop emits no spurious settle edge mid-drag. A
279
+ // count (not a boolean) keeps the @frame-path quiesce check O(1) + allocation-free (REQ-DRIVER-021).
280
+ heldCount: number
281
+ readonly committed: Record<string, number>
282
+ // Gesture-phase live velocity per held prop (REQ-DRIVER-025, G-INV-3, review r4 c14e15): the
283
+ // platform velocity the most recent `write` carried, mirrored from the reference driver. A prop
284
+ // WITH an entry here is held; uiLiveFor reports it (no generator ⇒ this is the live velocity) so
285
+ // the release-edge C1 seam is continuous; cleared per prop by the matching command; a
286
+ // settled/never-written prop has no entry ⇒ 0.
287
+ readonly heldVelocity: Record<string, number>
288
+ // Last recognizer-write version applied per prop. A live `write` is scheduled while `liveFor`
289
+ // crosses synchronously, so the JS side compares this marker with its queued-write mirror to
290
+ // provide read-your-writes without replaying or reordering the UI lane (REQ-DRIVER-024/-025).
291
+ readonly writeVersion: Record<string, number>
292
+ readonly props: Record<string, PropStep>
293
+ // A layout host proxy owns a complete transform array while it is in flight. Each property
294
+ // therefore exposes its live command state so the proxy mirror follows the ACTUAL gesture,
295
+ // keyframe, exit, or bound-value authority instead of a render-time animate snapshot.
296
+ readonly layoutMirror: Record<string, WorkletDriverPropertyState>
297
+ // Contention target per gesture-held prop (REQ-DRIVER-026), mirrored from the reference driver: the
298
+ // declarative `to` + transition a held prop animates toward while the finger owns it. A finger write
299
+ // RE-ANCHORS the contending generator to the freshly-committed finger value so the drift continues
300
+ // FROM the finger's position, never jumping back to the generator's stale trajectory (review r10
301
+ // major 29, G-INV-2). Cleared at release/stop and when the finger takes over a settled contention.
302
+ readonly contending: Record<string, { to: PropTargetTo; transition: PlainTransition }>
303
+ // The frame loop's iteration list for `props`, rebuilt at command edges (uiApplyCommand) —
304
+ // Object.keys per element per vsync is frame-executed garbage (REQ-DRIVER-021).
305
+ propKeys: string[]
306
+ readonly shared: Record<string, SharedValue<number>>
307
+ // T23 C: the onUpdate marshal arm. When armed, the frame step emits ONE primitive payload
308
+ // per frame to JS while any committed value changed since the last emission (the pin's
309
+ // value-change cadence, VisualElement.ts:574-586). lastNotified is the change discriminator.
310
+ onUpdateArmed: boolean
311
+ readonly lastNotified: Record<string, number>
312
+ // The live onUpdate callback's registration generation, mirrored from the JS arm/disarm edge.
313
+ // A staged frame entry carries it so the JS envelope drain drops an entry whose listener
314
+ // registration changed between commit and drain (packet §2a live-generation lookup).
315
+ registrationGeneration: number
316
+ // Off-frame-prefilled member slots for the sealed frame envelope (REQ-DRIVER-021): capacity
317
+ // grows only where propKeys is rebuilt (command/registration edge); uiEmitFrameValues fills
318
+ // keys/values by index and never constructs a Record or grows these in-frame.
319
+ readonly frameKeys: string[]
320
+ readonly frameValues: number[]
321
+ path: {
322
+ progress: PropStep
323
+ geometry: PathChannelGeometry
324
+ // Caller-owned command-edge scratch for the allocation-free arc samplers (REQ-DRIVER-021,
325
+ // packet §6 arc-return-records repair): uiStepAll writes these slots every frame.
326
+ sample: PathProgressSample
327
+ scratch: ArcScratch
328
+ } | null
329
+ }
330
+
331
+ // `Driver.write` is the sole production event-write seam (REQ-DRIVER-029). Gesture writes hold
332
+ // until their release command; bound MotionValues are instantaneous retargets with no release.
333
+ // The shape travels with that one write call so the UI owner can preserve both laws without a
334
+ // binding-only side channel.
335
+ export type WorkletDriverWriteShape = 'gesture-hold' | 'bound-value-retarget'
336
+
337
+ export type WorkletDriver = Omit<Driver, 'write'> & {
338
+ write(
339
+ handle: ElementHandle,
340
+ values: Readonly<Record<string, number>>,
341
+ velocities?: Readonly<Record<string, number>>,
342
+ eventShape?: WorkletDriverWriteShape,
343
+ ): void
344
+ }
345
+
346
+ /**
347
+ * Give the fold's refusal the SAME identity the reference backend gives it (fix-up review MAJOR).
348
+ *
349
+ * `buildRepeatedGenerator` returns a bare `Error` — it is shared with the UI runtime, where a throw
350
+ * is a `std::terminate`, so it never constructs a severity. The reference backend re-wraps that into
351
+ * an `InvalidTransitionError` naming `repeat`; this backend cannot construct that class here (a core
352
+ * class inside a worklet), and would gain nothing if it could, because `uiGuardLane` marshals only
353
+ * `{name, message}` to RN. So it reproduces the two fields the crossing actually carries, from the
354
+ * shared builder, and `driverParity.differential.test.ts` compares the two backends' refusals
355
+ * directly — the trajectory differential is blind to a fold that BOTH backends refuse, which is how
356
+ * this one survived a round.
357
+ */
358
+ // alloc-ok: lifecycle-edge — built only on the refusal path, once per rejected animate command.
359
+ function uiRepeatRefusal(repeat: number, refusal: Error): Error {
360
+ 'worklet'
361
+ const named = new Error(repeatRefusalMessage(repeat, refusal.message)) as Error & { key: string }
362
+ named.name = REPEAT_REFUSAL_NAME
363
+ // The offending property. `uiGuard` marshals it beside the name, so the severity router
364
+ // recognizes this refusal after the crossing exactly as it recognizes the reference backend's.
365
+ named.key = REPEAT_REFUSAL_KEY
366
+ return named
367
+ }
368
+
369
+ /**
370
+ * The dual-authority bound-value refusal's marshalled identity (REQ-DRIVER-034). The message is
371
+ * byte-identical to what the synchronous lane throws — the adapter's catch matches its prefix —
372
+ * but a name is what survives the scheduled lane's `{name, message, key}` marshal, where an
373
+ * `instanceof` cannot. Thrown ONLY from `uiApplyWrite`'s validate-all-keys phase (before ANY
374
+ * mutation), so a live-lane guard that recognizes it knows the element is exactly as the refused
375
+ * write found it.
376
+ */
377
+ const BOUND_VALUE_WRITE_REFUSAL_NAME = 'BoundValueWriteRefusal'
378
+
379
+ // alloc-ok: lifecycle-edge — built only on the refusal path, once per refused retarget write.
380
+ function uiBoundValueWriteRefusal(key: string): Error {
381
+ 'worklet'
382
+ const named = new Error(
383
+ `cannot write bound value '${key}': it is owned by a gesture or declarative command.`,
384
+ ) as Error & { key: string }
385
+ named.name = BOUND_VALUE_WRITE_REFUSAL_NAME
386
+ // The offending property. `uiGuard` marshals it beside the name, so the JS-side delivery names
387
+ // the same key the synchronous rethrow carries.
388
+ named.key = key
389
+ return named
390
+ }
391
+
392
+ /** The scheduled lane's recognition half, decided on the marshalled record (structural, like the fold refusal's). */
393
+ function isBoundValueWriteRefusalIdentity(name: unknown): boolean {
394
+ 'worklet'
395
+ return name === BOUND_VALUE_WRITE_REFUSAL_NAME
396
+ }
397
+
398
+ /** UI-runtime state a layout-transform mirror reads without crossing back to JS. */
399
+ export interface WorkletDriverPropertyState {
400
+ readonly active: SharedValue<boolean>
401
+ readonly target: SharedValue<number>
402
+ }
403
+
404
+ interface UiDriverState {
405
+ readonly driverId: number
406
+ readonly elements: UiElementState[]
407
+ // One settle notification per in-flight→idle edge (REQ-DRIVER-020). Starts true (nothing in
408
+ // flight); any start/retarget command re-arms it.
409
+ settleNotified: boolean
410
+ // The JS-owned command/lifecycle epoch that opened the current live run. Carrying this plain
411
+ // number back with the UI settle edge lets JS reject a stale edge without reading a SharedValue
412
+ // (which itself can synchronously cross back into the UI runtime).
413
+ loopEpoch: number
414
+ // The layout-identity engine (specs/LAYOUT-UITHREAD-PORT-BUILD-PACKET.md), created lazily at the
415
+ // first layout attach so element-only drivers pay nothing for it.
416
+ layout: UiLayoutState | null
417
+ // Per-identity settle watchers. These are intentionally separate from the driver's global
418
+ // quiescence edge: one unrelated property animation or pending identity must not delay a
419
+ // mounted layoutId completion for an identity that has already reached rest.
420
+ layoutSettleWatchers: UiLayoutSettleWatcher[]
421
+ // The reusable sealed-envelope emission transaction (REQ-DRIVER-021) — constructed with the
422
+ // driver record, prefilled at command/registration edges, sealed at most once per frame.
423
+ readonly emission: UiFrameEmissionState
424
+ }
425
+
426
+ interface UiLayoutSettleWatcher {
427
+ readonly id: string
428
+ readonly key: number
429
+ }
430
+
431
+ // --- Sealed frame mailbox envelope (REQ-DRIVER-021, packet §2a closed-envelope law) --------------
432
+ // One UI frame stages every demanded entry into the driver's reusable emission transaction with
433
+ // indexed writes into off-frame-prefilled slots, then the seal edge serializes exactly ONE closed
434
+ // envelope across one scheduleOnRN(dispatchChannelMailboxTurnEnvelope, envelope) crossing. The
435
+ // entry shapes structurally mirror the motion layer's ChannelMailboxEntry vocabulary (the driver
436
+ // layer stays motion-import-free); values here are numbers only, so the element member sequence
437
+ // carries number tags alone.
438
+
439
+ interface UiFrameMailboxElementMember {
440
+ readonly key: string
441
+ readonly kind: 'number'
442
+ readonly value: number
443
+ }
444
+
445
+ type UiFrameMailboxEntry =
446
+ | {
447
+ readonly kind: 'ELEMENT_HOST_COMMIT'
448
+ readonly index: number
449
+ readonly driverId: number
450
+ readonly handleId: number
451
+ readonly registrationGeneration: number
452
+ readonly commitSequence: number
453
+ readonly values: readonly UiFrameMailboxElementMember[]
454
+ }
455
+ | {
456
+ readonly kind: 'LAYOUT_RELEASE'
457
+ readonly index: number
458
+ readonly layoutRegistrationId: number
459
+ }
460
+ | {
461
+ readonly kind: 'PROJECTION_STALE'
462
+ readonly index: number
463
+ readonly report: { readonly source: string; readonly code: string; readonly detail: string }
464
+ }
465
+ | { readonly kind: 'LAYOUT_SETTLE'; readonly index: number; readonly layoutAnimationId: number }
466
+ | {
467
+ readonly kind: 'DRIVER_SETTLE'
468
+ readonly index: number
469
+ readonly driverId: number
470
+ readonly schedulerEpoch: number
471
+ }
472
+ // The driver's last envelope: the UI record is deleted right after this seals, so nothing more
473
+ // can be sealed for the driver and JS may forget its mailbox on receipt — in order, behind every
474
+ // envelope sealed before the dispose crossing landed.
475
+ | { readonly kind: 'DRIVER_DISPOSED'; readonly index: number; readonly driverId: number }
476
+
477
+ interface UiFrameMailboxEnvelope {
478
+ readonly kind: 'CHANNEL_MAILBOX_TURN_ENVELOPE'
479
+ readonly mailboxOrigin: 'UI'
480
+ readonly mailboxDrainId: number
481
+ readonly driverId: number
482
+ readonly eventCount: number
483
+ readonly endIndex: number
484
+ readonly entries: readonly UiFrameMailboxEntry[]
485
+ readonly closingMarker: { readonly kind: 'CHANNEL_MAILBOX_END'; readonly index: number }
486
+ }
487
+
488
+ // The reusable per-driver emission transaction: every slot array is prefilled at a command or
489
+ // registration edge; a frame only fills by index and bumps counts. The sole allocation is the
490
+ // closed-envelope serialization at the seal edge (alloc-ok:mailbox-envelope-emission).
491
+ interface UiFrameEmissionState {
492
+ // Changed-element entry slots (capacity = elements.length, grown at the register edge).
493
+ readonly elementHandles: number[]
494
+ readonly elementGenerations: number[]
495
+ readonly elementCommitSequences: number[]
496
+ readonly elementMemberCounts: number[]
497
+ elementEntryCount: number
498
+ // Layout-settle staging (capacity = layoutSettleWatchers.length, grown at subscription).
499
+ readonly layoutSettledKeys: number[]
500
+ layoutSettledCount: number
501
+ // The driver settle edge: -1 when no settle is staged this frame.
502
+ driverSettleEpoch: number
503
+ // Set by the dispose crossing for the driver's final envelope.
504
+ disposeStaged: boolean
505
+ // Monotonic per-driver envelope id (doubles as the origin-scoped mailbox drain id).
506
+ commitSequence: number
507
+ }
508
+
509
+ // The JS callback registry deliberately lives outside each worklet closure. A per-identity
510
+ // listener can close over the MotionView lifecycle, whose unsubscribe path points back to this
511
+ // registry. Capturing that Map from the UI-frame worklet turns the listener graph cyclic while
512
+ // iOS serializes it. UI worklets carry only the driver/key pair; these JS dispatchers recover the
513
+ // live callback after the RN crossing.
514
+ interface LayoutCallbackRegistry {
515
+ readonly release: Map<number, () => void>
516
+ readonly settled: Map<number, () => void>
517
+ }
518
+
519
+ const layoutCallbacksByDriver = new Map<number, LayoutCallbackRegistry>()
520
+
521
+ // The global driver settle edge shares the layout emitter's ownership rule: a UI worklet carries
522
+ // only a driver id and a primitive epoch. Keeping the hook-local callback on JS avoids serializing
523
+ // React state, the frame callback, or a callback registry back into the UI frame closure.
524
+ const driverSettleCallbacks = new Map<number, (epoch: number) => void>()
525
+
526
+ // `scheduleOnRN` callbacks originate from UI worklets. Worklets can invoke the JS target before the
527
+ // UI dispatcher has unwound, so application callbacks must never run in that native callback extent:
528
+ // a synchronous read (`committed`, `liveFor`, or a layout query) would re-enter `runSyncSerialized`
529
+ // and recreate the iOS AB-BA lock inversion. Every driver-owned UI→JS dispatcher therefore first
530
+ // moves delivery onto the next JS task. A promise microtask is insufficient: Hermes can drain it
531
+ // while the synchronous JSI dispatchEventToJS invocation is still unwinding. Keep this
532
+ // JS-thread-local depth around the deferred
533
+ // delivery too: commands preserve the existing queued re-entry law, and the depth remains correct
534
+ // if a consumer synchronously induces a second driver callback.
535
+ let uiOriginatedCallbackDepth = 0
536
+
537
+ // React Native's event emitter and the worklet frame dispatcher both enter JavaScript while the
538
+ // UI runtime still owns the inbound call. Keep one marker for both origins: every mutating driver
539
+ // crossing consults it before selecting its synchronous verification lane.
540
+ export function runNativeEventCallback<T>(callback: () => T): T {
541
+ uiOriginatedCallbackDepth += 1
542
+ try {
543
+ return callback()
544
+ } finally {
545
+ uiOriginatedCallbackDepth -= 1
546
+ }
547
+ }
548
+
549
+ function deliverUiOriginatedCallback(callback: () => void): void {
550
+ runNativeEventCallback(callback)
551
+ }
552
+
553
+ function deferUiOriginatedCallback(callback: () => void): void {
554
+ // A zero-delay task is a scheduling boundary, not a retry: it preserves FIFO delivery among
555
+ // one frame's notifications while guaranteeing the native UI→JS dispatcher has returned.
556
+ setTimeout(() => deliverUiOriginatedCallback(callback), 0)
557
+ }
558
+
559
+ // The deferral seam is exported for the motion layer's scheduleOnRN severity reporters (seal
560
+ // r3, F1): a JS throw or report delivered inside the native UI→JS dispatch extent is the AB-BA
561
+ // class — every severity delivery lands one JS task later, exactly like the settle dispatches.
562
+ export { deferUiOriginatedCallback }
563
+
564
+ // An RN host event's layoutId measure/commit path needs a stronger boundary than the user
565
+ // callback marker above: the lifecycle currently returns synchronous role/flight results, so it
566
+ // runs in the next FIFO task after `dispatchEventToJS` has unwound rather than attempting a
567
+ // partial asynchronous layout API with fabricated return values.
568
+ export function deferNativeEventWork(callback: () => void): void {
569
+ setTimeout(callback, 0)
570
+ }
571
+
572
+ function deliverDriverSettled(driverId: number, epoch: number): void {
573
+ const callback = driverSettleCallbacks.get(driverId)
574
+ callback?.(epoch)
575
+ }
576
+
577
+ function deliverLayoutRelease(driverId: number, key: number): void {
578
+ const registry = layoutCallbacksByDriver.get(driverId)
579
+ const callback = registry?.release.get(key)
580
+ if (callback === undefined) return
581
+ registry!.release.delete(key)
582
+ callback()
583
+ }
584
+
585
+ function deliverLayoutSettled(driverId: number, key: number): void {
586
+ const registry = layoutCallbacksByDriver.get(driverId)
587
+ const callback = registry?.settled.get(key)
588
+ if (callback === undefined) return
589
+ registry!.settled.delete(key)
590
+ callback()
591
+ }
592
+
593
+ interface ElementFrameValueCallbackRegistration {
594
+ readonly generation: number
595
+ readonly callback: (values: Readonly<Record<string, number>>) => void
596
+ }
597
+
598
+ const elementFrameValueCallbacks = new Map<
599
+ number,
600
+ Map<number, ElementFrameValueCallbackRegistration>
601
+ >()
602
+ let elementFrameValueRegistrationGeneration = 0
603
+ const acceptedFrameMailboxDrainIdByDriver = new Map<number, number>()
604
+
605
+ function nextElementFrameValueRegistrationGeneration(): number {
606
+ if (elementFrameValueRegistrationGeneration >= Number.MAX_SAFE_INTEGER) {
607
+ throw new Error('frame-value listener registration generation exhausted.')
608
+ }
609
+ elementFrameValueRegistrationGeneration += 1
610
+ return elementFrameValueRegistrationGeneration
611
+ }
612
+
613
+ function validateFrameMailboxEnvelope(envelope: UiFrameMailboxEnvelope): void {
614
+ if (
615
+ envelope.kind !== 'CHANNEL_MAILBOX_TURN_ENVELOPE' ||
616
+ envelope.mailboxOrigin !== 'UI' ||
617
+ envelope.eventCount <= 0 ||
618
+ envelope.eventCount !== envelope.entries.length ||
619
+ envelope.endIndex !== envelope.eventCount - 1 ||
620
+ envelope.closingMarker.kind !== 'CHANNEL_MAILBOX_END' ||
621
+ envelope.closingMarker.index !== envelope.endIndex
622
+ ) {
623
+ throw new Error('invalid closed driver-frame mailbox envelope.')
624
+ }
625
+ for (let index = 0; index < envelope.entries.length; index += 1) {
626
+ const entry = envelope.entries[index]!
627
+ if (entry.index !== index) throw new Error('non-contiguous driver-frame mailbox envelope.')
628
+ if (
629
+ (entry.kind === 'ELEMENT_HOST_COMMIT' ||
630
+ entry.kind === 'DRIVER_SETTLE' ||
631
+ entry.kind === 'DRIVER_DISPOSED') &&
632
+ entry.driverId !== envelope.driverId
633
+ ) {
634
+ throw new Error('cross-driver entry in driver-frame mailbox envelope.')
635
+ }
636
+ }
637
+ const accepted = acceptedFrameMailboxDrainIdByDriver.get(envelope.driverId) ?? 0
638
+ if (envelope.mailboxDrainId !== accepted + 1) {
639
+ throw new Error('duplicate or out-of-order driver-frame mailbox envelope.')
640
+ }
641
+ }
642
+
643
+ function drainFrameMailboxEnvelope(envelope: UiFrameMailboxEnvelope): void {
644
+ validateFrameMailboxEnvelope(envelope)
645
+ acceptedFrameMailboxDrainIdByDriver.set(envelope.driverId, envelope.mailboxDrainId)
646
+ for (const entry of envelope.entries) {
647
+ switch (entry.kind) {
648
+ case 'ELEMENT_HOST_COMMIT': {
649
+ // The live-generation lookup precedes public record materialization: a listener replaced
650
+ // after UI commit cannot receive its predecessor registration's queued frame.
651
+ const registration = elementFrameValueCallbacks.get(entry.driverId)?.get(entry.handleId)
652
+ if (
653
+ registration === undefined ||
654
+ registration.generation !== entry.registrationGeneration
655
+ ) {
656
+ break
657
+ }
658
+ const values: Record<string, number> = {}
659
+ for (const member of entry.values) {
660
+ Object.defineProperty(values, member.key, {
661
+ enumerable: true,
662
+ value: member.value,
663
+ })
664
+ }
665
+ registration.callback(values)
666
+ break
667
+ }
668
+ case 'LAYOUT_RELEASE':
669
+ deliverLayoutRelease(envelope.driverId, entry.layoutRegistrationId)
670
+ break
671
+ case 'PROJECTION_STALE':
672
+ deliverProjectionStaleReport(entry.report)
673
+ break
674
+ case 'LAYOUT_SETTLE':
675
+ deliverLayoutSettled(envelope.driverId, entry.layoutAnimationId)
676
+ break
677
+ case 'DRIVER_SETTLE': {
678
+ // Proof seam witness (no-op when the hook is unset): receipt and the UI emission count
679
+ // are observed only after the envelope receipt has crossed the AB-BA task boundary.
680
+ const marker = (globalThis as ProofMarkerJsGlobal).__nativeMotionProofMarker
681
+ if (marker !== undefined) {
682
+ mirrorProofSettleEmissionToJs(uiProofSettleEmissionCount())
683
+ marker(`settle-dispatch driver=${entry.driverId} epoch=${entry.schedulerEpoch}`)
684
+ }
685
+ deliverDriverSettled(entry.driverId, entry.schedulerEpoch)
686
+ break
687
+ }
688
+ case 'DRIVER_DISPOSED':
689
+ // The UI record is gone and this envelope validated in order behind everything sealed
690
+ // before it, so the accepted drain id has nothing left to guard. Forgetting it at
691
+ // dispose() instead reset the counter under an envelope still in flight, which validated
692
+ // as "duplicate or out-of-order" and killed the app on the presence card (Android bug
693
+ // bash 2026-09-05).
694
+ acceptedFrameMailboxDrainIdByDriver.delete(entry.driverId)
695
+ break
696
+ default: {
697
+ const exhaustive: never = entry
698
+ throw new Error(`unrecognized driver-frame mailbox entry: ${String(exhaustive)}`)
699
+ }
700
+ }
701
+ }
702
+ }
703
+
704
+ // The arrival-ordered drain queue. Receipts keep the one AB-BA deferral (the flush runs one JS
705
+ // task after the native dispatch extent, entry handlers in envelope order, no second scheduling
706
+ // boundary), but the DRAIN ORDER is this queue's — never the host timer's. Every receipt used to
707
+ // defer its drain behind its OWN setTimeout(0); RN's JavaTimerManager holds timers in a
708
+ // PriorityQueue ordered by targetTime alone (JavaTimerManager.kt, no timer-id tie-break), so two
709
+ // receipts registering in the same catch-up millisecond under host contention could fire
710
+ // inverted — the later envelope then validates one ahead of the accepted counter, the counter
711
+ // never passes the gap, and every later envelope of the driver fails "duplicate or out-of-order"
712
+ // (bugbash 2026-09-08, three AUTO-walk deaths around cards 96/97 with 459 ms stretched frames).
713
+ const pendingFrameMailboxEnvelopes: UiFrameMailboxEnvelope[] = []
714
+ let frameMailboxFlushScheduled = false
715
+
716
+ function flushFrameMailboxEnvelopes(): void {
717
+ frameMailboxFlushScheduled = false
718
+ while (pendingFrameMailboxEnvelopes.length > 0) {
719
+ drainFrameMailboxEnvelope(pendingFrameMailboxEnvelopes.shift()!)
720
+ }
721
+ }
722
+
723
+ function dispatchChannelMailboxTurnEnvelope(envelope: UiFrameMailboxEnvelope): void {
724
+ pendingFrameMailboxEnvelopes.push(envelope)
725
+ if (frameMailboxFlushScheduled) return
726
+ frameMailboxFlushScheduled = true
727
+ deferUiOriginatedCallback(flushFrameMailboxEnvelopes)
728
+ }
729
+
730
+ interface PendingGestureWrite extends LivePropState {
731
+ readonly version: number
732
+ }
733
+
734
+ // Per-driver UI-runtime registry, namespaced on the worklet global by a JS-side id so two drivers never
735
+ // share state. Lazily created UI-side; never serialized back. dispose() deletes the record —
736
+ // component unmount must not strand holder outputs and graph state in the worklet global
737
+ // (review round 4 major 5).
738
+ interface UiManualGroupSlot {
739
+ lastCommittedTimestampMs: number
740
+ pendingAbsoluteTimestampMs: number
741
+ attendeeCount: number
742
+ attendees: number[]
743
+ }
744
+
745
+ interface UiGlobal {
746
+ __nativeMotionDrivers?: Record<number, UiDriverState>
747
+ // Host layout channels keep only primitive current-frame records here. It shares the driver's
748
+ // lifetime: disposal removes the record so a remounted gallery never inherits stale geometry.
749
+ __nativeMotionProjectionHostFrames?: Record<number, unknown>
750
+ // Command-edge-prefilled manual group slots. Frame lookup is peek-only.
751
+ __nativeMotionManualGroups?: Record<number, UiManualGroupSlot>
752
+ }
753
+
754
+ function uiDriverState(driverId: number): UiDriverState {
755
+ 'worklet'
756
+ const g = globalThis as UiGlobal
757
+ if (g.__nativeMotionDrivers === undefined) g.__nativeMotionDrivers = {}
758
+ let state = g.__nativeMotionDrivers[driverId]
759
+ if (state === undefined) {
760
+ state = {
761
+ driverId,
762
+ elements: [],
763
+ settleNotified: true,
764
+ loopEpoch: 0,
765
+ layout: null,
766
+ layoutSettleWatchers: [],
767
+ emission: {
768
+ elementHandles: [],
769
+ elementGenerations: [],
770
+ elementCommitSequences: [],
771
+ elementMemberCounts: [],
772
+ elementEntryCount: 0,
773
+ layoutSettledKeys: [],
774
+ layoutSettledCount: 0,
775
+ driverSettleEpoch: -1,
776
+ disposeStaged: false,
777
+ commitSequence: 0,
778
+ },
779
+ }
780
+ g.__nativeMotionDrivers[driverId] = state
781
+ }
782
+ return state
783
+ }
784
+
785
+ // @frame-path — non-creating read for the per-frame paths: after dispose() a late vsync or
786
+ // settle-edge re-check must observe "gone" — the get-or-create above would silently resurrect an
787
+ // empty record and re-leak it.
788
+ function uiDriverStatePeek(driverId: number): UiDriverState | null {
789
+ 'worklet'
790
+ const g = globalThis as UiGlobal
791
+ if (g.__nativeMotionDrivers === undefined) return null
792
+ return g.__nativeMotionDrivers[driverId] ?? null
793
+ }
794
+
795
+ // A JS error thrown across `executeOnUIRuntimeSync` is FATAL on react-native-worklets 0.5.1
796
+ // Release builds: the sync return path dereferences state the pending Hermes exception left
797
+ // behind and the process dies with SIGSEGV (fault addr 0x28, mqt_v_js) instead of rethrowing —
798
+ // no red screen, no log line (packet finding 2026-07-10: bisected to the layout-screens rewire
799
+ // 3494387, the first tree whose registry fail-loud throws crossed inside a sync worklet;
800
+ // reproduced on both platforms; the un-workletized-crossing SIGSEGV draft gains a sibling).
801
+ // Engine ops therefore NEVER throw across the boundary: the crossing worklet catches via
802
+ // `uiGuard`, marshals `{name, message}` back as plain data, and the JS side rethrows the same
803
+ // typed error through `unwrapUiOutcome`. Every caller keeps the exact fail-loud contract
804
+ // (name + message verbatim); only the transport is defensive.
805
+ //
806
+ // Shape: the crossing worklet is passed DIRECTLY to `executeOnUIRuntimeSync` (the pattern this
807
+ // file relies on everywhere), the module-scope worklet `uiGuard` (same capture class as
808
+ // uiDriverState) does the catching, and the fallible body is a UI-BORN closure created inside
809
+ // the crossing worklet — no generic `wrap(fn)` helper, so no worklet-in-closure serialization
810
+ // is ever required. The scheduled lane (`scheduleOnUI`) cannot rethrow synchronously and keeps
811
+ // its pre-existing loud UI-runtime failure via uiGuardLane — a recorded follow-up (packet).
812
+ // `key` completes the refusal's IDENTITY across the crossing. Class identity cannot be marshalled,
813
+ // so a consumer recognizes a typed refusal structurally (`isRepeatFoldRefusal`) — and it reads
814
+ // `name` AND `key`. Carrying only `name` and `message` left the shipping backend's refusal
815
+ // unrecognizable to the severity router, which then rethrew it uncaught out of a React lifecycle:
816
+ // the exact defect the router was added to prevent (T18-a round-9 review BLOCKING 1). A plain
817
+ // string rides the marshal; `undefined` means the failure named no property.
818
+ type UiOutcome<T> =
819
+ | { readonly ok: true; readonly value: T }
820
+ | {
821
+ readonly ok: false
822
+ readonly name: string
823
+ readonly message: string
824
+ readonly key: string | undefined
825
+ }
826
+
827
+ // alloc-ok: construction — reusable UI outcome slots filled in place on every crossing; the
828
+ // frame path never mints a fresh {ok,value} / {ok,name,message,key} record.
829
+ const uiGuardSuccessSlot: { ok: true; value: unknown } = { ok: true, value: undefined }
830
+ const uiGuardFailureSlot: {
831
+ ok: false
832
+ name: string
833
+ message: string
834
+ key: string | undefined
835
+ } = { ok: false, name: 'Error', message: '', key: undefined }
836
+
837
+ function uiGuard<T>(run: () => T): UiOutcome<T> {
838
+ 'worklet'
839
+ try {
840
+ uiGuardSuccessSlot.value = run()
841
+ return uiGuardSuccessSlot as UiOutcome<T>
842
+ } catch (e) {
843
+ if (!(e instanceof Error)) {
844
+ uiGuardFailureSlot.name = 'Error'
845
+ uiGuardFailureSlot.message = String(e)
846
+ uiGuardFailureSlot.key = undefined
847
+ return uiGuardFailureSlot
848
+ }
849
+ const key = (e as Error & { key?: unknown }).key
850
+ uiGuardFailureSlot.name = e.name
851
+ uiGuardFailureSlot.message = e.message
852
+ uiGuardFailureSlot.key = typeof key === 'string' ? key : undefined
853
+ return uiGuardFailureSlot
854
+ }
855
+ }
856
+
857
+ // DECLARATION ORDER IS LOAD-BEARING for every worklet below: the workletizer emits each one as
858
+ // a non-hoisted const, so a worklet may only reference worklets declared ABOVE it — a forward
859
+ // capture is `undefined` at module load under Hermes and dies as "undefined is not a function"
860
+ // on the UI runtime, with no JS stack. Pinned by check-worklet-forward-refs.test.ts.
861
+
862
+ function uiSetMirrorActive(element: UiElementState, key: string, active: boolean): void {
863
+ 'worklet'
864
+ const mirror = element.layoutMirror[key]
865
+ if (mirror !== undefined) mirror.active.value = active
866
+ }
867
+
868
+ function uiSetMirrorTarget(element: UiElementState, key: string, target: number): void {
869
+ 'worklet'
870
+ const mirror = element.layoutMirror[key]
871
+ if (mirror !== undefined) mirror.target.value = target
872
+ }
873
+
874
+ // Two-lane guard for crossings that ride `cross()`: the manual (sync) lane marshals the outcome
875
+ // back for a JS-side rethrow; the live (scheduled) lane cannot marshal synchronously — and must
876
+ // NEVER rethrow on the UI runtime (a bare std::terminate on iOS — the AUTO SIGABRT class, payload
877
+ // agent-2026-07-29-967871). A live-lane failure marshals {name, message, code} to RN through the
878
+ // same scheduleOnRN severity channel the settle dispatches ride (loud there — development throws
879
+ // on the RN side after unwind, production reports) and retires the affected element's flight:
880
+ // its props/holds clear so it settles at its committed values — the last valid visual state,
881
+ // never a half-applied frame continuing silently.
882
+ function uiGuardLane(
883
+ manual: boolean,
884
+ run: () => void,
885
+ driverId: number,
886
+ affectedHandle: number | undefined,
887
+ // Whether `run` commits ATOMICALLY — builds everything before mutating anything, so a refusal
888
+ // leaves the element exactly as it found it. Only the command lane does; `uiApplyWrite`
889
+ // re-anchors a contending generator inside its per-key mutation loop, so a refusal there is
890
+ // half-applied and MUST still retire. Passed explicitly at every site (never defaulted) so a new
891
+ // lane has to state its own answer rather than inherit a wrong one (round-10 BLOCKING 2).
892
+ commitsAtomically: boolean,
893
+ ): UiOutcome<void> {
894
+ 'worklet'
895
+ const outcome = uiGuard(run)
896
+ if (!manual && !outcome.ok) {
897
+ // REQ-DRIVER-034: a dual-authority refusal of a bound-value retarget is a designed operating
898
+ // condition of the ADMITTED pin pattern (a bound MotionValue animated on the same key), not a
899
+ // lane fault. The synchronous lane hands it to the adapter's catch-and-latch; the scheduled
900
+ // lane cannot marshal a throw back, so it delivers the refusal to the driver's JS subscribers
901
+ // (the adapter's latch) instead. The refusal throws from the write's validate-BEFORE-mutate
902
+ // phase, so the element is exactly as the refused write found it — no retire, no severity
903
+ // report while a subscriber exists, and the admitted flight runs on. The crossing stays
904
+ // fire-and-forget O(1) (REQ-DRIVER-013): one extra scheduleOnRN on the refusal path only.
905
+ if (isBoundValueWriteRefusalIdentity(outcome.name)) {
906
+ scheduleOnRN(
907
+ notifyBoundValueWriteRefusal,
908
+ driverId,
909
+ affectedHandle ?? -1,
910
+ outcome.key ?? '',
911
+ outcome.message,
912
+ )
913
+ return outcome
914
+ }
915
+ scheduleOnRN(
916
+ dispatchDriverLaneFailure,
917
+ driverId,
918
+ outcome.name,
919
+ outcome.message,
920
+ 'driver-lane-failure',
921
+ // Definite string across the marshal — `undefined` is not a value this crossing carries
922
+ // (the `releasesGesture` precedent, r8 major 24). Empty means the failure named no property.
923
+ outcome.key ?? '',
924
+ )
925
+ // A FOLD refusal from an ATOMIC lane is not a lane fault. It is a typed, per-property refusal
926
+ // raised in the build phase, so the element is exactly as the refused command found it and
927
+ // every other prop is healthy. Retiring destroyed all of them — the direct negation of the
928
+ // ratified severity law's "remaining keys proceed" (round-9 BLOCKING 2). Every other failure,
929
+ // and every failure from a lane that mutates as it goes, leaves UI state of unknown validity
930
+ // and still retires.
931
+ const perPropertyRefusal =
932
+ commitsAtomically && isRepeatFoldRefusalIdentity(outcome.name, outcome.key)
933
+ if (perPropertyRefusal) {
934
+ // Nothing to retire — but the refused command still consumed a loop epoch, and the JS side
935
+ // already re-armed the vsync callback for it. The retire used to re-arm the settle edge as a
936
+ // side effect; without it a refusal on an ALREADY-SETTLED element leaves `settleNotified`
937
+ // latched true from the previous settle, so `frameStep` never reaches its quiescent branch:
938
+ // the loop runs forever and the settle dispatch never fires (round-10 BLOCKING 1).
939
+ // PEEK, never get-or-create: a refusal must not mint a record for a driver that has been
940
+ // disposed. The retire arm is reached only with a live handle, so it cannot; this arm has no
941
+ // such guarantee once a future lane passes `commitsAtomically` (round-10 review observation).
942
+ const state = uiDriverStatePeek(driverId)
943
+ if (state !== null) state.settleNotified = false
944
+ } else if (affectedHandle !== undefined) {
945
+ const state = uiDriverState(driverId)
946
+ const element = state.elements[affectedHandle]
947
+ if (element !== undefined) {
948
+ // The retire mirrors the stop command's COMPLETE semantics (review 0f37d35ab9cd): the
949
+ // collections are readonly properties, so keys are deleted rather than the cells
950
+ // reassigned; propKeys is emptied (propKeys mirrors props at every mutation edge — a
951
+ // stale entry breaks uiStepAll's invariant); and every mirror active flag clears with
952
+ // its target reset to committed, so the layout mirror lane can END — a latched
953
+ // active=true would withhold a concurrent layout flight indefinitely. Committed values
954
+ // stay — the last valid visual state.
955
+ for (const key of Object.keys(element.props)) delete element.props[key]
956
+ for (const key of Object.keys(element.contending)) delete element.contending[key]
957
+ for (const key of Object.keys(element.heldVelocity)) delete element.heldVelocity[key]
958
+ for (const key of Object.keys(element.layoutMirror)) {
959
+ uiSetMirrorActive(element, key, false)
960
+ const committed = element.committed[key]
961
+ if (committed !== undefined) uiSetMirrorTarget(element, key, committed)
962
+ }
963
+ element.active = false
964
+ element.heldCount = 0
965
+ element.propKeys.length = 0
966
+ state.settleNotified = false // re-arm the settle edge so the retirement notifies
967
+ }
968
+ }
969
+ }
970
+ return outcome
971
+ }
972
+
973
+ // The RN-side severity-law reporter for live driver-lane failures (iOS AUTO SIGABRT Phase 2):
974
+ // scheduled from the UI lane like the settle dispatches. Development throws HERE on the RN side
975
+ // (the standing severity law); production reports through the error channel. Mirrors
976
+ // motion/severity.ts's ambient law — the driver layer stays motion-import-free, and the law is
977
+ // __DEV__-defined so the mirror cannot drift.
978
+ declare const __DEV__: boolean | undefined
979
+
980
+ function dispatchDriverLaneFailure(
981
+ driverId: number,
982
+ name: string,
983
+ message: string,
984
+ code: string,
985
+ // Empty means the failure named no property — `undefined` never crosses this marshal.
986
+ key: string,
987
+ ): void {
988
+ const error = new Error(
989
+ `worklet driver ${String(driverId)}: [${code}] ${name}: ${message}`,
990
+ ) as Error & { name: string; key?: string }
991
+ // The refusal's identity survives to the RN side too: a consumer's error channel receives the
992
+ // same `{name, key}` the synchronous lane's rethrow carries, so one predicate recognizes a fold
993
+ // refusal wherever it arrives from.
994
+ if (key !== '') {
995
+ error.name = name
996
+ error.key = key
997
+ }
998
+ // Severity delivery defers one JS task (seal r3, F1): this scheduleOnRN target runs inside
999
+ // the native UI→JS dispatch extent, and a JS throw crossing that extent is the AB-BA class —
1000
+ // the same deferral seam the settle dispatches ride. Development still throws, production
1001
+ // still reports — one task later, after the dispatcher unwinds.
1002
+ deferUiOriginatedCallback(() => {
1003
+ if (typeof __DEV__ === 'undefined' || __DEV__ !== false) throw error
1004
+ console.error(error)
1005
+ })
1006
+ }
1007
+
1008
+ // REQ-DRIVER-034: per-driver JS subscribers to the scheduled lane's dual-authority bound-value
1009
+ // refusals. Keyed by driverId because the UI→RN marshal carries primitives only; the driver's
1010
+ // `onBoundValueWriteRefusal` is the only writer and `dispose()` the only bulk deleter.
1011
+ const boundValueWriteRefusalListenersByDriverId = new Map<
1012
+ number,
1013
+ Set<(handle: number, key: string) => void>
1014
+ >()
1015
+
1016
+ /**
1017
+ * The scheduled lane's dual-authority refusal delivery (REQ-DRIVER-034). The live lane cannot
1018
+ * marshal the refusal back into the adapter's synchronous catch, so it is delivered to the
1019
+ * driver's subscribers — the adapter latches `delegateRefusesOwnedWrites` and owned writes stay
1020
+ * with the flight. A refusal with NO subscriber is a consumer riding the retarget lane without
1021
+ * its designed terminal: it reports through the same severity channel any lane failure takes.
1022
+ * Delivery defers one JS task like every UI-originated dispatch (the seal-r3 AB-BA law).
1023
+ */
1024
+ function notifyBoundValueWriteRefusal(
1025
+ driverId: number,
1026
+ handle: number,
1027
+ key: string,
1028
+ message: string,
1029
+ ): void {
1030
+ const listeners = boundValueWriteRefusalListenersByDriverId.get(driverId)
1031
+ if (listeners === undefined || listeners.size === 0) {
1032
+ dispatchDriverLaneFailure(
1033
+ driverId,
1034
+ BOUND_VALUE_WRITE_REFUSAL_NAME,
1035
+ message,
1036
+ 'driver-lane-failure',
1037
+ key,
1038
+ )
1039
+ return
1040
+ }
1041
+ deferUiOriginatedCallback(() => {
1042
+ for (const listener of Array.from(listeners)) listener(handle, key)
1043
+ })
1044
+ }
1045
+
1046
+ // The RN-side severity-law reporter for drained stale projection-tree reports (iOS AUTO SIGABRT
1047
+ // F2): a stale-tree event on a KNOWN-then-torn-down id is a ROUTINE teardown race, not a contract
1048
+ // violation — development warns, production reports an Error through the console channel, and
1049
+ // neither lane throws (the tree already no-oped the stale update at its last valid visual state).
1050
+ // THREE codes escalate (seal r7 F4, seal r8 F2, seal r9 F1) plus the ring's own overflow record
1051
+ // (seal r9 F3 — contained reports dropped before delivery): a never-registered id is a
1052
+ // wrong-forest/mistyped publish, an invalid-frame-input report is malformed input on a LIVE
1053
+ // registered node (stale ids are reported before validation ever runs), and a non-finite-output
1054
+ // report is undefined scale geometry on a live node (refused, frozen at the last valid finite
1055
+ // state) — fail-loud classes, so development throws, deferred one JS task through the same
1056
+ // seal-r3 seam as dispatchDriverLaneFailure, and production reports. The report type is a
1057
+ // structural mirror of the core tree's drain record (the worklet-layout barrel keeps the core
1058
+ // layout declaration sets pinned), and the law mirrors motion/severity.ts exactly like
1059
+ // dispatchDriverLaneFailure — the driver layer stays motion-import-free.
1060
+ function deliverProjectionStaleReport(report: {
1061
+ readonly source: string
1062
+ readonly code: string
1063
+ readonly detail: string
1064
+ }): void {
1065
+ const message = `[${report.code}] ${report.detail} (projection tree, iOS AUTO SIGABRT campaign)`
1066
+ if (
1067
+ report.code === 'projection-unregistered-node' ||
1068
+ report.code === 'projection-invalid-frame-input' ||
1069
+ report.code === 'projection-non-finite-output' ||
1070
+ report.code === 'projection-report-overflow'
1071
+ ) {
1072
+ const refusal = new Error(message)
1073
+ if (typeof __DEV__ === 'undefined' || __DEV__ !== false) throw refusal
1074
+ console.error(refusal)
1075
+ return
1076
+ }
1077
+ if (typeof __DEV__ === 'undefined' || __DEV__ !== false) {
1078
+ console.warn(message)
1079
+ return
1080
+ }
1081
+ console.error(new Error(message))
1082
+ }
1083
+
1084
+ function unwrapUiOutcome<T>(outcome: UiOutcome<T>): T {
1085
+ if (outcome.ok) return outcome.value
1086
+ const error = new Error(outcome.message) as Error & { key?: string }
1087
+ error.name = outcome.name
1088
+ // Reattached so the rethrown error carries the same identity the reference backend's typed
1089
+ // refusal does — `{name, key}` is what a consumer branches on (`isRepeatFoldRefusal`).
1090
+ if (outcome.key !== undefined) error.key = outcome.key
1091
+ throw error
1092
+ }
1093
+
1094
+ // Async half of `cross`: named worklet so scheduleOnUI does not mint an anonymous UI lambda
1095
+ // that captures another worklet (diamond-DAG serialization class). `work` is a schedule arg.
1096
+ function invokeAsyncCrossWork(work: (synchronous: boolean) => UiOutcome<void>): void {
1097
+ 'worklet'
1098
+ work(false)
1099
+ }
1100
+
1101
+ // Live count of UI-runtime driver records — the disposal path's observable (device proofs assert
1102
+ // it stays bounded across remounts; a leak here grows monotonically, review round 4 major 5).
1103
+ export function uiDriverStateCount(): number {
1104
+ return unwrapUiOutcome(
1105
+ executeOnUIRuntimeSync(() => {
1106
+ 'worklet'
1107
+ return uiGuard(() => {
1108
+ const g = globalThis as UiGlobal
1109
+ if (g.__nativeMotionDrivers === undefined) return 0
1110
+ return Object.keys(g.__nativeMotionDrivers).length
1111
+ })
1112
+ })(),
1113
+ )
1114
+ }
1115
+
1116
+ // Settle-channel forensics (iOS AUTO SIGABRT settle-gap investigation, seal-r15 F2; seal-r16
1117
+ // F2 moves the public surface behind `@unrulysystems/native-motion/verification`). The JS-side
1118
+ // marker is installed by the proof app's ExamplesScreen; unset elsewhere, so every push is a
1119
+ // no-op in ordinary builds. UI-side counters are PRIMITIVE UI-global cells that never ride the
1120
+ // channel under test — and they only mutate when the proof app has armed the witness (the
1121
+ // marker install at driver mount arms UI; ordinary consumers never arm, so settle/completion
1122
+ // paths stay zero-cost).
1123
+ interface ProofSettleChannelUiGlobal {
1124
+ __nativeMotionProofWitnessArmed?: boolean
1125
+ __nativeMotionProofSettleEmissions?: number
1126
+ __nativeMotionProofLayoutCallbackInvocations?: number
1127
+ __nativeMotionProofLayoutCallbackEpochDrops?: number
1128
+ __nativeMotionProofLayoutCallbackCancelled?: number
1129
+ __nativeMotionProofLayoutOnCompleteScheduled?: number
1130
+ }
1131
+ type ProofMarkerJsGlobal = {
1132
+ __nativeMotionProofMarker?: (marker: string) => void
1133
+ __nativeMotionLastProofSettleEmissions?: number
1134
+ __nativeMotionLastProofLayoutCallback?: string
1135
+ }
1136
+
1137
+ function isProofMarkerInstalled(): boolean {
1138
+ return (globalThis as ProofMarkerJsGlobal).__nativeMotionProofMarker !== undefined
1139
+ }
1140
+
1141
+ // Arm the UI-runtime witness cells. Called from driver mount when the proof marker is present,
1142
+ // and from the verification entry for isolation tests / verification screens.
1143
+ export function armUiProofWitnesses(): void {
1144
+ unwrapUiOutcome(
1145
+ executeOnUIRuntimeSync(() => {
1146
+ 'worklet'
1147
+ return uiGuard(() => {
1148
+ ;(globalThis as ProofSettleChannelUiGlobal).__nativeMotionProofWitnessArmed = true
1149
+ })
1150
+ })(),
1151
+ )
1152
+ }
1153
+
1154
+ // Mirror the latest UI settle-emission count onto the JS global + marker. Called from the
1155
+ // JS-side settle dispatch (not from the UI worklet) so the worklet-closures gate never sees a
1156
+ // non-worklet helper capture on the frame path. The public gallery reads the JS cell without
1157
+ // importing engine-internal readers (purity ban: only App.tsx may import `/verification`).
1158
+ function mirrorProofSettleEmissionToJs(count: number): void {
1159
+ const g = globalThis as ProofMarkerJsGlobal
1160
+ g.__nativeMotionLastProofSettleEmissions = count
1161
+ g.__nativeMotionProofMarker?.(`ui-settle-emissions=${count}`)
1162
+ }
1163
+
1164
+ // The UI-side settle-emission witness count, read synchronously (never through the channel
1165
+ // under test). Verification/internal only — not on the consumer-facing package root.
1166
+ export function uiProofSettleEmissionCount(): number {
1167
+ return unwrapUiOutcome(
1168
+ executeOnUIRuntimeSync(() => {
1169
+ 'worklet'
1170
+ return uiGuard(
1171
+ () => (globalThis as ProofSettleChannelUiGlobal).__nativeMotionProofSettleEmissions ?? 0,
1172
+ )
1173
+ })(),
1174
+ )
1175
+ }
1176
+
1177
+ // The layout-completion callback's witness counters packed for one proof-log line —
1178
+ // invocations / epoch-guard drops / cancellations / onComplete scheduled (seal-r15 F2). A
1179
+ // zero invocation count names Reanimated's completion delivery; invocations > scheduled names
1180
+ // our guard or the cancel path; scheduled > JS receipt names the crossing.
1181
+ export function uiProofLayoutCallbackWitness(): string {
1182
+ return unwrapUiOutcome(
1183
+ executeOnUIRuntimeSync(() => {
1184
+ 'worklet'
1185
+ return uiGuard(() => {
1186
+ const g = globalThis as ProofSettleChannelUiGlobal
1187
+ return `${g.__nativeMotionProofLayoutCallbackInvocations ?? 0}/${g.__nativeMotionProofLayoutCallbackEpochDrops ?? 0}/${g.__nativeMotionProofLayoutCallbackCancelled ?? 0}/${g.__nativeMotionProofLayoutOnCompleteScheduled ?? 0}`
1188
+ })
1189
+ })(),
1190
+ )
1191
+ }
1192
+
1193
+ // Last-pushed JS-side copies (filled by scheduleOnRN when armed). The proof gallery reads these
1194
+ // without a package import; verification screens can use the sync readers above instead.
1195
+ export function lastPushedProofSettleEmissionCount(): number {
1196
+ return (globalThis as ProofMarkerJsGlobal).__nativeMotionLastProofSettleEmissions ?? 0
1197
+ }
1198
+
1199
+ export function lastPushedProofLayoutCallbackWitness(): string {
1200
+ return (globalThis as ProofMarkerJsGlobal).__nativeMotionLastProofLayoutCallback ?? '0/0/0/0'
1201
+ }
1202
+
1203
+ function uiLayoutSlot(state: UiDriverState, spring: SpringConfig | undefined): UiLayoutState {
1204
+ 'worklet'
1205
+ if (state.layout === null) state.layout = createUiLayoutState(spring, state.driverId)
1206
+ return state.layout
1207
+ }
1208
+
1209
+ function uiElementAt(state: UiDriverState, handle: number): UiElementState {
1210
+ 'worklet'
1211
+ const element = state.elements[handle]
1212
+ if (element === undefined) {
1213
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN through the lane's
1214
+ // designed transport (Phase 2); this throw is the payload, never the delivery.
1215
+ throw new Error(
1216
+ `unknown element handle ${String(handle)}: it was never registered on this driver.`,
1217
+ )
1218
+ }
1219
+ return element
1220
+ }
1221
+
1222
+ // Generator construction ON the UI runtime — the reference driver's startGenerator/retargetGenerator,
1223
+ // verbatim semantics (spring default, tween positional reseed, spring retarget carries velocity).
1224
+ // `velocity` is the REQ-DRIVER-023 seed (the gesture release's platform handoff, unscaled); a tween is
1225
+ // position-based and ignores it by construction — explicit 0 so the contract reads at the seam.
1226
+ // A CONTENDING target drifts FROM the committed finger value — never a backward jump off the finger
1227
+ // (REQ-DRIVER-026 / G-INV-2, review major 25; mirror of the reference driver's anchorContentionToLive).
1228
+ // A keyframe array's non-null FIRST element would restart near keyframes[0], rewinding the drift;
1229
+ // re-seed the first keyframe from-current (null-first, R8-F2) so the trajectory runs the live value →
1230
+ // remaining keyframes. A scalar and an already-null-first array are unchanged.
1231
+ function uiAnchorContentionToLive(to: PropTargetTo): PropTargetTo {
1232
+ 'worklet'
1233
+ if (typeof to === 'number' || to[0] === null) return to
1234
+ return Object.freeze([null, ...to.slice(1)])
1235
+ }
1236
+
1237
+ const UI_KEYFRAME_DEFAULT_DURATION_MS = 300
1238
+ const UI_KEYFRAME_VELOCITY_SAMPLE_MS = 5
1239
+
1240
+ interface UiKeyframeSegment {
1241
+ readonly start: number
1242
+ readonly end: number
1243
+ readonly to: number
1244
+ readonly generator: Generator | null
1245
+ }
1246
+
1247
+ function uiKeyframesValueAt(
1248
+ segments: readonly UiKeyframeSegment[],
1249
+ first: number,
1250
+ firstTime: number,
1251
+ last: number,
1252
+ lastTime: number,
1253
+ elapsedMs: number,
1254
+ ): number {
1255
+ 'worklet'
1256
+ if (elapsedMs <= firstTime) return first
1257
+ if (elapsedMs >= lastTime) return last
1258
+ for (let index = 0; index < segments.length; index++) {
1259
+ const segment = segments[index]
1260
+ if (segment === undefined) break
1261
+ if (elapsedMs <= segment.end) {
1262
+ if (segment.generator === null) return segment.to
1263
+ return segment.generator.sample(elapsedMs - segment.start).value
1264
+ }
1265
+ }
1266
+ return last
1267
+ }
1268
+
1269
+ // UI-safe counterpart of core's public keyframesGenerator. `uiStartGenerator` receives only targets
1270
+ // and configs that the JS-side adapter has already validated and normalized, so it must not cross
1271
+ // core's raw public-boundary capture/refusal helpers. File-tagging that public module made the
1272
+ // Worklets plugin rewrite ordinary typed-error classes to absent `__classFactory` calls on device.
1273
+ // This constructor retains the pinned timing semantics for valid prepared input while closing that
1274
+ // boundary: each segment delegates easing math to the already UI-safe timingGenerator.
1275
+ //
1276
+ // alloc-ok: command edge. Segment generators and their mutable samples are constructed once; the
1277
+ // returned sampler mutates only its reused output record on the frame path.
1278
+ export function uiKeyframesGenerator(
1279
+ keyframes: readonly (number | null)[],
1280
+ config: KeyframesConfig,
1281
+ from: number,
1282
+ ): Generator {
1283
+ 'worklet'
1284
+ const count = keyframes.length
1285
+ const duration = config.duration ?? UI_KEYFRAME_DEFAULT_DURATION_MS
1286
+ const values: number[] = []
1287
+ for (let index = 0; index < count; index++) {
1288
+ const keyframe = keyframes[index]
1289
+ if (keyframe === undefined)
1290
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN (Phase 2); a total
1291
+ // target guard the JS-side boundary already enforces, never a UI delivery.
1292
+ throw new Error(`prepared keyframes target is missing index ${String(index)}`)
1293
+ values.push(keyframe === null ? from : keyframe)
1294
+ }
1295
+
1296
+ const segments: UiKeyframeSegment[] = []
1297
+ for (let index = 0; index < count - 1; index++) {
1298
+ const segmentFrom = values[index]
1299
+ const segmentTo = values[index + 1]
1300
+ if (segmentFrom === undefined || segmentTo === undefined)
1301
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN (Phase 2); a total
1302
+ // target guard the JS-side boundary already enforces, never a UI delivery.
1303
+ throw new Error(`prepared keyframes target is incomplete at segment ${String(index)}`)
1304
+ const start = keyframeOffsetAt(config.times, index, count) * duration
1305
+ const end = keyframeOffsetAt(config.times, index + 1, count) * duration
1306
+ const span = end - start
1307
+ segments.push({
1308
+ start,
1309
+ end,
1310
+ to: segmentTo,
1311
+ // A duplicated offset is legal: core treats it as an instantaneous transition. Avoid
1312
+ // timingGenerator's t/0 path and preserve the same exact `to` result here.
1313
+ generator:
1314
+ span <= 0
1315
+ ? null
1316
+ : timingGenerator(segmentTo, {
1317
+ duration: span,
1318
+ ease: keyframeSegmentEase(config.ease, index),
1319
+ })({ from: segmentFrom, velocity: 0 }),
1320
+ })
1321
+ }
1322
+
1323
+ const firstSegment = segments[0]
1324
+ const lastSegment = segments[segments.length - 1]
1325
+ const first = firstSegment === undefined ? from : values[0]!
1326
+ const last = lastSegment === undefined ? first : lastSegment.to
1327
+ const firstTime = firstSegment?.start ?? 0
1328
+ const lastTime = lastSegment?.end ?? 0
1329
+
1330
+ const out = { value: first, velocity: 0, done: false }
1331
+ return {
1332
+ sample(elapsedMs: number) {
1333
+ const current = uiKeyframesValueAt(segments, first, firstTime, last, lastTime, elapsedMs)
1334
+ const previousElapsedMs = Math.max(elapsedMs - UI_KEYFRAME_VELOCITY_SAMPLE_MS, 0)
1335
+ const elapsedDeltaMs = elapsedMs - previousElapsedMs
1336
+ out.value = elapsedMs >= duration ? last : current
1337
+ out.velocity = elapsedDeltaMs
1338
+ ? ((current -
1339
+ uiKeyframesValueAt(segments, first, firstTime, last, lastTime, previousElapsedMs)) /
1340
+ elapsedDeltaMs) *
1341
+ 1000
1342
+ : 0
1343
+ out.done = elapsedMs >= duration
1344
+ return out
1345
+ },
1346
+ }
1347
+ }
1348
+
1349
+ // R8-F1 (UI-runtime mirror of the reference driver's springKeyframeGenerator): a keyframe ARRAY under
1350
+ // an EXPLICIT spring. A spring interpolates EXACTLY two keyframes — the pinned Motion throws for more.
1351
+ // Two run spring physics from keyframes[0] (the explicit origin; null first reads `from`, R8-F2) to
1352
+ // keyframes[1]; a longer array FAILS LOUD naming the successor so it never reaches the spring generator.
1353
+ function uiSpringKeyframeGenerator(
1354
+ config: SpringConfig,
1355
+ keyframes: readonly (number | null)[],
1356
+ from: number,
1357
+ velocity: number,
1358
+ ): Generator {
1359
+ 'worklet'
1360
+ if (keyframes.length !== 2) {
1361
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — the R8-F1 refusal marshals to RN through the
1362
+ // lane's designed transport (Phase 2); the message names the successor, never a UI delivery.
1363
+ throw new Error(
1364
+ `a spring transition cannot interpolate through ${keyframes.length} keyframes — a spring ` +
1365
+ 'animates exactly two (origin → target). Keyframe index 2 (the successor) is unreachable; ' +
1366
+ 'use a tween for a multi-keyframe array (REQ-API-033, R8-F1).',
1367
+ )
1368
+ }
1369
+ const origin = keyframes[0]
1370
+ const target = keyframes[1]
1371
+ // Validation guarantees a length-2 array whose only legal null is index 0 and whose target parses;
1372
+ // the guard keeps this TOTAL under noUncheckedIndexedAccess (never hit for a valid target).
1373
+ if (origin === undefined || target === undefined || target === null) {
1374
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN (Phase 2); a total
1375
+ // shape guard the JS-side validation already enforces, never a UI delivery.
1376
+ throw new Error(
1377
+ 'malformed two-keyframe spring array — a non-null numeric target is required (REQ-API-033).',
1378
+ )
1379
+ }
1380
+ const seedFrom = origin === null ? from : origin
1381
+ return resolveSpringGenerator(target, config)({ from: seedFrom, velocity })
1382
+ }
1383
+
1384
+ // The transition's own resolved iteration length in ms, or null when only the scan can measure it —
1385
+ // the UI mirror of the reference driver's `knownIterationDurationMs` (the pin's `calculatedDuration`
1386
+ // rule). Configs were resolved JS-side, so this reads them directly.
1387
+ function uiKnownIterationDurationMs(transition: PlainTransition, to: PropTargetTo): number | null {
1388
+ 'worklet'
1389
+ // Narrows the plain-data union off the inertia arm (and the U7a instant arm, which never
1390
+ // reaches the fold that consumes this) as well as answering the lane's first question.
1391
+ if (transition.kind === 'inertia' || transition.kind === 'instant') return null
1392
+ // Core's ONE lane rule, never re-derived here — this backend held the second parallel copy that
1393
+ // the T18-a fix-up review found drifting (round 3 MAJOR).
1394
+ const lane = iterationMeasurementLane(
1395
+ typeof to === 'number',
1396
+ transition.explicitSpring,
1397
+ transition.kind === 'tween',
1398
+ )
1399
+ if (lane === 'keyframes') return transition.keyframes.duration ?? null
1400
+ if (lane === 'timing') return transition.config.duration ?? null
1401
+ const resolved = resolveSpring(transition.config)
1402
+ return resolved.calculatedDuration !== null && resolved.calculatedDuration > 0
1403
+ ? resolved.calculatedDuration
1404
+ : null
1405
+ }
1406
+
1407
+ function uiUnrepeatedGenerator(
1408
+ transition: PlainTransition,
1409
+ to: PropTargetTo,
1410
+ from: number,
1411
+ velocity: number,
1412
+ reversed: boolean,
1413
+ ): Generator {
1414
+ 'worklet'
1415
+ // U7a: the instant lane — no trajectory generator runs; the final keyframe commits past t=0
1416
+ // (the command edge seeds committed from sample(0), so nothing moves at command time). Never
1417
+ // folded, so `reversed` is always false here; the arm stays total over the union regardless.
1418
+ if (transition.kind === 'instant') {
1419
+ return instantGenerator(instantFinalKeyframe(to))({ from, velocity: 0 })
1420
+ }
1421
+ if (typeof to !== 'number' && reversed && transition.kind !== 'inertia') {
1422
+ // Resolve from-current nulls BEFORE reversing (R8-F2), mirroring the reference driver: the pin
1423
+ // mirrors the RESOLVED keyframes, and a null that slid to the tail would re-read `from` at the
1424
+ // wrong end. `times`/`ease` are deliberately untouched — the pin reverses keyframes only.
1425
+ const resolvedKeyframes: number[] = []
1426
+ for (let index = to.length - 1; index >= 0; index--) {
1427
+ const value = to[index]
1428
+ resolvedKeyframes.push(value === null || value === undefined ? from : value)
1429
+ }
1430
+ const mirroredFrom = resolvedKeyframes[0] ?? from
1431
+ if (transition.explicitSpring) {
1432
+ return uiSpringKeyframeGenerator(
1433
+ transition.config,
1434
+ resolvedKeyframes,
1435
+ mirroredFrom,
1436
+ -velocity,
1437
+ )
1438
+ }
1439
+ return uiKeyframesGenerator(resolvedKeyframes, transition.keyframes, mirroredFrom)
1440
+ }
1441
+ if (typeof to !== 'number') {
1442
+ if (transition.kind === 'inertia') {
1443
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — a keyframe array under an inertia transition
1444
+ // is a command-shape defect the release never produces (R16); fail loud, never a silent
1445
+ // spring/keyframe misroute.
1446
+ throw new Error(
1447
+ 'a keyframe array cannot run under an inertia transition (REQ-GESTURE-023, R16).',
1448
+ )
1449
+ }
1450
+ // A keyframe ARRAY routes by `explicitSpring`, NOT `kind` — the UI-runtime mirror of the reference
1451
+ // driver's `transition.type === 'spring'` (REQ-API-033, R8-F1; no cross-engine skew). Only a
1452
+ // LITERAL spring interpolates a keyframe array as spring (two → physics; >2 → fail loud naming the
1453
+ // successor); a tween OR the DEFAULT (an omitted type, whether it resolved 'tween' by REQ-TIMING-006
1454
+ // or stayed the 'spring' fallback) routes the
1455
+ // array THROUGH the keyframes generator (position-based, seed velocity dropped, null-first reads
1456
+ // `from`). Configs were resolved JS-side (toSpringConfig/toKeyframesConfig), identical here.
1457
+ if (transition.explicitSpring)
1458
+ return uiSpringKeyframeGenerator(transition.config, to, from, velocity)
1459
+ return uiKeyframesGenerator(to, transition.keyframes, from)
1460
+ }
1461
+ if (transition.kind === 'inertia') {
1462
+ // R16 (REQ-GESTURE-023): the two-phase generator recomputes its ideal from the seed and owns
1463
+ // the walls; `to` is the mirror terminal only. The seed velocity is the platform release
1464
+ // velocity, momentum-gated upstream (REQ-GESTURE-027).
1465
+ return inertiaGenerator({
1466
+ power: transition.config.power,
1467
+ timeConstant: transition.config.timeConstant,
1468
+ bounceStiffness: transition.config.bounceStiffness,
1469
+ bounceDamping: transition.config.bounceDamping,
1470
+ restDelta: transition.config.restDelta,
1471
+ restSpeed: transition.config.restSpeed,
1472
+ ...(transition.config.min !== undefined ? { min: transition.config.min } : {}),
1473
+ ...(transition.config.max !== undefined ? { max: transition.config.max } : {}),
1474
+ })({ from, velocity })
1475
+ }
1476
+ // The scalar mirrored leg: origin and target swap and the seed velocity negates (the pin builds
1477
+ // its second generator with `velocity: -velocity`). Mirrors the reference driver exactly.
1478
+ const scalarTo = reversed ? from : to
1479
+ const scalarFrom = reversed ? to : from
1480
+ const scalarVelocity = reversed ? -velocity : velocity
1481
+ if (transition.kind === 'tween') {
1482
+ // transition-default-selection F5: a zero-length timing base takes the pin's
1483
+ // makeAnimationInstant SHAPE — hold the seed at t ≤ 0, commit the final keyframe past it —
1484
+ // mirroring the reference driver's `unrepeatedGenerator`. Only repeatDelay-bearing bags reach
1485
+ // this (a zero duration without one resolved to kind:'instant' JS-side); it keeps the fold's
1486
+ // zero-length plays from committing at the command edge.
1487
+ if (transition.config.duration === 0) {
1488
+ return instantGenerator(scalarTo)({ from: scalarFrom, velocity: 0 })
1489
+ }
1490
+ return timingGenerator(scalarTo, transition.config)({ from: scalarFrom, velocity: 0 })
1491
+ }
1492
+ return resolveSpringGenerator(
1493
+ scalarTo,
1494
+ transition.config,
1495
+ )({
1496
+ from: scalarFrom,
1497
+ velocity: scalarVelocity,
1498
+ })
1499
+ }
1500
+
1501
+ // T18-a (REQ-DRIVER-031): the UI-runtime entry that applies the repeat fold. It calls core's SHARED
1502
+ // `buildRepeatedGenerator` with its own unrepeated construction — the identical call the reference
1503
+ // driver makes. Parity with that backend is enforced by `driverParity.differential.test.ts`, which
1504
+ // executes both over the same commands and compares trajectories frame for frame; sharing the call
1505
+ // is not enough on its own (both seams shared it and still skewed on retarget). An inertia release
1506
+ // never carries authored options, so it never folds.
1507
+ function uiStartGenerator(
1508
+ transition: PlainTransition,
1509
+ to: PropTargetTo,
1510
+ from: number,
1511
+ velocity: number,
1512
+ ): Generator {
1513
+ 'worklet'
1514
+ // U7a: the instant lane never folds (the pin's shouldSkip ignores repeat outright), so it
1515
+ // narrows off with inertia here; its authored delay still crosses through `delayMs` below.
1516
+ const fold =
1517
+ transition.kind === 'inertia' || transition.kind === 'instant' ? null : transition.repeat
1518
+ // T18-b (REQ-TIMING-004 / REQ-DRIVER-032): the delay rebase wraps AROUND the fold-or-generator,
1519
+ // through core's SHARED `delayGenerator` — the identical call the reference driver makes at
1520
+ // both phases. An inertia release carries no authored delay (its arm omits `delayMs`, the same
1521
+ // totality law as `repeat`). Parity is held by the executing differential floor, not by the
1522
+ // shared call alone.
1523
+ const delayMs = transition.kind === 'inertia' ? 0 : transition.delayMs
1524
+ if (fold === null)
1525
+ return delayGenerator(uiUnrepeatedGenerator(transition, to, from, velocity, false), delayMs)
1526
+ // Legs built EAGERLY and passed as values (never a factory invoked on UI — the wrong-runtime
1527
+ // crash class the closures gate fails closed on); identical shape to the reference driver.
1528
+ const leg: RepeatLeg =
1529
+ fold.repeatType === 'mirror'
1530
+ ? { type: 'mirror', mirrored: uiUnrepeatedGenerator(transition, to, from, velocity, true) }
1531
+ : { type: fold.repeatType }
1532
+ const folded = buildRepeatedGenerator(
1533
+ fold.repeat,
1534
+ leg,
1535
+ fold.repeatDelayMs,
1536
+ uiUnrepeatedGenerator(transition, to, from, velocity, false),
1537
+ uiKnownIterationDurationMs(transition, to),
1538
+ )
1539
+ if (folded instanceof Error) {
1540
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — the T18-a L3 refusal marshals to RN through the
1541
+ // lane's designed transport, naming the unexecutable fold rather than running a NaN trajectory.
1542
+ throw uiRepeatRefusal(fold.repeat, folded)
1543
+ }
1544
+ return delayGenerator(folded, delayMs)
1545
+ }
1546
+
1547
+ function uiUnrepeatedRetargetGenerator(
1548
+ transition: PlainTransition,
1549
+ live: PropStep,
1550
+ to: number,
1551
+ ): Generator {
1552
+ 'worklet'
1553
+ // U7a: the instant lane at the retarget seam — commit the new target past t=0 from the live
1554
+ // value. The retarget's own authored delay wraps this in uiRetargetGenerator, exactly like
1555
+ // the reference driver's retargetGenerator.
1556
+ if (transition.kind === 'instant') {
1557
+ return instantGenerator(to)({ from: live.value, velocity: 0 })
1558
+ }
1559
+ if (transition.kind === 'tween') {
1560
+ // F5: the zero-length timing base takes the instant SHAPE here too — the same routing as
1561
+ // `uiUnrepeatedGenerator`, seeded from the live value (no cross-seam skew).
1562
+ if (transition.config.duration === 0) {
1563
+ return instantGenerator(to)({ from: live.value, velocity: 0 })
1564
+ }
1565
+ return timingGenerator(to, transition.config)({ from: live.value, velocity: 0 })
1566
+ }
1567
+ if (transition.kind === 'inertia') {
1568
+ // R16: a retarget into an inertia transition re-seeds the two-phase generator from the LIVE
1569
+ // generator's sample — the same continuity law as a spring retarget (C0/C1).
1570
+ const at = live.generator.sample(live.elapsed)
1571
+ return inertiaGenerator({
1572
+ power: transition.config.power,
1573
+ timeConstant: transition.config.timeConstant,
1574
+ bounceStiffness: transition.config.bounceStiffness,
1575
+ bounceDamping: transition.config.bounceDamping,
1576
+ restDelta: transition.config.restDelta,
1577
+ restSpeed: transition.config.restSpeed,
1578
+ ...(transition.config.min !== undefined ? { min: transition.config.min } : {}),
1579
+ ...(transition.config.max !== undefined ? { max: transition.config.max } : {}),
1580
+ })({ from: at.value, velocity: at.velocity })
1581
+ }
1582
+ return retargetSpring(live.generator, live.elapsed, to, transition.config)
1583
+ }
1584
+
1585
+ /**
1586
+ * T18-a fix-up (review BLOCKER): the retarget seam carries the fold, identically to
1587
+ * `uiStartGenerator`. An endless repeat is never `done`, so every changed scalar animate key
1588
+ * arrives HERE rather than at `start` — dropping the fold stopped the loop permanently on the
1589
+ * first retarget. Declared below `uiUnrepeatedRetargetGenerator` because the workletizer emits
1590
+ * each worklet as a non-hoisted const (see the declaration-order note above).
1591
+ */
1592
+ function uiRetargetGenerator(transition: PlainTransition, live: PropStep, to: number): Generator {
1593
+ 'worklet'
1594
+ const base = uiUnrepeatedRetargetGenerator(transition, live, to)
1595
+ // U7a: the instant lane never folds — same narrowing as uiStartGenerator.
1596
+ const fold =
1597
+ transition.kind === 'inertia' || transition.kind === 'instant' ? null : transition.repeat
1598
+ // T18-b (REQ-DRIVER-032): a retarget applies its OWN delay — the pin starts a fresh animation
1599
+ // per retarget — wrapping the (possibly folded) trajectory AROUND the fold, identical to the
1600
+ // reference driver's retargetGenerator. A mid-delay retarget reseeds from the held first
1601
+ // keyframe at velocity 0, which is exactly what the outgoing delay wrapper reports while held.
1602
+ const delayMs = transition.kind === 'inertia' ? 0 : transition.delayMs
1603
+ if (fold === null) return delayGenerator(base, delayMs)
1604
+ const leg: RepeatLeg =
1605
+ fold.repeatType === 'mirror'
1606
+ ? { type: 'mirror', mirrored: uiUnrepeatedGenerator(transition, to, live.value, 0, true) }
1607
+ : { type: fold.repeatType }
1608
+ const folded = buildRepeatedGenerator(
1609
+ fold.repeat,
1610
+ leg,
1611
+ fold.repeatDelayMs,
1612
+ base,
1613
+ uiKnownIterationDurationMs(transition, to),
1614
+ )
1615
+ if (folded instanceof Error) {
1616
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — the L3 refusal marshals to RN through the
1617
+ // lane's designed transport, exactly as the start seam does.
1618
+ throw uiRepeatRefusal(fold.repeat, folded)
1619
+ }
1620
+ return delayGenerator(folded, delayMs)
1621
+ }
1622
+
1623
+ // Clear the gesture hold on ONE prop (the matching release/stop owns it now). Idempotent — clearing
1624
+ // an unheld prop is a no-op, so heldCount stays exact (review r7 major 23). Command-edge only.
1625
+ function uiReleaseHold(element: UiElementState, key: string): void {
1626
+ 'worklet'
1627
+ if (element.heldVelocity[key] !== undefined) {
1628
+ delete element.heldVelocity[key]
1629
+ element.heldCount--
1630
+ }
1631
+ }
1632
+
1633
+ function uiTerminalTarget(to: PropTargetTo): number {
1634
+ 'worklet'
1635
+ if (typeof to === 'number') return to
1636
+ for (let index = to.length - 1; index >= 0; index--) {
1637
+ const value = to[index]
1638
+ if (value !== null && value !== undefined) return value
1639
+ }
1640
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN (Phase 2); a total
1641
+ // shape guard the JS-side boundary already enforces, never a UI delivery.
1642
+ throw new Error('a property command needs a numeric terminal target for its layout mirror.')
1643
+ }
1644
+
1645
+ // alloc-ok: command-edge — grow an element's frame member slots to cover every committed key the
1646
+ // frame can stage (propKeys plus the path-claimed x/y/pathRotation extras). Called wherever
1647
+ // propKeys is rebuilt; uiEmitFrameValues fills by index and never grows these in-frame.
1648
+ function uiPrefillElementFrameSlots(element: UiElementState): void {
1649
+ 'worklet'
1650
+ const required = element.propKeys.length + 3
1651
+ while (element.frameKeys.length < required) {
1652
+ element.frameKeys.push('')
1653
+ element.frameValues.push(0)
1654
+ }
1655
+ }
1656
+
1657
+ function uiApplyCommand(state: UiDriverState, handle: number, command: PlainCommand): void {
1658
+ 'worklet'
1659
+ const element = uiElementAt(state, handle)
1660
+ switch (command.kind) {
1661
+ case 'start': {
1662
+ // Only a gesture RELEASE (`releasesGesture`) or a `stop` ends a hold. A declarative start on a
1663
+ // gesture-held prop CONTENDS instead of being skipped (REQ-DRIVER-026, Motion's shared-value
1664
+ // model): it seeds a generator that drifts the live value while the finger still owns the prop,
1665
+ // so a mid-drag `animate` change moves the element and the release reads the drifted origin. The
1666
+ // finger's release velocity is preserved (uiLiveFor held-velocity precedence, REQ-DRIVER-025);
1667
+ // the hold clears only at release/stop. Mirror of the reference driver.
1668
+ const releasesGesture = command.releasesGesture === true
1669
+ // PHASE 1 — build every prop's generator WITHOUT mutating UI state; a construction that throws
1670
+ // (an explicit spring on a >2-keyframe array, R8-F1) aborts the WHOLE command before ANY prop
1671
+ // commits (fail-closed atomicity — mirror of the reference driver; review major 19).
1672
+ const plans: {
1673
+ key: string
1674
+ stillHeld: boolean
1675
+ seededValue: number
1676
+ generator: Generator
1677
+ to: PropTargetTo
1678
+ transition: PlainTransition
1679
+ }[] = []
1680
+ for (const key of Object.keys(command.targets)) {
1681
+ const target = command.targets[key]! // key came from Object.keys
1682
+ // A gesture-RELEASE start takes over the prop (hold clears in phase 2), so it is treated as NOT
1683
+ // still-held and uses the command's `from`. A CONTENDING start on a prop the finger still holds
1684
+ // drifts from the CURRENT live value (never `from`) so committed does not jump off the finger.
1685
+ const stillHeld = !releasesGesture && element.heldVelocity[key] !== undefined
1686
+ const from = stillHeld ? element.committed[key]! : target.from
1687
+ // The per-prop transition wins over the command-level one for THIS prop (R8 M2), mirror of the
1688
+ // reference driver. The prepared target's transition is complete; no backend fallback remains.
1689
+ const transition = target.transition
1690
+ // A still-held (contending) start drifts from the finger value: seed the first keyframe
1691
+ // from-current so it never rewinds to keyframes[0] (major 25); a non-held start plays the
1692
+ // array's own origin verbatim. Mirror of the reference driver.
1693
+ const generator = uiStartGenerator(
1694
+ transition,
1695
+ stillHeld ? uiAnchorContentionToLive(target.to) : target.to,
1696
+ from,
1697
+ target.velocity,
1698
+ )
1699
+ // Seed committed/value/shared from the generator's TICK-0, not the raw `from` (REQ-API-033 seed
1700
+ // law; review major dfeaa4bf4bcf). A scalar generator samples `from` at t=0 (no-op). A
1701
+ // still-held start keeps the finger-authoritative committed. sample(0) is a pure read.
1702
+ const seededValue = stillHeld ? from : generator.sample(0).value
1703
+ plans.push({ key, stillHeld, seededValue, generator, to: target.to, transition })
1704
+ }
1705
+ // PHASE 2 — commit every plan. No throw path here, so UI state mutates atomically.
1706
+ for (const plan of plans) {
1707
+ if (releasesGesture) uiReleaseHold(element, plan.key)
1708
+ element.props[plan.key] = {
1709
+ generator: plan.generator,
1710
+ elapsed: 0,
1711
+ value: plan.seededValue,
1712
+ done: false,
1713
+ }
1714
+ element.committed[plan.key] = plan.seededValue
1715
+ const shared = element.shared[plan.key]
1716
+ if (shared !== undefined) shared.value = plan.seededValue
1717
+ uiSetMirrorTarget(element, plan.key, uiTerminalTarget(plan.to))
1718
+ uiSetMirrorActive(element, plan.key, true)
1719
+ // Track the contention target so a later finger write re-anchors this generator (major 29);
1720
+ // `plan.to` was already captured at command time (major 20). A released/non-held start owns the
1721
+ // prop outright, so clear any prior contention.
1722
+ if (plan.stillHeld)
1723
+ element.contending[plan.key] = { to: plan.to, transition: plan.transition }
1724
+ else delete element.contending[plan.key]
1725
+ }
1726
+ // Command-edge rebuild of the frame loop's iteration list (REQ-DRIVER-021): Object.keys here
1727
+ // is O(1) per command; in uiStepAll it was per element per vsync.
1728
+ if (command.path !== undefined) {
1729
+ const geometry = command.path
1730
+ const generator = uiStartGenerator(geometry.transition, 1000, 0, 0)
1731
+ const seeded = generator.sample(0).value
1732
+ element.path = {
1733
+ progress: { generator, elapsed: 0, value: seeded, done: false },
1734
+ geometry: {
1735
+ fromX: geometry.fromX,
1736
+ fromY: geometry.fromY,
1737
+ toX: geometry.toX,
1738
+ toY: geometry.toY,
1739
+ peak: geometry.peak,
1740
+ signedStrength: geometry.signedStrength,
1741
+ rotationScale: geometry.rotationScale,
1742
+ },
1743
+ sample: { x: 0, y: 0, pathRotation: 0 },
1744
+ scratch: { control: { x: 0, y: 0 }, sample: { x: 0, y: 0, rotate: 0 } },
1745
+ }
1746
+ delete element.props.x
1747
+ delete element.props.y
1748
+ delete element.props.pathRotation
1749
+ applyPathProgressInto(
1750
+ seeded,
1751
+ element.path.geometry,
1752
+ false,
1753
+ element.path.sample,
1754
+ element.path.scratch,
1755
+ )
1756
+ const sample = element.path.sample
1757
+ if (element.committed.x !== undefined) {
1758
+ element.committed.x = sample.x
1759
+ const sharedX = element.shared.x
1760
+ if (sharedX !== undefined) sharedX.value = sample.x
1761
+ }
1762
+ if (element.committed.y !== undefined) {
1763
+ element.committed.y = sample.y
1764
+ const sharedY = element.shared.y
1765
+ if (sharedY !== undefined) sharedY.value = sample.y
1766
+ }
1767
+ if (geometry.rotationScale && element.committed.pathRotation !== undefined) {
1768
+ element.committed.pathRotation = sample.pathRotation
1769
+ const sharedR = element.shared.pathRotation
1770
+ if (sharedR !== undefined) sharedR.value = sample.pathRotation
1771
+ }
1772
+ } else if (element.path !== null) {
1773
+ if (element.path.geometry.rotationScale && element.committed.pathRotation !== undefined) {
1774
+ element.committed.pathRotation = 0
1775
+ const sharedR = element.shared.pathRotation
1776
+ if (sharedR !== undefined) sharedR.value = 0
1777
+ }
1778
+ element.path = null
1779
+ }
1780
+ element.propKeys = Object.keys(element.props)
1781
+ uiPrefillElementFrameSlots(element)
1782
+ element.active = element.propKeys.length > 0 || element.heldCount > 0 || element.path !== null
1783
+ state.settleNotified = false // re-arm the settle edge
1784
+ break
1785
+ }
1786
+ case 'retarget': {
1787
+ // PHASE 1 — build every prop's generator WITHOUT mutating UI state, exactly as `start` does.
1788
+ // A retarget can be refused from BELOW (a fold re-measures its iteration from the LIVE value,
1789
+ // so a `repeat` that `start` accepted can be unexecutable here), and the severity router's
1790
+ // per-key re-issue is built on "a refused command is as if it had never been issued". This
1791
+ // backend committed per key inside the build loop, so a refusal on a later key left every
1792
+ // earlier key already re-seeded — the reference backend's invariant, absent on the backend
1793
+ // that ships (T18-a round-9 review BLOCKING 2).
1794
+ const plans: {
1795
+ key: string
1796
+ to: number
1797
+ from: number
1798
+ generator: Generator
1799
+ held: boolean
1800
+ }[] = []
1801
+ for (const key of Object.keys(command.targets)) {
1802
+ const to = command.targets[key]! // key came from Object.keys
1803
+ const transition = command.targetTransitions?.[key] ?? command.transition
1804
+ // A retarget is never the gesture release. A retarget on a gesture-HELD prop CONTENDS
1805
+ // (REQ-DRIVER-026): it seeds a fresh generator from the finger-authoritative COMMITTED value
1806
+ // (not the possibly-stale generator value — a finger write may have advanced committed past it,
1807
+ // review r10 major 29) toward the new target, and records the contention so a later write
1808
+ // re-anchors it. A non-held prop retargets normally with velocity continuity. Same fail-loud
1809
+ // rule: a retarget of a prop never started/registered is a caller bug, never a silent 0. Mirror
1810
+ // of the reference driver.
1811
+ const held = element.heldVelocity[key] !== undefined
1812
+ if (held) {
1813
+ const from = element.committed[key]! // held ⇒ written ⇒ committed exists
1814
+ plans.push({
1815
+ key,
1816
+ to,
1817
+ from,
1818
+ generator: uiStartGenerator(transition, to, from, 0),
1819
+ held,
1820
+ })
1821
+ continue
1822
+ }
1823
+ const live = element.props[key]
1824
+ if (live !== undefined && !live.done) {
1825
+ plans.push({
1826
+ key,
1827
+ to,
1828
+ from: live.value,
1829
+ generator: uiRetargetGenerator(transition, live, to),
1830
+ held,
1831
+ })
1832
+ continue
1833
+ }
1834
+ const base = live !== undefined ? live.value : element.committed[key]
1835
+ if (base === undefined) {
1836
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN through the
1837
+ // lane's designed transport (Phase 2); this throw is the payload, never the delivery.
1838
+ throw new Error(
1839
+ `cannot retarget '${key}': it was never registered or started on this element.`,
1840
+ )
1841
+ }
1842
+ plans.push({
1843
+ key,
1844
+ to,
1845
+ from: base,
1846
+ generator: uiStartGenerator(transition, to, base, 0),
1847
+ held,
1848
+ })
1849
+ }
1850
+ // PHASE 2 — commit every plan. No throw path here, so UI state mutates atomically.
1851
+ for (const plan of plans) {
1852
+ if (plan.held)
1853
+ element.contending[plan.key] = {
1854
+ to: plan.to,
1855
+ transition: command.targetTransitions?.[plan.key] ?? command.transition,
1856
+ }
1857
+ else delete element.contending[plan.key]
1858
+ element.props[plan.key] = {
1859
+ generator: plan.generator,
1860
+ elapsed: 0,
1861
+ value: plan.from,
1862
+ done: false,
1863
+ }
1864
+ element.committed[plan.key] = plan.from
1865
+ uiSetMirrorTarget(element, plan.key, plan.to)
1866
+ uiSetMirrorActive(element, plan.key, true)
1867
+ }
1868
+ element.propKeys = Object.keys(element.props) // command-edge rebuild (REQ-DRIVER-021)
1869
+ uiPrefillElementFrameSlots(element)
1870
+ // Recompute from what remains in flight: a held retarget seeds a contending generator and a held
1871
+ // element stays active regardless (heldCount > 0). Mirror of the reference driver.
1872
+ element.active = element.propKeys.length > 0 || element.heldCount > 0
1873
+ state.settleNotified = false // re-arm the settle edge
1874
+ break
1875
+ }
1876
+ case 'stop': {
1877
+ // Halt every animation AND release every gesture hold (stop = halt everything).
1878
+ if (element.path !== null) {
1879
+ if (element.path.geometry.rotationScale && element.committed.pathRotation !== undefined) {
1880
+ element.committed.pathRotation = 0
1881
+ const sharedR = element.shared.pathRotation
1882
+ if (sharedR !== undefined) sharedR.value = 0
1883
+ }
1884
+ element.path = null
1885
+ }
1886
+ for (const key of Object.keys(element.props)) delete element.props[key]
1887
+ for (const key of Object.keys(element.heldVelocity)) delete element.heldVelocity[key]
1888
+ for (const key of Object.keys(element.contending)) delete element.contending[key]
1889
+ for (const key of Object.keys(element.layoutMirror)) {
1890
+ uiSetMirrorActive(element, key, false)
1891
+ const committed = element.committed[key]
1892
+ if (committed !== undefined) uiSetMirrorTarget(element, key, committed)
1893
+ }
1894
+ element.heldCount = 0
1895
+ element.propKeys.length = 0
1896
+ element.active = false
1897
+ break
1898
+ }
1899
+ }
1900
+ }
1901
+
1902
+ // The gesture write lane ON the UI runtime (REQ-DRIVER-024): commit event-driven values and
1903
+ // interrupt any in-flight animation on the written props through ONE entry point — committed
1904
+ // state, the shared values the style binds, and animation state can never skew. EVENT-edge, not
1905
+ // frame-path: Object.keys here is O(1) per recognizer event (the uiApplyCommand precedent), and
1906
+ // the frame loop never calls this.
1907
+ function uiApplyWrite(
1908
+ state: UiDriverState,
1909
+ handle: number,
1910
+ values: Readonly<Record<string, number>>,
1911
+ velocities: Readonly<Record<string, number>> | undefined,
1912
+ writeVersion: number,
1913
+ eventShape: WorkletDriverWriteShape,
1914
+ ): void {
1915
+ 'worklet'
1916
+ const element = uiElementAt(state, handle)
1917
+ // Validate ALL keys before mutating ANY — a malformed write never half-applies (fail-closed).
1918
+ for (const key of Object.keys(values)) {
1919
+ if (!(key in element.committed)) {
1920
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN through the lane's
1921
+ // designed transport (Phase 2); this throw is the payload, never the delivery.
1922
+ throw new Error(`cannot write '${key}': it was never registered on this element.`)
1923
+ }
1924
+ if (
1925
+ eventShape === 'bound-value-retarget' &&
1926
+ (element.heldVelocity[key] !== undefined || element.props[key] !== undefined)
1927
+ ) {
1928
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN through the lane's
1929
+ // designed transport (Phase 2); this throw is the payload, never the delivery. The NAMED
1930
+ // refusal (validation phase, nothing mutated) is what the live arm routes to the adapter's
1931
+ // refusal handling instead of retiring an admitted flight (REQ-DRIVER-034).
1932
+ throw uiBoundValueWriteRefusal(key)
1933
+ }
1934
+ }
1935
+ for (const key of Object.keys(values)) {
1936
+ const finger = values[key]!
1937
+ if (eventShape === 'bound-value-retarget') {
1938
+ element.committed[key] = finger
1939
+ const shared = element.shared[key]
1940
+ if (shared !== undefined) shared.value = finger
1941
+ uiSetMirrorTarget(element, key, finger)
1942
+ uiSetMirrorActive(element, key, false)
1943
+ continue
1944
+ }
1945
+ if (element.heldVelocity[key] === undefined) element.heldCount++ // count a NEW hold once
1946
+ element.committed[key] = finger
1947
+ element.writeVersion[key] = writeVersion
1948
+ const shared = element.shared[key]
1949
+ if (shared !== undefined) shared.value = finger
1950
+ uiSetMirrorTarget(element, key, finger)
1951
+ uiSetMirrorActive(element, key, true)
1952
+ // Refresh the per-prop held platform velocity so uiLiveFor reports it while held (G-INV-3 C1 seam,
1953
+ // held-velocity precedence over any contending generator); omitted ⇒ 0.
1954
+ element.heldVelocity[key] = velocities?.[key] ?? 0
1955
+ // Reconcile the prop's generator with the finger write (REQ-DRIVER-026). A LIVE declarative
1956
+ // contention is RE-ANCHORED to the freshly-committed finger value so it drifts FROM here — never a
1957
+ // backward jump to the generator's stale trajectory (review r10 major 29, G-INV-2). Otherwise the
1958
+ // finger owns the value outright: a plain grab interrupts the pre-existing animation
1959
+ // (REQ-GESTURE-012), and a SETTLED contention is finished, so its generator is dropped (else
1960
+ // uiStepAll keeps forcing its value). Mirror of the reference driver.
1961
+ const contention = element.contending[key]
1962
+ const prop = element.props[key]
1963
+ if (contention !== undefined && prop !== undefined && !prop.done) {
1964
+ element.props[key] = {
1965
+ // Re-anchor FROM the finger: seed a keyframe array's first element from-current so the drift
1966
+ // continues from `finger`, never rewinding to keyframes[0] (major 25). Mirror of reference.
1967
+ generator: uiStartGenerator(
1968
+ contention.transition,
1969
+ uiAnchorContentionToLive(contention.to),
1970
+ finger,
1971
+ 0,
1972
+ ),
1973
+ elapsed: 0,
1974
+ value: finger,
1975
+ done: false,
1976
+ }
1977
+ } else {
1978
+ if (prop !== undefined) delete element.props[key]
1979
+ delete element.contending[key]
1980
+ }
1981
+ }
1982
+ element.propKeys = Object.keys(element.props) // command-edge rebuild (REQ-DRIVER-021)
1983
+ uiPrefillElementFrameSlots(element)
1984
+ if (eventShape === 'bound-value-retarget') {
1985
+ // A MotionValue set has no release edge. It commits through the same write lane, but is
1986
+ // immediately quiescent unless another property was already active.
1987
+ element.active = element.propKeys.length > 0 || element.heldCount > 0
1988
+ return
1989
+ }
1990
+ // A gesture write holds the element unsettled (G-INV-7): a finger is down driving it, so it stays
1991
+ // active with no animation until the matching release command clears the hold — never a spurious
1992
+ // settle edge mid-drag (the reviewer-reproduced re-grab defect).
1993
+ element.active = true
1994
+ }
1995
+
1996
+ // The grab live-read ON the UI runtime (REQ-DRIVER-025). An in-flight prop re-samples its
1997
+ // generator at the current elapsed — pure by elapsed time (REQ-CORE-001), scalars copied out
1998
+ // immediately (the sample record is generator-owned and reused). Settled/never-animated ⇒
1999
+ // velocity 0. Gesture-edge only, never frame-executed.
2000
+ function uiLiveFor(
2001
+ state: UiDriverState,
2002
+ handle: number,
2003
+ key: string,
2004
+ ): LivePropState & { readonly writeVersion: number } {
2005
+ 'worklet'
2006
+ const element = uiElementAt(state, handle)
2007
+ // HELD-VELOCITY PRECEDENCE (REQ-DRIVER-025): a held (finger-down) prop reports the platform finger
2008
+ // velocity its write carried EVEN when a contending declarative generator is in flight
2009
+ // (REQ-DRIVER-026) — the release seed is the finger's number, never the generator's. The value is
2010
+ // the live committed number (finger position, or the contending animation's drift). Read BEFORE the
2011
+ // generator branch so contention can't leak the generator velocity into the release seam (per-prop
2012
+ // since review r7 major 23). Mirror of the reference driver.
2013
+ const heldV = element.heldVelocity[key]
2014
+ if (heldV !== undefined) {
2015
+ const base = element.committed[key]
2016
+ if (base === undefined) {
2017
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN through the lane's
2018
+ // designed transport (Phase 2); this throw is the payload, never the delivery.
2019
+ throw new Error(`cannot read '${key}': it was never registered on this element.`)
2020
+ }
2021
+ return { value: base, velocity: heldV, writeVersion: element.writeVersion[key] ?? 0 }
2022
+ }
2023
+ const live = element.props[key]
2024
+ if (live !== undefined && !live.done) {
2025
+ const sample = live.generator.sample(live.elapsed)
2026
+ return {
2027
+ value: sample.value,
2028
+ velocity: sample.velocity,
2029
+ writeVersion: element.writeVersion[key] ?? 0,
2030
+ }
2031
+ }
2032
+ const base = element.committed[key]
2033
+ if (base === undefined) {
2034
+ // ui-throw-ok: guarded by uiGuard/uiGuardLane — failures marshal to RN through the lane's
2035
+ // designed transport (Phase 2); this throw is the payload, never the delivery.
2036
+ throw new Error(`cannot read '${key}': it was never registered on this element.`)
2037
+ }
2038
+ // A settled or never-animated non-held prop has velocity 0 (REQ-DRIVER-025).
2039
+ return { value: base, velocity: 0, writeVersion: element.writeVersion[key] ?? 0 }
2040
+ }
2041
+
2042
+ // @frame-path — advance every active element by one frame (core's pure step), mirror into shared
2043
+ // values, quiesce settled elements. Returns whether ANY element is still in flight. Indexed loops
2044
+ // over the command-edge-maintained propKeys list: for..of iterators and Object.keys arrays per
2045
+ // element per vsync are frame-executed garbage (REQ-DRIVER-021).
2046
+ function uiStepAll(state: UiDriverState, elapsedMs: number): boolean {
2047
+ 'worklet'
2048
+ let anyElementActive = false
2049
+ for (let e = 0; e < state.elements.length; e++) {
2050
+ const element = state.elements[e]!
2051
+ if (!element.active) continue
2052
+ let anyPropActive = false
2053
+ for (let i = 0; i < element.propKeys.length; i++) {
2054
+ const key = element.propKeys[i]!
2055
+ const prop = element.props[key]! // propKeys mirrors props at every command edge
2056
+ stepProp(prop, elapsedMs)
2057
+ element.committed[key] = prop.value
2058
+ const shared = element.shared[key]
2059
+ if (shared !== undefined) shared.value = prop.value
2060
+ if (prop.done && element.heldVelocity[key] === undefined) {
2061
+ uiSetMirrorTarget(element, key, prop.value)
2062
+ uiSetMirrorActive(element, key, false)
2063
+ }
2064
+ if (!prop.done) anyPropActive = true
2065
+ }
2066
+ if (element.path !== null) {
2067
+ const path = element.path
2068
+ stepProp(path.progress, elapsedMs)
2069
+ applyPathProgressInto(
2070
+ path.progress.value,
2071
+ path.geometry,
2072
+ path.progress.done,
2073
+ path.sample,
2074
+ path.scratch,
2075
+ )
2076
+ const sample = path.sample
2077
+ if (element.committed.x !== undefined) {
2078
+ element.committed.x = sample.x
2079
+ const sharedX = element.shared.x
2080
+ if (sharedX !== undefined) sharedX.value = sample.x
2081
+ }
2082
+ if (element.committed.y !== undefined) {
2083
+ element.committed.y = sample.y
2084
+ const sharedY = element.shared.y
2085
+ if (sharedY !== undefined) sharedY.value = sample.y
2086
+ }
2087
+ if (path.geometry.rotationScale && element.committed.pathRotation !== undefined) {
2088
+ element.committed.pathRotation = sample.pathRotation
2089
+ const sharedR = element.shared.pathRotation
2090
+ if (sharedR !== undefined) sharedR.value = sample.pathRotation
2091
+ }
2092
+ if (path.progress.done) element.path = null
2093
+ else anyPropActive = true
2094
+ }
2095
+ // Quiesce once every prop settles (REQ-DRIVER-020) — UNLESS a gesture still holds a prop (a
2096
+ // finger is down, no animation but unsettled, G-INV-7). heldCount is O(1) so this @frame-path
2097
+ // check allocates nothing (REQ-DRIVER-021). A held element stays active until the matching
2098
+ // release clears it, so no spurious settle edge fires mid-drag.
2099
+ if (!anyPropActive && element.heldCount === 0) element.active = false
2100
+ else anyElementActive = true
2101
+ }
2102
+ return anyElementActive
2103
+ }
2104
+
2105
+ // T23 C (@frame-path): fill the changed-element prefix of the reusable emission transaction.
2106
+ // Public records do not exist on UI: this path copies into command-edge-prefilled primitive slots,
2107
+ // and the single envelope seal serializes them after every participant has committed.
2108
+ function uiEmitFrameValues(state: UiDriverState): void {
2109
+ 'worklet'
2110
+ const emission = state.emission
2111
+ for (let i = 0; i < state.elements.length; i++) {
2112
+ const element = state.elements[i]!
2113
+ if (!element.onUpdateArmed) continue
2114
+ let changed = false
2115
+ for (let k = 0; k < element.propKeys.length; k++) {
2116
+ const key = element.propKeys[k]!
2117
+ if (element.lastNotified[key] !== element.committed[key]) {
2118
+ changed = true
2119
+ break
2120
+ }
2121
+ }
2122
+ // U8: path claims x/y/pathRotation out of `props` (they are not in propKeys). Still
2123
+ // emit them — the pin's onUpdate sees path-driven latest values.
2124
+ if (!changed) {
2125
+ if (element.committed.x !== undefined && element.lastNotified.x !== element.committed.x)
2126
+ changed = true
2127
+ else if (element.committed.y !== undefined && element.lastNotified.y !== element.committed.y)
2128
+ changed = true
2129
+ else if (
2130
+ element.committed.pathRotation !== undefined &&
2131
+ element.lastNotified.pathRotation !== element.committed.pathRotation
2132
+ )
2133
+ changed = true
2134
+ }
2135
+ if (!changed) continue
2136
+ let memberCount = 0
2137
+ for (let k = 0; k < element.propKeys.length; k++) {
2138
+ const key = element.propKeys[k]!
2139
+ const value = element.committed[key]!
2140
+ element.frameKeys[memberCount] = key
2141
+ element.frameValues[memberCount] = value
2142
+ memberCount += 1
2143
+ element.lastNotified[key] = value
2144
+ }
2145
+ if (element.committed.x !== undefined && element.props.x === undefined) {
2146
+ element.frameKeys[memberCount] = 'x'
2147
+ element.frameValues[memberCount] = element.committed.x
2148
+ memberCount += 1
2149
+ element.lastNotified.x = element.committed.x
2150
+ }
2151
+ if (element.committed.y !== undefined && element.props.y === undefined) {
2152
+ element.frameKeys[memberCount] = 'y'
2153
+ element.frameValues[memberCount] = element.committed.y
2154
+ memberCount += 1
2155
+ element.lastNotified.y = element.committed.y
2156
+ }
2157
+ if (element.committed.pathRotation !== undefined && element.props.pathRotation === undefined) {
2158
+ element.frameKeys[memberCount] = 'pathRotation'
2159
+ element.frameValues[memberCount] = element.committed.pathRotation
2160
+ memberCount += 1
2161
+ element.lastNotified.pathRotation = element.committed.pathRotation
2162
+ }
2163
+ const entryIndex = emission.elementEntryCount
2164
+ emission.elementHandles[entryIndex] = i
2165
+ emission.elementGenerations[entryIndex] = element.registrationGeneration
2166
+ emission.elementCommitSequences[entryIndex] = emission.commitSequence + 1
2167
+ emission.elementMemberCounts[entryIndex] = memberCount
2168
+ emission.elementEntryCount += 1
2169
+ }
2170
+ }
2171
+
2172
+ function uiStageSettledLayoutWatchers(state: UiDriverState): void {
2173
+ 'worklet'
2174
+ if (state.layout === null) return
2175
+ let retainedCount = 0
2176
+ for (let watcherIndex = 0; watcherIndex < state.layoutSettleWatchers.length; watcherIndex += 1) {
2177
+ const watcher = state.layoutSettleWatchers[watcherIndex]!
2178
+ if (state.layout.registry.isSettled(watcher.id)) {
2179
+ const stagedIndex = state.emission.layoutSettledCount
2180
+ state.emission.layoutSettledKeys[stagedIndex] = watcher.key
2181
+ state.emission.layoutSettledCount += 1
2182
+ } else {
2183
+ state.layoutSettleWatchers[retainedCount] = watcher
2184
+ retainedCount += 1
2185
+ }
2186
+ }
2187
+ state.layoutSettleWatchers.length = retainedCount
2188
+ }
2189
+
2190
+ // alloc-ok:mailbox-envelope-emission — this is the one conditional UI serialization edge. Every
2191
+ // frame producer above fills reusable primitive slots; only a demanded non-empty turn constructs
2192
+ // the closed transport records and crosses once to the named JS receipt.
2193
+ function uiSealFrameEmission(
2194
+ state: UiDriverState,
2195
+ staleReports:
2196
+ | readonly {
2197
+ readonly source: string
2198
+ readonly code: string
2199
+ readonly detail: string
2200
+ }[]
2201
+ | null,
2202
+ ): void {
2203
+ 'worklet'
2204
+ const emission = state.emission
2205
+ const layoutReleaseCount = state.layout?.releaseCount ?? 0
2206
+ const staleReportCount = staleReports?.length ?? 0
2207
+ const driverSettleCount = emission.driverSettleEpoch < 0 ? 0 : 1
2208
+ const disposeCount = emission.disposeStaged ? 1 : 0
2209
+ const eventCount =
2210
+ emission.elementEntryCount +
2211
+ layoutReleaseCount +
2212
+ staleReportCount +
2213
+ emission.layoutSettledCount +
2214
+ driverSettleCount +
2215
+ disposeCount
2216
+ if (eventCount === 0) return
2217
+
2218
+ const mailboxDrainId = emission.commitSequence + 1
2219
+ const entries = Array.from({ length: eventCount }) as UiFrameMailboxEntry[]
2220
+ let entryIndex = 0
2221
+ for (let stagedIndex = 0; stagedIndex < emission.elementEntryCount; stagedIndex += 1) {
2222
+ const handle = emission.elementHandles[stagedIndex]!
2223
+ const element = state.elements[handle]!
2224
+ const memberCount = emission.elementMemberCounts[stagedIndex]!
2225
+ const values = Array.from({ length: memberCount }) as UiFrameMailboxElementMember[]
2226
+ for (let memberIndex = 0; memberIndex < memberCount; memberIndex += 1) {
2227
+ values[memberIndex] = {
2228
+ key: element.frameKeys[memberIndex]!,
2229
+ kind: 'number',
2230
+ value: element.frameValues[memberIndex]!,
2231
+ }
2232
+ }
2233
+ entries[entryIndex] = {
2234
+ kind: 'ELEMENT_HOST_COMMIT',
2235
+ index: entryIndex,
2236
+ driverId: state.driverId,
2237
+ handleId: handle,
2238
+ registrationGeneration: emission.elementGenerations[stagedIndex]!,
2239
+ commitSequence: emission.elementCommitSequences[stagedIndex]!,
2240
+ values,
2241
+ }
2242
+ entryIndex += 1
2243
+ }
2244
+ if (state.layout !== null) {
2245
+ for (let releaseIndex = 0; releaseIndex < layoutReleaseCount; releaseIndex += 1) {
2246
+ entries[entryIndex] = {
2247
+ kind: 'LAYOUT_RELEASE',
2248
+ index: entryIndex,
2249
+ layoutRegistrationId: state.layout.releaseKeys[releaseIndex]!,
2250
+ }
2251
+ entryIndex += 1
2252
+ }
2253
+ }
2254
+ if (staleReports !== null) {
2255
+ for (let reportIndex = 0; reportIndex < staleReportCount; reportIndex += 1) {
2256
+ entries[entryIndex] = {
2257
+ kind: 'PROJECTION_STALE',
2258
+ index: entryIndex,
2259
+ report: staleReports[reportIndex]!,
2260
+ }
2261
+ entryIndex += 1
2262
+ }
2263
+ }
2264
+ for (let settledIndex = 0; settledIndex < emission.layoutSettledCount; settledIndex += 1) {
2265
+ entries[entryIndex] = {
2266
+ kind: 'LAYOUT_SETTLE',
2267
+ index: entryIndex,
2268
+ layoutAnimationId: emission.layoutSettledKeys[settledIndex]!,
2269
+ }
2270
+ entryIndex += 1
2271
+ }
2272
+ if (emission.driverSettleEpoch >= 0) {
2273
+ entries[entryIndex] = {
2274
+ kind: 'DRIVER_SETTLE',
2275
+ index: entryIndex,
2276
+ driverId: state.driverId,
2277
+ schedulerEpoch: emission.driverSettleEpoch,
2278
+ }
2279
+ entryIndex += 1
2280
+ }
2281
+ if (emission.disposeStaged) {
2282
+ entries[entryIndex] = { kind: 'DRIVER_DISPOSED', index: entryIndex, driverId: state.driverId }
2283
+ }
2284
+
2285
+ const endIndex = eventCount - 1
2286
+ const envelope: UiFrameMailboxEnvelope = {
2287
+ kind: 'CHANNEL_MAILBOX_TURN_ENVELOPE',
2288
+ mailboxOrigin: 'UI',
2289
+ mailboxDrainId,
2290
+ driverId: state.driverId,
2291
+ eventCount,
2292
+ endIndex,
2293
+ entries,
2294
+ closingMarker: { kind: 'CHANNEL_MAILBOX_END', index: endIndex },
2295
+ }
2296
+
2297
+ // Close/reset before the host call: test hosts may invoke scheduleOnRN synchronously, and
2298
+ // callback reentry belongs to a later transaction even under that deliberately eager seam.
2299
+ emission.elementEntryCount = 0
2300
+ emission.layoutSettledCount = 0
2301
+ emission.driverSettleEpoch = -1
2302
+ emission.disposeStaged = false
2303
+ if (state.layout !== null) state.layout.releaseCount = 0
2304
+ scheduleOnRN(dispatchChannelMailboxTurnEnvelope, envelope)
2305
+ // The drain id is consumed only once the envelope has crossed. Advancing the counter at seal
2306
+ // start let a seal whose host crossing failed skip an id the JS side never receives, and every
2307
+ // later envelope validated as "duplicate or out-of-order" (bugbash 2026-09-08, AUTO walk).
2308
+ emission.commitSequence = mailboxDrainId
2309
+ }
2310
+
2311
+ function uiManualGroupPeek(groupId: number): UiManualGroupSlot | null {
2312
+ 'worklet'
2313
+ const g = globalThis as UiGlobal
2314
+ if (g.__nativeMotionManualGroups === undefined) return null
2315
+ return g.__nativeMotionManualGroups[groupId] ?? null
2316
+ }
2317
+
2318
+ function uiPrefillManualGroupSlot(groupId: number, attendeeCapacity: number): void {
2319
+ 'worklet'
2320
+ const g = globalThis as UiGlobal
2321
+ if (g.__nativeMotionManualGroups === undefined) g.__nativeMotionManualGroups = {}
2322
+ const existing = g.__nativeMotionManualGroups[groupId]
2323
+ if (existing !== undefined) {
2324
+ while (existing.attendees.length < attendeeCapacity) existing.attendees.push(-1)
2325
+ return
2326
+ }
2327
+ const attendees: number[] = []
2328
+ for (let i = 0; i < attendeeCapacity; i += 1) attendees.push(-1)
2329
+ g.__nativeMotionManualGroups[groupId] = {
2330
+ lastCommittedTimestampMs: 0,
2331
+ pendingAbsoluteTimestampMs: 0,
2332
+ attendeeCount: 0,
2333
+ attendees,
2334
+ }
2335
+ }
2336
+
2337
+ function uiPrefillManualGroupSlotGuarded(
2338
+ groupId: number,
2339
+ attendeeCapacity: number,
2340
+ ): UiOutcome<void> {
2341
+ 'worklet'
2342
+ return uiGuard(() => {
2343
+ uiPrefillManualGroupSlot(groupId, attendeeCapacity)
2344
+ })
2345
+ }
2346
+
2347
+ function uiCopyManualAttendeesIntoSlots(
2348
+ groupId: number,
2349
+ orderedAttendees: readonly number[],
2350
+ ): void {
2351
+ 'worklet'
2352
+ const group = uiManualGroupPeek(groupId)
2353
+ if (group === null) return
2354
+ const count = orderedAttendees.length
2355
+ if (count > group.attendees.length) {
2356
+ // ui-throw-ok: guarded by uiGuard (uiExecuteManualFrame) — the manual lane crosses
2357
+ // synchronously, so the JS caller unwraps this as a loud lifecycle defect: a group step was
2358
+ // handed more attendees than its command-edge prefill sized.
2359
+ throw new Error('manual frame attendee batch exceeds preallocated group capacity.')
2360
+ }
2361
+ for (let i = 0; i < count; i += 1) group.attendees[i] = orderedAttendees[i]!
2362
+ group.attendeeCount = count
2363
+ }
2364
+
2365
+ function uiBeginManualFrame(
2366
+ groupId: number,
2367
+ elapsedMs: number,
2368
+ orderedAttendees: readonly number[],
2369
+ ): void {
2370
+ 'worklet'
2371
+ uiCopyManualAttendeesIntoSlots(groupId, orderedAttendees)
2372
+ const group = uiManualGroupPeek(groupId)
2373
+ if (group === null) return
2374
+ group.pendingAbsoluteTimestampMs = group.lastCommittedTimestampMs + elapsedMs
2375
+ for (let i = 0; i < group.attendeeCount; i += 1) {
2376
+ uiDriverStatePeek(group.attendees[i]!)
2377
+ }
2378
+ }
2379
+
2380
+ function uiInvokeManualParticipant(groupId: number, elapsedMs: number, driverId: number): void {
2381
+ 'worklet'
2382
+ const state = uiDriverStatePeek(driverId)
2383
+ if (state === null) return
2384
+ if (uiManualGroupPeek(groupId) === null) return
2385
+ // The caller's elapsed primitive steps the props directly. Deriving it as a difference of two
2386
+ // accumulated absolute timestamps drifts by ULPs, and a delay or repeat-fold boundary that the
2387
+ // reference driver crosses at an exact frame then lands one frame late here
2388
+ // (driverParity.differential.test.ts, the repeatDelay and endless-repeat retarget rows).
2389
+ uiStepAll(state, elapsedMs)
2390
+ if (state.layout !== null) {
2391
+ uiLayoutStep(state.layout, elapsedMs)
2392
+ }
2393
+ }
2394
+
2395
+ function uiRevokeManualParticipant(groupId: number, driverId: number): void {
2396
+ 'worklet'
2397
+ void groupId
2398
+ uiDriverStatePeek(driverId)
2399
+ }
2400
+
2401
+ function uiCommitManualFrame(groupId: number): void {
2402
+ 'worklet'
2403
+ const group = uiManualGroupPeek(groupId)
2404
+ if (group === null) return
2405
+ group.lastCommittedTimestampMs = group.pendingAbsoluteTimestampMs
2406
+ }
2407
+
2408
+ function uiExecuteManualFrame(
2409
+ groupId: number,
2410
+ elapsedMs: number,
2411
+ orderedAttendees: readonly number[],
2412
+ ): UiOutcome<void> {
2413
+ 'worklet'
2414
+ return uiGuard(() => {
2415
+ uiBeginManualFrame(groupId, elapsedMs, orderedAttendees)
2416
+ const group = uiManualGroupPeek(groupId)
2417
+ if (group === null) return
2418
+ for (let i = 0; i < group.attendeeCount; i += 1) {
2419
+ const driverId = group.attendees[i]!
2420
+ if (uiDriverStatePeek(driverId) === null) {
2421
+ uiRevokeManualParticipant(groupId, driverId)
2422
+ continue
2423
+ }
2424
+ uiInvokeManualParticipant(groupId, elapsedMs, driverId)
2425
+ const state = uiDriverStatePeek(driverId)
2426
+ if (state === null) continue
2427
+ uiEmitFrameValues(state)
2428
+ let staleReports:
2429
+ | readonly {
2430
+ readonly source: string
2431
+ readonly code: string
2432
+ readonly detail: string
2433
+ }[]
2434
+ | null = null
2435
+ if (state.layout !== null) {
2436
+ staleReports = state.layout.projection.tree.drainStaleReports()
2437
+ uiStageSettledLayoutWatchers(state)
2438
+ }
2439
+ uiSealFrameEmission(state, staleReports)
2440
+ }
2441
+ uiCommitManualFrame(groupId)
2442
+ })
2443
+ }
2444
+
2445
+ class ManualFrameGroup {
2446
+ constructor(
2447
+ readonly groupId: number,
2448
+ readonly participantDriverIds: number[],
2449
+ ) {}
2450
+
2451
+ get size(): number {
2452
+ return this.participantDriverIds.length
2453
+ }
2454
+
2455
+ step(elapsedMs: number, invocationOrder?: readonly number[]): void {
2456
+ const orderedAttendees = invocationOrder ?? this.participantDriverIds
2457
+ // alloc-ok:manual-crossing — one frozen primitive attendee batch per group step
2458
+ const frozen = Object.freeze(orderedAttendees.slice())
2459
+ // The elapsed primitive crosses as authored; the UI slot owns the absolute clock so every
2460
+ // participant steps by exactly what the caller advanced (no timestamp round trip).
2461
+ unwrapUiOutcome(executeOnUIRuntimeSync(uiExecuteManualFrame)(this.groupId, elapsedMs, frozen))
2462
+ }
2463
+ }
2464
+
2465
+ export interface WorkletDriverOptions {
2466
+ // Fired on the JS thread once per in-flight→settled edge (every element quiesced, frame callback
2467
+ // deactivated). One O(1) UI→JS crossing per edge — never per frame.
2468
+ readonly onAllSettled?: (() => void) | undefined
2469
+ // When true the driver never runs a frame source; time advances only through explicit
2470
+ // `step(elapsedMs)` calls with SYNCHRONOUS command crossings. This is the deterministic mode the
2471
+ // on-device behavioral checks drive. Live drivers come from `useWorkletDriver` (REQ-DRIVER-011).
2472
+ readonly manualStepping?: boolean
2473
+ // The layout-identity flight + crossfade spring (one config drives both — registry law). Fixed
2474
+ // at the layout slot's lazy creation; the registry default applies when omitted.
2475
+ readonly layoutSpring?: SpringConfig
2476
+ }
2477
+
2478
+ // One layout holder's animated-style outputs: the top-left-anchored transform correction, the
2479
+ // crossfade opacity, and z-lift as 0/1 (the screen maps it to zIndex/elevation). Written UI-side
2480
+ // every frame while the layout engine is in flight — never from JS.
2481
+ export interface LayoutIdentityHolderShared {
2482
+ readonly translateX: SharedValue<number>
2483
+ readonly translateY: SharedValue<number>
2484
+ readonly scaleX: SharedValue<number>
2485
+ readonly scaleY: SharedValue<number>
2486
+ readonly opacity: SharedValue<number>
2487
+ readonly zLifted: SharedValue<number>
2488
+ }
2489
+
2490
+ /** One entry of the U6 W4 opacity witness: a held holder's token and its live painted opacity. */
2491
+ export interface LayoutIdentityHolderOpacityWitness {
2492
+ readonly token: string
2493
+ readonly opacity: number
2494
+ }
2495
+
2496
+ // The driver's layout-identity surface (specs/LAYOUT-UITHREAD-PORT-BUILD-PACKET.md §The crossing
2497
+ // contract). Every lifecycle call crosses SYNCHRONOUSLY on one lane, so re-entry orderings
2498
+ // (detach then attach of the same token in one JS turn) can never reorder; per-frame work never
2499
+ // crosses. Rects are already validated + branded by the binding (fail-loud stays JS-side).
2500
+ /** Per-edge layout-identity options (REQ-API-035 MotionConfig policy → layoutId lane). */
2501
+ export interface WorkletLayoutFlightOptions {
2502
+ readonly instant?: boolean
2503
+ /** Component-tree address for a layoutId holder whose transform must feed descendants. */
2504
+ readonly projectionComponentId?: string
2505
+ /** Family-3: duration/ease (ms) for layoutId projection flight — data-only for worklets. */
2506
+ readonly timing?: import('@unrulysystems/native-motion-core').TimingConfig
2507
+ /** T22: per-flight spring (engine SpringConfig ms) — data-only for worklets; excludes timing. */
2508
+ readonly spring?: import('@unrulysystems/native-motion-core').SpringConfig
2509
+ }
2510
+
2511
+ export interface WorkletProjectionRect {
2512
+ readonly x: number
2513
+ readonly y: number
2514
+ readonly width: number
2515
+ readonly height: number
2516
+ }
2517
+
2518
+ export interface WorkletProjectionShared {
2519
+ readonly translateX: SharedValue<number>
2520
+ readonly translateY: SharedValue<number>
2521
+ readonly scaleX: SharedValue<number>
2522
+ readonly scaleY: SharedValue<number>
2523
+ // NaN denotes no radius correction. MotionView formats the actual host style at its boundary.
2524
+ readonly borderRadiusX: SharedValue<number>
2525
+ readonly borderRadiusY: SharedValue<number>
2526
+ readonly targetDeltaScaleX: SharedValue<number>
2527
+ readonly targetDeltaScaleY: SharedValue<number>
2528
+ readonly treeScaleX: SharedValue<number>
2529
+ readonly treeScaleY: SharedValue<number>
2530
+ }
2531
+
2532
+ /** Primitive address of one UI-runtime projection record for a host-layout frame callback. */
2533
+ export interface WorkletProjectionHostFeed {
2534
+ readonly driverId: number
2535
+ readonly componentId: string
2536
+ }
2537
+
2538
+ /** UI-runtime projection-tree lifecycle, separate from layoutId holder identity. */
2539
+ export interface WorkletDriverProjection {
2540
+ attach(
2541
+ componentId: string,
2542
+ parentId: string | null,
2543
+ projectionParticipant: boolean,
2544
+ borderRadius?: number | null,
2545
+ ): void
2546
+ detach(componentId: string): void
2547
+ /**
2548
+ * Register, replace, or clear (null) the host's live radius feed (seal r13, F4): an
2549
+ * animated/gesture/bound radius channel's driver cell, read every frame so the tree's
2550
+ * correction answers the CURRENT authored radius. Unmount compaction rides detach.
2551
+ */
2552
+ setProjectionRadiusFeed(componentId: string, cell: SharedValue<number> | null): void
2553
+ setProjection(
2554
+ componentId: string,
2555
+ source: WorkletProjectionRect,
2556
+ target: WorkletProjectionRect,
2557
+ progress: number,
2558
+ ): void
2559
+ sharedFor(componentId: string): WorkletProjectionShared
2560
+ hostFeedFor(componentId: string): WorkletProjectionHostFeed
2561
+ }
2562
+
2563
+ export interface WorkletDriverLayout {
2564
+ attach(
2565
+ id: string,
2566
+ token: LayoutIdToken,
2567
+ rect: WindowSpaceRect | null,
2568
+ options?: WorkletLayoutFlightOptions,
2569
+ ): LayoutIdentityRole
2570
+ commitRect(
2571
+ id: string,
2572
+ token: LayoutIdToken,
2573
+ rect: WindowSpaceRect,
2574
+ options?: WorkletLayoutFlightOptions,
2575
+ ): boolean
2576
+ // Returns whether this detach retained the holder as an exiting reverse-flight record. The
2577
+ // lifecycle uses this current edge result rather than an historical attach role.
2578
+ detach(id: string, token: LayoutIdToken, options?: WorkletLayoutFlightOptions): boolean
2579
+ // Get-or-create: styles bind at render time, before the first attach lands at onLayout; a
2580
+ // never-attached holder's record sits at rest pose.
2581
+ sharedFor(id: string, token: LayoutIdToken): LayoutIdentityHolderShared
2582
+ // U6 W4 verification witness (the device proof cards' crossfade row): the live painted opacity
2583
+ // of every holder record the identity still holds — the mounted lead AND an exiting retained
2584
+ // follower, never a departed ghost. A lifecycle-edge sync read, never per frame; an unknown or
2585
+ // dropped id witnesses empty.
2586
+ opacityWitnessFor(id: string): readonly LayoutIdentityHolderOpacityWitness[]
2587
+ releasable(id: string, token: LayoutIdToken): boolean
2588
+ isSettled(id: string): boolean
2589
+ holds(id: string, token: LayoutIdToken): boolean
2590
+ isLead(id: string, token: LayoutIdToken): boolean
2591
+ // Fires once when this identity reaches rest, independent of other active elements or layout
2592
+ // identities. An already-settled identity delivers synchronously at registration.
2593
+ subscribeSettled(id: string, onSettled: () => void): () => void
2594
+ // Couple an exiting holder's release to a presence callback: fires EXACTLY ONCE when the
2595
+ // registry reports the crossfade is done with the holder (first report wins on both runtimes);
2596
+ // returns an uncouple for unmount-before-settle.
2597
+ coupleRelease(id: string, token: LayoutIdToken, safeToRemove: () => void): () => void
2598
+ readonly projection: WorkletDriverProjection
2599
+ }
2600
+
2601
+ // The native driver + the binding surface the smoke/screens need beyond the core seam: per-prop shared
2602
+ // values for animated styles. `driver` satisfies core's `Driver` contract exactly.
2603
+ export interface WorkletDriverBinding {
2604
+ readonly driver: WorkletDriver
2605
+ sharedFor(handle: ElementHandle, key: string): SharedValue<number>
2606
+ /**
2607
+ * Legacy test/check-surface adapter. Production bindings call `driver.write` with the explicit
2608
+ * retarget shape; this adapter delegates to that same seam and never crosses to UI directly.
2609
+ */
2610
+ writeBoundValue(handle: ElementHandle, values: Readonly<Record<string, number>>): void
2611
+ /**
2612
+ * REQ-DRIVER-034: subscribe to this driver's dual-authority `cannot write bound value`
2613
+ * refusals delivered on the SCHEDULED lane. The synchronous lane still throws the refusal
2614
+ * into the caller's catch; the scheduled lane cannot marshal a throw back, so the refusal is
2615
+ * delivered here (one JS task later, like every UI-originated dispatch) instead of retiring
2616
+ * the admitted flight. Returns the unsubscribe; unsubscribing after dispose() is a no-op.
2617
+ */
2618
+ onBoundValueWriteRefusal(listener: (handle: ElementHandle, key: string) => void): () => void
2619
+ /** The current command/hold state for a mounted property, consumed only by layout mirrors. */
2620
+ propertyStateFor?(handle: ElementHandle, key: string): WorkletDriverPropertyState
2621
+ /**
2622
+ * T23 C: arm/clear the per-frame committed-values listener for one element (the onUpdate
2623
+ * marshal). While armed, JS receives ONE primitive payload per frame in which any committed
2624
+ * value changed — the whole registered record each time (the pin's latest-values contract).
2625
+ * Delivery is post-commit fire-and-forget observability; null clears the arm.
2626
+ */
2627
+ setFrameValuesListener(
2628
+ handle: ElementHandle,
2629
+ listener: ((values: Readonly<Record<string, number>>) => void) | null,
2630
+ ): void
2631
+ readonly layout: WorkletDriverLayout
2632
+ // Quiesce-before-unmount (review round 4 major 5): deletes this driver's UI-runtime state —
2633
+ // elements, the layout slot, every holder output. `useWorkletDriver` calls it automatically at
2634
+ // component unmount; manual (deterministic) drivers call it when their rig is done. Idempotent.
2635
+ //
2636
+ // Post-dispose contract: constructive calls (register/command/step/attach/couple/reads of element
2637
+ // state) fail loud — they are use-after-free. Teardown-convergent calls are DEFINED, not defects,
2638
+ // because React deletes a subtree parent-first: the owning hook's dispose() cleanup runs before
2639
+ // descendant unmount effects, which still call deactivate/detach on the way out (device-observed
2640
+ // release FATAL, 2026-07-10: assertLive→setActive→MotionView unmount inside
2641
+ // commitPassiveUnmountEffectsInsideOfDeletedTree). dispose() already deleted everything those
2642
+ // calls would touch, so: setActive(false) and layout.detach no-op; a late async measureInWindow
2643
+ // completion's layout.commitRect is dropped; layout.releasable/isSettled read true (the
2644
+ // registry's dropped-id semantics extended to a dropped driver).
2645
+ dispose(): void
2646
+ // Liveness read (REQ-DRIVER-022's non-throwing class): true once dispose() ran. The hook's
2647
+ // effect setup consults it to fail loud on a StrictMode replay (strictModeReplay.ts).
2648
+ isDisposed(): boolean
2649
+ }
2650
+
2651
+ // Internal live-loop wiring, provided only by `useWorkletDriver`: activate the (single) frame
2652
+ // callback when a command lands.
2653
+ interface LiveLoopHooks {
2654
+ readonly nextEpoch: () => number
2655
+ readonly startLoop: () => void
2656
+ }
2657
+
2658
+ let nextDriverId = 0
2659
+ const identityValueChannelDriverIds = new WeakMap<WorkletDriverBinding, number>()
2660
+
2661
+ /** Engine-internal identity lookup; the public/native binding shape does not expose driver ids. */
2662
+ export function identityValueChannelDriverIdForBinding(
2663
+ binding: WorkletDriverBinding,
2664
+ ): number | null {
2665
+ return identityValueChannelDriverIds.get(binding) ?? null
2666
+ }
2667
+
2668
+ function createDriverCore(
2669
+ options: WorkletDriverOptions,
2670
+ live: LiveLoopHooks | null,
2671
+ ): {
2672
+ binding: WorkletDriverBinding
2673
+ driverId: number
2674
+ } {
2675
+ const driverId = nextDriverId++
2676
+ const manual = options.manualStepping === true
2677
+ const privateGroup = new ManualFrameGroup(driverId, [driverId])
2678
+ if (manual) {
2679
+ unwrapUiOutcome(executeOnUIRuntimeSync(uiPrefillManualGroupSlotGuarded)(driverId, 1))
2680
+ }
2681
+ if (!manual && live === null) {
2682
+ // REQ-DRIVER-011: the live frame source is the hook-owned useFrameCallback; there is no
2683
+ // frame-source-less live mode to fall back to.
2684
+ throw new Error(
2685
+ 'live worklet drivers are created via useWorkletDriver(); use manualStepping for the deterministic mode.',
2686
+ )
2687
+ }
2688
+ // JS-side mirror of each element's shared values so sharedFor() needs no UI-runtime round trip.
2689
+ const sharedByHandle: Record<string, SharedValue<number>>[] = []
2690
+ const layoutMirrorByHandle: Record<string, WorkletDriverPropertyState>[] = []
2691
+ // The live write lane is asynchronous but gesture-edge reads are synchronous. Mirror only the
2692
+ // newest queued recognizer write per prop; `liveFor` uses the UI-applied version to distinguish a
2693
+ // genuinely pending write from an applied write whose value may since have drifted under
2694
+ // contention. The mirror never mutates UI state, so command/write/release ordering stays intact.
2695
+ const pendingWriteByHandle: Record<string, PendingGestureWrite>[] = []
2696
+ // T23 (device crash 3): the JS-side registration snapshot per handle. Live mode QUEUES the
2697
+ // register cross while committed() reads through the immediate sync lane, so a same-turn
2698
+ // read (a label entrance's discrete/color retarget in the mounting render) outruns the
2699
+ // push. A never-stepped element's committed values ARE its registration values — steps run
2700
+ // on the UI runtime BEHIND the same queue — so serving the snapshot is exact, never a
2701
+ // fallback; truly-unknown handles stay loud via this array's own range.
2702
+ const registeredCommittedByHandle: Record<string, number>[] = []
2703
+ let nextGestureWriteVersion = 1
2704
+
2705
+ // Crossing discipline: the live path schedules (fire-and-forget, O(1) per command,
2706
+ // REQ-DRIVER-013); the manual/verification path normally crosses SYNCHRONOUSLY so
2707
+ // command→step→read sequences are strictly ordered. The exception is a command transitively
2708
+ // invoked by a UI→JS callback: it queues even in manual mode to avoid a `runSyncSerialized`
2709
+ // re-entry while the UI thread is in `triggerUI`. A queued command preserves FIFO order with its
2710
+ // originating callback; direct manual calls retain their deterministic synchronous contract.
2711
+ // Crossing works return a UiOutcome (their bodies ride uiGuardLane): the sync lane unwraps it
2712
+ // JS-side; the scheduled lane's failures marshal to RN through the settle-dispatch channel and
2713
+ // retire the affected element (iOS AUTO SIGABRT Phase 2) — never a UI-runtime rethrow.
2714
+ function cross(work: (synchronous: boolean) => UiOutcome<void>): void {
2715
+ const synchronous = manual && uiOriginatedCallbackDepth === 0
2716
+ if (synchronous) unwrapUiOutcome(executeOnUIRuntimeSync(() => work(true))())
2717
+ // Named worklet + worklet arg (not an anonymous UI lambda that captures another worklet —
2718
+ // diamond-DAG hang class). Pass `work` as a scheduleOnUI argument (branded worklet ref).
2719
+ else scheduleOnUI(invokeAsyncCrossWork, work)
2720
+ }
2721
+
2722
+ // Disposal latch: CONSTRUCTIVE use after dispose() is a lifecycle defect — fail loud instead of
2723
+ // silently resurrecting an empty UI record through uiDriverState's get-or-create. Teardown-
2724
+ // convergent calls (setActive(false), layout.detach, a late commitRect, liveness reads) are
2725
+ // instead defined no-ops/terminal values — see the WorkletDriverBinding.dispose() contract.
2726
+ let disposed = false
2727
+ function assertLive(operation: string): void {
2728
+ if (disposed) {
2729
+ throw new Error(
2730
+ `worklet driver ${String(driverId)} is disposed: ${operation} after dispose() is a lifecycle defect.`,
2731
+ )
2732
+ }
2733
+ }
2734
+
2735
+ // JS-side Transition → plain-data resolution, single-sourced through the COMPONENT converters —
2736
+ // identical selection logic to the reference driver's startGenerator (spring is the fallback;
2737
+ // 'tween' for an explicit tween OR an authored duration-only untyped bag, REQ-TIMING-006).
2738
+ function toPlainTransition(rawTransition: PropTransition): PlainTransition {
2739
+ // The shared core adapter already detached and froze this transition. Re-capturing here would
2740
+ // reintroduce backend-specific materialization after the REQ-DRIVER-030 boundary.
2741
+ const transition = rawTransition
2742
+ // U7b: the GLOBAL instant window (the pin's MotionGlobalConfig.instantAnimations consult,
2743
+ // motion-dom@12.42.2 motion-value.ts:100-109) — while open, EVERY starting transition takes
2744
+ // the U7a instant commit with delay ZEROED (the pin zeroes delay only for the global flag;
2745
+ // the authored type:false below honors it — F2). Read HERE, JS-side, before the marshaling —
2746
+ // the pin's inertia lane honors the flag too, so this precedes every kind.
2747
+ if (isInstantWindowOpen()) {
2748
+ return { kind: 'instant', delayMs: 0 }
2749
+ }
2750
+ // A controller-resolved default (DefaultTransition, consult agent-2026-07-19-e715fd) normalizes
2751
+ // HERE, JS-side, mirroring the reference driver's normalizeTransition (no cross-driver skew): a
2752
+ // `type:'keyframes'` default → an equivalent tween (an ABSENT `ease` is preserved so the keyframes
2753
+ // generator keeps its intrinsic per-segment easeInOut for the 800ms default); a spring default
2754
+ // carries `restSpeed` into the SpringConfig settle threshold. Since U7c (REQ-SPRING-014) an
2755
+ // authored spring may carry restSpeed/restDelta too — the same extraction reads either, and
2756
+ // toSpringConfig's own passthrough agrees. The UI runtime stays config-only; it never sees the
2757
+ // union.
2758
+ const restSpeed =
2759
+ transition.type === 'spring' && 'restSpeed' in transition ? transition.restSpeed : undefined
2760
+ const authored: Transition | InertiaTransition =
2761
+ transition.type === 'keyframes'
2762
+ ? {
2763
+ type: 'tween',
2764
+ duration: transition.duration,
2765
+ ...(transition.ease !== undefined ? { ease: transition.ease } : {}),
2766
+ ...(transition.delay !== undefined ? { delay: transition.delay } : {}),
2767
+ ...(transition.repeat !== undefined ? { repeat: transition.repeat } : {}),
2768
+ ...(transition.repeatType !== undefined ? { repeatType: transition.repeatType } : {}),
2769
+ ...(transition.repeatDelay !== undefined
2770
+ ? { repeatDelay: transition.repeatDelay }
2771
+ : {}),
2772
+ }
2773
+ : transition
2774
+ // Every plain transition carries the keyframes config too (REQ-API-033): a keyframe ARRAY target
2775
+ // selects it UI-side, matching the reference driver's `toKeyframesConfig`. `explicitSpring` carries
2776
+ // the LITERAL type so the array branch can distinguish an explicit spring from the omitted-type
2777
+ // fallback (which resolves to `kind: 'spring'` when no `duration` is authored but must route
2778
+ // arrays to keyframes — review major 8).
2779
+ // An inertia transition never interpolates a keyframe array — it short-circuits BEFORE the
2780
+ // keyframes converter, which would refuse the unknown type (R16).
2781
+ if (authored.type === 'inertia')
2782
+ return {
2783
+ kind: 'inertia',
2784
+ config: {
2785
+ power: authored.power,
2786
+ timeConstant: authored.timeConstant,
2787
+ bounceStiffness: authored.bounceStiffness,
2788
+ bounceDamping: authored.bounceDamping,
2789
+ restDelta: authored.restDelta,
2790
+ restSpeed: authored.restSpeed,
2791
+ min: authored.min,
2792
+ max: authored.max,
2793
+ },
2794
+ explicitSpring: false,
2795
+ }
2796
+ // U7a + transition-default-selection F5: the instant lane short-circuits BEFORE the keyframes
2797
+ // converter (which owns no `false` type) — the pin's guard is type-independent
2798
+ // (`type: false` OR `duration === 0 && !repeatDelay`, motion-value.ts:89-98). Plain data
2799
+ // only: the delay rebase shift in ms.
2800
+ if (authored.type === false || isDurationZeroInstantTransition(authored)) {
2801
+ return { kind: 'instant', delayMs: toDelayMs(authored) }
2802
+ }
2803
+ const keyframes = toKeyframesConfig(authored)
2804
+ // T18-a (REQ-API-049): the repeat family resolves JS-side into plain scalars, exactly like every
2805
+ // other config — the UI runtime receives numbers and a string, never the authored object. `null`
2806
+ // (not `undefined`) marks an inert fold so the marshaled shape stays a closed, total record.
2807
+ const repeat = toRepeatFoldOptions(authored)
2808
+ // T18-b (REQ-TIMING-004 / REQ-API-050): the delay shift crosses seconds→ms HERE, JS-side,
2809
+ // through the same `toDelayMs` boundary the reference driver reads — the UI runtime receives
2810
+ // a plain number, never the authored seconds.
2811
+ const delayMs = toDelayMs(authored)
2812
+ if (authored.type === 'tween' || isTimingLaneTransition(authored))
2813
+ return {
2814
+ kind: 'tween',
2815
+ // REQ-TIMING-006 + transition-default-selection F1/F6: the timing lane — a typed tween OR
2816
+ // an authored duration-ONLY bag OR a defined ease-only bag (F6's `isTimingLaneTransition`,
2817
+ // the pin's defined-typeless lane at the 300ms default, JSAnimation.ts:106-116) — resolves
2818
+ // through the ONE config builder (`toTimingLaneConfig`): the pin's `animateMotionValue`
2819
+ // `ease: "easeOut"` base (motion-dom@12.42.2 motion-value.ts:45-49) whenever no ease
2820
+ // spelling is authored, the authored ease verbatim for the ease-only form. The generator
2821
+ // default [0.25, 0.1, 0.35, 1] (REQ-TIMING-002) no longer governs an authored
2822
+ // typed tween; it survives only as the pin's per-key default tween. Never the old
2823
+ // omitted-type duration-spring (device-measured peak 1.046, u7a-2026-08-18), and never the
2824
+ // REQ-SPRING-003 generic spring for an ease-only bag.
2825
+ config: toTimingLaneConfig(authored),
2826
+ keyframes,
2827
+ explicitSpring: false,
2828
+ repeat,
2829
+ delayMs,
2830
+ }
2831
+ return {
2832
+ kind: 'spring',
2833
+ config:
2834
+ restSpeed !== undefined
2835
+ ? { ...toSpringConfig(authored), restSpeed }
2836
+ : toSpringConfig(authored),
2837
+ keyframes,
2838
+ explicitSpring: authored.type === 'spring',
2839
+ repeat,
2840
+ delayMs,
2841
+ }
2842
+ }
2843
+
2844
+ const backend: Omit<PreparedDriver, 'write'> & {
2845
+ write(
2846
+ handle: ElementHandle,
2847
+ values: Readonly<Record<string, number>>,
2848
+ velocities?: Readonly<Record<string, number>>,
2849
+ eventShape?: WorkletDriverWriteShape,
2850
+ ): void
2851
+ } = {
2852
+ register(initial) {
2853
+ assertLive('register')
2854
+ const shared: Record<string, SharedValue<number>> = {}
2855
+ const layoutMirror: Record<string, WorkletDriverPropertyState> = {}
2856
+ for (const key of Object.keys(initial)) {
2857
+ shared[key] = makeMutable(initial[key]!)
2858
+ layoutMirror[key] = { active: makeMutable(false), target: makeMutable(initial[key]!) }
2859
+ }
2860
+ const handle = sharedByHandle.length as ElementHandle
2861
+ sharedByHandle.push(shared)
2862
+ layoutMirrorByHandle.push(layoutMirror)
2863
+ pendingWriteByHandle.push({})
2864
+ registeredCommittedByHandle.push({ ...initial })
2865
+ const committed = { ...initial }
2866
+ cross((synchronous) => {
2867
+ 'worklet'
2868
+ return uiGuardLane(
2869
+ synchronous,
2870
+ () => {
2871
+ const state = uiDriverState(driverId)
2872
+ state.elements.push({
2873
+ active: false,
2874
+ heldCount: 0,
2875
+ committed,
2876
+ heldVelocity: {},
2877
+ writeVersion: {},
2878
+ props: {},
2879
+ contending: {},
2880
+ propKeys: [],
2881
+ shared,
2882
+ layoutMirror,
2883
+ onUpdateArmed: false,
2884
+ lastNotified: {},
2885
+ registrationGeneration: 0,
2886
+ frameKeys: [],
2887
+ frameValues: [],
2888
+ path: null,
2889
+ })
2890
+ // alloc-ok: registration edge — grow the emission transaction's entry capacity to
2891
+ // cover the new element; the frame only fills these slots by index.
2892
+ state.emission.elementHandles.push(0)
2893
+ state.emission.elementGenerations.push(0)
2894
+ state.emission.elementCommitSequences.push(0)
2895
+ state.emission.elementMemberCounts.push(0)
2896
+ },
2897
+ driverId,
2898
+ undefined,
2899
+ false, // register: a fresh element push; the fold cannot reach this lane
2900
+ )
2901
+ })
2902
+ return handle
2903
+ },
2904
+
2905
+ command(handle, command) {
2906
+ assertLive('command')
2907
+ let plain: PlainCommand
2908
+ if (command.kind === 'stop') {
2909
+ plain = { kind: 'stop' }
2910
+ } else if (command.kind === 'start') {
2911
+ plain = {
2912
+ kind: 'start',
2913
+ targets: marshalStartTargets(command.targets, toPlainTransition),
2914
+ // Carry the release marker across the marshal as a definite boolean (plain data, no undefined
2915
+ // over the worklet boundary) so the UI runtime honors ownership (r8 major 24).
2916
+ releasesGesture: command.releasesGesture,
2917
+ ...(command.path === undefined
2918
+ ? {}
2919
+ : {
2920
+ path: {
2921
+ fromX: command.path.fromX,
2922
+ fromY: command.path.fromY,
2923
+ toX: command.path.toX,
2924
+ toY: command.path.toY,
2925
+ peak: command.path.peak,
2926
+ signedStrength: command.path.signedStrength,
2927
+ rotationScale: command.path.rotationScale,
2928
+ transition: toPlainTransition(command.path.transition),
2929
+ },
2930
+ }),
2931
+ }
2932
+ } else {
2933
+ plain = {
2934
+ kind: 'retarget',
2935
+ targets: marshalRetargetTargets(command.targets),
2936
+ transition: toPlainTransition(command.transition),
2937
+ ...(command.targetTransitions === undefined
2938
+ ? {}
2939
+ : {
2940
+ targetTransitions: marshalRetargetTransitions(
2941
+ command.targetTransitions,
2942
+ toPlainTransition,
2943
+ )!,
2944
+ }),
2945
+ }
2946
+ }
2947
+ const loopEpoch = live !== null && plain.kind !== 'stop' ? live.nextEpoch() : null
2948
+ cross((synchronous) => {
2949
+ 'worklet'
2950
+ return uiGuardLane(
2951
+ synchronous,
2952
+ () => {
2953
+ const state = uiDriverState(driverId)
2954
+ // Stamp the epoch BEFORE the apply: a failed command re-arms the settle edge from
2955
+ // inside uiGuardLane — with a retire for a genuine lane fault, without one for a fold
2956
+ // refusal (which mutated nothing) — and that re-arm must not be dropped as stale by
2957
+ // the epoch gate, because the failed command is still the latest intent (seal r2, F1).
2958
+ if (loopEpoch !== null) state.loopEpoch = loopEpoch
2959
+ uiApplyCommand(state, handle, plain)
2960
+ },
2961
+ driverId,
2962
+ handle as number,
2963
+ // uiApplyCommand builds every generator before committing anything, on both the start
2964
+ // and retarget cases, so a refusal here leaves the element exactly as it found it.
2965
+ true,
2966
+ )
2967
+ })
2968
+ if (loopEpoch !== null) live!.startLoop()
2969
+ },
2970
+
2971
+ step(elapsedMs) {
2972
+ assertLive('step')
2973
+ // Legacy single-member adapter: the group owns the one JS→UI crossing. Multi-member groups
2974
+ // must call ManualFrameGroup.step directly.
2975
+ if (privateGroup.size !== 1) {
2976
+ throw new Error(
2977
+ `worklet driver ${String(driverId)} belongs to a multi-member ManualFrameGroup: call group.step instead of driver.step.`,
2978
+ )
2979
+ }
2980
+ privateGroup.step(elapsedMs)
2981
+ },
2982
+
2983
+ committed(handle) {
2984
+ assertLive('committed')
2985
+ // Unknown-handle loudness moves JS-side (this driver assigned every valid handle); the
2986
+ // UI read returns null while the register cross is still queued and the snapshot serves.
2987
+ const registered = registeredCommittedByHandle[handle as number]
2988
+ if (registered === undefined) {
2989
+ throw new Error(
2990
+ `unknown element handle ${String(handle)}: it was never registered on this driver.`,
2991
+ )
2992
+ }
2993
+ const outcome = unwrapUiOutcome(
2994
+ executeOnUIRuntimeSync(() => {
2995
+ 'worklet'
2996
+ return uiGuard(() => {
2997
+ const state = uiDriverState(driverId)
2998
+ if ((handle as number) >= state.elements.length) return null
2999
+ return { ...uiElementAt(state, handle as number).committed }
3000
+ })
3001
+ })(),
3002
+ )
3003
+ return outcome === null ? { ...registered } : outcome
3004
+ },
3005
+
3006
+ setActive(handle, active) {
3007
+ // Deleted-tree ordering: the owner's dispose() runs before a child MotionView's unmount calls
3008
+ // setActive(false). The element record is already gone — deactivation is subsumed, and the
3009
+ // early return must not cross (uiDriverState's get-or-create would resurrect the record).
3010
+ // Activation stays fail-loud: animating a disposed driver is use-after-free.
3011
+ if (disposed && !active) return
3012
+ assertLive('setActive')
3013
+ const loopEpoch = active && live !== null ? live.nextEpoch() : null
3014
+ cross((synchronous) => {
3015
+ 'worklet'
3016
+ return uiGuardLane(
3017
+ synchronous,
3018
+ () => {
3019
+ const state = uiDriverState(driverId)
3020
+ uiElementAt(state, handle as number).active = active
3021
+ if (loopEpoch !== null) state.loopEpoch = loopEpoch
3022
+ },
3023
+ driverId,
3024
+ handle as number,
3025
+ false, // setActive: a flag write, not a build; the fold cannot reach this lane
3026
+ )
3027
+ })
3028
+ if (loopEpoch !== null) live!.startLoop()
3029
+ },
3030
+
3031
+ isActive(handle) {
3032
+ assertLive('isActive')
3033
+ return unwrapUiOutcome(
3034
+ executeOnUIRuntimeSync(() => {
3035
+ 'worklet'
3036
+ return uiGuard(() => {
3037
+ return uiElementAt(uiDriverState(driverId), handle as number).active
3038
+ })
3039
+ })(),
3040
+ )
3041
+ },
3042
+
3043
+ // The one event-write lane (REQ-DRIVER-024/-029): recognizer writes and bound-value retargets
3044
+ // ride the same ordered crossing. The shape decides ownership; no secondary binding seam.
3045
+ // Neither event starts an animation (the declarative/release command arms the loop).
3046
+ write(handle, values, velocities, eventShape = 'gesture-hold') {
3047
+ assertLive('write')
3048
+ const writeVersion =
3049
+ eventShape === 'gesture-hold' ? nextGestureWriteVersion++ : nextGestureWriteVersion
3050
+ const keys = Object.keys(values)
3051
+ const registered = sharedByHandle[handle as number]
3052
+ const pending = pendingWriteByHandle[handle as number]
3053
+ // Do not mirror a malformed multi-prop write: uiApplyWrite validates the whole write before
3054
+ // mutation, so reflecting only its valid subset would manufacture a state the UI rejects.
3055
+ let canMirror = registered !== undefined && pending !== undefined
3056
+ for (const key of keys) {
3057
+ if (registered?.[key] === undefined) canMirror = false
3058
+ }
3059
+ cross((synchronous) => {
3060
+ 'worklet'
3061
+ return uiGuardLane(
3062
+ synchronous,
3063
+ () => {
3064
+ uiApplyWrite(
3065
+ uiDriverState(driverId),
3066
+ handle as number,
3067
+ values,
3068
+ velocities,
3069
+ writeVersion,
3070
+ eventShape,
3071
+ )
3072
+ },
3073
+ driverId,
3074
+ handle as number,
3075
+ // uiApplyWrite re-anchors a contending generator INSIDE its per-key mutation loop, so
3076
+ // a refusal there is half-applied and must retire (round-10 BLOCKING 2).
3077
+ false,
3078
+ )
3079
+ })
3080
+ if (eventShape === 'gesture-hold' && canMirror && pending !== undefined) {
3081
+ for (const key of keys) {
3082
+ pending[key] = {
3083
+ version: writeVersion,
3084
+ value: values[key]!,
3085
+ velocity: velocities?.[key] ?? 0,
3086
+ }
3087
+ }
3088
+ }
3089
+ },
3090
+
3091
+ // The grab live-read (REQ-DRIVER-025): synchronous — the begin seed is needed before the next
3092
+ // recognizer event lands. O(1) per gesture edge. A queued recognizer write is covered by the
3093
+ // per-prop version fence below, so terminal reads cannot overtake it. A sync read can still
3094
+ // overtake a JUST-scheduled release command; it then reads the pre-release value — exactly what
3095
+ // the element still renders, so an immediate re-grab's C0 seam holds.
3096
+ liveFor(handle, key) {
3097
+ assertLive('liveFor')
3098
+ const liveRead = unwrapUiOutcome(
3099
+ executeOnUIRuntimeSync(() => {
3100
+ 'worklet'
3101
+ return uiGuard(() => {
3102
+ return uiLiveFor(uiDriverState(driverId), handle as number, key)
3103
+ })
3104
+ })(),
3105
+ )
3106
+ const pendingByKey = pendingWriteByHandle[handle as number]
3107
+ if (pendingByKey !== undefined) {
3108
+ const pending = pendingByKey[key]
3109
+ if (pending === undefined) return { value: liveRead.value, velocity: liveRead.velocity }
3110
+ if (liveRead.writeVersion < pending.version) {
3111
+ return { value: pending.value, velocity: pending.velocity }
3112
+ }
3113
+ delete pendingByKey[key]
3114
+ }
3115
+ return { value: liveRead.value, velocity: liveRead.velocity }
3116
+ },
3117
+ }
3118
+
3119
+ const preparedDriver = adaptPreparedDriver(backend, () => {
3120
+ assertLive('command')
3121
+ })
3122
+ const driver: WorkletDriver = {
3123
+ ...preparedDriver,
3124
+ write(handle, values, velocities, eventShape = 'gesture-hold') {
3125
+ if (eventShape === 'gesture-hold') {
3126
+ preparedDriver.write(handle, values, velocities)
3127
+ return
3128
+ }
3129
+ backend.write(handle, values, velocities, eventShape)
3130
+ },
3131
+ }
3132
+
3133
+ // ——— The layout-identity surface (specs/LAYOUT-UITHREAD-PORT-BUILD-PACKET.md) ———
3134
+ // Lifecycle crossings are SYNCHRONOUS on one lane: they are O(1) per mount/commit/unmount edge,
3135
+ // and a scheduled lane would let a re-attach of a token overtake its own detach. Per-frame work
3136
+ // never crosses; the frame callback (or manual step) carries release edges via scheduleOnRN.
3137
+ const layoutSpring = options.layoutSpring
3138
+ const layoutSharedByHolder = new Map<string, LayoutIdentityHolderShared>()
3139
+ // Projection-tree output belongs to a concrete Motion component, not a layoutId holder. Keep
3140
+ // its shared cells on JS solely for render-time lookup; their values are written only by the UI
3141
+ // ledger in uiLayoutEngine.
3142
+ const projectionSharedByComponent = new Map<string, WorkletProjectionShared>()
3143
+ let nextCouplingKey = 0
3144
+ const layoutCallbacks: LayoutCallbackRegistry = {
3145
+ release: new Map<number, () => void>(),
3146
+ settled: new Map<number, () => void>(),
3147
+ }
3148
+ layoutCallbacksByDriver.set(driverId, layoutCallbacks)
3149
+ let nextSettleWatcherKey = 0
3150
+ const layoutReleaseCallbacks = layoutCallbacks.release
3151
+ const layoutSettleCallbacks = layoutCallbacks.settled
3152
+
3153
+ // JSON-encode the pair: unambiguous for any id/token content (no separator to collide with) and
3154
+ // text-safe (review round 2026-07-09: a NUL-byte separator made this file classify as binary).
3155
+ function layoutHolderKey(id: string, token: LayoutIdToken): string {
3156
+ return JSON.stringify([id, String(token)])
3157
+ }
3158
+
3159
+ // Get-or-create: animated styles must bind holder outputs at RENDER time, before the first
3160
+ // attach lands at onLayout — a throwing lookup would invert the render→layout order. A record
3161
+ // for a never-attached holder just sits at rest pose (identity transform, opacity 1, no lift).
3162
+ function ensureHolderShared(id: string, token: LayoutIdToken): LayoutIdentityHolderShared {
3163
+ const holderKey = layoutHolderKey(id, token)
3164
+ let shared = layoutSharedByHolder.get(holderKey)
3165
+ if (shared === undefined) {
3166
+ shared = {
3167
+ translateX: makeMutable(0),
3168
+ translateY: makeMutable(0),
3169
+ scaleX: makeMutable(1),
3170
+ scaleY: makeMutable(1),
3171
+ opacity: makeMutable(1),
3172
+ zLifted: makeMutable(0),
3173
+ }
3174
+ layoutSharedByHolder.set(holderKey, shared)
3175
+ }
3176
+ return shared
3177
+ }
3178
+
3179
+ function ensureProjectionShared(componentId: string): WorkletProjectionShared {
3180
+ let shared = projectionSharedByComponent.get(componentId)
3181
+ if (shared === undefined) {
3182
+ shared = {
3183
+ translateX: makeMutable(0),
3184
+ translateY: makeMutable(0),
3185
+ scaleX: makeMutable(1),
3186
+ scaleY: makeMutable(1),
3187
+ borderRadiusX: makeMutable(Number.NaN),
3188
+ borderRadiusY: makeMutable(Number.NaN),
3189
+ targetDeltaScaleX: makeMutable(1),
3190
+ targetDeltaScaleY: makeMutable(1),
3191
+ treeScaleX: makeMutable(1),
3192
+ treeScaleY: makeMutable(1),
3193
+ }
3194
+ projectionSharedByComponent.set(componentId, shared)
3195
+ }
3196
+ return shared
3197
+ }
3198
+
3199
+ const layout: WorkletDriverLayout = {
3200
+ projection: {
3201
+ attach(componentId, parentId, projectionParticipant, borderRadius = null) {
3202
+ assertLive('layout.projection.attach')
3203
+ const outputs = ensureProjectionShared(componentId) as unknown as UiProjectionOutputs
3204
+ unwrapUiOutcome(
3205
+ executeOnUIRuntimeSync(() => {
3206
+ 'worklet'
3207
+ return uiGuard(() => {
3208
+ uiProjectionAttach(
3209
+ uiLayoutSlot(uiDriverState(driverId), layoutSpring),
3210
+ componentId,
3211
+ parentId,
3212
+ outputs,
3213
+ projectionParticipant,
3214
+ borderRadius,
3215
+ )
3216
+ })
3217
+ })(),
3218
+ )
3219
+ },
3220
+
3221
+ detach(componentId) {
3222
+ // React's child-before-parent cleanup is a tree invariant. A disposed driver already
3223
+ // owns all records, so a trailing unmount is convergent just like layout.detach.
3224
+ if (disposed) return
3225
+ assertLive('layout.projection.detach')
3226
+ projectionSharedByComponent.delete(componentId)
3227
+ unwrapUiOutcome(
3228
+ executeOnUIRuntimeSync(() => {
3229
+ 'worklet'
3230
+ return uiGuard(() => {
3231
+ uiProjectionDetach(uiLayoutSlot(uiDriverState(driverId), layoutSpring), componentId)
3232
+ })
3233
+ })(),
3234
+ )
3235
+ },
3236
+
3237
+ setProjectionRadiusFeed(componentId, cell) {
3238
+ assertLive('layout.projection.setProjectionRadiusFeed')
3239
+ unwrapUiOutcome(
3240
+ executeOnUIRuntimeSync(() => {
3241
+ 'worklet'
3242
+ return uiGuard(() => {
3243
+ uiSetProjectionRadiusFeed(
3244
+ uiLayoutSlot(uiDriverState(driverId), layoutSpring),
3245
+ componentId,
3246
+ cell,
3247
+ )
3248
+ })
3249
+ })(),
3250
+ )
3251
+ },
3252
+
3253
+ setProjection(componentId, source, target, progress) {
3254
+ assertLive('layout.projection.setProjection')
3255
+ const loopEpoch = live?.nextEpoch() ?? null
3256
+ unwrapUiOutcome(
3257
+ executeOnUIRuntimeSync(() => {
3258
+ 'worklet'
3259
+ return uiGuard(() => {
3260
+ const state = uiDriverState(driverId)
3261
+ if (loopEpoch !== null) state.loopEpoch = loopEpoch
3262
+ uiProjectionSetProjection(
3263
+ uiLayoutSlot(state, layoutSpring),
3264
+ componentId,
3265
+ source,
3266
+ target,
3267
+ progress,
3268
+ )
3269
+ })
3270
+ })(),
3271
+ )
3272
+ if (loopEpoch !== null) live!.startLoop()
3273
+ },
3274
+
3275
+ sharedFor(componentId) {
3276
+ assertLive('layout.projection.sharedFor')
3277
+ return ensureProjectionShared(componentId)
3278
+ },
3279
+
3280
+ hostFeedFor(componentId) {
3281
+ assertLive('layout.projection.hostFeedFor')
3282
+ // This is only an address. The host-layout channel reads/writes the UI-global ledger
3283
+ // directly on every frame; no function or JS→UI crossing crosses this boundary.
3284
+ return { driverId, componentId }
3285
+ },
3286
+ },
3287
+
3288
+ attach(id, token, rect, options) {
3289
+ assertLive('layout.attach')
3290
+ const shared = ensureHolderShared(id, token)
3291
+ const outputs = shared as unknown as UiLayoutHolderOutputs
3292
+ // Capture primitives so the worklet closure does not close over `options`.
3293
+ // optionsPresent: retain (omit) vs clear ({} without timing) vs set (tjynor).
3294
+ const optionsPresent = options !== undefined
3295
+ const instant = options?.instant === true
3296
+ const projectionComponentId = options?.projectionComponentId ?? null
3297
+ const timing = options?.timing ?? null
3298
+ const spring = options?.spring ?? null
3299
+ const loopEpoch = live?.nextEpoch() ?? null
3300
+ const role = unwrapUiOutcome(
3301
+ executeOnUIRuntimeSync(() => {
3302
+ 'worklet'
3303
+ return uiGuard(() => {
3304
+ const state = uiDriverState(driverId)
3305
+ state.settleNotified = false // an entering pairing may begin a flight — re-arm the edge
3306
+ if (loopEpoch !== null) state.loopEpoch = loopEpoch
3307
+ return uiLayoutAttach(
3308
+ uiLayoutSlot(state, layoutSpring),
3309
+ id,
3310
+ token,
3311
+ rect,
3312
+ outputs,
3313
+ instant,
3314
+ projectionComponentId,
3315
+ timing,
3316
+ spring,
3317
+ optionsPresent,
3318
+ )
3319
+ })
3320
+ })(),
3321
+ )
3322
+ if (loopEpoch !== null) live!.startLoop()
3323
+ return role
3324
+ },
3325
+
3326
+ commitRect(id, token, rect, options) {
3327
+ // measureInWindow completes async, so a commit can land after the tree unmounted and the
3328
+ // owner disposed. The registry it would commit into is gone — drop the stale measurement
3329
+ // (same late-completion class as the frame callback's post-dispose vsync peek); throwing
3330
+ // here would detonate inside the host's measurement callback.
3331
+ if (disposed) return false
3332
+ assertLive('layout.commitRect')
3333
+ const optionsPresent = options !== undefined
3334
+ const instant = options?.instant === true
3335
+ const timing = options?.timing ?? null
3336
+ const spring = options?.spring ?? null
3337
+ const loopEpoch = live?.nextEpoch() ?? null
3338
+ const committedLead = unwrapUiOutcome(
3339
+ executeOnUIRuntimeSync(() => {
3340
+ 'worklet'
3341
+ return uiGuard(() => {
3342
+ const state = uiDriverState(driverId)
3343
+ state.settleNotified = false // a lead commit retargets the flight
3344
+ if (loopEpoch !== null) state.loopEpoch = loopEpoch
3345
+ return uiLayoutCommit(
3346
+ uiLayoutSlot(state, layoutSpring),
3347
+ id,
3348
+ token,
3349
+ rect,
3350
+ instant,
3351
+ timing,
3352
+ spring,
3353
+ optionsPresent,
3354
+ )
3355
+ })
3356
+ })(),
3357
+ )
3358
+ if (loopEpoch !== null) live!.startLoop()
3359
+ return committedLead
3360
+ },
3361
+
3362
+ detach(id, token, options) {
3363
+ // Same teardown-convergence rule as setActive(false): dispose() dropped the registry and
3364
+ // every holder, which subsumes leave(). A descendant detaching after the owner disposed is
3365
+ // legal deleted-tree ordering, not use-after-free.
3366
+ if (disposed) return false
3367
+ assertLive('layout.detach')
3368
+ const optionsPresent = options !== undefined
3369
+ const instant = options?.instant === true
3370
+ const timing = options?.timing ?? null
3371
+ const spring = options?.spring ?? null
3372
+ // A retained layout holder receives its final detach from the UI-originated release
3373
+ // callback. It does not need the `holds()` result at that point (the release already won),
3374
+ // so queue the cleanup rather than synchronously re-entering the UI registry while the UI
3375
+ // runtime is still dispatching its callback. Ordinary lifecycle detaches retain their
3376
+ // exact synchronous result contract below.
3377
+ if (uiOriginatedCallbackDepth > 0) {
3378
+ const loopEpoch = live?.nextEpoch() ?? null
3379
+ cross((synchronous) => {
3380
+ 'worklet'
3381
+ return uiGuardLane(
3382
+ synchronous,
3383
+ () => {
3384
+ const state = uiDriverState(driverId)
3385
+ state.settleNotified = false
3386
+ if (loopEpoch !== null) state.loopEpoch = loopEpoch
3387
+ uiLayoutDetach(
3388
+ uiLayoutSlot(state, layoutSpring),
3389
+ id,
3390
+ token,
3391
+ instant,
3392
+ timing,
3393
+ spring,
3394
+ optionsPresent,
3395
+ )
3396
+ },
3397
+ driverId,
3398
+ undefined,
3399
+ false, // layout detach: no element handle; the fold cannot reach this lane
3400
+ )
3401
+ })
3402
+ if (loopEpoch !== null) live!.startLoop()
3403
+ return false
3404
+ }
3405
+ const loopEpoch = live?.nextEpoch() ?? null
3406
+ const exitingHolderRetained = unwrapUiOutcome(
3407
+ executeOnUIRuntimeSync(() => {
3408
+ 'worklet'
3409
+ return uiGuard(() => {
3410
+ const state = uiDriverState(driverId)
3411
+ state.settleNotified = false // a lead leaving begins the reverse flight
3412
+ if (loopEpoch !== null) state.loopEpoch = loopEpoch
3413
+ uiLayoutDetach(
3414
+ uiLayoutSlot(state, layoutSpring),
3415
+ id,
3416
+ token,
3417
+ instant,
3418
+ timing,
3419
+ spring,
3420
+ optionsPresent,
3421
+ )
3422
+ return state.layout!.registry.holds(id, token)
3423
+ })
3424
+ })(),
3425
+ )
3426
+ if (loopEpoch !== null) live!.startLoop()
3427
+ return exitingHolderRetained
3428
+ },
3429
+
3430
+ sharedFor(id, token) {
3431
+ assertLive('layout.sharedFor')
3432
+ return ensureHolderShared(id, token)
3433
+ },
3434
+
3435
+ releasable(id, token) {
3436
+ // Liveness queries are non-throwing by contract (a dropped id reads releasable). dispose()
3437
+ // drops everything, so the dropped-id axiom extends to the whole driver — an exit polling
3438
+ // across the teardown boundary converges instead of crashing.
3439
+ if (disposed) return true
3440
+ assertLive('layout.releasable')
3441
+ return unwrapUiOutcome(
3442
+ executeOnUIRuntimeSync(() => {
3443
+ 'worklet'
3444
+ return uiGuard(() => {
3445
+ const state = uiDriverStatePeek(driverId)
3446
+ const slot = state === null ? null : state.layout
3447
+ // No slot means nothing was ever tracked — the registry's dropped-id semantics.
3448
+ return slot === null ? true : slot.registry.releasable(id, token)
3449
+ })
3450
+ })(),
3451
+ )
3452
+ },
3453
+
3454
+ isSettled(id) {
3455
+ // Same dropped-driver axiom as releasable(): nothing tracked means nothing in flight.
3456
+ if (disposed) return true
3457
+ assertLive('layout.isSettled')
3458
+ return unwrapUiOutcome(
3459
+ executeOnUIRuntimeSync(() => {
3460
+ 'worklet'
3461
+ return uiGuard(() => {
3462
+ const state = uiDriverStatePeek(driverId)
3463
+ const slot = state === null ? null : state.layout
3464
+ return slot === null ? true : slot.registry.isSettled(id)
3465
+ })
3466
+ })(),
3467
+ )
3468
+ },
3469
+
3470
+ holds(id, token) {
3471
+ if (disposed) return false
3472
+ assertLive('layout.holds')
3473
+ return unwrapUiOutcome(
3474
+ executeOnUIRuntimeSync(() => {
3475
+ 'worklet'
3476
+ return uiGuard(() => {
3477
+ const state = uiDriverStatePeek(driverId)
3478
+ const slot = state === null ? null : state.layout
3479
+ return slot === null ? false : slot.registry.holds(id, token)
3480
+ })
3481
+ })(),
3482
+ )
3483
+ },
3484
+
3485
+ isLead(id, token) {
3486
+ if (disposed) return false
3487
+ assertLive('layout.isLead')
3488
+ return unwrapUiOutcome(
3489
+ executeOnUIRuntimeSync(() => {
3490
+ 'worklet'
3491
+ return uiGuard(() => {
3492
+ const state = uiDriverStatePeek(driverId)
3493
+ const slot = state === null ? null : state.layout
3494
+ return slot === null ? false : slot.registry.isLead(id, token)
3495
+ })
3496
+ })(),
3497
+ )
3498
+ },
3499
+
3500
+ opacityWitnessFor(id) {
3501
+ // Same dropped-driver axiom as isSettled(): a dropped driver holds nothing to witness.
3502
+ if (disposed) return []
3503
+ assertLive('layout.opacityWitnessFor')
3504
+ return unwrapUiOutcome(
3505
+ executeOnUIRuntimeSync(() => {
3506
+ 'worklet'
3507
+ return uiGuard(() => {
3508
+ const state = uiDriverStatePeek(driverId)
3509
+ const slot = state === null ? null : state.layout
3510
+ return slot === null ? [] : uiLayoutOpacityWitness(slot, id)
3511
+ })
3512
+ })(),
3513
+ )
3514
+ },
3515
+
3516
+ subscribeSettled(id, onSettled) {
3517
+ assertLive('layout.subscribeSettled')
3518
+ const key = nextSettleWatcherKey++
3519
+ layoutSettleCallbacks.set(key, onSettled)
3520
+ const emittedNow = unwrapUiOutcome(
3521
+ executeOnUIRuntimeSync(() => {
3522
+ 'worklet'
3523
+ return uiGuard(() => {
3524
+ const state = uiDriverState(driverId)
3525
+ const slot = state.layout
3526
+ if (slot === null || slot.registry.isSettled(id)) return true
3527
+ state.layoutSettleWatchers.push({ id, key })
3528
+ // alloc-ok: lifecycle edge — one staged-settle slot per registered watcher. The frame
3529
+ // writes by index and never grows this transport array.
3530
+ state.emission.layoutSettledKeys.push(0)
3531
+ return false
3532
+ })
3533
+ })(),
3534
+ )
3535
+ if (emittedNow) {
3536
+ deliverLayoutSettled(driverId, key)
3537
+ return () => {}
3538
+ }
3539
+ return () => {
3540
+ layoutSettleCallbacks.delete(key)
3541
+ unwrapUiOutcome(
3542
+ executeOnUIRuntimeSync(() => {
3543
+ 'worklet'
3544
+ return uiGuard(() => {
3545
+ const state = uiDriverStatePeek(driverId)
3546
+ if (state === null) return
3547
+ let w = 0
3548
+ for (let i = 0; i < state.layoutSettleWatchers.length; i++) {
3549
+ const watcher = state.layoutSettleWatchers[i]!
3550
+ if (watcher.key !== key) {
3551
+ state.layoutSettleWatchers[w] = watcher
3552
+ w += 1
3553
+ }
3554
+ }
3555
+ state.layoutSettleWatchers.length = w
3556
+ })
3557
+ })(),
3558
+ )
3559
+ }
3560
+ },
3561
+
3562
+ coupleRelease(id, token, safeToRemove) {
3563
+ assertLive('layout.coupleRelease')
3564
+ const key = nextCouplingKey++
3565
+ layoutReleaseCallbacks.set(key, safeToRemove)
3566
+ const loopEpoch = live?.nextEpoch() ?? null
3567
+ // Registration and the already-releasable check are ONE atomic UI hop (the engine re-checks
3568
+ // at registration), so the immediate-release frontier cannot race a frame in between.
3569
+ const emittedNow = unwrapUiOutcome(
3570
+ executeOnUIRuntimeSync(() => {
3571
+ 'worklet'
3572
+ return uiGuard(() => {
3573
+ const state = uiDriverState(driverId)
3574
+ if (loopEpoch !== null) state.loopEpoch = loopEpoch
3575
+ let emitted = false
3576
+ uiLayoutCouple(uiLayoutSlot(state, layoutSpring), id, token, key, () => {
3577
+ emitted = true
3578
+ })
3579
+ return emitted
3580
+ })
3581
+ })(),
3582
+ )
3583
+ if (emittedNow) {
3584
+ deliverLayoutRelease(driverId, key)
3585
+ return () => {}
3586
+ }
3587
+ if (loopEpoch !== null) live!.startLoop()
3588
+ return () => {
3589
+ layoutReleaseCallbacks.delete(key)
3590
+ unwrapUiOutcome(
3591
+ executeOnUIRuntimeSync(() => {
3592
+ 'worklet'
3593
+ return uiGuard(() => {
3594
+ const state = uiDriverStatePeek(driverId)
3595
+ const slot = state === null ? null : state.layout
3596
+ if (slot !== null) uiLayoutUncouple(slot, key)
3597
+ })
3598
+ })(),
3599
+ )
3600
+ }
3601
+ },
3602
+ }
3603
+
3604
+ const binding: WorkletDriverBinding = {
3605
+ driver,
3606
+ sharedFor(handle, key) {
3607
+ assertLive('sharedFor')
3608
+ const shared = sharedByHandle[handle as number]?.[key]
3609
+ if (shared === undefined) {
3610
+ throw new Error(
3611
+ `no shared value for handle ${String(handle)} prop '${key}': register it first.`,
3612
+ )
3613
+ }
3614
+ return shared
3615
+ },
3616
+ writeBoundValue(handle, values) {
3617
+ driver.write(handle, values, undefined, 'bound-value-retarget')
3618
+ },
3619
+ onBoundValueWriteRefusal(listener) {
3620
+ assertLive('onBoundValueWriteRefusal')
3621
+ let listeners = boundValueWriteRefusalListenersByDriverId.get(driverId)
3622
+ if (listeners === undefined) {
3623
+ listeners = new Set()
3624
+ boundValueWriteRefusalListenersByDriverId.set(driverId, listeners)
3625
+ }
3626
+ const subscribed = listener as (handle: number, key: string) => void
3627
+ listeners.add(subscribed)
3628
+ return () => {
3629
+ listeners.delete(subscribed)
3630
+ if (listeners.size === 0) boundValueWriteRefusalListenersByDriverId.delete(driverId)
3631
+ }
3632
+ },
3633
+ setFrameValuesListener(handle, listener) {
3634
+ assertLive('setFrameValuesListener')
3635
+ // JS registry first (the crossing below is fire-and-forget): the listener must be
3636
+ // recoverable the moment the UI arm lands, and clearing must drop it even if the
3637
+ // disarm crossing is still queued.
3638
+ let byHandle = elementFrameValueCallbacks.get(driverId)
3639
+ if (byHandle === undefined) {
3640
+ byHandle = new Map()
3641
+ elementFrameValueCallbacks.set(driverId, byHandle)
3642
+ }
3643
+ const registrationGeneration = nextElementFrameValueRegistrationGeneration()
3644
+ if (listener === null) byHandle.delete(handle as number)
3645
+ else
3646
+ byHandle.set(handle as number, {
3647
+ generation: registrationGeneration,
3648
+ callback: listener,
3649
+ })
3650
+ const armed = listener !== null
3651
+ cross((synchronous) => {
3652
+ 'worklet'
3653
+ return uiGuardLane(
3654
+ synchronous,
3655
+ () => {
3656
+ const element = uiElementAt(uiDriverState(driverId), handle as number)
3657
+ element.onUpdateArmed = armed
3658
+ element.registrationGeneration = registrationGeneration
3659
+ // The pin's cadence is value-change only, so an arm edge emits nothing by itself.
3660
+ // Disarm clears the change markers so a later re-arm cannot suppress its first
3661
+ // genuinely-changed frame against stale lastNotified values.
3662
+ if (!armed) {
3663
+ for (const key of Object.keys(element.lastNotified)) {
3664
+ delete element.lastNotified[key]
3665
+ }
3666
+ }
3667
+ },
3668
+ driverId,
3669
+ undefined,
3670
+ false, // arm flag write: no build; the fold cannot reach this lane
3671
+ )
3672
+ })
3673
+ },
3674
+ propertyStateFor(handle, key) {
3675
+ assertLive('propertyStateFor')
3676
+ const state = layoutMirrorByHandle[handle as number]?.[key]
3677
+ if (state === undefined) {
3678
+ throw new Error(
3679
+ `no layout mirror state for handle ${String(handle)} prop '${key}': register it first.`,
3680
+ )
3681
+ }
3682
+ return state
3683
+ },
3684
+ layout,
3685
+ dispose() {
3686
+ if (disposed) return // idempotent — an unmount effect and an explicit call may both run
3687
+ disposed = true
3688
+ layoutReleaseCallbacks.clear() // a pending safeToRemove must not fire into an unmounted tree
3689
+ layoutSettleCallbacks.clear()
3690
+ layoutCallbacksByDriver.delete(driverId)
3691
+ driverSettleCallbacks.delete(driverId)
3692
+ elementFrameValueCallbacks.delete(driverId)
3693
+ boundValueWriteRefusalListenersByDriverId.delete(driverId)
3694
+ // The mailbox's accepted drain id is NOT forgotten here: envelopes the UI runtime sealed
3695
+ // before this crossing lands are still in flight, and they validate against it. The UI
3696
+ // side seals the driver's final DRIVER_DISPOSED envelope before deleting the record; JS
3697
+ // forgets the mailbox on that receipt, in order.
3698
+ cross((synchronous) => {
3699
+ 'worklet'
3700
+ return uiGuardLane(
3701
+ synchronous,
3702
+ () => {
3703
+ const g = globalThis as UiGlobal
3704
+ const state = uiDriverStatePeek(driverId)
3705
+ if (state !== null) {
3706
+ state.emission.disposeStaged = true
3707
+ uiSealFrameEmission(state, null)
3708
+ }
3709
+ if (g.__nativeMotionDrivers !== undefined) delete g.__nativeMotionDrivers[driverId]
3710
+ if (g.__nativeMotionProjectionHostFrames !== undefined)
3711
+ delete g.__nativeMotionProjectionHostFrames[driverId]
3712
+ if (g.__nativeMotionManualGroups !== undefined)
3713
+ delete g.__nativeMotionManualGroups[driverId]
3714
+ },
3715
+ driverId,
3716
+ undefined,
3717
+ false, // dispose: no element to retire; the fold cannot reach this lane
3718
+ )
3719
+ })
3720
+ },
3721
+ isDisposed: () => disposed,
3722
+ }
3723
+ identityValueChannelDriverIds.set(binding, driverId)
3724
+ return { binding, driverId }
3725
+ }
3726
+
3727
+ // Deterministic (manual-stepping) driver — the on-device verification surface. Live drivers MUST come
3728
+ // from `useWorkletDriver` (REQ-DRIVER-011), so `manualStepping: true` is required here.
3729
+ export function createWorkletDriver(options: WorkletDriverOptions): WorkletDriverBinding {
3730
+ return createDriverCore(options, null).binding
3731
+ }
3732
+
3733
+ // The LIVE native driver: one driver-owned `useFrameCallback` (real vsync, REQ-DRIVER-011) steps the
3734
+ // whole graph. Created inactive; a command activates it; the settle edge deactivates it.
3735
+ export function useWorkletDriver(options: WorkletDriverOptions = {}): WorkletDriverBinding {
3736
+ // The UI dispatcher is stable for the driver's lifetime, but its React callback is read-current:
3737
+ // a layoutId introduced after mount can install the Presence settle owner on a later render.
3738
+ const onAllSettledRef = useRef(options.onAllSettled)
3739
+ onAllSettledRef.current = options.onAllSettled
3740
+
3741
+ // A stale UI settle may be queued to JS while a new JS command has already started. The epoch
3742
+ // changes synchronously on that command edge, so the JS dispatcher can reject the stale settle
3743
+ // without a synchronous UI-runtime re-check (which deadlocks iOS when the frame is mid-crossing).
3744
+ const loopEpochRef = useRef(0)
3745
+
3746
+ // Create the driver core once, BEFORE the frame callback, so the worklet captures the plain
3747
+ // driverId directly. The startLoop/settle closures reference `frameCallback` lazily — they only run
3748
+ // after a command lands (post-render), when the const below is initialized.
3749
+ const stateRef = useRef<{
3750
+ binding: WorkletDriverBinding
3751
+ driverId: number
3752
+ } | null>(null)
3753
+ if (stateRef.current === null) {
3754
+ stateRef.current = createDriverCore(
3755
+ { ...options, manualStepping: false },
3756
+ {
3757
+ nextEpoch: () => ++loopEpochRef.current,
3758
+ startLoop: () => frameCallback.setActive(true),
3759
+ },
3760
+ )
3761
+ // Seal-r16 F2: the proof app installs `__nativeMotionProofMarker` at module load; arm the
3762
+ // UI witness cells once so settle/completion counters stay zero-cost for ordinary consumers.
3763
+ if (isProofMarkerInstalled()) armUiProofWitnesses()
3764
+ }
3765
+ const driverId = stateRef.current.driverId
3766
+
3767
+ // The frame reaches this JS closure only through deliverDriverSettled() below. The primitive
3768
+ // epoch closes the command-vs-settle race without JS→UI `runSyncSerialized` re-entry from a
3769
+ // UI-originated dispatchEvent callback.
3770
+ const settleHandlerRef = useRef<((epoch: number) => void) | null>(null)
3771
+ if (settleHandlerRef.current === null) {
3772
+ settleHandlerRef.current = (settledEpoch) => {
3773
+ // Proof seam witness (no-op when the hook is unset): the epoch guard's verdict with both
3774
+ // epochs AND the driver id — delivered vs dropped-stale names whether the guard is the dead
3775
+ // link; the driver id attributes the verdict to the MotionView that owns it (finding B:
3776
+ // the 2026-08-18 capture could not attribute its three delivered lines).
3777
+ const proofMarker = (globalThis as ProofMarkerJsGlobal).__nativeMotionProofMarker
3778
+ if (loopEpochRef.current !== settledEpoch) {
3779
+ proofMarker?.(
3780
+ `settle-verdict dropped-stale driver=${driverId} settled=${settledEpoch} current=${loopEpochRef.current}`,
3781
+ )
3782
+ return
3783
+ }
3784
+ proofMarker?.(
3785
+ `settle-verdict delivered driver=${driverId} settled=${settledEpoch} current=${loopEpochRef.current}`,
3786
+ )
3787
+ frameCallback.setActive(false)
3788
+ onAllSettledRef.current?.()
3789
+ }
3790
+ }
3791
+ driverSettleCallbacks.set(driverId, settleHandlerRef.current)
3792
+
3793
+ // @frame-path — THE per-frame step (REQ-DRIVER-011 single callback; REQ-DRIVER-021 scan root).
3794
+ // Starts inactive; per frame it advances the whole graph by the vsync-reported delta (null on
3795
+ // the first invocation → 0; a post-background gap is clamped by core's dt ceiling inside
3796
+ // stepProp, REQ-CLOCK-003). Peek, don't create: a vsync racing dispose() must not resurrect the
3797
+ // deleted record.
3798
+ const frameStep = (frame: FrameInfo) => {
3799
+ 'worklet'
3800
+ const state = uiDriverStatePeek(driverId)
3801
+ if (state === null) return
3802
+ const dt = frame.timeSincePreviousFrame ?? 0
3803
+ const anyElementActive = uiStepAll(state, dt)
3804
+ uiEmitFrameValues(state)
3805
+ // The layout engine rides the SAME frame source (REQ-DRIVER-011) and joins the same settle
3806
+ // edge (REQ-DRIVER-020). Every conditional notification stages into the same primitive turn;
3807
+ // the seal below is the frame's only UI→JS crossing.
3808
+ let anyLayoutActive = false
3809
+ let staleReports:
3810
+ | readonly {
3811
+ readonly source: string
3812
+ readonly code: string
3813
+ readonly detail: string
3814
+ }[]
3815
+ | null = null
3816
+ if (state.layout !== null) {
3817
+ anyLayoutActive = uiLayoutStep(state.layout, dt)
3818
+ // The stale projection-report drain rides the SAME frame edge (F2): a layoutId-only scene
3819
+ // has no layoutTransition commit/frame drains, so every driver frame drains the tree's
3820
+ // ring to RN — a quiet frame gets the shared frozen empty array, allocating nothing.
3821
+ staleReports = state.layout.projection.tree.drainStaleReports()
3822
+ // Watch only identities with an interested completion owner. This is O(watchers), never a
3823
+ // scan of every mounted identity, and emits as soon as the watched id settles even while
3824
+ // another identity or property remains active.
3825
+ uiStageSettledLayoutWatchers(state)
3826
+ }
3827
+ if (!anyElementActive && !anyLayoutActive && !state.settleNotified) {
3828
+ state.settleNotified = true
3829
+ // The UI-side settle-emission witness (seal-r15 F2, proof-only; seal-r16 F2 gated): a
3830
+ // primitive counter on the UI global — never crossing on the channel under test. Armed
3831
+ // only when the proof app installed the marker (driver mount); ordinary consumers skip.
3832
+ // The JS-side last-pushed cell is filled from the envelope's DRIVER_SETTLE handler (not a
3833
+ // proof-only crossing — the frame retains its one-envelope traffic law).
3834
+ const proofGlobal = globalThis as ProofSettleChannelUiGlobal
3835
+ if (proofGlobal.__nativeMotionProofWitnessArmed === true) {
3836
+ proofGlobal.__nativeMotionProofSettleEmissions =
3837
+ (proofGlobal.__nativeMotionProofSettleEmissions ?? 0) + 1
3838
+ }
3839
+ state.emission.driverSettleEpoch = state.loopEpoch
3840
+ }
3841
+ uiSealFrameEmission(state, staleReports)
3842
+ }
3843
+ const frameCallback = useFrameCallback(frameStep, false)
3844
+
3845
+ // Quiesce-before-unmount (round 4 major 5): the component owning this driver is going away —
3846
+ // delete the UI-runtime state so remounts (gallery page changes, auto-mode key swaps) never
3847
+ // strand holder outputs and graph state in the worklet global. The binding survives re-renders
3848
+ // in stateRef. A StrictMode setup→cleanup→setup replay reaches this setup with the binding
3849
+ // already disposed — that replay is a deferred capability and fails loud (REQ-DRIVER-022;
3850
+ // BRIEF Decisions 2026-07-10), never a silent resurrection.
3851
+ useEffect(() => {
3852
+ const binding = stateRef.current!.binding
3853
+ assertSetupNotReplayedOnDisposed(binding.isDisposed())
3854
+ return () => {
3855
+ binding.dispose()
3856
+ }
3857
+ }, [])
3858
+
3859
+ return stateRef.current.binding
3860
+ }