@rootnative/inertia 0.0.9 → 0.0.11

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 (105) hide show
  1. package/CHANGELOG.md +95 -1
  2. package/README.md +8 -8
  3. package/dist/{chunk-MFAFB4K7.mjs → chunk-2Q6JMDYZ.mjs} +8 -10
  4. package/dist/chunk-2UREDR4Z.js +8 -0
  5. package/dist/{chunk-ZBHQPVWE.js → chunk-3PB27UIE.js} +2 -2
  6. package/dist/{chunk-IX6SEOSK.mjs → chunk-5EYIZG72.mjs} +60 -45
  7. package/dist/{chunk-EN4PN7X3.mjs → chunk-7RMSLKQQ.mjs} +1 -1
  8. package/dist/{chunk-6YIUETBF.mjs → chunk-DT2KVAAO.mjs} +1 -1
  9. package/dist/{chunk-L4JFECXU.mjs → chunk-FKJLPYGY.mjs} +6 -3
  10. package/dist/{chunk-E3ALFSH2.js → chunk-HBURWGCZ.js} +86 -71
  11. package/dist/chunk-JAJHKJC5.js +8 -0
  12. package/dist/chunk-JBMTTICC.js +8 -0
  13. package/dist/{chunk-GSUC4HWP.mjs → chunk-JGQ6YRDN.mjs} +1 -1
  14. package/dist/chunk-KFHZMFAX.js +23 -0
  15. package/dist/{chunk-M4766VUV.mjs → chunk-LHW4VNWD.mjs} +1 -1
  16. package/dist/{chunk-MUT6BTZS.js → chunk-NTG6PSGZ.js} +10 -7
  17. package/dist/chunk-OH46TGEA.mjs +20 -0
  18. package/dist/{chunk-6AN5X3YE.js → chunk-QDMSFRQR.js} +10 -11
  19. package/dist/{chunk-W7NTRSPD.mjs → chunk-QIRKPDXA.mjs} +118 -68
  20. package/dist/{chunk-WNVHPMBI.js → chunk-QX5JHRZ2.js} +118 -66
  21. package/dist/{chunk-6NOCY764.js → chunk-R7YD6L3D.js} +2 -2
  22. package/dist/{chunk-JBK5ZLMH.mjs → chunk-RO4XFTQ6.mjs} +1 -1
  23. package/dist/{chunk-QNLSCOR5.mjs → chunk-UGYBFAVY.mjs} +1 -1
  24. package/dist/{chunk-IHVMNOS4.js → chunk-XLKI2MSD.js} +2 -2
  25. package/dist/gestureLayer/index.d.mts +3 -3
  26. package/dist/gestureLayer/index.d.ts +3 -3
  27. package/dist/gestureLayer/index.js +12 -18
  28. package/dist/gestureLayer/index.mjs +5 -11
  29. package/dist/index.d.mts +108 -49
  30. package/dist/index.d.ts +108 -49
  31. package/dist/index.js +114 -91
  32. package/dist/index.mjs +66 -59
  33. package/dist/motion/FlatList.d.mts +2 -1
  34. package/dist/motion/FlatList.d.ts +2 -1
  35. package/dist/motion/FlatList.js +5 -5
  36. package/dist/motion/FlatList.mjs +4 -4
  37. package/dist/motion/Image.d.mts +2 -1
  38. package/dist/motion/Image.d.ts +2 -1
  39. package/dist/motion/Image.js +5 -5
  40. package/dist/motion/Image.mjs +4 -4
  41. package/dist/motion/Pressable.d.mts +2 -1
  42. package/dist/motion/Pressable.d.ts +2 -1
  43. package/dist/motion/Pressable.js +5 -5
  44. package/dist/motion/Pressable.mjs +4 -4
  45. package/dist/motion/ScrollView.d.mts +2 -1
  46. package/dist/motion/ScrollView.d.ts +2 -1
  47. package/dist/motion/ScrollView.js +5 -5
  48. package/dist/motion/ScrollView.mjs +4 -4
  49. package/dist/motion/Text.d.mts +2 -1
  50. package/dist/motion/Text.d.ts +2 -1
  51. package/dist/motion/Text.js +5 -5
  52. package/dist/motion/Text.mjs +4 -4
  53. package/dist/motion/View.d.mts +2 -1
  54. package/dist/motion/View.d.ts +2 -1
  55. package/dist/motion/View.js +5 -5
  56. package/dist/motion/View.mjs +4 -4
  57. package/dist/touch/index.d.mts +5 -3
  58. package/dist/touch/index.d.ts +5 -3
  59. package/dist/touch/index.js +16 -31
  60. package/dist/touch/index.mjs +11 -29
  61. package/dist/{types-DyJpG64F.d.mts → types-C5Zbs9MC.d.mts} +22 -4
  62. package/dist/{types-DyJpG64F.d.ts → types-C5Zbs9MC.d.ts} +22 -4
  63. package/dist/{useGesture-DbH46EBp.d.ts → useGesture-Be0ZhCWj.d.ts} +1 -1
  64. package/dist/{useGesture-CnZQnYHH.d.mts → useGesture-Ve8EGjTr.d.mts} +1 -1
  65. package/dist/useTranslateStyle-BDrOD97O.d.mts +58 -0
  66. package/dist/useTranslateStyle-BDrOD97O.d.ts +58 -0
  67. package/jest-preset.js +31 -2
  68. package/llms.txt +2 -1
  69. package/package.json +15 -12
  70. package/src/config/namedTransitions.ts +7 -9
  71. package/src/gestureLayer/index.ts +3 -0
  72. package/src/gestureLayer/useGestureLayer.ts +6 -9
  73. package/src/gestures/focusVisibility.ts +19 -17
  74. package/src/gestures/index.ts +1 -1
  75. package/src/index.ts +12 -0
  76. package/src/internal/boxShadow.ts +24 -2
  77. package/src/layout/resolveLayout.ts +4 -1
  78. package/src/layout/useSharedLayout.ts +2 -1
  79. package/src/motion/createMotionComponent.tsx +92 -44
  80. package/src/presence/Presence.tsx +5 -7
  81. package/src/touch/applyBounds.ts +30 -0
  82. package/src/touch/index.ts +1 -0
  83. package/src/touch/useTouchDrag.ts +18 -46
  84. package/src/transitions/constants.ts +14 -0
  85. package/src/transitions/easing.ts +23 -11
  86. package/src/transitions/index.ts +5 -0
  87. package/src/transitions/keys.ts +0 -2
  88. package/src/transitions/resolve.ts +86 -11
  89. package/src/transitions/resolveSequence.ts +45 -3
  90. package/src/transitions/runtime.ts +4 -7
  91. package/src/transitions/spring.ts +7 -2
  92. package/src/types.ts +23 -3
  93. package/src/values/extrapolation.ts +21 -0
  94. package/src/values/index.ts +3 -0
  95. package/src/values/useColorCascade.ts +3 -3
  96. package/src/values/useColorTransition.ts +17 -2
  97. package/src/values/useGesture.ts +9 -2
  98. package/src/values/useInterpolatedStyle.ts +13 -13
  99. package/src/values/useShadow.ts +36 -9
  100. package/src/values/useSpring.ts +34 -25
  101. package/src/values/useTransform.ts +24 -20
  102. package/src/values/useTranslateStyle.ts +47 -0
  103. package/dist/chunk-24NUL23E.js +0 -8
  104. package/dist/chunk-4XBX7KOD.js +0 -8
  105. package/dist/chunk-Z5USXWTE.js +0 -8
