@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,624 @@
1
+ // REQ-API-020/-021/-023 (FLAG 5a, specs/SPEC-GESTURE.md) — the drag prop
2
+ // conversion boundary, SINGLE-SOURCED in core so both engines resolve identical accepted sets
3
+ // (BRIEF "one API, one behavior" / "no forked logic"). Motion's public drag shapes
4
+ // (`dragConstraints {left/right/top/bottom}`, `dragElastic`, `dragSnapPoints {x?/y?}`) convert to
5
+ // core's gesture config here; every incompatible-but-valid shape is a TYPED refusal naming what
6
+ // and why — never a silent ignore. Host-agnostic (REQ-CORE-003): no react/react-native/motion
7
+ // import; the severity model is a plain policy enum + callback, adapted by each engine.
8
+ //
9
+ // Native uses the resolved `ResolvedDragConfig` to build the gesture session; the web engine
10
+ // calls this for the VERDICT (identical accept/refuse) then passes Motion's own shapes through to
11
+ // motion/react and maps `dragSnapPoints` to `dragTransition.modifyTarget` from the same points.
12
+ // Adopted from the PINNED oracle, cited not hand-transcribed (BRIEF numeric-constants law):
13
+ // framer-motion/src/gestures/drag/utils/constraints.ts:195 `export const defaultElastic = 0.35`
14
+ // (motion@12.42.2, source SHA 61833240). Applies when dragConstraints are present and
15
+ // dragElastic is omitted — Motion's own default, so omitted-elastic behaves identically on both
16
+ // engines.
17
+ export const DEFAULT_DRAG_ELASTIC = 0.35;
18
+ // The hook's replacement box is a boundary INPUT, validated identically on both engines
19
+ // (review r7 majors 3): complete — all four sides present numbers; legal — never NaN,
20
+ // infinities only with the correct polarity (the constraint legality vocabulary, G-INV-8);
21
+ // ordered per axis. A malformed replacement is a typed refusal — severity-adapted by the
22
+ // caller (native: the grab/poll report channel; web: the forwarded wrapper, which drops the
23
+ // replacement in production).
24
+ export function validateMeasureReplacementBox(replaced) {
25
+ // TOTAL (review r8 minor 5): a non-object truthy return (number, boolean, string — outside
26
+ // the declared box shape) is the same typed refusal, never a raw property-access TypeError.
27
+ // (Falsy returns — undefined/null/false — are the pin's no-op: `!!userConstraints`.)
28
+ if (typeof replaced !== 'object' || replaced === null) {
29
+ throw new Error(`onMeasureDragConstraints must return a complete { top, left, bottom, right } box of ` +
30
+ `finite numbers — got ${describeValue(replaced)} (REQ-API-042, G-INV-8).`);
31
+ }
32
+ const side = (name, value, polarity) => {
33
+ if (typeof value !== 'number' ||
34
+ Number.isNaN(value) ||
35
+ (!Number.isFinite(value) && value !== polarity * Number.POSITIVE_INFINITY)) {
36
+ throw new Error(`onMeasureDragConstraints must return a complete { top, left, bottom, right } box of ` +
37
+ `finite numbers (an open ${name} side only as ` +
38
+ `${polarity === -1 ? '-Infinity' : '+Infinity'}) — got ${name}: ` +
39
+ `${describeValue(value)} (REQ-API-042).`);
40
+ }
41
+ return value;
42
+ };
43
+ const left = side('left', replaced.left, -1);
44
+ const right = side('right', replaced.right, 1);
45
+ const top = side('top', replaced.top, -1);
46
+ const bottom = side('bottom', replaced.bottom, 1);
47
+ if (left > right || top > bottom) {
48
+ throw new Error(`onMeasureDragConstraints returned inverted bounds (left ${left} > right ${right} or ` +
49
+ `top ${top} > bottom ${bottom}) — an inverted box is a malformed replacement, refused ` +
50
+ '(REQ-API-042, G-INV-8).');
51
+ }
52
+ return { top, left, bottom, right };
53
+ }
54
+ const DRAG_CONTROL_OPTION_KEYS = new Set(['distanceThreshold', 'snapToCursor']);
55
+ /**
56
+ * Runtime-validate DragControls.start options (G-INV-8; seal review r6/r7/r8).
57
+ * Shared by native and web so both engines refuse malformed options identically.
58
+ * Omitted options stay undefined (pin defaults at the session); present keys must match shape.
59
+ * Closed plain-object shape only — unknown keys and non-plain objects refuse loud.
60
+ */
61
+ export function validateDragControlOptions(options) {
62
+ if (options === undefined)
63
+ return undefined;
64
+ if (typeof options !== 'object' ||
65
+ options === null ||
66
+ Array.isArray(options) ||
67
+ Object.getPrototypeOf(options) !== Object.prototype) {
68
+ throw new Error(`DragControls.start options must be a plain object, got ${describeValue(options)} ` +
69
+ '(REQ-GESTURE-026; G-INV-8).');
70
+ }
71
+ const record = options;
72
+ // ONE ownKeys snapshot (seal review r10 major): never call getOwnPropertyNames and
73
+ // getOwnPropertySymbols separately — a Proxy can change between the two reads.
74
+ const ownKeys = Reflect.ownKeys(record);
75
+ for (const key of ownKeys) {
76
+ if (typeof key === 'symbol') {
77
+ throw new Error(`DragControls.start options: symbol keys are not accepted (REQ-GESTURE-026; G-INV-8).`);
78
+ }
79
+ if (!DRAG_CONTROL_OPTION_KEYS.has(key)) {
80
+ throw new Error(`DragControls.start options: unknown key '${key}' — only distanceThreshold and ` +
81
+ `snapToCursor are accepted (REQ-GESTURE-026; G-INV-8).`);
82
+ }
83
+ }
84
+ // ONE snapshot per key (seal review r9 major): never re-read an accessor that can change.
85
+ const out = {};
86
+ if (Object.prototype.hasOwnProperty.call(record, 'distanceThreshold')) {
87
+ const value = record.distanceThreshold;
88
+ if (value !== undefined) {
89
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
90
+ throw new Error(`DragControls.start distanceThreshold must be a finite non-negative number, got ` +
91
+ `${describeValue(value)} (REQ-GESTURE-026; G-INV-8).`);
92
+ }
93
+ out.distanceThreshold = value;
94
+ }
95
+ }
96
+ if (Object.prototype.hasOwnProperty.call(record, 'snapToCursor')) {
97
+ const value = record.snapToCursor;
98
+ if (value !== undefined) {
99
+ if (typeof value !== 'boolean') {
100
+ throw new Error(`DragControls.start snapToCursor must be a boolean, got ` +
101
+ `${describeValue(value)} (REQ-GESTURE-026; G-INV-8).`);
102
+ }
103
+ out.snapToCursor = value;
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ // The dragged axes in recognizer-field order (deterministic for every consumer loop).
109
+ export function resolvedDragAxes(config) {
110
+ return ['x', 'y'].filter((axis) => config[axis] !== undefined);
111
+ }
112
+ // The fixed-at-mount comparison signature (REQ-API-016, shared by BOTH engines so the verdict
113
+ // is identical by construction): the resolved config canonicalized for STABLE equality. The
114
+ // element-ref channel (REQ-API-042) contributes the ref OBJECT'S identity via a process-local
115
+ // token, never its payload — `.current` is a live host element (a DOM node on web: circular
116
+ // under JSON.stringify; a host component ref on device), and the measured box re-resolves per
117
+ // grab regardless (REQ-GESTURE-028). Passing the same ref object every render (the universal
118
+ // useRef pattern) is a NO-OP; swapping the ref object IS a config change → the refusal fires.
119
+ const refIdentityTokens = new WeakMap();
120
+ let nextRefIdentityToken = 0;
121
+ function refIdentityToken(ref) {
122
+ const existing = refIdentityTokens.get(ref);
123
+ if (existing !== undefined)
124
+ return existing;
125
+ const token = (nextRefIdentityToken += 1);
126
+ refIdentityTokens.set(ref, token);
127
+ return token;
128
+ }
129
+ export function dragConfigMountSignature(config) {
130
+ if (config === null)
131
+ return 'null';
132
+ const axis = (axisConfig) => axisConfig === undefined
133
+ ? undefined
134
+ : {
135
+ ...(axisConfig.constraints === undefined ? {} : { constraints: axisConfig.constraints }),
136
+ ...(axisConfig.snap === undefined ? {} : { snap: axisConfig.snap }),
137
+ ...(axisConfig.constraintsRef === undefined
138
+ ? {}
139
+ : {
140
+ constraintsRef: {
141
+ ref: refIdentityToken(axisConfig.constraintsRef.ref),
142
+ elastic: axisConfig.constraintsRef.elastic,
143
+ },
144
+ }),
145
+ };
146
+ return JSON.stringify({
147
+ ...(config.x === undefined ? {} : { x: axis(config.x) }),
148
+ ...(config.y === undefined ? {} : { y: axis(config.y) }),
149
+ dragPropagation: config.dragPropagation,
150
+ dragListener: config.dragListener,
151
+ dragControlsToken: config.dragControlsToken,
152
+ dragDirectionLock: config.dragDirectionLock,
153
+ dragMomentum: config.dragMomentum,
154
+ dragTransition: config.dragTransition,
155
+ dragSnapToOrigin: config.dragSnapToOrigin,
156
+ });
157
+ }
158
+ function describeShape(value) {
159
+ if (value === null)
160
+ return 'null';
161
+ if (Array.isArray(value))
162
+ return 'an array';
163
+ return typeof value;
164
+ }
165
+ // A SAFE value formatter for typed error messages (review minor): the inputs are `unknown`, so
166
+ // `JSON.stringify` would THROW on a BigInt (and drop `undefined`/functions/symbols) — a raw
167
+ // serialization TypeError instead of the typed drag error naming the property. This never throws:
168
+ // primitives stringify directly, BigInt gets an `n` suffix, everything else falls back to the shape.
169
+ // Exported (review r4 minor 844ee5e) so the native drag wiring + handoff binding format recognizer
170
+ // fields through this ONE safe formatter — no duplicated JSON.stringify that a BigInt field crashes.
171
+ export function describeValue(value) {
172
+ // TOTAL even for hostile objects (M3 r2 major 830433fc74f2): the SHAPE probe itself can
173
+ // throw — including an Error's hostile `message` getter or Array.isArray on a REVOKED PROXY
174
+ // — and a throwing formatter turns every typed refusal built over it into a raw crash with
175
+ // zero reports. The catch is the formatter's totality contract, not error suppression: the
176
+ // refusal that carries this description is the loud path.
177
+ try {
178
+ const t = typeof value;
179
+ if (t === 'bigint')
180
+ return `${String(value)}n`;
181
+ if (t === 'string')
182
+ return `"${String(value)}"`;
183
+ if (t === 'number' || t === 'boolean' || t === 'undefined' || t === 'symbol')
184
+ return String(value);
185
+ if (value instanceof Error)
186
+ return value.message;
187
+ return describeShape(value);
188
+ }
189
+ catch {
190
+ return 'an unreadable object (a revoked proxy or a throwing probe trap)';
191
+ }
192
+ }
193
+ // Motion's constraint keys per axis: the drag value is a single axis, so exactly that axis's
194
+ // bound pair applies; the other pair is an axis-inconsistent config (REQ-API-021), refused.
195
+ const AXIS_BOUNDS = {
196
+ x: { lower: 'left', upper: 'right' },
197
+ y: { lower: 'top', upper: 'bottom' },
198
+ };
199
+ function requireFiniteBound(value, key) {
200
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
201
+ throw new Error(`dragConstraints.${key} must be a finite number, got ${describeValue(value)} (REQ-API-021).`);
202
+ }
203
+ return value;
204
+ }
205
+ // Motion's dragElastic: number in [0, 1]; R15 D5 also ships boolean (true → pin 0.35, false → 0).
206
+ // Per-side object form stays deferred until a demand case. Omitted = pinned oracle default (0.35).
207
+ function resolveElastic(elastic) {
208
+ if (elastic === undefined)
209
+ return DEFAULT_DRAG_ELASTIC;
210
+ // R15 D5 (REQ-API-045): bare true/false are the pin's boolean forms.
211
+ if (elastic === true)
212
+ return DEFAULT_DRAG_ELASTIC;
213
+ if (elastic === false)
214
+ return 0;
215
+ if (typeof elastic !== 'number') {
216
+ throw new Error(`dragElastic ${describeShape(elastic)} forms (per-side object) are deferred — ` +
217
+ 'a number in [0, 1] or boolean is the D5 shape (REQ-API-045).');
218
+ }
219
+ if (!Number.isFinite(elastic) || elastic < 0 || elastic > 1) {
220
+ throw new Error(`dragElastic must be in [0, 1], got ${elastic} (REQ-API-021).`);
221
+ }
222
+ return elastic;
223
+ }
224
+ // The constraints record shape checks shared by every drag form (review major: a typo like
225
+ // `typoBound` must fail loud, never be an accepted-but-ignored config — REQ-API-013/-015,
226
+ // G-INV-8). The numeric form accepts only the four bound keys; the ref form is recognized
227
+ // structurally by `current` and may carry unrelated metadata (review r9 minor 2), never a
228
+ // bound key. Single-axis resolution catches the wrong-axis pair below with a more specific
229
+ // message.
230
+ function requireConstraintsRecord(constraints) {
231
+ if (typeof constraints !== 'object' || constraints === null || Array.isArray(constraints)) {
232
+ throw new Error(`dragConstraints must be a { left/right/top/bottom } object or an element ref ({ current }), got ${describeShape(constraints)} (REQ-API-020/-042).`);
233
+ }
234
+ const record = constraints;
235
+ const keys = Object.keys(record);
236
+ if ('current' in record) {
237
+ // REQ-API-042 (R15 D2): the element-ref form ships — recognized STRUCTURALLY by `current`
238
+ // (upstream Motion's isRefObject). The exclusivity law prohibits mixing `current` with
239
+ // NUMERIC BOUNDS (left/right/top/bottom) — never merged into one config (fail-loud,
240
+ // REQ-API-015); unrelated ref metadata rides along ignored (review r9 minor 2 — upstream
241
+ // recognizes a ref by `current` alone, so a metadata-carrying ref is valid input).
242
+ for (const key of keys) {
243
+ if (key === 'left' || key === 'right' || key === 'top' || key === 'bottom') {
244
+ throw new Error("dragConstraints with 'current' is the element-ref form — it never merges with numeric " +
245
+ `bounds (got '${key}' alongside); pass either a ref ({ current }) or { left/right/top/bottom } (REQ-API-042).`);
246
+ }
247
+ }
248
+ return record;
249
+ }
250
+ for (const key of keys) {
251
+ if (key !== 'left' && key !== 'right' && key !== 'top' && key !== 'bottom') {
252
+ throw new Error(`dragConstraints has an unknown key '${key}' — only { left, right, top, bottom } or an ` +
253
+ 'element ref ({ current }) are valid (REQ-API-015/-042); an unrecognized bound is a typo, refused not ignored.');
254
+ }
255
+ }
256
+ return record;
257
+ }
258
+ // Narrow a validated constraints record to the element-ref form (REQ-API-042): `requireConstraintsRecord`
259
+ // already refused any numeric bound key alongside `current`, so the presence check IS the
260
+ // discrimination (unrelated metadata is tolerated — review r9 minor 2). The resolved config
261
+ // carries this object BY IDENTITY — the grab-time measure reads `.current` lazily
262
+ // (native-motion's per-grab measure channel); a re-wrap would freeze the pre-commit null.
263
+ function isConstraintsRefRecord(record) {
264
+ return 'current' in record;
265
+ }
266
+ // Resolve one axis's bound pair to core's `DragConstraints {min, max, elastic}`. Presence =
267
+ // defined value, not `key in record` (exactOptionalPropertyTypes + Motion parity):
268
+ // `{ left: 0, right: undefined }` is a legal public shape meaning "right omitted / open upper".
269
+ // One-sided numeric form (REQ-API-024 shipping slice): open side is ±Infinity so
270
+ // applyConstraints skips that side. Authored bounds still require finite numbers.
271
+ function resolveAxisBounds(axis, lowerRaw, upperRaw, elastic) {
272
+ const bounds = AXIS_BOUNDS[axis];
273
+ const hasLower = lowerRaw !== undefined;
274
+ const hasUpper = upperRaw !== undefined;
275
+ const min = hasLower ? requireFiniteBound(lowerRaw, bounds.lower) : Number.NEGATIVE_INFINITY;
276
+ const max = hasUpper ? requireFiniteBound(upperRaw, bounds.upper) : Number.POSITIVE_INFINITY;
277
+ if (hasLower && hasUpper && min > max) {
278
+ throw new Error(`dragConstraints.${bounds.lower} (${min}) must be <= dragConstraints.${bounds.upper} (${max}) (REQ-API-021).`);
279
+ }
280
+ return { min, max, elastic: resolveElastic(elastic) };
281
+ }
282
+ // Convert Motion's `dragConstraints {left/right/top/bottom}` + `dragElastic` to core's
283
+ // `DragConstraints {min, max, elastic}` for the declared axis.
284
+ function resolveConstraints(axis, constraints, elastic) {
285
+ const record = requireConstraintsRecord(constraints);
286
+ const bounds = AXIS_BOUNDS[axis];
287
+ const other = AXIS_BOUNDS[axis === 'x' ? 'y' : 'x'];
288
+ // Same defined-value presence as open-side omission (review major 4ec3a607f2bd): public types
289
+ // mark cross-axis keys as `?: undefined`, so `{ left: 0, top: undefined }` is a legal pass-through
290
+ // shape. `key in record` would refuse it; only a DEFINED off-axis number is inconsistent.
291
+ if (record[other.lower] !== undefined || record[other.upper] !== undefined) {
292
+ throw new Error(`dragConstraints declares ${other.lower}/${other.upper} but drag is '${axis}' — bounds for ` +
293
+ 'a non-dragged axis are inconsistent with the declared axis and are refused, never ' +
294
+ 'ignored (REQ-API-021).');
295
+ }
296
+ const lowerRaw = record[bounds.lower];
297
+ const upperRaw = record[bounds.upper];
298
+ if (lowerRaw === undefined && upperRaw === undefined) {
299
+ throw new Error(`dragConstraints declares no bound for axis '${axis}' — expected ${bounds.lower} and/or ` +
300
+ `${bounds.upper} (REQ-API-020; one-sided numeric form ships).`);
301
+ }
302
+ return resolveAxisBounds(axis, lowerRaw, upperRaw, elastic);
303
+ }
304
+ // R15 D1 (REQ-API-041 / REQ-GESTURE-024): both-axes constraints — every bound key is legal and
305
+ // each axis resolves independently; an axis with no defined bound drags free (no channel entry).
306
+ function resolveBothAxesConstraints(constraints, elastic) {
307
+ const record = requireConstraintsRecord(constraints);
308
+ // ONE read per bound, captured before presence checks AND resolution (review round-12 major):
309
+ // an accessor-backed bound must be checked and resolved from the SAME snapshot.
310
+ const left = record['left'];
311
+ const right = record['right'];
312
+ const top = record['top'];
313
+ const bottom = record['bottom'];
314
+ const hasX = left !== undefined || right !== undefined;
315
+ const hasY = top !== undefined || bottom !== undefined;
316
+ if (!hasX && !hasY) {
317
+ throw new Error('dragConstraints declares no bound for either axis — expected at least one of ' +
318
+ 'left/right/top/bottom (REQ-API-020/-041; one-sided numeric form ships).');
319
+ }
320
+ return {
321
+ ...(hasX ? { x: resolveAxisBounds('x', left, right, elastic) } : {}),
322
+ ...(hasY ? { y: resolveAxisBounds('y', top, bottom, elastic) } : {}),
323
+ };
324
+ }
325
+ // dragSnapPoints — the ratified Native-Motion extension: ABSOLUTE positions, one array per axis.
326
+ function resolveSnap(axis, snap) {
327
+ if (typeof snap !== 'object' || snap === null || Array.isArray(snap)) {
328
+ throw new Error(`dragSnapPoints must be an object with per-axis arrays ({x?, y?}), got ${describeShape(snap)} (REQ-API-020).`);
329
+ }
330
+ const record = snap;
331
+ // Reject UNKNOWN keys (review major): a typo like `typoAxis` must fail loud, never be an
332
+ // accepted-but-ignored config (REQ-API-013/-015, G-INV-8). Only `x`/`y` are valid axes.
333
+ for (const key of Object.keys(record)) {
334
+ if (key !== 'x' && key !== 'y') {
335
+ throw new Error(`dragSnapPoints has an unknown key '${key}' — only per-axis arrays { x?, y? } are valid ` +
336
+ '(REQ-API-015); an unrecognized axis is a typo, refused not ignored.');
337
+ }
338
+ }
339
+ const declaredAxes = ['x', 'y'].filter((key) => record[key] !== undefined);
340
+ if (declaredAxes.length === 0) {
341
+ throw new Error('dragSnapPoints declares no axis — expected absolute snap positions under x and/or y (REQ-API-020).');
342
+ }
343
+ const offAxis = declaredAxes.find((key) => key !== axis);
344
+ if (offAxis !== undefined) {
345
+ throw new Error(`dragSnapPoints.${offAxis} is declared but drag is '${axis}' — snap points for a ` +
346
+ 'non-dragged axis are refused, never ignored (REQ-API-021).');
347
+ }
348
+ const points = record[axis];
349
+ if (!Array.isArray(points)) {
350
+ throw new Error(`dragSnapPoints.${axis} must be an array of finite numbers, got ${describeShape(points)} (REQ-API-020).`);
351
+ }
352
+ if (points.length === 0) {
353
+ throw new Error(`dragSnapPoints.${axis} must declare at least one snap point (REQ-API-020).`);
354
+ }
355
+ for (const point of points) {
356
+ if (typeof point !== 'number' || !Number.isFinite(point)) {
357
+ // describeValue (safe) not String() (review r5 major 21): String(Object.create(null)) throws
358
+ // a raw "No default value" TypeError; the typed error must name the field for any bad member.
359
+ throw new Error(`dragSnapPoints.${axis} must contain only finite numbers, got ${describeValue(point)} (REQ-API-020).`);
360
+ }
361
+ }
362
+ // SNAPSHOT the points (review r8 major 25): return a COPY, never the caller's array by reference.
363
+ // The resolved config is retained as the fixed-at-mount config and compared against re-resolves; an
364
+ // aliased array would let a later in-place mutation change the "mounted" config too (comparison
365
+ // bypassed → dynamic gestures silently enable, post-mount out-of-constraints validation bypassed, and
366
+ // native — which drives the gesture session off this array — diverges from web). One copy at the
367
+ // boundary makes every downstream holder independent of the caller.
368
+ return { points: [...points] };
369
+ }
370
+ // The measure hook requires the element-ref constraints form (R15 D2 review r3 major 1): the
371
+ // pin fires it only inside resolveRefConstraints — with numeric or absent constraints it would
372
+ // be a silently dead callback (G-INV-8). Called after constraints resolution in every drag
373
+ // branch; the function SHAPE stays entry-gated (a malformed callback drops there).
374
+ function requireMeasureHookHasRefConstraints(onMeasureDragConstraints, constraintsRef) {
375
+ if (onMeasureDragConstraints !== undefined && constraintsRef === undefined) {
376
+ throw new Error('onMeasureDragConstraints requires an element-ref dragConstraints ({ current }) — the pin ' +
377
+ 'fires the measure hook only inside resolveRefConstraints; with numeric or absent ' +
378
+ 'constraints it would be a silently dead callback (REQ-API-042, G-INV-8).');
379
+ }
380
+ }
381
+ function isDragControlsHandle(value) {
382
+ if (typeof value !== 'object' || value === null)
383
+ return false;
384
+ const hub = value;
385
+ // subscribe is required: web motion.div and native MotionView both subscribe; a structural
386
+ // start/cancel/stop-only object is not a hub (seal review r8 major — shared parity).
387
+ return (typeof hub['start'] === 'function' &&
388
+ typeof hub['cancel'] === 'function' &&
389
+ typeof hub['stop'] === 'function' &&
390
+ typeof hub['subscribe'] === 'function');
391
+ }
392
+ function resolveOrThrow(input) {
393
+ // ONE read per member, captured at the boundary (reviews round-8 / round-11): a mutating
394
+ // getter must validate and resolve the SAME snapshot — never two reads that can disagree.
395
+ const { drag, dragConstraints, dragElastic, dragSnapPoints, dragPropagation, onMeasureDragConstraints, dragControls, dragListener, dragDirectionLock, dragMomentum, dragTransition, dragSnapToOrigin, } = input;
396
+ if (drag === undefined) {
397
+ // Config without the capability is refused, never ignored.
398
+ const orphan = dragConstraints !== undefined
399
+ ? 'dragConstraints'
400
+ : dragElastic !== undefined
401
+ ? 'dragElastic'
402
+ : dragSnapPoints !== undefined
403
+ ? 'dragSnapPoints'
404
+ : onMeasureDragConstraints !== undefined
405
+ ? 'onMeasureDragConstraints'
406
+ : dragControls !== undefined
407
+ ? 'dragControls'
408
+ : dragListener !== undefined
409
+ ? 'dragListener'
410
+ : dragDirectionLock !== undefined
411
+ ? 'dragDirectionLock'
412
+ : dragMomentum !== undefined
413
+ ? 'dragMomentum'
414
+ : dragTransition !== undefined
415
+ ? 'dragTransition'
416
+ : dragSnapToOrigin !== undefined
417
+ ? 'dragSnapToOrigin'
418
+ : 'dragPropagation';
419
+ throw new Error(`${orphan} requires drag to be 'x', 'y', or true (REQ-API-021).`);
420
+ }
421
+ if (dragPropagation !== undefined && typeof dragPropagation !== 'boolean') {
422
+ throw new Error(`dragPropagation must be a boolean, got ${describeValue(dragPropagation)} (REQ-API-021).`);
423
+ }
424
+ // R15 D3 (REQ-API-043 / G-INV-8): shared severity boundary — never accept-but-inert.
425
+ if (dragListener !== undefined && typeof dragListener !== 'boolean') {
426
+ throw new Error(`dragListener must be a boolean, got ${describeValue(dragListener)} (REQ-API-043).`);
427
+ }
428
+ if (dragControls !== undefined && !isDragControlsHandle(dragControls)) {
429
+ throw new Error(`dragControls must be a DragControls hub (start/cancel/stop/subscribe), got ${describeValue(dragControls)} (REQ-API-043).`);
430
+ }
431
+ // R15 D4 (REQ-API-044): direction lock is boolean; omitted === false.
432
+ if (dragDirectionLock !== undefined && typeof dragDirectionLock !== 'boolean') {
433
+ throw new Error(`dragDirectionLock must be a boolean, got ${describeValue(dragDirectionLock)} (REQ-API-044).`);
434
+ }
435
+ // R15 D5 (REQ-API-045): release physics props.
436
+ if (dragMomentum !== undefined && typeof dragMomentum !== 'boolean') {
437
+ throw new Error(`dragMomentum must be a boolean, got ${describeValue(dragMomentum)} (REQ-API-045).`);
438
+ }
439
+ if (dragSnapToOrigin !== undefined &&
440
+ dragSnapToOrigin !== true &&
441
+ dragSnapToOrigin !== false &&
442
+ dragSnapToOrigin !== 'x' &&
443
+ dragSnapToOrigin !== 'y') {
444
+ throw new Error(`dragSnapToOrigin must be boolean | 'x' | 'y', got ${describeValue(dragSnapToOrigin)} (REQ-API-045).`);
445
+ }
446
+ let resolvedDragTransition = null;
447
+ if (dragTransition !== undefined) {
448
+ if (dragTransition === null ||
449
+ typeof dragTransition !== 'object' ||
450
+ Array.isArray(dragTransition)) {
451
+ throw new Error(`dragTransition must be an object with bounceStiffness/bounceDamping numbers, got ${describeValue(dragTransition)} (REQ-API-045).`);
452
+ }
453
+ const t = dragTransition;
454
+ // Allowlist only bounce fields (seal H3): unknown keys refuse, never silent strip (G-INV-8).
455
+ for (const key of Object.keys(t)) {
456
+ if (key !== 'bounceStiffness' && key !== 'bounceDamping') {
457
+ throw new Error(`dragTransition unknown key '${key}' — only bounceStiffness/bounceDamping ship at D5; ` +
458
+ 'other InertiaOptions stay deferred (REQ-API-045 / G-INV-8).');
459
+ }
460
+ }
461
+ if (t.bounceStiffness !== undefined && typeof t.bounceStiffness !== 'number') {
462
+ throw new Error(`dragTransition.bounceStiffness must be a number, got ${describeValue(t.bounceStiffness)} (REQ-API-045).`);
463
+ }
464
+ if (t.bounceDamping !== undefined && typeof t.bounceDamping !== 'number') {
465
+ throw new Error(`dragTransition.bounceDamping must be a number, got ${describeValue(t.bounceDamping)} (REQ-API-045).`);
466
+ }
467
+ if ((typeof t.bounceStiffness === 'number' && !Number.isFinite(t.bounceStiffness)) ||
468
+ (typeof t.bounceDamping === 'number' && !Number.isFinite(t.bounceDamping))) {
469
+ throw new Error(`dragTransition bounceStiffness/bounceDamping must be finite numbers (REQ-API-045).`);
470
+ }
471
+ // R16 review minor 7: the bounce pair names the inertia generator's boundary spring
472
+ // (REQ-GESTURE-027). The pin accepts 0, but core's solver degenerates on a non-positive pair
473
+ // (a silent NaN trajectory on the external channel, a late release-time throw on the host
474
+ // path) — refuse it HERE, at mount, the fail-loud altitude (a tightening, not a parity break).
475
+ if ((typeof t.bounceStiffness === 'number' && t.bounceStiffness <= 0) ||
476
+ (typeof t.bounceDamping === 'number' && t.bounceDamping <= 0)) {
477
+ throw new Error(`dragTransition bounceStiffness/bounceDamping must be greater than zero, got ` +
478
+ `bounceStiffness=${describeValue(t.bounceStiffness)} bounceDamping=${describeValue(t.bounceDamping)} (REQ-API-045).`);
479
+ }
480
+ resolvedDragTransition = {
481
+ ...(typeof t.bounceStiffness === 'number' ? { bounceStiffness: t.bounceStiffness } : {}),
482
+ ...(typeof t.bounceDamping === 'number' ? { bounceDamping: t.bounceDamping } : {}),
483
+ };
484
+ }
485
+ // Canonicalize omission vs the explicit default (review round-3 minor): omitted and
486
+ // `dragPropagation={false}` are the SAME configuration — the fixed-at-mount comparison must
487
+ // never split them (the dragElastic default-normalization precedent).
488
+ // R15 D3: dragListener default true (pin); dragControls identity via WeakMap token.
489
+ // R15 D4: dragDirectionLock default false.
490
+ // R15 D5: dragMomentum default true (pin); dragSnapToOrigin default false; dragTransition null.
491
+ const hubFields = {
492
+ dragPropagation: dragPropagation === true,
493
+ dragListener: dragListener !== false,
494
+ dragControlsToken: dragControls !== undefined && typeof dragControls === 'object' && dragControls !== null
495
+ ? refIdentityToken(dragControls)
496
+ : null,
497
+ dragDirectionLock: dragDirectionLock === true,
498
+ dragMomentum: dragMomentum !== false,
499
+ dragTransition: resolvedDragTransition,
500
+ dragSnapToOrigin: dragSnapToOrigin === true || dragSnapToOrigin === 'x' || dragSnapToOrigin === 'y'
501
+ ? dragSnapToOrigin
502
+ : false,
503
+ };
504
+ if (dragElastic !== undefined) {
505
+ // Validate even when constraints are absent (U7f no-op uses the value only if
506
+ // constraints exist). Out-of-range / per-side object still refuse.
507
+ resolveElastic(dragElastic);
508
+ }
509
+ if (drag === false) {
510
+ return hubFields;
511
+ }
512
+ if (drag === true) {
513
+ // R15 D1 (REQ-API-041): both-axes drag ships. Snap stays refused with it: the web mapping
514
+ // projects release targets through Motion's single axis-blind modifyTarget (REQ-WEB-013),
515
+ // which per-axis snap sets cannot pass faithfully — a recorded fidelity divergence deferred
516
+ // to its own rung, identical on both engines by construction (this single verdict).
517
+ if (dragSnapPoints !== undefined) {
518
+ throw new Error('dragSnapPoints with both-axes drag (drag={true}) is deferred — the release ' +
519
+ "projection routes through Motion's single axis-blind modifyTarget, which cannot " +
520
+ 'express per-axis snap sets faithfully (REQ-WEB-013; recorded fidelity divergence, ' +
521
+ 'its own rung).');
522
+ }
523
+ const constraintsRecord = dragConstraints !== undefined ? requireConstraintsRecord(dragConstraints) : undefined;
524
+ const constraintsRef = constraintsRecord !== undefined && isConstraintsRefRecord(constraintsRecord)
525
+ ? { ref: constraintsRecord, elastic: resolveElastic(dragElastic) }
526
+ : undefined;
527
+ const both = constraintsRecord !== undefined && constraintsRef === undefined
528
+ ? resolveBothAxesConstraints(constraintsRecord, dragElastic)
529
+ : undefined;
530
+ requireMeasureHookHasRefConstraints(onMeasureDragConstraints, constraintsRef);
531
+ return {
532
+ x: constraintsRef !== undefined
533
+ ? { constraintsRef }
534
+ : both?.x === undefined
535
+ ? {}
536
+ : { constraints: both.x },
537
+ y: constraintsRef !== undefined
538
+ ? { constraintsRef }
539
+ : both?.y === undefined
540
+ ? {}
541
+ : { constraints: both.y },
542
+ ...hubFields,
543
+ };
544
+ }
545
+ if (drag !== 'x' && drag !== 'y') {
546
+ throw new Error(`drag must be 'x', 'y', true, or false, got ${describeValue(drag)} (REQ-API-020/-023).`);
547
+ }
548
+ const constraintsRecord = dragConstraints !== undefined ? requireConstraintsRecord(dragConstraints) : undefined;
549
+ const constraintsRef = constraintsRecord !== undefined && isConstraintsRefRecord(constraintsRecord)
550
+ ? { ref: constraintsRecord, elastic: resolveElastic(dragElastic) }
551
+ : undefined;
552
+ // Ref + snap is REFUSED identically on both engines (review r7 major 2): the pin's web
553
+ // engine applies its axis-blind modifyTarget with NO measurement-time bounds check, while
554
+ // native's law is the typed grab-time contradiction refusal (r5 major 20) — the two engines
555
+ // cannot validate the combination identically, so both refuse it at resolve (recorded
556
+ // fidelity divergence, its own rung — the both-axes snap precedent). The wiring keeps a
557
+ // grab-time snap-vs-measured check as the deep defense for host-constructed configs.
558
+ if (constraintsRef !== undefined && dragSnapPoints !== undefined) {
559
+ throw new Error("dragSnapPoints with element-ref dragConstraints is deferred — the pin's web engine " +
560
+ 'applies its axis-blind modifyTarget with no measurement-time bounds check, so the ' +
561
+ 'snap-vs-bounds contradiction law cannot run identically on both engines; the ' +
562
+ 'combination is refused on BOTH (recorded fidelity divergence, its own rung).');
563
+ }
564
+ const constraints = constraintsRecord !== undefined && constraintsRef === undefined
565
+ ? resolveConstraints(drag, constraintsRecord, dragElastic)
566
+ : undefined;
567
+ requireMeasureHookHasRefConstraints(onMeasureDragConstraints, constraintsRef);
568
+ // Free-drag (REQ-GESTURE-023): snap is optional. When present, resolve axis/shape; when absent,
569
+ // release uses projected landing (optionally constrained). Empty/wrong-axis snap still fails in
570
+ // resolveSnap.
571
+ const snap = dragSnapPoints !== undefined ? resolveSnap(drag, dragSnapPoints) : undefined;
572
+ // A snap point OUTSIDE the declared hard constraints is a CONTRADICTORY config (review r5 major 20,
573
+ // REQ-GESTURE-015): selectReleaseTarget returns the projected-then-snapped point UNCLAMPED when the
574
+ // release is within bounds, so an out-of-bounds snap would settle the box outside the allowed
575
+ // region — and diverge from the web engine's constrained inertia path. Reject the incompatible set
576
+ // at THIS boundary (both engines call resolveDragConfig) so every terminal state stays within the
577
+ // hard bounds by construction. Bounds are inclusive: a snap AT min/max is valid. The ref form's
578
+ // bounds are measured per grab — snap-vs-ref-bounds validates at the grab-time measure instead.
579
+ if (constraints !== undefined && snap !== undefined) {
580
+ for (const point of snap.points) {
581
+ if (point < constraints.min || point > constraints.max) {
582
+ throw new Error(`dragSnapPoints.${drag} contains ${point}, outside the declared dragConstraints ` +
583
+ `[${constraints.min}, ${constraints.max}] — a snap outside the hard bounds would settle ` +
584
+ 'out of range; every snap point must lie within the constraints (REQ-GESTURE-015).');
585
+ }
586
+ }
587
+ }
588
+ const axisConfig = {
589
+ ...(constraints === undefined ? {} : { constraints }),
590
+ ...(constraintsRef === undefined ? {} : { constraintsRef }),
591
+ ...(snap === undefined ? {} : { snap }),
592
+ };
593
+ return drag === 'x' ? { x: axisConfig, ...hubFields } : { y: axisConfig, ...hubFields };
594
+ }
595
+ // REQ-API-016: an onDrag* callback on an element with no `drag` axis would be an
596
+ // accepted-but-ignored prop — it can never fire. Refused under the severity law, never silent.
597
+ export function gateOrphanDragCallbacks(callbackName, severity, report) {
598
+ const error = new Error(`${callbackName} requires drag to be 'x', 'y', or true — a drag callback without a drag axis can ` +
599
+ 'never fire and is refused, never silently ignored (REQ-API-016/-021).');
600
+ if (severity === 'development')
601
+ throw error;
602
+ report(error);
603
+ }
604
+ // The resolver catches values from user-owned proxy traps as well as ordinary Errors. Normalize
605
+ // every catch at this boundary; even an Error instance can have a hostile `message` getter.
606
+ // `describeValue` owns every property read under its total guard, so both severity lanes retain
607
+ // their typed drag refusal instead of leaking a secondary coercion failure.
608
+ function normalizedResolverError(error) {
609
+ return new Error(`drag configuration resolution failed: ${describeValue(error)} (REQ-API-021).`);
610
+ }
611
+ // The severity gate (REQ-API-021): development throws through; production reports the typed error
612
+ // and refuses the whole gesture surface for this element (null) — fail-closed, no partial config.
613
+ export function resolveDragConfig(input, severity, report) {
614
+ try {
615
+ return resolveOrThrow(input);
616
+ }
617
+ catch (error) {
618
+ const normalized = normalizedResolverError(error);
619
+ if (severity === 'development')
620
+ throw normalized;
621
+ report(normalized);
622
+ return null;
623
+ }
624
+ }