@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,1302 @@
1
+ // The L3 no-flash layout transition (specs/L3-BUILD-PACKET.md §path B): a custom Reanimated
2
+ // LayoutAnimationFunction whose commit-hook `initialValues` paint the first post-commit frame at
3
+ // the OLD rect (zero wrong frames — proven by probe 2), and whose per-channel animations step
4
+ // CORE's generators (no forked math, REQ-DRIVER-003) toward the new layout. The inversion
5
+ // is expressed in origin-space by the commit hook itself (initialValues = current rect ≡ the
6
+ // FLIP inversion at t0); core owns the TRAJECTORY: `resolveSpringGenerator` or `timingGenerator`
7
+ // seeds each channel, `stepProp` advances it (dt-clamped internally, REQ-CLOCK-003), and a
8
+ // mid-flight spring re-seeds via `retargetSpring` from the live (value, velocity) — C0/C1
9
+ // continuity, the session's own interruption law. Reanimated's layout channels are UI-thread
10
+ // paint props, not
11
+ // layout-authoritative style: probe 2 measured that the committed layout stays truthful during
12
+ // the flight (REQ-LAYOUT-001 posture, no measurement feedback). Distinct from the forbidden
13
+ // sharedTransitionTag machinery, which stays off.
14
+ //
15
+ // Structure: `buildCoreLayoutChannel` is the PURE AnimationObject builder (deterministically
16
+ // driven by the checks rows via manual onStart/onFrame); `defineAnimation` wraps it only at the
17
+ // transition boundary (Reanimated resolves the definition worklet-side).
18
+
19
+ import {
20
+ resolveSpringGenerator,
21
+ retargetSpring,
22
+ stepProp,
23
+ timingGenerator,
24
+ type Generator,
25
+ type ProjectionTree,
26
+ type SpringConfig,
27
+ type TimingConfig,
28
+ } from '@unrulysystems/native-motion-core'
29
+ import {
30
+ defineAnimation,
31
+ makeMutable,
32
+ type AnimationObject,
33
+ type LayoutAnimationFunction,
34
+ } from 'react-native-reanimated'
35
+ import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets'
36
+ import {
37
+ compareMappedTransformKeys,
38
+ nativeTransformEntryName,
39
+ type MappedTransformKey,
40
+ } from './composeTransform'
41
+ import { refuseUnsafeSerializablePayload } from './serializablePayload'
42
+
43
+ // The channel animation's mutable state, decorated by Reanimated at runtime. `prop` mirrors the
44
+ // driver's PropStep shape (generator + accumulated elapsed) so the trajectory is core's exactly.
45
+ export interface CoreLayoutChannelAnimation extends AnimationObject<number> {
46
+ onStart: (
47
+ animation: CoreLayoutChannelAnimation,
48
+ value: number,
49
+ now: number,
50
+ previous: CoreLayoutChannelAnimation | null | undefined,
51
+ ) => void
52
+ onFrame: (animation: CoreLayoutChannelAnimation, now: number) => boolean
53
+ current: number
54
+ lastTimestamp: number
55
+ delayUntil: number
56
+ prop: { generator: Generator; elapsed: number; value: number; done: boolean } | null
57
+ completionReported: boolean
58
+ projectionCompletionReported: boolean
59
+ }
60
+
61
+ export type LayoutGeneratorConfig =
62
+ | { readonly type: 'spring'; readonly config: SpringConfig; readonly delayMs?: number }
63
+ | { readonly type: 'tween'; readonly config: TimingConfig; readonly delayMs?: number }
64
+
65
+ // One stale projection-tree report as the tree's drain surfaces it (iOS AUTO SIGABRT Phase 1.5):
66
+ // typed off the core tree's drain so the declaration sets of the core layout barrels stay pinned.
67
+ type StaleProjectionNodeReport = ReturnType<ProjectionTree['drainStaleReports']>[number]
68
+
69
+ // Deliver every drained stale-tree report to RN through the established scheduleOnRN severity
70
+ // channel (Phase 1 pattern). A quiet frame drains the shared frozen empty array — the index loop
71
+ // allocates nothing (REQ-DRIVER-021; for..of is banned here for the iterator).
72
+ function reportStaleProjectionReports(
73
+ projection: { readonly tree: Pick<ProjectionTree, 'drainStaleReports'> },
74
+ onStaleProjectionNodeRefusal?: (report: StaleProjectionNodeReport) => void,
75
+ ): void {
76
+ 'worklet'
77
+ const staleReports = projection.tree.drainStaleReports()
78
+ for (let index = 0; index < staleReports.length; index += 1) {
79
+ if (onStaleProjectionNodeRefusal !== undefined)
80
+ scheduleOnRN(onStaleProjectionNodeRefusal, staleReports[index]!)
81
+ }
82
+ }
83
+
84
+ // Retire ONE projection host-frame channel at its terminal edge — completion OR unseeded
85
+ // retirement (iOS AUTO SIGABRT seal r2, F2). Generation-scoped: at zero remaining channels the
86
+ // frame record and its completion owner release, exactly like the normal completion path. A
87
+ // module-level worklet so the produced channel's closure stays primitive-only (a nested callable
88
+ // capture serializes a second worklet graph — undefined on the iOS UI runtime).
89
+ function retireProjectionHostFrameChannel(
90
+ driverId: number,
91
+ componentId: string,
92
+ generation: number,
93
+ ownerKey?: number,
94
+ ): void {
95
+ 'worklet'
96
+ const projectionGlobal = globalThis as ProjectionHostUiGlobal
97
+ const driverFrames = projectionGlobal.__nativeMotionProjectionHostFrames?.[driverId]
98
+ const frame = driverFrames?.frames[componentId]
99
+ if (frame === undefined || frame.generation !== generation) return
100
+ frame.remainingChannels -= 1
101
+ if (frame.remainingChannels > 0) return
102
+ delete driverFrames!.frames[componentId]
103
+ if (ownerKey === undefined) return
104
+ const owners = projectionGlobal.__nativeMotionProjectionHostFrameOwners
105
+ const owner = owners?.[ownerKey]
106
+ if (
107
+ owner?.driverId === driverId &&
108
+ owner.componentId === componentId &&
109
+ owner.generation === generation
110
+ )
111
+ delete owners![ownerKey]
112
+ }
113
+
114
+ // Retire ONE transform-mirror flight channel at its terminal edge — completion OR unseeded
115
+ // retirement (seal r2, F2). Generation-scoped: at zero remaining channels the flight record
116
+ // releases, so no mirror stays awake on a flight its host has finished.
117
+ function retireLayoutTransformMirrorFlightChannel(key: number, generation: number): void {
118
+ 'worklet'
119
+ const flights = (globalThis as LayoutTransformMirrorUiGlobal)
120
+ .__nativeMotionLayoutTransformMirrorFlights
121
+ const flight = flights?.flights[key]
122
+ if (flight === undefined || flight.generation !== generation) return
123
+ flight.remainingChannels -= 1
124
+ if (flight.remainingChannels === 0) delete flights!.flights[key]
125
+ }
126
+
127
+ // Seal-r15 F2 proof witnesses for the layout-completion callback (the settle-gap card): plain
128
+ // primitive counters on the UI global — they never ride the channel under test, so a dead
129
+ // crossing cannot hide them. Seal-r16 F2: mutations are gated on the arm flag (set only when
130
+ // the proof app's marker is present); ordinary settle/completion paths allocate nothing. The
131
+ // JS-side last-pushed cell is filled from MotionView's layoutCompletion (engine-internal
132
+ // reader) — this worklet body must not capture non-worklet helpers (worklet-closures gate).
133
+ interface ProofLayoutCallbackUiGlobal {
134
+ __nativeMotionProofWitnessArmed?: boolean
135
+ __nativeMotionProofLayoutCallbackInvocations?: number
136
+ __nativeMotionProofLayoutCallbackEpochDrops?: number
137
+ __nativeMotionProofLayoutCallbackCancelled?: number
138
+ __nativeMotionProofLayoutOnCompleteScheduled?: number
139
+ }
140
+
141
+ /**
142
+ * A property-driver transform that must remain visible while Reanimated owns the same host's
143
+ * layout frame. Reanimated's layout proxy receives only geometry values, so each layout frame
144
+ * otherwise replaces the driver's whole `transform` array. The mirror makes that array part of
145
+ * the layout result and reads the driver's UI-runtime value at the same frame boundary.
146
+ */
147
+ export interface LayoutTransformMirror {
148
+ readonly key: MappedTransformKey
149
+ readonly shared: { readonly value: number }
150
+ // The driver owns both values on the UI runtime. `target` changes with the currently active
151
+ // command (rather than capturing the base `animate` target), while `active` keeps gesture
152
+ // holds live until their release command takes ownership.
153
+ readonly target: { readonly value: number }
154
+ readonly active: { readonly value: boolean }
155
+ }
156
+
157
+ /**
158
+ * Primitive address of the ancestor-owned UI projection ledger. The layout callback captures this
159
+ * data only; it never captures a callable worklet or crosses back to JS on a host frame.
160
+ */
161
+ export interface LayoutProjectionHostFeed {
162
+ readonly driverId: number
163
+ readonly componentId: string
164
+ }
165
+
166
+ type LayoutTransformMirrorAnimation = AnimationObject<string | number> & {
167
+ current: string | number
168
+ }
169
+
170
+ interface MirrorConfigurableLayoutTransition extends LayoutAnimationFunction {
171
+ configureLayoutTransformMirrors(enabled: boolean): void
172
+ configureProjectionHostFeed(feed: LayoutProjectionHostFeed | null): void
173
+ }
174
+
175
+ /** Configure a concrete host transition with its UI-resident projection-tree address. */
176
+ export function configureLayoutProjectionHostFeed(
177
+ transition: LayoutAnimationFunction,
178
+ feed: LayoutProjectionHostFeed | null,
179
+ ): void {
180
+ ;(transition as Partial<MirrorConfigurableLayoutTransition>).configureProjectionHostFeed?.(feed)
181
+ }
182
+
183
+ // Guest-side SharedValue writes (`cell.value = …` from JS) ride reanimated's modify-scheduling
184
+ // lane: each write becomes scheduleOnUI(() => { mutable.value = … }), then worklets'
185
+ // flushUIQueue wraps that job as createSerializable(() => workletFunction(...workletArgs)).
186
+ // The live hang tail was exactly that shape:
187
+ // /__closure/workletFunction/__closure/mutable/__closure
188
+ // (PROOF §SIGABRT-1 wedge recurrence Q3). Layout-enabled hosts call configureProjectionHostFeed
189
+ // on every render — guest writes there would mint that graph on the gallery/lightbox path.
190
+ // Module-level worklet + primitive args (M1-r19 / reassert shape): the UI lane mutates the
191
+ // cells; the guest only serializes a named worklet and branded SharedValue refs.
192
+ function setProjectionHostFeedCellsOnUI(
193
+ driverIdCell: { value: number },
194
+ componentIdCell: { value: string },
195
+ driverId: number,
196
+ componentId: string,
197
+ ): void {
198
+ 'worklet'
199
+ driverIdCell.value = driverId
200
+ componentIdCell.value = componentId
201
+ }
202
+
203
+ function createLayoutTransformMirror(
204
+ mirror: LayoutTransformMirror,
205
+ completionKey: number | undefined,
206
+ flightGeneration: number | undefined,
207
+ ): LayoutTransformMirrorAnimation {
208
+ 'worklet'
209
+ return {
210
+ onStart(animation) {
211
+ 'worklet'
212
+ const current = mirror.shared.value
213
+ animation.current =
214
+ mirror.key === 'rotate' ||
215
+ mirror.key === 'rotateX' ||
216
+ mirror.key === 'rotateY' ||
217
+ mirror.key === 'skewX' ||
218
+ mirror.key === 'skewY'
219
+ ? `${current}deg`
220
+ : current
221
+ },
222
+ onFrame(animation) {
223
+ 'worklet'
224
+ const current = mirror.shared.value
225
+ animation.current =
226
+ mirror.key === 'rotate' ||
227
+ mirror.key === 'rotateX' ||
228
+ mirror.key === 'rotateY' ||
229
+ mirror.key === 'skewX' ||
230
+ mirror.key === 'skewY'
231
+ ? `${current}deg`
232
+ : current
233
+ // Reanimated permanently retires an animation after its first `true` frame. A property can
234
+ // be idle at host-flight start yet activate later through a gesture, exit, or bound value;
235
+ // keep this transform mirror awake until that real geometry flight ends. This nested frame
236
+ // worklet must not capture another callable worklet: Reanimated serializes those closures
237
+ // independently and iOS resolved the helper as undefined at runtime. Read the primitive
238
+ // UI-global ledger directly instead.
239
+ if (completionKey !== undefined && flightGeneration !== undefined) {
240
+ const flight = (globalThis as LayoutTransformMirrorUiGlobal)
241
+ .__nativeMotionLayoutTransformMirrorFlights?.flights[completionKey]
242
+ if (flight?.generation === flightGeneration) return false
243
+ }
244
+ // Property and layout consume the same gated transition. Keep the layout's transform
245
+ // entry live until the property lane reaches its target; then both complete together.
246
+ return !mirror.active.value && Math.abs(current - mirror.target.value) <= 0.000_001
247
+ },
248
+ current:
249
+ mirror.key === 'rotate' ||
250
+ mirror.key === 'rotateX' ||
251
+ mirror.key === 'rotateY' ||
252
+ mirror.key === 'skewX' ||
253
+ mirror.key === 'skewY'
254
+ ? `${mirror.shared.value}deg`
255
+ : mirror.shared.value,
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Preserve transform targets from the driver while a host-layout transition writes its own frame.
261
+ * A transform key absent from this list remains solely driver-owned, so ordinary property-only
262
+ * animation never acquires a layout dependency.
263
+ */
264
+ export function mirrorLayoutTransformValues(
265
+ transition: LayoutAnimationFunction,
266
+ mirrors: readonly LayoutTransformMirror[],
267
+ completionEpoch?: LayoutCompletionEpoch,
268
+ ): LayoutAnimationFunction | undefined {
269
+ // The host transition creates its UI-global ledger only when an actual transform mirror needs
270
+ // to observe it. `MotionView` always visits this adapter, including the zero-mirror case.
271
+ const mirrorConfigurable = transition as Partial<MirrorConfigurableLayoutTransition>
272
+ mirrorConfigurable.configureLayoutTransformMirrors?.(mirrors.length > 0)
273
+ if (mirrors.length === 0) return transition
274
+ // The wedge law (iOS AUTO SIGABRT): every record the produced wrapper captures must be a
275
+ // finite, acyclic payload — a cyclic/unbounded mirror graph would peg the main thread inside
276
+ // SerializableObject::toJSValue, not fail at schedule time. Dev throws at this factory edge;
277
+ // production reports and the mirror lane is REFUSED (seal r7, F2): undefined means no layout
278
+ // transition at all — the host keeps its last valid static layout, never an unmirrored
279
+ // transition substituted silently (that fallback erases/freezes live transforms).
280
+ if (refuseUnsafeSerializablePayload(mirrors, 'mirrors') !== null) return undefined
281
+ // Do not leak authoring/mount order into the host proxy. `composeTransform` is the ordinary
282
+ // lane's canonical producer; this mirror follows the same single source of truth.
283
+ const orderedMirrors = [...mirrors].sort((a, b) => compareMappedTransformKeys(a.key, b.key))
284
+ // A NAMED declarator (iOS AUTO SIGABRT seal, F1): the closure graph roots produced worklets by
285
+ // declarator name — an anonymous wrapper would take a positional plugin alias and evade the
286
+ // closure roots entirely (the exact device-only invisibility the seal fixes).
287
+ const mirroredTransition: LayoutAnimationFunction = (values) => {
288
+ 'worklet'
289
+ const result = transition(values)
290
+ const completionKey = completionEpoch?.key
291
+ const flightGeneration =
292
+ completionKey === undefined
293
+ ? undefined
294
+ : currentLayoutTransformMirrorFlightGeneration(completionKey)
295
+ return {
296
+ ...result,
297
+ animations: {
298
+ ...result.animations,
299
+ transform: orderedMirrors.map((mirror) => ({
300
+ [nativeTransformEntryName(mirror.key)]: createLayoutTransformMirror(
301
+ mirror,
302
+ completionKey,
303
+ flightGeneration,
304
+ ),
305
+ })),
306
+ },
307
+ } as unknown as ReturnType<LayoutAnimationFunction>
308
+ }
309
+ return mirroredTransition
310
+ }
311
+
312
+ // A layout transition function is read-current on every React render, while Reanimated may still
313
+ // deliver a terminal callback from the prior host commit. The host key crosses rebuilt functions
314
+ // as a primitive; the mutable epoch lives only in the UI runtime because Worklets deserializes a
315
+ // fresh ordinary-object capture for every rebuilt function.
316
+ export interface LayoutCompletionEpoch {
317
+ readonly key: number
318
+ }
319
+
320
+ let nextLayoutCompletionEpochKey = 1
321
+
322
+ // The UI-global owner registry needs a hard bound: cancellation and teardown release owners,
323
+ // while a pathological live tree fails loud instead of retaining unbounded completion state.
324
+ // Never evict a live owner — that would silently suppress its public callback.
325
+ export const MAX_ACTIVE_LAYOUT_COMPLETION_OWNERS = 512
326
+
327
+ // Transform mirrors have a separate UI-global lifetime from public completion callbacks. Bound
328
+ // them independently: never evict a live mirror, because its host can still receive an authority
329
+ // change before its geometry settles.
330
+ export const MAX_ACTIVE_LAYOUT_TRANSFORM_MIRROR_FLIGHTS = 512
331
+
332
+ // Host-frame records are separate UI-global state from completion/mirror owners. They are active
333
+ // only while a host layout channel can still publish geometry into the projection tree, and must
334
+ // never become an unbounded tombstone registry when flights are cancelled or hosts unmount.
335
+ export const MAX_ACTIVE_PROJECTION_HOST_FRAMES = 512
336
+
337
+ export function createLayoutCompletionEpoch(): LayoutCompletionEpoch {
338
+ return { key: nextLayoutCompletionEpochKey++ }
339
+ }
340
+
341
+ interface LayoutCompletionUiGlobal {
342
+ __nativeMotionLayoutCompletionState?: {
343
+ nextGeneration: number
344
+ readonly activeEpochs: Record<number, number>
345
+ }
346
+ }
347
+
348
+ // A host layout flight and its transform mirrors run as independent Reanimated animations. This
349
+ // UI-global ledger gives them one primitive, generation-guarded lifecycle without serializing a
350
+ // mutable capture into each worklet. A superseding host flight replaces the record, so old channel
351
+ // completions cannot wake or finish the new flight.
352
+ interface LayoutTransformMirrorUiGlobal {
353
+ __nativeMotionLayoutTransformMirrorFlights?: {
354
+ nextGeneration: number
355
+ readonly flights: Record<number, { readonly generation: number; remainingChannels: number }>
356
+ }
357
+ }
358
+
359
+ // The host layout callback runs inside Reanimated, while the projection tree is owned by the
360
+ // driver's UI-global ledger. Keep the bridge structural and primitive-only: importing/capturing
361
+ // driver worklets here would serialize a second callable graph and reproduces the iOS crash this
362
+ // module is specifically designed to avoid.
363
+ interface ProjectionHostFrame {
364
+ readonly generation: number
365
+ currentOriginX: number
366
+ currentOriginY: number
367
+ currentWidth: number
368
+ currentHeight: number
369
+ readonly targetOriginX: number
370
+ readonly targetOriginY: number
371
+ readonly targetWidth: number
372
+ readonly targetHeight: number
373
+ remainingChannels: number
374
+ }
375
+
376
+ interface ProjectionHostFrames {
377
+ nextGeneration: number
378
+ readonly frames: Record<string, ProjectionHostFrame>
379
+ }
380
+
381
+ /**
382
+ * The layout callback is serialized to Reanimated's UI runtime before a host can begin a
383
+ * geometry flight. A lexical `let` mutated by render-time configuration is cloned as its initial
384
+ * `null` value into that worklet, which silently drops the parent frame on device. These two
385
+ * cells are the deliberate cross-runtime address: they are set at the React edge and read only
386
+ * as primitive values in the frame callback.
387
+ */
388
+ interface ProjectionHostFeedCells {
389
+ readonly driverId: { value: number }
390
+ readonly componentId: { value: string }
391
+ }
392
+
393
+ interface ProjectionHostUiGlobal {
394
+ __nativeMotionProjectionHostFrames?: Record<number, ProjectionHostFrames>
395
+ __nativeMotionProjectionHostFrameOwners?: Record<
396
+ number,
397
+ { readonly driverId: number; readonly componentId: string; readonly generation: number }
398
+ >
399
+ __nativeMotionDrivers?: Record<
400
+ number,
401
+ {
402
+ readonly layout: {
403
+ readonly projection: {
404
+ readonly tree: {
405
+ setHostFrame(
406
+ componentId: string,
407
+ currentX: number,
408
+ currentY: number,
409
+ currentWidth: number,
410
+ currentHeight: number,
411
+ targetX: number,
412
+ targetY: number,
413
+ targetWidth: number,
414
+ targetHeight: number,
415
+ ): void
416
+ clearProjection(componentId: string): void
417
+ project(): void
418
+ output(componentId: string): {
419
+ readonly translateX: number
420
+ readonly translateY: number
421
+ readonly scaleX: number
422
+ readonly scaleY: number
423
+ readonly borderRadius: { readonly x: number; readonly y: number } | null
424
+ readonly targetDeltaScaleX: number
425
+ readonly targetDeltaScaleY: number
426
+ readonly treeScaleX: number
427
+ readonly treeScaleY: number
428
+ }
429
+ drainStaleReports(): readonly StaleProjectionNodeReport[]
430
+ }
431
+ readonly records: readonly {
432
+ readonly componentId: string
433
+ readonly outputs: {
434
+ readonly translateX: { value: number }
435
+ readonly translateY: { value: number }
436
+ readonly scaleX: { value: number }
437
+ readonly scaleY: { value: number }
438
+ readonly borderRadiusX: { value: number }
439
+ readonly borderRadiusY: { value: number }
440
+ readonly targetDeltaScaleX: { value: number }
441
+ readonly targetDeltaScaleY: { value: number }
442
+ readonly treeScaleX: { value: number }
443
+ readonly treeScaleY: { value: number }
444
+ }
445
+ }[]
446
+ dirty: boolean
447
+ }
448
+ } | null
449
+ }
450
+ >
451
+ }
452
+
453
+ function layoutTransformMirrorFlights(): NonNullable<
454
+ LayoutTransformMirrorUiGlobal['__nativeMotionLayoutTransformMirrorFlights']
455
+ > {
456
+ 'worklet'
457
+ const global = globalThis as LayoutTransformMirrorUiGlobal
458
+ if (global.__nativeMotionLayoutTransformMirrorFlights === undefined) {
459
+ global.__nativeMotionLayoutTransformMirrorFlights = { nextGeneration: 0, flights: {} }
460
+ }
461
+ return global.__nativeMotionLayoutTransformMirrorFlights
462
+ }
463
+
464
+ // Keep this module-scope worklet primitive-only. In particular, do not return a worklet closure
465
+ // from here for `defineAnimation` to capture: Worklets would serialize that nested graph per host
466
+ // flight, recreating the iOS AB-BA wedge that attempt 15 intentionally removed.
467
+ function beginLayoutTransformMirrorFlight(key: number, channelCount: number): number | undefined {
468
+ 'worklet'
469
+ const state = layoutTransformMirrorFlights()
470
+ if (
471
+ state.flights[key] === undefined &&
472
+ Object.keys(state.flights).length >= MAX_ACTIVE_LAYOUT_TRANSFORM_MIRROR_FLIGHTS
473
+ ) {
474
+ return undefined
475
+ }
476
+ state.nextGeneration += 1
477
+ const generation = state.nextGeneration
478
+ state.flights[key] = { generation, remainingChannels: channelCount }
479
+ return generation
480
+ }
481
+
482
+ function currentLayoutTransformMirrorFlightGeneration(key: number): number | undefined {
483
+ 'worklet'
484
+ return (globalThis as LayoutTransformMirrorUiGlobal).__nativeMotionLayoutTransformMirrorFlights
485
+ ?.flights[key]?.generation
486
+ }
487
+
488
+ function beginLayoutCompletionFlight(key: number): number | undefined {
489
+ 'worklet'
490
+ const global = globalThis as LayoutCompletionUiGlobal
491
+ if (global.__nativeMotionLayoutCompletionState === undefined) {
492
+ global.__nativeMotionLayoutCompletionState = {
493
+ nextGeneration: 0,
494
+ activeEpochs: {},
495
+ }
496
+ }
497
+ const state = global.__nativeMotionLayoutCompletionState
498
+ // Generations are globally monotonic, rather than per-key counters discarded on completion.
499
+ // A late A callback can therefore never equal a later C callback after B completed and released
500
+ // this host key. The map holds ACTIVE owners only: an entry is released by its own terminal
501
+ // callback, and a new flight on the same host supersedes the old one. Do not evict another
502
+ // active host to impose an arbitrary cache limit — that would suppress its real completion.
503
+ if (
504
+ state.activeEpochs[key] === undefined &&
505
+ Object.keys(state.activeEpochs).length >= MAX_ACTIVE_LAYOUT_COMPLETION_OWNERS
506
+ ) {
507
+ return undefined
508
+ }
509
+ state.nextGeneration += 1
510
+ const next = state.nextGeneration
511
+ state.activeEpochs[key] = next
512
+ return next
513
+ }
514
+
515
+ function isCurrentLayoutCompletionFlight(key: number, epoch: number): boolean {
516
+ 'worklet'
517
+ const global = globalThis as LayoutCompletionUiGlobal
518
+ return global.__nativeMotionLayoutCompletionState?.activeEpochs[key] === epoch
519
+ }
520
+
521
+ function completeLayoutCompletionFlight(key: number, epoch: number): void {
522
+ 'worklet'
523
+ const global = globalThis as LayoutCompletionUiGlobal
524
+ if (global.__nativeMotionLayoutCompletionState?.activeEpochs[key] === epoch)
525
+ delete global.__nativeMotionLayoutCompletionState.activeEpochs[key]
526
+ }
527
+
528
+ // The public start callback's per-LOGICAL-flight qualification ledger (U7e). The pin
529
+ // fires onLayoutAnimationStart from didUpdate when the rounded target changed or no
530
+ // animation is active (create-projection-node.ts:527-560; boxEqualsRounded). A start-
531
+ // driven rerender rebuilds the worklet and Reanimated cancels the replaced animation —
532
+ // that rebound is the SAME flight, so cancel must NOT release this ledger (device proof
533
+ // 870c5b64 / f841bc52). Settle also must not: Reanimated re-runs the layout function
534
+ // at complete (iOS 21e910d0: starts@[+2037,+5063]), which is extra host granularity,
535
+ // not a pin didUpdate. A later React generation overwrites and starts (review M2).
536
+ // Lifetime teardown still releases.
537
+ interface LayoutFlightStartUiGlobal {
538
+ __nativeMotionLayoutFlightStarts?: {
539
+ readonly flights: Record<number, { generation: number }>
540
+ }
541
+ }
542
+
543
+ function beginLayoutFlightStart(key: number, generation: number): boolean {
544
+ 'worklet'
545
+ const global = globalThis as LayoutFlightStartUiGlobal
546
+ if (global.__nativeMotionLayoutFlightStarts === undefined) {
547
+ global.__nativeMotionLayoutFlightStarts = { flights: {} }
548
+ }
549
+ const flights = global.__nativeMotionLayoutFlightStarts.flights
550
+ const active = flights[key]
551
+ // Same React didUpdate (own geometry + inherited parent generation): a start-driven
552
+ // rerender or a second Yoga commit can carry a different rounded box (iOS 66f7ccfc
553
+ // flight A; Android C+15ms). That is not a pin retarget. A new generation is.
554
+ if (active !== undefined && active.generation === generation) return false
555
+ flights[key] = { generation }
556
+ return true
557
+ }
558
+
559
+ function clearLayoutCompletionEpoch(key: number): void {
560
+ 'worklet'
561
+ const global = globalThis as LayoutCompletionUiGlobal
562
+ if (global.__nativeMotionLayoutCompletionState !== undefined)
563
+ delete global.__nativeMotionLayoutCompletionState.activeEpochs[key]
564
+ // Inlined start-qualification release: this worklet crosses by reference through scheduleOnUI,
565
+ // so its body stays primitive-only (the projection release below carries the same law).
566
+ const flightStartGlobal = globalThis as LayoutFlightStartUiGlobal
567
+ if (flightStartGlobal.__nativeMotionLayoutFlightStarts !== undefined)
568
+ delete flightStartGlobal.__nativeMotionLayoutFlightStarts.flights[key]
569
+ const mirrorGlobal = globalThis as LayoutTransformMirrorUiGlobal
570
+ if (mirrorGlobal.__nativeMotionLayoutTransformMirrorFlights !== undefined)
571
+ delete mirrorGlobal.__nativeMotionLayoutTransformMirrorFlights.flights[key]
572
+ // Inlined projection host-frame + tree release: this worklet crosses to the UI runtime by
573
+ // reference through scheduleOnUI, so its body must stay primitive-only — a helper worklet
574
+ // call resolves undefined after the crossing. Cancel/teardown must clear BOTH the UI-global
575
+ // registry and the tree cell so cancelled mid-flight geometry cannot be re-projected
576
+ // (review major c7e4a91f2b08).
577
+ const projectionGlobal = globalThis as ProjectionHostUiGlobal
578
+ const owner = projectionGlobal.__nativeMotionProjectionHostFrameOwners?.[key]
579
+ if (owner !== undefined) {
580
+ const frame =
581
+ projectionGlobal.__nativeMotionProjectionHostFrames?.[owner.driverId]?.frames[
582
+ owner.componentId
583
+ ]
584
+ if (frame !== undefined && frame.generation === owner.generation)
585
+ delete projectionGlobal.__nativeMotionProjectionHostFrames?.[owner.driverId]?.frames[
586
+ owner.componentId
587
+ ]
588
+ delete projectionGlobal.__nativeMotionProjectionHostFrameOwners?.[key]
589
+ const projection = projectionGlobal.__nativeMotionDrivers?.[owner.driverId]?.layout?.projection
590
+ if (projection !== undefined) {
591
+ projection.tree.clearProjection(owner.componentId)
592
+ projection.tree.project()
593
+ for (let index = 0; index < projection.records.length; index += 1) {
594
+ const record = projection.records[index]!
595
+ const output = projection.tree.output(record.componentId)
596
+ record.outputs.translateX.value = output.translateX
597
+ record.outputs.translateY.value = output.translateY
598
+ record.outputs.scaleX.value = output.scaleX
599
+ record.outputs.scaleY.value = output.scaleY
600
+ record.outputs.borderRadiusX.value = output.borderRadius?.x ?? Number.NaN
601
+ record.outputs.borderRadiusY.value = output.borderRadius?.y ?? Number.NaN
602
+ }
603
+ projection.dirty = false
604
+ }
605
+ }
606
+ }
607
+
608
+ // React owns host teardown while the completion registry lives on UI. Layout enablement owns a
609
+ // key lifetime: a disabled→enabled host receives a fresh key, so delayed old-lifetime cleanup
610
+ // cannot release the replacement flight's owner.
611
+ export function teardownLayoutCompletionEpoch(epoch: LayoutCompletionEpoch): void {
612
+ scheduleOnUI(clearLayoutCompletionEpoch, epoch.key)
613
+ }
614
+
615
+ // Pure builder for one layout channel (originX/originY/width/height) moving toward `to` with
616
+ // core's math. Springs re-seed through retargetSpring for C1 velocity carry. Tweens seed core's
617
+ // timing generator from the host-provided live value; timing intentionally ignores velocity.
618
+ export function buildCoreLayoutChannel(
619
+ to: number,
620
+ transition: LayoutGeneratorConfig,
621
+ mirrorFlightKey?: number,
622
+ mirrorFlightGeneration?: number,
623
+ projectionDriverId?: number,
624
+ projectionComponentId?: string,
625
+ projectionGeneration?: number,
626
+ projectionChannel?: 'originX' | 'originY' | 'width' | 'height',
627
+ onUnseededChannelRefusal?: (code: string) => void,
628
+ onStaleProjectionNodeRefusal?: (report: StaleProjectionNodeReport) => void,
629
+ ): CoreLayoutChannelAnimation {
630
+ 'worklet'
631
+ return {
632
+ onStart(animation, value, now, previous) {
633
+ 'worklet'
634
+ // A live previous channel means this layout change interrupted an in-flight one: re-seed
635
+ // from its (value, velocity) via retargetSpring — never from rest (the C1 law). A settled
636
+ // or absent previous seeds from rest at the host-provided start value. Reanimated passes
637
+ // `undefined` (not null) when there is no previous animation — check both, or the very
638
+ // first commit crashes the UI runtime (observed on-device, packet Findings).
639
+ let generator: Generator
640
+ if (transition.type === 'tween') {
641
+ generator = timingGenerator(to, transition.config)({ from: value, velocity: 0 })
642
+ } else {
643
+ const livePrevious =
644
+ previous !== null &&
645
+ previous !== undefined &&
646
+ previous.prop !== null &&
647
+ !previous.prop.done
648
+ ? previous.prop
649
+ : null
650
+ generator =
651
+ livePrevious !== null
652
+ ? retargetSpring(livePrevious.generator, livePrevious.elapsed, to, transition.config)
653
+ : resolveSpringGenerator(to, transition.config)({ from: value, velocity: 0 })
654
+ }
655
+ // Motion's JSAnimation subtracts its signed delay from currentTime before sampling. A
656
+ // negative delay therefore starts this channel partway through CORE's trajectory, rather
657
+ // than merely making its positive-delay hold deadline lie in the past.
658
+ const delayMs = transition.delayMs ?? 0
659
+ animation.prop = {
660
+ generator,
661
+ elapsed: delayMs < 0 ? -delayMs : 0,
662
+ value,
663
+ done: false,
664
+ }
665
+ animation.lastTimestamp = now
666
+ // Only a positive delay holds frames. Negative delay is represented by the seeded CORE
667
+ // elapsed clock above, so every channel starts from the same advanced trajectory point.
668
+ animation.delayUntil = now + (delayMs > 0 ? delayMs : 0)
669
+ animation.current = value
670
+ },
671
+ onFrame(animation, now) {
672
+ 'worklet'
673
+ const prop = animation.prop
674
+ if (prop === null) {
675
+ // A frame before onStart is a Reanimated contract violation — but a UI-thread throw is
676
+ // a bare std::terminate on device (iOS AUTO SIGABRT), never a refusal. Report the
677
+ // unseeded channel to RN through the established scheduleOnRN severity channel and
678
+ // retire it: returning true ends the animation, preserving the last valid visual
679
+ // state (never seed or coast a corrupt channel).
680
+ if (onUnseededChannelRefusal !== undefined)
681
+ scheduleOnRN(onUnseededChannelRefusal, 'layout-channel-unseeded')
682
+ // Terminal for EVERY owner (seal r2, F2): retire this channel from both flight ledgers
683
+ // exactly as a completion would — generation-scoped — so neither ledger waits forever
684
+ // on a channel that never started (mirrors would stay awake; the seeded host-frame
685
+ // record would be retained on a still-mounted host).
686
+ if (
687
+ projectionDriverId !== undefined &&
688
+ projectionComponentId !== undefined &&
689
+ projectionGeneration !== undefined
690
+ )
691
+ retireProjectionHostFrameChannel(
692
+ projectionDriverId,
693
+ projectionComponentId,
694
+ projectionGeneration,
695
+ mirrorFlightKey,
696
+ )
697
+ if (mirrorFlightKey !== undefined && mirrorFlightGeneration !== undefined)
698
+ retireLayoutTransformMirrorFlightChannel(mirrorFlightKey, mirrorFlightGeneration)
699
+ return true
700
+ }
701
+ const frameStart = animation.lastTimestamp
702
+ animation.lastTimestamp = now
703
+ if (now < animation.delayUntil) return false
704
+ // Delay is a clock offset around CORE's generator, identical for every channel. Once the
705
+ // window ends, only the post-delay slice reaches stepProp; trajectory math stays CORE's.
706
+ const activeStart = frameStart < animation.delayUntil ? animation.delayUntil : frameStart
707
+ stepProp(prop, now - activeStart)
708
+ animation.current = prop.value
709
+ // A host-layout channel is the only faithful per-frame source for its geometry. The frame
710
+ // record and projection tree both live on UI; this is deliberately direct global mutation,
711
+ // not a nested callable-worklet capture or a JS↔UI crossing.
712
+ if (
713
+ projectionDriverId !== undefined &&
714
+ projectionComponentId !== undefined &&
715
+ projectionGeneration !== undefined &&
716
+ projectionChannel !== undefined
717
+ ) {
718
+ const projectionGlobal = globalThis as ProjectionHostUiGlobal
719
+ const frame =
720
+ projectionGlobal.__nativeMotionProjectionHostFrames?.[projectionDriverId]?.frames[
721
+ projectionComponentId
722
+ ]
723
+ if (frame !== undefined && frame.generation === projectionGeneration) {
724
+ switch (projectionChannel) {
725
+ case 'originX':
726
+ frame.currentOriginX = prop.value
727
+ break
728
+ case 'originY':
729
+ frame.currentOriginY = prop.value
730
+ break
731
+ case 'width':
732
+ frame.currentWidth = prop.value
733
+ break
734
+ case 'height':
735
+ frame.currentHeight = prop.value
736
+ break
737
+ }
738
+ const projection =
739
+ projectionGlobal.__nativeMotionDrivers?.[projectionDriverId]?.layout?.projection
740
+ if (projection !== undefined) {
741
+ projection.tree.setHostFrame(
742
+ projectionComponentId,
743
+ frame.currentOriginX,
744
+ frame.currentOriginY,
745
+ frame.currentWidth,
746
+ frame.currentHeight,
747
+ frame.targetOriginX,
748
+ frame.targetOriginY,
749
+ frame.targetWidth,
750
+ frame.targetHeight,
751
+ )
752
+ projection.tree.project()
753
+ for (let index = 0; index < projection.records.length; index += 1) {
754
+ const record = projection.records[index]!
755
+ const output = projection.tree.output(record.componentId)
756
+ record.outputs.translateX.value = output.translateX
757
+ record.outputs.translateY.value = output.translateY
758
+ record.outputs.scaleX.value = output.scaleX
759
+ record.outputs.scaleY.value = output.scaleY
760
+ record.outputs.borderRadiusX.value = output.borderRadius?.x ?? Number.NaN
761
+ record.outputs.borderRadiusY.value = output.borderRadius?.y ?? Number.NaN
762
+ }
763
+ projection.dirty = false
764
+ // Drain the tree's stale-report ring on this frame edge (iOS AUTO SIGABRT Phase
765
+ // 1.5): teardown races surface through the RN severity channel, never a UI throw.
766
+ reportStaleProjectionReports(projection, onStaleProjectionNodeRefusal)
767
+ }
768
+ if (prop.done && !animation.projectionCompletionReported) {
769
+ animation.projectionCompletionReported = true
770
+ retireProjectionHostFrameChannel(
771
+ projectionDriverId,
772
+ projectionComponentId,
773
+ projectionGeneration,
774
+ mirrorFlightKey,
775
+ )
776
+ }
777
+ } else if (frame === undefined) {
778
+ // The host detached mid-flight and its frame record is gone (iOS AUTO SIGABRT Phase
779
+ // 1.5): retire the torn-down flight at this frame boundary — prop.done runs the
780
+ // completion bookkeeping below, no further frames are scheduled for the host, and the
781
+ // last painted value (animation.current, already advanced this frame) is preserved.
782
+ prop.done = true
783
+ }
784
+ }
785
+ if (prop.done && !animation.completionReported) {
786
+ animation.completionReported = true
787
+ if (mirrorFlightKey !== undefined && mirrorFlightGeneration !== undefined) {
788
+ // This onFrame belongs to Reanimated's deferred defineAnimation factory. The mirror
789
+ // flight ledger retires through a module-level worklet so this closure stays
790
+ // primitive-only (a nested callable serializes a second worklet graph — undefined on
791
+ // the iOS UI runtime). The registry data itself is UI-global.
792
+ retireLayoutTransformMirrorFlightChannel(mirrorFlightKey, mirrorFlightGeneration)
793
+ }
794
+ }
795
+ return prop.done
796
+ },
797
+ current: to,
798
+ lastTimestamp: 0,
799
+ delayUntil: 0,
800
+ prop: null,
801
+ completionReported: false,
802
+ projectionCompletionReported: false,
803
+ }
804
+ }
805
+
806
+ // The deterministic verification screen owns this spring-specific seam. Preserve it as a thin
807
+ // wrapper so the generic channel adds tween support without invalidating the existing spring proof.
808
+ export function buildCoreSpringChannel(
809
+ to: number,
810
+ spring: SpringConfig,
811
+ ): CoreLayoutChannelAnimation {
812
+ 'worklet'
813
+ return buildCoreLayoutChannel(to, { type: 'spring', config: spring })
814
+ }
815
+
816
+ // The default flight spring — the ratified session default (LAYOUT_PROJECTION_SPRING profile).
817
+ const DEFAULT_SPRING: SpringConfig = { stiffness: 550, damping: 45 }
818
+ const DEFAULT_TRANSITION: LayoutGeneratorConfig = {
819
+ type: 'spring',
820
+ config: DEFAULT_SPRING,
821
+ delayMs: 0,
822
+ }
823
+
824
+ export type LayoutTransitionChannels = 'both' | 'position' | 'size' | 'preserve-aspect'
825
+
826
+ // Motion's pinned projection oracle treats aspect ratios within 0.2 as preserved. This constant
827
+ // configures channel selection only; CORE remains the sole owner of each selected channel's math.
828
+ const PRESERVED_ASPECT_RATIO_TOLERANCE = 0.2
829
+
830
+ // One channel wrapped for Reanimated's layout-animation slot. The `starting` decoration value is
831
+ // the channel's current paint value; the factory builds the pure channel worklet-side.
832
+ function coreLayoutChannel(
833
+ to: number,
834
+ transition: LayoutGeneratorConfig,
835
+ mirrorFlightKey?: number,
836
+ mirrorFlightGeneration?: number,
837
+ projectionDriverId?: number,
838
+ projectionComponentId?: string,
839
+ projectionGeneration?: number,
840
+ projectionChannel?: 'originX' | 'originY' | 'width' | 'height',
841
+ onUnseededChannelRefusal?: (code: string) => void,
842
+ onStaleProjectionNodeRefusal?: (report: StaleProjectionNodeReport) => void,
843
+ ): number {
844
+ 'worklet'
845
+ // A NAMED declarator (iOS AUTO SIGABRT seal, F1 — the closure graph roots produced worklets by
846
+ // declarator name; an inline arrow would take a positional plugin alias and evade the roots).
847
+ // Captures stay primitives only: the lifecycle mutation stays in the module-scope worklet
848
+ // above, never in a dynamically-created completion worklet captured by this factory.
849
+ const coreLayoutAnimationFactory = (): CoreLayoutChannelAnimation => {
850
+ 'worklet'
851
+ return buildCoreLayoutChannel(
852
+ to,
853
+ transition,
854
+ mirrorFlightKey,
855
+ mirrorFlightGeneration,
856
+ projectionDriverId,
857
+ projectionComponentId,
858
+ projectionGeneration,
859
+ projectionChannel,
860
+ onUnseededChannelRefusal,
861
+ onStaleProjectionNodeRefusal,
862
+ )
863
+ }
864
+ return defineAnimation<CoreLayoutChannelAnimation>(
865
+ to,
866
+ coreLayoutAnimationFactory,
867
+ ) as unknown as number
868
+ }
869
+
870
+ // Build the no-flash layout transition: initialValues = the current (pre-commit) rect — the
871
+ // first post-commit frame paints the OLD rect — and channel animations = core generators toward
872
+ // the target rect.
873
+ export function createNoFlashLayoutTransition(
874
+ generatorConfig: LayoutGeneratorConfig = DEFAULT_TRANSITION,
875
+ channels: LayoutTransitionChannels = 'both',
876
+ onComplete?: () => void,
877
+ onStart?: () => void,
878
+ completionEpoch: LayoutCompletionEpoch = createLayoutCompletionEpoch(),
879
+ onOwnerLimitRefusal?: () => void,
880
+ onMirrorFlightLimitRefusal?: () => void,
881
+ onUnseededChannelRefusal?: (code: string) => void,
882
+ onProjectionHostFrameCapRefusal?: (code: string) => void,
883
+ onStaleProjectionNodeRefusal?: (report: StaleProjectionNodeReport) => void,
884
+ // The PUBLIC layout-flight start callback (U7e): unlike the internal onStart (which runs at
885
+ // EVERY commit — the combined-flight reset must not weaken), this crossing is qualified per
886
+ // logical flight (beginLayoutFlightStart: React generation, the pin's didUpdate analogue).
887
+ onFlightStart?: () => void,
888
+ // Incremented when the React layout signature changes (mode / authored transition /
889
+ // layout-relevant style). The primitive is the test/fallback capture; the mutable cell
890
+ // is the live didUpdate read so a mid-flight retarget on a STALE layout function still
891
+ // sees the new generation (Android 57acf50c dropped C's start — the in-flight worklet
892
+ // still held B's captured generation).
893
+ layoutFlightGeneration = 0,
894
+ layoutFlightGenerationCell?: { value: number },
895
+ ): LayoutAnimationFunction | undefined {
896
+ // The wedge law (iOS AUTO SIGABRT): the generator config rides the produced worklet's closure
897
+ // into the UI runtime — it must be a finite, acyclic payload. Dev throws at the factory edge;
898
+ // production reports and the layout request is REFUSED (seal r7, F2): undefined means no
899
+ // transition at all — the host keeps its last valid static layout, never a pinned default
900
+ // transition substituted silently (an invalid authored transition running with different
901
+ // timing).
902
+ if (refuseUnsafeSerializablePayload(generatorConfig, 'generatorConfig') !== null) return undefined
903
+ const safeGeneratorConfig = generatorConfig
904
+ // Extract before entering the worklet. The key is primitive capture data, so Worklets preserves
905
+ // identity across rebuilt layout functions even though it clones the containing JS object.
906
+ const completionKey = completionEpoch.key
907
+ const flightGeneration = layoutFlightGeneration
908
+ const flightGenerationCell = layoutFlightGenerationCell
909
+ let hasLayoutTransformMirrors = false
910
+ // `configureProjectionHostFeed` runs after this function is constructed. Unlike a lexical
911
+ // variable, a Reanimated mutable stays shared when `layoutTransition` is serialized, so the
912
+ // root host's frame reaches the same ancestor-owned tree as its nested descendants.
913
+ const projectionHostFeed: ProjectionHostFeedCells = {
914
+ driverId: makeMutable(-1),
915
+ componentId: makeMutable(''),
916
+ }
917
+ const layoutTransition: MirrorConfigurableLayoutTransition = (values) => {
918
+ 'worklet'
919
+
920
+ // Only public completion owners enter the UI registry. Layout-only flights have no terminal
921
+ // callback that could release an entry, so registering them would retain a dead owner and
922
+ // eventually interfere with unrelated hosts.
923
+ const flightEpoch =
924
+ onComplete === undefined ? undefined : beginLayoutCompletionFlight(completionKey)
925
+ if (onComplete !== undefined && flightEpoch === undefined && onOwnerLimitRefusal !== undefined)
926
+ scheduleOnRN(onOwnerLimitRefusal)
927
+
928
+ const beginMirrorFlight = (channelCount: number): number | undefined => {
929
+ 'worklet'
930
+ if (!hasLayoutTransformMirrors) return undefined
931
+ const generation = beginLayoutTransformMirrorFlight(completionKey, channelCount)
932
+ if (generation === undefined && onMirrorFlightLimitRefusal !== undefined)
933
+ scheduleOnRN(onMirrorFlightLimitRefusal)
934
+ return generation
935
+ }
936
+
937
+ // A combined layout/layoutId host owns a completion epoch per ACTUAL host flight, never per
938
+ // React render. This UI commit edge is the faithful start boundary for Reanimated layout.
939
+ if (onStart !== undefined) scheduleOnRN(onStart)
940
+ // The PUBLIC start crossing is qualified per LOGICAL flight (React generation — the pin's
941
+ // didUpdate analogue). The internal onStart above keeps firing per commit — the F1
942
+ // combined-flight reset is never weakened by the qualification.
943
+ const liveGeneration =
944
+ flightGenerationCell !== undefined ? flightGenerationCell.value : flightGeneration
945
+ if (onFlightStart !== undefined && beginLayoutFlightStart(completionKey, liveGeneration))
946
+ scheduleOnRN(onFlightStart)
947
+
948
+ // `preserve-aspect` follows Motion's documented conditional: a material ratio change snaps
949
+ // size to the host layout and animates position only. The 0.2 comparison matches the pinned
950
+ // oracle's `shouldAnimatePositionOnly`; it selects channels and never forks trajectory math.
951
+ const currentAspectRatio = values.currentWidth / values.currentHeight
952
+ const targetAspectRatio = values.targetWidth / values.targetHeight
953
+ const positionOnly =
954
+ channels === 'position' ||
955
+ (channels === 'preserve-aspect' &&
956
+ !(Math.abs(currentAspectRatio - targetAspectRatio) <= PRESERVED_ASPECT_RATIO_TOLERANCE))
957
+
958
+ // Register the host's initial/current rectangle in a UI-global record. The frame channels
959
+ // below mutate only their scalar field and flush the ancestor tree from that same record.
960
+ // A new host commit replaces the generation, so stale completion frames cannot restart it.
961
+ const projectionChannelCount = positionOnly || channels === 'size' ? 2 : 4
962
+ const configuredProjectionDriverId = projectionHostFeed.driverId.value
963
+ const configuredProjectionComponentId = projectionHostFeed.componentId.value
964
+ const projectionDriverId =
965
+ configuredProjectionDriverId >= 0 ? configuredProjectionDriverId : undefined
966
+ const projectionComponentId =
967
+ configuredProjectionComponentId.length > 0 ? configuredProjectionComponentId : undefined
968
+ let projectionGeneration: number | undefined
969
+ // Capacity pressure is a bounded DEGRADATION, never a process kill (iOS AUTO SIGABRT): the
970
+ // projection host-frame cap refuses ONLY the projection enhancement/flight for the excess
971
+ // host — it takes the plain layout path, one cap report reaches RN through scheduleOnRN,
972
+ // and the animation itself completes. The 512 cap itself stands.
973
+ let projectionCapRefused = false
974
+ const projectionGlobal = globalThis as ProjectionHostUiGlobal
975
+ if (projectionDriverId !== undefined && projectionComponentId !== undefined) {
976
+ if (projectionGlobal.__nativeMotionProjectionHostFrames === undefined)
977
+ projectionGlobal.__nativeMotionProjectionHostFrames = {}
978
+ if (projectionGlobal.__nativeMotionProjectionHostFrameOwners === undefined)
979
+ projectionGlobal.__nativeMotionProjectionHostFrameOwners = {}
980
+ if (
981
+ projectionGlobal.__nativeMotionProjectionHostFrameOwners[completionKey] === undefined &&
982
+ Object.keys(projectionGlobal.__nativeMotionProjectionHostFrameOwners).length >=
983
+ MAX_ACTIVE_PROJECTION_HOST_FRAMES
984
+ ) {
985
+ projectionCapRefused = true
986
+ if (onProjectionHostFrameCapRefusal !== undefined)
987
+ scheduleOnRN(onProjectionHostFrameCapRefusal, 'projection-host-frame-cap')
988
+ }
989
+ }
990
+ if (
991
+ !projectionCapRefused &&
992
+ projectionDriverId !== undefined &&
993
+ projectionComponentId !== undefined
994
+ ) {
995
+ // The ids-defined block above always initializes these maps first — asserted, never re-checked.
996
+ const hostFrames = projectionGlobal.__nativeMotionProjectionHostFrames!
997
+ const hostFrameOwners = projectionGlobal.__nativeMotionProjectionHostFrameOwners!
998
+ let driverFrames = hostFrames[projectionDriverId]
999
+ if (driverFrames === undefined) {
1000
+ driverFrames = { nextGeneration: 0, frames: {} }
1001
+ hostFrames[projectionDriverId] = driverFrames
1002
+ }
1003
+ driverFrames.nextGeneration += 1
1004
+ projectionGeneration = driverFrames.nextGeneration
1005
+ driverFrames.frames[projectionComponentId] = {
1006
+ generation: projectionGeneration,
1007
+ currentOriginX: channels === 'size' ? values.targetOriginX : values.currentOriginX,
1008
+ currentOriginY: channels === 'size' ? values.targetOriginY : values.currentOriginY,
1009
+ currentWidth: positionOnly ? values.targetWidth : values.currentWidth,
1010
+ currentHeight: positionOnly ? values.targetHeight : values.currentHeight,
1011
+ targetOriginX: values.targetOriginX,
1012
+ targetOriginY: values.targetOriginY,
1013
+ targetWidth: values.targetWidth,
1014
+ targetHeight: values.targetHeight,
1015
+ remainingChannels: projectionChannelCount,
1016
+ }
1017
+ hostFrameOwners[completionKey] = {
1018
+ driverId: projectionDriverId,
1019
+ componentId: projectionComponentId,
1020
+ generation: projectionGeneration,
1021
+ }
1022
+ // Positive-delay channels do not enter onFrame until their hold expires. Seed the exact
1023
+ // initial host rect now so targetless descendants counter-scale from the first painted
1024
+ // hold frame instead of visibly inheriting the parent's inverted scale.
1025
+ const projection =
1026
+ projectionGlobal.__nativeMotionDrivers?.[projectionDriverId]?.layout?.projection
1027
+ const frame = driverFrames.frames[projectionComponentId]
1028
+ if (projection !== undefined && frame !== undefined) {
1029
+ projection.tree.setHostFrame(
1030
+ projectionComponentId,
1031
+ frame.currentOriginX,
1032
+ frame.currentOriginY,
1033
+ frame.currentWidth,
1034
+ frame.currentHeight,
1035
+ frame.targetOriginX,
1036
+ frame.targetOriginY,
1037
+ frame.targetWidth,
1038
+ frame.targetHeight,
1039
+ )
1040
+ projection.tree.project()
1041
+ for (let index = 0; index < projection.records.length; index += 1) {
1042
+ const record = projection.records[index]!
1043
+ const output = projection.tree.output(record.componentId)
1044
+ record.outputs.translateX.value = output.translateX
1045
+ record.outputs.translateY.value = output.translateY
1046
+ record.outputs.scaleX.value = output.scaleX
1047
+ record.outputs.scaleY.value = output.scaleY
1048
+ record.outputs.borderRadiusX.value = output.borderRadius?.x ?? Number.NaN
1049
+ record.outputs.borderRadiusY.value = output.borderRadius?.y ?? Number.NaN
1050
+ }
1051
+ projection.dirty = false
1052
+ // The commit edge drains the tree's stale-report ring too (iOS AUTO SIGABRT Phase 1.5):
1053
+ // teardown races from superseded lanes surface through the RN severity channel.
1054
+ reportStaleProjectionReports(projection, onStaleProjectionNodeRefusal)
1055
+ }
1056
+ }
1057
+
1058
+ // Reanimated owns the commit-hook lifecycle, so its terminal callback is the only faithful
1059
+ // settle edge for `layout`. Cancellation and success both release the projection host record:
1060
+ // a cancelled frame must not keep feeding obsolete geometry into a later tree traversal.
1061
+ let completionDelivered = false
1062
+ const callback =
1063
+ onComplete === undefined
1064
+ ? undefined
1065
+ : (finished: boolean): void => {
1066
+ 'worklet'
1067
+ // Seal-r15 F2 proof witnesses (primitive counters, never the channel under test;
1068
+ // seal-r16 F2: only when the proof app armed the UI cells — zero-cost otherwise):
1069
+ // invocations vs epoch-guard drops vs cancellations vs onComplete scheduled — each
1070
+ // pair names a different dead link. No scheduleOnRN of proof helpers here: the
1071
+ // worklet-closures gate forbids capturing non-worklet module functions; JS-side
1072
+ // last-pushed cells are filled from MotionView.layoutCompletion.
1073
+ const proofGlobal = globalThis as ProofLayoutCallbackUiGlobal
1074
+ const proofArmed = proofGlobal.__nativeMotionProofWitnessArmed === true
1075
+ if (proofArmed) {
1076
+ proofGlobal.__nativeMotionProofLayoutCallbackInvocations =
1077
+ (proofGlobal.__nativeMotionProofLayoutCallbackInvocations ?? 0) + 1
1078
+ }
1079
+ if (
1080
+ flightEpoch === undefined ||
1081
+ !isCurrentLayoutCompletionFlight(completionKey, flightEpoch)
1082
+ ) {
1083
+ if (proofArmed) {
1084
+ proofGlobal.__nativeMotionProofLayoutCallbackEpochDrops =
1085
+ (proofGlobal.__nativeMotionProofLayoutCallbackEpochDrops ?? 0) + 1
1086
+ }
1087
+ return
1088
+ }
1089
+ // Same primitive-only rule as clearLayoutCompletionEpoch: nested worklet bodies must
1090
+ // not call helper worklets. Cancel and success both release the host-frame registry
1091
+ // AND clear the tree cell so cancelled mid-flight geometry cannot be re-projected.
1092
+ const projectionGlobal = globalThis as ProjectionHostUiGlobal
1093
+ const projectionOwner =
1094
+ projectionGlobal.__nativeMotionProjectionHostFrameOwners?.[completionKey]
1095
+ if (projectionOwner !== undefined) {
1096
+ const projectionFrame =
1097
+ projectionGlobal.__nativeMotionProjectionHostFrames?.[projectionOwner.driverId]
1098
+ ?.frames[projectionOwner.componentId]
1099
+ if (
1100
+ projectionFrame !== undefined &&
1101
+ projectionFrame.generation === projectionOwner.generation
1102
+ )
1103
+ delete projectionGlobal.__nativeMotionProjectionHostFrames?.[
1104
+ projectionOwner.driverId
1105
+ ]?.frames[projectionOwner.componentId]
1106
+ delete projectionGlobal.__nativeMotionProjectionHostFrameOwners?.[completionKey]
1107
+ const projection =
1108
+ projectionGlobal.__nativeMotionDrivers?.[projectionOwner.driverId]?.layout
1109
+ ?.projection
1110
+ if (projection !== undefined) {
1111
+ projection.tree.clearProjection(projectionOwner.componentId)
1112
+ projection.tree.project()
1113
+ for (let index = 0; index < projection.records.length; index += 1) {
1114
+ const record = projection.records[index]!
1115
+ const output = projection.tree.output(record.componentId)
1116
+ record.outputs.translateX.value = output.translateX
1117
+ record.outputs.translateY.value = output.translateY
1118
+ record.outputs.scaleX.value = output.scaleX
1119
+ record.outputs.scaleY.value = output.scaleY
1120
+ record.outputs.borderRadiusX.value = output.borderRadius?.x ?? Number.NaN
1121
+ record.outputs.borderRadiusY.value = output.borderRadius?.y ?? Number.NaN
1122
+ }
1123
+ projection.dirty = false
1124
+ }
1125
+ }
1126
+ if (!finished) {
1127
+ if (proofArmed) {
1128
+ proofGlobal.__nativeMotionProofLayoutCallbackCancelled =
1129
+ (proofGlobal.__nativeMotionProofLayoutCallbackCancelled ?? 0) + 1
1130
+ }
1131
+ completeLayoutCompletionFlight(completionKey, flightEpoch)
1132
+ // Do not release the start ledger on cancel: a start-driven rerender rebuilds
1133
+ // this worklet and Reanimated cancels the replaced animation — that rebound
1134
+ // must not re-arm the public callback (device proof f841bc52). Settle also
1135
+ // keeps the ledger (iOS 21e910d0 re-fire-at-settle). A later React generation
1136
+ // overwrites; lifetime teardown still releases.
1137
+ return
1138
+ }
1139
+ if (completionDelivered) return
1140
+ completionDelivered = true
1141
+ completeLayoutCompletionFlight(completionKey, flightEpoch)
1142
+ // Do not release the start ledger on settle: Reanimated re-invokes this
1143
+ // worklet at complete with the same React generation (iOS 21e910d0 flight A
1144
+ // starts=2 at +2037,+5063). That is extra host-commit granularity, not a
1145
+ // pin didUpdate. A later React generation overwrites the ledger in
1146
+ // beginLayoutFlightStart. Teardown still releases.
1147
+ if (proofArmed) {
1148
+ proofGlobal.__nativeMotionProofLayoutOnCompleteScheduled =
1149
+ (proofGlobal.__nativeMotionProofLayoutOnCompleteScheduled ?? 0) + 1
1150
+ }
1151
+ scheduleOnRN(onComplete)
1152
+ }
1153
+
1154
+ if (positionOnly) {
1155
+ const mirrorFlightGeneration = beginMirrorFlight(2)
1156
+ return {
1157
+ initialValues: {
1158
+ originX: values.currentOriginX,
1159
+ originY: values.currentOriginY,
1160
+ },
1161
+ animations: {
1162
+ originX: coreLayoutChannel(
1163
+ values.targetOriginX,
1164
+ safeGeneratorConfig,
1165
+ completionKey,
1166
+ mirrorFlightGeneration,
1167
+ projectionDriverId,
1168
+ projectionComponentId,
1169
+ projectionGeneration,
1170
+ 'originX',
1171
+ onUnseededChannelRefusal,
1172
+ onStaleProjectionNodeRefusal,
1173
+ ),
1174
+ originY: coreLayoutChannel(
1175
+ values.targetOriginY,
1176
+ safeGeneratorConfig,
1177
+ completionKey,
1178
+ mirrorFlightGeneration,
1179
+ projectionDriverId,
1180
+ projectionComponentId,
1181
+ projectionGeneration,
1182
+ 'originY',
1183
+ onUnseededChannelRefusal,
1184
+ onStaleProjectionNodeRefusal,
1185
+ ),
1186
+ },
1187
+ ...(callback === undefined ? {} : { callback }),
1188
+ }
1189
+ }
1190
+
1191
+ if (channels === 'size') {
1192
+ const mirrorFlightGeneration = beginMirrorFlight(2)
1193
+ return {
1194
+ initialValues: {
1195
+ width: values.currentWidth,
1196
+ height: values.currentHeight,
1197
+ },
1198
+ animations: {
1199
+ width: coreLayoutChannel(
1200
+ values.targetWidth,
1201
+ safeGeneratorConfig,
1202
+ completionKey,
1203
+ mirrorFlightGeneration,
1204
+ projectionDriverId,
1205
+ projectionComponentId,
1206
+ projectionGeneration,
1207
+ 'width',
1208
+ onUnseededChannelRefusal,
1209
+ onStaleProjectionNodeRefusal,
1210
+ ),
1211
+ height: coreLayoutChannel(
1212
+ values.targetHeight,
1213
+ safeGeneratorConfig,
1214
+ completionKey,
1215
+ mirrorFlightGeneration,
1216
+ projectionDriverId,
1217
+ projectionComponentId,
1218
+ projectionGeneration,
1219
+ 'height',
1220
+ onUnseededChannelRefusal,
1221
+ onStaleProjectionNodeRefusal,
1222
+ ),
1223
+ },
1224
+ ...(callback === undefined ? {} : { callback }),
1225
+ }
1226
+ }
1227
+
1228
+ const mirrorFlightGeneration = beginMirrorFlight(4)
1229
+ return {
1230
+ initialValues: {
1231
+ originX: values.currentOriginX,
1232
+ originY: values.currentOriginY,
1233
+ width: values.currentWidth,
1234
+ height: values.currentHeight,
1235
+ },
1236
+ animations: {
1237
+ originX: coreLayoutChannel(
1238
+ values.targetOriginX,
1239
+ safeGeneratorConfig,
1240
+ completionKey,
1241
+ mirrorFlightGeneration,
1242
+ projectionDriverId,
1243
+ projectionComponentId,
1244
+ projectionGeneration,
1245
+ 'originX',
1246
+ onUnseededChannelRefusal,
1247
+ onStaleProjectionNodeRefusal,
1248
+ ),
1249
+ originY: coreLayoutChannel(
1250
+ values.targetOriginY,
1251
+ safeGeneratorConfig,
1252
+ completionKey,
1253
+ mirrorFlightGeneration,
1254
+ projectionDriverId,
1255
+ projectionComponentId,
1256
+ projectionGeneration,
1257
+ 'originY',
1258
+ onUnseededChannelRefusal,
1259
+ onStaleProjectionNodeRefusal,
1260
+ ),
1261
+ width: coreLayoutChannel(
1262
+ values.targetWidth,
1263
+ safeGeneratorConfig,
1264
+ completionKey,
1265
+ mirrorFlightGeneration,
1266
+ projectionDriverId,
1267
+ projectionComponentId,
1268
+ projectionGeneration,
1269
+ 'width',
1270
+ onUnseededChannelRefusal,
1271
+ onStaleProjectionNodeRefusal,
1272
+ ),
1273
+ height: coreLayoutChannel(
1274
+ values.targetHeight,
1275
+ safeGeneratorConfig,
1276
+ completionKey,
1277
+ mirrorFlightGeneration,
1278
+ projectionDriverId,
1279
+ projectionComponentId,
1280
+ projectionGeneration,
1281
+ 'height',
1282
+ onUnseededChannelRefusal,
1283
+ onStaleProjectionNodeRefusal,
1284
+ ),
1285
+ },
1286
+ ...(callback === undefined ? {} : { callback }),
1287
+ }
1288
+ }
1289
+ layoutTransition.configureLayoutTransformMirrors = (enabled) => {
1290
+ hasLayoutTransformMirrors = enabled
1291
+ }
1292
+ layoutTransition.configureProjectionHostFeed = (feed) => {
1293
+ scheduleOnUI(
1294
+ setProjectionHostFeedCellsOnUI,
1295
+ projectionHostFeed.driverId,
1296
+ projectionHostFeed.componentId,
1297
+ feed?.driverId ?? -1,
1298
+ feed?.componentId ?? '',
1299
+ )
1300
+ }
1301
+ return layoutTransition
1302
+ }