@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,250 @@
1
+ // REQ-API-022 (FLAG 5a, specs/SPEC-GESTURE.md) — MotionRoot, the public root
2
+ // primitive. On native it OWNS the platform gesture root (GestureHandlerRootView — consumers
3
+ // never import the substrate, the dogfood ban law); the web entry's counterpart renders children
4
+ // unchanged, keeping the one-import universal surface. A gesture-carrying View mounted outside
5
+ // this root would silently never receive touches — the context lets the component fail loud
6
+ // instead (gateMotionRoot), per the severity law.
7
+ //
8
+ // R15 D3 (REQ-API-043 / packet Finding 8): also hosts the hub touch stream — a manual-activation
9
+ // pan spanning the root that records touches and drives hub-originated drag sessions so sibling
10
+ // handles work (the pin's window-level PanSession analog).
11
+
12
+ import { createContext, useContext, useMemo, useRef } from 'react'
13
+ import type { ViewStyle } from 'react-native'
14
+ import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
15
+ import { runNativeEventCallback, useWorkletDriver } from '../driver/workletDriver'
16
+ import { createDragHubStream, type DragHubStream } from './dragHubStream'
17
+ import { createLayoutIdentityBinding, type LayoutIdentityBinding } from './layoutIdBinding'
18
+ import type { MotionComponentId, NativeSeverity } from './motionViewController'
19
+
20
+ // Null outside a MotionRoot: gesture props use the presence bit, while layoutId Views consume the
21
+ // binding itself. One provider value therefore owns both root-scoped capabilities without parallel
22
+ // root contexts that could skew under nested roots.
23
+ interface MotionRootContextValue {
24
+ readonly layoutIdentity: LayoutIdentityBinding
25
+ readonly subscribeLayoutIdentitySettle: (id: string, listener: () => void) => () => void
26
+ /** R15 D3: the hub touch stream for imperative dragControls.start (Finding 8). */
27
+ readonly dragHubStream: DragHubStream
28
+ }
29
+
30
+ const MotionRootContext = createContext<MotionRootContextValue | null>(null)
31
+
32
+ export function useInsideMotionRoot(): boolean {
33
+ return useContext(MotionRootContext) !== null
34
+ }
35
+
36
+ export function useLayoutIdentityRootBinding(): LayoutIdentityBinding | null {
37
+ return useContext(MotionRootContext)?.layoutIdentity ?? null
38
+ }
39
+
40
+ // Projection geometry must live on ONE forest under MotionRoot so layoutId's identity driver
41
+ // (writeHolder → setDirectProjection) and host-frame feeds share the same tree. Without this,
42
+ // the first non-layoutId Motion ancestor opens a private useWorkletDriver() forest and a nested
43
+ // layoutId child's projectionComponentId is never registered where the root frame publishes it.
44
+ export function useLayoutIdentityProjectionBinding():
45
+ | LayoutIdentityBinding['projectionBinding']
46
+ | null {
47
+ return useContext(MotionRootContext)?.layoutIdentity.projectionBinding ?? null
48
+ }
49
+
50
+ // layoutId flights run on MotionRoot's shared driver, while each host owns its ordinary property
51
+ // driver. This subscription delivers the shared driver's PER-IDENTITY settle edge — keyed by the
52
+ // identity id, independent of unrelated driver work — to the mounted host that initiated an
53
+ // identity flight; it never polls or crosses the UI runtime per frame.
54
+ export function useLayoutIdentitySettleSubscription():
55
+ | ((id: string, listener: () => void) => () => void)
56
+ | null {
57
+ return useContext(MotionRootContext)?.subscribeLayoutIdentitySettle ?? null
58
+ }
59
+
60
+ /** R15 D3: the hub stream for DragControls.start correlation (null outside MotionRoot). */
61
+ export function useDragHubStream(): DragHubStream | null {
62
+ return useContext(MotionRootContext)?.dragHubStream ?? null
63
+ }
64
+
65
+ // The REQ-API-022 gate, pure so the suite pins it: true = proceed; development throws naming
66
+ // MotionRoot; production reports and refuses (the caller drops the gesture wiring, fail-closed).
67
+ export function gateMotionRoot(
68
+ insideRoot: boolean,
69
+ severity: NativeSeverity,
70
+ report: (error: Error) => void,
71
+ componentId: MotionComponentId = '<Motion.View>',
72
+ ): boolean {
73
+ if (insideRoot) return true
74
+ const error = new Error(
75
+ `${componentId}: gesture props need a <MotionRoot> above them: MotionRoot owns the platform gesture root ` +
76
+ "(REQ-API-022). Wrap your app (or the gesture subtree) in MotionRoot from '@unrulysystems/native-motion'.",
77
+ )
78
+ if (severity === 'development') throw error
79
+ report(error)
80
+ return false
81
+ }
82
+
83
+ // Children only — deliberately NO style knob on either entry: the web counterpart renders no
84
+ // element, so a style prop would be web-ignored (an accepted-but-ignored prop, REQ-API-016), and
85
+ // no example needs root styling. A layout knob can land ADDITIVELY when an example demands it.
86
+ export interface MotionRootProps {
87
+ readonly children?: React.ReactNode
88
+ }
89
+
90
+ // GestureHandlerRootView must span the gesture area; flex: 1 is the substrate's own documented
91
+ // expectation for an app-root mount.
92
+ const ROOT_FILL: ViewStyle = { flex: 1 }
93
+
94
+ export function MotionRoot({ children }: MotionRootProps) {
95
+ // The registry lives in the driver's UI-runtime layout slot. Creating this driver here makes the
96
+ // registry identity root-scoped: every descendant layoutId View exchanges through this ONE
97
+ // binding, while each View keeps its existing private driver for ordinary animated properties.
98
+ const driver = useWorkletDriver()
99
+ const valueRef = useRef<MotionRootContextValue | null>(null)
100
+ if (valueRef.current === null) {
101
+ valueRef.current = {
102
+ layoutIdentity: createLayoutIdentityBinding(driver),
103
+ subscribeLayoutIdentitySettle: (id, listener) => driver.layout.subscribeSettled(id, listener),
104
+ dragHubStream: createDragHubStream(),
105
+ }
106
+ }
107
+ const stream = valueRef.current.dragHubStream
108
+
109
+ // R15 D3 hub pan: manual-activation, simultaneous with everything (never blocks scroll/child
110
+ // gestures), records touches, activates only when a hub start has armed the stream.
111
+ const hubPan = useMemo(
112
+ () =>
113
+ Gesture.Pan()
114
+ .runOnJS(true)
115
+ .manualActivation(true)
116
+ // Unarmed touches are never activated — child/scroll recognizers keep the stream.
117
+ .onTouchesDown((event, manager) => {
118
+ runNativeEventCallback(() => {
119
+ for (const touch of event.allTouches) {
120
+ stream.recordTouch({
121
+ id: touch.id,
122
+ absoluteX: touch.absoluteX,
123
+ absoluteY: touch.absoluteY,
124
+ })
125
+ }
126
+ // U7d tap observation rides CHANGED touches — one notification per physical down
127
+ // (allTouches re-reports already-active touches on every subsequent down).
128
+ for (const touch of event.changedTouches) {
129
+ stream.notifyTapDown({
130
+ id: touch.id,
131
+ absoluteX: touch.absoluteX,
132
+ absoluteY: touch.absoluteY,
133
+ })
134
+ }
135
+ // Stay inert unless already armed by a hub start between down and move.
136
+ if (stream.armed === null) {
137
+ // Do not activate — leave the touch for child/scroll recognizers.
138
+ void manager
139
+ }
140
+ })
141
+ })
142
+ .onTouchesMove((event, manager) => {
143
+ runNativeEventCallback(() => {
144
+ const armed = stream.armed
145
+ if (armed === null) return
146
+ // Route ONLY the correlated touchId (seal review r3 — multi-touch ownership).
147
+ // Synthetic arm id -1 also accepts any touch within correlation (rebound on
148
+ // recordTouch; seal review r4 major).
149
+ const touch =
150
+ armed.touchId === -1
151
+ ? (event.allTouches[0] ?? event.changedTouches[0])
152
+ : (event.allTouches.find((t) => t.id === armed.touchId) ??
153
+ event.changedTouches.find((t) => t.id === armed.touchId))
154
+ if (touch === undefined) return
155
+ // Activate the recognizer so onUpdate/onEnd own velocity (REQ-GESTURE-014).
156
+ // Once activated, ONLY the recognizer .onUpdate feed drives the session — never
157
+ // dual-feed onTouchesMove + onUpdate (seal review r4 major / Finding 8).
158
+ if (!armed.activated) {
159
+ manager.activate()
160
+ // Pre-activation bridge: one absolute sample so distanceThreshold can clear
161
+ // before the first .onUpdate (same-frame Pressable arm race).
162
+ stream.onPanUpdate({
163
+ absoluteX: touch.absoluteX,
164
+ absoluteY: touch.absoluteY,
165
+ translationX: touch.absoluteX - armed.originAbsoluteX,
166
+ translationY: touch.absoluteY - armed.originAbsoluteY,
167
+ velocityX: 0,
168
+ velocityY: 0,
169
+ })
170
+ }
171
+ })
172
+ })
173
+ .onTouchesUp((event) => {
174
+ runNativeEventCallback(() => {
175
+ // RNGH Android delivers onTouchesUp BEFORE the handler processes the up event
176
+ // (GestureHandlerOrchestrator), so onEnd still follows with the real velocity
177
+ // (REQ-GESTURE-014). Never end/finalize here when activated — that would zero
178
+ // velocity and disarm before onEnd (seal review r2 major).
179
+ // Pre-activation: cancel when the CORRELATED touch lifts (not a second finger).
180
+ // Synthetic arm id -1 (Pressable before recordTouch rebind): any lift correlates —
181
+ // real changedTouches never carry id -1 (adversarial seal Issue 3).
182
+ const armed = stream.armed
183
+ const correlatedUp =
184
+ armed !== null &&
185
+ event.changedTouches.length > 0 &&
186
+ (armed.touchId === -1 || event.changedTouches.some((t) => t.id === armed.touchId))
187
+ if (armed !== null && !armed.activated && correlatedUp) {
188
+ stream.terminate(false)
189
+ }
190
+ for (const touch of event.changedTouches) {
191
+ stream.notifyTapUp({
192
+ id: touch.id,
193
+ absoluteX: touch.absoluteX,
194
+ absoluteY: touch.absoluteY,
195
+ })
196
+ stream.forgetTouch(touch.id)
197
+ }
198
+ })
199
+ })
200
+ .onTouchesCancelled((event) => {
201
+ runNativeEventCallback(() => {
202
+ // Cancel when the correlated touch is cancelled. Synthetic -1: any cancel
203
+ // correlates (same as up; Issue 3).
204
+ const armed = stream.armed
205
+ const correlated =
206
+ armed !== null &&
207
+ event.changedTouches.length > 0 &&
208
+ (armed.touchId === -1 || event.changedTouches.some((t) => t.id === armed.touchId))
209
+ if (armed !== null && correlated) {
210
+ stream.terminate(false)
211
+ }
212
+ for (const touch of event.changedTouches) {
213
+ // U7d: the touch-cancel path is the global press's ONLY cancel source (the R11
214
+ // touch-steal mapping, reproduced through the stream's own cancel event — F1).
215
+ stream.notifyTapCancel({
216
+ id: touch.id,
217
+ absoluteX: touch.absoluteX,
218
+ absoluteY: touch.absoluteY,
219
+ })
220
+ stream.forgetTouch(touch.id)
221
+ }
222
+ })
223
+ })
224
+ // Recognizer lifecycle owns velocity when ACTIVE (REQ-GESTURE-014 preferred path).
225
+ .onStart((event) => runNativeEventCallback(() => stream.onPanStart(event)))
226
+ .onUpdate((event) => runNativeEventCallback(() => stream.onPanUpdate(event)))
227
+ .onEnd((event, success) => runNativeEventCallback(() => stream.onPanEnd(event, success)))
228
+ .onFinalize((event, success) =>
229
+ runNativeEventCallback(() => stream.onPanFinalize(event, success)),
230
+ ),
231
+ [stream],
232
+ )
233
+
234
+ // RNGH GestureDetector requires exactly one child element (Wrap.tsx Children.only).
235
+ // MotionRootProps still accepts arbitrary ReactNode — wrap so empty/multi-child trees
236
+ // match the web root (no public API restriction) without crashing (review seal major).
237
+ // Nested GestureHandlerRootView is a single native host for the detector; mocks already
238
+ // provide it as a passthrough (no react-native View import required in this module).
239
+ return (
240
+ <MotionRootContext.Provider value={valueRef.current}>
241
+ <GestureHandlerRootView style={ROOT_FILL}>
242
+ <GestureDetector gesture={hubPan}>
243
+ <GestureHandlerRootView style={ROOT_FILL} collapsable={false}>
244
+ {children}
245
+ </GestureHandlerRootView>
246
+ </GestureDetector>
247
+ </GestureHandlerRootView>
248
+ </MotionRootContext.Provider>
249
+ )
250
+ }