@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,19 @@
1
+ "use strict";
2
+ // Public surface of the SPEC-PRESENCE subsystem: the host-agnostic exit-lifecycle controller `core` defines
3
+ // and the native/web presence surfaces bind to. Host-agnostic (SPEC-PRESENCE §1) — imports no react, no
4
+ // driver, no host package. The lifecycle types + total state machine land first (Milestone 1); the
5
+ // controller, settle integration, and cancellation join this surface as they land.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createPresenceController = exports.UnimplementedPresenceModeError = exports.transition = exports.createPresenceLedger = void 0;
8
+ // The total exit-lifecycle state machine (SPEC-PRESENCE §3): the pure transition + the key→state ledger the
9
+ // controller owns.
10
+ var machine_1 = require("./machine.cjs");
11
+ Object.defineProperty(exports, "createPresenceLedger", { enumerable: true, get: function () { return machine_1.createPresenceLedger; } });
12
+ Object.defineProperty(exports, "transition", { enumerable: true, get: function () { return machine_1.transition; } });
13
+ // The presence controller (REQ-PRESENCE-001/011): mount authority + exit-target resolution + retention,
14
+ // driven off the MotionGraph settle ledger. The React usePresence/useIsPresent hooks bind over it (M2 host).
15
+ // The legacy typed error remains exported for public-surface compatibility; popLayout itself now
16
+ // uses core's ordinary lifecycle ordering and is composed by the host presence layers.
17
+ var controller_1 = require("./controller.cjs");
18
+ Object.defineProperty(exports, "UnimplementedPresenceModeError", { enumerable: true, get: function () { return controller_1.UnimplementedPresenceModeError; } });
19
+ Object.defineProperty(exports, "createPresenceController", { enumerable: true, get: function () { return controller_1.createPresenceController; } });
@@ -0,0 +1,4 @@
1
+ export type { PresenceChild, PresenceController, PresenceControllerOptions, PresenceMode, PresenceState, } from "./types.cjs";
2
+ export { createPresenceLedger, transition } from "./machine.cjs";
3
+ export type { PresenceEvent, PresenceLedger } from "./machine.cjs";
4
+ export { UnimplementedPresenceModeError, createPresenceController } from "./controller.cjs";
@@ -0,0 +1,4 @@
1
+ export type { PresenceChild, PresenceController, PresenceControllerOptions, PresenceMode, PresenceState, } from "./types.js";
2
+ export { createPresenceLedger, transition } from "./machine.js";
3
+ export type { PresenceEvent, PresenceLedger } from "./machine.js";
4
+ export { UnimplementedPresenceModeError, createPresenceController } from "./controller.js";
@@ -0,0 +1,12 @@
1
+ // Public surface of the SPEC-PRESENCE subsystem: the host-agnostic exit-lifecycle controller `core` defines
2
+ // and the native/web presence surfaces bind to. Host-agnostic (SPEC-PRESENCE §1) — imports no react, no
3
+ // driver, no host package. The lifecycle types + total state machine land first (Milestone 1); the
4
+ // controller, settle integration, and cancellation join this surface as they land.
5
+ // The total exit-lifecycle state machine (SPEC-PRESENCE §3): the pure transition + the key→state ledger the
6
+ // controller owns.
7
+ export { createPresenceLedger, transition } from "./machine.js";
8
+ // The presence controller (REQ-PRESENCE-001/011): mount authority + exit-target resolution + retention,
9
+ // driven off the MotionGraph settle ledger. The React usePresence/useIsPresent hooks bind over it (M2 host).
10
+ // The legacy typed error remains exported for public-surface compatibility; popLayout itself now
11
+ // uses core's ordinary lifecycle ordering and is composed by the host presence layers.
12
+ export { UnimplementedPresenceModeError, createPresenceController } from "./controller.js";
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ // SPEC-PRESENCE §3 — the total exit-lifecycle state machine. A presence-tracked key is always in exactly one
3
+ // of `present | exiting | removed`, and only four edges move it. Every other edge throws (fail-loud): an
4
+ // illegal transition is a controller bug, never a silent no-op or a state teleport. Host-agnostic
5
+ // (REQ-CORE-003): relative imports only, no clock/host reads.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.transition = transition;
8
+ exports.createPresenceLedger = createPresenceLedger;
9
+ const EDGES = {
10
+ 'drop-with-exit': { from: 'present', to: 'exiting' },
11
+ 'drop-immediate': { from: 'present', to: 'removed' },
12
+ resolve: { from: 'exiting', to: 'removed' },
13
+ cancel: { from: 'exiting', to: 'present' },
14
+ };
15
+ // Pure total transition: map `(current, event)` to the next state, or throw on an illegal edge. Deterministic.
16
+ function transition(current, event) {
17
+ const edge = EDGES[event];
18
+ if (current !== edge.from) {
19
+ throw new Error(`illegal presence transition '${event}' from '${current}': only legal from '${edge.from}'.`);
20
+ }
21
+ return edge.to;
22
+ }
23
+ function createPresenceLedger() {
24
+ // Holds only non-terminal states; absence of a key IS the terminal `removed` state.
25
+ const states = new Map();
26
+ return {
27
+ track(key) {
28
+ if (states.has(key))
29
+ throw new Error(`presence key '${key}' is already tracked`);
30
+ states.set(key, 'present');
31
+ },
32
+ stateOf(key) {
33
+ return states.get(key) ?? 'removed';
34
+ },
35
+ send(key, event) {
36
+ const current = states.get(key) ?? 'removed';
37
+ const next = transition(current, event);
38
+ if (next === 'removed') {
39
+ states.delete(key);
40
+ }
41
+ else {
42
+ states.set(key, next);
43
+ }
44
+ return next;
45
+ },
46
+ trackedKeys() {
47
+ return [...states.keys()];
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,10 @@
1
+ import type { PresenceState } from "./types.cjs";
2
+ export type PresenceEvent = 'drop-with-exit' | 'drop-immediate' | 'resolve' | 'cancel';
3
+ export declare function transition(current: PresenceState, event: PresenceEvent): PresenceState;
4
+ export interface PresenceLedger {
5
+ track(key: string): void;
6
+ stateOf(key: string): PresenceState;
7
+ send(key: string, event: PresenceEvent): PresenceState;
8
+ trackedKeys(): readonly string[];
9
+ }
10
+ export declare function createPresenceLedger(): PresenceLedger;
@@ -0,0 +1,10 @@
1
+ import type { PresenceState } from "./types.js";
2
+ export type PresenceEvent = 'drop-with-exit' | 'drop-immediate' | 'resolve' | 'cancel';
3
+ export declare function transition(current: PresenceState, event: PresenceEvent): PresenceState;
4
+ export interface PresenceLedger {
5
+ track(key: string): void;
6
+ stateOf(key: string): PresenceState;
7
+ send(key: string, event: PresenceEvent): PresenceState;
8
+ trackedKeys(): readonly string[];
9
+ }
10
+ export declare function createPresenceLedger(): PresenceLedger;
@@ -0,0 +1,46 @@
1
+ // SPEC-PRESENCE §3 — the total exit-lifecycle state machine. A presence-tracked key is always in exactly one
2
+ // of `present | exiting | removed`, and only four edges move it. Every other edge throws (fail-loud): an
3
+ // illegal transition is a controller bug, never a silent no-op or a state teleport. Host-agnostic
4
+ // (REQ-CORE-003): relative imports only, no clock/host reads.
5
+ const EDGES = {
6
+ 'drop-with-exit': { from: 'present', to: 'exiting' },
7
+ 'drop-immediate': { from: 'present', to: 'removed' },
8
+ resolve: { from: 'exiting', to: 'removed' },
9
+ cancel: { from: 'exiting', to: 'present' },
10
+ };
11
+ // Pure total transition: map `(current, event)` to the next state, or throw on an illegal edge. Deterministic.
12
+ export function transition(current, event) {
13
+ const edge = EDGES[event];
14
+ if (current !== edge.from) {
15
+ throw new Error(`illegal presence transition '${event}' from '${current}': only legal from '${edge.from}'.`);
16
+ }
17
+ return edge.to;
18
+ }
19
+ export function createPresenceLedger() {
20
+ // Holds only non-terminal states; absence of a key IS the terminal `removed` state.
21
+ const states = new Map();
22
+ return {
23
+ track(key) {
24
+ if (states.has(key))
25
+ throw new Error(`presence key '${key}' is already tracked`);
26
+ states.set(key, 'present');
27
+ },
28
+ stateOf(key) {
29
+ return states.get(key) ?? 'removed';
30
+ },
31
+ send(key, event) {
32
+ const current = states.get(key) ?? 'removed';
33
+ const next = transition(current, event);
34
+ if (next === 'removed') {
35
+ states.delete(key);
36
+ }
37
+ else {
38
+ states.set(key, next);
39
+ }
40
+ return next;
41
+ },
42
+ trackedKeys() {
43
+ return [...states.keys()];
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ // SPEC-PRESENCE §2 — the presence/exit lifecycle type contract. Host-agnostic (REQ-CORE-003): the controller
3
+ // lives in `core`, drives exits over the injected MotionGraph + clock, and never imports a driver, React, or
4
+ // the DOM (SPEC-PRESENCE §1). The React `usePresence`/`useIsPresent` hooks are Milestone-2 host bindings over
5
+ // the `isPresent`/`safeToRemove` semantics declared here.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,32 @@
1
+ import type { ResolvedTargetValue, ResolvedValue, Target, Transition } from "../component/index.cjs";
2
+ import type { MotionGraph } from "../types.cjs";
3
+ export type PresenceState = 'present' | 'exiting' | 'removed';
4
+ export type PresenceMode = 'sync' | 'wait' | 'popLayout';
5
+ export type PresenceLiveValues = Readonly<Record<string, ResolvedValue>>;
6
+ export type PresenceResolvedExits = Readonly<Record<string, readonly (number | null)[]>>;
7
+ export interface PresenceChild {
8
+ readonly key: string;
9
+ readonly exit?: Target;
10
+ readonly animate?: Target;
11
+ readonly transition?: Transition;
12
+ readonly deferred?: boolean;
13
+ readonly mountedKeys?: readonly string[];
14
+ }
15
+ export interface PresenceControllerOptions {
16
+ readonly graph: MotionGraph;
17
+ readonly mode?: PresenceMode;
18
+ readonly initial?: boolean;
19
+ readonly onExitComplete?: () => void;
20
+ }
21
+ export interface PresenceController {
22
+ syncChildren(children: readonly PresenceChild[], liveValuesByKey?: ReadonlyMap<string, PresenceLiveValues>, resolvedExitsByKey?: ReadonlyMap<string, PresenceResolvedExits>): void;
23
+ mountedKeys(): readonly string[];
24
+ stateOf(key: string): PresenceState;
25
+ isPresent(key: string): boolean;
26
+ safeToRemove(key: string): void;
27
+ deferExit(key: string): void;
28
+ resolvedExitTarget(key: string): Readonly<Record<string, ResolvedTargetValue>> | undefined;
29
+ enterSuppressed(key: string): boolean;
30
+ exitProgress(key: string): number | undefined;
31
+ exitVelocity(key: string): number | undefined;
32
+ }
@@ -0,0 +1,32 @@
1
+ import type { ResolvedTargetValue, ResolvedValue, Target, Transition } from "../component/index.js";
2
+ import type { MotionGraph } from "../types.js";
3
+ export type PresenceState = 'present' | 'exiting' | 'removed';
4
+ export type PresenceMode = 'sync' | 'wait' | 'popLayout';
5
+ export type PresenceLiveValues = Readonly<Record<string, ResolvedValue>>;
6
+ export type PresenceResolvedExits = Readonly<Record<string, readonly (number | null)[]>>;
7
+ export interface PresenceChild {
8
+ readonly key: string;
9
+ readonly exit?: Target;
10
+ readonly animate?: Target;
11
+ readonly transition?: Transition;
12
+ readonly deferred?: boolean;
13
+ readonly mountedKeys?: readonly string[];
14
+ }
15
+ export interface PresenceControllerOptions {
16
+ readonly graph: MotionGraph;
17
+ readonly mode?: PresenceMode;
18
+ readonly initial?: boolean;
19
+ readonly onExitComplete?: () => void;
20
+ }
21
+ export interface PresenceController {
22
+ syncChildren(children: readonly PresenceChild[], liveValuesByKey?: ReadonlyMap<string, PresenceLiveValues>, resolvedExitsByKey?: ReadonlyMap<string, PresenceResolvedExits>): void;
23
+ mountedKeys(): readonly string[];
24
+ stateOf(key: string): PresenceState;
25
+ isPresent(key: string): boolean;
26
+ safeToRemove(key: string): void;
27
+ deferExit(key: string): void;
28
+ resolvedExitTarget(key: string): Readonly<Record<string, ResolvedTargetValue>> | undefined;
29
+ enterSuppressed(key: string): boolean;
30
+ exitProgress(key: string): number | undefined;
31
+ exitVelocity(key: string): number | undefined;
32
+ }
@@ -0,0 +1,5 @@
1
+ // SPEC-PRESENCE §2 — the presence/exit lifecycle type contract. Host-agnostic (REQ-CORE-003): the controller
2
+ // lives in `core`, drives exits over the injected MotionGraph + clock, and never imports a driver, React, or
3
+ // the DOM (SPEC-PRESENCE §1). The React `usePresence`/`useIsPresent` hooks are Milestone-2 host bindings over
4
+ // the `isPresent`/`safeToRemove` semantics declared here.
5
+ export {};
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+ // @frame-path — executes inside the UI-runtime frame step; allocation-gated (REQ-DRIVER-021).
3
+ // T18-a / REQ-TIMING-003 — the repeat elapsed-time FOLD. `repeat`/`repeatType`/`repeatDelay` are not
4
+ // generator features in the pinned Motion: they are a pure `t → t'` map wrapped AROUND a generator
5
+ // (motion-dom@12.42.2 `JSAnimation.initAnimation` + `tick`). This module is that map, so every
6
+ // generator core owns — spring, tween, keyframe array — repeats identically and none of them learns
7
+ // about repetition. Host-agnostic (REQ-CORE-003): relative imports only. Numerics are pinned by
8
+ // per-sample goldens in repeat.test.ts.
9
+ //
10
+ // The fold divides by the RESOLVED iteration length (play + repeatDelay), so it is TOTAL only
11
+ // when that is finite and positive: a zero-length play with no repeatDelay produces NaN at the
12
+ // pin and is refused upstream (`repeatIterationRefusal`), as is an unbounded one. Zero-length
13
+ // plays separated by a repeatDelay are the pin's executable shape (transition-default-selection
14
+ // F5) and fold fine.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.REPEAT_REFUSAL_KEY = exports.REPEAT_REFUSAL_NAME = exports.MAX_ITERATION_DURATION_MS = exports.ITERATION_SCAN_STEP_MS = void 0;
17
+ exports.calcIterationDurationMs = calcIterationDurationMs;
18
+ exports.repeatIterationRefusal = repeatIterationRefusal;
19
+ exports.iterationMeasurementLane = iterationMeasurementLane;
20
+ exports.isRepeatFoldRefusalIdentity = isRepeatFoldRefusalIdentity;
21
+ exports.isRepeatFoldRefusal = isRepeatFoldRefusal;
22
+ exports.repeatRefusalMessage = repeatRefusalMessage;
23
+ exports.repeatFoldGeometry = repeatFoldGeometry;
24
+ exports.repeatGenerator = repeatGenerator;
25
+ exports.buildRepeatedGenerator = buildRepeatedGenerator;
26
+ // The pin's `calcGeneratorDuration` scan constants (motion-dom keyframes/calc-duration.ts). The
27
+ // 50ms grid is why a physics spring's iteration length is COARSE — it is the pin's number, not an
28
+ // analytic settle time, and reproducing it exactly is what keeps repeated springs in parity.
29
+ exports.ITERATION_SCAN_STEP_MS = 50;
30
+ exports.MAX_ITERATION_DURATION_MS = 20_000;
31
+ /**
32
+ * The iteration length of a generator that does not know its own duration (a physics or
33
+ * visualDuration spring — the pin's `calculatedDuration === null` case). Samples on the pin's 50ms
34
+ * grid until the trajectory reports done, and reports `Infinity` at the cap rather than scanning
35
+ * forever. Safe to run against a live generator: core's generators are pure functions of elapsed
36
+ * time (they latch no state, only reuse their sample record).
37
+ */
38
+ // alloc-ok: lifecycle-edge — the scan runs once per animate command, never per frame.
39
+ function calcIterationDurationMs(generator) {
40
+ let duration = 0;
41
+ let state = generator.sample(duration);
42
+ while (!state.done && duration < exports.MAX_ITERATION_DURATION_MS) {
43
+ duration += exports.ITERATION_SCAN_STEP_MS;
44
+ state = generator.sample(duration);
45
+ }
46
+ return duration >= exports.MAX_ITERATION_DURATION_MS ? Infinity : duration;
47
+ }
48
+ /**
49
+ * Refuse a fold the pin cannot execute (packet L3). The fold divides by the RESOLVED iteration
50
+ * length (play + repeatDelay), so a zero-length play with NOTHING separating it yields NaN at the
51
+ * pin and is refused here. A zero-length play separated by a `repeatDelay` is a different shape:
52
+ * the pin totals the delay gaps around the zero-length plays (`JSAnimation.initAnimation` —
53
+ * `totalDuration = calculatedDuration * (repeat + 1) + repeatDelay * repeat`), so the fold
54
+ * EXECUTES, holding each play's final keyframe through the gaps (transition-default-selection
55
+ * F5). An unbounded iteration still emits NaN for the rest of the animation and is refused either
56
+ * way. The refusal is CROSS-FIELD — either length is legal without `repeat`, where nothing
57
+ * divides. Returns the loud refusal or null (the `springKeyframeCountRefusal` shape: this module
58
+ * crosses to the UI runtime, where a throw is a bare `std::terminate`).
59
+ */
60
+ // alloc-ok: lifecycle-edge — a refusal built at command/validation time, never per frame.
61
+ function repeatIterationRefusal(iterationDurationMs, repeat, repeatDelayMs = 0) {
62
+ if (repeat <= 0)
63
+ return null;
64
+ if (iterationDurationMs <= 0 && repeatDelayMs <= 0) {
65
+ return new Error('a repeated transition cannot fold a zero-length iteration — `repeat` divides by the ' +
66
+ "iteration's duration, so a zero-duration play repeats nothing (the pinned Motion " +
67
+ 'silently holds the final keyframe instead). Give the transition a duration, or drop ' +
68
+ '`repeat` (REQ-TIMING-003, T18-a L3).');
69
+ }
70
+ if (!Number.isFinite(iterationDurationMs)) {
71
+ return new Error(`a repeated transition cannot fold an iteration that never settles — this trajectory is ` +
72
+ `still moving after ${exports.MAX_ITERATION_DURATION_MS}ms, past the point where the pinned ` +
73
+ 'Motion stops measuring it and starts emitting NaN. Bound the transition (duration, or ' +
74
+ 'stiffer physics), or drop `repeat` (REQ-TIMING-003, T18-a L3).');
75
+ }
76
+ return null;
77
+ }
78
+ function iterationMeasurementLane(targetIsNumeric, isExplicitSpring, isTween) {
79
+ // A keyframe ARRAY target is played by the keyframes generator unless the transition explicitly
80
+ // asked for a spring — so an untyped or tween-typed transition changes lane with the target.
81
+ if (!targetIsNumeric && !isExplicitSpring)
82
+ return 'keyframes';
83
+ if (isTween)
84
+ return 'timing';
85
+ return 'spring';
86
+ }
87
+ /**
88
+ * The refusal's identity, shared by BOTH backends (T18-a fix-up, review MAJOR).
89
+ *
90
+ * `repeatIterationRefusal` returns a bare `Error` because it crosses to the UI runtime. The
91
+ * reference backend re-wraps it as an `InvalidTransitionError`, which names the offending property
92
+ * instead of surfacing an anonymous driver-lane fault. The worklet backend cannot: it must not
93
+ * construct a core class inside a worklet, and the lane marshals only `{name, message}` to RN
94
+ * anyway, so the class identity could not survive the crossing even if it could be built.
95
+ *
96
+ * What CAN be identical across the crossing is exactly what the crossing carries — the name and the
97
+ * message. Both backends therefore build them here, so a developer sees one refusal whichever
98
+ * engine they are on, and `repeatRefusalParity` pins the two against each other.
99
+ */
100
+ exports.REPEAT_REFUSAL_NAME = 'InvalidTransitionError';
101
+ /** The offending property a fold refusal names — `InvalidTransitionError.key` on the JS backend. */
102
+ exports.REPEAT_REFUSAL_KEY = 'repeat';
103
+ /**
104
+ * A fold refusal, as a CONSUMER can recognize it after any transport.
105
+ *
106
+ * The severity router used `error instanceof InvalidTransitionError && error.key === 'repeat'`,
107
+ * which is decidable only on the reference backend: the UI runtime cannot construct a core class,
108
+ * and the crossing marshals plain data, so the shipping backend's refusal arrived as a bare `Error`
109
+ * and the router silently stopped firing (T18-a round-9 review BLOCKING 1). Class identity is not
110
+ * available across the crossing, so the recognition contract cannot be built on it — it is
111
+ * STRUCTURAL by necessity, and it lives here, beside the name and the message it completes, so the
112
+ * whole identity is one thing that one floor can hold.
113
+ *
114
+ * `isRepeatFoldRefusalIdentity` is the two-field core, so the UI lane — which holds the refusal as a
115
+ * marshalled `{name, message, key}` record, never an `Error` — decides it with the SAME rule the
116
+ * consumer applies to the rethrown error rather than a second copy that can drift.
117
+ */
118
+ function isRepeatFoldRefusalIdentity(name, key) {
119
+ return name === exports.REPEAT_REFUSAL_NAME && key === exports.REPEAT_REFUSAL_KEY;
120
+ }
121
+ function isRepeatFoldRefusal(error) {
122
+ if (typeof error !== 'object' || error === null)
123
+ return false;
124
+ const candidate = error;
125
+ return isRepeatFoldRefusalIdentity(candidate.name, candidate.key);
126
+ }
127
+ // Mirrors `InvalidTransitionError`'s message format for an anonymous component with a numeric
128
+ // value. It is duplicated rather than imported because `validate.ts` is JS-side-only (it reaches
129
+ // the subset registry and the value-type parsers) and this module crosses to the UI runtime. The
130
+ // duplication is held honest by the cross-backend floor, not by review.
131
+ // alloc-ok: lifecycle-edge — a refusal built at command time, never per frame.
132
+ function repeatRefusalMessage(repeat, reason) {
133
+ return `<Motion.View>: invalid transition option 'repeat' = ${String(repeat)} — ${reason}`;
134
+ }
135
+ /**
136
+ * The fold's geometry, verbatim from the pin (`JSAnimation.initAnimation:169-170`). The trailing
137
+ * delay of the LAST iteration is subtracted: a repeat delay separates plays, it does not extend
138
+ * the animation past its final one.
139
+ */
140
+ // alloc-ok: lifecycle-edge — geometry resolved once per animate command.
141
+ function repeatFoldGeometry(config) {
142
+ const resolvedDurationMs = config.iterationDurationMs + config.repeatDelayMs;
143
+ return {
144
+ resolvedDurationMs,
145
+ totalDurationMs: resolvedDurationMs * (config.repeat + 1) - config.repeatDelayMs,
146
+ };
147
+ }
148
+ function clamp01(value) {
149
+ return value > 1 ? 1 : value < 0 ? 0 : value;
150
+ }
151
+ // DECLARATION ORDER IS LOAD-BEARING in this module: it ships FILE-tagged, so the workletizer
152
+ // emits every function as a non-hoisted const and a worklet may only reference worklets declared
153
+ // ABOVE it. `buildRepeatedGenerator` calls this, so this comes first. Pinned by
154
+ // check-worklet-forward-refs.test.ts.
155
+ /**
156
+ * Wrap `base` in the repeat fold (REQ-TIMING-003). The returned generator is a `Generator` like any
157
+ * other — the driver steps it without knowing it repeats.
158
+ *
159
+ * Two laws diverge deliberately from a naive port of the pin's `tick`:
160
+ *
161
+ * - A REVERSED leg negates the reported velocity. Our `GeneratorSample` carries the ANALYTIC
162
+ * velocity that the C1 retarget seam and `liveFor` read (REQ-SPRING-009 / REQ-DRIVER-025),
163
+ * whereas the pin's user-visible velocity is a finite-diff of the value — correctly signed by
164
+ * construction even though its generator's internal velocity on a reversed leg is not.
165
+ * Reproducing the pin's internal sign would seed a mid-reverse retarget in the wrong direction.
166
+ * - `done` follows the fold's `totalDuration`, never the base trajectory's own settle. The base
167
+ * settles once per iteration; only the last one ends the animation, and an endless repeat is
168
+ * never done at all (packet L5 — such an element intentionally never quiesces).
169
+ */
170
+ // alloc-ok: lifecycle-edge — construction resolves geometry and builds the ONE reused sample
171
+ // record; sample() below mutates it and allocates nothing (REQ-DRIVER-021).
172
+ function repeatGenerator(base, config) {
173
+ const { repeat, leg, repeatDelayMs, iterationDurationMs } = config;
174
+ const { resolvedDurationMs, totalDurationMs } = repeatFoldGeometry({
175
+ repeat,
176
+ repeatDelayMs,
177
+ iterationDurationMs,
178
+ });
179
+ const mirrored = leg.type === 'mirror' ? leg.mirrored : null;
180
+ const isReverse = leg.type === 'reverse';
181
+ // The pin's terminal keyframe rule (motion-dom keyframes/get-final.ts), reduced to the only case
182
+ // where it disagrees with where this fold naturally lands:
183
+ //
184
+ // useFirstKeyframe = speed < 0 || (repeat && repeatType !== 'loop' && repeat % 2 === 1)
185
+ //
186
+ // `repeat % 2 === 1` selects exactly the ODD INTEGERS, and `repeatType !== 'loop'` puts BOTH
187
+ // `reverse` and `mirror` in that branch. An integer fold already ENDS on the pin's keyframe by
188
+ // construction — a whole-number progress is corrected to `iterationProgress` 1; a `reverse` leg
189
+ // reflects that to 0 and a `mirror` leg samples the mirrored trajectory at ITS end, which is the
190
+ // base origin, so both odd cases report the pin's FIRST keyframe while a `loop` leg and every
191
+ // EVEN case hold at 1 and report the pin's LAST. So the integer path needs no snap and must not
192
+ // get one. (Measured on the pin, `[0,100]` and `[0,100,30]`, repeats 1-4 × all three types: odd
193
+ // reverse and odd mirror both land on FIRST, everything else on LAST.) A FRACTIONAL repeat is
194
+ // the gap: at forward speed it satisfies no branch of the rule, so the pin commits the LAST keyframe while this
195
+ // fold is mid-play. `speed < 0` has no native producer (drivers accumulate non-negative elapsed
196
+ // only), so that half of the rule is stated and not implemented.
197
+ const snapsAtTerminal = Number.isFinite(repeat) && repeat > 0 && !Number.isInteger(repeat);
198
+ // The pin's snap is NOT part of the fold: `JSAnimation.tick` computes the fold, then applies
199
+ // `getFinalKeyframe` under its `isAnimationFinished` gate
200
+ // (`holdTime === null && (state === 'finished' || (state === 'running' && done))`), and only then
201
+ // calls `finish()` — which never touches the value. A FRESH, non-autoplaying instance never
202
+ // satisfies that gate: it is `paused`, so `holdTime` is non-null and neither disjunct holds, and
203
+ // its `sample()` returns the raw fold at EVERY time. An already-FINISHED instance takes the
204
+ // override at ANY sampled time, mid-play included; a RUNNING one only at or past `totalDuration`,
205
+ // because that disjunct also requires `done`. Measured on `repeat: 1.5 reverse`, whose raw fold is
206
+ // 80 mid-play and 50 at the terminal — paused 80/50, running 80/100, finished 100/100.
207
+ //
208
+ // `gen-repeat-goldens.mjs` builds a FRESH `autoplay: false` instance per sample, so the golden
209
+ // records both layers: `samples` are raw-fold seeks (50 at the terminal) and `terminalValue` comes
210
+ // from playing to the end (100). Keeping `sample` seek-faithful and exposing the commit separately
211
+ // reproduces that split instead of collapsing it; putting the snap inside `sample` broke the
212
+ // pinned `tween-reverse-fractional` samples, which is the golden reporting the wrong layer (H2 F1).
213
+ //
214
+ // The value is the FORWARD base at its end, never the leg the partial play was on: the pin
215
+ // commits `resolvedKeyframes[length - 1]` whatever direction it was travelling, so a `reverse` or
216
+ // `mirror` fold descending toward the origin jumps UP to the target. Core's generators are pure
217
+ // functions of elapsed, so sampling one here is safe and allocation-free at the frame path.
218
+ const finalValue = snapsAtTerminal ? base.sample(resolvedDurationMs).value : undefined;
219
+ // The delay's share of one resolved iteration — the reverse leg's re-bias factor, hoisted so the
220
+ // frame path does no division beyond the progress one.
221
+ const delayFraction = repeatDelayMs / resolvedDurationMs;
222
+ const out = { value: 0, velocity: 0, done: false };
223
+ return {
224
+ finalValue,
225
+ sample(rawElapsedMs) {
226
+ // The pin clamps before folding (`JSAnimation.tick`: `Math.max(timeWithoutDelay, 0)`), and so
227
+ // must this: `floor` of a negative progress is -1, an ODD iteration, so a `reverse` leg would
228
+ // reflect and report the far endpoint at a moment the animation has not begun. Today's
229
+ // drivers only ever accumulate non-negative deltas, but the pin's one producer of negative
230
+ // elapsed is `currentTime - delay` — property-lane `delay`, this packet's successor rung.
231
+ // `Math.max` rather than `rawElapsedMs > 0 ? rawElapsedMs : 0` — they agree on every number
232
+ // and disagree on NaN, which the comparison silently maps to 0 (reporting the trajectory's
233
+ // ORIGIN for a broken clock) while `Math.max` propagates it. A NaN clock is a programmer
234
+ // error, and it must stay loud rather than resolve to a plausible-looking value (review
235
+ // MINOR).
236
+ const elapsedMs = Math.max(rawElapsedMs, 0);
237
+ let elapsed = elapsedMs;
238
+ let frameGenerator = base;
239
+ let reversedLeg = false;
240
+ if (repeat > 0) {
241
+ // Progress across the WHOLE fold in iteration units: 2.5 is halfway through the third play.
242
+ const progress = Math.min(elapsedMs, totalDurationMs) / resolvedDurationMs;
243
+ let currentIteration = Math.floor(progress);
244
+ let iterationProgress = progress % 1;
245
+ // A whole-number progress at or past the first boundary is the END of the PREVIOUS
246
+ // iteration, not the start of the next — otherwise every boundary flickers a frame of the
247
+ // successor's origin.
248
+ if (iterationProgress === 0 && progress >= 1) {
249
+ iterationProgress = 1;
250
+ currentIteration--;
251
+ }
252
+ if (currentIteration > repeat + 1)
253
+ currentIteration = repeat + 1;
254
+ if (currentIteration % 2 !== 0) {
255
+ if (isReverse) {
256
+ iterationProgress = 1 - iterationProgress;
257
+ // Reflecting progress would put the dead time at the START of the reversed play; this
258
+ // subtraction moves it back to the END, so the rule stays uniform across repeat types:
259
+ // every play holds on the value it FINISHED on. A reversed leg therefore begins
260
+ // descending immediately from the iteration's end value and pauses at its start value.
261
+ if (repeatDelayMs > 0)
262
+ iterationProgress -= delayFraction;
263
+ reversedLeg = true;
264
+ }
265
+ else if (mirrored !== null) {
266
+ frameGenerator = mirrored;
267
+ }
268
+ }
269
+ elapsed = clamp01(iterationProgress) * resolvedDurationMs;
270
+ }
271
+ const sample = frameGenerator.sample(elapsed);
272
+ out.value = sample.value;
273
+ out.velocity = reversedLeg ? -sample.velocity : sample.velocity;
274
+ out.done = elapsedMs >= totalDurationMs;
275
+ return out;
276
+ },
277
+ };
278
+ }
279
+ /**
280
+ * Build the folded generator for a repeated transition — the ONE implementation every driver seam
281
+ * calls (both drivers' start AND retarget seams). Sharing this call does not by itself prevent
282
+ * skew: the fix-up review found both backends sharing it while both omitted it from retarget.
283
+ * Parity is held by the executing floor, `driverParity.differential.test.ts`. Each driver supplies
284
+ * only what it alone knows:
285
+ *
286
+ * - `base` / `leg` — its own already-CONSTRUCTED generators. The mirrored trajectory rides inside
287
+ * the `mirror` leg (origin/target swapped, seed velocity negated, keyframe list reversed), so a
288
+ * mirror fold without one stays unrepresentable.
289
+ * - `knownIterationDurationMs` — the transition's own resolved duration when it has one (a tween,
290
+ * a keyframe array, or a duration-resolved spring, mirroring the pin's `calculatedDuration`), or
291
+ * null for a physics/visualDuration spring, whose length is measured by the scan. A known ZERO
292
+ * is honored exactly: an authored `duration: 0` under a repeatDelay fold has genuinely
293
+ * zero-length plays (transition-default-selection F5) — scanning the base instead would invent
294
+ * a 50ms iteration the pin does not have.
295
+ *
296
+ * Generators are passed as VALUES, never as a factory callback: synchronously invoking a captured
297
+ * function parameter on the UI runtime is the wrong-runtime crash class, and `check:worklet-closures`
298
+ * fails closed on it. Both drivers therefore build their own legs eagerly and hand them over.
299
+ *
300
+ * Returns the folded generator, or the loud refusal for a fold the pin cannot execute (L3). The
301
+ * caller routes that refusal through its severity boundary — this never throws, because it runs on
302
+ * the UI runtime where a throw is a bare `std::terminate`.
303
+ */
304
+ // alloc-ok: lifecycle-edge — one fold construction per animate command, never per frame.
305
+ function buildRepeatedGenerator(repeat, leg, repeatDelayMs, base, knownIterationDurationMs) {
306
+ const iterationDurationMs = knownIterationDurationMs ?? calcIterationDurationMs(base);
307
+ const refusal = repeatIterationRefusal(iterationDurationMs, repeat, repeatDelayMs);
308
+ if (refusal !== null)
309
+ return refusal;
310
+ return repeatGenerator(base, { repeat, leg, repeatDelayMs, iterationDurationMs });
311
+ }