package/CHANGELOG.md CHANGED
@@ -4,6 +4,98 @@ All notable changes to `@rootnative/inertia` are documented here. The format fol
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.0.11] - 2026-09-09
8
+
9
+ ### Changed
10
+
11
+ - **Breaking: the peer ranges are narrowed to the Expo SDK 57 band.** `react` is now `>=19.2.3 <20.0.0`, `react-native` is `>=0.83.0 <0.87.0`, `react-native-reanimated` is `>=4.5.0 <4.6.0`, and `react-native-worklets` is `>=0.10.0 <0.11.0`. The old ranges advertised a floor that npm rejects. `0.0.10` declares `react-native >=0.81.0` and `react-native-reanimated >=4.0.0`, and the newest Reanimated that range admits requires `react-native 0.83 - 0.87`, so a fresh install can fail with `ERESOLVE`. Nothing in this repository changed when that became true — Reanimated narrowed its own range after the release, which is why a range needs a gate that re-checks it and not only a review at the time it is written.
12
+
13
+ The band cannot be wider. Each peer range resolves on its own, so any set that admits both SDK 54 (`react-native 0.81` with Reanimated `4.1`) and SDK 57 (`react-native 0.86` with Reanimated `4.5`) also admits `react-native 0.81` with Reanimated `4.6`, which cannot install. A range promises every combination it permits, not only the combinations that were tested.
14
+
15
+ A new `compat.yml` workflow now packs the release candidate and installs it into every Expo SDK fixture in `rootnative/sdk-compat` before the publish, with `enforce-range: true`. The test suite in this repository says nothing about what a consumer can install, and that gap is what let the `0.0.10` range ship.
16
+
17
+ Migration: move the app to Expo SDK 57 (React `19.2.3`, React Native `0.86.3`, Reanimated `4.5.1`, Worklets `0.10.1`). A project on SDK 54 stays on `0.0.10`. The dev pins and the example app moved to the same band, so the package now develops against the runtime it declares. `sdk-compat` records SDK 54 as a documented boundary, not as a failure.
18
+
19
+ - **`@rootnative/inertia/jest-preset` needs `@react-native/jest-preset` in the consuming project.** React Native `0.86` moved its Jest preset into that package and left `react-native/jest-preset` as a shim that re-exports it. The shim declares the new package as an **optional** peer, and no package manager installs an optional peer, so the shim throws a migration error for every consumer who did not add the package by hand. The preset now resolves `@react-native/jest-preset` directly and throws a named error that says what to install, so the failure states this package's requirement instead of surfacing as a React Native migration message. Add it as a devDependency at the version that matches your `react-native`. The docs testing page carries the install step and the new `preset` spelling.
20
+
21
+ The package is deliberately **not** a peer dependency here. React Native pins it to an exact version — `0.86.3` requires exactly `@react-native/jest-preset@0.86.3` — so any range declared in this package would advertise versions that cannot install, and the `sdk-compat` range audit fails on it. An exact pin would break on every React Native patch. The version relationship belongs to `react-native`, which already declares it.
22
+
23
+ ### Added
24
+
25
+ - **Three style types are exported: `ColorStyle`, `TranslateStyle` and `ShadowStyle`.** They are what `useColorTransition`, `useColorCascade`, `useTranslateStyle` and `useShadow` return. Each hook was public while the type of the style object it hands back was not, so a consumer could not name the value in a typed variable, a prop, or a wrapper hook's signature without rebuilding the type by hand.
26
+
27
+ ### Fixed
28
+
29
+ - **A keyframe array no longer crashes the whole render under reduced motion.** `withSequence` writes a `finished` flag onto every argument it is handed. Reduced motion collapses each step to `no-animation`, and `no-animation` resolves to the bare target rather than an animation object, so `animate={{ translateY: [0, -8, 0] }}` reached `withSequence(0, -8, 0)` and threw `TypeError: Cannot create property 'finished' on number` from inside render. React unmounted the tree, so any consumer using the documented keyframe form — the shape in this package's own `llms.txt` — served a blank page to every visitor with reduce-motion switched on at the OS level. The resolver now settles the property on the last keyframe instead of building a sequence, which is where the sequence would have ended; `repeat` and `delay` were already dropped for `no-animation`, so nothing is left pending. A per-step `{ type: 'no-animation' }` inside a sequence that otherwise animates hit the same throw and now becomes a zero-length timing, keeping its slot in the order.
30
+
31
+ Found from a consumer, not from CI, and the reason is worth keeping: **the Jest mock cannot observe this class of bug.** Its `withSequence` is `(...args) => args[args.length - 1]` and its `withTiming` / `withSpring` return their target, so every step looks like a bare value whether the gate is on or off — the existing reduced-motion sequence tests passed against the crashing code. The regression tests therefore assert the one thing that stays observable under the mock: a sequence under the gate never reaches `withSequence` at all. For the same reason the gate is now detected from the resolved config (`no-animation` base), never by inspecting the shape of a resolved step.
32
+
33
+ ## [0.0.10] - 2026-09-06
34
+
35
+ ### Changed
36
+
37
+ - **`react-native-worklets` is a required peer, not an optional one.** Core imports `isWorkletFunction` from it without a guard in `transitions/easing.ts` and `values/useTransform.ts`, so the package never worked without it. The `optional: true` flag in `peerDependenciesMeta` is removed. A package manager now warns when the peer is missing instead of staying silent. The installation docs already list it as a peer, so no install step changes. Core also declares it as a devDependency, pinned to `~0.5.1` next to Reanimated `~4.1.1`; before, the workspace typecheck passed only because the example app hoisted it. Found by the `1.0.0` readiness audit (2026-09-05).
38
+
39
+ - **`@testing-library/react-native` is declared as an optional peer (`>=13.0.0`).** The `@rootnative/inertia/testing` entry point imports its `render`, but no field in `package.json` said so. A consumer without the library got a bare module-not-found error from that subpath. It is optional because only that entry point needs it. The docs testing page now names the requirement. Found by the `1.0.0` readiness audit (2026-09-05).
40
+
41
+ ### Removed
42
+
43
+ - **Breaking: `restSpeedThreshold` and `restDisplacementThreshold` are removed from `SpringTransition`.** Both keys were silently inert on every release since `0.0.1`. They are Reanimated 3 names: Reanimated 4 settles springs on an internal `energyThreshold`, its `SpringConfig` type does not have either key, and its runtime never reads them. Inertia forwarded both into `withSpring`, where they were ignored — a documented public key that did nothing, which is the defect class the `0.0.5` type narrowing exists to prevent. Found by the pre-`1.0.0` review of the transition vocabulary.
44
+
45
+ Migration: delete the two keys from any spring config. Their removal cannot change animation behavior, because they never had any. If a settle knob is needed later, Reanimated 4's `overshootClamping` / `energyThreshold` are the candidates, as a non-breaking addition.
46
+
47
+ ### Fixed
48
+
49
+ - **`repeat: 0` (and any count below 1) runs the animation once instead of forever.** Reanimated's `withRepeat` reads a count of `0` or less as endless, and the resolver forwarded the count as given, so `repeat: 0` — the natural way to write "do not repeat" from a variable — looped without end. Counts below `1` are now treated as "run once", warn once in dev, and are not forwarded. `onAnimationEnd`'s iteration counter and `<Presence>`'s settle gate use the same rule through the new `repeatIterationsOf` helper, so the three cannot disagree. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
50
+
51
+ - **`repeat.alternate` is no longer forwarded for a sequence, and an explicit `alternate: true` on a sequence warns.** Reanimated's `reverse` flag only swaps the wrapped animation's target, which a `withSequence` ignores — every pass restarts at the first step — so the flag never alternated a sequence. The resolver now passes `reverse: false` for sequences, the `RepeatConfig` docs state the limit, and the sequences page shows the working form: write the reverse steps into the sequence. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
52
+
53
+ - **`<Presence>` no longer releases an exiting child early when its animation run is replaced mid-exit.** The settle counter that gates `safeToRemove` counted every callback, including the `finished: false` callbacks Reanimated fires for animations that a later effect run replaced. If the value-driving effect re-ran during an exit (a controller change, a reduce-motion flip), the replaced run drained its own counter and unmounted the child while the new run was still animating. Each run is now a token: the effect's cleanup marks the run done, and a superseded run's callbacks are ignored. Pinned by `phase3-audit.test.tsx`, which fails against the previous implementation. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
54
+
55
+ - **An `onAnimationEnd` handler added after mount is wired without waiting for the next animation change.** The callback factory skips callback installation when there is no handler and nothing to settle. A handler that went from `undefined` to defined on a later render was read through a ref, but no callback existed to read it, so it stayed silent until `animate` or `transition` changed. The effect is now also keyed on the handler's presence (not its identity — a changing handler still costs nothing). Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
56
+
57
+ - **`useSpring` and `useBooleanSpring` honour reduced motion.** Under `<MotionConfig reducedMotion>` both hooks assign the target to the output directly, with no spring, on the plain-number path and the shared-value path. They were the only value-layer hooks that ignored the setting. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
58
+
59
+ - **Dev warnings fire once per misconfiguration, not once per render.** The unknown-variant warning (`animate="open"` with no matching variant), the unknown named-transition warning, the `<Presence>` keyless-child warning, the `useSpring` spring-only warning, and the `useInterpolatedStyle` range-length warning all went through a bare `console.warn` in render or in a per-render resolver, so a single mistake flooded the console. All now use `warnOnce`, keyed per distinct misconfiguration. `useSwipe`'s decay `releaseTransition` warning in `-gestures` is guarded the same way. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
60
+
61
+ - **`useTouchDrag` no longer rebuilds its `PanResponder` when an inline callback changes identity.** `onDragStart` / `onDragEnd` / `onRelease` were dependencies of the responder memo, so `onRelease={(e) => ...}` written inline rebuilt the responder on every render. The callbacks are now read through a ref at call time; the responder depends only on the axis, constraint, and elastic scalars, and it always calls the latest callback. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
62
+
63
+ - **`useShadow`, `useTransform`, and `useGestureLayer` no longer rebuild their worklet for inline literals.** `useShadow` re-parsed the `boxShadow` pairs on every render, `useTransform` captured a fresh `inputRange` / `outputRange` array, and `useGestureLayer` keyed its per-key table on the identity of each `states` layer object. Each render with inline literals — the documented way to call all three — produced a new UI-thread closure. All three now memoise on a structural signature (`stableSig`). Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
64
+
65
+ - **`sideEffects: false` is now true.** The `focusVisible` input-modality tracker attached its web `document` listeners at import time, which is a side effect the package declared it did not have. The listeners now attach from a mount effect of the first component that tracks `focusVisible` (the `gesture` prop or `useGesture`). Mount time is early enough: the click that focuses an element lands on an element that is already mounted, so the preceding `mousedown` is observed. `installFocusVisibility` is exported from `./gestures` for the same purpose. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
66
+
67
+ - **A key added to `animate` after mount now animates on native.** The `useAnimatedStyle` worklet read the active key set through plain `useRef` objects. `react-native-worklets` clones a captured plain object once, when the worklet is first serialised, and in dev it freezes the original, so later writes to `.current` were dropped with a `Tried to modify key 'current'` warning. The worklet only ever saw the mount-time set: a parent that changed `animate={{ opacity: 1 }}` to `animate={{ opacity: 1, translateX: 120 }}` drove the `translateX` shared value, but the element did not move. The set now crosses to the UI thread through a shared value that is rewritten once per growth. The Jest mock runs the worklet on the JS thread and never showed the defect; it was confirmed and fixed against an Android emulator. Found by the `1.0.0` readiness audit (2026-09-05).
68
+
69
+ - **`useSpring` follows a swapped `SharedValue` target.** The UI-thread reaction listed only the target kind and the spring config as dependencies, and an explicit list replaces Reanimated's closure-derived one. A caller that changed which shared value drives the spring kept the reaction wired to the first one. The current target is now in the list; a plain-number target maps to `null` there, so a changing number does not re-register the inert reaction. Found by the `1.0.0` readiness audit (2026-09-05).
70
+
71
+ ### Added
72
+
73
+ - **`useTranslateStyle(x, y)` is exported from the root barrel.** The `transform: [{ translateX }, { translateY }]` animated style that `useTouchDrag`, `useDrag`, `usePan`, and `useSwipe` all return; the four hooks now share this one implementation. Use it directly when a custom gesture owns its own translation values. Documented in `docs/docs/api/hooks.md`. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
74
+
75
+ - **`applyBounds(value, min, max, elastic)` is exported from the root barrel.** The worklet-safe clamp with optional rubber-band overshoot that `useTouchDrag` and `-gestures`' `useDrag` apply their `constraints` and `elastic` options through; the two hooks carried identical private copies. Documented in `docs/docs/api/hooks.md`. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
76
+
77
+ - **`isTopLevelTransition(value)` is exported from the root barrel.** It is the structural discriminator the factory uses to tell a single `TransitionConfig` from a per-property map: every key must be a known config field. Custom animated components that accept both shapes need the same test; `'type' in transition` is wrong because `SpringTransition.type` is optional. The three adapter packages had that bug and now use this export; see `docs/docs/api/transition-utilities.md`.
78
+
79
+ - **`stableSig(value)` is exported from the root barrel.** It is the order-independent structural signature the factory already keys its `transition` effects on. Custom animated components built on `resolveTransition` need the same thing: an inline `transition` literal is a new object every render, so an effect keyed on its identity restarts the animation each time the parent renders. The three adapter packages now use it for exactly that; see `docs/docs/api/transition-utilities.md`.
80
+
81
+ - **Three types that already appeared in exported signatures are now exported.** `GestureLayerTransitions` (the per-layer form of a gesture `transition`; from the root and from `@rootnative/inertia/gesture-layer`), `CallbackFactory` (the third parameter of `resolveAnimatableValue`), and `MotionComponentProps` (the props of a `Motion.*` primitive for a given component, for typing a wrapper). Pinned by `__type-tests__/public-types.test-d.tsx`. Found by the `1.0.0` readiness audit (2026-09-05).
82
+
83
+ - **Dev warning when `controller` and `animate` are set on the same primitive.** The controller drives the animation and `animate` is ignored; that precedence was documented but silent. It now warns once in dev.
84
+
85
+ ### Documentation
86
+
87
+ - `useVariants` documents that the controller pins the variants map from the first render, and that later renders passing a different map object do not re-key it.
88
+
89
+ - `RepeatConfig` and the sequences page document the count rule (total iterations, minimum `1`) and the `alternate` limit for sequences. `useSpring` documents that it honours reduced motion.
90
+
91
+ ### Internal
92
+
93
+ - The adapter peer range on `@rootnative/inertia` now has an upper bound: `>=<version> <next-minor>` instead of `>=<version>`. Pre-`1.0.0`, a minor bump is where a breaking change lands, so an adapter at `0.0.x` must not accept core `0.1.0`. `scripts/check-versions.mjs` check 2 and both release workflows compute the same bound. Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
94
+
95
+ - Duplicated code consolidated: `mapExtrapolation` (was in `useTransform` and `useInterpolatedStyle`) lives in `values/extrapolation.ts`; `DEFAULT_TIMING_DURATION` (250 ms, was declared in both `resolve.ts` and `runtime.ts`) and `DEFAULT_LAYOUT_DURATION` (300 ms, was a literal in two layout files) live in `transitions/constants.ts`; `buildReleaseAnimation` unwraps an `EasingFunctionFactory` through the same worklet helper `ensureWorkletEasing` uses (`unwrapEasingFactory`). Found by the `1.0.0` readiness audit (2026-09-05), Phase 3.
96
+
97
+ - Tests added for the gaps the audit listed: `useVariants().transitionTo` driving a primitive, the `Motion.Image` runtime path, `useDrag`'s `onDragStart` and decay branch, `usePan`'s `withDecay` config, `MotionLinearGradient`'s locations-presence throw, and `MotionPath`'s `initial.d` mismatch throw.
98
+
7
99
  ## [0.0.9] - 2026-08-22
