@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
@@ -0,0 +1,58 @@
1
+ import { ViewStyle } from 'react-native';
2
+ import { SharedValue } from 'react-native-reanimated';
3
+
4
+ /**
5
+ * Clamp `value` to `[min, max]`. When `elastic > 0` the overshoot past a
6
+ * bound is scaled by `elastic` instead of hard-clamped, giving a rubber-band
7
+ * feel. `min` / `max` may be `undefined` to leave that side unbounded.
8
+ *
9
+ * Worklet, so it runs both inside a gesture-handler pan handler on the UI
10
+ * thread (`useDrag`) and from a JS-thread `PanResponder` callback
11
+ * (`useTouchDrag`).
12
+ *
13
+ * ```ts
14
+ * applyBounds(120, -100, 100, 0) // 100
15
+ * applyBounds(120, -100, 100, 0.5) // 110
16
+ * applyBounds(50, undefined, 100, 0) // 50
17
+ * ```
18
+ */
19
+ declare function applyBounds(value: number, min: number | undefined, max: number | undefined, elastic: number): number;
20
+
21
+ /**
22
+ * What {@link useTranslateStyle} returns: a plain style object that owns
23
+ * `transform`.
24
+ *
25
+ * Deliberately **not** `ReturnType<typeof useAnimatedStyle>`. Reanimated 4.5
26
+ * brands that value (`AnimatedStyleHandle`), and a branded type is rejected
27
+ * inside a `StyleProp<ViewStyle>` array — so every call site would need a cast,
28
+ * which is the exact defect `InterpolatedStyle` was introduced to remove in
29
+ * `0.0.9`. The brand is compile-time only, so the runtime value is unchanged:
30
+ * it is still the animated style Reanimated produced.
31
+ */
32
+ type TranslateStyle = {
33
+ transform: NonNullable<ViewStyle['transform']>;
34
+ };
35
+ /**
36
+ * Animated style that translates by two shared values:
37
+ * `transform: [{ translateX: x }, { translateY: y }]`.
38
+ *
39
+ * This is the style fragment every drag-style hook returns (`useTouchDrag`,
40
+ * and `useDrag` / `usePan` / `useSwipe` in `@rootnative/inertia-gestures`).
41
+ * Use it directly when a custom gesture owns its own translation values.
42
+ *
43
+ * The style owns the whole `transform` key. Do not stack a second transform
44
+ * style beside it — `transform` is one key in React Native, so the later
45
+ * style replaces this one instead of merging. Nest another animated view, or
46
+ * build one style from `x` / `y` with `useInterpolatedStyle`, to add a
47
+ * transform.
48
+ *
49
+ * ```tsx
50
+ * const x = useMotionValue(0)
51
+ * const y = useMotionValue(0)
52
+ * const style = useTranslateStyle(x, y)
53
+ * return <Motion.View style={style} />
54
+ * ```
55
+ */
56
+ declare function useTranslateStyle(x: SharedValue<number>, y: SharedValue<number>): TranslateStyle;
57
+
58
+ export { type TranslateStyle as T, applyBounds as a, useTranslateStyle as u };
@@ -0,0 +1,58 @@
1
+ import { ViewStyle } from 'react-native';
2
+ import { SharedValue } from 'react-native-reanimated';
3
+
4
+ /**
5
+ * Clamp `value` to `[min, max]`. When `elastic > 0` the overshoot past a
6
+ * bound is scaled by `elastic` instead of hard-clamped, giving a rubber-band
7
+ * feel. `min` / `max` may be `undefined` to leave that side unbounded.
8
+ *
9
+ * Worklet, so it runs both inside a gesture-handler pan handler on the UI
10
+ * thread (`useDrag`) and from a JS-thread `PanResponder` callback
11
+ * (`useTouchDrag`).
12
+ *
13
+ * ```ts
14
+ * applyBounds(120, -100, 100, 0) // 100
15
+ * applyBounds(120, -100, 100, 0.5) // 110
16
+ * applyBounds(50, undefined, 100, 0) // 50
17
+ * ```
18
+ */
19
+ declare function applyBounds(value: number, min: number | undefined, max: number | undefined, elastic: number): number;
20
+
21
+ /**
22
+ * What {@link useTranslateStyle} returns: a plain style object that owns
23
+ * `transform`.
24
+ *
25
+ * Deliberately **not** `ReturnType<typeof useAnimatedStyle>`. Reanimated 4.5
26
+ * brands that value (`AnimatedStyleHandle`), and a branded type is rejected
27
+ * inside a `StyleProp<ViewStyle>` array — so every call site would need a cast,
28
+ * which is the exact defect `InterpolatedStyle` was introduced to remove in
29
+ * `0.0.9`. The brand is compile-time only, so the runtime value is unchanged:
30
+ * it is still the animated style Reanimated produced.
31
+ */
32
+ type TranslateStyle = {
33
+ transform: NonNullable<ViewStyle['transform']>;
34
+ };
35
+ /**
36
+ * Animated style that translates by two shared values:
37
+ * `transform: [{ translateX: x }, { translateY: y }]`.
38
+ *
39
+ * This is the style fragment every drag-style hook returns (`useTouchDrag`,
40
+ * and `useDrag` / `usePan` / `useSwipe` in `@rootnative/inertia-gestures`).
41
+ * Use it directly when a custom gesture owns its own translation values.
42
+ *
43
+ * The style owns the whole `transform` key. Do not stack a second transform
44
+ * style beside it — `transform` is one key in React Native, so the later
45
+ * style replaces this one instead of merging. Nest another animated view, or
46
+ * build one style from `x` / `y` with `useInterpolatedStyle`, to add a
47
+ * transform.
48
+ *
49
+ * ```tsx
50
+ * const x = useMotionValue(0)
51
+ * const y = useMotionValue(0)
52
+ * const style = useTranslateStyle(x, y)
53
+ * return <Motion.View style={style} />
54
+ * ```
55
+ */
56
+ declare function useTranslateStyle(x: SharedValue<number>, y: SharedValue<number>): TranslateStyle;
57
+
58
+ export { type TranslateStyle as T, applyBounds as a, useTranslateStyle as u };
package/jest-preset.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Jest preset for projects consuming `@rootnative/inertia` and its sibling
2
2
  // adapter packages (`@rootnative/inertia-gestures`, `-gradients`, `-svg`).
