@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,286 @@
1
+ // The gesture-state priority resolver (R6, REQ-API-031) as pure, host-agnostic logic. The
2
+ // pinned law (motion-dom variantPriorityOrder + animation-state removed-key handling): a
3
+ // layer's activation OVERLAYS the keys it names unless a higher ACTIVE layer holds them; its
4
+ // deactivation REVERTS each named key to the next-lower ACTIVE layer's definition — animated
5
+ // with the DESTINATION layer's transition (R6-F1), else the element transition — falling to
6
+ // the caller's base resolution (REQ-API-011) when no layer defines the key. Unnamed keys
7
+ // never move (the partial-overlay law, REQ-API-002/012). MotionView wires recognizer
8
+ // lifecycles to setActive and applies the returned retargets through the existing update lane.
9
+
10
+ import {
11
+ ORCHESTRATION_OPTION_KEYS,
12
+ type Target,
13
+ type Transition,
14
+ } from '@unrulysystems/native-motion-core'
15
+
16
+ const ORCHESTRATION_KEY_SET: ReadonlySet<string> = new Set(ORCHESTRATION_OPTION_KEYS)
17
+
18
+ /**
19
+ * The driver-lane strip (T21, REQ-API-053): the tree-lane orchestration options execute at the
20
+ * variant-tree altitude (MotionView's orchestration channel) — a driver-facing consumer of the
21
+ * same bag takes the remainder. A bag without the keys flows by IDENTITY.
22
+ */
23
+ export function stripOrchestrationOptions(
24
+ transition: Transition | undefined,
25
+ ): Transition | undefined {
26
+ if (transition === undefined) return undefined
27
+ const keys = Reflect.ownKeys(transition)
28
+ if (!keys.some((key) => typeof key === 'string' && ORCHESTRATION_KEY_SET.has(key))) {
29
+ return transition
30
+ }
31
+ const stripped: Record<PropertyKey, unknown> = {}
32
+ for (const key of keys) {
33
+ if (typeof key !== 'string' || ORCHESTRATION_KEY_SET.has(key)) continue
34
+ stripped[key] = (transition as Record<string, unknown>)[key]
35
+ }
36
+ return stripped as Transition
37
+ }
38
+
39
+ /** The shipped subset of the pinned variantPriorityOrder, LOW to HIGH. */
40
+ export const GESTURE_STATE_PRIORITY = ['animate', 'whileTap', 'whileDrag'] as const
41
+ export type GestureStateLayerName = (typeof GESTURE_STATE_PRIORITY)[number]
42
+
43
+ export interface GestureStateLayer {
44
+ readonly target: Target
45
+ /** Runtime-only functional definition, materialized exactly once on the activation edge. */
46
+ readonly resolveAtActivation?: (() => GestureStateLayer) | undefined
47
+ /** The layer's own transition; absent → the element transition applies (R6-F1). */
48
+ readonly transition?: Transition | undefined
49
+ /**
50
+ * R7 (REQ-API-032 law b): per-key attribution from a label-form definition — the
51
+ * transition of the label application that last named the key. Consulted FIRST; an
52
+ * unattributed key falls back down the R6-F1 chain (layer transition, else element).
53
+ */
54
+ readonly transitions?: Readonly<Record<string, Transition>> | undefined
55
+ }
56
+
57
+ export interface GestureStateRetarget {
58
+ readonly key: string
59
+ readonly value: unknown
60
+ readonly transition: Transition | undefined
61
+ }
62
+
63
+ export interface GestureStateResolver {
64
+ /**
65
+ * Flip a layer's active state; returns the retargets the flip produces, in the layer
66
+ * target's key order. Idempotent: a no-op flip returns nothing.
67
+ */
68
+ setActive(layer: GestureStateLayerName, active: boolean): GestureStateRetarget[]
69
+ /**
70
+ * Keys currently OWNED by an active non-animate layer — a render-lane animate update must
71
+ * exclude them or it would clobber the live gesture state (motion's protectedKeys law).
72
+ */
73
+ heldKeys(): ReadonlySet<string>
74
+ /**
75
+ * Reconcile every ACTIVE layer against its LIVE definition (r5 major 2f0ea1c7d3b4): a
76
+ * changed value retargets, a removed key reverts to its lower destination, and a vanished
77
+ * layer deactivates fully (revert everything it applied, clear the active bit — never a
78
+ * latch). Idempotent: an unchanged layer contributes nothing. The caller runs this once
79
+ * per render, after refreshing the live layers.
80
+ */
81
+ reconcile(): GestureStateRetarget[]
82
+ /**
83
+ * Re-emit every key an active non-animate layer currently OWNS, with the live definition's
84
+ * value and destination transition (review 2d7c3be235a4). The caller runs this on a MEASURE
85
+ * epoch (parent/viewport context change, or a landed intrinsic auto measure): reconcile()
86
+ * compares RAW values, so a context-only change to a measure-resolved target ('50%', 'auto',
87
+ * calc) emits nothing on its own. Numeric re-issues diff to no-ops in the update lane.
88
+ */
89
+ reissueActiveTargets(): GestureStateRetarget[]
90
+ }
91
+
92
+ export function createGestureStateResolver(options: {
93
+ /**
94
+ * LIVE layer access (r4 major 19): read at every flip/query, never snapshotted — an animate
95
+ * rerender or a changed gesture target is in force the moment the caller's accessor reflects
96
+ * it (motion's animation-state reads current props the same way).
97
+ */
98
+ readonly layers: () => Partial<Record<GestureStateLayerName, GestureStateLayer>>
99
+ /** REQ-API-011: the style-base value a key reverts to when no active layer defines it. */
100
+ readonly baseValueFor: (key: string) => unknown
101
+ readonly elementTransition?: (() => Transition | undefined) | undefined
102
+ }): GestureStateResolver {
103
+ const { layers: readLayers, baseValueFor } = options
104
+ const elementTransition = (): Transition | undefined => options.elementTransition?.()
105
+ const active = new Set<GestureStateLayerName>(['animate']) // animate is always active
106
+ // What each ACTIVE layer overlaid (its definition snapshot at activation, kept current by
107
+ // reconcile): deactivation and reconcile revert from THIS, never from the live definition —
108
+ // a definition that changed or vanished while active must still revert what it applied.
109
+ const applied = new Map<GestureStateLayerName, Record<string, unknown>>()
110
+ const activatedDefinitions = new Map<GestureStateLayerName, GestureStateLayer>()
111
+
112
+ const layerIndex = (layer: GestureStateLayerName): number => GESTURE_STATE_PRIORITY.indexOf(layer)
113
+
114
+ const layerFor = (
115
+ name: GestureStateLayerName,
116
+ live: Partial<Record<GestureStateLayerName, GestureStateLayer>> = readLayers(),
117
+ ): GestureStateLayer | undefined => activatedDefinitions.get(name) ?? live[name]
118
+
119
+ /**
120
+ * The R6-F1 chain with R7's per-key attribution first (law b). The winning bag is stripped
121
+ * of tree-lane orchestration options at this single choke point (T21, review r2 major 1):
122
+ * an ANIMATE-layer attribution may legally carry a variant ROOT bag with the keys (the
123
+ * tree lane executes them at label application), but every retarget this resolver emits
124
+ * feeds a property-lane command — a driver-facing read takes the remainder.
125
+ */
126
+ const transitionFor = (layer: GestureStateLayer, key: string): Transition | undefined =>
127
+ stripOrchestrationOptions(layer.transitions?.[key] ?? layer.transition ?? elementTransition())
128
+
129
+ /** The highest ACTIVE layer defining `key`, or null. */
130
+ const owningLayer = (key: string): GestureStateLayerName | null => {
131
+ const layers = readLayers()
132
+ for (let i = GESTURE_STATE_PRIORITY.length - 1; i >= 0; i--) {
133
+ const name = GESTURE_STATE_PRIORITY[i]!
134
+ if (!active.has(name)) continue
135
+ const layer = layerFor(name, layers)
136
+ if (layer !== undefined && key in layer.target) return name
137
+ }
138
+ return null
139
+ }
140
+
141
+ const destinationFor = (key: string): GestureStateRetarget | null => {
142
+ const owner = owningLayer(key)
143
+ if (owner !== null) {
144
+ const layer = layerFor(owner)!
145
+ return {
146
+ key,
147
+ value: (layer.target as Record<string, unknown>)[key],
148
+ transition: transitionFor(layer, key),
149
+ }
150
+ }
151
+ const base = baseValueFor(key)
152
+ // No layer and no base: the resolver never invents a destination — the caller's mount
153
+ // law owns that case (REQ-API-011 resolved the first paint; an unmoved key stays put).
154
+ if (base === undefined) return null
155
+ return { key, value: base, transition: elementTransition() }
156
+ }
157
+
158
+ /**
159
+ * The applied snapshot of an ACTIVE layer. Activation always records one (setActive's
160
+ * activate path), so absence is the resolver's own bookkeeping broken — degrading to an
161
+ * empty target would silently turn a release into a no-op and leave values stuck
162
+ * (r6 minor 3d497c80a16b); fail loud instead.
163
+ */
164
+ const appliedFor = (layer: GestureStateLayerName): Record<string, unknown> => {
165
+ const snapshot = applied.get(layer)
166
+ if (snapshot === undefined) {
167
+ throw new Error(
168
+ `gestureStateResolver: active layer '${layer}' has no applied snapshot — ` +
169
+ 'activation must record one (internal invariant, r6 minor 3d497c80a16b).',
170
+ )
171
+ }
172
+ return snapshot
173
+ }
174
+
175
+ /** Revert `keys` after `layer` released them: each key goes to its fresh destination. */
176
+ const revertKeys = (
177
+ layer: GestureStateLayerName,
178
+ keys: readonly string[],
179
+ ): GestureStateRetarget[] => {
180
+ const retargets: GestureStateRetarget[] = []
181
+ for (const key of keys) {
182
+ // A HIGHER active holder keeps the key — the release changes nothing for it.
183
+ const newOwner = owningLayer(key)
184
+ if (newOwner !== null && layerIndex(newOwner) > layerIndex(layer)) continue
185
+ const destination = destinationFor(key)
186
+ if (destination !== null) retargets.push(destination)
187
+ }
188
+ return retargets
189
+ }
190
+
191
+ return {
192
+ reconcile() {
193
+ const layers = readLayers()
194
+ const retargets: GestureStateRetarget[] = []
195
+ for (const name of GESTURE_STATE_PRIORITY) {
196
+ if (name === 'animate' || !active.has(name)) continue
197
+ const authoredDefinition = layers[name]
198
+ const appliedTarget = appliedFor(name)
199
+ if (authoredDefinition === undefined) {
200
+ // The layer left the tree while active: full deactivation, bit cleared (no latch).
201
+ active.delete(name)
202
+ applied.delete(name)
203
+ activatedDefinitions.delete(name)
204
+ retargets.push(...revertKeys(name, Object.keys(appliedTarget)))
205
+ continue
206
+ }
207
+ const definition = layerFor(name, layers)!
208
+ const current = definition.target as Record<string, unknown>
209
+ const removed = Object.keys(appliedTarget).filter((key) => !(key in current))
210
+ retargets.push(...revertKeys(name, removed))
211
+ for (const [key, value] of Object.entries(current)) {
212
+ if (key in appliedTarget && appliedTarget[key] === value) continue
213
+ // Added or changed: retarget only if THIS layer owns the key right now.
214
+ if (owningLayer(key) !== name) continue
215
+ retargets.push({ key, value, transition: transitionFor(definition, key) })
216
+ }
217
+ applied.set(name, { ...current })
218
+ }
219
+ return retargets
220
+ },
221
+ heldKeys() {
222
+ const layers = readLayers()
223
+ const held = new Set<string>()
224
+ for (const name of GESTURE_STATE_PRIORITY) {
225
+ if (name === 'animate' || !active.has(name)) continue
226
+ const layer = layerFor(name, layers)
227
+ if (layer === undefined) continue
228
+ for (const key of Object.keys(layer.target)) {
229
+ if (owningLayer(key) === name) held.add(key)
230
+ }
231
+ }
232
+ return held
233
+ },
234
+ reissueActiveTargets() {
235
+ const layers = readLayers()
236
+ const retargets: GestureStateRetarget[] = []
237
+ for (const name of GESTURE_STATE_PRIORITY) {
238
+ if (name === 'animate' || !active.has(name)) continue
239
+ const layer = layerFor(name, layers)
240
+ if (layer === undefined) continue
241
+ for (const key of Object.keys(layer.target)) {
242
+ // Only the OWNING layer re-issues its key — a higher active holder's value wins.
243
+ if (owningLayer(key) !== name) continue
244
+ retargets.push({
245
+ key,
246
+ value: (layer.target as Record<string, unknown>)[key],
247
+ transition: transitionFor(layer, key),
248
+ })
249
+ }
250
+ }
251
+ return retargets
252
+ },
253
+ setActive(layer, isActive) {
254
+ if (layer === 'animate') return [] // the base layer never flips
255
+ if (active.has(layer) === isActive) return [] // idempotent
256
+ if (isActive) {
257
+ const authoredDefinition = readLayers()[layer]
258
+ if (authoredDefinition === undefined) return [] // nothing to overlay; stays inactive
259
+ const definition = authoredDefinition.resolveAtActivation?.() ?? authoredDefinition
260
+ if (authoredDefinition.resolveAtActivation !== undefined) {
261
+ activatedDefinitions.set(layer, definition)
262
+ }
263
+ active.add(layer)
264
+ applied.set(layer, { ...(definition.target as Record<string, unknown>) })
265
+ const retargets: GestureStateRetarget[] = []
266
+ for (const key of Object.keys(definition.target)) {
267
+ // A HIGHER active layer holds the key: the flip changes nothing for it.
268
+ if (owningLayer(key) !== layer) continue
269
+ retargets.push({
270
+ key,
271
+ value: (definition.target as Record<string, unknown>)[key],
272
+ transition: transitionFor(definition, key),
273
+ })
274
+ }
275
+ return retargets
276
+ }
277
+ // Deactivation reverts what was APPLIED (r5 major 2f0ea1c7d3b4) — the live definition
278
+ // may have shrunk or vanished since activation; the applied snapshot is the truth.
279
+ const appliedTarget = appliedFor(layer)
280
+ active.delete(layer)
281
+ applied.delete(layer)
282
+ activatedDefinitions.delete(layer)
283
+ return revertKeys(layer, Object.keys(appliedTarget))
284
+ },
285
+ }
286
+ }