8
100
 
9
101
  ### Changed
@@ -285,7 +377,9 @@ Initial alpha publish. The full initial surface is in place; APIs are still subj
285
377
  - SVG path morphing, gradient interpolation, and shared-element transitions across screens are out of scope until `0.2.x` / `1.x` per the roadmap.
286
378
  - `react-native-gesture-handler` integration (drag, pan, swipe sub-states) lands in `0.2` via the optional `@rootnative/inertia-gestures` adapter.
287
379
 
288
- [unreleased]: https://github.com/rootnative/inertia/compare/core+gestures+gradients+svg@0.0.9...HEAD
380
+ [unreleased]: https://github.com/rootnative/inertia/compare/core+gestures+gradients+svg@0.0.11...HEAD
381
+ [0.0.11]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.11
382
+ [0.0.10]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.10
289
383
  [0.0.9]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.9
290
384
  [0.0.8]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.8
291
385
  [0.0.7]: https://github.com/rootnative/inertia/releases/tag/core+gestures+gradients+svg@0.0.7
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  Declarative animation primitives for React Native, built as a thin wrapper around [`react-native-reanimated`](https://docs.swmansion.com/react-native-reanimated/). Inspired by Framer Motion (web) and react-spring (cross-platform).
14
14
 
15
- > **Status:** `0.0.9` — stable. Pre-`1.0.0` minor versions may break — see the root [README](https://github.com/rootnative/inertia#versioning--release).
15
+ > **Status:** `0.0.11` — stable. Pre-`1.0.0` minor versions may break — see the [CHANGELOG](https://github.com/rootnative/inertia/blob/main/packages/core/CHANGELOG.md).
16
16
 
17
17
  ## Install
18
18
 
@@ -22,7 +22,7 @@ pnpm add @rootnative/inertia react-native-reanimated
22
22
 
23
23
  Then enable the [Reanimated Babel plugin](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation).
24
24
 
25
- **Peer dependencies:** `react >=19.0.0`, `react-native >=0.81.0`, `react-native-reanimated >=4.0.0`.
25
+ **Peer dependencies:** `react >=19.2.3`, `react-native >=0.83.0 <0.87.0`, `react-native-reanimated >=4.5.0 <4.6.0`, `react-native-worklets >=0.10.0 <0.11.0`.
26
26
 
27
27
  ## Quick start
28
28
 
@@ -76,12 +76,12 @@ Both forms land at ~4.1–4.2 kB brotlied for a single primitive (peers excluded
76
76
 
77
77
  ## Transitions
78
78
 
79
- | `type` | Public config keys | Maps to |
80
- | -------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------- |
81
- | `'spring'` (default) | `tension`, `friction`, `mass`, `velocity`, `restSpeedThreshold`, `restDisplacementThreshold` | `withSpring` |
82
- | `'timing'` | `duration`, `easing`, `delay` | `withTiming` |
83
- | `'decay'` | `velocity`, `deceleration`, `clamp` | `withDecay` |
84
- | `'no-animation'` | — | direct assignment, no interpolation |
79
+ | `type` | Public config keys | Maps to |
80
+ | -------------------- | ----------------------------------------- | ----------------------------------- |
81
+ | `'spring'` (default) | `tension`, `friction`, `mass`, `velocity` | `withSpring` |
82
+ | `'timing'` | `duration`, `easing`, `delay` | `withTiming` |
83
+ | `'decay'` | `velocity`, `deceleration`, `clamp` | `withDecay` |
84
+ | `'no-animation'` | — | direct assignment, no interpolation |
85
85
 
86
86
  Plus, on any transition: `delay`, `repeat`. Per-property transitions take precedence over the top-level transition. Spring config uses **react-spring vocabulary** (`tension`/`friction`); Reanimated's raw `stiffness`/`damping` is never on the public surface.
87
87
 
@@ -1,4 +1,4 @@
1
- import { stableSig, isTopLevelTransition } from './chunk-W7NTRSPD.mjs';
1
+ import { stableSig, warnOnce, isTopLevelTransition } from './chunk-QIRKPDXA.mjs';
2
2
  import { createContext, useContext, useMemo } from 'react';
3
3
  import { useReducedMotion } from 'react-native-reanimated';
4
4
  import { jsx } from 'react/jsx-runtime';
@@ -47,11 +47,10 @@ var UNKNOWN_NAME_FALLBACK = { type: "spring" };
47
47
  function lookupNamedTransition(name, registry) {
48
48
  const cfg = registry[name];
49
49
  if (cfg) return cfg;
50
- if (__DEV__) {
51
- console.warn(
52
- `[inertia] Unknown transition name "${name}" \u2014 falling back to the default spring. Register it on a provider: <MotionConfig transitions={{ '${name}': { ... } }}>.`
53
- );
54
- }
50
+ warnOnce(
51
+ `unknown-transition:${name}`,
52
+ `[inertia] Unknown transition name "${name}" \u2014 falling back to the default spring. Register it on a provider: <MotionConfig transitions={{ '${name}': { ... } }}>.`
53
+ );
55
54
  return UNKNOWN_NAME_FALLBACK;
56
55
  }
57
56
  function resolveNamedTransition(input, registry) {
@@ -84,7 +83,7 @@ function setKeyboard() {
84
83
  function setPointer() {
85
84
  modality = "pointer";
86
85
  }
87
- function ensureInstalled() {
86
+ function installFocusVisibility() {
88
87
  if (installed) return;
89
88
  if (Platform.OS !== "web") return;
90
89
  if (typeof document === "undefined") return;
@@ -94,11 +93,10 @@ function ensureInstalled() {
94
93
  document.addEventListener("touchstart", setPointer, true);
95
94
  installed = true;
96
95
  }
97
- ensureInstalled();
98
96
  function isFocusVisible() {
99
97
  if (Platform.OS !== "web") return true;
100
- ensureInstalled();
98
+ installFocusVisibility();
101
99
  return modality === "keyboard";
102
100
  }
103
101
 
104
- export { MotionConfig, isFocusVisible, lookupNamedTransition, resolveNamedTransition, resolveNamedTransitionProp, useMotionConfig, useNamedTransitions, useShouldReduceMotion };
102
+ export { MotionConfig, installFocusVisibility, isFocusVisible, lookupNamedTransition, resolveNamedTransition, resolveNamedTransitionProp, useMotionConfig, useNamedTransitions, useShouldReduceMotion };
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ var chunkHBURWGCZ_js = require('./chunk-HBURWGCZ.js');
4
+ var reactNative = require('react-native');
5
+
6
+ var MotionView = chunkHBURWGCZ_js.createMotionComponent(reactNative.View);
7
+
8
+ exports.MotionView = MotionView;
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
- var chunkE3ALFSH2_js = require('./chunk-E3ALFSH2.js');
3
+ var chunkHBURWGCZ_js = require('./chunk-HBURWGCZ.js');
4
4
  var reactNative = require('react-native');
5
5
 
6
- var MotionScrollView = chunkE3ALFSH2_js.createMotionComponent(reactNative.ScrollView);
6
+ var MotionScrollView = chunkHBURWGCZ_js.createMotionComponent(reactNative.ScrollView);
7
7
 
8
8
  exports.MotionScrollView = MotionScrollView;
@@ -1,5 +1,5 @@
1
- import { useNamedTransitions, resolveNamedTransitionProp, lookupNamedTransition, useShouldReduceMotion, isFocusVisible } from './chunk-MFAFB4K7.mjs';
2
- import { warnOnce, stableSig, applyDelay, resolveAnimatableValue, isTopLevelTransition, resolveTransition, ensureWorkletEasing, springToReanimated, DEFAULT_SPRING } from './chunk-W7NTRSPD.mjs';
1
+ import { useNamedTransitions, resolveNamedTransitionProp, lookupNamedTransition, useShouldReduceMotion, installFocusVisibility, isFocusVisible } from './chunk-2Q6JMDYZ.mjs';
2
+ import { warnOnce, stableSig, applyDelay, resolveAnimatableValue, isTopLevelTransition, resolveTransition, repeatIterationsOf, DEFAULT_LAYOUT_DURATION, ensureWorkletEasing, springToReanimated, DEFAULT_SPRING } from './chunk-QIRKPDXA.mjs';
3
3
  import { createContext, useContext, useMemo, Children, isValidElement, useState, useRef, useCallback, forwardRef, useEffect } from 'react';
4
4
  import { jsx, Fragment } from 'react/jsx-runtime';
5
5
  import Animated, { useSharedValue, cancelAnimation, useAnimatedStyle, interpolateColor, reanimatedVersion, runOnJS, LinearTransition, withSequence, withTiming, withSpring } from 'react-native-reanimated';
@@ -39,11 +39,10 @@ function Presence({ children }) {
39
39
  Children.forEach(children, (child) => {
40
40
  if (!isValidElement(child)) return;
41
41
  if (child.key === null) {
42
- if (__DEV__) {
43
- console.warn(
44
- "[inertia] <Presence> children must have a `key`. Skipping a keyless child."
45
- );
46
- }
42
+ warnOnce(
43
+ "presence-keyless-child",
44
+ "[inertia] <Presence> children must have a `key`. Skipping a keyless child."
45
+ );
47
46
  return;
48
47
  }
49
48
  out.push(child);
@@ -261,7 +260,7 @@ function resolveBoxShadowInput(input) {
261
260
  offsetY: coerceLength(layer.offsetY, "offsetY"),
262
261
  blurRadius: coerceLength(layer.blurRadius, "blurRadius"),
263
262
  spreadDistance: coerceLength(layer.spreadDistance, "spreadDistance"),
264
- color: layer.color ?? "black",
263
+ color: coerceColor(layer.color),
265
264
  inset: layer.inset ?? false
266
265
  }));
267
266
  }
@@ -292,6 +291,13 @@ function invisibleLayer(inset) {
292
291
  inset
293
292
  };
294
293
  }
294
+ function coerceColor(value) {
295
+ if (value === void 0) return "black";
296
+ if (typeof value === "string") return value;
297
+ throw new Error(
298
+ `[inertia] boxShadow: color must be a colour string, got ${typeof value}. PlatformColor and DynamicColorIOS values cannot be interpolated \u2014 pass a string colour on an animated shadow.`
299
+ );
300
+ }
295
301
  function coerceLength(value, field) {
296
302
  if (value === void 0) return 0;
297
303
  if (typeof value === "number") return value;
@@ -358,7 +364,9 @@ function resolveLayoutTransition(layout) {
358
364
  const cfg = layout === true ? { type: "spring" } : layout;
359
365
  if (cfg.type === "no-animation") return void 0;
360
366
  if (cfg.type === "timing") {
361
- let builder2 = LinearTransition.duration(cfg.duration ?? 300);
367
+ let builder2 = LinearTransition.duration(
368
+ cfg.duration ?? DEFAULT_LAYOUT_DURATION
369
+ );
362
370
  const easing = ensureWorkletEasing(cfg.easing);
363
371
  if (easing) builder2 = builder2.easing(easing);
364
372
  if (cfg.delay) builder2 = builder2.delay(cfg.delay);
@@ -554,7 +562,7 @@ function useSharedLayout(options) {
554
562
  }
555
563
  function legBuilder(transition) {
556
564
  if (transition?.type === "timing") {
557
- const duration = transition.duration ?? 300;
565
+ const duration = transition.duration ?? DEFAULT_LAYOUT_DURATION;
558
566
  return (from, to) => withSequence(
559
567
  withTiming(from, { duration: 0 }),
560
568
  withTiming(to, { duration })
@@ -974,11 +982,8 @@ function createMotionComponent(Component) {
974
982
  }
975
983
  }
976
984
  }
977
- const activeKeysRef = useRef(null);
978
- const hasTransformRef = useRef(false);
979
- const hasShadowOffsetRef = useRef(false);
980
- const hasBoxShadowRef = useRef(false);
981
- const prevActive = activeKeysRef.current;
985
+ const activeSetRef = useRef(null);
986
+ const prevActive = activeSetRef.current?.keys ?? null;
982
987
  let grew = prevActive === null;
983
988
  if (!grew && prevActive) {
984
989
  for (const k of touched) {
@@ -991,30 +996,32 @@ function createMotionComponent(Component) {
991
996
  if (grew) {
992
997
  const merged = new Set(prevActive ?? []);
993
998
  for (const k of touched) merged.add(k);
994
- activeKeysRef.current = ALL_KEYS.filter((k) => merged.has(k));
995
- hasTransformRef.current = activeKeysRef.current.some(
996
- (k) => TRANSFORM_KEY_SET.has(k)
997
- );
998
- hasShadowOffsetRef.current = activeKeysRef.current.some(
999
- (k) => SHADOW_OFFSET_KEY_SET.has(k)
1000
- );
1001
- hasBoxShadowRef.current = activeKeysRef.current.includes("boxShadow");
1002
- if (__DEV__ && hasBoxShadowRef.current && activeKeysRef.current.some(
1003
- (k) => k !== "boxShadow" && k.startsWith("shadow")
1004
- )) {
999
+ const keys = ALL_KEYS.filter((k) => merged.has(k));
1000
+ activeSetRef.current = {
1001
+ keys,
1002
+ hasTransform: keys.some((k) => TRANSFORM_KEY_SET.has(k)),
1003
+ hasShadowOffset: keys.some((k) => SHADOW_OFFSET_KEY_SET.has(k)),
1004
+ hasBoxShadow: keys.includes("boxShadow")
1005
+ };
1006
+ if (__DEV__ && activeSetRef.current.hasBoxShadow && keys.some((k) => k !== "boxShadow" && k.startsWith("shadow"))) {
1005
1007
  warnOnce(
1006
1008
  "boxShadow-with-native-shadow",
1007
1009
  "[inertia] `boxShadow` is animated alongside the native `shadow*` keys on the same element, which applies two shadow systems at once. Pick one: `boxShadow` for the cross-platform CSS form, or the native `shadow*` keys."
1008
1010
  );
1009
1011
  }
1010
1012
  }
1013
+ const activeKeys = activeSetRef.current.keys;
1014
+ const activeSetSV = useSharedValue(activeSetRef.current);
1015
+ const activeSet = activeSetRef.current;
1016
+ useEffect(() => {
1017
+ if (activeSetSV.value !== activeSet) activeSetSV.value = activeSet;
1018
+ }, [activeSetSV, activeSet]);
1011
1019
  const drivenNow = /* @__PURE__ */ new Set();
1012
1020
  collectTouchedKeys(drivenNow, animateRecord);
1013
1021
  if (initialRecord) collectTouchedKeys(drivenNow, initialRecord);
1014
1022
  if (isExiting && exitRecord) collectTouchedKeys(drivenNow, exitRecord);
1015
1023
  const everDrivenRef = useRef(/* @__PURE__ */ new Set());
1016
1024
  for (const k of drivenNow) everDrivenRef.current.add(k);
1017
- const activeKeys = activeKeysRef.current;
1018
1025
  const everDriven = everDrivenRef.current;
1019
1026
  const styleResting = {};
1020
1027
  let styleRestingShadow = null;
@@ -1072,7 +1079,7 @@ function createMotionComponent(Component) {
1072
1079
  );
1073
1080
  const styleRestingSig = stableSig(styleResting);
1074
1081
  useEffect(() => {
1075
- for (const key of activeKeysRef.current) {
1082
+ for (const key of activeKeys) {
1076
1083
  if (everDrivenRef.current.has(key)) continue;
1077
1084
  sharedValues[key].value = styleResting[key] ?? DEFAULT_RESTING[key];
1078
1085
  if (key === "boxShadow") {
@@ -1105,6 +1112,7 @@ function createMotionComponent(Component) {
1105
1112
  const baseRecord = isExiting && exitRecord ? { ...animateRecord, ...exitRecord } : animateRecord;
1106
1113
  const baseSig = stableSig(baseRecord) + (isExiting ? "|exit" : "") + (shouldReduceMotion ? "|rm" : "");
1107
1114
  const transitionSig = stableSig(transition);
1115
+ const hasAnimationEnd = onAnimationEnd !== void 0;
1108
1116
  const safeToRemoveRef = useRef(void 0);
1109
1117
  safeToRemoveRef.current = presence?.safeToRemove;
1110
1118
  useEffect(() => {
@@ -1191,7 +1199,10 @@ function createMotionComponent(Component) {
1191
1199
  if (isExiting && pending === 0) {
1192
1200
  safeToRemoveRef.current?.();
1193
1201
  }
1194
- }, [baseSig, transitionSig]);
1202
+ return () => {
1203
+ done = true;
1204
+ };
1205
+ }, [baseSig, transitionSig, hasAnimationEnd]);
1195
1206
  useGestureLayerProgress(
1196
1207
  pressedProgress,
1197
1208
  pressed,
@@ -1232,7 +1243,7 @@ function createMotionComponent(Component) {
1232
1243
  if (!hasLayoutId) return void 0;
1233
1244
  const out = {};
1234
1245
  let any = false;
1235
- for (const key of activeKeysRef.current) {
1246
+ for (const key of activeKeys) {
1236
1247
  if (!SHARED_STYLE_KEY_SET.has(key)) continue;
1237
1248
  const v = sharedValues[key].value;
1238
1249
  if (typeof v !== "number" && typeof v !== "string") continue;
@@ -1252,10 +1263,11 @@ function createMotionComponent(Component) {
1252
1263
  const flip = sharedLayout.flip;
1253
1264
  const carry = sharedLayout.carry;
1254
1265
  const animatedStyle = useAnimatedStyle(() => {
1255
- const activeKeys2 = activeKeysRef.current;
1256
- const hasTransform = hasTransformRef.current;
1257
- const hasShadowOffset = hasShadowOffsetRef.current;
1258
- const hasBoxShadow = hasBoxShadowRef.current;
1266
+ const activeSet2 = activeSetSV.value;
1267
+ const activeKeys2 = activeSet2.keys;
1268
+ const hasTransform = activeSet2.hasTransform;
1269
+ const hasShadowOffset = activeSet2.hasShadowOffset;
1270
+ const hasBoxShadow = activeSet2.hasBoxShadow;
1259
1271
  const out = {};
1260
1272
  const transform = [];
1261
1273
  let shadowOffsetW = 0;
@@ -1554,12 +1566,7 @@ function stepCountOf(v) {
1554
1566
  }
1555
1567
  function totalIterationsOf(cfg) {
1556
1568
  if (!cfg || cfg.type === "no-animation" || cfg.type === "decay") return 1;
1557
- const r = cfg.repeat;
1558
- if (r === void 0) return 1;
1559
- if (r === "infinite") return Number.POSITIVE_INFINITY;
1560
- if (typeof r === "number") return r;
1561
- if (r.count === "infinite") return Number.POSITIVE_INFINITY;
1562
- return r.count;
1569
+ return repeatIterationsOf(cfg.repeat);
1563
1570
  }
1564
1571
  function reachesTerminalPhase(cfg) {
1565
1572
  return Number.isFinite(totalIterationsOf(cfg));
@@ -1586,16 +1593,21 @@ function useControllerKey(controller) {
1586
1593
  return controller?.current;
1587
1594
  }
1588
1595
  function resolveAnimateInput(animate, variants, controllerKey) {
1596
+ if (controllerKey !== void 0 && animate !== void 0) {
1597
+ warnOnce(
1598
+ "controller-and-animate",
1599
+ "[inertia] Both `controller` and `animate` are set on the same primitive. The controller drives the animation and `animate` is ignored \u2014 remove one of the two."
1600
+ );
1601
+ }
1589
1602
  if (controllerKey !== void 0 && variants && controllerKey in variants) {
1590
1603
  return variants[controllerKey];
1591
1604
  }
1592
1605
  if (typeof animate === "string") {
1593
1606
  if (variants && animate in variants) return variants[animate];
1594
- if (__DEV__) {
1595
- console.warn(
1596
- `[inertia] animate="${animate}" but no matching variant. Did you forget to pass \`variants\`?`
1597
- );
1598
- }
1607
+ warnOnce(
1608
+ `missing-variant:${animate}`,
1609
+ `[inertia] animate="${animate}" but no matching variant. Did you forget to pass \`variants\`?`
1610
+ );
1599
1611
  return void 0;
1600
1612
  }
1601
1613
  return animate;
@@ -1663,6 +1675,9 @@ function useGestureHandlers(gesture, rest, setPressed, setFocused, setFocusVisib
1663
1675
  const hasFocused = gesture?.focused ? 1 : 0;
1664
1676
  const hasFocusVisible = gesture?.focusVisible ? 1 : 0;
1665
1677
  const hasHovered = gesture?.hovered ? 1 : 0;
1678
+ useEffect(() => {
1679
+ if (hasFocusVisible) installFocusVisibility();
1680
+ }, [hasFocusVisible]);
1666
1681
  return useMemo(() => {
1667
1682
  if (!gesture) return {};
1668
1683
  const handlers = {};
@@ -1,4 +1,4 @@
1
- import { createMotionComponent } from './chunk-IX6SEOSK.mjs';
1
+ import { createMotionComponent } from './chunk-5EYIZG72.mjs';
2
2
  import Animated from 'react-native-reanimated';
3
3
 
4
4
  var MotionFlatList = createMotionComponent(
@@ -1,4 +1,4 @@
1
- import { createMotionComponent } from './chunk-IX6SEOSK.mjs';
1
+ import { createMotionComponent } from './chunk-5EYIZG72.mjs';
2
2
  import { Image } from 'react-native';
3
3
 
4
4
  var MotionImage = createMotionComponent(Image);
@@ -1,6 +1,6 @@
1
- import { useShouldReduceMotion, resolveNamedTransitionProp, useNamedTransitions, isFocusVisible } from './chunk-MFAFB4K7.mjs';
2
- import { resolveTransition, isTopLevelTransition } from './chunk-W7NTRSPD.mjs';
3
- import { useCallback, useMemo } from 'react';
1
+ import { useShouldReduceMotion, resolveNamedTransitionProp, useNamedTransitions, installFocusVisibility, isFocusVisible } from './chunk-2Q6JMDYZ.mjs';
2
+ import { resolveTransition, isTopLevelTransition } from './chunk-QIRKPDXA.mjs';
3
+ import { useEffect, useCallback, useMemo } from 'react';
4
4
  import { useSharedValue } from 'react-native-reanimated';
5
5
 
6
6
  function useGesture(transition) {
@@ -10,6 +10,9 @@ function useGesture(transition) {
10
10
  const hovered = useSharedValue(0);
11
11
  const shouldReduceMotion = useShouldReduceMotion();
12
12
  const resolved = resolveNamedTransitionProp(transition, useNamedTransitions());
13
+ useEffect(() => {
14
+ installFocusVisibility();
15
+ }, []);
13
16
  const setLayer = useCallback(
14
17
  (sv, layer, target) => {
15
18
  const cfg = shouldReduceMotion ? { type: "no-animation" } : layerTransition(layer, resolved) ?? { type: "spring" };