@unrulysystems/native-motion-core 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 (326) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +48 -0
  4. package/dist/clock.cjs +71 -0
  5. package/dist/clock.d.cts +23 -0
  6. package/dist/clock.d.ts +23 -0
  7. package/dist/clock.js +66 -0
  8. package/dist/collect-reads.cjs +68 -0
  9. package/dist/collect-reads.d.cts +23 -0
  10. package/dist/collect-reads.d.ts +23 -0
  11. package/dist/collect-reads.js +62 -0
  12. package/dist/component/boundedArray.cjs +74 -0
  13. package/dist/component/boundedArray.d.cts +25 -0
  14. package/dist/component/boundedArray.d.ts +25 -0
  15. package/dist/component/boundedArray.js +68 -0
  16. package/dist/component/index.cjs +57 -0
  17. package/dist/component/index.d.cts +12 -0
  18. package/dist/component/index.d.ts +12 -0
  19. package/dist/component/index.js +22 -0
  20. package/dist/component/orchestration.cjs +129 -0
  21. package/dist/component/orchestration.d.cts +66 -0
  22. package/dist/component/orchestration.d.ts +66 -0
  23. package/dist/component/orchestration.js +124 -0
  24. package/dist/component/resolve.cjs +104 -0
  25. package/dist/component/resolve.d.cts +11 -0
  26. package/dist/component/resolve.d.ts +11 -0
  27. package/dist/component/resolve.js +97 -0
  28. package/dist/component/transition.cjs +352 -0
  29. package/dist/component/transition.d.cts +33 -0
  30. package/dist/component/transition.d.ts +33 -0
  31. package/dist/component/transition.js +339 -0
  32. package/dist/component/types.cjs +114 -0
  33. package/dist/component/types.d.cts +60 -0
  34. package/dist/component/types.d.ts +60 -0
  35. package/dist/component/types.js +111 -0
  36. package/dist/component/validate.cjs +1015 -0
  37. package/dist/component/validate.d.cts +37 -0
  38. package/dist/component/validate.d.ts +37 -0
  39. package/dist/component/validate.js +1002 -0
  40. package/dist/component/variants.cjs +333 -0
  41. package/dist/component/variants.d.cts +106 -0
  42. package/dist/component/variants.d.ts +106 -0
  43. package/dist/component/variants.js +321 -0
  44. package/dist/config/constants.cjs +41 -0
  45. package/dist/config/constants.d.cts +29 -0
  46. package/dist/config/constants.d.ts +29 -0
  47. package/dist/config/constants.js +38 -0
  48. package/dist/delay.cjs +72 -0
  49. package/dist/delay.d.cts +26 -0
  50. package/dist/delay.d.ts +26 -0
  51. package/dist/delay.js +70 -0
  52. package/dist/derived.cjs +143 -0
  53. package/dist/derived.d.cts +41 -0
  54. package/dist/derived.d.ts +41 -0
  55. package/dist/derived.js +139 -0
  56. package/dist/driver/index.cjs +22 -0
  57. package/dist/driver/index.d.cts +7 -0
  58. package/dist/driver/index.d.ts +7 -0
  59. package/dist/driver/index.js +14 -0
  60. package/dist/driver/keyframeTiming.cjs +130 -0
  61. package/dist/driver/keyframeTiming.d.cts +18 -0
  62. package/dist/driver/keyframeTiming.d.ts +18 -0
  63. package/dist/driver/keyframeTiming.js +124 -0
  64. package/dist/driver/keyframeTimingConfig.cjs +24 -0
  65. package/dist/driver/keyframeTimingConfig.d.cts +5 -0
  66. package/dist/driver/keyframeTimingConfig.d.ts +5 -0
  67. package/dist/driver/keyframeTimingConfig.js +22 -0
  68. package/dist/driver/prepare.cjs +459 -0
  69. package/dist/driver/prepare.d.cts +4 -0
  70. package/dist/driver/prepare.d.ts +4 -0
  71. package/dist/driver/prepare.js +454 -0
  72. package/dist/driver/reference.cjs +762 -0
  73. package/dist/driver/reference.d.cts +53 -0
  74. package/dist/driver/reference.d.ts +53 -0
  75. package/dist/driver/reference.js +757 -0
  76. package/dist/driver/step.cjs +55 -0
  77. package/dist/driver/step.d.cts +9 -0
  78. package/dist/driver/step.d.ts +9 -0
  79. package/dist/driver/step.js +52 -0
  80. package/dist/driver/tiers.cjs +40 -0
  81. package/dist/driver/tiers.d.cts +2 -0
  82. package/dist/driver/tiers.d.ts +2 -0
  83. package/dist/driver/tiers.js +37 -0
  84. package/dist/driver/types.cjs +9 -0
  85. package/dist/driver/types.d.cts +68 -0
  86. package/dist/driver/types.d.ts +68 -0
  87. package/dist/driver/types.js +8 -0
  88. package/dist/external-animation-ledger.cjs +1250 -0
  89. package/dist/external-animation-ledger.d.cts +416 -0
  90. package/dist/external-animation-ledger.d.ts +416 -0
  91. package/dist/external-animation-ledger.js +1243 -0
  92. package/dist/gesture/directionLock.cjs +25 -0
  93. package/dist/gesture/directionLock.d.cts +8 -0
  94. package/dist/gesture/directionLock.d.ts +8 -0
  95. package/dist/gesture/directionLock.js +21 -0
  96. package/dist/gesture/dragConfig.cjs +634 -0
  97. package/dist/gesture/dragConfig.d.cts +298 -0
  98. package/dist/gesture/dragConfig.d.ts +298 -0
  99. package/dist/gesture/dragConfig.js +624 -0
  100. package/dist/gesture/elastic.cjs +44 -0
  101. package/dist/gesture/elastic.d.cts +4 -0
  102. package/dist/gesture/elastic.d.ts +4 -0
  103. package/dist/gesture/elastic.js +39 -0
  104. package/dist/gesture/handoffSession.cjs +161 -0
  105. package/dist/gesture/handoffSession.d.cts +35 -0
  106. package/dist/gesture/handoffSession.d.ts +35 -0
  107. package/dist/gesture/handoffSession.js +158 -0
  108. package/dist/gesture/index.cjs +37 -0
  109. package/dist/gesture/index.d.cts +12 -0
  110. package/dist/gesture/index.d.ts +12 -0
  111. package/dist/gesture/index.js +11 -0
  112. package/dist/gesture/projection.cjs +95 -0
  113. package/dist/gesture/projection.d.cts +7 -0
  114. package/dist/gesture/projection.d.ts +7 -0
  115. package/dist/gesture/projection.js +87 -0
  116. package/dist/gesture/session.cjs +162 -0
  117. package/dist/gesture/session.d.cts +29 -0
  118. package/dist/gesture/session.d.ts +29 -0
  119. package/dist/gesture/session.js +158 -0
  120. package/dist/gesture/types.cjs +5 -0
  121. package/dist/gesture/types.d.cts +13 -0
  122. package/dist/gesture/types.d.ts +13 -0
  123. package/dist/gesture/types.js +4 -0
  124. package/dist/gesture/viewportConstraints.cjs +38 -0
  125. package/dist/gesture/viewportConstraints.d.cts +6 -0
  126. package/dist/gesture/viewportConstraints.d.ts +6 -0
  127. package/dist/gesture/viewportConstraints.js +34 -0
  128. package/dist/graph.cjs +226 -0
  129. package/dist/graph.d.cts +2 -0
  130. package/dist/graph.d.ts +2 -0
  131. package/dist/graph.js +223 -0
  132. package/dist/index.cjs +140 -0
  133. package/dist/index.d.cts +33 -0
  134. package/dist/index.d.ts +33 -0
  135. package/dist/index.js +70 -0
  136. package/dist/inertia.cjs +214 -0
  137. package/dist/inertia.d.cts +53 -0
  138. package/dist/inertia.d.ts +53 -0
  139. package/dist/inertia.js +212 -0
  140. package/dist/instant.cjs +66 -0
  141. package/dist/instant.d.cts +16 -0
  142. package/dist/instant.d.ts +16 -0
  143. package/dist/instant.js +63 -0
  144. package/dist/internal-driver.cjs +81 -0
  145. package/dist/internal-driver.d.cts +19 -0
  146. package/dist/internal-driver.d.ts +19 -0
  147. package/dist/internal-driver.js +35 -0
  148. package/dist/keyframes.cjs +191 -0
  149. package/dist/keyframes.d.cts +13 -0
  150. package/dist/keyframes.d.ts +13 -0
  151. package/dist/keyframes.js +188 -0
  152. package/dist/layout/commitDetector.cjs +67 -0
  153. package/dist/layout/commitDetector.d.cts +21 -0
  154. package/dist/layout/commitDetector.d.ts +21 -0
  155. package/dist/layout/commitDetector.js +64 -0
  156. package/dist/layout/compose.cjs +67 -0
  157. package/dist/layout/compose.d.cts +30 -0
  158. package/dist/layout/compose.d.ts +30 -0
  159. package/dist/layout/compose.js +65 -0
  160. package/dist/layout/constants.cjs +13 -0
  161. package/dist/layout/constants.d.cts +6 -0
  162. package/dist/layout/constants.d.ts +6 -0
  163. package/dist/layout/constants.js +10 -0
  164. package/dist/layout/identity.cjs +700 -0
  165. package/dist/layout/identity.d.cts +67 -0
  166. package/dist/layout/identity.d.ts +67 -0
  167. package/dist/layout/identity.js +698 -0
  168. package/dist/layout/index.cjs +36 -0
  169. package/dist/layout/index.d.cts +18 -0
  170. package/dist/layout/index.d.ts +18 -0
  171. package/dist/layout/index.js +13 -0
  172. package/dist/layout/measure.cjs +81 -0
  173. package/dist/layout/measure.d.cts +43 -0
  174. package/dist/layout/measure.d.ts +43 -0
  175. package/dist/layout/measure.js +78 -0
  176. package/dist/layout/projection.cjs +99 -0
  177. package/dist/layout/projection.d.cts +23 -0
  178. package/dist/layout/projection.d.ts +23 -0
  179. package/dist/layout/projection.js +97 -0
  180. package/dist/layout/scroll.cjs +27 -0
  181. package/dist/layout/scroll.d.cts +13 -0
  182. package/dist/layout/scroll.d.ts +13 -0
  183. package/dist/layout/scroll.js +24 -0
  184. package/dist/layout/session.cjs +207 -0
  185. package/dist/layout/session.d.cts +73 -0
  186. package/dist/layout/session.d.ts +73 -0
  187. package/dist/layout/session.js +205 -0
  188. package/dist/layout/tree.cjs +826 -0
  189. package/dist/layout/tree.d.cts +70 -0
  190. package/dist/layout/tree.d.ts +70 -0
  191. package/dist/layout/tree.js +823 -0
  192. package/dist/layout/types.cjs +36 -0
  193. package/dist/layout/types.d.cts +31 -0
  194. package/dist/layout/types.d.ts +31 -0
  195. package/dist/layout/types.js +35 -0
  196. package/dist/motion-arc.cjs +184 -0
  197. package/dist/motion-arc.d.cts +78 -0
  198. package/dist/motion-arc.d.ts +78 -0
  199. package/dist/motion-arc.js +183 -0
  200. package/dist/motion-mix.cjs +205 -0
  201. package/dist/motion-mix.d.cts +3 -0
  202. package/dist/motion-mix.d.ts +3 -0
  203. package/dist/motion-mix.js +202 -0
  204. package/dist/motion-value-driver-port.cjs +636 -0
  205. package/dist/motion-value-driver-port.d.cts +406 -0
  206. package/dist/motion-value-driver-port.d.ts +406 -0
  207. package/dist/motion-value-driver-port.js +627 -0
  208. package/dist/motion-value.cjs +189 -0
  209. package/dist/motion-value.d.cts +51 -0
  210. package/dist/motion-value.d.ts +51 -0
  211. package/dist/motion-value.js +185 -0
  212. package/dist/presence/controller.cjs +657 -0
  213. package/dist/presence/controller.d.cts +6 -0
  214. package/dist/presence/controller.d.ts +6 -0
  215. package/dist/presence/controller.js +652 -0
  216. package/dist/presence/index.cjs +19 -0
  217. package/dist/presence/index.d.cts +4 -0
  218. package/dist/presence/index.d.ts +4 -0
  219. package/dist/presence/index.js +12 -0
  220. package/dist/presence/machine.cjs +50 -0
  221. package/dist/presence/machine.d.cts +10 -0
  222. package/dist/presence/machine.d.ts +10 -0
  223. package/dist/presence/machine.js +46 -0
  224. package/dist/presence/types.cjs +6 -0
  225. package/dist/presence/types.d.cts +32 -0
  226. package/dist/presence/types.d.ts +32 -0
  227. package/dist/presence/types.js +5 -0
  228. package/dist/repeat.cjs +311 -0
  229. package/dist/repeat.d.cts +174 -0
  230. package/dist/repeat.d.ts +174 -0
  231. package/dist/repeat.js +300 -0
  232. package/dist/spring.cjs +128 -0
  233. package/dist/spring.d.cts +18 -0
  234. package/dist/spring.d.ts +18 -0
  235. package/dist/spring.js +125 -0
  236. package/dist/subscriptions.cjs +74 -0
  237. package/dist/subscriptions.d.cts +19 -0
  238. package/dist/subscriptions.d.ts +19 -0
  239. package/dist/subscriptions.js +69 -0
  240. package/dist/subset/index.cjs +23 -0
  241. package/dist/subset/index.d.cts +6 -0
  242. package/dist/subset/index.d.ts +6 -0
  243. package/dist/subset/index.js +13 -0
  244. package/dist/subset/normalize.cjs +90 -0
  245. package/dist/subset/normalize.d.cts +10 -0
  246. package/dist/subset/normalize.d.ts +10 -0
  247. package/dist/subset/normalize.js +85 -0
  248. package/dist/subset/registry.cjs +259 -0
  249. package/dist/subset/registry.d.cts +25 -0
  250. package/dist/subset/registry.d.ts +25 -0
  251. package/dist/subset/registry.js +256 -0
  252. package/dist/subset/resolve.cjs +98 -0
  253. package/dist/subset/resolve.d.cts +24 -0
  254. package/dist/subset/resolve.d.ts +24 -0
  255. package/dist/subset/resolve.js +90 -0
  256. package/dist/timing.cjs +206 -0
  257. package/dist/timing.d.cts +17 -0
  258. package/dist/timing.d.ts +17 -0
  259. package/dist/timing.js +201 -0
  260. package/dist/transformTemplate.cjs +219 -0
  261. package/dist/transformTemplate.d.cts +29 -0
  262. package/dist/transformTemplate.d.ts +29 -0
  263. package/dist/transformTemplate.js +215 -0
  264. package/dist/transition.cjs +231 -0
  265. package/dist/transition.d.cts +89 -0
  266. package/dist/transition.d.ts +89 -0
  267. package/dist/transition.js +223 -0
  268. package/dist/types.cjs +4 -0
  269. package/dist/types.d.cts +175 -0
  270. package/dist/types.d.ts +175 -0
  271. package/dist/types.js +3 -0
  272. package/dist/value-types/color.cjs +220 -0
  273. package/dist/value-types/color.d.cts +19 -0
  274. package/dist/value-types/color.d.ts +19 -0
  275. package/dist/value-types/color.js +218 -0
  276. package/dist/value-types/complex.cjs +160 -0
  277. package/dist/value-types/complex.d.cts +19 -0
  278. package/dist/value-types/complex.d.ts +19 -0
  279. package/dist/value-types/complex.js +155 -0
  280. package/dist/value-types/constants.cjs +15 -0
  281. package/dist/value-types/constants.d.cts +6 -0
  282. package/dist/value-types/constants.d.ts +6 -0
  283. package/dist/value-types/constants.js +12 -0
  284. package/dist/value-types/discrete.cjs +62 -0
  285. package/dist/value-types/discrete.d.cts +4 -0
  286. package/dist/value-types/discrete.d.ts +4 -0
  287. package/dist/value-types/discrete.js +56 -0
  288. package/dist/value-types/index.cjs +59 -0
  289. package/dist/value-types/index.d.cts +14 -0
  290. package/dist/value-types/index.d.ts +14 -0
  291. package/dist/value-types/index.js +24 -0
  292. package/dist/value-types/measure-resolve.cjs +325 -0
  293. package/dist/value-types/measure-resolve.d.cts +91 -0
  294. package/dist/value-types/measure-resolve.d.ts +91 -0
  295. package/dist/value-types/measure-resolve.js +313 -0
  296. package/dist/value-types/mix.cjs +90 -0
  297. package/dist/value-types/mix.d.cts +27 -0
  298. package/dist/value-types/mix.d.ts +27 -0
  299. package/dist/value-types/mix.js +85 -0
  300. package/dist/value-types/named-colors.cjs +61 -0
  301. package/dist/value-types/named-colors.d.cts +2 -0
  302. package/dist/value-types/named-colors.d.ts +2 -0
  303. package/dist/value-types/named-colors.js +58 -0
  304. package/dist/value-types/numeric.cjs +86 -0
  305. package/dist/value-types/numeric.d.cts +16 -0
  306. package/dist/value-types/numeric.d.ts +16 -0
  307. package/dist/value-types/numeric.js +79 -0
  308. package/dist/worklet-layout/config/constants.js +39 -0
  309. package/dist/worklet-layout/layout/constants.js +11 -0
  310. package/dist/worklet-layout/layout/identity.js +699 -0
  311. package/dist/worklet-layout/layout/projection.js +98 -0
  312. package/dist/worklet-layout/layout/session.js +206 -0
  313. package/dist/worklet-layout/layout/tree.js +824 -0
  314. package/dist/worklet-layout/layout/types.js +36 -0
  315. package/dist/worklet-layout/spring.js +125 -0
  316. package/dist/worklet-layout/timing.js +201 -0
  317. package/dist/worklet-layout/transition.js +223 -0
  318. package/dist/worklet-layout.cjs +47 -0
  319. package/dist/worklet-layout.d.cts +15 -0
  320. package/dist/worklet-layout.d.ts +15 -0
  321. package/dist/worklet-layout.js +28 -0
  322. package/dist/wrap.cjs +7 -0
  323. package/dist/wrap.d.cts +1 -0
  324. package/dist/wrap.d.ts +1 -0
  325. package/dist/wrap.js +4 -0
  326. package/package.json +45 -0