3
3
  //
4
- // Layered on top of `react-native`'s own preset. Adds:
4
+ // Layered on top of `@react-native/jest-preset`. Adds:
5
5
  // - the `react-native-worklets` + Reanimated mock surface Inertia exercises
6
6
  // (worklet stubs, animation primitives, color/layout utilities)
7
7
  // - `transformIgnorePatterns` widened so Jest transforms the published
@@ -19,7 +19,36 @@
19
19
  // If you need to allowlist additional packages for transformation, extend
20
20
  // `transformIgnorePatterns` in your own config — Jest merges over the preset.
21
21
 
22
- const rnPreset = require('react-native/jest-preset')
22
+ // This package is deliberately **not** declared as a peer dependency of
23
+ // `@rootnative/inertia`. React Native pins it to an exact version (RN 0.86.3
24
+ // requires exactly `@react-native/jest-preset@0.86.3`), so any range we
25
+ // declared would advertise versions that cannot install — the sdk-compat range
26
+ // audit fails on it — and an exact pin would break on every RN patch. The
27
+ // version relationship belongs to `react-native`, which already declares it.
28
+ //
29
+ // Resolve `@react-native/jest-preset` directly rather than through the
30
+ // `react-native/jest-preset` shim. RN 0.86 moved the preset into its own
31
+ // package and left the old path as a shim that re-exports it — but it declares
32
+ // the new package as an **optional** peer, and no package manager installs an
33
+ // optional peer. So on RN 0.86 the shim throws a migration error for any
34
+ // consumer who has not installed it by hand, which is what a consumer of this
35
+ // preset would have hit. Requiring it here means the failure names this
36
+ // package's requirement instead.
37
+ let rnPreset
38
+ try {
39
+ rnPreset = require('@react-native/jest-preset')
40
+ } catch (error) {
41
+ if (error.code === 'MODULE_NOT_FOUND') {
42
+ throw new Error(
43
+ '[inertia] `@rootnative/inertia/jest-preset` needs `@react-native/jest-preset`.\n' +
44
+ 'React Native 0.86 moved its Jest preset into that package and declares\n' +
45
+ 'it as an optional peer, so it is not installed for you. Add it as a\n' +
46
+ 'devDependency at the version matching your react-native:\n\n' +
47
+ ' npm install --save-dev @react-native/jest-preset\n',
48
+ )
49
+ }
50
+ throw error
51
+ }
23
52
 
