@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,1015 @@
1
+ "use strict";
2
+ // SPEC-COMPONENT §2 — validateTarget (REQ-API-013/014/016): the eager, exhaustive, fail-closed runtime
3
+ // validator for a declarative animation target. It is the fail-loud backstop the compile-time Target type
4
+ // (REQ-API-015) cannot be — it accepts an untyped, dynamically-shaped input and rejects, with a
5
+ // descriptive error, any property that is unknown, off-host, wrong-typed, or reserved-but-unimplemented.
6
+ // An invalid target NEVER partially applies: the first offending property aborts the whole target
7
+ // (fail-closed). It composes the SUBSET resolver (membership/off-host authority) and normalizer (value
8
+ // validity) — it does not fork a second membership list or a second parser. Host-agnostic (REQ-CORE-003).
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.InvalidTransitionError = exports.InvalidTargetError = void 0;
11
+ exports.targetShapeRefusal = targetShapeRefusal;
12
+ exports.assertTargetShape = assertTargetShape;
13
+ exports.validateTargetRefusal = validateTargetRefusal;
14
+ exports.validateTarget = validateTarget;
15
+ exports.validateTargetSnapshot = validateTargetSnapshot;
16
+ exports.validateTransitionRefusal = validateTransitionRefusal;
17
+ exports.transitionShapeRefusal = transitionShapeRefusal;
18
+ exports.validateTransition = validateTransition;
19
+ const resolve_1 = require("../subset/resolve.cjs");
20
+ const normalize_1 = require("../subset/normalize.cjs");
21
+ const discrete_1 = require("../value-types/discrete.cjs");
22
+ const mix_1 = require("../value-types/mix.cjs");
23
+ const constants_1 = require("../config/constants.cjs");
24
+ const measure_resolve_1 = require("../value-types/measure-resolve.cjs");
25
+ const motion_arc_1 = require("../motion-arc.cjs");
26
+ const types_1 = require("./types.cjs");
27
+ const transition_1 = require("./transition.cjs");
28
+ const repeat_1 = require("../repeat.cjs");
29
+ const transition_2 = require("../transition.cjs");
30
+ const timing_1 = require("../timing.cjs");
31
+ const boundedArray_1 = require("./boundedArray.cjs");
32
+ // Thrown when a declarative target property is not a valid animatable value for the host. Structured
33
+ // fields let tooling locate the offending prop; the message names the component, key, received value, and
34
+ // reason (REQ-API-014). The originating SUBSET error (if any) is preserved via `cause` — this class only
35
+ // ADDS the component/target context, it never swallows a failure.
36
+ class InvalidTargetError extends Error {
37
+ key;
38
+ value;
39
+ componentId;
40
+ constructor(componentId, key, value, reason, cause) {
41
+ super(`${componentLabel(componentId)}: invalid animation target property '${key}' = ${renderValue(value)} — ${reason}`, cause !== undefined ? { cause } : undefined);
42
+ this.name = 'InvalidTargetError';
43
+ this.key = key;
44
+ this.value = value;
45
+ this.componentId = componentId;
46
+ }
47
+ }
48
+ exports.InvalidTargetError = InvalidTargetError;
49
+ function componentLabel(componentId) {
50
+ // Public hosts supply their own complete identity (`<Text>`, `<Motion.Text>`, …). Wrapping
51
+ // that in a View-specific label corrupts the diagnostic and breaks REQ-API-014 ownership.
52
+ return componentId ?? '<Motion.View>';
53
+ }
54
+ // A compact, readable rendering of the received value for the error message — a quoted string, a bare
55
+ // scalar, or a type tag for non-primitives (so an object does not print as an opaque `[object Object]`
56
+ // without context).
57
+ function renderValue(value) {
58
+ if (typeof value === 'string')
59
+ return JSON.stringify(value);
60
+ if (typeof value === 'number' || typeof value === 'boolean' || value === null)
61
+ return String(value);
62
+ if (value === undefined)
63
+ return 'undefined';
64
+ if (Array.isArray(value))
65
+ return `an array (length ${value.length})`;
66
+ return `a ${typeof value} value`;
67
+ }
68
+ // The developer-facing name of a value type, for the "expected …" clause of a wrong-shape message.
69
+ function describeValueType(valueType) {
70
+ switch (valueType) {
71
+ case 'length':
72
+ // R13 value-breadth (REQ-VALUETYPE-013): measure-resolved %/auto/simple calc join number|px.
73
+ return 'length (a number of dp, a px string, or a measure-resolved %/auto/simple calc)';
74
+ case 'unitless':
75
+ return 'unitless numeric';
76
+ case 'angle':
77
+ return 'angle (a number of degrees, or a deg/rad string)';
78
+ case 'rgba':
79
+ return 'color';
80
+ case 'complex':
81
+ return 'complex';
82
+ case 'discrete':
83
+ return 'discrete keyword (a CSS keyword string like none/flex/hidden/visible)';
84
+ case 'gesture':
85
+ return 'gesture';
86
+ }
87
+ }
88
+ // Validate one property's value against its capability entry (REQ-API-013). Numeric/color value types are
89
+ // checked by the SUBSET normalizer — the single value validator, which parses the value as its type and
90
+ // throws loudly on anything unparseable or unit-incompatible. complex/gesture value types are reserved in
91
+ // the registry but their animation is not implemented, so they fail loud as "not yet supported"
92
+ // (REQ-API-016) rather than silently no-op.
93
+ // RETURN-shaped (M3 r6 major 30a9c21bf960; completed M3 r7 majors 32946c21f150 +
94
+ // 22c76ce970f3): the validator's OWN refusals return and BOTH dependency gates
95
+ // (capability resolution, value normalization) are verdict-shaped all the way down — no
96
+ // catch exists anywhere on this path, so a returned refusal is validator-made by
97
+ // construction and anything THROWN is a fault that keeps its exact identity.
98
+ function validateValueRefusal(componentId, key, entry, value) {
99
+ // A keyframe ARRAY target (REQ-API-033, R8): the value interpolates THROUGH the elements. Validate
100
+ // the array shape + every element as this key's single value-type (the array is one value-type), then
101
+ // fall through to the scalar checks for a non-array value.
102
+ if (Array.isArray(value)) {
103
+ return validateKeyframeArrayRefusal(componentId, key, entry, value);
104
+ }
105
+ switch (entry.valueType) {
106
+ case 'length':
107
+ case 'unitless':
108
+ case 'angle':
109
+ case 'rgba': {
110
+ if (typeof value !== 'number' && typeof value !== 'string') {
111
+ return new InvalidTargetError(componentId, key, value, `expected a ${describeValueType(entry.valueType)} value (number or string), received ${renderValue(value)}`);
112
+ }
113
+ // The key-aware executable matrix runs BEFORE normalization (review fe5d455ffde7): a
114
+ // length string — px included — executes only on the positional keys, and auto only for
115
+ // width/height. A px-normalizable string on a non-positional key must not slip through
116
+ // the normalizer's direct acceptance, and a context-requiring form must not ride the
117
+ // parse rescue key-blind.
118
+ if (entry.valueType === 'length' && typeof value === 'string') {
119
+ const executableRefusal = (0, measure_resolve_1.executableLengthValueRefusal)(key, value);
120
+ if (executableRefusal !== null) {
121
+ return new InvalidTargetError(componentId, key, value, executableRefusal, new Error(executableRefusal));
122
+ }
123
+ }
124
+ {
125
+ // Reuse the SUBSET normalizer as the authoritative value gate (parses + enforces unit
126
+ // compatibility + rejects malformed colors), VERDICT-shaped (M3 r7 major 22c76ce970f3):
127
+ // its returned refusal enriches with the target/component context (REQ-API-014) — the
128
+ // normalizer's message is preserved verbatim in the reason and as `cause` — while a
129
+ // THROW from a polluted parse dependency propagates untouched (no catch exists here).
130
+ const verdict = (0, normalize_1.normalizeCapabilityVerdict)(entry, value);
131
+ if (verdict.refusal !== null) {
132
+ // R13 value-breadth (REQ-VALUETYPE-013): length may be measure-resolved (%/auto/simple
133
+ // calc). Those forms are not px-normalizable without layout context — accept when
134
+ // parseLengthTargetVerdict succeeds (the key-aware matrix already refused what it
135
+ // must not execute, above); unsupported calc remainder stays loud.
136
+ if (entry.valueType === 'length') {
137
+ const lengthVerdict = (0, measure_resolve_1.parseLengthTargetVerdict)(value);
138
+ if (lengthVerdict.refusal === null)
139
+ return null;
140
+ return new InvalidTargetError(componentId, key, value, `not a valid ${describeValueType(entry.valueType)} value (${lengthVerdict.refusal})`, new Error(lengthVerdict.refusal));
141
+ }
142
+ return new InvalidTargetError(componentId, key, value, `not a valid ${describeValueType(entry.valueType)} value (${verdict.refusal.message})`, verdict.refusal);
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+ case 'discrete': {
148
+ // T23 B2b: a single keyword string of the discrete FAMILY. isDiscreteKeyword pre-guards the
149
+ // parse (a letters-only string can never throw in parseValue); the family verdict refuses
150
+ // keywords other families own ('auto', named colors). WHICH mix law a pair takes — the
151
+ // mixVisibility step or the zero-slot target constant (REQ-VALUETYPE-015) — is a mix-time
152
+ // question over TWO values, answered at the engines' pair boundaries and not by this
153
+ // single-value gate. No discrete pair is refused on account of its endpoints' visibility.
154
+ if (typeof value !== 'string' ||
155
+ !(0, discrete_1.isDiscreteKeyword)(value) ||
156
+ (0, mix_1.parseValue)(value).kind !== 'discrete') {
157
+ return new InvalidTargetError(componentId, key, value, `expected a ${describeValueType(entry.valueType)} value, received ${renderValue(value)}`);
158
+ }
159
+ return null;
160
+ }
161
+ case 'complex':
162
+ case 'gesture':
163
+ return new InvalidTargetError(componentId, key, value, `not yet supported — '${key}' is reserved in the universal-subset registry (valueType '${entry.valueType}') but its animation is not yet implemented (REQ-API-016)`);
164
+ }
165
+ return null;
166
+ }
167
+ // Validate a keyframe ARRAY target (REQ-API-033, R8): length ≥2, and every element a valid value of the
168
+ // key's single value-type — the array is ONE value-type, so each element reuses `validateValueRefusal`
169
+ // (no forked parser). A `null` FIRST element is legal (from-current seed, R8-F2); a null anywhere else,
170
+ // too-few keyframes, or a mixed/incompatible element FAILS LOUD naming the offending index. RETURN-shaped
171
+ // like its caller — the whole array is the reported value; the per-element refusal rides as `cause`.
172
+ function validateKeyframeArrayRefusal(componentId, key, entry, arr) {
173
+ if (arr.length < 2) {
174
+ return new InvalidTargetError(componentId, key, arr, `a keyframe array must have at least two keyframes (received length ${arr.length}) — REQ-API-033`);
175
+ }
176
+ for (let i = 0; i < arr.length; i++) {
177
+ const element = arr[i];
178
+ if (element === null) {
179
+ // R8-F2: only the FIRST keyframe may be null (seed from the element's current value).
180
+ if (i === 0)
181
+ continue;
182
+ return new InvalidTargetError(componentId, key, arr, `keyframe at index ${i} is null — only the FIRST keyframe may be null (from-current seed, R8-F2)`);
183
+ }
184
+ // Each keyframe is a SCALAR of the key's single value-type — a nested array is not a keyframe.
185
+ // Guard BEFORE delegating to the (array-aware) value validator, else a nested array recurses
186
+ // straight back into this function and slips through, reaching web Motion or the numeric generator
187
+ // as a malformed input (review 2026-07-18 major 7b423a46e6a1).
188
+ if (Array.isArray(element)) {
189
+ return new InvalidTargetError(componentId, key, arr, `keyframe at index ${i} is a nested array — each keyframe must be a single ${describeValueType(entry.valueType)} value, not an array (REQ-API-033)`);
190
+ }
191
+ const elementRefusal = validateValueRefusal(componentId, key, entry, element);
192
+ if (elementRefusal !== null) {
193
+ return new InvalidTargetError(componentId, key, arr, `keyframe at index ${i} (${renderValue(element)}) is not a valid ${describeValueType(entry.valueType)} value for '${key}' — a keyframe array is a single value-type (REQ-API-033)`, elementRefusal);
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+ // The target's SHAPE is part of the runtime contract (REQ-API-013, review round 9): before any
199
+ // per-property validation, the target itself must be a plain object. Without this gate,
200
+ // Object.keys silently coerced numbers/booleans to the empty target and raw-TypeErrored on null —
201
+ // either no failure at all, or an unnamed one at the wrong altitude. Exported so the severity
202
+ // lanes (native gate, web shim) can refuse a malformed target AS A UNIT under the production
203
+ // report-and-refuse posture.
204
+ function targetShapeRefusal(target, opts) {
205
+ if (typeof target === 'object' && target !== null && !Array.isArray(target)) {
206
+ // PLAIN means plain (review round 10 major 33): a Date/Map/Set/class instance carries no
207
+ // own enumerable target keys, so the per-key loop below would validate it CLEAN as the
208
+ // empty target — silent acceptance of a malformed input. Only object literals qualify:
209
+ // prototype Object.prototype (literals, {...spreads}) or null (Object.create(null)).
210
+ const proto = Object.getPrototypeOf(target);
211
+ if (proto === Object.prototype || proto === null)
212
+ return null;
213
+ }
214
+ return new InvalidTargetError(opts?.componentId, '(target)', target, 'a declarative target must be a plain object of animatable properties ' +
215
+ '(REQ-API-013 — a malformed shape fails loud, never coerces to the empty target)');
216
+ }
217
+ function assertTargetShape(target, opts) {
218
+ const refusal = targetShapeRefusal(target, opts);
219
+ if (refusal !== null)
220
+ throw refusal;
221
+ }
222
+ // Eagerly + exhaustively validate every property of a declarative target against a host (REQ-API-013).
223
+ // Fails CLOSED on the FIRST offending property — the throw aborts iteration so an invalid target never
224
+ // partially applies. Accepts a loose, untyped input (dynamically-shaped objects, JS callers) — this is the
225
+ // runtime authority the compile-time `Target` type (REQ-API-015) delegates units/ranges/dynamic shapes to.
226
+ function validateTargetRefusal(target, host, opts) {
227
+ // This is a root public boundary too, not merely the refusal-only half of a supplying
228
+ // gate. Delegate to the one-read snapshot authority so hostile keyframe arrays cannot
229
+ // grow or change between validation steps.
230
+ return validateTargetSnapshot(target, host, opts).refusal;
231
+ }
232
+ function validateTarget(target, host, opts) {
233
+ const refusal = validateTargetRefusal(target, host, opts);
234
+ if (refusal !== null)
235
+ throw refusal;
236
+ }
237
+ // Validate a target AND return the materialized one-read snapshot a FORWARDING caller must hand on
238
+ // (R8, review major 23). A boundary that validates a target and then forwards it to a second reader
239
+ // (motion/react on the web leg, or resolveTarget) must forward THIS snapshot, never the caller's raw
240
+ // dictionary: an accessor-backed array member (a getter returning a different value per read) passes
241
+ // validation on its first read yet forwards an UNVALIDATED second read downstream. Each ARRAY member
242
+ // is materialized from one bounded length + one read per own index and frozen BEFORE the value
243
+ // gate runs, so the validators and the forwarded target read the SAME single truth — the target
244
+ // counterpart of validateVariantEntrySnapshot. Non-array members pass through; scalars are already
245
+ // immutable. Refusal-only callers keep validateTargetRefusal (no forward ⇒ no second read).
246
+ function validateTargetSnapshot(target, host, opts) {
247
+ const shapeRefusal = targetShapeRefusal(target, opts);
248
+ if (shapeRefusal !== null)
249
+ return { refusal: shapeRefusal, target: null };
250
+ // READ phase — materialize every array member ONCE, before the value gate reads any element. The
251
+ // accumulator is NULL-PROTOTYPE (review major 24): an own enumerable '__proto__' target key assigned
252
+ // onto an ordinary object invokes the legacy prototype setter and vanishes BEFORE the capability gate
253
+ // runs — a silent empty snapshot with no refusal (G-INV-8 no-silent-failure). On a null-prototype
254
+ // object the assignment creates an own data property, so '__proto__' survives to be refused loudly.
255
+ const snapshot = Object.create(null);
256
+ for (const key of Object.keys(target)) {
257
+ const member = target[key];
258
+ if (!Array.isArray(member)) {
259
+ snapshot[key] = member;
260
+ continue;
261
+ }
262
+ const captured = (0, boundedArray_1.captureBoundedArray)(member);
263
+ if (captured.kind !== 'captured') {
264
+ return {
265
+ refusal: new InvalidTargetError(opts?.componentId, key, (0, boundedArray_1.capturedArrayDescription)(captured), `keyframe arrays must have a safe length between 0 and 100000 (REQ-API-033)`),
266
+ target: null,
267
+ };
268
+ }
269
+ snapshot[key] = captured.values;
270
+ }
271
+ const componentId = opts?.componentId;
272
+ for (const key of Object.keys(snapshot)) {
273
+ if (key === 'transitionEnd') {
274
+ // T23 B3 (pin visual-element-target.ts:38-46/159-168): a target-LEVEL sub-target whose
275
+ // values JUMP (apply instantly, never animate) after the target's animations settle.
276
+ // Validate each entry as a SINGLE value of its capability — keyframe arrays and a nested
277
+ // transitionEnd are meaningless for a jump and refuse loud. The snapshot KEEPS the
278
+ // sub-target: forwarding boundaries (the web leg) hand it to motion/react, which executes
279
+ // it natively; the native boundary destructures it into the settle-jump lane.
280
+ const sub = snapshot[key];
281
+ if (typeof sub !== 'object' || sub === null || Array.isArray(sub)) {
282
+ return {
283
+ refusal: new InvalidTargetError(componentId, key, sub, 'transitionEnd must be a plain object of target keys to JUMP to at settle (T23 B3)'),
284
+ target: null,
285
+ };
286
+ }
287
+ for (const subKey of Object.keys(sub)) {
288
+ const subValue = sub[subKey];
289
+ if (subKey === 'transitionEnd') {
290
+ return {
291
+ refusal: new InvalidTargetError(componentId, 'transitionEnd.transitionEnd', subValue, 'transitionEnd does not nest (T23 B3)'),
292
+ target: null,
293
+ };
294
+ }
295
+ if (Array.isArray(subValue)) {
296
+ return {
297
+ refusal: new InvalidTargetError(componentId, `transitionEnd.${subKey}`, subValue, 'transitionEnd values JUMP at settle — a keyframe array cannot jump (T23 B3)'),
298
+ target: null,
299
+ };
300
+ }
301
+ const { entry: subEntry, unsupported: subUnsupported } = (0, resolve_1.resolveCapabilityVerdict)(host, subKey);
302
+ if (subUnsupported !== null) {
303
+ return {
304
+ refusal: new InvalidTargetError(componentId, `transitionEnd.${subKey}`, subValue, subUnsupported.reason === 'unknown'
305
+ ? 'not a recognized universal-subset property'
306
+ : `not supported on host '${host.id}' — it is available on: ${subUnsupported.supportedOn.join(', ')}`, subUnsupported),
307
+ target: null,
308
+ };
309
+ }
310
+ const subRefusal = validateValueRefusal(componentId, subKey, subEntry, subValue);
311
+ if (subRefusal !== null) {
312
+ // Re-key to the full PATH: a supplying boundary (variants) surfaces this message
313
+ // verbatim, and the bare sub-key would read as the ANIMATED member. The value law
314
+ // itself ran key-aware above (the executable matrix needs the real key).
315
+ return {
316
+ refusal: new InvalidTargetError(componentId, `transitionEnd.${subKey}`, subValue, `refused as a settle-jump value — ${subRefusal.message}`, subRefusal),
317
+ target: null,
318
+ };
319
+ }
320
+ }
321
+ continue;
322
+ }
323
+ const { entry, unsupported } = (0, resolve_1.resolveCapabilityVerdict)(host, key);
324
+ if (unsupported !== null) {
325
+ const reason = unsupported.reason === 'unknown'
326
+ ? `not a recognized universal-subset property`
327
+ : `not supported on host '${host.id}' — it is available on: ${unsupported.supportedOn.join(', ')}`;
328
+ return {
329
+ refusal: new InvalidTargetError(componentId, key, snapshot[key], reason, unsupported),
330
+ target: null,
331
+ };
332
+ }
333
+ const valueRefusal = validateValueRefusal(componentId, key, entry, snapshot[key]);
334
+ if (valueRefusal !== null)
335
+ return { refusal: valueRefusal, target: null };
336
+ }
337
+ return { refusal: null, target: snapshot };
338
+ }
339
+ // Thrown when a transition config is malformed (G-INV-8: unsupported or malformed gesture/animation
340
+ // config fails loudly — never a silent no-op). Mirrors InvalidTargetError's structured shape so
341
+ // tooling can locate the offending option the same way.
342
+ class InvalidTransitionError extends Error {
343
+ key;
344
+ value;
345
+ componentId;
346
+ constructor(componentId, key, value, reason) {
347
+ super(`${componentLabel(componentId)}: invalid transition option '${key}' = ${renderValue(value)} — ${reason}`);
348
+ this.name = 'InvalidTransitionError';
349
+ this.key = key;
350
+ this.value = value;
351
+ this.componentId = componentId;
352
+ }
353
+ }
354
+ exports.InvalidTransitionError = InvalidTransitionError;
355
+ const TRANSITION_NUMBER_OPTIONS = new Set([
356
+ 'duration',
357
+ 'delay',
358
+ 'stiffness',
359
+ 'damping',
360
+ 'mass',
361
+ 'bounce',
362
+ 'visualDuration',
363
+ 'velocity',
364
+ // U7c (REQ-SPRING-014): the authored settle thresholds — finite numbers, no range floor here;
365
+ // a negative value's never-settling spring is caught by the derived-config probe instead.
366
+ 'restDelta',
367
+ 'restSpeed',
368
+ ]);
369
+ const NAMED_EASINGS = new Set(timing_1.NAMED_EASING_KEYS);
370
+ // A SINGLE easing: a named curve, or ONE 4-point cubic-bezier array. The 4-number bezier is one easing,
371
+ // NEVER four segment easings (REQ-API-033 — `isEasingList` disambiguates it from a per-segment list).
372
+ function isValidSingleEasing(value) {
373
+ if (typeof value === 'string')
374
+ return NAMED_EASINGS.has(value);
375
+ if (!Array.isArray(value) || value.length !== 4)
376
+ return false;
377
+ for (let index = 0; index < 4; index++) {
378
+ // Array.prototype.every skips holes. Missing cubic-bezier coordinates are invalid input, never
379
+ // implicit defaults — preserve the exact fail-closed timing shape at every boundary.
380
+ if (!Object.hasOwn(value, index))
381
+ return false;
382
+ const point = value[index];
383
+ if (typeof point !== 'number' || !Number.isFinite(point))
384
+ return false;
385
+ }
386
+ return true;
387
+ }
388
+ function everyPresent(value, predicate) {
389
+ for (let index = 0; index < value.length; index++) {
390
+ // Array.prototype.every skips holes, which previously allowed a sparse offsets/easing list to
391
+ // reach the generator and activate its fallback behavior.
392
+ if (!Object.hasOwn(value, index) || !predicate(value[index]))
393
+ return false;
394
+ }
395
+ return true;
396
+ }
397
+ function firstMissingArrayIndex(value) {
398
+ for (let index = 0; index < value.length; index++) {
399
+ if (!Object.hasOwn(value, index))
400
+ return index;
401
+ }
402
+ return null;
403
+ }
404
+ const TRANSITION_MAP_KEYS = new Set(['default', 'layout', ...types_1.TARGET_PROPERTY_KEYS]);
405
+ // T21 (REQ-API-053): the tree-lane family — legal on a root bag, refused inside per-value map
406
+ // entries (a consumer bag feeds ONE value's generator; orchestration schedules the TREE).
407
+ const ORCHESTRATION_KEY_SET = new Set(types_1.ORCHESTRATION_OPTION_KEYS);
408
+ function isTransitionMapKey(key) {
409
+ return TRANSITION_MAP_KEYS.has(key);
410
+ }
411
+ function validateTransitionMapEntryRefusal(componentId, mapKey, value, rootFlat) {
412
+ if (value === undefined)
413
+ return null;
414
+ const options = componentId === undefined ? undefined : { componentId };
415
+ const shapeRefusal = transitionShapeRefusal(value, options);
416
+ if (shapeRefusal !== null)
417
+ return shapeRefusal;
418
+ const record = value;
419
+ const flat = Object.create(null);
420
+ let inherits = false;
421
+ for (const key of Reflect.ownKeys(record)) {
422
+ const member = record[key];
423
+ if (typeof key !== 'string') {
424
+ return new InvalidTransitionError(componentId, mapKey, value, 'a nested transition bag permits only recognized string option keys (REQ-API-051)');
425
+ }
426
+ if (key === 'inherit') {
427
+ if (member !== undefined && typeof member !== 'boolean') {
428
+ return new InvalidTransitionError(componentId, mapKey + '.inherit', member, 'nested transition inherit must be a boolean (REQ-API-051)');
429
+ }
430
+ inherits = member === true;
431
+ continue;
432
+ }
433
+ if (ORCHESTRATION_KEY_SET.has(key)) {
434
+ return new InvalidTransitionError(componentId, mapKey + '.' + key, member, 'orchestration options live on the tree-level transition bag (element, variant, or ' +
435
+ 'MotionConfig default), never a per-value map entry (REQ-API-053)');
436
+ }
437
+ if (key === 'path') {
438
+ return new InvalidTransitionError(componentId, mapKey + '.path', member, mapKey === 'layout'
439
+ ? 'transition.layout.path is deferred (REQ-API-059 F3) — property-lane arc() only'
440
+ : 'path lives on the element transition bag, never a per-value map entry (REQ-API-059)');
441
+ }
442
+ if (isTransitionMapKey(key)) {
443
+ return new InvalidTransitionError(componentId, mapKey + '.' + key, member, 'nested transition maps do not recurse (REQ-API-051)');
444
+ }
445
+ flat[key] = member;
446
+ }
447
+ if (!inherits) {
448
+ // A replacing child is validated as its own flat bag. Root fields only join a selected child
449
+ // when the authored branch opts into `inherit: true`, matching the pinned getValueTransition path.
450
+ return validateTransitionRefusal(flat, options);
451
+ }
452
+ const merged = Object.create(null);
453
+ for (const key of Reflect.ownKeys(rootFlat))
454
+ merged[key] = rootFlat[key];
455
+ for (const key of Reflect.ownKeys(flat))
456
+ merged[key] = flat[key];
457
+ return validateTransitionRefusal(merged, options);
458
+ }
459
+ /**
460
+ * Eagerly validate a transition config (REQ-API-001 vocabulary, G-INV-8 fail-loud). The runtime
461
+ * authority the compile-time Transition type cannot be: it accepts an untyped input (a
462
+ * dynamically-shaped gesture-state member, a JS caller) and rejects, with a typed error, any shape
463
+ * that is not a plain object of known, well-typed transition options. `undefined` is valid absence.
464
+ */
465
+ function validateTransitionRefusal(transition, opts) {
466
+ if (transition === undefined)
467
+ return null;
468
+ const componentId = opts?.componentId;
469
+ const shapeRefusal = transitionShapeRefusal(transition, opts);
470
+ if (shapeRefusal !== null)
471
+ return shapeRefusal;
472
+ // `transitionShapeRefusal` above establishes the record shape. Keep the local cast after that
473
+ // boundary so every option read below belongs to the immutable validation truth.
474
+ const transitionRecord = transition;
475
+ // Transition shape is closed over ALL own keys, not just enumerable strings. An authored
476
+ // non-enumerable `times` still controls trajectory timing, while a symbol can never name a
477
+ // public option. Capture these keys exactly once into the immutable validation truth.
478
+ const snapshot = Object.create(null);
479
+ for (const key of Reflect.ownKeys(transitionRecord)) {
480
+ const rawValue = transitionRecord[key];
481
+ if (typeof key !== 'string') {
482
+ return new InvalidTransitionError(componentId, String(key), rawValue, 'a transition permits only recognized string option keys (G-INV-8)');
483
+ }
484
+ if ((key === 'times' || key === 'ease' || key === 'easings' || key === 'easing') &&
485
+ Array.isArray(rawValue)) {
486
+ const capture = (0, boundedArray_1.captureBoundedArray)(rawValue);
487
+ if (capture.kind !== 'captured') {
488
+ return new InvalidTransitionError(componentId, key, (0, boundedArray_1.capturedArrayDescription)(capture), `${key} must have a safe bounded array length (REQ-API-033)`);
489
+ }
490
+ const captured = [];
491
+ captured.length = capture.values.length;
492
+ for (let index = 0; index < capture.values.length; index++) {
493
+ if (!Object.hasOwn(capture.values, index))
494
+ continue;
495
+ const member = capture.values[index];
496
+ if (!Array.isArray(member)) {
497
+ captured[index] = member;
498
+ continue;
499
+ }
500
+ const nestedCapture = (0, boundedArray_1.captureBoundedCubicBezier)(member);
501
+ if (nestedCapture.kind !== 'captured') {
502
+ return new InvalidTransitionError(componentId, key, (0, boundedArray_1.capturedArrayDescription)(nestedCapture), `${key} contains an easing that is not a four-point cubic-bezier tuple (REQ-API-033)`);
503
+ }
504
+ captured[index] = nestedCapture.values;
505
+ }
506
+ snapshot[key] = Object.freeze(captured);
507
+ continue;
508
+ }
509
+ snapshot[key] = rawValue;
510
+ }
511
+ for (const key of Reflect.ownKeys(snapshot)) {
512
+ // Symbols returned above before snapshotting; this narrows the closed schema for TypeScript.
513
+ if (typeof key !== 'string')
514
+ continue;
515
+ const value = snapshot[key];
516
+ if (value === undefined)
517
+ continue; // an explicitly-undefined option reads as absent
518
+ if (isTransitionMapKey(key)) {
519
+ const rootFlat = Object.create(null);
520
+ for (const rootKey of Reflect.ownKeys(snapshot)) {
521
+ if (typeof rootKey === 'string' && rootKey !== 'inherit' && !isTransitionMapKey(rootKey)) {
522
+ rootFlat[rootKey] = snapshot[rootKey];
523
+ }
524
+ }
525
+ const mapRefusal = validateTransitionMapEntryRefusal(componentId, key, value, rootFlat);
526
+ if (mapRefusal !== null)
527
+ return mapRefusal;
528
+ continue;
529
+ }
530
+ if (!types_1.TRANSITION_OPTION_KEYS.includes(key)) {
531
+ return new InvalidTransitionError(componentId, key, value, `not a recognized transition option (known: ${types_1.TRANSITION_OPTION_KEYS.join(', ')})`);
532
+ }
533
+ if (key === 'type') {
534
+ // U7a: `false` is the authored instant lane (the pin's makeAnimationInstant — the
535
+ // generator is skipped and the final keyframe commits next frame, delay honored).
536
+ if (value !== 'spring' && value !== 'tween' && value !== false) {
537
+ return new InvalidTransitionError(componentId, key, value, "the guaranteed-subset transition types are 'spring', 'tween', and false");
538
+ }
539
+ continue;
540
+ }
541
+ if (key === 'ease' || key === 'easings' || key === 'easing') {
542
+ // R8 M2-B (REQ-API-033 law d): `ease`/`easings`/`easing` is a SINGLE easing (named curve or one 4-point
543
+ // cubic-bezier) OR — for a keyframe-array target — a PER-SEGMENT list of those. `isEasingList`
544
+ // distinguishes the two: a 4-number bezier is ONE easing (first element numeric), a list's
545
+ // elements are themselves easings. Shape-valid lists may have any count: the generator indexes
546
+ // available members, uses linear for missing members, and ignores extras (pin parity).
547
+ const isList = (0, timing_1.isEasingList)(value);
548
+ const missingIndex = Array.isArray(value) ? firstMissingArrayIndex(value) : null;
549
+ if (missingIndex !== null) {
550
+ return new InvalidTransitionError(componentId, key, value, `ease is missing index ${missingIndex}; sparse timing arrays are invalid (REQ-API-033)`);
551
+ }
552
+ const validCurve = isList
553
+ ? everyPresent(value, isValidSingleEasing)
554
+ : isValidSingleEasing(value);
555
+ if (!validCurve) {
556
+ return new InvalidTransitionError(componentId, key, value, 'ease must be a Motion-named curve (linear, easeIn/Out/InOut, circ*, back*, anticipate), a 4-point ' +
557
+ 'cubic-bezier array, or a per-segment list of those for a keyframe array (REQ-API-033)');
558
+ }
559
+ continue;
560
+ }
561
+ if (key === 'times') {
562
+ // R8 M2-B (REQ-API-033 law d): custom keyframe OFFSETS — finite numbers in [0, 1]. A count
563
+ // mismatch is shape-valid and makes the generator use even offsets, matching the pin.
564
+ const missingIndex = Array.isArray(value) ? firstMissingArrayIndex(value) : null;
565
+ if (missingIndex !== null) {
566
+ return new InvalidTransitionError(componentId, key, value, `times is missing index ${missingIndex}; sparse timing arrays are invalid (REQ-API-033)`);
567
+ }
568
+ const validTimes = Array.isArray(value) &&
569
+ everyPresent(value, (offset) => typeof offset === 'number' && Number.isFinite(offset) && offset >= 0 && offset <= 1);
570
+ if (!validTimes) {
571
+ return new InvalidTransitionError(componentId, key, value, 'times must be an array of finite offsets in [0, 1] (REQ-API-033)');
572
+ }
573
+ continue;
574
+ }
575
+ if (key === 'repeat') {
576
+ // The ONE option that may be non-finite: `Infinity` is the pinned endless spelling, so this
577
+ // key is deliberately outside TRANSITION_NUMBER_OPTIONS' finite check (REQ-API-049).
578
+ if (typeof value !== 'number' || Number.isNaN(value) || value < 0) {
579
+ return new InvalidTransitionError(componentId, key, value, "'repeat' must be a non-negative number of ADDITIONAL plays, or Infinity for endless");
580
+ }
581
+ // H2 (REQ-API-049): a FRACTIONAL repeat is accepted and executed. It used to be refused
582
+ // because the pin runs the partial play and then SNAPS to its final keyframe at finish — its
583
+ // rule tests `repeat % 2 === 1`, which selects only the odd integers, so no fraction takes
584
+ // the first-keyframe branch. The fold now reproduces that commit (REQ-TIMING-003,
585
+ // `repeatGenerator`'s `finalValue`), so no validation refusal stands in for it. The negative
586
+ // / NaN / non-number branch above is a different question and is unchanged.
587
+ continue;
588
+ }
589
+ if (key === 'repeatType') {
590
+ if (value !== 'loop' && value !== 'reverse' && value !== 'mirror') {
591
+ return new InvalidTransitionError(componentId, key, value, "'repeatType' is one of 'loop', 'reverse', or 'mirror'");
592
+ }
593
+ continue;
594
+ }
595
+ if (key === 'repeatDelay') {
596
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
597
+ return new InvalidTransitionError(componentId, key, value, "'repeatDelay' must be a finite, non-negative number of seconds");
598
+ }
599
+ if (!Number.isFinite(value * 1000)) {
600
+ return new InvalidTransitionError(componentId, key, value, 'the seconds→ms crossing overflows to a non-finite repeatDelay (G-INV-8)');
601
+ }
602
+ // The same horizon `delay` and `duration` are bound to. Without it a repeatDelay that is
603
+ // finite after ×1000 but past any reachable time drives `resolvedDuration` so high that
604
+ // `totalDuration` overflows to Infinity — and a FINITE repeat then never completes, silently
605
+ // acquiring the endless repeat's never-settles semantics the author did not ask for.
606
+ if (value * 1000 > constants_1.SETTLE_HORIZON_MS) {
607
+ return new InvalidTransitionError(componentId, key, value, 'the dead time between plays exceeds the settle horizon (2**53 ms) — a repeat that ' +
608
+ 'can never reach its next iteration is not executable (G-INV-8)');
609
+ }
610
+ continue;
611
+ }
612
+ // T21 (REQ-API-053): the orchestration family. `when` is the closed vocabulary — the pinned
613
+ // runtime treats ANY other truthy value as children-first, a silent misauthoring this engine
614
+ // refuses. delayChildren mirrors the shipped `delay` sign law (negative = elapsed-time
615
+ // offset), so it has NO sign floor — only finite-or-function.
616
+ if (key === 'when') {
617
+ if (value !== false && value !== 'beforeChildren' && value !== 'afterChildren') {
618
+ return new InvalidTransitionError(componentId, key, value, "'when' is false, 'beforeChildren', or 'afterChildren' (REQ-API-053)");
619
+ }
620
+ continue;
621
+ }
622
+ if (key === 'delayChildren') {
623
+ const validDelayChildren = typeof value === 'function' || (typeof value === 'number' && Number.isFinite(value));
624
+ if (!validDelayChildren) {
625
+ return new InvalidTransitionError(componentId, key, value, "'delayChildren' is a finite number of seconds (negative = elapsed-time offset, the " +
626
+ 'shipped delay law) or a stagger()-shaped function (index, total) => seconds ' +
627
+ '(REQ-API-053)');
628
+ }
629
+ continue;
630
+ }
631
+ if (key === 'staggerChildren') {
632
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
633
+ return new InvalidTransitionError(componentId, key, value, "'staggerChildren' must be a finite number of seconds per index step (REQ-API-053)");
634
+ }
635
+ continue;
636
+ }
637
+ if (key === 'staggerDirection') {
638
+ if (value !== 1 && value !== -1) {
639
+ return new InvalidTransitionError(componentId, key, value, "'staggerDirection' is 1 (first→last) or -1 (last→first) (REQ-API-053)");
640
+ }
641
+ continue;
642
+ }
643
+ if (key === 'path') {
644
+ if (!(0, motion_arc_1.isNativeArcPath)(value)) {
645
+ return new InvalidTransitionError(componentId, key, value, 'transition.path must be the value returned by arc() (REQ-API-059)');
646
+ }
647
+ continue;
648
+ }
649
+ if (TRANSITION_NUMBER_OPTIONS.has(key)) {
650
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
651
+ return new InvalidTransitionError(componentId, key, value, `'${key}' must be a finite number`);
652
+ }
653
+ // Executable ranges (G-INV-8): the reference-driver probes are the law's evidence —
654
+ // mass/stiffness 0 and visualDuration 0 emit NaN, damping 0 never settles; duration 0
655
+ // and delay 0 execute exactly (kept valid, motion-idiomatic); bounce is the [0, 1]
656
+ // shape contract. A config the driver cannot execute must refuse HERE, never reach it.
657
+ if ((key === 'stiffness' || key === 'damping' || key === 'mass' || key === 'visualDuration') &&
658
+ value <= 0) {
659
+ return new InvalidTransitionError(componentId, key, value, `'${key}' must be positive — a zero/negative value is not executable (the spring ` +
660
+ 'resolution emits non-finite or never-settling output; G-INV-8)');
661
+ }
662
+ // `delay` deliberately has NO sign floor (r6 major 1a8d6f29c44e): pinned Motion
663
+ // implements a negative delay as an elapsed-time offset (start mid-trajectory), and
664
+ // this repo pins that behavior (layoutProp.test.ts negative-delay coverage).
665
+ if (key === 'duration' && value < 0) {
666
+ return new InvalidTransitionError(componentId, key, value, `'${key}' cannot be negative`);
667
+ }
668
+ if (key === 'bounce' && (value < 0 || value > 1)) {
669
+ return new InvalidTransitionError(componentId, key, value, "'bounce' is the [0, 1] shape parameter (0 = no bounce, 1 = extremely bouncy)");
670
+ }
671
+ }
672
+ }
673
+ // T18-a (REQ-API-049): `repeatType`/`repeatDelay` only mean something to a fold that runs. With
674
+ // `repeat` absent or 0 the fold is inert, so they would ride along as silent no-ops — the same
675
+ // law that refuses `delay` on a lane which cannot execute it (G-INV-8).
676
+ const repeatCount = snapshot['repeat'];
677
+ const foldRuns = typeof repeatCount === 'number' && repeatCount > 0;
678
+ if (!foldRuns) {
679
+ for (const companion of ['repeatType', 'repeatDelay']) {
680
+ if (snapshot[companion] !== undefined) {
681
+ return new InvalidTransitionError(componentId, companion, snapshot[companion], `'${companion}' has no effect without a positive 'repeat' — an option this transition ` +
682
+ 'would silently ignore is refused (G-INV-8)');
683
+ }
684
+ }
685
+ }
686
+ // T21 (REQ-API-053): the repeat family's inert-companion law, applied to the stagger axis.
687
+ // `staggerDirection` steers a numeric stagger that must EXIST and be nonzero; a function
688
+ // `delayChildren` (stagger()) displaces the numeric axis entirely in the pinned runtime, so
689
+ // pairing it with either numeric option is dual stagger authorship (G-INV-8 — the
690
+ // ease-alias-pair precedent).
691
+ const delayChildrenValue = snapshot['delayChildren'];
692
+ if (typeof delayChildrenValue === 'function') {
693
+ for (const displaced of ['staggerChildren', 'staggerDirection']) {
694
+ if (snapshot[displaced] !== undefined) {
695
+ return new InvalidTransitionError(componentId, displaced, snapshot[displaced], `a function 'delayChildren' (stagger()) displaces '${displaced}' in the pinned ` +
696
+ 'runtime — authoring both is dual stagger authorship; keep exactly one ' +
697
+ '(REQ-API-053)');
698
+ }
699
+ }
700
+ }
701
+ else if (snapshot['staggerDirection'] !== undefined) {
702
+ const staggerChildrenValue = snapshot['staggerChildren'];
703
+ if (typeof staggerChildrenValue !== 'number' || staggerChildrenValue === 0) {
704
+ return new InvalidTransitionError(componentId, 'staggerDirection', snapshot['staggerDirection'], "'staggerDirection' steers 'staggerChildren'; without a nonzero staggerChildren it is " +
705
+ 'a silent no-op this engine refuses (REQ-API-053)');
706
+ }
707
+ }
708
+ // Family-2 / T20: ease, easings, and easing are exclusive aliases (G-INV-8).
709
+ const easeAliasKeys = ['ease', 'easings', 'easing'].filter((key) => snapshot[key] !== undefined);
710
+ if (easeAliasKeys.length > 1) {
711
+ const extra = easeAliasKeys.includes('easing') ? 'easing' : 'easings';
712
+ return new InvalidTransitionError(componentId, extra, snapshot[extra], `${extra} is an alias of ease — author exactly one of ease, easings, easing (REQ-API-052)`);
713
+ }
714
+ return probeDerivedConfigRefusal(snapshot, componentId);
715
+ }
716
+ /**
717
+ * Shape-only preflight for transition snapshot boundaries. This must run before a null-prototype
718
+ * snapshot can normalize a Date, class instance, or inherited-only record into an empty object.
719
+ */
720
+ function transitionShapeRefusal(transition, opts) {
721
+ if (transition === undefined)
722
+ return null;
723
+ const componentId = opts?.componentId;
724
+ const proto = typeof transition === 'object' && transition !== null
725
+ ? Object.getPrototypeOf(transition)
726
+ : undefined;
727
+ if (typeof transition !== 'object' ||
728
+ transition === null ||
729
+ Array.isArray(transition) ||
730
+ (proto !== Object.prototype && proto !== null)) {
731
+ return new InvalidTransitionError(componentId, '(transition)', transition, 'a transition must be a plain object of transition options (G-INV-8 — a malformed config ' +
732
+ 'fails loud, never coerces to the default transition)');
733
+ }
734
+ return null;
735
+ }
736
+ function validateTransition(transition, opts) {
737
+ const refusal = validateTransitionRefusal(transition, opts);
738
+ if (refusal !== null)
739
+ throw refusal;
740
+ }
741
+ // Sample instants for the derived-config probe: the numeric failure modes are exponential
742
+ // (exp/cos overflow grows with t), so a spread of early-to-late instants catches what a
743
+ // single endpoint sample cannot.
744
+ const PROBE_SAMPLE_INSTANTS_MS = [0, 1, 16, 250, 1000];
745
+ const SPRING_PROBE_OPTIONS = ['stiffness', 'damping', 'mass', 'bounce', 'visualDuration'];
746
+ // The settle law's horizon (r8 3a929cbff062; refined through r13 minor 1d6f4b8a2e90):
747
+ // single-sourced in config/constants and shared verbatim with stepProp's freeze-settle —
748
+ // every accepted trajectory (spring AND tween) must be DONE by it.
749
+ const PROBE_SETTLE_HORIZON_MS = constants_1.SETTLE_HORIZON_MS;
750
+ // Motion's default tween duration when a transition names none.
751
+ const DEFAULT_TWEEN_DURATION_MS = 300;
752
+ /**
753
+ * Probe the DERIVED spring for `transition`; returns the failure reason or null. Runs the
754
+ * SAME conversion seams the engine runs (r7 major c814532d1b66): finite samples at a spread
755
+ * of instants AND bounded settle (r8 major 3a929cbff062 — a permanently-active config is not
756
+ * executable even when every sample is finite).
757
+ */
758
+ // alloc-ok: lifecycle-edge — boundary validation constructs one probe generator per config.
759
+ function springProbeFailure(transition, settleHorizonMs = PROBE_SETTLE_HORIZON_MS) {
760
+ const spring = (0, transition_1.toSpringConfig)(transition);
761
+ const resolved = (0, transition_2.resolveSpring)(spring);
762
+ if (resolved.calculatedDuration !== null && !Number.isFinite(resolved.calculatedDuration)) {
763
+ return 'the derived spring has a non-finite settle duration';
764
+ }
765
+ const generator = (0, transition_2.resolveSpringGenerator)(1, spring)({ from: 0, velocity: spring.velocity ?? 0 });
766
+ for (const at of PROBE_SAMPLE_INSTANTS_MS) {
767
+ const sample = generator.sample(at);
768
+ if (!Number.isFinite(sample.value) || !Number.isFinite(sample.velocity)) {
769
+ return `the derived spring emits a non-finite sample at ${at}ms`;
770
+ }
771
+ }
772
+ const settleAt = resolved.calculatedDuration !== null
773
+ ? Math.min(resolved.calculatedDuration + 1, settleHorizonMs)
774
+ : settleHorizonMs;
775
+ const settled = generator.sample(settleAt);
776
+ if (!Number.isFinite(settled.value) || settled.done !== true) {
777
+ return ('the derived spring is not done by the settle horizon (2**53 ms — the shared bound ' +
778
+ "stepProp's freeze-settle also lands on) — a practically-never-settling config is " +
779
+ 'not executable');
780
+ }
781
+ return null;
782
+ }
783
+ /**
784
+ * The DERIVED-config executability law (r7 major c814532d1b66, G-INV-8): per-option
785
+ * sign/finiteness cannot see what the converters and generators DERIVE. On a probe failure
786
+ * the error LOCALIZES the offense (r8 minor 0c88b6d4e219): each present option re-probes
787
+ * alone and the first solo failure names itself; a pure interaction names every participant.
788
+ */
789
+ // alloc-ok: lifecycle-edge — boundary validation constructs probe generators per config.
790
+ function probeDerivedConfigRefusal(transition, componentId) {
791
+ const springRelevant = transition.type === 'spring' ||
792
+ SPRING_PROBE_OPTIONS.some((option) => transition[option] !== undefined);
793
+ // Family-2 / T20: every ease alias must enter the derived probe under the authored key.
794
+ const easeField = (0, transition_1.effectiveTransitionEase)(transition);
795
+ const easeKey = transition.ease !== undefined ? 'ease' : transition.easings !== undefined ? 'easings' : 'easing';
796
+ const tweenRelevant = transition.type === 'tween' || transition.duration !== undefined || easeField !== undefined;
797
+ // The seconds→ms crossing must stay finite for BOTH duration-family options (r8 major
798
+ // c723a65f4e8b: an Infinity delay holds a layout transition forever; -Infinity seeds an
799
+ // infinite elapsed clock).
800
+ for (const option of ['duration', 'delay']) {
801
+ const value = transition[option];
802
+ if (value !== undefined && !Number.isFinite(value * 1000)) {
803
+ return new InvalidTransitionError(componentId, option, value, `the seconds→ms crossing overflows to a non-finite ${option} (G-INV-8)`);
804
+ }
805
+ }
806
+ // A POSITIVE delay is a wall-clock hold phase (r14 major f3a91c7d5e62): the layout lane
807
+ // waits until delayUntil before any stepping begins, so freeze-settle cannot bound it —
808
+ // the hold itself must fit the settle horizon. Negative delay seeds elapsed instead (no
809
+ // hold; the no-rewind freeze-settle covers any seed).
810
+ if (transition.delay !== undefined && transition.delay * 1000 > constants_1.SETTLE_HORIZON_MS) {
811
+ return new InvalidTransitionError(componentId, 'delay', transition.delay, 'the hold phase exceeds the settle horizon (2**53 ms) — a practically-never-starting ' +
812
+ 'config is not executable (G-INV-8)');
813
+ }
814
+ if (springRelevant) {
815
+ const positiveDelayMs = transition.delay !== undefined && transition.delay > 0 ? transition.delay * 1000 : 0;
816
+ const failure = springProbeFailure(transition, PROBE_SETTLE_HORIZON_MS - positiveDelayMs);
817
+ if (failure !== null) {
818
+ // A delayed spring must finish in the time remaining after its hold phase. Distinguish that
819
+ // combined failure from an intrinsically non-settling spring so the boundary names delay and
820
+ // does not let stepProp freeze a mid-flight sample at the shared horizon (review major n4wse8).
821
+ if (positiveDelayMs > 0 && springProbeFailure(transition, PROBE_SETTLE_HORIZON_MS) === null) {
822
+ return new InvalidTransitionError(componentId, 'delay', transition.delay, 'the positive delay plus the spring exceeds the settle horizon (2**53 ms) — ' +
823
+ 'the animation cannot complete before the shared freeze-settle bound (G-INV-8)');
824
+ }
825
+ // U7c (REQ-SPRING-014): restDelta/restSpeed join the localization list (NOT
826
+ // SPRING_PROBE_OPTIONS — a tween carrying them must never trigger the spring probe; the
827
+ // pin ignores them outside the spring generator). A negative threshold's never-settling
828
+ // spring names itself here.
829
+ const present = [
830
+ ...SPRING_PROBE_OPTIONS,
831
+ 'velocity',
832
+ 'restDelta',
833
+ 'restSpeed',
834
+ ].filter((option) => transition[option] !== undefined);
835
+ for (const option of present) {
836
+ const solo = { type: 'spring', [option]: transition[option] };
837
+ if (springProbeFailure(solo) !== null) {
838
+ return new InvalidTransitionError(componentId, option, transition[option], `${failure} (G-INV-8)`);
839
+ }
840
+ }
841
+ // No option fails alone: the INTERACTION is the offense — name every participant.
842
+ const participants = present
843
+ .map((option) => `${option} = ${String(transition[option])}`)
844
+ .join(', ');
845
+ return new InvalidTransitionError(componentId, '(transition)', Object.fromEntries(present.map((option) => [option, transition[option]])), `${failure}; the interaction of ${participants} is the offense (G-INV-8)`);
846
+ }
847
+ }
848
+ if (tweenRelevant) {
849
+ const durationMs = transition.duration !== undefined ? transition.duration * 1000 : DEFAULT_TWEEN_DURATION_MS;
850
+ // The settle law binds tweens too (r13 major 7f1c9e2a4b60): stepProp's freeze-settle
851
+ // lands on the shared horizon, so a tween still mid-flight there would commit a
852
+ // mid-flight sample as its terminal value — refuse it at the boundary instead.
853
+ if (durationMs > constants_1.SETTLE_HORIZON_MS) {
854
+ return new InvalidTransitionError(componentId, 'duration', transition.duration, 'the derived tween is not done by the settle horizon (2**53 ms) — a ' +
855
+ 'practically-never-settling config is not executable (G-INV-8)');
856
+ }
857
+ const positiveDelayMs = transition.delay !== undefined && transition.delay > 0 ? transition.delay * 1000 : 0;
858
+ if (positiveDelayMs > constants_1.SETTLE_HORIZON_MS - durationMs) {
859
+ return new InvalidTransitionError(componentId, 'delay', transition.delay, 'the positive delay plus the tween exceeds the settle horizon (2**53 ms) — ' +
860
+ 'the animation cannot complete before the shared freeze-settle bound (G-INV-8)');
861
+ }
862
+ // Per-segment ease lists never enter the scalar tween path (`toTimingConfig` drops them).
863
+ // Probe each segment curve directly so finite-but-non-executable cubics fail loud (i73cik).
864
+ if (easeField !== undefined && (0, timing_1.isEasingList)(easeField)) {
865
+ for (const segment of easeField) {
866
+ const easer = (0, timing_1.resolveEasing)(segment);
867
+ for (const progress of [0, 0.25, 0.5, 0.75, 1]) {
868
+ if (!Number.isFinite(easer(progress))) {
869
+ return new InvalidTransitionError(componentId, easeKey, easeField, `the derived per-segment ${easeKey} emits a non-finite sample at progress ${progress} — this ` +
870
+ 'config is not executable (G-INV-8)');
871
+ }
872
+ }
873
+ }
874
+ }
875
+ else {
876
+ const timing = (0, transition_1.toTimingConfig)(transition);
877
+ const generator = (0, timing_1.timingGenerator)(1, timing)({ from: 0, velocity: 0 });
878
+ for (const fraction of [0, 0.25, 0.5, 0.75, 1]) {
879
+ const sample = generator.sample(durationMs * fraction);
880
+ if (!Number.isFinite(sample.value)) {
881
+ return new InvalidTransitionError(componentId, easeKey, easeField, `the derived tween emits a non-finite sample at ${fraction} of the duration — this ` +
882
+ 'config is not executable (G-INV-8)');
883
+ }
884
+ }
885
+ }
886
+ }
887
+ // T18-a L3, moved here from the driver (fix-up review MAJOR). The fold's two degenerate bases —
888
+ // a zero-length play, and a base still moving past the pin's 20s measuring window — were refused
889
+ // only inside `buildRepeatedGenerator`, i.e. at animate time. That is too late for two reasons:
890
+ // the failure arrives as a bare driver-lane Error rather than a typed InvalidTransitionError
891
+ // naming the property, and the parity classifier (which asks THIS function whether a config is
892
+ // executable) certified those configs as shipped surface while the runtime refused them. Both
893
+ // are decidable from the authored transition, so they are decided here. The driver keeps its own
894
+ // check as the defence-in-depth backstop for command shapes that never pass through validation.
895
+ //
896
+ // transition-default-selection F5 (packet §2): the zero-length arm is RETIRED. Its only producer
897
+ // was an authored `duration: 0`, and the pin handles that bag without any fold
898
+ // (`makeAnimationInstant`, motion-dom@12.42.2 motion-value.ts:89-98): without a repeatDelay it
899
+ // INSTANTS — type-independently — so the whole repeat arm is skipped by the gate below; with a
900
+ // repeatDelay the pin totals the delay gaps around zero-length plays and the fold executes.
901
+ // What was a recorded approximation (refuse-loud) is amended under the fidelity presumption; the
902
+ // fold's own `repeatIterationRefusal` remains the backstop for the shape that still cannot run
903
+ // (zero-length plays with nothing separating them — unreachable from an authored bag, which
904
+ // instants instead).
905
+ if (transition.repeat !== undefined &&
906
+ transition.repeat > 0 &&
907
+ !(0, transition_1.isDurationZeroInstantTransition)(transition)) {
908
+ // This function sees a TRANSITION and no TARGET, and the drivers' measurement lane depends on
909
+ // both — a keyframe-array target is played by the keyframes generator unless the transition
910
+ // explicitly asked for a spring. So the same transition can legitimately measure zero (refused)
911
+ // against an array target and non-zero (executed) against a numeric one, and NO answer computed
912
+ // from the transition alone is right for both.
913
+ //
914
+ // That is the same shape as the never-settling arm, and it takes the same resolution: validation
915
+ // refuses only what is refused for EVERY target shape, and the driver — which holds the target —
916
+ // owns the rest. Four review rounds were spent re-deriving a lane rule instead; the rule now
917
+ // lives once in core (`iterationMeasurementLane`) and is evaluated here for both shapes.
918
+ const iterationByLane = (targetIsNumeric) => {
919
+ switch ((0, repeat_1.iterationMeasurementLane)(targetIsNumeric, transition.type === 'spring',
920
+ // REQ-TIMING-006 + F6: an untyped timing-lane bag (duration-only OR ease-only) rides the
921
+ // timing lane, same as the drivers.
922
+ transition.type === 'tween' || (0, transition_1.isTimingLaneTransition)(transition))) {
923
+ case 'keyframes':
924
+ return (0, transition_1.toKeyframesConfig)(transition).duration ?? null;
925
+ case 'timing':
926
+ return (0, transition_1.toTimingConfig)(transition).duration ?? null;
927
+ case 'spring':
928
+ return (0, transition_2.resolveSpring)((0, transition_1.toSpringConfig)(transition)).calculatedDuration;
929
+ }
930
+ };
931
+ // `null` is "only the driver's scan can measure this", never "zero" — so a lane that cannot be
932
+ // measured here can never contribute to a refusal.
933
+ const numericLaneMs = iterationByLane(true);
934
+ const arrayLaneMs = iterationByLane(false);
935
+ // F5: a lane measuring ZERO is exactly an authored `duration: 0` (the spring lane clamps to a
936
+ // live 10ms rather than resolving zero). The instant gate above already routed the
937
+ // no-repeatDelay form out of this arm; the repeatDelay form reaches here and EXECUTES — the
938
+ // pin totals the delay gaps around the zero-length plays — so there is no zero-length refusal
939
+ // left for validation to own.
940
+ // `duration`, `delay` and `repeatDelay` are each bound to the settle horizon; `repeat` is the
941
+ // MULTIPLIER that produces `totalDuration` and was not (review MAJOR). A FINITE repeat past the
942
+ // horizon never completes — `onAnimationComplete` never fires, `whenSettled()` never resolves,
943
+ // and `stepProp`'s precision freeze commits a value short of the target. An ENDLESS repeat is
944
+ // exempt: never settling is its design, not an overflow.
945
+ if (Number.isFinite(transition.repeat)) {
946
+ // The floor must be the SHORTEST iteration this transition could possibly run — across BOTH
947
+ // target shapes, since the lane changes with the target — or it refuses animations that would
948
+ // have completed. Per lane:
949
+ //
950
+ // measured here (`timing`/`keyframes` duration, or a DURATION-resolved spring's
951
+ // `calculatedDuration`) exact and distance-independent, so use it. A duration-resolved
952
+ // spring can be well under a scan step (`duration: 0.01` resolves to 10ms), which is why
953
+ // flooring it at the scan step over-refused (review MINOR).
954
+ // scan-measured (a PHYSICS or `visualDuration` spring, both of which resolve
955
+ // `calculatedDuration === null`) the length depends on the distance, which this function
956
+ // cannot see. The scan reports a multiple of its step, so among iterations that EXECUTE at
957
+ // all one step is the shortest that exists.
958
+ // absent duration on a `timing`/`keyframes` lane both generators default to 300ms.
959
+ //
960
+ // The floor is therefore sound in the never-over-refuse direction, and deliberately partial in
961
+ // the other: a scan-measured spring whose real iteration is far longer than one step can still
962
+ // carry a repeat count that overflows the horizon and be accepted here. The driver is where
963
+ // that becomes observable, and it is the same reason the never-settles arm lives there.
964
+ // A lane measuring ZERO is an authored `duration: 0` (the spring lane clamps to a live 10ms,
965
+ // never zero). Post-F5 that bag EXECUTES: without a repeatDelay it instants (the gate above
966
+ // keeps it out of this arm entirely); with one the pin totals the delay gaps around
967
+ // zero-length plays, so 0 IS the exact, distance-independent floor. Adopting it is no longer
968
+ // the round-5 MAJOR — that bug adopted a zero floor for a clamped 10ms spring lane and so
969
+ // zeroed a REAL total; here the zero-length plays genuinely total only their repeatDelay
970
+ // gaps, and the horizon check stays live on the delay term.
971
+ const laneFloorMs = (laneMs, targetIsNumeric) => {
972
+ if (laneMs !== null && laneMs > 0)
973
+ return laneMs;
974
+ if (laneMs === 0)
975
+ return 0;
976
+ // `null` = this lane is scan-measured, or carries no authored duration.
977
+ return (0, repeat_1.iterationMeasurementLane)(targetIsNumeric, transition.type === 'spring', transition.type === 'tween' || (0, transition_1.isTimingLaneTransition)(transition)) === 'spring'
978
+ ? repeat_1.ITERATION_SCAN_STEP_MS
979
+ : DEFAULT_TWEEN_DURATION_MS;
980
+ };
981
+ const executableLaneFloorsMs = [
982
+ laneFloorMs(numericLaneMs, true),
983
+ laneFloorMs(arrayLaneMs, false),
984
+ ].filter((laneMs) => laneMs !== null);
985
+ // `laneFloorMs` is total (every lane yields a floor), so an empty list is unreachable; the
986
+ // assert stays rather than a default because the obvious default is `0`, and adopting `0`
987
+ // blindly is precisely the round-5 MAJOR — it zeroes the fold total and switches the horizon
988
+ // check off. The zero floor is lawful ONLY through the F5 duration-zero lane above; a silent
989
+ // fallback here would reconstitute that bug the moment either arm's condition drifted.
990
+ if (executableLaneFloorsMs.length === 0) {
991
+ throw new Error('native-motion internal invariant: the settle-horizon arm reached a fold with no ' +
992
+ 'executable lane. Refusing to ' +
993
+ 'default the iteration to 0 — that would disable the horizon check (G-INV-8).');
994
+ }
995
+ const iterationDurationMs = Math.min(...executableLaneFloorsMs);
996
+ const { totalDurationMs } = (0, repeat_1.repeatFoldGeometry)({
997
+ repeat: transition.repeat,
998
+ repeatDelayMs: transition.repeatDelay !== undefined ? transition.repeatDelay * 1000 : 0,
999
+ iterationDurationMs,
1000
+ });
1001
+ if (totalDurationMs > constants_1.SETTLE_HORIZON_MS) {
1002
+ return new InvalidTransitionError(componentId, 'repeat', transition.repeat, 'the repeated animation cannot finish before the settle horizon (2**53 ms) — a finite ' +
1003
+ "repeat that never completes silently acquires the endless repeat's never-settles " +
1004
+ 'semantics, which is not what `repeat` asked for. Use `repeat: Infinity` if the ' +
1005
+ 'animation is meant to run forever (G-INV-8)');
1006
+ }
1007
+ const positiveDelayMs = transition.delay !== undefined && transition.delay > 0 ? transition.delay * 1000 : 0;
1008
+ if (positiveDelayMs > constants_1.SETTLE_HORIZON_MS - totalDurationMs) {
1009
+ return new InvalidTransitionError(componentId, 'delay', transition.delay, 'the positive delay plus the repeated animation exceeds the settle horizon (2**53 ms) — ' +
1010
+ 'a finite repeat cannot complete before the shared freeze-settle bound (G-INV-8)');
1011
+ }
1012
+ }
1013
+ }
1014
+ return null;
1015
+ }