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