24
53
  module.exports = {
25
54
  ...rnPreset,
package/llms.txt CHANGED
@@ -88,7 +88,8 @@ import { MotionFlatList } from '@rootnative/inertia/flat-list'
88
88
  - `resolveAnimatableValue(value, base, factory?)` — one level up: resolves a full per-property `animate` value (plain target, `{ to, ...override }` step, or keyframe array → `withSequence` with sequence-level `repeat`) into one animation. Gives custom components the complete `animate` grammar.
89
89
  - `cubicBezier(x1, y1, x2, y2)` / `cubicBezier(css)` — builds a `timing.easing` value from cubic-bezier control points: four numbers, a W3C CSS `cubic-bezier(0.2, 0, 0, 1)` string (design-token form), or a CSS keyword (`'linear'` | `'ease'` | `'ease-in'` | `'ease-out'` | `'ease-in-out'`). Makes CSS-format easing tokens directly consumable — pair with named transitions to feed a theme's motion tokens into the registry. Invalid input throws (`x1`/`x2` must be within [0, 1]; `step-*` and `linear(...)` unsupported).
90
90
  - `ensureWorkletEasing(easing)` — normalizes any accepted easing input (plain function, worklet, or Reanimated 4 `EasingFunctionFactory`) into a worklet `withTiming` can consume. Only needed for manual `withTiming` calls — the resolvers and primitives apply it automatically.
91
- - `TRANSPARENT` — `'rgba(0, 0, 0, 0)'`, the value to seed a colour shared value with when a custom component has no other source. Not `'transparent'`: Reanimated's colour table maps that keyword to `undefined`, so it is the one CSS colour name `isColor()` rejects, and a slot resting at it cannot be animated away from under `withTiming` the target still snaps in at the end, under `withSpring` (the default) the animation never settles. Only affects values passed to `withSpring` / `withTiming`; `interpolateColor` parses the keyword fine, so hooks that interpolate colours themselves are unaffected.
91
+ - `TRANSPARENT` — `'rgba(0, 0, 0, 0)'`, the value to seed a colour shared value with when a custom component has no other source. It exists because Reanimated up to 4.4 mapped the CSS keyword `'transparent'` to `undefined` in its colour table, making it the one name `isColor()` rejected: a slot resting at it could not be animated away from, and under `withSpring` (the default) the animation never settled. **Reanimated 4.5 fixed that** the keyword now maps to a real packed colour — so within the supported peer band (`>=4.5.0 <4.6.0`) either spelling animates. The constant stays exported as an explicit, unambiguous seed and as the safe choice for code that may run against an older Reanimated.
92
+ - `TranslateStyle` / `ColorStyle` / `ShadowStyle` (types) — what `useTranslateStyle`, `useColorTransition` / `useColorCascade`, and `useShadow` return. Plain style objects, **not** Reanimated's branded `AnimatedStyleHandle`: Reanimated 4.5 brands the value `useAnimatedStyle` returns, and a branded value is rejected inside a `StyleProp` array, so stating the plain shape is what keeps a style-array call site cast-free. Same reasoning as `InterpolatedStyle<K>` above. The brand is compile-time only — the value is still the animated style Reanimated produced.
92
93
  - `SharedValue<T>` (type) — re-exported from the root entry so props/refs that carry a value-layer hook result can be typed without importing from `react-native-reanimated`.
93
94
  - `@rootnative/inertia/reanimated` — render-layer interop subpath: pure re-exports of `Animated`, `createAnimatedComponent`, `useAnimatedStyle`, `useAnimatedProps`, `useDerivedValue`, `interpolate`, `interpolateColor`, `Extrapolation`, `cancelAnimation` (plus `SharedValue` / `DerivedValue` / `AnimatedStyle` / `AnimatedProps` types) under their original Reanimated names, for components that own their own `useAnimatedStyle` worklet over Inertia-driven shared values. Names must stay original — the Reanimated Babel plugin auto-workletizes callbacks by callee name. The `with*` animation factories are deliberately not re-exported; start animations with `useAnimation` / `resolveTransition` / `buildReleaseAnimation`.
94
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rootnative/inertia",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Declarative animation primitives for React Native, built on react-native-reanimated.",
5
5
  "license": "MIT",
6
6
  "author": "RootNative",
@@ -125,27 +125,30 @@
125
125
  "!**/*.test.*"
126
126
  ],
