@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,563 @@
1
+ // The gesture-state boundary gate (R6, REQ-API-031; R7 REQ-API-032 adds the label form): an
2
+ // orphan whileDrag (no drag axis) refuses under the R6-F2 native law (the pinned web engine
3
+ // leaves it silently inert — a recorded divergence; this package never ships a silently dead
4
+ // gesture surface); a whileDrag key colliding with the drag axis is a dual-authority refusal
5
+ // (REQ-DRIVER-029); embedded transitions split from the target; the remaining target
6
+ // validates through the SAME core boundary the animate lane rides (REQ-API-013). A LABEL
7
+ // definition resolves against the element's gated dictionary (entries already carry core
8
+ // validity + native drivability from the dictionary gate) into a per-key-attributed layer
9
+ // (law b); the member conflicts (axis, bound XOR) apply identically to both forms.
10
+
11
+ import {
12
+ describeValue,
13
+ InvalidTargetError,
14
+ InvalidTransitionError,
15
+ captureTransition,
16
+ flattenVariantApplications,
17
+ hostCapabilities,
18
+ TARGET_PROPERTY_KEYS,
19
+ validateTargetRefusal,
20
+ validateTransitionRefusal,
21
+ type DragAxis,
22
+ type Target,
23
+ type Transition,
24
+ type VariantResolver,
25
+ type VariantsDictionary,
26
+ } from '@unrulysystems/native-motion-core'
27
+ import {
28
+ executableLengthValueRefusal,
29
+ resolveTransitionForKey,
30
+ } from '@unrulysystems/native-motion-core/internal-driver'
31
+ import type { GestureStateLayer } from './gestureStateResolver'
32
+ import {
33
+ isNativeExecutableLengthValue,
34
+ NATIVE_PROPERTY_TRANSITION_OPTIONS,
35
+ nativeMountTargetValueKind,
36
+ } from './shippedSurface'
37
+ import { keyframeTransitionRefusal } from '@unrulysystems/native-motion-core/internal-driver'
38
+ import {
39
+ captureBoundedArray,
40
+ capturedArrayDescription,
41
+ } from '@unrulysystems/native-motion-core/internal-driver'
42
+ import type { StyleValueGate } from './styleValueBinding'
43
+ import type { MotionComponentId } from './motionViewController'
44
+ import { gateLabelDefinition, resolveVariantApplicationsGated } from './variantProps'
45
+
46
+ const THROW_GATE: StyleValueGate = {
47
+ severity: 'development',
48
+ report: () => {},
49
+ componentId: '<Motion.View>',
50
+ }
51
+ const NATIVE_HOST = hostCapabilities('native', ['universal', 'native-extension'])
52
+ const TRANSITION_MAP_KEYS = new Set<string>(['default', 'layout', ...TARGET_PROPERTY_KEYS])
53
+
54
+ function hasTransitionMap(transition: Transition): boolean {
55
+ return Reflect.ownKeys(transition).some(
56
+ (key) => typeof key === 'string' && TRANSITION_MAP_KEYS.has(key),
57
+ )
58
+ }
59
+
60
+ function nativePropertyTransitionRefusal(
61
+ componentId: MotionComponentId,
62
+ path: string,
63
+ transition: Transition,
64
+ propertyKeys: readonly string[] = ['__root__'],
65
+ ): Error | null {
66
+ const refusalForOption = (
67
+ option: string,
68
+ optionValue: unknown,
69
+ optionPath: string,
70
+ ): Error | null =>
71
+ NATIVE_PROPERTY_TRANSITION_OPTIONS.has(option)
72
+ ? null
73
+ : new InvalidTransitionError(
74
+ componentId,
75
+ option,
76
+ optionValue,
77
+ optionPath +
78
+ ' is not executable by the native property lane (it drives: ' +
79
+ NATIVE_PROPERTY_TRANSITION_OPTIONS.options.join(', ') +
80
+ ') — a silently inert option is refused (G-INV-8, shippedSurface.ts)',
81
+ )
82
+
83
+ const refusalForFlatTransition = (selected: Transition, selectedPath: string): Error | null => {
84
+ for (const option of Reflect.ownKeys(selected)) {
85
+ if (typeof option !== 'string' || TRANSITION_MAP_KEYS.has(option) || option === 'inherit')
86
+ continue
87
+ const optionValue = (selected as Record<string, unknown>)[option]
88
+ if (optionValue === undefined) continue
89
+ const refusal = refusalForOption(option, optionValue, selectedPath + '.' + option)
90
+ if (refusal !== null) return refusal
91
+ }
92
+ return null
93
+ }
94
+
95
+ if (!hasTransitionMap(transition)) return refusalForFlatTransition(transition, path)
96
+
97
+ const source = transition as Record<PropertyKey, unknown>
98
+ const selectors = propertyKeys.length === 0 ? ['__root__'] : propertyKeys
99
+ const seen = new Set<string>()
100
+ for (const propertyKey of selectors) {
101
+ const mapKey =
102
+ Object.hasOwn(source, propertyKey) &&
103
+ source[propertyKey] !== undefined &&
104
+ source[propertyKey] !== null
105
+ ? propertyKey
106
+ : Object.hasOwn(source, 'default') && source['default'] !== undefined
107
+ ? 'default'
108
+ : 'root'
109
+ if (seen.has(mapKey)) continue
110
+ seen.add(mapKey)
111
+ const selected = resolveTransitionForKey(transition, propertyKey)
112
+ const refusal = refusalForFlatTransition(
113
+ selected,
114
+ mapKey === 'root' ? path : path + '.' + mapKey,
115
+ )
116
+ if (refusal !== null) return refusal
117
+ }
118
+ return null
119
+ }
120
+
121
+ // Capture a caller-owned embedded transition ONCE into a frozen snapshot (review major 0cb829). A malformed
122
+ // SHAPE (primitive, array, prototype-carrying) stays raw so validateTransitionRefusal throws its typed
123
+ // refusal rather than a capture laundering it into an empty `{}`.
124
+ function captureEmbeddedTransition(transition: Transition | undefined): Transition | undefined {
125
+ if (transition === undefined) return undefined
126
+ if (typeof transition !== 'object' || transition === null || Array.isArray(transition)) {
127
+ return transition
128
+ }
129
+ const proto = Object.getPrototypeOf(transition) as unknown
130
+ if (proto !== Object.prototype && proto !== null) return transition
131
+ return captureTransition(transition)
132
+ }
133
+
134
+ export interface GestureStatePropsInput {
135
+ readonly whileTap?: unknown
136
+ readonly whileDrag?: unknown
137
+ }
138
+
139
+ export interface AcceptedGestureState {
140
+ whileTap?: GestureStateLayer
141
+ whileDrag?: GestureStateLayer
142
+ }
143
+
144
+ export interface GestureStateGateResult {
145
+ readonly state: AcceptedGestureState
146
+ /** A property selected the element transition before production refused that property. */
147
+ readonly fallbackAttempted: boolean
148
+ }
149
+
150
+ export function gateGestureStateProps(
151
+ props: GestureStatePropsInput,
152
+ context: {
153
+ readonly dragAxes: readonly DragAxis[]
154
+ /** Style-bound keys (REQ-DRIVER-029 XOR): a bound key can never be gesture-driven. */
155
+ readonly boundKeys?: readonly string[]
156
+ /** Exact direct-public numeric bindings whose identity channel owns declarative collisions. */
157
+ readonly identityEligibleBoundKeys?: readonly string[]
158
+ /** R7: the element's GATED dictionary — label definitions resolve against it (law a). */
159
+ readonly variantsDictionary?: VariantsDictionary | undefined
160
+ /**
161
+ * The element transition's caller-independent snapshot. A gesture member without an attributed or
162
+ * embedded transition executes through this fallback, so its target/timing pair is gated here too.
163
+ */
164
+ readonly elementTransition?: Transition | undefined
165
+ // T24 B1: the component custom payload for RESOLVER labels in gesture-state slots.
166
+ readonly custom?: unknown
167
+ readonly readValueState?:
168
+ | (() => {
169
+ readonly current: Readonly<Record<string, unknown>>
170
+ readonly velocity: Readonly<Record<string, number>>
171
+ })
172
+ | undefined
173
+ readonly componentId?: MotionComponentId
174
+ },
175
+ gate: StyleValueGate = THROW_GATE,
176
+ ): AcceptedGestureState {
177
+ return gateGestureStatePropsWithEvidence(props, context, gate).state
178
+ }
179
+
180
+ /**
181
+ * Keep fallback-attempt evidence beside the accepted layers. The public-shaped layer object remains
182
+ * free of bookkeeping; MotionView alone needs this to avoid a second inert-transition report.
183
+ */
184
+ export function gateGestureStatePropsWithEvidence(
185
+ props: GestureStatePropsInput,
186
+ context: {
187
+ readonly dragAxes: readonly DragAxis[]
188
+ readonly boundKeys?: readonly string[]
189
+ readonly identityEligibleBoundKeys?: readonly string[]
190
+ readonly variantsDictionary?: VariantsDictionary | undefined
191
+ readonly elementTransition?: Transition | undefined
192
+ // T24 B1: the component custom payload for RESOLVER labels in gesture-state slots.
193
+ readonly custom?: unknown
194
+ readonly readValueState?:
195
+ | (() => {
196
+ readonly current: Readonly<Record<string, unknown>>
197
+ readonly velocity: Readonly<Record<string, number>>
198
+ })
199
+ | undefined
200
+ readonly componentId?: MotionComponentId
201
+ },
202
+ gate: StyleValueGate = THROW_GATE,
203
+ ): GestureStateGateResult {
204
+ const componentId = context.componentId ?? '<Motion.View>'
205
+ const accepted: AcceptedGestureState = {}
206
+ let fallbackAttempted = false
207
+ const refuse = (error: Error): void => {
208
+ if (gate.severity === 'development') throw error
209
+ gate.report(error)
210
+ }
211
+ /**
212
+ * The member conflicts BOTH forms ride (per-KEY granularity, r4 major 20): the drag-axis
213
+ * collision and the bound-key XOR (REQ-DRIVER-029) — the error, or null when the member
214
+ * is conflict-free.
215
+ */
216
+ const memberConflict = (key: 'whileTap' | 'whileDrag', memberKey: string): Error | null => {
217
+ if (key === 'whileDrag' && context.dragAxes.includes(memberKey as DragAxis)) {
218
+ return new Error(
219
+ `${componentId}: R6 scope: whileDrag names the drag axis key '${memberKey}' — a key has ONE ` +
220
+ 'reconciliation authority (REQ-DRIVER-029); the recognizer owns the axis while ' +
221
+ 'dragging.',
222
+ )
223
+ }
224
+ if (
225
+ context.boundKeys?.includes(memberKey) === true &&
226
+ context.identityEligibleBoundKeys?.includes(memberKey) !== true
227
+ ) {
228
+ return new Error(
229
+ `${componentId}: R6 scope: '${memberKey}' is style-bound AND a ${key} target key — a key has ONE ` +
230
+ 'reconciliation authority (REQ-DRIVER-029); drive it through the binding or the ' +
231
+ 'gesture state, never both.',
232
+ )
233
+ }
234
+ return null
235
+ }
236
+ for (const key of ['whileTap', 'whileDrag'] as const) {
237
+ const value = props[key]
238
+ if (value === undefined) continue
239
+ if (typeof value === 'function' || typeof value === 'string' || Array.isArray(value)) {
240
+ // The LABEL form (R7, law b): normalize the definition, resolve against the gated
241
+ // dictionary (a local miss contributes nothing — law a's propagation-source shape),
242
+ // flatten to per-key attribution, then run the SAME member conflicts the object form
243
+ // runs. Core validity, drivability, and the property-lane option law already ran at
244
+ // the dictionary gate — an entry that failed them never reached this dictionary.
245
+ const labels =
246
+ typeof value === 'function'
247
+ ? null
248
+ : gateLabelDefinition(key, value as string | readonly unknown[], gate, componentId)
249
+ if (labels === undefined) continue // production refused the label prop — no layer
250
+ if (key === 'whileDrag' && context.dragAxes.length === 0) {
251
+ refuse(
252
+ new Error(
253
+ `${componentId}: "whileDrag" requires an enabled drag axis — without one the state could never ` +
254
+ 'activate (a silently dead gesture surface; R6-F2 native law, recorded ' +
255
+ 'divergence from the pinned inertness).',
256
+ ),
257
+ )
258
+ continue
259
+ }
260
+ const definition: string | readonly string[] | VariantResolver =
261
+ typeof value === 'function' ? (value as VariantResolver) : labels!
262
+ const hasResolver =
263
+ typeof definition === 'function' ||
264
+ definition.some(
265
+ (label) =>
266
+ context.variantsDictionary !== undefined &&
267
+ Object.hasOwn(context.variantsDictionary, label) &&
268
+ typeof context.variantsDictionary[label] === 'function',
269
+ )
270
+ if (hasResolver) {
271
+ accepted[key] = {
272
+ target: {},
273
+ resolveAtActivation: () => {
274
+ const applications = resolveVariantApplicationsGated(
275
+ definition,
276
+ context.variantsDictionary,
277
+ gate,
278
+ componentId,
279
+ context.custom,
280
+ context.readValueState,
281
+ )
282
+ const synthetic: Record<string, Target & { transition?: Transition }> = {}
283
+ const syntheticLabels: string[] = []
284
+ applications.forEach((application, index) => {
285
+ const label = `__activation_${index}`
286
+ syntheticLabels.push(label)
287
+ synthetic[label] = {
288
+ ...application.target,
289
+ ...(application.transition === undefined
290
+ ? {}
291
+ : { transition: application.transition }),
292
+ }
293
+ })
294
+ const resolved = gateGestureStatePropsWithEvidence(
295
+ { [key]: syntheticLabels },
296
+ { ...context, variantsDictionary: synthetic },
297
+ gate,
298
+ ).state[key]
299
+ return resolved ?? { target: {} }
300
+ },
301
+ }
302
+ continue
303
+ }
304
+ const flattened = flattenVariantApplications(
305
+ resolveVariantApplicationsGated(
306
+ definition,
307
+ context.variantsDictionary,
308
+ gate,
309
+ componentId,
310
+ context.custom,
311
+ context.readValueState,
312
+ ),
313
+ )
314
+ const labelTarget: Record<string, unknown> = {}
315
+ const labelTransitions: Record<string, Transition> = {}
316
+ for (const [memberKey, memberValue] of Object.entries(flattened.target)) {
317
+ const conflict = memberConflict(key, memberKey)
318
+ if (conflict !== null) {
319
+ refuse(conflict)
320
+ continue
321
+ }
322
+ // The label lane runs the SAME native-depth executable matrix as the object lane
323
+ // (review 91d41675d511): a label-resolved scalar or array element outside the matrix
324
+ // refuses at the supplying boundary, never latently at activation.
325
+ if (nativeMountTargetValueKind(memberKey) !== 'color') {
326
+ const labelLengthRefusal = executableLengthValueRefusal(memberKey, memberValue)
327
+ if (labelLengthRefusal !== null) {
328
+ refuse(
329
+ new Error(
330
+ `${componentId}: R6 scope: ${key}.${memberKey} received ${describeValue(memberValue)} — ` +
331
+ `${labelLengthRefusal} (REQ-API-031).`,
332
+ ),
333
+ )
334
+ continue
335
+ }
336
+ }
337
+ // A resolved label's per-key attribution wins; otherwise it inherits the element transition
338
+ // just like the object form. Validate the EFFECTIVE transition before constructing the resolver
339
+ // so a timing mismatch cannot wait until gesture activation.
340
+ let attributed: Transition | undefined = flattened.transitions[memberKey]
341
+ // The SAME property-lane option law the object form runs on its embedded transition
342
+ // (review r2 major 1): the dictionary gate ADMITS the tree-lane orchestration options
343
+ // (they are legal on the animate lane, where the channel executes them), so a
344
+ // gesture-pulled label's attribution must refuse them HERE — gesture-lane wave
345
+ // orchestration is an unshipped REQ-API-053 deferred sibling, and an admitted key
346
+ // would ride a retarget into the property lane. Production drops the ATTRIBUTION only
347
+ // (the member survives on the element fallback, which the resolver supplies
348
+ // driver-clean) — mirroring the object form's transition-drop granularity.
349
+ if (attributed !== undefined) {
350
+ const attributionRefusal = nativePropertyTransitionRefusal(
351
+ componentId,
352
+ key + '.' + memberKey,
353
+ attributed,
354
+ [memberKey],
355
+ )
356
+ if (attributionRefusal !== null) {
357
+ refuse(attributionRefusal)
358
+ attributed = undefined
359
+ }
360
+ }
361
+ const effectiveTransition =
362
+ attributed !== undefined
363
+ ? resolveTransitionForKey(attributed, memberKey)
364
+ : context.elementTransition === undefined
365
+ ? undefined
366
+ : resolveTransitionForKey(context.elementTransition, memberKey)
367
+ // Evidence reads the LOCAL attribution: a production-dropped orchestration-bearing
368
+ // attribution really does consume the element fallback (the object form's drop path
369
+ // reaches the same conclusion through its own local `transition`).
370
+ if (attributed === undefined && context.elementTransition !== undefined) {
371
+ fallbackAttempted = true
372
+ }
373
+ const timingRefusal = keyframeTransitionRefusal(
374
+ componentId,
375
+ memberKey,
376
+ memberValue,
377
+ effectiveTransition,
378
+ )
379
+ if (timingRefusal !== null) {
380
+ refuse(timingRefusal)
381
+ continue
382
+ }
383
+ labelTarget[memberKey] = memberValue
384
+ if (attributed !== undefined) labelTransitions[memberKey] = effectiveTransition!
385
+ }
386
+ accepted[key] = {
387
+ target: labelTarget as Target,
388
+ ...(Object.keys(labelTransitions).length === 0 ? {} : { transitions: labelTransitions }),
389
+ }
390
+ continue
391
+ }
392
+ // PLAIN targets only (r3 major 16): a prototype-carrying object (new Date(0)) destructures
393
+ // to an empty target and would mount a silently dead gesture layer.
394
+ const proto =
395
+ typeof value === 'object' && value !== null ? Object.getPrototypeOf(value) : undefined
396
+ if (
397
+ typeof value !== 'object' ||
398
+ value === null ||
399
+ (proto !== Object.prototype && proto !== null)
400
+ ) {
401
+ refuse(
402
+ new Error(
403
+ `${componentId}: "${key}" must be a plain target object, received ${value === null ? 'null' : typeof value} (REQ-API-031).`,
404
+ ),
405
+ )
406
+ continue
407
+ }
408
+ if (key === 'whileDrag' && context.dragAxes.length === 0) {
409
+ refuse(
410
+ new Error(
411
+ `${componentId}: "whileDrag" requires an enabled drag axis — without one the state could never ` +
412
+ 'activate (a silently dead gesture surface; R6-F2 native law, recorded divergence ' +
413
+ 'from the pinned inertness).',
414
+ ),
415
+ )
416
+ continue
417
+ }
418
+ // Read `value.transition` EXACTLY ONCE via the destructure (review major a13e69f) — re-reading it for
419
+ // the capture would observe an outer-property accessor twice. Capture the embedded transition ONCE
420
+ // (review major 0cb829): it is validated here AND stored in the accepted layer that gesture activation
421
+ // later executes — a caller-owned accessor validated as `0.2` must not mutate to `-1` after this render
422
+ // and change what the gesture forwards. A malformed SHAPE stays raw so validateTransitionRefusal throws
423
+ // its typed refusal (a capture would launder it).
424
+ const { transition: rawTransition, ...target } = value as Target & { transition?: Transition }
425
+ let transition = captureEmbeddedTransition(rawTransition)
426
+ // The embedded transition is runtime-validated like any other member (r4 major 21,
427
+ // G-INV-8): `transition: null` must never ride a nullish fallback into "absent".
428
+ // Production drops the malformed member with a named report; the target survives.
429
+ // Beyond core's vocabulary/range law, the NATIVE property lane executes only its own
430
+ // option set (r5 major d6c3850af147): `delay` is rebased by the property driver and
431
+ // `velocity` seeds the spring since T21 — the surviving refusal exemplar is a tree-lane
432
+ // orchestration key, which this lane never reads and must never ride as a silent no-op.
433
+ // RETURN-shaped, no catch (M3 r8 major b9eb82c0d429): only a returned/constructed
434
+ // refusal is refused; a dependency fault thrown through core validation propagates
435
+ // with its exact identity.
436
+ const transitionRefusal = validateTransitionRefusal(transition, {
437
+ componentId,
438
+ })
439
+ if (transitionRefusal !== null) {
440
+ refuse(transitionRefusal)
441
+ transition = undefined
442
+ } else if (transition !== undefined) {
443
+ const nativeTransitionRefusal = nativePropertyTransitionRefusal(
444
+ componentId,
445
+ key + '.transition',
446
+ transition,
447
+ Object.keys(target),
448
+ )
449
+ if (nativeTransitionRefusal !== null) {
450
+ refuse(nativeTransitionRefusal)
451
+ transition = undefined
452
+ }
453
+ }
454
+ // PER-KEY validation (r3 major 16) mirroring the shared severity law's granularity: core
455
+ // validateTarget AND the native drivability law per member — development throws on the
456
+ // first offender, production reports it and KEEPS the valid members. The drivability check
457
+ // exists because a unit string (x: '10px') is core-valid but this rung's native lanes
458
+ // drive numbers (rgba keys ride the color lane) — accepting it would mount a dead value.
459
+ const acceptedTarget: Record<string, unknown> = {}
460
+ for (const [memberKey, rawMember] of Object.entries(target)) {
461
+ // Per-KEY refusal (r4 major 20; the bound XOR strips HERE, r3 major 17): the conflict
462
+ // drops this member only — valid siblings survive, the severity law's granularity.
463
+ const conflict = memberConflict(key, memberKey)
464
+ if (conflict !== null) {
465
+ refuse(conflict)
466
+ continue
467
+ }
468
+ // Materialize a keyframe ARRAY member into a frozen snapshot ONCE, before validation (review
469
+ // major 40, the r3-major-16 one-read law): an accessor-backed array could return a valid member
470
+ // to the validator and a different value (null) when the controller later reads it. The single
471
+ // frozen snapshot is the ONE truth validated AND forwarded — a scalar is already immutable.
472
+ let memberValue: unknown = rawMember
473
+ if (Array.isArray(rawMember)) {
474
+ const capture = captureBoundedArray(rawMember)
475
+ if (capture.kind !== 'captured') {
476
+ refuse(
477
+ new InvalidTargetError(
478
+ componentId,
479
+ memberKey,
480
+ capturedArrayDescription(capture),
481
+ 'keyframe arrays must have a safe bounded length (REQ-API-033)',
482
+ ),
483
+ )
484
+ continue
485
+ }
486
+ memberValue = capture.values
487
+ }
488
+ const memberRefusal = validateTargetRefusal(
489
+ { [memberKey]: memberValue } as Target,
490
+ NATIVE_HOST,
491
+ {
492
+ componentId,
493
+ },
494
+ )
495
+ if (memberRefusal !== null) {
496
+ refuse(memberRefusal)
497
+ continue
498
+ }
499
+ const valueKind = nativeMountTargetValueKind(memberKey)
500
+ // Both lanes drive a scalar OR a keyframe ARRAY (REQ-API-033 on the gesture-state surface, majors
501
+ // 31 + 35): core validation already proved a well-formed same-value-type array, and the
502
+ // value-agnostic resolver + array-aware controller drive it — the color lane through the progress
503
+ // projection (projectColorSequence). Length strings ride the ONE key-aware executable matrix
504
+ // element-wise (review 91d41675d511): positional keys, auto for width/height only.
505
+ const lengthRefusal =
506
+ valueKind === 'color' ? null : executableLengthValueRefusal(memberKey, memberValue)
507
+ const drivable =
508
+ valueKind === 'color'
509
+ ? typeof memberValue === 'string' || Array.isArray(memberValue)
510
+ : isNativeExecutableLengthValue(memberKey, memberValue)
511
+ if (!drivable) {
512
+ refuse(
513
+ new Error(
514
+ `${componentId}: R6 scope: ${key}.${memberKey} received ${describeValue(memberValue)} — ${
515
+ lengthRefusal ??
516
+ `the native ${valueKind} lane drives ${
517
+ valueKind === 'color'
518
+ ? 'color strings or a color keyframe array'
519
+ : 'numbers, numeric keyframe arrays, or measure-resolved positional lengths'
520
+ }; the supplied value is not executable (REQ-API-031).`
521
+ }`,
522
+ ),
523
+ )
524
+ continue
525
+ }
526
+ // R8-F1 at the SUPPLYING boundary (review major 42): a keyframe array member under an EMBEDDED
527
+ // spring interpolates exactly two keyframes — a longer array is refused EAGERLY here (production
528
+ // reports + drops the member, siblings survive) instead of an uncaught controller throw on
529
+ // activation. An array with NO embedded spring rides the element-transition fallback, refused by
530
+ // the controller's own severity-aware compat filter.
531
+ const effectiveTransition =
532
+ transition !== undefined
533
+ ? resolveTransitionForKey(transition, memberKey)
534
+ : context.elementTransition === undefined
535
+ ? undefined
536
+ : resolveTransitionForKey(context.elementTransition, memberKey)
537
+ // A supplied embedded transition owns this member even when it is later refused. Only a member
538
+ // authored without one attempts the element fallback.
539
+ // A malformed embedded transition is refused before this member is evaluated. If the
540
+ // surviving target then reaches the element fallback, it is a real fallback attempt just
541
+ // like an omitted embedded transition; raw authorship is not execution ownership.
542
+ if (transition === undefined && context.elementTransition !== undefined) {
543
+ fallbackAttempted = true
544
+ }
545
+ // R8 M2-B (REQ-API-033 law d): validate this layer's EMBEDDED keyframe timing. Shape-valid
546
+ // count mismatches use the pin's offsets/easing fallback; keyframe-only timing on a scalar
547
+ // remains inert and refuses. Report + drop per member under severity; siblings survive.
548
+ const timingRefusal = keyframeTransitionRefusal(
549
+ componentId,
550
+ memberKey,
551
+ memberValue,
552
+ effectiveTransition,
553
+ )
554
+ if (timingRefusal !== null) {
555
+ refuse(timingRefusal)
556
+ continue
557
+ }
558
+ acceptedTarget[memberKey] = memberValue
559
+ }
560
+ accepted[key] = { target: acceptedTarget as Target, transition }
561
+ }
562
+ return { state: accepted, fallbackAttempted }
563
+ }