@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,698 @@
1
+ // @frame-path — executes inside the UI-runtime frame step; allocation-gated (REQ-DRIVER-021).
2
+ // Layout identity registry (specs/L2-BUILD-PACKET.md) — the ratified shared layout-identity seam
3
+ // (BRIEF Decisions 2026-07-06). Pairs LAYOUT geometry with PRESENCE-owned lifetimes without owning
4
+ // either: per `layoutId` a HOLDER STACK in mount order plus ONE shared flight (the existing
5
+ // projection session — reused, not reimplemented), which every holder reads RELATIVE TO ITS OWN
6
+ // layout rect (both elements coincide visually mid-flight; no reparenting anywhere,
7
+ // REQ-LAYOUT-015). Host-agnostic and deterministic under the fake clock; within-screen only
8
+ // (REQ-LAYOUT-003).
9
+ //
10
+ // Modeling notes:
11
+ // - The flight is a per-id `createProjectionSession`; holders derive their transforms from the
12
+ // SHARED visual rect via `invert(visual, ownLayout)` — the lead's math and the trailing's math
13
+ // are the same function over different layout rects, so coincidence holds by construction.
14
+ // - The crossfade rides ONE scalar on the [0, 100] internal progress convention (REQ-SPRING-013,
15
+ // the same convention L1 colors use), driven through the same graph and spring config as the
16
+ // rect flight: lead opacity = clamp01(s / 100), the flight's departure partner = 1 − that.
17
+ // Opacity CLAMPS on read (spring overshoot past 100 must not produce out-of-range opacity).
18
+ // - Per-id settle derives from the flight channels + scalar against the layout epsilons —
19
+ // `graph.isSettled()` is GLOBAL (any other animation on the shared graph would leak into it),
20
+ // so the registry never uses it for per-id state.
21
+ import { resolveSpringGenerator } from "../transition.js";
22
+ import { timingGenerator } from "../timing.js";
23
+ import { EPSILON_POS, EPSILON_VEL } from "./constants.js";
24
+ import { createProjectionSession } from "./session.js";
25
+ // Frames of exclusive-remount grace after a measured sole leave. Covers deferred host onLayout
26
+ // attach after React unmount/mount; far longer delays must not invent flights from forgotten rects.
27
+ const EXCLUSIVE_HANDOFF_GRACE_FRAMES = 8;
28
+ // The crossfade scalar's internal progress span (REQ-SPRING-013 convention, shared with L1 colors).
29
+ const SCALAR_END = 100;
30
+ // One edge authors at most ONE flight mode — the host resolver is single-outcome (T22 L1);
31
+ // both present is an authoring contradiction the registry refuses loud (severity law: the
32
+ // host maps this to dev-throw / prod-report at its boundary).
33
+ function assertSingleFlightMode(options) {
34
+ if (options?.timing !== undefined && options?.spring !== undefined) {
35
+ throw new Error('layout identity: flight options carry BOTH spring and timing — one edge executes exactly one mode; author one.');
36
+ }
37
+ }
38
+ // Spring-config identity for session reuse (beside timingEqual): null session-spring pairs
39
+ // with an undefined request (both mean the registry default); otherwise field-wise over the
40
+ // engine SpringConfig surface.
41
+ function springEqual(a, b) {
42
+ if (a === null || b === undefined)
43
+ return a === null && b === undefined;
44
+ return (a.stiffness === b.stiffness &&
45
+ a.damping === b.damping &&
46
+ a.mass === b.mass &&
47
+ a.duration === b.duration &&
48
+ a.bounce === b.bounce &&
49
+ a.visualDuration === b.visualDuration &&
50
+ a.velocity === b.velocity &&
51
+ a.restSpeed === b.restSpeed &&
52
+ a.restDelta === b.restDelta);
53
+ }
54
+ function timingEqual(a, b) {
55
+ if (b === undefined)
56
+ return a === null;
57
+ if (a === null)
58
+ return false;
59
+ if (a.duration !== b.duration)
60
+ return false;
61
+ const ae = a.ease;
62
+ const be = b.ease;
63
+ if (ae === be)
64
+ return true;
65
+ if (ae === undefined || be === undefined)
66
+ return ae === be;
67
+ if (typeof ae === 'string' || typeof be === 'string')
68
+ return ae === be;
69
+ if (!Array.isArray(ae) || !Array.isArray(be) || ae.length !== be.length)
70
+ return false;
71
+ for (let i = 0; i < ae.length; i++) {
72
+ if (ae[i] !== be[i])
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+ function requireFiniteRect(id, operation, rect) {
78
+ if (!Number.isFinite(rect.x) ||
79
+ !Number.isFinite(rect.y) ||
80
+ !Number.isFinite(rect.width) ||
81
+ !Number.isFinite(rect.height) ||
82
+ rect.width <= 0 ||
83
+ rect.height <= 0) {
84
+ throw new Error(`layout identity '${id}': ${operation} received a malformed rect ` +
85
+ `(${JSON.stringify(rect)}) — non-finite fields never fabricate a flight (packet §semantics 6).`);
86
+ }
87
+ }
88
+ function requireRegistryRect(id, operation, rect) {
89
+ requireFiniteRect(id, operation, rect);
90
+ return rect;
91
+ }
92
+ function clamp01(v) {
93
+ return v < 0 ? 0 : v > 1 ? 1 : v;
94
+ }
95
+ // alloc-ok: construction — one registry per driver; the per-frame surface is readInto/holds/
96
+ // isSettled/releasable + the sessions' in-place reads, which allocate nothing.
97
+ export function createLayoutIdentityRegistry(options) {
98
+ 'worklet';
99
+ if (options.graph === undefined) {
100
+ throw new Error('createLayoutIdentityRegistry: a MotionGraph is required.');
101
+ }
102
+ const { graph } = options;
103
+ // Handle provenance + resolution (round-8 major 6, round-9 major 7): the private mint map.
104
+ // One WeakMap.get per readInto does BOTH the membership check (by the handle object's
105
+ // reference identity — clones/forgeries/foreign handles miss) and the internals lookup (the
106
+ // data lives here, not on the handle, so mutating a minted handle redirects nothing). Weakly
107
+ // held — a departed holder's handle is GC-able the moment the engine drops it.
108
+ const handleData = new WeakMap();
109
+ // One spring drives the flight AND the crossfade scalar (packet §Design); the default matches
110
+ // the session's own LAYOUT_PROJECTION_SPRING.
111
+ const flightSpring = options.spring ?? { stiffness: 550, damping: 45 };
112
+ const entries = new Map();
113
+ // One-shot exclusive-remount handoffs: measured sole leave → next enter within frame grace is
114
+ // lead-from-snapshot. Parallel id list avoids Map iterators on the frame-path tick.
115
+ const exclusiveHandoffs = new Map();
116
+ const exclusiveHandoffFrames = new Map();
117
+ const exclusiveHandoffIds = [];
118
+ function seedExclusiveHandoff(id, rect) {
119
+ if (!exclusiveHandoffs.has(id))
120
+ exclusiveHandoffIds.push(id);
121
+ exclusiveHandoffs.set(id, rect);
122
+ exclusiveHandoffFrames.set(id, EXCLUSIVE_HANDOFF_GRACE_FRAMES);
123
+ }
124
+ function clearExclusiveHandoff(id) {
125
+ if (!exclusiveHandoffs.has(id))
126
+ return;
127
+ exclusiveHandoffs.delete(id);
128
+ exclusiveHandoffFrames.delete(id);
129
+ for (let i = 0; i < exclusiveHandoffIds.length; i++) {
130
+ if (exclusiveHandoffIds[i] === id) {
131
+ exclusiveHandoffIds[i] = exclusiveHandoffIds[exclusiveHandoffIds.length - 1];
132
+ exclusiveHandoffIds.length -= 1;
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ function entryOf(id, operation) {
138
+ const entry = entries.get(id);
139
+ if (entry === undefined) {
140
+ throw new Error(`layout identity '${id}': no holders are registered (${operation}).`);
141
+ }
142
+ return entry;
143
+ }
144
+ function holderIn(list, token) {
145
+ // Indexed lookup: this runs per painted holder per frame through readInto/releasable —
146
+ // Array.find allocates its callback closure per call (REQ-DRIVER-015, review round 6).
147
+ for (let i = 0; i < list.length; i++) {
148
+ const h = list[i];
149
+ if (h.token === token)
150
+ return h;
151
+ }
152
+ return undefined;
153
+ }
154
+ function requireHolder(entry, id, token, operation) {
155
+ const holder = holderIn(entry.holders, token) ?? holderIn(entry.exiting, token);
156
+ if (holder === undefined) {
157
+ throw new Error(`layout identity '${id}': '${String(token)}' does not hold this id (${operation}).`);
158
+ }
159
+ return holder;
160
+ }
161
+ function lead(entry) {
162
+ return entry.holders[entry.holders.length - 1];
163
+ }
164
+ // Ensure a projection session matches the requested flight mode (spring vs duration tween)
165
+ // AND config (per-flight spring vs registry default). Mode or config changes dispose and
166
+ // rebuild — never retarget across modes or spring configs (T22 L2).
167
+ // alloc-ok: lifecycle-edge — pairing only, never per frame.
168
+ function ensureSession(entry, timing, spring) {
169
+ if (entry.session !== null &&
170
+ timingEqual(entry.sessionTiming, timing) &&
171
+ springEqual(entry.sessionSpring, spring)) {
172
+ return;
173
+ }
174
+ entry.session?.dispose();
175
+ if (timing !== undefined) {
176
+ entry.session = createProjectionSession({ graph, timing });
177
+ entry.sessionTiming = timing;
178
+ entry.sessionSpring = null;
179
+ }
180
+ else {
181
+ entry.session = createProjectionSession({ graph, spring: spring ?? flightSpring });
182
+ entry.sessionTiming = null;
183
+ entry.sessionSpring = spring ?? null;
184
+ }
185
+ }
186
+ function animateScalarToEnd(entry, timing, spring) {
187
+ if (entry.scalar === null)
188
+ return;
189
+ if (timing !== undefined) {
190
+ graph.animate(entry.scalar, timingGenerator(SCALAR_END, timing));
191
+ }
192
+ else {
193
+ graph.animate(entry.scalar, resolveSpringGenerator(SCALAR_END, spring ?? flightSpring));
194
+ }
195
+ }
196
+ // Restart the crossfade scalar at 0 (a fresh fade for a fresh pairing; the READ mapping —
197
+ // who is lead, who fades — carries visual continuity across re-pairings). It ANIMATES only
198
+ // when the flight it rides actually exists: a pairing made before the lead measured holds at
199
+ // 0 (lead invisible, predecessor opaque) until the first commit begins the real flight.
200
+ // `instant` jumps straight to SCALAR_END (pin type:false — no multi-frame crossfade).
201
+ function restartScalar(entry, animate, instant = false, timing, spring) {
202
+ if (entry.scalar === null)
203
+ entry.scalar = graph.motionValue(0);
204
+ else
205
+ entry.scalar.jump(0);
206
+ if (instant) {
207
+ entry.scalar.jump(SCALAR_END);
208
+ entry.scalarPending = false;
209
+ return;
210
+ }
211
+ if (animate) {
212
+ animateScalarToEnd(entry, timing, spring);
213
+ entry.scalarPending = false;
214
+ }
215
+ else {
216
+ entry.scalarPending = true;
217
+ }
218
+ }
219
+ // Instant settle of rect + crossfade (REQ-API-035 layout lane under MotionConfig policy).
220
+ // Jump visual to target, scalar to end, release every exiting rider — no multi-frame flight.
221
+ // alloc-ok: lifecycle-edge — enter/commitRect/leave only; never per frame.
222
+ function settleInstant(entry, target, fadingFrom) {
223
+ // Retain the live spring config (a spring-mode session needs no rebuild just to settle);
224
+ // a timing session still collapses to the default spring, as before.
225
+ ensureSession(entry, undefined, entry.sessionSpring ?? undefined);
226
+ entry.session.settleAt(target);
227
+ entry.target = target;
228
+ entry.pendingBefore = null;
229
+ entry.fadingFrom = fadingFrom;
230
+ restartScalar(entry, false, true);
231
+ // Crossfade already complete: every exiting rider is releasable (no flight left to await).
232
+ // Indexed loop: for..of iterator allocation is banned on frame-path modules (REQ-DRIVER-021).
233
+ for (let i = 0; i < entry.exiting.length; i++) {
234
+ entry.exiting[i].released = true;
235
+ }
236
+ }
237
+ // Begin (or re-pair) the shared flight from `before` toward the lead's rect. Uses retarget when
238
+ // a measured flight is live (C0/C1 via the session's own semantics); begin otherwise.
239
+ // alloc-ok: lifecycle-edge — pairing/retarget resolution at enter/leave edges, never per frame.
240
+ function fly(entry, id, before, fadingFrom, options) {
241
+ const instant = options?.instant === true;
242
+ // Read-current tri-state on the enter/leave edges (review r2 major 1 — the commitRect
243
+ // 7htzcf law verbatim): options===undefined keeps the live mode (internal paths and
244
+ // callers that do not restate policy); an options object without timing/spring clears to
245
+ // the registry default; a config sets.
246
+ const timing = options === undefined ? (entry.sessionTiming ?? undefined) : options.timing;
247
+ const spring = options === undefined ? (entry.sessionSpring ?? undefined) : options.spring;
248
+ const target = lead(entry)?.rect ?? null;
249
+ if (instant && target !== null) {
250
+ settleInstant(entry, target, fadingFrom);
251
+ return;
252
+ }
253
+ if (entry.session !== null &&
254
+ timingEqual(entry.sessionTiming, timing) &&
255
+ springEqual(entry.sessionSpring, spring) &&
256
+ entry.session.state() === 'projecting' &&
257
+ entry.session.measured() &&
258
+ target !== null) {
259
+ entry.session.retarget(target);
260
+ }
261
+ else if (before !== null) {
262
+ ensureSession(entry, timing, spring);
263
+ entry.session.begin(id, before, target);
264
+ if (target === null)
265
+ entry.pendingBefore = before;
266
+ }
267
+ else {
268
+ // No visible departure exists anywhere in the stack (nothing ever measured): the flight
269
+ // is impossible. Degrade to a plain crossfade at natural spots and never park a
270
+ // fail-closed session — an unmeasurable wait would hold the graph's settle ledger open
271
+ // (review cycle 4 major 9).
272
+ entry.session?.dispose();
273
+ entry.session = null;
274
+ entry.sessionTiming = null;
275
+ entry.sessionSpring = null;
276
+ entry.pendingBefore = null;
277
+ }
278
+ // A new pairing supersedes every exiting fade it does not itself carry: a superseded
279
+ // exiting holder paints 0 from now on and is owed nothing — release it rather than strand
280
+ // it behind the new flight's settle (review cycle 7 major 12). The incoming fadingFrom may
281
+ // itself be an exiting holder (the leave path) — that one still rides.
282
+ for (const ex of entry.exiting) {
283
+ if (ex.token !== fadingFrom)
284
+ ex.released = true;
285
+ }
286
+ entry.target = target;
287
+ entry.fadingFrom = fadingFrom;
288
+ restartScalar(entry, target !== null, false, timing, spring);
289
+ }
290
+ function flightSettled(entry) {
291
+ // The rect leg: no session means no rect flight is pending (never flown, a reverted
292
+ // pairing, or a degraded plain fade) — settled rect-wise, but NEVER settled overall while
293
+ // the crossfade below is still live (review cycle 5 major 10: a degraded fade is active
294
+ // graph work; per-id settle must not lie and read() must render the fade, not snap it).
295
+ // Allocation-free (REQ-DRIVER-015): the session compares its live channels in place —
296
+ // this runs per active id per frame, so visualRect()/velocity() record-building is banned.
297
+ if (entry.session !== null) {
298
+ if (entry.target === null)
299
+ return false; // unmeasured target: fail-closed, never settled
300
+ if (!entry.session.settledAgainst(entry.target, EPSILON_POS, EPSILON_VEL))
301
+ return false;
302
+ }
303
+ const s = entry.scalar;
304
+ if (s === null)
305
+ return true;
306
+ return Math.abs(s.get() - SCALAR_END) < EPSILON_POS && Math.abs(s.getVelocity()) < EPSILON_VEL;
307
+ }
308
+ // The registry's one per-frame scratch rect: readHolderInto runs per painted holder per frame,
309
+ // so the visual rect is written in place here instead of allocated per read (REQ-DRIVER-015).
310
+ const scratchVisual = { x: 0, y: 0, width: 0, height: 0 };
311
+ // Single source of truth for read() and readInto(): a RESOLVED entry+holder in, ONE settle
312
+ // evaluation (read+zLifted as separate calls evaluated flightSettled twice), written field-wise
313
+ // into the caller-owned record. read() resolves by token (js-thread); readInto() dereferences
314
+ // its enter()-minted handle O(1) (the per-frame path, round-7 major 4).
315
+ function writeHolderView(entry, h, out) {
316
+ const holder = h.token;
317
+ const settled = flightSettled(entry);
318
+ const isLead = lead(entry)?.token === holder;
319
+ // Opacity: the lead fades in with the scalar; the flight's departure partner fades out;
320
+ // everyone else (older stack members, exiting holders no longer paired) sits at 0. Exact
321
+ // endpoints at settle — no epsilon residue on what the substrate paints. With no scalar
322
+ // (no pairing ever, or a reverted one) the identity sits with the LEAD alone — two
323
+ // holders both painting opaque is never a valid state (review cycle 6 major 11).
324
+ if (entry.scalar === null)
325
+ out.opacity = isLead ? 1 : 0;
326
+ else if (isLead)
327
+ out.opacity = settled ? 1 : clamp01(entry.scalar.get() / SCALAR_END);
328
+ else if (entry.fadingFrom === holder) {
329
+ out.opacity = settled ? 0 : 1 - clamp01(entry.scalar.get() / SCALAR_END);
330
+ }
331
+ else
332
+ out.opacity = 0;
333
+ // Transform: the shared visual rect relative to this holder's OWN layout rect. Identity
334
+ // while no flight exists, the holder is unmeasured, or the flight is unmeasured
335
+ // (fail-closed — never a fabricated delta, REQ-LAYOUT-011). The invert is written field-wise
336
+ // (projection.invert allocates its Transform — banned per frame); the divisions are safe by
337
+ // construction: every stored holder rect passed requireRegistryRect's width/height > 0 gate.
338
+ if (entry.session === null ||
339
+ !entry.session.writeVisualInto(scratchVisual) ||
340
+ h.rect === null) {
341
+ out.translateX = 0;
342
+ out.translateY = 0;
343
+ out.scaleX = 1;
344
+ out.scaleY = 1;
345
+ }
346
+ else {
347
+ out.translateX = scratchVisual.x - h.rect.x;
348
+ out.translateY = scratchVisual.y - h.rect.y;
349
+ out.scaleX = scratchVisual.width / h.rect.width;
350
+ out.scaleY = scratchVisual.height / h.rect.height;
351
+ }
352
+ // zLifted()'s exact definition, folded in so the hot path is one call, not three.
353
+ out.zLifted = isLead && !settled;
354
+ }
355
+ return {
356
+ // alloc-ok: lifecycle-edge — holder mount (React commit); builds the entry/holder records once.
357
+ enter(id, holder, rect, options) {
358
+ assertSingleFlightMode(options);
359
+ const registryRect = rect === null ? null : requireRegistryRect(id, 'enter', rect);
360
+ let entry = entries.get(id);
361
+ if (entry === undefined) {
362
+ entry = {
363
+ holders: [],
364
+ exiting: [],
365
+ session: null,
366
+ sessionTiming: null,
367
+ sessionSpring: null,
368
+ target: null,
369
+ pendingBefore: null,
370
+ fadingFrom: null,
371
+ scalar: null,
372
+ scalarPending: false,
373
+ };
374
+ entries.set(id, entry);
375
+ }
376
+ if (holderIn(entry.holders, holder) !== undefined ||
377
+ holderIn(entry.exiting, holder) !== undefined) {
378
+ throw new Error(`layout identity '${id}': token '${String(holder)}' already holds this id (enter).`);
379
+ }
380
+ const predecessor = lead(entry);
381
+ const h = { token: holder, rect: registryRect };
382
+ entry.holders.push(h);
383
+ // Frozen field-less token: nothing to mutate, nothing to leak (round-9 major 7).
384
+ const handle = Object.freeze({
385
+ __identityHolderHandle: true,
386
+ });
387
+ handleData.set(handle, { id, entry, holder: h });
388
+ if (predecessor === undefined) {
389
+ // Exclusive remount handoff: measured sole leave left a one-shot departure rect.
390
+ const handoff = exclusiveHandoffs.get(id);
391
+ clearExclusiveHandoff(id);
392
+ if (handoff !== undefined && handoff.width > 0 && handoff.height > 0) {
393
+ fly(entry, id, handoff, null, options);
394
+ return { role: 'lead', handle };
395
+ }
396
+ return { role: 'sole', handle };
397
+ }
398
+ // Live concurrent predecessor wins over any stale exclusive handoff.
399
+ clearExclusiveHandoff(id);
400
+ // The flight departs from — and the crossfade fades out — the last VISIBLE identity, not
401
+ // blindly the immediate predecessor: an unmeasured pending lead was never painted and can
402
+ // be neither the departure rect nor the fade-out partner (review cycle 4 major 9). A live
403
+ // flight's visual rect wins (C0 on rapid re-pairing); otherwise the nearest MEASURED
404
+ // holder down the stack; an all-unmeasured stack degrades to a plain crossfade in fly().
405
+ const liveVisual = entry.session?.visualRect() ?? null;
406
+ const nearestMeasured = [...entry.holders]
407
+ .reverse()
408
+ .find((c) => c.token !== holder && c.rect !== null);
409
+ const before = liveVisual ?? nearestMeasured?.rect ?? null;
410
+ const fadingFrom = predecessor.rect !== null
411
+ ? predecessor.token
412
+ : (entry.fadingFrom ?? nearestMeasured?.token ?? predecessor.token);
413
+ fly(entry, id, before, fadingFrom, options);
414
+ return { role: 'lead', handle };
415
+ },
416
+ commitRect(id, holder, rect, options) {
417
+ assertSingleFlightMode(options);
418
+ const instant = options?.instant === true;
419
+ const registryRect = requireRegistryRect(id, 'commitRect', rect);
420
+ const entry = entryOf(id, 'commitRect');
421
+ const h = requireHolder(entry, id, holder, 'commitRect');
422
+ h.rect = registryRect;
423
+ const currentLead = lead(entry);
424
+ if (currentLead?.token !== holder)
425
+ return;
426
+ // A LEAD commit (re)targets the shared flight — host layout stays authoritative
427
+ // (REQ-LAYOUT-002). A pairing made before the lead measured departs from the recorded
428
+ // predecessor rect now that a target exists. A degraded pairing (no session — nothing in
429
+ // the stack ever measured) skips the flight but still starts its deferred crossfade below.
430
+ if (instant) {
431
+ settleInstant(entry, registryRect, entry.fadingFrom);
432
+ return;
433
+ }
434
+ // Read-current flight mode (7htzcf): when this edge carries options, timing/spring
435
+ // absence means the default spring (clear prior mode/config). options===undefined keeps
436
+ // the live mode (internal paths that do not restate policy).
437
+ const priorTiming = entry.sessionTiming;
438
+ const priorSpring = entry.sessionSpring;
439
+ const flightTiming = options === undefined ? (entry.sessionTiming ?? undefined) : options.timing;
440
+ const flightSpringConfig = options === undefined ? (entry.sessionSpring ?? undefined) : options.spring;
441
+ if (entry.session === null) {
442
+ // fall through to the deferred-crossfade start
443
+ }
444
+ else if (entry.pendingBefore !== null) {
445
+ const before = entry.pendingBefore;
446
+ entry.pendingBefore = null;
447
+ ensureSession(entry, flightTiming, flightSpringConfig);
448
+ entry.session.begin(id, before, registryRect);
449
+ entry.target = registryRect;
450
+ }
451
+ else if (timingEqual(entry.sessionTiming, flightTiming) &&
452
+ springEqual(entry.sessionSpring, flightSpringConfig) &&
453
+ entry.session.state() === 'projecting' &&
454
+ entry.session.measured()) {
455
+ entry.session.retarget(registryRect);
456
+ entry.target = registryRect;
457
+ }
458
+ else {
459
+ const before = entry.session.visualRect() ?? entry.target;
460
+ if (before === null) {
461
+ // No departure rect can ever exist (the predecessor never measured): the flight is
462
+ // impossible. CLOSE the rect leg instead of parking the session in a wait that can
463
+ // never resolve (review cycle 3 major 6) — the deferred crossfade below still hands
464
+ // the identity over as a plain fade; geometry stays fail-closed at identity.
465
+ entry.session.dispose();
466
+ entry.session = null;
467
+ entry.sessionTiming = null;
468
+ entry.sessionSpring = null;
469
+ entry.target = null;
470
+ entry.pendingBefore = null;
471
+ }
472
+ else {
473
+ ensureSession(entry, flightTiming, flightSpringConfig);
474
+ entry.session.begin(id, before, registryRect);
475
+ entry.target = registryRect;
476
+ }
477
+ }
478
+ if (entry.scalarPending && entry.scalar !== null) {
479
+ // The deferred crossfade starts with the REAL flight — or degrades to a plain fade when
480
+ // the predecessor never measured (geometry stays fail-closed, but a lead stranded
481
+ // invisible would be a silent severity-law violation).
482
+ entry.scalar.jump(0);
483
+ animateScalarToEnd(entry, flightTiming, flightSpringConfig);
484
+ entry.scalarPending = false;
485
+ }
486
+ else if (
487
+ // Live scalar must share read-current config with geometry (2vqwp8 5d2c7f9a8e41):
488
+ // session rebuild alone left a prior duration tween finishing for hundreds of frames.
489
+ entry.scalar !== null &&
490
+ options !== undefined &&
491
+ (!timingEqual(priorTiming, flightTiming) || !springEqual(priorSpring, flightSpringConfig))) {
492
+ animateScalarToEnd(entry, flightTiming, flightSpringConfig);
493
+ }
494
+ },
495
+ // alloc-ok: lifecycle-edge — holder unmount (React commit); exiting-fade bookkeeping.
496
+ leave(id, holder, options) {
497
+ assertSingleFlightMode(options);
498
+ const entry = entryOf(id, 'leave');
499
+ const exitingHolder = holderIn(entry.exiting, holder);
500
+ if (exitingHolder !== undefined) {
501
+ // A former lead being released (post-crossfade removal path).
502
+ entry.exiting.splice(entry.exiting.indexOf(exitingHolder), 1);
503
+ exitingHolder.departed = true; // handle validity ends with registry membership
504
+ }
505
+ else {
506
+ const h = requireHolder(entry, id, holder, 'leave');
507
+ const wasLead = lead(entry)?.token === holder;
508
+ entry.holders.splice(entry.holders.indexOf(h), 1);
509
+ if (wasLead && entry.holders.length > 0) {
510
+ const before = entry.session?.visualRect() ?? h.rect;
511
+ if (before === null) {
512
+ h.departed = true; // dropped outright — no exit ride, handle validity ends here
513
+ // The leaver never became visible (unmeasured, no live flight): it drops outright —
514
+ // immediately releasable, no exit ride, no crossfade owed (review cycle 2 major 4).
515
+ // What remains is decided by what sits beneath it (review cycle 6 major 11):
516
+ const resumed = lead(entry);
517
+ const resumedSource = resumed === undefined
518
+ ? undefined
519
+ : [...entry.holders]
520
+ .reverse()
521
+ .find((c) => c.token !== resumed.token && c.rect !== null);
522
+ if (resumed !== undefined &&
523
+ resumed.rect === null &&
524
+ (resumedSource !== undefined || entry.fadingFrom !== null)) {
525
+ // An older PENDING pairing survives beneath the leaver (an unmeasured resumed
526
+ // lead over a visible identity): RE-ESTABLISH it — the resumed lead's first
527
+ // commit must still run the original shared flight + crossfade, never snap in.
528
+ fly(entry, id, resumedSource?.rect ?? null, entry.fadingFrom ?? resumedSource?.token ?? null, options);
529
+ }
530
+ else {
531
+ // Nothing pending underneath (measured resumed lead, or nothing visible
532
+ // anywhere): the entry returns to its at-rest state. dispose() releases the
533
+ // session's waiting-for-measurement activity — an abandoned session must not
534
+ // hold the graph's settle ledger open (review cycle 3 major 6).
535
+ entry.session?.dispose();
536
+ entry.session = null;
537
+ entry.sessionTiming = null;
538
+ entry.sessionSpring = null;
539
+ entry.target = null;
540
+ entry.pendingBefore = null;
541
+ entry.fadingFrom = null;
542
+ entry.scalar = null;
543
+ entry.scalarPending = false;
544
+ }
545
+ }
546
+ else {
547
+ // The lead leaving hands lead back and REVERSES the flight; the leaver rides as
548
+ // exiting-trailing (fade-out) until settle, then `releasable` (packet §Design) —
549
+ // its handle stays valid for the ride.
550
+ entry.exiting.push(h);
551
+ fly(entry, id, before, holder, options);
552
+ }
553
+ }
554
+ else {
555
+ // A trailing/non-lead holder (or the last holder) leaving gets no exit ride: its
556
+ // handle's validity ends with its registry membership. Presence-retained followers
557
+ // must not take this leave while the successor's forward flight is live
558
+ // (REQ-PRESENCE-024): beginExit skip-detaches; unmount still leaves.
559
+ h.departed = true;
560
+ // Exclusive remount: last measured holder seeds a short-lived handoff for the successor
561
+ // that React mounts in the same commit (catalog Shared-layout-reparenting).
562
+ if (entry.holders.length === 0 && entry.exiting.length === 0 && h.rect !== null) {
563
+ seedExclusiveHandoff(id, h.rect);
564
+ }
565
+ if (entry.fadingFrom === holder) {
566
+ // The fade-out partner left mid-crossfade: nothing remains to fade against — the lead
567
+ // paints fully opaque immediately (a half-faded lead with no visible counterpart is a
568
+ // continuity dip, review cycle 2 minor 5). jump() itself ends the live scalar spring
569
+ // (the pinned notify-then-stop contract, r17) — no pre-stop, which would reverse the
570
+ // cancellation/change order. The rect flight continues unaffected.
571
+ entry.fadingFrom = null;
572
+ if (entry.scalar !== null) {
573
+ entry.scalar.jump(SCALAR_END);
574
+ entry.scalarPending = false;
575
+ }
576
+ }
577
+ }
578
+ }
579
+ if (entry.holders.length === 0 && entry.exiting.length === 0) {
580
+ // GC (packet §Design): the last holder leaving drops the id. A measured sole leave may
581
+ // have seeded exclusiveHandoffs above — that snapshot is independent of the entry GC.
582
+ // dispose() closes out the session with the entry: a fail-closed wait releases its
583
+ // activity, a live flight's springs stop (nothing reads them anymore) — either way the
584
+ // graph's settle ledger is left clean (review cycle 3 major 6).
585
+ entry.session?.dispose();
586
+ entries.delete(id);
587
+ }
588
+ },
589
+ // alloc-ok: js-thread — record-building read for JS-side callers; the UI frame path uses
590
+ // readInto's caller-owned record.
591
+ read(id, holder) {
592
+ // Delegates to the same single-source computation the hot path uses; this record-building
593
+ // shape stays for JS-side callers (checks, e2e) — the per-frame path is readInto().
594
+ const out = {
595
+ translateX: 0,
596
+ translateY: 0,
597
+ scaleX: 1,
598
+ scaleY: 1,
599
+ opacity: 0,
600
+ zLifted: false,
601
+ };
602
+ const entry = entryOf(id, 'read');
603
+ const h = requireHolder(entry, id, holder, 'read');
604
+ writeHolderView(entry, h, out);
605
+ return {
606
+ transform: {
607
+ translateX: out.translateX,
608
+ translateY: out.translateY,
609
+ scaleX: out.scaleX,
610
+ scaleY: out.scaleY,
611
+ },
612
+ opacity: out.opacity,
613
+ };
614
+ },
615
+ readInto(handle, out) {
616
+ // O(1) handle resolution (round-7 major 4) with the by-token discipline's fail-loud
617
+ // posture intact: ONE WeakMap.get is both the provenance check and the lookup (rounds
618
+ // 8–9) — forged objects, spread-clones, foreign-registry handles, and mutated real
619
+ // handles all miss THIS registry's private map or resolve to the untouched internals;
620
+ // only then is liveness checked. WeakMap.get on a primitive returns undefined, so the
621
+ // single miss branch covers malformed values too.
622
+ const hi = handleData.get(handle);
623
+ if (hi === undefined) {
624
+ throw new Error('layout identity: readInto received a value that is not an enter()-minted ' +
625
+ 'IdentityHolderHandle of this registry (forged and cross-registry handles never dereference).');
626
+ }
627
+ if (hi.holder.departed === true) {
628
+ throw new Error(`layout identity '${hi.id}': this handle's holder ('${String(hi.holder.token)}') no longer holds the id (readInto).`);
629
+ }
630
+ writeHolderView(hi.entry, hi.holder, out);
631
+ },
632
+ releasable(id, holder) {
633
+ const entry = entries.get(id);
634
+ if (entry === undefined)
635
+ return true; // the id was dropped — nothing holds the element
636
+ const exitingHolder = holderIn(entry.exiting, holder);
637
+ if (exitingHolder !== undefined) {
638
+ // Sticky latch (review cycle 7 major 12): once the fade this holder rides has settled
639
+ // (or a new pairing superseded it in fly()), the release is permanent — a later
640
+ // unrelated flight must never flip it back.
641
+ if (exitingHolder.released === true)
642
+ return true;
643
+ if (flightSettled(entry)) {
644
+ exitingHolder.released = true;
645
+ return true;
646
+ }
647
+ return false;
648
+ }
649
+ // Still holding → not releasable; not tracked at all → the registry is done with it
650
+ // (e.g. a reverted pending pairing dropped the leaver without an exit ride — review
651
+ // cycle 2 major 4: a coupled safeToRemove must still fire for it).
652
+ return holderIn(entry.holders, holder) === undefined;
653
+ },
654
+ zLifted(id, holder) {
655
+ const entry = entryOf(id, 'zLifted');
656
+ requireHolder(entry, id, holder, 'zLifted');
657
+ return lead(entry)?.token === holder && !flightSettled(entry);
658
+ },
659
+ isSettled(id) {
660
+ const entry = entries.get(id);
661
+ if (entry === undefined)
662
+ return true;
663
+ return flightSettled(entry);
664
+ },
665
+ holds(id, holder) {
666
+ const entry = entries.get(id);
667
+ if (entry === undefined)
668
+ return false; // the id was dropped — nothing is tracked
669
+ return (holderIn(entry.holders, holder) !== undefined ||
670
+ holderIn(entry.exiting, holder) !== undefined);
671
+ },
672
+ isLead(id, holder) {
673
+ const entry = entries.get(id);
674
+ if (entry === undefined)
675
+ return false;
676
+ return lead(entry)?.token === holder;
677
+ },
678
+ // Once per UI frame from the layout engine step (REQ-DRIVER-015: indexed, no Map iterators).
679
+ tickExclusiveHandoffs() {
680
+ for (let i = 0; i < exclusiveHandoffIds.length;) {
681
+ const id = exclusiveHandoffIds[i];
682
+ const remaining = exclusiveHandoffFrames.get(id);
683
+ if (remaining === undefined || remaining <= 1) {
684
+ exclusiveHandoffs.delete(id);
685
+ exclusiveHandoffFrames.delete(id);
686
+ exclusiveHandoffIds[i] = exclusiveHandoffIds[exclusiveHandoffIds.length - 1];
687
+ exclusiveHandoffIds.length -= 1;
688
+ continue;
689
+ }
690
+ exclusiveHandoffFrames.set(id, remaining - 1);
691
+ i += 1;
692
+ }
693
+ },
694
+ hasPendingExclusiveHandoffs() {
695
+ return exclusiveHandoffIds.length > 0;
696
+ },
697
+ };
698
+ }