@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,839 @@
1
+ // U7b instant-transition device checks (the U7b packet's floor 3): the native card mirroring the
2
+ // pinned catalog useInstantTransition.tsx example — on mount, startInstantTransition(() =>
3
+ // setX(100)) against a MotionView initial={false} animate={{ x }} — plus the window's global
4
+ // scope, delay-zeroing, relock, and token-guard semantics as self-driven rows. Six claims:
5
+ //
6
+ // 1. MOUNT (the pin example verbatim) — the value reaches 100 with NO intermediate flight
7
+ // values (the generator is skipped entirely — NOT a 0-duration tween), and the same-tick
8
+ // read at the call still reads the origin: the commit lands on the driver's next update
9
+ // tick, never synchronously at the call. The commit's JS readback is observed by a rAF
10
+ // poll with a fail-closed deadline, NEVER a fixed frame budget: the pin's law covers the
11
+ // UI-side commit (the driver's next update tick), while this card measures the JS
12
+ // readback, which adds the command crossing, the t=0 frame (null dt — the pin's
13
+ // frame.update deferral), and the marshal's return — a device-dependent latency
14
+ // (cold-boot iOS 2026-08-18 landed past a fixed 5-frame chain, with the boot capture
15
+ // proving the commit by +1400ms; warm Android landed at frame 3). The landing frame/time
16
+ // is EVIDENCE; the gates are never-synchronous and no-flight.
17
+ // 2. PRESS + GLOBAL SCOPE — a button press (timeline-fired through the same handler — the iOS
18
+ // simulator has no tap injection, so the card self-drives) opens the window and retargets
19
+ // TWO boxes on DIFFERENT components: the window is global (any component, any motion
20
+ // value), so both commit instantly.
21
+ // 3. DELAY ZEROED — a third retarget inside the same press carries an AUTHORED
22
+ // { type: 'tween', duration: 0.6, delay: 0.5 }: the window's consult replaces it with the
23
+ // instant commit and zeroes the delay (the pin's global-flag asymmetry — the authored
24
+ // type:false honors a nonzero delay, the global flag does not). A +150ms sample — well
25
+ // before the authored 500ms — must already read the target.
26
+ // 4. THE RELOCK — a retarget started ~100ms post-press (past the two-frame relock) on an
27
+ // ordinary short tween (0.5s) FLIES: post-retarget committed samples must land STRICTLY
28
+ // BETWEEN origin and target — a stuck window commits instantly and shows ZERO
29
+ // intermediates (both 2026-08-18 platforms showed the flight). The terminal value's
30
+ // settle-edge marshal is awaited by a poll with a fail-closed deadline and its landing
31
+ // latency is DISCLOSED as evidence, never gated to a fixed offset: the settle frame's
32
+ // emission is unit-pinned (workletDriver.lanes "the settle frame emits its final
33
+ // change"), but its device delivery lags — both platforms read the last in-flight frame
34
+ // (88.8) at +900ms and the terminal 90.0 later, the 7a settle-notification finding's
35
+ // family. A terminal that never lands inside the deadline fails loud as the
36
+ // genuinely-missing-marshal class.
37
+ // 5. BACK-TO-BACK EXTENSION — two calls ONE FRAME apart; the second retarget still commits
38
+ // instantly: the second call bumped the render-count token, so the first call's stale
39
+ // unlock bails instead of closing the window the second call extended (the pin's own
40
+ // stale-token test, use-instant-transition.test.tsx:150-176). Each call's commit is
41
+ // observed by its own deadline poll — the double forced render lands the readback
42
+ // systematically later than the single-render press leg (the 2026-08-18 runs), so fixed
43
+ // frame budgets are evidence here too. A premature close shows as FLIGHT samples on the
44
+ // second box, and those stay gated.
45
+ // 6. CLEAN RUN — zero refusal reports through the console tap (the U5 pattern: console.error
46
+ // is the severity law's production error channel) across the whole timeline.
47
+ //
48
+ // Readback honesty: every x sample rides the element's own onUpdate (the driver's per-frame
49
+ // committed-values marshal) — the value the host actually received, never a JS-side mirror. A
50
+ // screenshot is the assertion; the console tap is evidence capture, never suppression.
51
+
52
+ import { useCallback, useEffect, useRef, useState } from 'react'
53
+ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
54
+ import { MotionView } from '../../native/motion/MotionView'
55
+ import { ambientNativeSeverity } from '../../native/motion/severity'
56
+ import { useInstantTransition } from '../../native/motion/useInstantTransition'
57
+
58
+ const ROW_MOUNT = 'mount (pin example verbatim): instant commit — never synchronous, no flight'
59
+ const ROW_PRESS = 'press + global scope: two boxes on different components both commit instantly'
60
+ const ROW_DELAY = 'delay zeroed: an authored delay 0.5 inside the window commits without waiting'
61
+ const ROW_RELOCK = 'the relock: a retarget 100ms post-press on a 0.5s tween FLIES (window closed)'
62
+ const ROW_B2B = 'back-to-back: two calls one frame apart — the second retarget still commits'
63
+ const ROW_CLEAN = 'clean run: zero refusal reports through the console tap'
64
+ const ROW_ORDER = [ROW_MOUNT, ROW_PRESS, ROW_DELAY, ROW_RELOCK, ROW_B2B, ROW_CLEAN] as const
65
+
66
+ // Every subject starts at x = 0; the targets are distinct so a crossed wire reads as a wrong
67
+ // value, never a coincidence.
68
+ const MOUNT_TARGET = 100
69
+ const PRESS_A_TARGET = 60
70
+ const PRESS_B_TARGET = -60
71
+ const DELAY_TARGET = 80
72
+ const RELOCK_TARGET = 90
73
+ const B2B_A_TARGET = -70
74
+ const B2B_B_TARGET = 70
75
+ // Committed-x equality / flight margins (device px): the instant commit lands exactly on the
76
+ // target; a "flight" sample must clear 2px off both endpoints to count.
77
+ const EPS = 0.5
78
+ const FLIGHT_MARGIN = 2
79
+
80
+ // Fail-closed observation deadlines (ms after the triggering call/retarget). The instant-commit
81
+ // readback is a few bridge frames warm and ran past a fixed 5-frame chain at cold-boot iOS;
82
+ // the settle-edge terminal of the 0.5s relock tween landed past +900ms on both 2026-08-18
83
+ // platforms. The deadlines sit far past every observed landing; missing one is a loud fail,
84
+ // never a silent wait.
85
+ const COMMIT_DEADLINE_MS = 2000
86
+ const SETTLE_DEADLINE_MS = 3000
87
+
88
+ // Timeline (ms). Dwells make every staged state capturable: the boot dwell (0–2400) holds the
89
+ // pin example's post-mount state (the mount box committed at x=100, every other subject at its
90
+ // origin); the press fires at 2400, the delay leg's early sample at +150ms and the relock
91
+ // retarget at +100ms; the dwell 3400–5400 holds both press boxes and the delay box committed
92
+ // while the relock box's flight settles (its terminal marshal lands inside this window); the
93
+ // back-to-back pair fires at 5400; the final verdict holds from ~8000 on. Rows 1, 4, and 5
94
+ // SELF-FINALIZE when their deadline polls resolve (landing or deadline); rows 2, 3, and 6
95
+ // finalize at fixed offsets (3400 / 7800) — proven green on both 2026-08-18 platforms.
96
+ const T_PRESS = 2400
97
+ const T_RELOCK_RETARGET = 2500
98
+ const T_DELAY_EARLY = 2550
99
+ const T_PRESS_FINALIZE = 3400
100
+ const T_B2B = 5400
101
+ const T_CLEAN = 7800
102
+ const T_DONE = 8000
103
+
104
+ interface RowResult {
105
+ readonly pass: boolean
106
+ readonly detail: string
107
+ }
108
+
109
+ interface XSample {
110
+ readonly at: number
111
+ readonly value: number
112
+ }
113
+
114
+ interface SubjectTrace {
115
+ latest: number
116
+ readonly samples: XSample[]
117
+ }
118
+
119
+ // A deadline poll's resolution: the rAF frame count and the mount-relative time at which the
120
+ // JS readback first read the target — or null when the deadline passed first.
121
+ interface Landing {
122
+ readonly frames: number
123
+ readonly at: number
124
+ }
125
+
126
+ type SubjectKey = 'mount' | 'pressA' | 'pressB' | 'delay' | 'relock' | 'b2bA' | 'b2bB'
127
+
128
+ const freshTrace = (): SubjectTrace => ({ latest: 0, samples: [] })
129
+
130
+ function freshTraces(): Record<SubjectKey, SubjectTrace> {
131
+ return {
132
+ mount: freshTrace(),
133
+ pressA: freshTrace(),
134
+ pressB: freshTrace(),
135
+ delay: freshTrace(),
136
+ relock: freshTrace(),
137
+ b2bA: freshTrace(),
138
+ b2bB: freshTrace(),
139
+ }
140
+ }
141
+
142
+ function readX(latest: Readonly<Record<string, string | number>>): number | null {
143
+ const raw = latest['x']
144
+ const value = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN
145
+ return Number.isFinite(value) ? value : null
146
+ }
147
+
148
+ // A post-retarget sample strictly between the endpoints is a FLIGHT — under an instant commit
149
+ // none may exist. lo/hi are the sorted endpoints.
150
+ function flightSample(
151
+ trace: SubjectTrace,
152
+ stamp: number,
153
+ lo: number,
154
+ hi: number,
155
+ ): XSample | undefined {
156
+ return trace.samples.find(
157
+ (s) => s.at >= stamp && s.value > lo + FLIGHT_MARGIN && s.value < hi - FLIGHT_MARGIN,
158
+ )
159
+ }
160
+
161
+ // The first rAF-chain frame (1-based) reading the target, or 0 when it never lands.
162
+ function targetFrame(chain: readonly number[], target: number): number {
163
+ return chain.findIndex((value) => Math.abs(value - target) <= EPS) + 1
164
+ }
165
+
166
+ // The rows 1/4/5 observation primitive: poll per rAF until the trace's latest committed value
167
+ // reads `target` or the fail-closed deadline passes. Replaces the fixed 5-frame chains — the
168
+ // commit/settle JS-readback latency is device-dependent, so the landing is measured EVIDENCE
169
+ // and the deadline is the only time gate. Fires `onResolve` exactly once.
170
+ function pollLanding(
171
+ trace: SubjectTrace,
172
+ target: number,
173
+ deadlineAt: number,
174
+ now: () => number,
175
+ trackRaf: (id: ReturnType<typeof requestAnimationFrame>) => void,
176
+ onResolve: (landing: Landing | null) => void,
177
+ ): void {
178
+ let frames = 0
179
+ const step = (): void => {
180
+ frames += 1
181
+ if (Math.abs(trace.latest - target) <= EPS) {
182
+ onResolve({ frames, at: now() })
183
+ return
184
+ }
185
+ if (now() >= deadlineAt) {
186
+ onResolve(null)
187
+ return
188
+ }
189
+ trackRaf(requestAnimationFrame(step))
190
+ }
191
+ trackRaf(requestAnimationFrame(step))
192
+ }
193
+
194
+ function fmt(value: number | null): string {
195
+ return value === null ? 'unavailable' : value.toFixed(1)
196
+ }
197
+
198
+ // Row 2's two subjects are DIFFERENT components by construction — the window is global, so a
199
+ // per-component authority would commit only one of them.
200
+ function PressSubjectA({
201
+ target,
202
+ onSample,
203
+ }: {
204
+ readonly target: number
205
+ readonly onSample: (value: number) => void
206
+ }) {
207
+ return (
208
+ <MotionView
209
+ animate={{ x: target }}
210
+ initial={false}
211
+ onUpdate={(latest) => {
212
+ const value = readX(latest)
213
+ if (value !== null) onSample(value)
214
+ }}
215
+ style={styles.pressBoxA}
216
+ accessibilityLabel="u7b-press-subject-a"
217
+ />
218
+ )
219
+ }
220
+
221
+ function PressSubjectB({
222
+ target,
223
+ onSample,
224
+ }: {
225
+ readonly target: number
226
+ readonly onSample: (value: number) => void
227
+ }) {
228
+ return (
229
+ <MotionView
230
+ animate={{ x: target }}
231
+ initial={false}
232
+ onUpdate={(latest) => {
233
+ const value = readX(latest)
234
+ if (value !== null) onSample(value)
235
+ }}
236
+ style={styles.pressBoxB}
237
+ accessibilityLabel="u7b-press-subject-b"
238
+ />
239
+ )
240
+ }
241
+
242
+ export function InstantTransitionChecksScreen() {
243
+ const severity = ambientNativeSeverity()
244
+ const startInstantTransition = useInstantTransition()
245
+
246
+ // The pin example's exact state: `const [x, setX] = useState(0)`.
247
+ const [x, setX] = useState(0)
248
+ const [pressTargetA, setPressTargetA] = useState(0)
249
+ const [pressTargetB, setPressTargetB] = useState(0)
250
+ const [delayTarget, setDelayTarget] = useState(0)
251
+ const [relockTarget, setRelockTarget] = useState(0)
252
+ const [b2bTargetA, setB2bTargetA] = useState(0)
253
+ const [b2bTargetB, setB2bTargetB] = useState(0)
254
+ const [pressCount, setPressCount] = useState(0)
255
+ const [stage, setStage] = useState('boot: pin example mounted (initial={false}), window firing')
256
+ const [live, setLive] = useState<Partial<Record<SubjectKey, string>>>({})
257
+ const [rows, setRows] = useState<Partial<Record<(typeof ROW_ORDER)[number], RowResult>>>({})
258
+
259
+ const mountStampRef = useRef(Date.now())
260
+ const tracesRef = useRef<Record<SubjectKey, SubjectTrace> | null>(null)
261
+ if (tracesRef.current === null) tracesRef.current = freshTraces()
262
+
263
+ // Row 1 evidence: the same-tick read at the mount call (the poll reports the landing itself).
264
+ const mountSameTickRef = useRef<number | null>(null)
265
+
266
+ // Row 2/3 evidence: the press stamp, the same-tick reads, and the shared post-press chain.
267
+ const pressStampRef = useRef(0)
268
+ const pressSameTickRef = useRef<readonly number[] | null>(null)
269
+ const pressChainRef = useRef<readonly { a: number; b: number; d: number }[]>([])
270
+ const delayEarlyRef = useRef<number | null>(null)
271
+
272
+ // Row 5 evidence: both call stamps, both same-tick reads, both poll resolutions
273
+ // (undefined = the poll is still running; null = the deadline passed first).
274
+ const b2bStampARef = useRef(0)
275
+ const b2bStampBRef = useRef(0)
276
+ const b2bSameTickARef = useRef<number | null>(null)
277
+ const b2bSameTickBRef = useRef<number | null>(null)
278
+ const b2bLandingARef = useRef<Landing | null | undefined>(undefined)
279
+ const b2bLandingBRef = useRef<Landing | null | undefined>(undefined)
280
+
281
+ const rafsRef = useRef<ReturnType<typeof requestAnimationFrame>[]>([])
282
+ const reportsRef = useRef<string[]>([])
283
+
284
+ const trackRaf = (id: ReturnType<typeof requestAnimationFrame>): void => {
285
+ rafsRef.current.push(id)
286
+ }
287
+
288
+ const record = useCallback((key: SubjectKey, value: number): void => {
289
+ const trace = tracesRef.current![key]
290
+ trace.latest = value
291
+ trace.samples.push({ at: Date.now() - mountStampRef.current, value })
292
+ setLive((previous) => ({ ...previous, [key]: value.toFixed(1) }))
293
+ }, [])
294
+
295
+ const finalize = (id: (typeof ROW_ORDER)[number], pass: boolean, detail: string): void => {
296
+ setRows((previous) => ({ ...previous, [id]: { pass, detail } }))
297
+ }
298
+
299
+ // 1. MOUNT — the pin example verbatim (dev/react/src/examples/useInstantTransition.tsx:14-16):
300
+ // on mount, startInstantTransition(() => setX(100)). The instrumentation around the call
301
+ // does not touch its semantics: the same-tick read proves the commit is NOT synchronous at
302
+ // the call, and the deadline poll observes when the commit's JS readback lands.
303
+ useEffect(() => {
304
+ const traces = tracesRef.current!
305
+ const now = (): number => Date.now() - mountStampRef.current
306
+ const stamp = now()
307
+ startInstantTransition(() => setX(MOUNT_TARGET))
308
+ mountSameTickRef.current = traces.mount.latest
309
+ pollLanding(
310
+ traces.mount,
311
+ MOUNT_TARGET,
312
+ stamp + COMMIT_DEADLINE_MS,
313
+ now,
314
+ trackRaf,
315
+ (landing) => {
316
+ const sameTick = mountSameTickRef.current
317
+ const post = traces.mount.samples.filter((s) => s.at >= stamp)
318
+ const postText = post.map((s) => s.value.toFixed(1)).join(', ')
319
+ const flight = flightSample(traces.mount, stamp, 0, MOUNT_TARGET)
320
+ let bad: string | null = null
321
+ if (sameTick === null || Math.abs(sameTick - 0) > EPS) {
322
+ bad = `the same-tick read at the call reads ${fmt(sameTick)}, want origin 0 — a synchronous commit inside startInstantTransition`
323
+ } else if (landing === null) {
324
+ bad = `the commit's JS readback never landed within the ${COMMIT_DEADLINE_MS}ms deadline (latest ${traces.mount.latest.toFixed(1)}; post-call onUpdates [${postText || 'none'}])`
325
+ } else if (flight !== undefined) {
326
+ bad = `a post-call sample reads ${flight.value.toFixed(1)} at +${flight.at.toFixed(0)}ms — that is a flight, not an instant commit (a generator ran)`
327
+ }
328
+ finalize(
329
+ ROW_MOUNT,
330
+ bad === null,
331
+ bad ??
332
+ `same-tick read ${sameTick!.toFixed(1)} = origin (the commit is NOT synchronous at ` +
333
+ `the call); target ${MOUNT_TARGET} committed — the JS readback landed at ` +
334
+ `+${landing!.at.toFixed(0)}ms (rAF frame ${landing!.frames} of the poll); ` +
335
+ `post-call onUpdates [${postText}] — no flight. The landing latency is the ` +
336
+ 'device-dependent readback of the UI-side next-tick commit (command crossing + ' +
337
+ 'the null-dt t=0 frame + the marshal return) — EVIDENCE, never the gate; the ' +
338
+ 'gates are never-synchronous and no-flight',
339
+ )
340
+ setStage('mount row finalized — press fires next (two components + the delay box)')
341
+ },
342
+ )
343
+ // The hook's callback is stable by contract; the mount leg runs once per mount.
344
+ // eslint-disable-next-line react-hooks/exhaustive-deps
345
+ }, [])
346
+
347
+ // The press handler — the Pressable below and the timeline both fire THIS: one handler, so
348
+ // the self-driven run exercises the same path a tap would.
349
+ const firePress = useCallback((): void => {
350
+ const traces = tracesRef.current!
351
+ pressStampRef.current = Date.now() - mountStampRef.current
352
+ pressChainRef.current = []
353
+ startInstantTransition(() => {
354
+ setPressTargetA(PRESS_A_TARGET)
355
+ setPressTargetB(PRESS_B_TARGET)
356
+ setDelayTarget(DELAY_TARGET)
357
+ })
358
+ pressSameTickRef.current = [traces.pressA.latest, traces.pressB.latest, traces.delay.latest]
359
+ setPressCount((count) => count + 1)
360
+ let frame = 0
361
+ const step = (): void => {
362
+ frame += 1
363
+ pressChainRef.current = [
364
+ ...pressChainRef.current,
365
+ { a: traces.pressA.latest, b: traces.pressB.latest, d: traces.delay.latest },
366
+ ]
367
+ if (frame < 5) trackRaf(requestAnimationFrame(step))
368
+ }
369
+ trackRaf(requestAnimationFrame(step))
370
+ }, [startInstantTransition])
371
+
372
+ useEffect(() => {
373
+ const mountStamp = mountStampRef.current
374
+ const now = (): number => Date.now() - mountStamp
375
+ const traces = tracesRef.current!
376
+
377
+ // The production error channel tap (the U5 pattern): refusal reports ride consoleReporter →
378
+ // console.error. Installed for this screen's lifetime only; everything passes through.
379
+ const originalError = console.error
380
+ console.error = (...args: unknown[]) => {
381
+ for (const arg of args) {
382
+ const message = arg instanceof Error ? arg.message : typeof arg === 'string' ? arg : null
383
+ if (message !== null && /motion/i.test(message)) reportsRef.current.push(message)
384
+ }
385
+ originalError(...args)
386
+ }
387
+
388
+ const timers: ReturnType<typeof setTimeout>[] = []
389
+ const at = (ms: number, run: () => void): void => {
390
+ timers.push(setTimeout(run, ms))
391
+ }
392
+
393
+ // 2/3. THE PRESS — one handler opens the window and retargets both components plus the
394
+ // authored-delay box.
395
+ at(T_PRESS, () => {
396
+ firePress()
397
+ setStage(
398
+ `press fired: ${PRESS_A_TARGET} / ${PRESS_B_TARGET} on two components, ` +
399
+ `${DELAY_TARGET} on the authored delay-0.5 box — all inside the window`,
400
+ )
401
+ })
402
+ at(T_DELAY_EARLY, () => {
403
+ delayEarlyRef.current = traces.delay.latest
404
+ setStage(
405
+ 'delay leg: +150ms sample taken (well before the authored 500ms — must read the target)',
406
+ )
407
+ })
408
+
409
+ // 4. THE RELOCK — ~100ms post-press, past the two-frame relock: an ordinary short tween
410
+ // must FLY. The row self-finalizes when the terminal's settle-edge marshal lands (or
411
+ // the deadline passes): the flight witness is the existence of intermediate committed
412
+ // samples — immune to the flight's start latency, unlike a fixed-offset mid sample.
413
+ at(T_RELOCK_RETARGET, () => {
414
+ const stamp = now()
415
+ setRelockTarget(RELOCK_TARGET)
416
+ setStage(`relock leg: 0 → ${RELOCK_TARGET} on a plain 0.5s tween, started 100ms post-press`)
417
+ pollLanding(
418
+ traces.relock,
419
+ RELOCK_TARGET,
420
+ stamp + SETTLE_DEADLINE_MS,
421
+ now,
422
+ trackRaf,
423
+ (landing) => {
424
+ const post = traces.relock.samples.filter((s) => s.at >= stamp)
425
+ const interior = post.filter(
426
+ (s) => s.value > 0 + FLIGHT_MARGIN && s.value < RELOCK_TARGET - FLIGHT_MARGIN,
427
+ )
428
+ const gap = stamp - pressStampRef.current
429
+ let bad: string | null = null
430
+ if (interior.length === 0) {
431
+ bad =
432
+ landing !== null
433
+ ? `ZERO intermediate committed samples — the retarget committed INSTANTLY ` +
434
+ `${gap.toFixed(0)}ms post-press (readback +${(landing.at - stamp).toFixed(0)}ms): ` +
435
+ 'the window is stuck open (the relock never fired)'
436
+ : `no intermediate samples AND the terminal never landed within the ` +
437
+ `${SETTLE_DEADLINE_MS}ms deadline (latest ${traces.relock.latest.toFixed(1)}) — ` +
438
+ 'the post-window tween did not fly'
439
+ } else if (landing === null) {
440
+ bad =
441
+ `the flight flew (${interior.length} intermediate samples) but the terminal ` +
442
+ `${RELOCK_TARGET}'s marshal never landed within the ${SETTLE_DEADLINE_MS}ms ` +
443
+ `deadline (latest ${traces.relock.latest.toFixed(1)}) — a genuinely-missing ` +
444
+ "settle-edge marshal (the 7a finding-B class), not the card's timing"
445
+ }
446
+ finalize(
447
+ ROW_RELOCK,
448
+ bad === null,
449
+ bad ??
450
+ `the retarget FLEW: ${interior.length} intermediate committed samples ` +
451
+ `(${interior[0]!.value.toFixed(1)} → ${interior[interior.length - 1]!.value.toFixed(1)}, ` +
452
+ `strictly between 0 and ${RELOCK_TARGET}) — a stuck window commits instantly with ` +
453
+ `ZERO intermediates; the terminal ${RELOCK_TARGET} landed at +${(landing!.at - stamp).toFixed(0)}ms ` +
454
+ 'post-retarget (the 500ms flight + the settle-edge delivery — the landing ' +
455
+ 'latency is DISCLOSED, not gated to a fixed offset: the settle frame’s emission ' +
456
+ 'is unit-pinned, its device delivery lags — the 7a settle-notification ' +
457
+ `finding’s family); the window closed on schedule (the retarget fired ` +
458
+ `${gap.toFixed(0)}ms post-press, past the two-post-render-frame relock)`,
459
+ )
460
+ setStage('relock row finalized — back-to-back pair next')
461
+ },
462
+ )
463
+ })
464
+
465
+ at(T_PRESS_FINALIZE, () => {
466
+ const pressStamp = pressStampRef.current
467
+ const chain = pressChainRef.current
468
+ const sameTick = pressSameTickRef.current
469
+ const chainText = chain
470
+ .map((c, i) => `f${i + 1}=(${c.a.toFixed(1)},${c.b.toFixed(1)},${c.d.toFixed(1)})`)
471
+ .join(' ')
472
+
473
+ // Row 2 — both components committed instantly.
474
+ const frameA = targetFrame(
475
+ chain.map((c) => c.a),
476
+ PRESS_A_TARGET,
477
+ )
478
+ const frameB = targetFrame(
479
+ chain.map((c) => c.b),
480
+ PRESS_B_TARGET,
481
+ )
482
+ const flightA = flightSample(traces.pressA, pressStamp, 0, PRESS_A_TARGET)
483
+ const flightB = flightSample(traces.pressB, pressStamp, PRESS_B_TARGET, 0)
484
+ const committedA = Math.abs(traces.pressA.latest - PRESS_A_TARGET) <= EPS
485
+ const committedB = Math.abs(traces.pressB.latest - PRESS_B_TARGET) <= EPS
486
+ let badPress: string | null = null
487
+ if (sameTick === null || sameTick.some((value) => Math.abs(value - 0) > EPS)) {
488
+ badPress = `a same-tick read at the press reads [${sameTick?.map((v) => v.toFixed(1)).join(', ') ?? 'none'}], want origins [0, 0, 0] — a synchronous commit`
489
+ } else if (frameA === 0 || frameA > 4 || frameB === 0 || frameB > 4) {
490
+ badPress = `a target never landed within the 4-frame band (A frame ${frameA}, B frame ${frameB}; chain: ${chainText || 'empty'})`
491
+ } else if (flightA !== undefined) {
492
+ badPress = `component A flew (sample ${flightA.value.toFixed(1)} at +${flightA.at.toFixed(0)}ms) — the window did not cover it`
493
+ } else if (flightB !== undefined) {
494
+ badPress = `component B flew (sample ${flightB.value.toFixed(1)} at +${flightB.at.toFixed(0)}ms) — the window is not global`
495
+ } else if (!committedA || !committedB) {
496
+ badPress = `committed values A=${traces.pressA.latest.toFixed(1)} B=${traces.pressB.latest.toFixed(1)}, want ${PRESS_A_TARGET} / ${PRESS_B_TARGET}`
497
+ }
498
+ finalize(
499
+ ROW_PRESS,
500
+ badPress === null,
501
+ badPress ??
502
+ `same-tick reads [${sameTick!.map((v) => v.toFixed(1)).join(', ')}] = origins; ` +
503
+ `component A committed ${PRESS_A_TARGET} at rAF frame ${frameA}, component B ` +
504
+ `committed ${PRESS_B_TARGET} at frame ${frameB} (chain A/B/D: ${chainText}); no ` +
505
+ 'flight on either — the window is GLOBAL across components',
506
+ )
507
+
508
+ // Row 3 — the authored 500ms delay was zeroed: the +150ms sample already reads the
509
+ // target, and the commit landed inside the same band as the undelayed boxes.
510
+ const frameD = targetFrame(
511
+ chain.map((c) => c.d),
512
+ DELAY_TARGET,
513
+ )
514
+ const early = delayEarlyRef.current
515
+ const flightD = flightSample(traces.delay, pressStamp, 0, DELAY_TARGET)
516
+ const committedD = Math.abs(traces.delay.latest - DELAY_TARGET) <= EPS
517
+ let badDelay: string | null = null
518
+ if (early === null || Math.abs(early - DELAY_TARGET) > EPS) {
519
+ badDelay = `the +150ms sample reads ${fmt(early)}, want target ${DELAY_TARGET} — the authored 500ms delay was NOT zeroed inside the window`
520
+ } else if (frameD === 0 || frameD > 4) {
521
+ badDelay = `the delayed box's target never landed within the 4-frame band (chain: ${chainText || 'empty'})`
522
+ } else if (flightD !== undefined) {
523
+ badDelay = `the delayed box flew (sample ${flightD.value.toFixed(1)}) — a generator ran despite the window`
524
+ } else if (!committedD) {
525
+ badDelay = `the delayed box committed ${traces.delay.latest.toFixed(1)}, want ${DELAY_TARGET}`
526
+ }
527
+ finalize(
528
+ ROW_DELAY,
529
+ badDelay === null,
530
+ badDelay ??
531
+ `+150ms sample ${early!.toFixed(1)} = target ${DELAY_TARGET} (well before the ` +
532
+ `authored 500ms), committed at rAF frame ${frameD} — the window zeroed the authored ` +
533
+ "{ type: 'tween', duration: 0.6, delay: 0.5 } (the pin's global-flag asymmetry: " +
534
+ 'the authored type:false honors delay, the global flag does not)',
535
+ )
536
+ setStage('press rows finalized — the relock poll and the back-to-back pair remain')
537
+ })
538
+
539
+ // 5. BACK-TO-BACK — two calls one frame apart; the token guard keeps the window open for
540
+ // the second. Each call's commit lands on its own deadline poll; the row finalizes once
541
+ // BOTH polls resolved.
542
+ const finalizeB2B = (): void => {
543
+ const landingA = b2bLandingARef.current
544
+ const landingB = b2bLandingBRef.current
545
+ if (landingA === undefined || landingB === undefined) return
546
+ const stampA = b2bStampARef.current
547
+ const stampB = b2bStampBRef.current
548
+ const sameTickA = b2bSameTickARef.current
549
+ const sameTickB = b2bSameTickBRef.current
550
+ const postA = traces.b2bA.samples.filter((s) => s.at >= stampA).map((s) => s.value.toFixed(1))
551
+ const postB = traces.b2bB.samples.filter((s) => s.at >= stampB).map((s) => s.value.toFixed(1))
552
+ const flightA = flightSample(traces.b2bA, stampA, B2B_A_TARGET, 0)
553
+ const flightB = flightSample(traces.b2bB, stampB, 0, B2B_B_TARGET)
554
+ let bad: string | null = null
555
+ if (
556
+ sameTickA === null ||
557
+ Math.abs(sameTickA - 0) > EPS ||
558
+ sameTickB === null ||
559
+ Math.abs(sameTickB - 0) > EPS
560
+ ) {
561
+ bad = `a same-tick read reads A=${fmt(sameTickA)} B=${fmt(sameTickB)}, want origins 0 — a synchronous commit`
562
+ } else if (landingA === null) {
563
+ bad = `call A's commit readback never landed within the ${COMMIT_DEADLINE_MS}ms deadline (post-call onUpdates [${postA.join(', ') || 'none'}])`
564
+ } else if (landingB === null) {
565
+ bad = `call B's commit readback never landed within the ${COMMIT_DEADLINE_MS}ms deadline (post-call onUpdates [${postB.join(', ') || 'none'}])`
566
+ } else if (flightA !== undefined) {
567
+ bad = `call A's box flew (sample ${flightA.value.toFixed(1)})`
568
+ } else if (flightB !== undefined) {
569
+ bad = `call B's box flew (sample ${flightB.value.toFixed(1)} at +${flightB.at.toFixed(0)}ms) — the token guard failed: the stale unlock closed the window under the second call`
570
+ }
571
+ finalize(
572
+ ROW_B2B,
573
+ bad === null,
574
+ bad ??
575
+ `call A committed ${B2B_A_TARGET} (readback +${(landingA!.at - stampA).toFixed(0)}ms, ` +
576
+ `rAF frame ${landingA!.frames} of its poll); call B — one frame later — committed ` +
577
+ `${B2B_B_TARGET} (+${(landingB!.at - stampB).toFixed(0)}ms, frame ${landingB!.frames}); ` +
578
+ `post-call onUpdates A[${postA.join(', ')}] B[${postB.join(', ')}] — no flight on ` +
579
+ "either: the second call's token kept the window open (the stale first unlock " +
580
+ 'bailed)',
581
+ )
582
+ setStage('back-to-back row finalized — clean-run verdict next')
583
+ }
584
+
585
+ at(T_B2B, () => {
586
+ b2bStampARef.current = now()
587
+ b2bLandingARef.current = undefined
588
+ b2bLandingBRef.current = undefined
589
+ startInstantTransition(() => setB2bTargetA(B2B_A_TARGET))
590
+ b2bSameTickARef.current = traces.b2bA.latest
591
+ pollLanding(
592
+ traces.b2bA,
593
+ B2B_A_TARGET,
594
+ b2bStampARef.current + COMMIT_DEADLINE_MS,
595
+ now,
596
+ trackRaf,
597
+ (landing) => {
598
+ b2bLandingARef.current = landing
599
+ finalizeB2B()
600
+ },
601
+ )
602
+ // The second call ONE FRAME later — the pin's stale-token shape: the first call's unlock
603
+ // (scheduled two frames out) must bail against the newer token, or it closes the window
604
+ // this call extended.
605
+ trackRaf(
606
+ requestAnimationFrame(() => {
607
+ b2bStampBRef.current = now()
608
+ startInstantTransition(() => setB2bTargetB(B2B_B_TARGET))
609
+ b2bSameTickBRef.current = traces.b2bB.latest
610
+ pollLanding(
611
+ traces.b2bB,
612
+ B2B_B_TARGET,
613
+ b2bStampBRef.current + COMMIT_DEADLINE_MS,
614
+ now,
615
+ trackRaf,
616
+ (landing) => {
617
+ b2bLandingBRef.current = landing
618
+ finalizeB2B()
619
+ },
620
+ )
621
+ }),
622
+ )
623
+ setStage(`back-to-back: call A → ${B2B_A_TARGET}, call B one frame later → ${B2B_B_TARGET}`)
624
+ })
625
+
626
+ // 6. CLEAN RUN — the console tap saw zero refusal reports across the whole timeline.
627
+ at(T_CLEAN, () => {
628
+ const reports = reportsRef.current
629
+ finalize(
630
+ ROW_CLEAN,
631
+ reports.length === 0,
632
+ reports.length === 0
633
+ ? `0 refusal reports through the console tap across ${now().toFixed(0)}ms of ` +
634
+ 'the mount commit, the press, the delay/relock legs, and the back-to-back pair'
635
+ : `${reports.length} report(s): ${reports
636
+ .map((message) => `"${message.slice(0, 80)}…"`)
637
+ .join(' | ')}`,
638
+ )
639
+ })
640
+ at(T_DONE, () => setStage('done'))
641
+
642
+ return () => {
643
+ for (const timer of timers) clearTimeout(timer)
644
+ for (const raf of rafsRef.current) cancelAnimationFrame(raf)
645
+ console.error = originalError
646
+ }
647
+ // All captured values are refs or stable setters; the timeline runs once per mount.
648
+ // eslint-disable-next-line react-hooks/exhaustive-deps
649
+ }, [])
650
+
651
+ const done = ROW_ORDER.every((id) => rows[id] !== undefined)
652
+ const failing = ROW_ORDER.filter((id) => rows[id] !== undefined && !rows[id]!.pass)
653
+ const verdict = !done ? 'RUNNING' : failing.length === 0 ? 'PASS' : 'FAIL'
654
+ const passed = Object.keys(rows).length - failing.length
655
+
656
+ return (
657
+ <ScrollView style={styles.scroll} contentContainerStyle={styles.container}>
658
+ <Text style={styles.title}>U7b instant-transition checks</Text>
659
+ <Text style={styles.subtitle}>
660
+ useInstantTransition global window — the pin example verbatim ({severity} severity)
661
+ </Text>
662
+ <View
663
+ style={[
664
+ styles.banner,
665
+ verdict === 'PASS' ? styles.pass : verdict === 'FAIL' ? styles.fail : styles.running,
666
+ ]}
667
+ >
668
+ <Text style={styles.bannerText}>{verdict}</Text>
669
+ </View>
670
+ <Text style={styles.tally}>
671
+ {done
672
+ ? `${passed} PASS / ${failing.length} FAIL of ${ROW_ORDER.length} rows`
673
+ : `${Object.keys(rows).length}/${ROW_ORDER.length} rows complete — RUNNING`}
674
+ </Text>
675
+ <Text style={styles.tallyDetail}>
676
+ {done
677
+ ? failing.length === 0
678
+ ? 'no failing rows'
679
+ : `FAILING: ${failing.join(' | ')}`
680
+ : `stage: ${stage}`}
681
+ </Text>
682
+ <Text style={styles.status}>
683
+ stage: {stage} · presses={pressCount} · mount x={live.mount ?? '0.0'} · A=
684
+ {live.pressA ?? '0.0'} B=
685
+ {live.pressB ?? '0.0'} · delay={live.delay ?? '0.0'} · relock={live.relock ?? '0.0'} · b2b=
686
+ {live.b2bA ?? '0.0'}/{live.b2bB ?? '0.0'}
687
+ </Text>
688
+
689
+ <Pressable onPress={firePress} style={styles.button} accessibilityLabel="u7b-press-button">
690
+ <Text style={styles.buttonText}>PRESS (auto-fires at +{T_PRESS}ms)</Text>
691
+ </Pressable>
692
+
693
+ {/* The subjects. The white box is the pin example verbatim (useInstantTransition.tsx:4-9,
694
+ 18): 100×100 white, initial={false}, animate={{ x }}. The press pair are two DIFFERENT
695
+ components retargeted inside one window; the delay box carries the authored 500ms
696
+ delay; the relock box flies a plain 0.5s tween after the window closes; the b2b pair
697
+ takes the two back-to-back calls. onUpdate on each is the committed-values readback. */}
698
+ <View style={styles.stageRow}>
699
+ <MotionView
700
+ initial={false}
701
+ animate={{ x }}
702
+ onUpdate={(latest) => {
703
+ const value = readX(latest)
704
+ if (value !== null) record('mount', value)
705
+ }}
706
+ style={styles.mountBox}
707
+ accessibilityLabel="u7b-mount-subject"
708
+ />
709
+ <PressSubjectA target={pressTargetA} onSample={(value) => record('pressA', value)} />
710
+ <PressSubjectB target={pressTargetB} onSample={(value) => record('pressB', value)} />
711
+ </View>
712
+ <View style={styles.stageRow}>
713
+ <MotionView
714
+ animate={{ x: delayTarget }}
715
+ initial={false}
716
+ transition={{ type: 'tween', duration: 0.6, delay: 0.5 }}
717
+ onUpdate={(latest) => {
718
+ const value = readX(latest)
719
+ if (value !== null) record('delay', value)
720
+ }}
721
+ style={styles.delayBox}
722
+ accessibilityLabel="u7b-delay-subject"
723
+ />
724
+ <MotionView
725
+ animate={{ x: relockTarget }}
726
+ initial={false}
727
+ transition={{ type: 'tween', duration: 0.5 }}
728
+ onUpdate={(latest) => {
729
+ const value = readX(latest)
730
+ if (value !== null) record('relock', value)
731
+ }}
732
+ style={styles.relockBox}
733
+ accessibilityLabel="u7b-relock-subject"
734
+ />
735
+ <MotionView
736
+ animate={{ x: b2bTargetA }}
737
+ initial={false}
738
+ onUpdate={(latest) => {
739
+ const value = readX(latest)
740
+ if (value !== null) record('b2bA', value)
741
+ }}
742
+ style={styles.b2bBoxA}
743
+ accessibilityLabel="u7b-b2b-subject-a"
744
+ />
745
+ <MotionView
746
+ animate={{ x: b2bTargetB }}
747
+ initial={false}
748
+ onUpdate={(latest) => {
749
+ const value = readX(latest)
750
+ if (value !== null) record('b2bB', value)
751
+ }}
752
+ style={styles.b2bBoxB}
753
+ accessibilityLabel="u7b-b2b-subject-b"
754
+ />
755
+ </View>
756
+
757
+ {ROW_ORDER.map((id) => {
758
+ const row = rows[id]
759
+ return (
760
+ <View key={id} style={styles.row}>
761
+ <Text
762
+ style={[
763
+ styles.verdict,
764
+ row === undefined ? styles.pending : row.pass ? styles.ok : styles.bad,
765
+ ]}
766
+ >
767
+ {row === undefined ? '…' : row.pass ? 'PASS' : 'FAIL'}
768
+ </Text>
769
+ <View style={styles.rowBody}>
770
+ <Text style={styles.rowId}>{id}</Text>
771
+ <Text style={styles.rowDetail} numberOfLines={4}>
772
+ {row === undefined ? 'pending' : row.detail}
773
+ </Text>
774
+ </View>
775
+ </View>
776
+ )
777
+ })}
778
+ </ScrollView>
779
+ )
780
+ }
781
+
782
+ const styles = StyleSheet.create({
783
+ scroll: { flex: 1, backgroundColor: '#0b0b12' },
784
+ container: { paddingVertical: 72, paddingHorizontal: 8 },
785
+ title: { color: '#fff', fontSize: 22, fontWeight: '700', textAlign: 'center' },
786
+ subtitle: { color: '#8a8aa0', fontSize: 12, marginTop: 6, textAlign: 'center' },
787
+ banner: {
788
+ alignSelf: 'center',
789
+ paddingHorizontal: 36,
790
+ paddingVertical: 10,
791
+ borderRadius: 10,
792
+ marginTop: 18,
793
+ marginBottom: 10,
794
+ },
795
+ pass: { backgroundColor: '#1d7a3e' },
796
+ fail: { backgroundColor: '#a32633' },
797
+ running: { backgroundColor: '#7a641d' },
798
+ bannerText: { color: '#fff', fontSize: 22, fontWeight: '800', letterSpacing: 2 },
799
+ tally: { color: '#e6e6f0', fontSize: 14, fontWeight: '700', textAlign: 'center' },
800
+ tallyDetail: { color: '#f87171', fontSize: 11, marginTop: 3, textAlign: 'center' },
801
+ status: {
802
+ color: '#c9c9d8',
803
+ fontSize: 11,
804
+ marginTop: 8,
805
+ textAlign: 'center',
806
+ fontFamily: 'Menlo',
807
+ },
808
+ button: {
809
+ alignSelf: 'center',
810
+ marginTop: 14,
811
+ paddingHorizontal: 20,
812
+ paddingVertical: 8,
813
+ borderRadius: 8,
814
+ backgroundColor: '#2a2a3a',
815
+ },
816
+ buttonText: { color: '#c9c9d8', fontSize: 12, fontWeight: '700' },
817
+ stageRow: {
818
+ flexDirection: 'row',
819
+ marginTop: 18,
820
+ alignItems: 'center',
821
+ justifyContent: 'space-evenly',
822
+ },
823
+ // The pin example's style verbatim (useInstantTransition.tsx:4-9): 100×100 white.
824
+ mountBox: { width: 100, height: 100, backgroundColor: 'white', borderRadius: 12 },
825
+ pressBoxA: { width: 60, height: 60, backgroundColor: '#3b82f6', borderRadius: 10 },
826
+ pressBoxB: { width: 60, height: 60, backgroundColor: '#f59e0b', borderRadius: 10 },
827
+ delayBox: { width: 60, height: 60, backgroundColor: '#a855f7', borderRadius: 10 },
828
+ relockBox: { width: 60, height: 60, backgroundColor: '#22c55e', borderRadius: 10 },
829
+ b2bBoxA: { width: 60, height: 60, backgroundColor: '#ec4899', borderRadius: 10 },
830
+ b2bBoxB: { width: 60, height: 60, backgroundColor: '#14b8a6', borderRadius: 10 },
831
+ row: { flexDirection: 'row', marginTop: 10 },
832
+ verdict: { width: 52, fontSize: 12, fontWeight: '800', paddingTop: 1 },
833
+ ok: { color: '#4ade80' },
834
+ bad: { color: '#f87171' },
835
+ pending: { color: '#8a8aa0' },
836
+ rowBody: { flex: 1 },
837
+ rowId: { color: '#e6e6f0', fontSize: 13, fontWeight: '600' },
838
+ rowDetail: { color: '#6f6f85', fontSize: 11, marginTop: 2 },
839
+ })