@@ -0,0 +1,757 @@
1
+ // SPEC-NATIVE-DRIVER §Requirements — the reference in-core Driver (REQ-DRIVER-013/020). A concrete,
2
+ // host-agnostic `Driver` the fake host adapter and the deterministic conformance drive; the native worklet
3
+ // and web drivers are the substrate-bound peers of this same interface. It composes the built ladder and
4
+ // forks nothing (REQ-DRIVER-003): the SPRING/TIMING generators (via the COMPONENT seconds→ms converters)
5
+ // supply every trajectory, `retargetSpring` supplies spring continuity, and the pure `stepProp` advances
6
+ // each frame. A command seeds state once (O(1)); `step` reads only seeded state — zero per-frame JS logic
7
+ // beyond the pure advance (REQ-DRIVER-013). An element quiesces when all its animations settle
8
+ // (REQ-DRIVER-020). Host-agnostic (REQ-CORE-003): relative imports only.
9
+ import { InvalidTransitionError, toDelayMs, toKeyframesConfig, toRepeatFoldOptions, toSpringConfig, toTimingConfig, } from "../component/index.js";
10
+ import { isDurationZeroInstantTransition, isTimingLaneTransition, toTimingLaneConfig, } from "../component/transition.js";
11
+ import { delayGenerator } from "../delay.js";
12
+ import { inertiaGenerator } from "../inertia.js";
13
+ import { instantFinalKeyframe, instantGenerator } from "../instant.js";
14
+ import { keyframesGenerator } from "../keyframes.js";
15
+ import { buildRepeatedGenerator, iterationMeasurementLane } from "../repeat.js";
16
+ import { timingGenerator } from "../timing.js";
17
+ import { resolveSpring, resolveSpringGenerator, retargetSpring, springKeyframeCountRefusal, } from "../transition.js";
18
+ import { applyPathProgress } from "../motion-arc.js";
19
+ import { adaptPreparedDriver } from "./prepare.js";
20
+ import { stepProp } from "./step.js";
21
+ // Clear the gesture hold on ONE prop (the matching release/stop command owns it now). Idempotent —
22
+ // clearing an unheld prop is a no-op, so the heldCount stays exact (review r7 major 23).
23
+ function releaseHold(element, key) {
24
+ if (element.heldVelocity[key] !== undefined) {
25
+ delete element.heldVelocity[key];
26
+ element.heldCount--;
27
+ }
28
+ }
29
+ // Build a fresh generator for a `start`: from the resolved base to the target, under the transition. Springs
30
+ // and tweens are selected by `transition.type` — with one pin-mandated addition (REQ-TIMING-006): an
31
+ // authored duration-ONLY bag (type omitted) also takes the timing generator, the pin's
32
+ // `type = keyframesGenerator` default for any DEFINED transition. Spring remains the fallback for
33
+ // everything else, reusing the COMPONENT seconds→ms
34
+ // converters + the built generators — no forked math (REQ-DRIVER-003). `velocity` is the REQ-DRIVER-023
35
+ // seed (the gesture release's platform handoff); a tween is position-based and ignores it by construction,
36
+ // kept explicit here so the contract reads at the seam.
37
+ // Normalize a PropTransition to the authored inputs the generators consume (R8 M2, consult
38
+ // agent-2026-07-19-e715fd). A controller-resolved default (`DefaultTransition`) carries shapes the
39
+ // authored `Transition` cannot: a `type:'keyframes'` default normalizes to the equivalent tween — an
40
+ // ABSENT `ease` is preserved so the keyframes generator keeps its intrinsic per-segment easeInOut (the
41
+ // 800ms default); a `type:'spring'` default carries `restSpeed` (Motion's default-transitions
42
+ // override), returned alongside to thread into the spring's settle threshold. Since U7c
43
+ // (REQ-SPRING-014) an AUTHORED spring may also carry restSpeed/restDelta — the extraction below
44
+ // reads the same key either way, and `springConfigWithRest` threads it over `toSpringConfig`'s own
45
+ // passthrough, so authored and default thresholds reach the solver identically.
46
+ function normalizeTransition(transition) {
47
+ if (transition.type === 'keyframes') {
48
+ return {
49
+ authored: {
50
+ type: 'tween',
51
+ duration: transition.duration,
52
+ ...(transition.ease !== undefined ? { ease: transition.ease } : {}),
53
+ ...(transition.delay !== undefined ? { delay: transition.delay } : {}),
54
+ ...(transition.repeat !== undefined ? { repeat: transition.repeat } : {}),
55
+ ...(transition.repeatType !== undefined ? { repeatType: transition.repeatType } : {}),
56
+ ...(transition.repeatDelay !== undefined ? { repeatDelay: transition.repeatDelay } : {}),
57
+ },
58
+ restSpeed: undefined,
59
+ };
60
+ }
61
+ const restSpeed = transition.type === 'spring' && 'restSpeed' in transition ? transition.restSpeed : undefined;
62
+ return { authored: transition, restSpeed };
63
+ }
64
+ // Thread a default spring's `restSpeed` (settle threshold) into the resolved SpringConfig;
65
+ // authored restDelta/restSpeed (U7c) already flow through `toSpringConfig`'s passthrough.
66
+ function springConfigWithRest(transition, restSpeed) {
67
+ return restSpeed !== undefined
68
+ ? { ...toSpringConfig(transition), restSpeed }
69
+ : toSpringConfig(transition);
70
+ }
71
+ /**
72
+ * T18-a (REQ-API-049 / REQ-DRIVER-031): apply the repeat fold around whatever trajectory the
73
+ * transition resolves to. The fold is generator-agnostic, so this wraps the ONE unrepeated
74
+ * construction below rather than teaching any generator about repetition, and it delegates to
75
+ * core's shared `buildRepeatedGenerator` — the same call the worklet driver makes. Sharing the call
76
+ * is necessary but NOT sufficient for parity: both backends shared it and still skewed, because
77
+ * only the start seam folded. What actually holds the two backends together is the executing floor,
78
+ * `driverParity.differential.test.ts`. (`check:resolution-skew` compares package.json export
79
+ * conditions — module ENTRY-POINT resolution — and has never inspected either driver.)
80
+ *
81
+ * An inertia transition never folds — and is not REFUSED, which an earlier draft of this comment
82
+ * claimed (fix-up review). There is nothing to refuse: `inertia` is absent from the public
83
+ * `TransitionType` (`'spring' | 'tween' | false`), so it is unauthorable, reaching this seam only as the
84
+ * command layer's own gesture-release settle, which carries no authored options. The branch is
85
+ * therefore a defensive skip over a shape that cannot carry a fold, not a severity boundary.
86
+ */
87
+ function startGenerator(transition, to, from, velocity) {
88
+ const { authored } = normalizeTransition(transition);
89
+ // U7a + transition-default-selection F5: the instant lane — the pin's makeAnimationInstant
90
+ // (motion-dom@12.42.2 motion-value.ts:89-98). The guard is the pin's, type-independent:
91
+ // `type: false` (U7a) OR `duration === 0 && !repeatDelay` (F5). NO trajectory generator, NO
92
+ // fold: the final keyframe commits on the driver's next update tick (never synchronously), and
93
+ // the authored delay is honored through the shared rebase. A repeatDelay-bearing duration-zero
94
+ // bag is exempt and keeps its generator below (the zero-length timing base there takes the same
95
+ // instant SHAPE so the fold's plays stay zero-length). The inertia narrow keeps the union
96
+ // discriminated for the delay crossing.
97
+ if (authored.type !== 'inertia' &&
98
+ (authored.type === false || isDurationZeroInstantTransition(authored))) {
99
+ return delayGenerator(instantGenerator(instantFinalKeyframe(to))({ from, velocity: 0 }), toDelayMs(authored));
100
+ }
101
+ const fold = authored.type === 'inertia' ? null : toRepeatFoldOptions(authored);
102
+ // T18-b (REQ-TIMING-004 / REQ-DRIVER-032): the delay rebase wraps AROUND the fold-or-generator —
103
+ // core's shared `delayGenerator`, the ONE rebase seam the worklet driver calls identically at
104
+ // BOTH phases. An inertia release is command-layer-only and carries no authored delay (mirroring
105
+ // the fold's defensive skip, not a severity boundary). `toDelayMs` is the seconds→ms crossing.
106
+ // Sharing the call is not parity on its own — `driverParity.differential.test.ts` executes both
107
+ // backends over delayed commands and compares frame for frame.
108
+ const delayMs = authored.type === 'inertia' ? 0 : toDelayMs(authored);
109
+ if (fold === null)
110
+ return delayGenerator(unrepeatedGenerator(transition, to, from, velocity, false), delayMs);
111
+ // Legs are built EAGERLY and passed as values — never a factory the fold would invoke, which on
112
+ // the UI runtime is the wrong-runtime crash class (`check:worklet-closures`). The worklet driver
113
+ // mirrors this shape exactly.
114
+ const leg = fold.repeatType === 'mirror'
115
+ ? { type: 'mirror', mirrored: unrepeatedGenerator(transition, to, from, velocity, true) }
116
+ : { type: fold.repeatType };
117
+ const folded = buildRepeatedGenerator(fold.repeat, leg, fold.repeatDelayMs, unrepeatedGenerator(transition, to, from, velocity, false), knownIterationDurationMs(transition, to));
118
+ if (folded instanceof Error)
119
+ throw repeatRefusal(fold, folded);
120
+ return delayGenerator(folded, delayMs);
121
+ }
122
+ /**
123
+ * The fold's refusal, TYPED (T18-a fix-up, review MAJOR). Whether a trajectory ever settles depends
124
+ * on the DISTANCE travelled and the seed velocity, so it is not decidable from the authored
125
+ * transition and validation cannot own it — this seam, which holds the real generator, does. It
126
+ * arrived here as a bare `Error`, which is what let the failure surface as an anonymous driver-lane
127
+ * fault instead of a refusal naming the offending property.
128
+ */
129
+ // alloc-ok: lifecycle-edge — built only on the refusal path, once per rejected animate command.
130
+ function repeatRefusal(fold, refusal) {
131
+ return new InvalidTransitionError(undefined, 'repeat', fold.repeat, refusal.message);
132
+ }
133
+ /**
134
+ * The transition's own resolved iteration length in ms, or null when only a scan can measure it —
135
+ * mirroring the pin's `calculatedDuration` rule (`spring.ts:378`: `isResolvedFromDuration ?
136
+ * duration || null : null`; the keyframes generator always knows its own `duration`).
137
+ */
138
+ function knownIterationDurationMs(transition, to) {
139
+ const { authored } = normalizeTransition(transition);
140
+ // Narrows `authored` off the inertia arm as well as answering the lane's first question.
141
+ if (authored.type === 'inertia')
142
+ return null;
143
+ // The lane comes from core's ONE rule, never re-derived here (T18-a fix-up review MAJOR: three
144
+ // parallel copies of it drifted across four rounds).
145
+ const lane = iterationMeasurementLane(typeof to === 'number', authored.type === 'spring',
146
+ // REQ-TIMING-006 + F6: the untyped timing-lane bag (duration-only OR ease-only) measures the
147
+ // timing lane (the authored duration, or the generator's 300ms default), never the
148
+ // duration-spring's resolved settle.
149
+ authored.type === 'tween' || isTimingLaneTransition(authored));
150
+ if (lane === 'keyframes')
151
+ return toKeyframesConfig(authored).duration ?? null;
152
+ if (lane === 'timing')
153
+ return toTimingConfig(authored).duration ?? null;
154
+ const resolved = resolveSpring(toSpringConfig(authored));
155
+ // `duration || null`: a zero-length resolution is NOT a known duration — the scan measures it,
156
+ // and the fold's own refusal then names the degenerate iteration (T18-a L3).
157
+ return resolved.calculatedDuration !== null && resolved.calculatedDuration > 0
158
+ ? resolved.calculatedDuration
159
+ : null;
160
+ }
161
+ /**
162
+ * One unrepeated trajectory. `reversed` builds the MIRRORED leg (the pin's second generator): the
163
+ * origin and target swap and the seed velocity negates, and a keyframe list is reversed AFTER its
164
+ * nulls resolve — `times`/`ease` are deliberately untouched, exactly as the pin leaves them, so a
165
+ * mirrored leg is a genuinely different trajectory rather than a time-reflection.
166
+ */
167
+ function unrepeatedGenerator(transition, to, from, velocity, reversed) {
168
+ const { authored, restSpeed } = normalizeTransition(transition);
169
+ if (typeof to !== 'number') {
170
+ if (reversed && authored.type !== 'inertia') {
171
+ // Resolve from-current nulls BEFORE reversing (R8-F2): the pin mirrors the RESOLVED
172
+ // keyframes, and a null that slid to the tail would re-read `from` at the wrong end.
173
+ const resolvedKeyframes = to.map((value) => (value === null ? from : value));
174
+ const mirroredKeyframes = [...resolvedKeyframes].reverse();
175
+ const mirroredFrom = mirroredKeyframes[0] ?? from;
176
+ if (authored.type === 'spring') {
177
+ return springKeyframeGenerator(authored, restSpeed, mirroredKeyframes, mirroredFrom, -velocity);
178
+ }
179
+ return keyframesGenerator(mirroredKeyframes, toKeyframesConfig(authored))({ from: mirroredFrom, velocity: 0 });
180
+ }
181
+ }
182
+ if (typeof to !== 'number') {
183
+ if (authored.type === 'inertia') {
184
+ // R16 shape totality (review major 3, G-INV-8/-10): a keyframe array under an inertia
185
+ // transition is a command-shape defect — the worklet driver throws the identical class;
186
+ // never silently run inertia ignoring the array.
187
+ throw new Error('a keyframe array cannot run under an inertia transition (REQ-GESTURE-023, R16).');
188
+ }
189
+ // A keyframe ARRAY HONORS `transition.type` (REQ-API-033, R8-F1). An EXPLICIT spring interpolates
190
+ // exactly two keyframes (Motion's `assertTwoKeyframes`): two → spring physics; a >2 spring FAILS
191
+ // LOUD naming the successor — it never reaches `resolveSpringGenerator` to be silently accepted. A
192
+ // tween or the DEFAULT routes the array THROUGH the keyframes generator (even offsets, per-segment
193
+ // ease); position-based, the seed velocity is dropped and a null FIRST element reads `from` (R8-F2).
194
+ if (authored.type === 'spring')
195
+ return springKeyframeGenerator(authored, restSpeed, to, from, velocity);
196
+ return keyframesGenerator(to, toKeyframesConfig(authored))({ from, velocity: 0 });
197
+ }
198
+ if (authored.type === 'inertia') {
199
+ // R16 (REQ-GESTURE-023): the gesture free-drag release's two-phase generator. `to` is the
200
+ // mirror terminal (the clamped ideal); the generator recomputes its own ideal from the seed
201
+ // and owns the walls via the boundary spring. The seed velocity is the platform release
202
+ // velocity, momentum-gated upstream (REQ-GESTURE-027).
203
+ return inertiaGenerator({
204
+ power: authored.power,
205
+ timeConstant: authored.timeConstant,
206
+ bounceStiffness: authored.bounceStiffness,
207
+ bounceDamping: authored.bounceDamping,
208
+ restDelta: authored.restDelta,
209
+ restSpeed: authored.restSpeed,
210
+ ...(authored.min !== undefined ? { min: authored.min } : {}),
211
+ ...(authored.max !== undefined ? { max: authored.max } : {}),
212
+ })({ from, velocity });
213
+ }
214
+ // The scalar mirrored leg: origin and target swap, and the seed velocity negates (the pin builds
215
+ // its second generator with `velocity: -velocity`).
216
+ const scalarTo = reversed ? from : to;
217
+ const scalarFrom = reversed ? to : from;
218
+ const scalarVelocity = reversed ? -velocity : velocity;
219
+ // REQ-TIMING-006 + transition-default-selection F1/F6: an authored duration-ONLY bag (type
220
+ // omitted) OR a defined ease-only bag takes the pin's keyframes/timing generator — monotone,
221
+ // `easeOut` by default when NO ease spelling is authored, the authored ease verbatim for the
222
+ // ease-only form (the pin's defined-typeless lane at the 300ms default, JSAnimation.ts:106-116) —
223
+ // never the old omitted-type duration-spring (U7a finding A, device peak 1.046) and never the
224
+ // REQ-SPRING-003 generic spring for an ease-only bag. F1 gives the TYPED tween the same easeOut
225
+ // base through the ONE config builder (component/transition.ts `toTimingLaneConfig`);
226
+ // `isTimingLaneTransition` is the single-sourced untyped arm.
227
+ if (authored.type === 'tween' || isTimingLaneTransition(authored)) {
228
+ // F5: a zero-length timing base takes the pin's makeAnimationInstant SHAPE — hold the seed at
229
+ // t ≤ 0, commit the final keyframe past it (motion-value.ts:89-98). Only repeatDelay-bearing
230
+ // bags reach this (a zero duration without one routes to the instant lane above); it keeps
231
+ // the fold's zero-length plays from committing at the command edge.
232
+ if (authored.duration === 0) {
233
+ return instantGenerator(scalarTo)({ from: scalarFrom, velocity: 0 });
234
+ }
235
+ return timingGenerator(scalarTo, toTimingLaneConfig(authored))({ from: scalarFrom, velocity: 0 });
236
+ }
237
+ return resolveSpringGenerator(scalarTo, springConfigWithRest(authored, restSpeed))({ from: scalarFrom, velocity: scalarVelocity });
238
+ }
239
+ // R8-F1: a keyframe ARRAY under an EXPLICIT spring. A spring interpolates EXACTLY two keyframes — the
240
+ // pinned Motion throws `spring-two-frames` for more. Two keyframes run spring physics from keyframes[0]
241
+ // (the explicit origin; a null first element reads `from`, from-current R8-F2) to keyframes[1] (the
242
+ // target); a longer array FAILS LOUD naming the successor so it never reaches `resolveSpringGenerator`.
243
+ function springKeyframeGenerator(transition, restSpeed, keyframes, from, velocity) {
244
+ const countRefusal = springKeyframeCountRefusal(keyframes.length);
245
+ if (countRefusal !== null)
246
+ throw countRefusal;
247
+ const origin = keyframes[0];
248
+ const target = keyframes[1];
249
+ // Validation guarantees a length-2 array whose only legal null is index 0 and whose target parses;
250
+ // the guard keeps this TOTAL under noUncheckedIndexedAccess (never hit for a valid target).
251
+ if (origin === undefined || target === undefined || target === null) {
252
+ throw new Error(`malformed two-keyframe spring array — a non-null numeric target is required (REQ-API-033).`);
253
+ }
254
+ const seedFrom = origin === null ? from : origin;
255
+ return resolveSpringGenerator(target, springConfigWithRest(transition, restSpeed))({
256
+ from: seedFrom,
257
+ velocity,
258
+ });
259
+ }
260
+ // Build a generator that CONTINUES from a live property's current value+velocity to a new target (REQ-API-
261
+ // 003 interruption continuity). Springs use SPRING's own `retargetSpring` (carries the analytic velocity);
262
+ // tweens are position-based, so they reseed from the live value with no carried velocity.
263
+ function unrepeatedRetargetGenerator(transition, live, to) {
264
+ // Normalize first, exactly as `unrepeatedGenerator` does: the authored form is what the config
265
+ // converters accept, and a `type:'keyframes'` default normalizes to its equivalent tween rather
266
+ // than falling through to the spring branch. The disjoint inertia arm runs FIRST so the union
267
+ // narrows to `Transition` for the converter accept below (arms are mutually exclusive; the
268
+ // order swap changes nothing observable).
269
+ const { authored, restSpeed } = normalizeTransition(transition);
270
+ if (authored.type === 'inertia') {
271
+ // R16: a retarget INTO an inertia transition re-seeds the two-phase generator from the LIVE
272
+ // sample — the same continuity law as a spring retarget (C0/C1). The worklet driver has always
273
+ // done this; the reference driver used to fall through to the spring branch and hand an inertia
274
+ // transition to `toSpringConfig`. Pinned as a parity row in driverParity.differential.test.ts.
275
+ const at = live.generator.sample(live.elapsed);
276
+ return unrepeatedGenerator(transition, to, at.value, at.velocity, false);
277
+ }
278
+ if (authored.type === 'tween' || isTimingLaneTransition(authored)) {
279
+ // F5: the zero-length timing base takes the instant SHAPE here too — the same routing as
280
+ // `unrepeatedGenerator`, seeded from the live value (no cross-seam skew).
281
+ if (authored.duration === 0) {
282
+ return instantGenerator(to)({ from: live.value, velocity: 0 });
283
+ }
284
+ return timingGenerator(to, toTimingLaneConfig(authored))({ from: live.value, velocity: 0 });
285
+ }
286
+ return retargetSpring(live.generator, live.elapsed, to, springConfigWithRest(authored, restSpeed));
287
+ }
288
+ /**
289
+ * T18-a fix-up (review BLOCKER, two independent reviewers): a retarget carries the repeat fold,
290
+ * exactly as `start` does. For a repeating animation this seam is not an edge case, it is the
291
+ * COMMON one — an endless repeat is never `done`, so every changed scalar animate key routes here
292
+ * instead of to `start`. Dropping the fold stopped the loop permanently on the first retarget and
293
+ * then fired the completion the transition says can never come. The internal tell was that the
294
+ * gesture-HELD branch already folds (it builds through `startGenerator`), so the same authoring
295
+ * repeated or not depending on whether a finger happened to be down.
296
+ *
297
+ * The pin rebuilds every retarget as a fresh animation spreading `...valueTransition`
298
+ * (`motion-dom/src/animation/interfaces/motion-value.ts`), so repeat/repeatType/repeatDelay ride
299
+ * through unchanged. The fold's BASE here is the continuity-carrying retarget generator, so
300
+ * iteration 0 keeps the live value and analytic velocity (REQ-API-003) and each later iteration
301
+ * replays from that same seed.
302
+ */
303
+ function retargetGenerator(transition, live, to) {
304
+ const { authored } = normalizeTransition(transition);
305
+ // U7a + F5: the instant lane at the retarget seam — the pin's guard is type-independent
306
+ // (`type: false` OR `duration === 0 && !repeatDelay`, motion-value.ts:89-98). Commit the new
307
+ // target on the next update tick from the live value, honoring the retarget's OWN authored
308
+ // delay (the pin starts a fresh animation per retarget). No fold, exactly as `startGenerator`.
309
+ // The inertia narrow keeps the union discriminated for the delay crossing.
310
+ if (authored.type !== 'inertia' &&
311
+ (authored.type === false || isDurationZeroInstantTransition(authored))) {
312
+ return delayGenerator(instantGenerator(to)({ from: live.value, velocity: 0 }), toDelayMs(authored));
313
+ }
314
+ const base = unrepeatedRetargetGenerator(transition, live, to);
315
+ const fold = authored.type === 'inertia' ? null : toRepeatFoldOptions(authored);
316
+ // T18-b (REQ-DRIVER-032): a retarget applies its OWN delay, as the pin starts a fresh animation
317
+ // per retarget. The rebase wraps the (possibly folded) retarget trajectory — AROUND the fold,
318
+ // the same core seam as `startGenerator`. A mid-delay retarget reseeds from the held first
319
+ // keyframe at velocity 0, because the outgoing delay wrapper reports exactly that while held
320
+ // (packet L2: no motion to be continuous WITH).
321
+ const delayMs = authored.type === 'inertia' ? 0 : toDelayMs(authored);
322
+ if (fold === null)
323
+ return delayGenerator(base, delayMs);
324
+ // Legs eager, never a factory the fold would invoke — the worklet driver mirrors this shape
325
+ // exactly (`check:worklet-closures`). The mirrored leg swaps origin/target around the LIVE value.
326
+ const leg = fold.repeatType === 'mirror'
327
+ ? { type: 'mirror', mirrored: unrepeatedGenerator(transition, to, live.value, 0, true) }
328
+ : { type: fold.repeatType };
329
+ const folded = buildRepeatedGenerator(fold.repeat, leg, fold.repeatDelayMs, base, knownIterationDurationMs(transition, to));
330
+ if (folded instanceof Error)
331
+ throw repeatRefusal(fold, folded);
332
+ return delayGenerator(folded, delayMs);
333
+ }
334
+ // A CONTENDING target drifts FROM the committed finger value — never a backward jump off the finger
335
+ // (REQ-DRIVER-026 / G-INV-2, review major 25). A keyframe array's non-null FIRST element would make the
336
+ // generator restart near keyframes[0], rewinding the drift; re-seed the first keyframe from-current
337
+ // (null-first, R8-F2) so the trajectory runs the live value → remaining keyframes. A scalar (springs/
338
+ // tweens already seed from `from`) and an already-null-first array are unchanged. `to` is a captured
339
+ // snapshot, so this reads it once more into a fresh frozen array only at a command/gesture edge.
340
+ function anchorContentionToLive(to) {
341
+ if (typeof to === 'number' || to[0] === null)
342
+ return to;
343
+ return Object.freeze([null, ...to.slice(1)]);
344
+ }
345
+ /**
346
+ * This backend's OWN verdict on a fold, for one transition and one target, with the boundary
347
+ * validation `command()` runs first deliberately NOT in the path.
348
+ *
349
+ * It exists because the parity floor that guards this rung was structurally half-blind without it
350
+ * (fix-up review MAJOR): `prepareDriverCommand` calls `validateTransitionRefusal` before reaching
351
+ * any generator, so a floor that reads `command()`'s throw sees validation's own refusal echoed back
352
+ * as "the driver refused". Every over-refusal therefore passed — a mutation that made validation
353
+ * reject 36 executable configs left the floor green. Only a verdict taken from BELOW validation can
354
+ * decide whether validation is measuring the fold the way this backend does.
355
+ *
356
+ * Returns the fold's refusal, or null when this backend would construct and run the generator.
357
+ *
358
+ * `from` is a real parameter, not a convenience: a null-first keyframe array (`[null, 100]`, the
359
+ * from-current form) resolves its first keyframe FROM the live value, so with `from` pinned at 0 it
360
+ * is indistinguishable from `[0, 100]` and the target axis loses a shape it claims to sweep (round-5
361
+ * review MINOR).
362
+ *
363
+ * Test seam for this package's own parity floor — NOT part of any published entry: exporting it from
364
+ * `internal-driver` would pull this whole backend into the seam the worklet driver imports.
365
+ */
366
+ export function referenceFoldRefusal(transition, to, from = 0) {
367
+ try {
368
+ startGenerator(transition, to, from, 0);
369
+ return null;
370
+ }
371
+ catch (error) {
372
+ // Only the fold's own refusal answers this question. Anything else is a different fault and must
373
+ // not be laundered into a verdict — it propagates.
374
+ if (error instanceof InvalidTransitionError && error.key === 'repeat')
375
+ return error;
376
+ throw error;
377
+ }
378
+ }
379
+ /**
380
+ * The same verdict at the RETARGET seam — the second half of the parity floor's oracle.
381
+ *
382
+ * Rounds 4-6 each fixed one axis pinned to a constant (`repeat`, then `repeatDelay`), and round 7
383
+ * found the pattern one level up: the floor's ORACLE was itself pinned to a constant — the `start`
384
+ * seam of the reference backend — while `buildRepeatedGenerator` has four seams (two backends x two
385
+ * phases). The retarget seam is not a duplicate of `start`:
386
+ *
387
+ * - it reseeds the scan from the live VALUE, so the distance it measures is the distance from
388
+ * wherever the animation currently IS — a different trajectory than `start` measured, which is
389
+ * why it can refuse where `start` ran (round-7 review MAJOR). It also carries the live velocity
390
+ * into the reseed; that is structurally present but NOT the demonstrated variable — sweeping the
391
+ * dwell from 0 to 480ms at a fixed distance changes no verdict, and removing the dwell entirely
392
+ * leaves the floor green (round-8 review MINOR). Distance is what moves it;
393
+ * - it cannot reach the keyframes lane at all — `DriverCommand`'s retarget targets are numbers, so
394
+ * `targetIsNumeric` is constantly true here. The floor's target axis is structurally
395
+ * inexpressible at this seam, and its axis is the LIVE STATE instead.
396
+ *
397
+ * Taken from BELOW validation for the same reason as `referenceFoldRefusal`: `command()` validates
398
+ * first, so reading its throw would make the driver echo the boundary and the floor could only ever
399
+ * catch over-acceptance.
400
+ *
401
+ * A fold that cannot even START is reported as refused — "would this backend run it at a retarget"
402
+ * is answered `no` either way, and conflating them here would make the phase axis lie about which
403
+ * seam refused.
404
+ *
405
+ * Test seam for this package's own parity floor — NOT part of any published entry.
406
+ */
407
+ export function referenceRetargetFoldRefusal(transition, startTo, from, retargetTo, dwellMs) {
408
+ let generator;
409
+ try {
410
+ generator = startGenerator(transition, startTo, from, 0);
411
+ }
412
+ catch (error) {
413
+ if (error instanceof InvalidTransitionError && error.key === 'repeat')
414
+ return error;
415
+ throw error;
416
+ }
417
+ // Reach a genuinely live state the way the driver does, so the retarget carries a real velocity.
418
+ const live = { generator, elapsed: 0, value: from, done: false };
419
+ stepProp(live, dwellMs);
420
+ try {
421
+ retargetGenerator(transition, live, retargetTo);
422
+ return null;
423
+ }
424
+ catch (error) {
425
+ if (error instanceof InvalidTransitionError && error.key === 'repeat')
426
+ return error;
427
+ throw error;
428
+ }
429
+ }
430
+ export function createReferenceDriver() {
431
+ // Indexed by handle (a plain number, worklet-friendly). register() appends; the index is the handle.
432
+ const elements = [];
433
+ function elementAt(handle) {
434
+ const element = elements[handle];
435
+ if (element === undefined) {
436
+ throw new Error(`unknown element handle ${String(handle)}: it was never registered on this driver.`);
437
+ }
438
+ return element;
439
+ }
440
+ const backend = {
441
+ register(initial) {
442
+ const handle = elements.length;
443
+ elements.push({
444
+ active: false,
445
+ heldCount: 0,
446
+ committed: { ...initial },
447
+ heldVelocity: {},
448
+ props: new Map(),
449
+ contending: {},
450
+ path: null,
451
+ });
452
+ return handle;
453
+ },
454
+ command(handle, command) {
455
+ const element = elementAt(handle);
456
+ switch (command.kind) {
457
+ case 'start': {
458
+ // Only a gesture RELEASE (`releasesGesture`) or a `stop` ends a hold. A declarative start on
459
+ // a gesture-held prop CONTENDS instead of being skipped (REQ-DRIVER-026, Motion's shared-
460
+ // value model): it seeds a generator that drifts the live value while the finger still owns
461
+ // the prop, so a mid-drag `animate` change moves the element and the release reads the
462
+ // drifted origin (the snap-selection parity fix). The finger's release velocity is preserved
463
+ // (liveFor held-velocity precedence, REQ-DRIVER-025); the hold clears only at release/stop.
464
+ const releasesGesture = command.releasesGesture;
465
+ // PHASE 1 — build every prop's generator WITHOUT mutating element state. A construction that
466
+ // throws (an explicit spring on a >2-keyframe array, R8-F1) aborts the WHOLE command here, so
467
+ // a rejected multi-prop start applies NOTHING (fail-closed atomicity, review major 19).
468
+ const plans = [];
469
+ for (const key of Object.keys(command.targets)) {
470
+ const target = command.targets[key]; // key came from Object.keys
471
+ // A gesture-RELEASE start takes over the prop (its hold clears in phase 2), so it is treated
472
+ // as NOT still-held and uses the command's nominal `from`. A start CONTENDING on a prop the
473
+ // finger still holds drifts from the CURRENT live value (never `from`) so committed does not
474
+ // jump off the finger. An ordinary non-held start uses `from`.
475
+ const stillHeld = !releasesGesture && element.heldVelocity[key] !== undefined;
476
+ const from = stillHeld ? element.committed[key] : target.from;
477
+ // `target` is already the complete immutable REQ-DRIVER-030 snapshot. No caller-owned
478
+ // PropTarget or fallback semantics reach this backend.
479
+ const to = target.to;
480
+ const transition = target.transition;
481
+ // A still-held (contending) start drifts from the finger value: seed the first keyframe
482
+ // from-current so it never rewinds to keyframes[0] (major 25). A non-held start keeps the
483
+ // array's own origin (a plain keyframe start plays [k0 … kN] verbatim).
484
+ const generator = startGenerator(transition, stillHeld ? anchorContentionToLive(to) : to, from, target.velocity);
485
+ // Seed committed/value from the generator's TICK-0, not the raw `from`: a keyframe array's
486
+ // origin is keyframes[0] (or the null-first seed), so committed() matches liveFor()'s
487
+ // zero-sample and never shows a one-frame `from` jump (review major dfeaa4bf4bcf). A scalar
488
+ // generator samples `from` at t=0 (no-op). A still-held start keeps the finger-authoritative
489
+ // committed. sample(0) is a pure read — no mutation, so phase 1 stays side-effect-free.
490
+ const seededValue = stillHeld ? from : generator.sample(0).value;
491
+ plans.push({ key, stillHeld, seededValue, generator, to, transition });
492
+ }
493
+ // PHASE 2 — commit every plan. No throw path here, so element state mutates atomically.
494
+ for (const plan of plans) {
495
+ if (releasesGesture)
496
+ releaseHold(element, plan.key);
497
+ element.props.set(plan.key, {
498
+ generator: plan.generator,
499
+ elapsed: 0,
500
+ value: plan.seededValue,
501
+ done: false,
502
+ });
503
+ element.committed[plan.key] = plan.seededValue;
504
+ // Track the contention target so a later finger write re-anchors this generator (major 29).
505
+ // `plan.to` is ALREADY the phase-1 snapshot the generator was built from (captured once,
506
+ // review major 20 + 21), so this stores the SAME frozen array — no second read, no skew; a
507
+ // released/non-held start owns the prop outright, so clear any prior contention.
508
+ if (plan.stillHeld)
509
+ element.contending[plan.key] = {
510
+ to: plan.to,
511
+ transition: plan.transition,
512
+ };
513
+ else
514
+ delete element.contending[plan.key];
515
+ }
516
+ if (command.path !== undefined) {
517
+ const geometry = command.path;
518
+ const generator = startGenerator(geometry.transition, 1000, 0, 0);
519
+ const seeded = generator.sample(0).value;
520
+ element.path = {
521
+ progress: { generator, elapsed: 0, value: seeded, done: false },
522
+ geometry,
523
+ };
524
+ element.props.delete('x');
525
+ element.props.delete('y');
526
+ element.props.delete('pathRotation');
527
+ const sample = applyPathProgress(seeded, geometry, false);
528
+ if ('x' in element.committed)
529
+ element.committed.x = sample.x;
530
+ if ('y' in element.committed)
531
+ element.committed.y = sample.y;
532
+ if (geometry.rotationScale && 'pathRotation' in element.committed)
533
+ element.committed.pathRotation = sample.pathRotation;
534
+ }
535
+ else if (element.path !== null) {
536
+ if (element.path.geometry.rotationScale && 'pathRotation' in element.committed)
537
+ element.committed.pathRotation = 0;
538
+ element.path = null;
539
+ }
540
+ element.active = element.props.size > 0 || element.heldCount > 0 || element.path !== null;
541
+ break;
542
+ }
543
+ case 'retarget': {
544
+ // BUILD every key first, COMMIT only once all of them succeeded — the same atomicity law
545
+ // `start` already carries (review major 36). Building can REFUSE (the fold re-measures from
546
+ // the live value, so a `repeat` executable at start can be refused here), and a per-key
547
+ // commit left the earlier keys already retargeted when a later one threw. A refused command
548
+ // must mutate nothing; the controller's severity lane depends on it, because it reports the
549
+ // refusal and lets the remaining keys proceed.
550
+ const staged = [];
551
+ for (const key of Object.keys(command.targets)) {
552
+ const to = command.targets[key]; // key came from Object.keys
553
+ const transition = command.targetTransitions?.[key] ?? command.transition;
554
+ // A retarget is NEVER the gesture release (the release is a seeded `start`). A retarget on
555
+ // a gesture-HELD prop CONTENDS (REQ-DRIVER-026): it seeds a fresh generator from the
556
+ // finger-authoritative COMMITTED value (not the possibly-stale generator `live.value` — a
557
+ // finger write may have advanced committed past it, review r10 major 29) toward the new
558
+ // target, and records the contention so a later write re-anchors it. A non-held prop
559
+ // retargets normally with velocity continuity.
560
+ const held = element.heldVelocity[key] !== undefined;
561
+ let from;
562
+ let generator;
563
+ if (held) {
564
+ from = element.committed[key]; // held ⇒ written ⇒ committed exists
565
+ generator = startGenerator(transition, to, from, 0);
566
+ staged.push({ key, from, generator, contendTo: to });
567
+ continue;
568
+ }
569
+ else {
570
+ // Retarget carries no base — it derives `from` from the in-flight value (continuity), or
571
+ // for a registered-but-idle prop, its last committed value. A key that is neither
572
+ // in-flight nor committed is a malformed retarget: fail loud, never silently seed 0.
573
+ const live = element.props.get(key);
574
+ if (live !== undefined) {
575
+ from = live.value;
576
+ generator = retargetGenerator(transition, live, to);
577
+ }
578
+ else {
579
+ const base = element.committed[key];
580
+ if (base === undefined) {
581
+ throw new Error(`cannot retarget '${key}': it was never registered or started on this element.`);
582
+ }
583
+ from = base;
584
+ generator = startGenerator(transition, to, from, 0);
585
+ }
586
+ }
587
+ staged.push({ key, from, generator });
588
+ }
589
+ // Past this line nothing can refuse, so the commit is all-or-nothing.
590
+ if (element.path !== null &&
591
+ staged.some(({ key }) => key === 'x' || key === 'y' || key === 'pathRotation')) {
592
+ if (element.path.geometry.rotationScale && 'pathRotation' in element.committed)
593
+ element.committed.pathRotation = 0;
594
+ element.path = null;
595
+ }
596
+ for (const { key, from, generator, contendTo } of staged) {
597
+ if (contendTo === undefined)
598
+ delete element.contending[key];
599
+ else
600
+ element.contending[key] = {
601
+ to: contendTo,
602
+ transition: command.targetTransitions?.[key] ?? command.transition,
603
+ };
604
+ element.props.set(key, { generator, elapsed: 0, value: from, done: false });
605
+ element.committed[key] = from;
606
+ }
607
+ // Recompute from what remains in flight: a held retarget seeds a contending generator
608
+ // (props.size grows) and a held element is active regardless (heldCount > 0).
609
+ element.active = element.props.size > 0 || element.heldCount > 0 || element.path !== null;
610
+ break;
611
+ }
612
+ case 'stop': {
613
+ // Halt every animation AND release every gesture hold (stop = halt everything); the
614
+ // committed values are held as-is (REQ-DRIVER-013). Pin onStop zeroes pathRotation.
615
+ element.props.clear();
616
+ for (const key of Object.keys(element.heldVelocity))
617
+ delete element.heldVelocity[key];
618
+ for (const key of Object.keys(element.contending))
619
+ delete element.contending[key];
620
+ if (element.path !== null) {
621
+ if (element.path.geometry.rotationScale && 'pathRotation' in element.committed)
622
+ element.committed.pathRotation = 0;
623
+ element.path = null;
624
+ }
625
+ element.heldCount = 0;
626
+ element.active = false;
627
+ break;
628
+ }
629
+ }
630
+ },
631
+ step(elapsedMs) {
632
+ for (const element of elements) {
633
+ if (!element.active)
634
+ continue;
635
+ let anyActive = false;
636
+ for (const [key, prop] of element.props) {
637
+ stepProp(prop, elapsedMs);
638
+ element.committed[key] = prop.value;
639
+ if (!prop.done)
640
+ anyActive = true;
641
+ }
642
+ if (element.path !== null) {
643
+ const path = element.path;
644
+ stepProp(path.progress, elapsedMs);
645
+ const sample = applyPathProgress(path.progress.value, path.geometry, path.progress.done);
646
+ if ('x' in element.committed)
647
+ element.committed.x = sample.x;
648
+ if ('y' in element.committed)
649
+ element.committed.y = sample.y;
650
+ if (path.geometry.rotationScale && 'pathRotation' in element.committed)
651
+ element.committed.pathRotation = sample.pathRotation;
652
+ if (path.progress.done)
653
+ element.path = null;
654
+ else
655
+ anyActive = true;
656
+ }
657
+ // Quiesce once every animation settles (REQ-DRIVER-020) — UNLESS a gesture still holds a
658
+ // prop (a finger is down, no animation but unsettled, G-INV-7). heldCount is O(1), so this
659
+ // stays allocation-free. A held element stays active until the matching release clears it.
660
+ if (!anyActive && element.heldCount === 0)
661
+ element.active = false;
662
+ }
663
+ },
664
+ committed(handle) {
665
+ return { ...elementAt(handle).committed };
666
+ },
667
+ setActive(handle, active) {
668
+ elementAt(handle).active = active;
669
+ },
670
+ isActive(handle) {
671
+ return elementAt(handle).active;
672
+ },
673
+ // The gesture write lane (REQ-DRIVER-024): commit event-driven values and interrupt any
674
+ // in-flight animation on the written props through ONE entry point — committed state and
675
+ // animation state can never skew. Never emits a settle edge for a prop that was not
676
+ // animating (the active flag recomputes from what actually remains in flight).
677
+ write(handle, values, velocities) {
678
+ const element = elementAt(handle);
679
+ for (const key of Object.keys(values)) {
680
+ if (!(key in element.committed)) {
681
+ throw new Error(`cannot write '${key}': it was never registered on this element.`);
682
+ }
683
+ }
684
+ // Validate ALL keys before mutating ANY (fail-closed: a malformed write never half-applies).
685
+ for (const key of Object.keys(values)) {
686
+ const finger = values[key];
687
+ if (element.heldVelocity[key] === undefined)
688
+ element.heldCount++; // count a NEW hold once
689
+ element.committed[key] = finger;
690
+ // Refresh the per-prop held platform velocity so liveFor reports it while held (G-INV-3 C1
691
+ // seam, held-velocity precedence over any contending generator); omitted ⇒ 0.
692
+ element.heldVelocity[key] = velocities?.[key] ?? 0;
693
+ // Reconcile the prop's generator with the finger write (REQ-DRIVER-026). A LIVE declarative
694
+ // contention is RE-ANCHORED to the freshly-committed finger value so it drifts FROM here —
695
+ // never a backward jump to the generator's stale trajectory (review r10 major 29, G-INV-2).
696
+ // Otherwise the finger owns the value outright: a plain grab interrupts the pre-existing
697
+ // animation (REQ-GESTURE-012), and a SETTLED contention is finished, so its generator is
698
+ // dropped (else `step` would keep forcing its value and the finger could never move it).
699
+ const contention = element.contending[key];
700
+ const prop = element.props.get(key);
701
+ if (contention !== undefined && prop !== undefined && !prop.done) {
702
+ element.props.set(key, {
703
+ // Re-anchor FROM the finger: seed a keyframe array's first element from-current so the
704
+ // drift continues from `finger`, never rewinding to keyframes[0] (major 25).
705
+ generator: startGenerator(contention.transition, anchorContentionToLive(contention.to), finger, 0),
706
+ elapsed: 0,
707
+ value: finger,
708
+ done: false,
709
+ });
710
+ }
711
+ else {
712
+ if (prop !== undefined)
713
+ element.props.delete(key);
714
+ delete element.contending[key];
715
+ }
716
+ }
717
+ // A gesture write holds the element unsettled (G-INV-7): a finger is down driving it, so it
718
+ // stays active until the matching release/stop clears the hold — never a spurious settle edge
719
+ // mid-drag (the reviewer-reproduced re-grab defect), even after a contending animation settles.
720
+ element.active = true;
721
+ },
722
+ // The grab live-read (REQ-DRIVER-025). An in-flight prop re-samples its generator at the
723
+ // current elapsed — pure by elapsed time (REQ-CORE-001), scalars copied out immediately (the
724
+ // sample record is generator-owned and reused). Settled/never-animated ⇒ velocity 0.
725
+ liveFor(handle, key) {
726
+ const element = elementAt(handle);
727
+ // HELD-VELOCITY PRECEDENCE (REQ-DRIVER-025): a held (finger-down) prop reports the platform
728
+ // finger velocity its write carried EVEN when a contending declarative generator is in flight
729
+ // (REQ-DRIVER-026) — the release seed is the finger's number, never the generator's. The value
730
+ // is the live committed number (the finger position, or the contending animation's drift). This
731
+ // read comes BEFORE the generator branch so contention can't leak the generator velocity into
732
+ // the release seam (the two-sided G-INV-3 C1 seam; per-prop since review r7 major 23).
733
+ const heldV = element.heldVelocity[key];
734
+ if (heldV !== undefined) {
735
+ const base = element.committed[key];
736
+ if (base === undefined) {
737
+ throw new Error(`cannot read '${key}': it was never registered on this element.`);
738
+ }
739
+ return { value: base, velocity: heldV };
740
+ }
741
+ const live = element.props.get(key);
742
+ if (live !== undefined && !live.done) {
743
+ const sample = live.generator.sample(live.elapsed);
744
+ return { value: sample.value, velocity: sample.velocity };
745
+ }
746
+ const base = element.committed[key];
747
+ if (base === undefined) {
748
+ throw new Error(`cannot read '${key}': it was never registered on this element.`);
749
+ }
750
+ // A settled or never-animated non-held prop has velocity 0 (REQ-DRIVER-025).
751
+ return { value: base, velocity: 0 };
752
+ },
753
+ };
754
+ return adaptPreparedDriver(backend, (handle) => {
755
+ elementAt(handle);
756
+ });
757
+ }