127
127
  "peerDependencies": {
128
- "react": ">=19.0.0",
129
- "react-native": ">=0.81.0",
130
- "react-native-reanimated": ">=4.0.0",
131
- "react-native-worklets": ">=0.5.0"
128
+ "@testing-library/react-native": ">=13.0.0",
129
+ "react": ">=19.2.3 <20.0.0",
130
+ "react-native": ">=0.83.0 <0.87.0",
131
+ "react-native-reanimated": ">=4.5.0 <4.6.0",
132
+ "react-native-worklets": ">=0.10.0 <0.11.0"
132
133
  },
133
134
  "peerDependenciesMeta": {
134
- "react-native-worklets": {
135
+ "@testing-library/react-native": {
135
136
  "optional": true
136
137
  }
137
138
  },
138
139
  "devDependencies": {
139
- "@react-native/babel-preset": "^0.81.5",
140
+ "@react-native/babel-preset": "^0.86.3",
141
+ "@react-native/jest-preset": "^0.86.3",
140
142
  "@size-limit/preset-small-lib": "^11.1.0",
141
143
  "@testing-library/react-native": "^13.3.3",
142
144
  "@types/jest": "^29.5.14",
143
- "@types/react": "^19.1.0",
145
+ "@types/react": "^19.2.0",
144
146
  "jest": "^29.7.0",
145
- "react": "19.1.0",
146
- "react-native": "0.81.5",
147
- "react-native-reanimated": "~4.1.1",
148
- "react-test-renderer": "19.1.0",
147
+ "react": "19.2.3",
148
+ "react-native": "0.86.3",
149
+ "react-native-reanimated": "4.5.1",
150
+ "react-native-worklets": "0.10.1",
151
+ "react-test-renderer": "19.2.3",
149
152
  "size-limit": "^11.1.0",
150
153
  "tsup": "^8.3.5",
151
154
  "typescript": "^5.7.3"
@@ -1,3 +1,4 @@
1
+ import { warnOnce } from '../internal/warnOnce'
1
2
  import { isTopLevelTransition } from '../transitions/keys'
2
3
  import {
3
4
  type NamedTransitions,
@@ -6,8 +7,6 @@ import {
6
7
  type TransitionInput,
7
8
  } from '../types'
8
9
 
9
- declare const __DEV__: boolean
10
-
11
10
  /**
12
11
  * Fallback used when a name isn't found in the registry: the library default
13
12
  * spring, matching what an omitted transition resolves to everywhere else.
@@ -26,13 +25,12 @@ export function lookupNamedTransition(
26
25
  ): TransitionConfig {
27
26
  const cfg = registry[name]
28
27
  if (cfg) return cfg
29
- if (__DEV__) {
30
- console.warn(
31
- `[inertia] Unknown transition name "${name}" — falling back to the ` +
32
- `default spring. Register it on a provider: ` +
33
- `<MotionConfig transitions={{ '${name}': { ... } }}>.`,
34
- )
35
- }
28
+ warnOnce(
29
+ `unknown-transition:${name}`,
30
+ `[inertia] Unknown transition name "${name}" — falling back to the ` +
31
+ `default spring. Register it on a provider: ` +
32
+ `<MotionConfig transitions={{ '${name}': { ... } }}>.`,
33
+ )
36
34
  return UNKNOWN_NAME_FALLBACK
37
35
  }
38
36
 
@@ -20,3 +20,6 @@ export type {
20
20
  UseGestureLayerOptions,
21
21
  UseGestureLayerResult,
22
22
  } from './useGestureLayer'
23
+ // The per-layer form of `UseGestureLayerOptions.transition`. Re-exported here
24
+ // so a consumer of this subpath can name it without also importing the root.
25
+ export type { GestureLayerTransitions } from '../types'
@@ -210,7 +210,10 @@ export function useGestureLayer(
210
210
  // JS-thread precompute: union of keys across all layers, per-key type
211
211
  // (number vs color), and a rest-fallback table. The worklet body reads
212
212
  // from `meta` instead of probing each layer per frame — the type check
213
- // only runs when layer identities change.
213
+ // only runs when the layers change structurally. Keyed on a signature, not
214
+ // on object identity: `states` is usually an inline literal, and a fresh
215
+ // `meta` each render would rebuild the animated style's worklet.
216
+ const statesSig = stableSig(states)
214
217
  const meta = useMemo(() => {
215
218
  const layers = {
216
219
  rest: states.rest,
@@ -251,14 +254,8 @@ export function useGestureLayer(
251
254
  restRaw !== undefined ? restRaw : isColor ? 'transparent' : 0
252
255
  }
253
256
  return { layers, keys, types, restValues }
254
- }, [
255
- states.rest,
256
- states.hovered,
257
- states.focused,
258
- states.focusVisible,
259
- states.pressed,
260
- states.disabled,
261
- ])
257
+ // eslint-disable-next-line react-hooks/exhaustive-deps
258
+ }, [statesSig])
262
259
 
263
260
  const style = useAnimatedStyle(() => {
264
261
  const { layers, keys, types, restValues } = meta
@@ -12,15 +12,16 @@ import { Platform } from 'react-native'
12
12
  * arrives via D-pad, screen reader, or hardware keyboard, all of which are
13
13
  * keyboard-equivalent — so `isFocusVisible()` is unconditionally `true`.
14
14
  *
15
- * The web listeners attach eagerly at module import (capture phase, so they
16
- * run before the focus event reaches the focused element) and stay installed
17
- * for the lifetime of the document. Eager installation matters: the very
18
- * first interaction with a page can be the mouse click that focuses a
19
- * gesture-wired element, and if the listeners only attached during that
20
- * focus dispatch the mousedown would already have passed unobserved
21
- * leaving the default `'keyboard'` modality and drawing a focus ring for a
22
- * pointer interaction. They are passive and idle-cheap; the cost is one
23
- * boolean read per `onFocus` dispatch.
15
+ * The web listeners attach when the first component that tracks
16
+ * `focusVisible` mounts (`installFocusVisibility`, called from a mount
17
+ * effect), in the capture phase so they run before the focus event reaches
18
+ * the focused element, and they stay installed for the lifetime of the
19
+ * document. Mount-time installation is early enough: the click that focuses
20
+ * a gesture-wired element can only land on an element that is already
21
+ * mounted, so its `mousedown` is observed. The listeners are not attached at
22
+ * import time `@rootnative/inertia` declares `sideEffects: false`, and an
23
+ * import-time listener would make that declaration false. They are passive
24
+ * and idle-cheap; the cost is one boolean read per `onFocus` dispatch.
24
25
  */
25
26
 
26
27
  type InputModality = 'keyboard' | 'pointer'
@@ -38,7 +39,14 @@ function setPointer() {
38
39
  modality = 'pointer'
39
40
  }
40
41
 
41
- function ensureInstalled(): void {
42
+ /**
43
+ * Attach the document listeners that track input modality on web. Idempotent
44
+ * and a no-op on native or without a `document`. Call it from a mount effect
45
+ * of any component that reads `isFocusVisible()`, so the pointer event that
46
+ * precedes the first focus is observed (see module doc above).
47
+ * `isFocusVisible` also calls it as a safety net.
48
+ */
49
+ export function installFocusVisibility(): void {
42
50
  if (installed) return
43
51
  if (Platform.OS !== 'web') return
44
52
  if (typeof document === 'undefined') return
@@ -49,12 +57,6 @@ function ensureInstalled(): void {
49
57
  installed = true
50
58
  }
51
59
 
52
- // Install at import time so the pointer event that precedes the first focus
53
- // is observed (see module doc above). `ensureInstalled` stays in
54
- // `isFocusVisible` as a safety net for environments where `document` appears
55
- // after import.
56
- ensureInstalled()
57
-
58
60
  /**
59
61
  * `true` if the next `onFocus` should be treated as "focus-visible" (keyboard
60
62
  * focus). On native, always `true`. On web, reflects the most recent user
@@ -62,7 +64,7 @@ ensureInstalled()
62
64
  */
63
65
  export function isFocusVisible(): boolean {
64
66
  if (Platform.OS !== 'web') return true
65
- ensureInstalled()
67
+ installFocusVisibility()
66
68
  return modality === 'keyboard'
67
69
  }
68
70
 
@@ -1 +1 @@
1
- export { isFocusVisible } from './focusVisibility'
1
+ export { installFocusVisibility, isFocusVisible } from './focusVisibility'
package/src/index.ts CHANGED
@@ -42,12 +42,18 @@ export {
42
42
  resolveTransition,
43
43
  resolveAnimatableValue,
44
44
  ensureWorkletEasing,
45
+ isTopLevelTransition,
46
+ stableSig,
45
47
  type AnimationCallback,
48
+ type CallbackFactory,
46
49
  } from './transitions'
47
50
  // Seed value for any color shared value a custom animated component drives
48
51
  // through `resolveTransition`. Exported because the obvious choice —
49
52
  // `'transparent'` — cannot be animated away from; see the symbol's own docs.
50
53
  export { TRANSPARENT } from './internal/color'
54
+ // Worklet-safe clamp shared by `useTouchDrag` and the gesture adapters'
55
+ // `useDrag`; exported so a custom drag can apply the same rubber-band rule.
56
+ export { applyBounds } from './touch/applyBounds'
51
57
  export {
52
58
  useAnimation,
53
59
  useAnimator,
@@ -61,6 +67,7 @@ export {
61
67
  useShadow,
62
68
  useSpring,
63
69
  useTransform,
70
+ useTranslateStyle,
64
71
  useVariants,
65
72
  } from './values'
66
73
  // The value-layer hooks above all return Reanimated `SharedValue`s — export
@@ -72,12 +79,15 @@ export type {
72
79
  Animator,
73
80
  BoxShadowLayer,
74
81
  ColorCascadeLayer,
82
+ ColorStyle,
75
83
  ColorStyleKey,
76
84
  ExtrapolationMode,
77
85
  InterpolatedStyle,
78
86
  InterpolatedStyleMap,
87
+ TranslateStyle,
79
88
  NumericStyleKey,
80
89
  ShadowConfig,
90
+ ShadowStyle,
81
91
  TransformKey,
82
92
  UseColorCascadeOptions,
83
93
  UseColorTransitionOptions,
@@ -97,8 +107,10 @@ export type {
97
107
  EasingFunction,
98
108
  EasingFunctionFactory,
99
109
  EasingInput,
110
+ GestureLayerTransitions,
100
111
  GestureSubStates,
101
112
  MotionComponent,
113
+ MotionComponentProps,
102
114
  MotionProps,
103
115
  NamedTransitions,
104
116
  NoAnimationTransition,
@@ -164,7 +164,7 @@ export function resolveBoxShadowInput(
164
164
  offsetY: coerceLength(layer.offsetY, 'offsetY'),
165
165
  blurRadius: coerceLength(layer.blurRadius, 'blurRadius'),
166
166
  spreadDistance: coerceLength(layer.spreadDistance, 'spreadDistance'),
167
- color: layer.color ?? 'black',
167
+ color: coerceColor(layer.color),
168
168
  inset: layer.inset ?? false,
169
169
  }))
170
170
  }
@@ -273,7 +273,10 @@ export type BoxShadowInput =
273
273
  // rejected) by `coerceLength` like every other length.
274
274
  blurRadius?: unknown
275
275
  spreadDistance?: number | string | undefined
276
- color?: string | undefined
276
+ // `unknown` because RN 0.86 widened this from `string` to `ColorValue`,
277
+ // which includes the opaque handle `PlatformColor()` returns. Coerced
278
+ // (and rejected) by `coerceColor`.
279
+ color?: unknown
277
280
  inset?: boolean | undefined
278
281
  }>
279
282
 
@@ -284,6 +287,25 @@ export type BoxShadowInput =
284
287
  * `parseBoxShadow` — px and unitless only, and anything else throws rather
285
288
  * than silently animating from a `NaN`.
286
289
  */
290
+ /**
291
+ * Coerce one RN colour field to a string.
292
+ *
293
+ * RN 0.86 widened `BoxShadowValue.color` from `string` to `ColorValue`, which
294
+ * admits the opaque handle `PlatformColor()` and `DynamicColorIOS()` return.
295
+ * That handle is a platform reference, not a colour this code can read, so it
296
+ * cannot be interpolated. Reject it here with a message that names the cause,
297
+ * rather than letting it reach the worklet and animate to `NaN`.
298
+ */
299
+ function coerceColor(value: unknown): string {
300
+ if (value === undefined) return 'black'
301
+ if (typeof value === 'string') return value
302
+ throw new Error(
303
+ '[inertia] boxShadow: color must be a colour string, got ' +
304
+ `${typeof value}. PlatformColor and DynamicColorIOS values cannot be ` +
305
+ 'interpolated — pass a string colour on an animated shadow.',
306
+ )
307
+ }
308
+
287
309
  function coerceLength(value: unknown, field: string): number {
288
310
  if (value === undefined) return 0
289
311
  if (typeof value === 'number') return value
@@ -1,4 +1,5 @@
1
1
  import { LinearTransition } from 'react-native-reanimated'
2
+ import { DEFAULT_LAYOUT_DURATION } from '../transitions/constants'
2
3
  import { ensureWorkletEasing } from '../transitions/easing'
3
4
  import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
4
5
  import { type TransitionConfig } from '../types'
@@ -34,7 +35,9 @@ export function resolveLayoutTransition(
34
35
  if (cfg.type === 'no-animation') return undefined
35
36
 
36
37
  if (cfg.type === 'timing') {
37
- let builder = LinearTransition.duration(cfg.duration ?? 300)
38
+ let builder = LinearTransition.duration(
39
+ cfg.duration ?? DEFAULT_LAYOUT_DURATION,
40
+ )
38
41
  const easing = ensureWorkletEasing(cfg.easing)
39
42
  if (easing) builder = builder.easing(easing)
40
43
  if (cfg.delay) builder = builder.delay(cfg.delay)
@@ -15,6 +15,7 @@ import {
15
15
  withSpring,
16
16
  withTiming,
17
17
  } from 'react-native-reanimated'
18
+ import { DEFAULT_LAYOUT_DURATION } from '../transitions/constants'
18
19
  import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
19
20
  import { type SpringTransition, type TransitionConfig } from '../types'
20
21
  import { measureWindowRect } from './measureWindow'
@@ -334,7 +335,7 @@ function legBuilder(
334
335
  transition: TransitionConfig | undefined,
335
336
  ): (from: number, to: number) => number {
336
337
  if (transition?.type === 'timing') {
337
- const duration = transition.duration ?? 300
338
+ const duration = transition.duration ?? DEFAULT_LAYOUT_DURATION
338
339
  return (from, to) =>
339
340
  withSequence(
340
341
  withTiming(from, { duration: 0 }),