@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,95 @@
1
+ "use strict";
2
+ // SPEC-GESTURE §2 — the projection function, project-then-snap selection, velocity normalization, and the
3
+ // shared release-target selector. Pure functions over plain numbers (REQ-GESTURE-010/016/017/019); no graph,
4
+ // no clock, no host. Host-agnostic (REQ-CORE-003): only a type import.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PROJECTION_POWER = void 0;
7
+ exports.project = project;
8
+ exports.selectSnap = selectSnap;
9
+ exports.normalizeVelocity = normalizeVelocity;
10
+ exports.absoluteDragOrigin = absoluteDragOrigin;
11
+ exports.selectReleaseTarget = selectReleaseTarget;
12
+ const elastic_1 = require("./elastic.cjs");
13
+ // The default projection power = the pinned `motion` 12.42.2 inertia `power` default (verified `power = 0.8`
14
+ // in node_modules/motion-dom/dist). The M1 selection/monotonicity invariants only need `power > 0`, so this
15
+ // exact value binds cross-engine PARITY (scenario 12, M2), not the M1 scenarios. A different value is a
16
+ // recorded divergence (REQ-GESTURE-019).
17
+ exports.PROJECTION_POWER = 0.8;
18
+ // P(origin, v) = origin + power·v (REQ-GESTURE-019, Motion's `power · velocity` model). The projected landing
19
+ // point the snap selector targets; `v = 0` collapses `ideal` to `origin` (nearest-to-release).
20
+ function project(origin, velocity, power = exports.PROJECTION_POWER) {
21
+ return origin + power * velocity;
22
+ }
23
+ // argmin_{s ∈ points} |s − ideal| (REQ-GESTURE-017). Fails loud on an empty set (G-INV-8) — a snap drag with
24
+ // no targets is a config error, never a silent no-op. Exact-midpoint ties resolve toward the SMALLER snap
25
+ // value, ORDER-INDEPENDENT — matching the web mapping (`native-motion-web` nearestSnap) byte-for-byte so
26
+ // the two engines never disagree on a tie (G-INV-10 cross-engine parity; review major 2). An
27
+ // order-dependent tie-break would return the far snap on native and the near one on web for the same input.
28
+ function selectSnap(ideal, points) {
29
+ if (points.length === 0) {
30
+ throw new Error('selectSnap: empty snap set — a snap drag needs at least one snap point (fail-loud, never a silent no-op)');
31
+ }
32
+ let best = points[0];
33
+ let bestDist = Math.abs(best - ideal);
34
+ for (let i = 1; i < points.length; i++) {
35
+ const p = points[i];
36
+ const d = Math.abs(p - ideal);
37
+ // Strictly nearer wins; on an exact tie the SMALLER value wins (web-parity, order-independent).
38
+ if (d < bestDist || (d === bestDist && p < best)) {
39
+ best = p;
40
+ bestDist = d;
41
+ }
42
+ }
43
+ return best;
44
+ }
45
+ // Normalize a recognizer velocity to the canonical px/s unit (REQ-GESTURE-010, Decision §G). The platform
46
+ // velocity is already px/s, so this is a unit-contract pass-through that carries sign — explicitly NO ÷1000
47
+ // (a px/ms conversion is the defect this guards). Fails loud on a non-finite value; it never silently zeroes
48
+ // (a hold-then-release yields a genuine ~0 sample from the platform, which is finite and passes through).
49
+ function normalizeVelocity(velocity) {
50
+ if (!Number.isFinite(velocity)) {
51
+ throw new Error(`normalizeVelocity: velocity must be a finite px/s value, got ${velocity}`);
52
+ }
53
+ return velocity;
54
+ }
55
+ // Product dual-authority baseline (REQ-GESTURE-022 class): host drag callbacks report gesture-relative
56
+ // `info.offset` (Motion PanInfo / native translationX/Y). The engine handoff projects from the absolute
57
+ // live release origin. Controlled product `animate` retargets must rebuild absolute origin as
58
+ // grabOrigin (last committed snap / settle) + gesture-relative offset — never offset alone when grab≠0.
59
+ function absoluteDragOrigin(grabOrigin, gestureRelativeOffset) {
60
+ if (!Number.isFinite(grabOrigin) || !Number.isFinite(gestureRelativeOffset)) {
61
+ throw new Error(`absoluteDragOrigin: grabOrigin and gestureRelativeOffset must be finite, got ${grabOrigin}, ${gestureRelativeOffset}`);
62
+ }
63
+ return grabOrigin + gestureRelativeOffset;
64
+ }
65
+ // The release target (REQ-GESTURE-016/017/023), shared verbatim by the M1 session and the handoff session
66
+ // (REQ-GESTURE-022) so the terminal decision can never fork: a boundary spring-back when released past a
67
+ // constraint bound, else project-then-snap onto the snap set, else — free-drag — the projected ideal
68
+ // clamped into the constraints. Under R16 that clamped ideal is NOT a spring target (the retired R13
69
+ // clamp): it is the inertia descriptor's MIRROR terminal — the two-phase generator recomputes its own
70
+ // ideal from the seed and owns the walls, so the actual settle can diverge from it (an out-of-bounds
71
+ // origin settles AT the violated bound via the t=0 latch, never at a clamped ideal that lies elsewhere).
72
+ function selectReleaseTarget(current, platformVelocity, constraints, snap) {
73
+ if (constraints !== undefined) {
74
+ // Same polarity law as applyConstraints (review major 0d144ea2f856): refuse wrong-polarity
75
+ // open sentinels before any comparison that would return ±Infinity as a spring target.
76
+ (0, elastic_1.assertLegalConstraintBounds)(constraints, 'selectReleaseTarget');
77
+ if (Number.isFinite(constraints.max) && current > constraints.max)
78
+ return constraints.max;
79
+ if (Number.isFinite(constraints.min) && current < constraints.min)
80
+ return constraints.min;
81
+ }
82
+ if (snap !== undefined)
83
+ return selectSnap(project(current, platformVelocity), snap.points);
84
+ // Free-drag (REQ-GESTURE-023): the inertia descriptor's MIRROR terminal — Motion inertia
85
+ // ideal = origin + power · v, clamped into the constraints. The generator owns the actual
86
+ // settle (R16); this value never steers the curve.
87
+ const ideal = project(current, platformVelocity);
88
+ if (constraints === undefined)
89
+ return ideal;
90
+ if (Number.isFinite(constraints.max) && ideal > constraints.max)
91
+ return constraints.max;
92
+ if (Number.isFinite(constraints.min) && ideal < constraints.min)
93
+ return constraints.min;
94
+ return ideal;
95
+ }
@@ -0,0 +1,7 @@
1
+ import type { DragConstraints, SnapConfig } from "./types.cjs";
2
+ export declare const PROJECTION_POWER = 0.8;
3
+ export declare function project(origin: number, velocity: number, power?: number): number;
4
+ export declare function selectSnap(ideal: number, points: readonly number[]): number;
5
+ export declare function normalizeVelocity(velocity: number): number;
6
+ export declare function absoluteDragOrigin(grabOrigin: number, gestureRelativeOffset: number): number;
7
+ export declare function selectReleaseTarget(current: number, platformVelocity: number, constraints: DragConstraints | undefined, snap: SnapConfig | undefined): number;
@@ -0,0 +1,7 @@
1
+ import type { DragConstraints, SnapConfig } from "./types.js";
2
+ export declare const PROJECTION_POWER = 0.8;
3
+ export declare function project(origin: number, velocity: number, power?: number): number;
4
+ export declare function selectSnap(ideal: number, points: readonly number[]): number;
5
+ export declare function normalizeVelocity(velocity: number): number;
6
+ export declare function absoluteDragOrigin(grabOrigin: number, gestureRelativeOffset: number): number;
7
+ export declare function selectReleaseTarget(current: number, platformVelocity: number, constraints: DragConstraints | undefined, snap: SnapConfig | undefined): number;
@@ -0,0 +1,87 @@
1
+ // SPEC-GESTURE §2 — the projection function, project-then-snap selection, velocity normalization, and the
2
+ // shared release-target selector. Pure functions over plain numbers (REQ-GESTURE-010/016/017/019); no graph,
3
+ // no clock, no host. Host-agnostic (REQ-CORE-003): only a type import.
4
+ import { assertLegalConstraintBounds } from "./elastic.js";
5
+ // The default projection power = the pinned `motion` 12.42.2 inertia `power` default (verified `power = 0.8`
6
+ // in node_modules/motion-dom/dist). The M1 selection/monotonicity invariants only need `power > 0`, so this
7
+ // exact value binds cross-engine PARITY (scenario 12, M2), not the M1 scenarios. A different value is a
8
+ // recorded divergence (REQ-GESTURE-019).
9
+ export const PROJECTION_POWER = 0.8;
10
+ // P(origin, v) = origin + power·v (REQ-GESTURE-019, Motion's `power · velocity` model). The projected landing
11
+ // point the snap selector targets; `v = 0` collapses `ideal` to `origin` (nearest-to-release).
12
+ export function project(origin, velocity, power = PROJECTION_POWER) {
13
+ return origin + power * velocity;
14
+ }
15
+ // argmin_{s ∈ points} |s − ideal| (REQ-GESTURE-017). Fails loud on an empty set (G-INV-8) — a snap drag with
16
+ // no targets is a config error, never a silent no-op. Exact-midpoint ties resolve toward the SMALLER snap
17
+ // value, ORDER-INDEPENDENT — matching the web mapping (`native-motion-web` nearestSnap) byte-for-byte so
18
+ // the two engines never disagree on a tie (G-INV-10 cross-engine parity; review major 2). An
19
+ // order-dependent tie-break would return the far snap on native and the near one on web for the same input.
20
+ export function selectSnap(ideal, points) {
21
+ if (points.length === 0) {
22
+ throw new Error('selectSnap: empty snap set — a snap drag needs at least one snap point (fail-loud, never a silent no-op)');
23
+ }
24
+ let best = points[0];
25
+ let bestDist = Math.abs(best - ideal);
26
+ for (let i = 1; i < points.length; i++) {
27
+ const p = points[i];
28
+ const d = Math.abs(p - ideal);
29
+ // Strictly nearer wins; on an exact tie the SMALLER value wins (web-parity, order-independent).
30
+ if (d < bestDist || (d === bestDist && p < best)) {
31
+ best = p;
32
+ bestDist = d;
33
+ }
34
+ }
35
+ return best;
36
+ }
37
+ // Normalize a recognizer velocity to the canonical px/s unit (REQ-GESTURE-010, Decision §G). The platform
38
+ // velocity is already px/s, so this is a unit-contract pass-through that carries sign — explicitly NO ÷1000
39
+ // (a px/ms conversion is the defect this guards). Fails loud on a non-finite value; it never silently zeroes
40
+ // (a hold-then-release yields a genuine ~0 sample from the platform, which is finite and passes through).
41
+ export function normalizeVelocity(velocity) {
42
+ if (!Number.isFinite(velocity)) {
43
+ throw new Error(`normalizeVelocity: velocity must be a finite px/s value, got ${velocity}`);
44
+ }
45
+ return velocity;
46
+ }
47
+ // Product dual-authority baseline (REQ-GESTURE-022 class): host drag callbacks report gesture-relative
48
+ // `info.offset` (Motion PanInfo / native translationX/Y). The engine handoff projects from the absolute
49
+ // live release origin. Controlled product `animate` retargets must rebuild absolute origin as
50
+ // grabOrigin (last committed snap / settle) + gesture-relative offset — never offset alone when grab≠0.
51
+ export function absoluteDragOrigin(grabOrigin, gestureRelativeOffset) {
52
+ if (!Number.isFinite(grabOrigin) || !Number.isFinite(gestureRelativeOffset)) {
53
+ throw new Error(`absoluteDragOrigin: grabOrigin and gestureRelativeOffset must be finite, got ${grabOrigin}, ${gestureRelativeOffset}`);
54
+ }
55
+ return grabOrigin + gestureRelativeOffset;
56
+ }
57
+ // The release target (REQ-GESTURE-016/017/023), shared verbatim by the M1 session and the handoff session
58
+ // (REQ-GESTURE-022) so the terminal decision can never fork: a boundary spring-back when released past a
59
+ // constraint bound, else project-then-snap onto the snap set, else — free-drag — the projected ideal
60
+ // clamped into the constraints. Under R16 that clamped ideal is NOT a spring target (the retired R13
61
+ // clamp): it is the inertia descriptor's MIRROR terminal — the two-phase generator recomputes its own
62
+ // ideal from the seed and owns the walls, so the actual settle can diverge from it (an out-of-bounds
63
+ // origin settles AT the violated bound via the t=0 latch, never at a clamped ideal that lies elsewhere).
64
+ export function selectReleaseTarget(current, platformVelocity, constraints, snap) {
65
+ if (constraints !== undefined) {
66
+ // Same polarity law as applyConstraints (review major 0d144ea2f856): refuse wrong-polarity
67
+ // open sentinels before any comparison that would return ±Infinity as a spring target.
68
+ assertLegalConstraintBounds(constraints, 'selectReleaseTarget');
69
+ if (Number.isFinite(constraints.max) && current > constraints.max)
70
+ return constraints.max;
71
+ if (Number.isFinite(constraints.min) && current < constraints.min)
72
+ return constraints.min;
73
+ }
74
+ if (snap !== undefined)
75
+ return selectSnap(project(current, platformVelocity), snap.points);
76
+ // Free-drag (REQ-GESTURE-023): the inertia descriptor's MIRROR terminal — Motion inertia
77
+ // ideal = origin + power · v, clamped into the constraints. The generator owns the actual
78
+ // settle (R16); this value never steers the curve.
79
+ const ideal = project(current, platformVelocity);
80
+ if (constraints === undefined)
81
+ return ideal;
82
+ if (Number.isFinite(constraints.max) && ideal > constraints.max)
83
+ return constraints.max;
84
+ if (Number.isFinite(constraints.min) && ideal < constraints.min)
85
+ return constraints.min;
86
+ return ideal;
87
+ }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ // SPEC-GESTURE §2 — the gesture SESSION: the total begin→active→end/cancel state machine that ingests a
3
+ // recognizer sample stream, drives a motion value continuously with grab continuity + live constraints, and
4
+ // on release hands off `(live value, platform velocity)` into the pinned two-phase inertia generator
5
+ // (free-drag, REQ-GESTURE-023 R16) or a velocity-seeded spring toward a project-then-snap target (snap).
6
+ // Drives the exit off the injected MotionGraph + the built generators — no forked math, no velocity
7
+ // estimator (Decision §G), no driver/React import. Host-agnostic (REQ-CORE-003): relative imports only.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.GESTURE_RELEASE_SPRING = void 0;
10
+ exports.createGestureSession = createGestureSession;
11
+ const transition_1 = require("../transition.cjs");
12
+ const inertia_1 = require("../inertia.cjs");
13
+ const elastic_1 = require("./elastic.cjs");
14
+ const projection_1 = require("./projection.cjs");
15
+ // First-spike release spring (physics ⇒ carries the seeded velocity, REQ-SPRING-007). A near-critical snap
16
+ // spring — minimal overshoot for a settling sheet. Config constant, not an inline literal; refinable (the
17
+ // exact stiffness/damping binds feel + cross-engine parity at M2, not the M1 continuity/settle invariants).
18
+ // Exported: the component's driver-integrated release (REQ-GESTURE-022) seeds the SAME default into the
19
+ // driver's spring — one constant, never a duplicated literal. Typed with REQUIRED fields (satisfies, not
20
+ // annotation) so consumers can lift it into shapes with required numbers under exactOptionalPropertyTypes.
21
+ exports.GESTURE_RELEASE_SPRING = {
22
+ stiffness: 400,
23
+ damping: 40,
24
+ };
25
+ // Wrap a generator factory so the spring seeds from the LIVE value (C0) but with an EXPLICIT velocity (the
26
+ // platform release velocity, §G) rather than the value's own tracked velocity. The release spring is physics-
27
+ // resolved (carryVelocity true), so the seeded velocity is honored.
28
+ function seedWith(factory, velocity) {
29
+ return (seed) => factory({ from: seed.from, velocity });
30
+ }
31
+ function createGestureSession(options) {
32
+ const { graph } = options;
33
+ const constraints = options.constraints;
34
+ const snap = options.snap;
35
+ const releaseSpring = options.releaseSpring ?? exports.GESTURE_RELEASE_SPRING;
36
+ const value = graph.motionValue(options.initial ?? 0);
37
+ let state = 'idle';
38
+ let grabValue = 0; // the value at the last grab (P0)
39
+ let activationTranslation = 0; // recognizer translation captured at activation (dead-zone subtraction)
40
+ let preGrabValue = 0; // the value before the current grab — the cancel target (REQ-GESTURE-018)
41
+ let animation; // the in-flight release/cancel spring, if any
42
+ let lastHandoffVelocity; // the velocity seeded at the last release/cancel (§G)
43
+ // A finger being down is unsettled work with no animation, so the graph would otherwise report settled
44
+ // mid-drag. Hold an activity handle for the duration of the active gesture (G-INV-7 / REQ-DRIVER-002).
45
+ let dragHandle;
46
+ function releaseDragHandle() {
47
+ dragHandle?.release();
48
+ dragHandle = undefined;
49
+ }
50
+ // Reconcile the internal state to the graph: once the release/cancel spring has settled, the session is
51
+ // idle again. The settle is asynchronous (a later frame step), so the session cannot mutate `state` at the
52
+ // moment it happens — every state-dependent entry point reconciles first. Without this the field stayed
53
+ // stale at 'settling' after settle, and a subsequent cancel()/fail() rewound to the PREVIOUS gesture's
54
+ // pre-grab value (the "idle cancel rewind"). The state() getter reconciled for reads but the mutators did
55
+ // not, so the public state and the behavior disagreed.
56
+ function sync() {
57
+ if (state === 'settling' && graph.isSettled())
58
+ state = 'idle';
59
+ }
60
+ // Map a recognizer translation to the constrained motion value: P0 + (translation − activation), then
61
+ // hard-clamp / rubber-band per constraints (REQ-GESTURE-011/015). Delta-since-activation gives C0 across a
62
+ // non-zero activeOffset dead zone.
63
+ function applyToValue(translation) {
64
+ const raw = grabValue + (translation - activationTranslation);
65
+ value.set(constraints === undefined ? raw : (0, elastic_1.applyConstraints)(raw, constraints));
66
+ }
67
+ function startSpring(target, seedVelocity) {
68
+ animation = graph.animate(value, seedWith((0, transition_1.resolveSpringGenerator)(target, releaseSpring), seedVelocity));
69
+ state = 'settling';
70
+ }
71
+ // R16 (REQ-GESTURE-023): the free-drag release runs the pinned two-phase inertia generator on
72
+ // the SAME graph — the decay + boundary-spring curve, never the projected-landing spring. The
73
+ // generator recomputes the ideal from the seed and owns the walls; the drag-layer physics are
74
+ // the pin's (timeConstant 750, power 0.8, restDelta 1, restSpeed 10, elastic-conditional bounce).
75
+ function startInertia(seedVelocity) {
76
+ const elastic = constraints?.elastic;
77
+ const bounce = (0, transition_1.dragInertiaBounce)(elastic);
78
+ animation = graph.animate(value, seedWith((0, inertia_1.inertiaGenerator)({
79
+ power: transition_1.DRAG_INERTIA_DEFAULTS.power,
80
+ timeConstant: transition_1.DRAG_INERTIA_DEFAULTS.timeConstant,
81
+ bounceStiffness: bounce.bounceStiffness,
82
+ bounceDamping: bounce.bounceDamping,
83
+ restDelta: transition_1.DRAG_INERTIA_DEFAULTS.restDelta,
84
+ restSpeed: transition_1.DRAG_INERTIA_DEFAULTS.restSpeed,
85
+ ...(constraints === undefined ? {} : { min: constraints.min, max: constraints.max }),
86
+ }), seedVelocity));
87
+ state = 'settling';
88
+ }
89
+ function cancelTo(target) {
90
+ sync(); // reconcile first — a settled release is 'idle', not a stale 'settling' (else the guard misfires)
91
+ if (state !== 'active' && state !== 'settling') {
92
+ // idle ⇒ no gesture in flight. Fail loud, consistent with the total state machine (G-INV-8:
93
+ // illegal transitions throw). NOT a silent no-op (which hides recognizer/caller misuse) and NOT a
94
+ // rewind — the throw fires before any mutation, so the settled value is untouched.
95
+ throw new Error(`gesture: cancel() in state '${state}' — no active or settling gesture to cancel`);
96
+ }
97
+ // state is 'active' (cancel the drag) or 'settling' (redirect the in-flight release) — both cancel to target.
98
+ animation?.stop();
99
+ lastHandoffVelocity = 0;
100
+ startSpring(target, 0); // cancel is a zero-velocity return to the cancel target (REQ-GESTURE-018)
101
+ releaseDragHandle(); // the spring now holds the graph unsettled; drop the drag handle
102
+ }
103
+ return {
104
+ begin(activation) {
105
+ sync(); // a settled prior release is idle, not 'settling' — a fresh grab is legal
106
+ if (state === 'active') {
107
+ throw new Error('gesture: begin() during an active gesture — end() or cancel() first');
108
+ }
109
+ animation?.stop(); // re-grab: stop any in-flight release (decrements activeAnimationCount, REQ-GESTURE-012)
110
+ animation = undefined;
111
+ releaseDragHandle(); // defensive: never stack handles across a re-grab
112
+ grabValue = value.get();
113
+ preGrabValue = value.get();
114
+ activationTranslation = activation;
115
+ dragHandle = graph.registerActivity(); // a finger is down ⇒ the graph is unsettled (G-INV-7)
116
+ state = 'active';
117
+ },
118
+ active(sample) {
119
+ sync();
120
+ if (state !== 'active') {
121
+ throw new Error(`gesture: active() in state '${state}' — begin() a gesture first`);
122
+ }
123
+ applyToValue(sample.translation);
124
+ },
125
+ end(sample) {
126
+ sync();
127
+ if (state !== 'active') {
128
+ throw new Error(`gesture: end() in state '${state}' — no active gesture to release`);
129
+ }
130
+ applyToValue(sample.translation); // commit the final active position (C0 seam)
131
+ const platformVelocity = (0, projection_1.normalizeVelocity)(sample.velocity);
132
+ if (snap === undefined) {
133
+ // Free-drag (REQ-GESTURE-023, R16): the two-phase inertia generator owns the curve.
134
+ lastHandoffVelocity = platformVelocity;
135
+ startInertia(platformVelocity);
136
+ }
137
+ else {
138
+ // Resolve the target BEFORE mutating settle state so a config error leaves the session
139
+ // untouched rather than half-released. The selector is the SHARED pure seam (REQ-GESTURE-022 parity).
140
+ const target = (0, projection_1.selectReleaseTarget)(value.get(), platformVelocity, constraints, snap);
141
+ lastHandoffVelocity = platformVelocity; // the velocity handed off, UNSCALED (§G / REQ-GESTURE-013)
142
+ startSpring(target, platformVelocity);
143
+ }
144
+ releaseDragHandle(); // release AFTER the spring starts so there is no spurious settled frame
145
+ },
146
+ cancel() {
147
+ cancelTo(preGrabValue);
148
+ },
149
+ fail() {
150
+ cancelTo(preGrabValue); // a recognizer fail is a cancel to the pre-grab value (REQ-GESTURE-018)
151
+ },
152
+ value: () => value.get(),
153
+ velocity: () => value.getVelocity(),
154
+ isSettled: () => graph.isSettled(),
155
+ // Reconcile the post-release state: once the release/cancel spring has settled, the session is idle.
156
+ state: () => {
157
+ sync();
158
+ return state;
159
+ },
160
+ handoffVelocity: () => lastHandoffVelocity,
161
+ };
162
+ }
@@ -0,0 +1,29 @@
1
+ import { type SpringConfig } from "../transition.cjs";
2
+ import type { MotionGraph } from "../types.cjs";
3
+ import type { DragConstraints, GestureSample, SnapConfig } from "./types.cjs";
4
+ export declare const GESTURE_RELEASE_SPRING: {
5
+ readonly stiffness: 400;
6
+ readonly damping: 40;
7
+ };
8
+ export type GestureSessionState = 'idle' | 'active' | 'settling';
9
+ export interface GestureSessionOptions {
10
+ readonly graph: MotionGraph;
11
+ readonly initial?: number;
12
+ readonly constraints?: DragConstraints;
13
+ readonly snap?: SnapConfig;
14
+ readonly releaseSpring?: SpringConfig;
15
+ }
16
+ export interface GestureSession {
17
+ begin(activationTranslation: number): void;
18
+ active(sample: GestureSample): void;
19
+ end(sample: GestureSample): void;
20
+ cancel(): void;
21
+ fail(): void;
22
+ value(): number;
23
+ velocity(): number;
24
+ isSettled(): boolean;
25
+ state(): GestureSessionState;
26
+ /** The velocity (px/s) seeded into the following animation at the last release/cancel; `undefined` before any. */
27
+ handoffVelocity(): number | undefined;
28
+ }
29
+ export declare function createGestureSession(options: GestureSessionOptions): GestureSession;
@@ -0,0 +1,29 @@
1
+ import { type SpringConfig } from "../transition.js";
2
+ import type { MotionGraph } from "../types.js";
3
+ import type { DragConstraints, GestureSample, SnapConfig } from "./types.js";
4
+ export declare const GESTURE_RELEASE_SPRING: {
5
+ readonly stiffness: 400;
6
+ readonly damping: 40;
7
+ };
8
+ export type GestureSessionState = 'idle' | 'active' | 'settling';
9
+ export interface GestureSessionOptions {
10
+ readonly graph: MotionGraph;
11
+ readonly initial?: number;
12
+ readonly constraints?: DragConstraints;
13
+ readonly snap?: SnapConfig;
14
+ readonly releaseSpring?: SpringConfig;
15
+ }
16
+ export interface GestureSession {
17
+ begin(activationTranslation: number): void;
18
+ active(sample: GestureSample): void;
19
+ end(sample: GestureSample): void;
20
+ cancel(): void;
21
+ fail(): void;
22
+ value(): number;
23
+ velocity(): number;
24
+ isSettled(): boolean;
25
+ state(): GestureSessionState;
26
+ /** The velocity (px/s) seeded into the following animation at the last release/cancel; `undefined` before any. */
27
+ handoffVelocity(): number | undefined;
28
+ }
29
+ export declare function createGestureSession(options: GestureSessionOptions): GestureSession;
@@ -0,0 +1,158 @@
1
+ // SPEC-GESTURE §2 — the gesture SESSION: the total begin→active→end/cancel state machine that ingests a
2
+ // recognizer sample stream, drives a motion value continuously with grab continuity + live constraints, and
3
+ // on release hands off `(live value, platform velocity)` into the pinned two-phase inertia generator
4
+ // (free-drag, REQ-GESTURE-023 R16) or a velocity-seeded spring toward a project-then-snap target (snap).
5
+ // Drives the exit off the injected MotionGraph + the built generators — no forked math, no velocity
6
+ // estimator (Decision §G), no driver/React import. Host-agnostic (REQ-CORE-003): relative imports only.
7
+ import { DRAG_INERTIA_DEFAULTS, dragInertiaBounce, resolveSpringGenerator, } from "../transition.js";
8
+ import { inertiaGenerator } from "../inertia.js";
9
+ import { applyConstraints } from "./elastic.js";
10
+ import { normalizeVelocity, selectReleaseTarget } from "./projection.js";
11
+ // First-spike release spring (physics ⇒ carries the seeded velocity, REQ-SPRING-007). A near-critical snap
12
+ // spring — minimal overshoot for a settling sheet. Config constant, not an inline literal; refinable (the
13
+ // exact stiffness/damping binds feel + cross-engine parity at M2, not the M1 continuity/settle invariants).
14
+ // Exported: the component's driver-integrated release (REQ-GESTURE-022) seeds the SAME default into the
15
+ // driver's spring — one constant, never a duplicated literal. Typed with REQUIRED fields (satisfies, not
16
+ // annotation) so consumers can lift it into shapes with required numbers under exactOptionalPropertyTypes.
17
+ export const GESTURE_RELEASE_SPRING = {
18
+ stiffness: 400,
19
+ damping: 40,
20
+ };
21
+ // Wrap a generator factory so the spring seeds from the LIVE value (C0) but with an EXPLICIT velocity (the
22
+ // platform release velocity, §G) rather than the value's own tracked velocity. The release spring is physics-
23
+ // resolved (carryVelocity true), so the seeded velocity is honored.
24
+ function seedWith(factory, velocity) {
25
+ return (seed) => factory({ from: seed.from, velocity });
26
+ }
27
+ export function createGestureSession(options) {
28
+ const { graph } = options;
29
+ const constraints = options.constraints;
30
+ const snap = options.snap;
31
+ const releaseSpring = options.releaseSpring ?? GESTURE_RELEASE_SPRING;
32
+ const value = graph.motionValue(options.initial ?? 0);
33
+ let state = 'idle';
34
+ let grabValue = 0; // the value at the last grab (P0)
35
+ let activationTranslation = 0; // recognizer translation captured at activation (dead-zone subtraction)
36
+ let preGrabValue = 0; // the value before the current grab — the cancel target (REQ-GESTURE-018)
37
+ let animation; // the in-flight release/cancel spring, if any
38
+ let lastHandoffVelocity; // the velocity seeded at the last release/cancel (§G)
39
+ // A finger being down is unsettled work with no animation, so the graph would otherwise report settled
40
+ // mid-drag. Hold an activity handle for the duration of the active gesture (G-INV-7 / REQ-DRIVER-002).
41
+ let dragHandle;
42
+ function releaseDragHandle() {
43
+ dragHandle?.release();
44
+ dragHandle = undefined;
45
+ }
46
+ // Reconcile the internal state to the graph: once the release/cancel spring has settled, the session is
47
+ // idle again. The settle is asynchronous (a later frame step), so the session cannot mutate `state` at the
48
+ // moment it happens — every state-dependent entry point reconciles first. Without this the field stayed
49
+ // stale at 'settling' after settle, and a subsequent cancel()/fail() rewound to the PREVIOUS gesture's
50
+ // pre-grab value (the "idle cancel rewind"). The state() getter reconciled for reads but the mutators did
51
+ // not, so the public state and the behavior disagreed.
52
+ function sync() {
53
+ if (state === 'settling' && graph.isSettled())
54
+ state = 'idle';
55
+ }
56
+ // Map a recognizer translation to the constrained motion value: P0 + (translation − activation), then
57
+ // hard-clamp / rubber-band per constraints (REQ-GESTURE-011/015). Delta-since-activation gives C0 across a
58
+ // non-zero activeOffset dead zone.
59
+ function applyToValue(translation) {
60
+ const raw = grabValue + (translation - activationTranslation);
61
+ value.set(constraints === undefined ? raw : applyConstraints(raw, constraints));
62
+ }
63
+ function startSpring(target, seedVelocity) {
64
+ animation = graph.animate(value, seedWith(resolveSpringGenerator(target, releaseSpring), seedVelocity));
65
+ state = 'settling';
66
+ }
67
+ // R16 (REQ-GESTURE-023): the free-drag release runs the pinned two-phase inertia generator on
68
+ // the SAME graph — the decay + boundary-spring curve, never the projected-landing spring. The
69
+ // generator recomputes the ideal from the seed and owns the walls; the drag-layer physics are
70
+ // the pin's (timeConstant 750, power 0.8, restDelta 1, restSpeed 10, elastic-conditional bounce).
71
+ function startInertia(seedVelocity) {
72
+ const elastic = constraints?.elastic;
73
+ const bounce = dragInertiaBounce(elastic);
74
+ animation = graph.animate(value, seedWith(inertiaGenerator({
75
+ power: DRAG_INERTIA_DEFAULTS.power,
76
+ timeConstant: DRAG_INERTIA_DEFAULTS.timeConstant,
77
+ bounceStiffness: bounce.bounceStiffness,
78
+ bounceDamping: bounce.bounceDamping,
79
+ restDelta: DRAG_INERTIA_DEFAULTS.restDelta,
80
+ restSpeed: DRAG_INERTIA_DEFAULTS.restSpeed,
81
+ ...(constraints === undefined ? {} : { min: constraints.min, max: constraints.max }),
82
+ }), seedVelocity));
83
+ state = 'settling';
84
+ }
85
+ function cancelTo(target) {
86
+ sync(); // reconcile first — a settled release is 'idle', not a stale 'settling' (else the guard misfires)
87
+ if (state !== 'active' && state !== 'settling') {
88
+ // idle ⇒ no gesture in flight. Fail loud, consistent with the total state machine (G-INV-8:
89
+ // illegal transitions throw). NOT a silent no-op (which hides recognizer/caller misuse) and NOT a
90
+ // rewind — the throw fires before any mutation, so the settled value is untouched.
91
+ throw new Error(`gesture: cancel() in state '${state}' — no active or settling gesture to cancel`);
92
+ }
93
+ // state is 'active' (cancel the drag) or 'settling' (redirect the in-flight release) — both cancel to target.
94
+ animation?.stop();
95
+ lastHandoffVelocity = 0;
96
+ startSpring(target, 0); // cancel is a zero-velocity return to the cancel target (REQ-GESTURE-018)
97
+ releaseDragHandle(); // the spring now holds the graph unsettled; drop the drag handle
98
+ }
99
+ return {
100
+ begin(activation) {
101
+ sync(); // a settled prior release is idle, not 'settling' — a fresh grab is legal
102
+ if (state === 'active') {
103
+ throw new Error('gesture: begin() during an active gesture — end() or cancel() first');
104
+ }
105
+ animation?.stop(); // re-grab: stop any in-flight release (decrements activeAnimationCount, REQ-GESTURE-012)
106
+ animation = undefined;
107
+ releaseDragHandle(); // defensive: never stack handles across a re-grab
108
+ grabValue = value.get();
109
+ preGrabValue = value.get();
110
+ activationTranslation = activation;
111
+ dragHandle = graph.registerActivity(); // a finger is down ⇒ the graph is unsettled (G-INV-7)
112
+ state = 'active';
113
+ },
114
+ active(sample) {
115
+ sync();
116
+ if (state !== 'active') {
117
+ throw new Error(`gesture: active() in state '${state}' — begin() a gesture first`);
118
+ }
119
+ applyToValue(sample.translation);
120
+ },
121
+ end(sample) {
122
+ sync();
123
+ if (state !== 'active') {
124
+ throw new Error(`gesture: end() in state '${state}' — no active gesture to release`);
125
+ }
126
+ applyToValue(sample.translation); // commit the final active position (C0 seam)
127
+ const platformVelocity = normalizeVelocity(sample.velocity);
128
+ if (snap === undefined) {
129
+ // Free-drag (REQ-GESTURE-023, R16): the two-phase inertia generator owns the curve.
130
+ lastHandoffVelocity = platformVelocity;
131
+ startInertia(platformVelocity);
132
+ }
133
+ else {
134
+ // Resolve the target BEFORE mutating settle state so a config error leaves the session
135
+ // untouched rather than half-released. The selector is the SHARED pure seam (REQ-GESTURE-022 parity).
136
+ const target = selectReleaseTarget(value.get(), platformVelocity, constraints, snap);
137
+ lastHandoffVelocity = platformVelocity; // the velocity handed off, UNSCALED (§G / REQ-GESTURE-013)
138
+ startSpring(target, platformVelocity);
139
+ }
140
+ releaseDragHandle(); // release AFTER the spring starts so there is no spurious settled frame
141
+ },
142
+ cancel() {
143
+ cancelTo(preGrabValue);
144
+ },
145
+ fail() {
146
+ cancelTo(preGrabValue); // a recognizer fail is a cancel to the pre-grab value (REQ-GESTURE-018)
147
+ },
148
+ value: () => value.get(),
149
+ velocity: () => value.getVelocity(),
150
+ isSettled: () => graph.isSettled(),
151
+ // Reconcile the post-release state: once the release/cancel spring has settled, the session is idle.
152
+ state: () => {
153
+ sync();
154
+ return state;
155
+ },
156
+ handoffVelocity: () => lastHandoffVelocity,
157
+ };
158
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ // SPEC-GESTURE §2 — the gesture ingest + constraint types. Host-agnostic (REQ-CORE-003): plain data only,
3
+ // no react/react-native/Gesture-Handler import. A recognizer stream is modeled as a sequence of these
4
+ // samples; the real Gesture Handler / motion-react bindings that produce them are Milestone 2 (deferred).
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,13 @@
1
+ export interface GestureSample {
2
+ readonly t: number;
3
+ readonly translation: number;
4
+ readonly velocity: number;
5
+ }
6
+ export interface DragConstraints {
7
+ readonly min: number;
8
+ readonly max: number;
9
+ readonly elastic?: number;
10
+ }
11
+ export interface SnapConfig {
12
+ readonly points: readonly number[];
13
+ }
@@ -0,0 +1,13 @@
1
+ export interface GestureSample {
2
+ readonly t: number;
3
+ readonly translation: number;
4
+ readonly velocity: number;
5
+ }
6
+ export interface DragConstraints {
7
+ readonly min: number;
8
+ readonly max: number;
9
+ readonly elastic?: number;
10
+ }
11
+ export interface SnapConfig {
12
+ readonly points: readonly number[];
13
+ }
@@ -0,0 +1,4 @@
1
+ // SPEC-GESTURE §2 — the gesture ingest + constraint types. Host-agnostic (REQ-CORE-003): plain data only,
2
+ // no react/react-native/Gesture-Handler import. A recognizer stream is modeled as a sequence of these
3
+ // samples; the real Gesture Handler / motion-react bindings that produce them are Milestone 2 (deferred).
4
+ export {};