@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
@@ -6,8 +6,10 @@ import {
6
6
  withSpring,
7
7
  withTiming,
8
8
  } from 'react-native-reanimated'
9
+ import { DEFAULT_TIMING_DURATION } from './constants'
9
10
  import { ensureWorkletEasing } from './easing'
10
11
  import { springToReanimated } from './spring'
12
+ import { warnOnce } from '../internal/warnOnce'
11
13
  import {
12
14
  type DecayTransition,
13
15
  type RepeatConfig,
@@ -37,8 +39,6 @@ export type CallbackFactory = (
37
39
  step: number | undefined,
38
40
  ) => AnimationCallback | undefined
39
41
 
40
- const DEFAULT_TIMING_DURATION = 250
41
-
42
42
  function buildSpring(
43
43
  cfg: SpringTransition,
44
44
  toValue: number | string,
@@ -94,26 +94,101 @@ function buildOne(
94
94
  return buildSpring(cfg as SpringTransition, toValue, cb)
95
95
  }
96
96
 
97
+ /**
98
+ * Normalised repeat: `count` is the total number of iterations
99
+ * (`Number.POSITIVE_INFINITY` for `'infinite'`), `alternate` is the resolved
100
+ * flag, `explicitAlternate` says whether the caller set `alternate` by hand.
101
+ */
102
+ export interface NormalizedRepeat {
103
+ count: number
104
+ alternate: boolean
105
+ explicitAlternate: boolean
106
+ }
107
+
108
+ /**
109
+ * Reduce the three public `repeat` shapes to one record, or `undefined` when
110
+ * the animation runs once. A count below `1` is treated as "run once" and
111
+ * warns in dev: Reanimated's `withRepeat` reads `0` and negative counts as
112
+ * endless, so forwarding them would turn `repeat: 0` into an infinite loop.
113
+ */
114
+ export function normalizeRepeat(
115
+ repeat: RepeatConfig | undefined,
116
+ ): NormalizedRepeat | undefined {
117
+ if (repeat === undefined) return undefined
118
+ if (repeat === 'infinite') {
119
+ return {
120
+ count: Number.POSITIVE_INFINITY,
121
+ alternate: true,
122
+ explicitAlternate: false,
123
+ }
124
+ }
125
+ const rawCount = typeof repeat === 'number' ? repeat : repeat.count
126
+ const alternate = typeof repeat === 'number' ? true : repeat.alternate
127
+ if (rawCount === 'infinite') {
128
+ return {
129
+ count: Number.POSITIVE_INFINITY,
130
+ alternate: alternate ?? true,
131
+ explicitAlternate: alternate !== undefined,
132
+ }
133
+ }
134
+ if (!(rawCount >= 1)) {
135
+ warnOnce(
136
+ `repeat-count:${String(rawCount)}`,
137
+ `[inertia] repeat count ${String(rawCount)} is below 1 — the ` +
138
+ `animation runs once. Use \`repeat: 2\` or more to repeat, or ` +
139
+ `\`repeat: 'infinite'\`. (Reanimated reads a count of 0 as endless, ` +
140
+ `so it is not forwarded.)`,
141
+ )
142
+ return undefined
143
+ }
144
+ return {
145
+ count: rawCount,
146
+ alternate: alternate ?? true,
147
+ explicitAlternate: alternate !== undefined,
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Total number of iterations an animation built from `repeat` runs, including
153
+ * the first pass. `1` when there is no repeat or the count is below 1;
154
+ * `Number.POSITIVE_INFINITY` for `'infinite'`.
155
+ */
156
+ export function repeatIterationsOf(repeat: RepeatConfig | undefined): number {
157
+ return normalizeRepeat(repeat)?.count ?? 1
158
+ }
159
+
97
160
  /**
98
161
  * Wrap an animation in `withRepeat` per the unified `repeat` shape:
99
162
  * - `number` → finite count, alternating direction
100
163
  * - `'infinite'` → endless, alternating direction
101
164
  * - `{ count, alternate }`→ explicit; `alternate` defaults to `true`
165
+ *
166
+ * Pass `{ sequence: true }` when `animation` is a `withSequence` result.
167
+ * Reanimated's `reverse` flag only swaps the wrapped animation's `toValue`,
168
+ * which a sequence ignores — it restarts at step 0 on every pass — so the
169
+ * flag is not forwarded for sequences. An explicit `alternate: true` on a
170
+ * sequence warns in dev; write the reverse steps into the sequence instead.
102
171
  */
103
172
  export function applyRepeat(
104
173
  animation: unknown,
105
174
  repeat: RepeatConfig | undefined,
175
+ options?: { sequence?: boolean },
106
176
  ) {
107
- if (repeat === undefined) return animation
108
- if (repeat === 'infinite') {
109
- return withRepeat(animation as never, -1, true)
110
- }
111
- if (typeof repeat === 'number') {
112
- return withRepeat(animation as never, repeat, true)
177
+ const r = normalizeRepeat(repeat)
178
+ if (r === undefined) return animation
179
+ const count = Number.isFinite(r.count) ? r.count : -1
180
+ if (options?.sequence) {
181
+ if (r.explicitAlternate && r.alternate) {
182
+ warnOnce(
183
+ 'repeat-sequence-alternate',
184
+ '[inertia] repeat.alternate has no effect on a sequence — Reanimated ' +
185
+ 'restarts a sequence at its first step on every pass. Append the ' +
186
+ 'reverse steps to the sequence to alternate.',
187
+ )
188
+ }
189
+ return withRepeat(animation as never, count, false)
113
190
  }
114
- const count = repeat.count === 'infinite' ? -1 : repeat.count
115
- const alternate = repeat.alternate ?? true
116
- return withRepeat(animation as never, count, alternate)
191
+ return withRepeat(animation as never, count, r.alternate)
117
192
  }
118
193
 
119
194
  /**
@@ -1,4 +1,4 @@
1
- import { withSequence } from 'react-native-reanimated'
1
+ import { withSequence, withTiming } from 'react-native-reanimated'
2
2
  import {
3
3
  applyRepeat,
4
4
  repeatOf,
@@ -13,6 +13,18 @@ import {
13
13
  type TransitionConfig,
14
14
  } from '../types'
15
15
 
16
+ /** A zero-length timing, for a snapped step inside an animated sequence. */
17
+ const INSTANT = { duration: 0 } as const
18
+
19
+ /**
20
+ * True when a resolved step is a bare target rather than a Reanimated
21
+ * animation object. `buildOne` returns the target itself for `no-animation`,
22
+ * which is what the reduced-motion gate collapses every step to.
23
+ */
24
+ function isSnapped(step: unknown): step is number | string {
25
+ return typeof step === 'number' || typeof step === 'string'
26
+ }
27
+
16
28
  /**
17
29
  * True when the value is a `{ to, ...transitionOverride }` sequence step.
18
30
  * Plain numbers and plain transition objects fail this check.
@@ -37,6 +49,12 @@ function isStepObject<V>(
37
49
  * 3. array of either → `withSequence` of resolved steps, with the
38
50
  * top-level `repeat` applied at the **sequence level** (not per step).
39
51
  * Per-step `repeat` overrides remain step-local.
52
+ *
53
+ * `withSequence` writes a `finished` flag onto each argument it is handed, so
54
+ * a bare target throws `Cannot create property 'finished' on number` and takes
55
+ * the whole render down with it. Two paths produce bare targets, and both are
56
+ * handled below rather than at the call site: reduced motion (every step) and
57
+ * a per-step `{ type: 'no-animation' }` override (some steps).
40
58
  */
41
59
  export function resolveAnimatableValue<V extends number | string>(
42
60
  value: AnimatableValue<V>,
@@ -49,8 +67,32 @@ export function resolveAnimatableValue<V extends number | string>(
49
67
  const animations = steps.map((step, i) =>
50
68
  resolveStep(step, stepBase, factory?.('step', i)),
51
69
  )
52
- const seq = withSequence(...(animations as never[]))
53
- return applyRepeat(seq, base ? repeatOf(base) : undefined)
70
+ // The reduced-motion gate. `mergeTransition` hands a `no-animation` base
71
+ // back untouched for every step, so the whole sequence is a snap and there
72
+ // is nothing to sequence. Settle on the last step — where the sequence
73
+ // would have ended. `repeat` and `delay` are already dropped for
74
+ // `no-animation` (`repeatOf` and `delayOf` both return `undefined`), so
75
+ // nothing needs to wrap this.
76
+ //
77
+ // Read off the config rather than sniffing the resolved values: under the
78
+ // Jest mock `withSpring` and `withTiming` also return bare targets, so a
79
+ // shape test reports every sequence as snapped and the gate stops being
80
+ // observable in either direction.
81
+ if (stepBase?.type === 'no-animation' && animations.length > 0) {
82
+ return animations[animations.length - 1]
83
+ }
84
+ // A step that declares `{ type: 'no-animation' }` inside a sequence that
85
+ // otherwise animates resolves to a bare target of its own. The step still
86
+ // owns its slot in the order, so it becomes an instant timing rather than
87
+ // being dropped.
88
+ const seq = withSequence(
89
+ ...(animations.map((a) =>
90
+ isSnapped(a) ? withTiming(a as number, INSTANT) : a,
91
+ ) as never[]),
92
+ )
93
+ return applyRepeat(seq, base ? repeatOf(base) : undefined, {
94
+ sequence: true,
95
+ })
54
96
  }
55
97
  const step = value as SequenceStep<V>
56
98
  const cb = factory?.('animation', undefined)
@@ -4,12 +4,12 @@ import {
4
4
  withSpring,
5
5
  withTiming,
6
6
  } from 'react-native-reanimated'
7
+ import { DEFAULT_TIMING_DURATION } from './constants'
8
+ import { unwrapEasingFactory } from './easing'
7
9
  import { springToReanimated } from './spring'
8
10
  import { type AnimationCallback } from './resolve'
9
11
  import { type TransitionConfig } from '../types'
10
12
 
11
- const DEFAULT_TIMING_DURATION = 250
12
-
13
13
  /**
14
14
  * Worklet-safe single-step animation builder. Mirrors a subset of
15
15
  * `resolveTransition` for the UI-thread path where the transition config is
@@ -58,13 +58,10 @@ export function buildReleaseAnimation(
58
58
  }
59
59
  if (transition.type === 'timing') {
60
60
  // Reanimated 4's `Easing.bezier(...)` returns an `EasingFunctionFactory`
61
- // rather than the function itself. Unwrap inline so consumers calling
61
+ // rather than the function itself. Unwrap so consumers calling
62
62
  // `buildReleaseAnimation` from a gesture worklet don't have to.
63
63
  const e = transition.easing
64
- const easingFn =
65
- e && typeof e === 'object' && 'factory' in e
66
- ? e.factory()
67
- : (e ?? Easing.inOut(Easing.ease))
64
+ const easingFn = e ? unwrapEasingFactory(e) : Easing.inOut(Easing.ease)
68
65
  return withTiming(
69
66
  toValue,
70
67
  {
@@ -28,6 +28,13 @@ export const DEFAULT_SPRING: Required<
28
28
  * names differ but the underlying physics constants are the same. We don't
29
29
  * surface the raw names publicly because the react-spring vocabulary is
30
30
  * what designers and prior-art consumers expect.
31
+ *
32
+ * Settle control is deliberately absent: Reanimated 4 settles springs on an
33
+ * internal `energyThreshold` and ignores Reanimated 3's `restSpeedThreshold`
34
+ * / `restDisplacementThreshold`. Inertia surfaced those two names through
35
+ * `0.0.9`, where they were silently inert — do not re-add them. If a settle
36
+ * knob is ever needed, expose Reanimated 4's `overshootClamping` or
37
+ * `energyThreshold`, which the runtime actually reads.
31
38
  */
32
39
  export function springToReanimated(t: SpringTransition) {
33
40
  'worklet'
@@ -36,7 +43,5 @@ export function springToReanimated(t: SpringTransition) {
36
43
  damping: t.friction ?? DEFAULT_SPRING.friction,
37
44
  mass: t.mass ?? DEFAULT_SPRING.mass,
38
45
  velocity: t.velocity,
39
- restSpeedThreshold: t.restSpeedThreshold,
40
- restDisplacementThreshold: t.restDisplacementThreshold,
41
46
  }
42
47
  }
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  type Ref,
6
6
  } from 'react'
7
7
  import { type BoxShadowValue, type StyleProp } from 'react-native'
8
+ import { type AnimatedProps } from 'react-native-reanimated'
8
9
 
9
10
  /**
10
11
  * A single animation step's destination, optionally overriding the transition
@@ -31,8 +32,6 @@ export interface SpringTransition {
31
32
  friction?: number
32
33
  mass?: number
33
34
  velocity?: number
34
- restSpeedThreshold?: number
35
- restDisplacementThreshold?: number
36
35
  delay?: number
37
36
  repeat?: RepeatConfig
38
37
  }
@@ -130,6 +129,15 @@ export type TransitionInput = TransitionConfig | TransitionName
130
129
 
131
130
  /**
132
131
  * Repeat config — one shape, not three flags. Default `alternate: true`.
132
+ *
133
+ * `count` is the total number of iterations, including the first pass, so
134
+ * `repeat: 2` plays the animation twice. A count below `1` runs the animation
135
+ * once and warns in dev.
136
+ *
137
+ * `alternate` reverses direction on every other pass for a single-value
138
+ * animation. It has no effect on a sequence (an array target): Reanimated
139
+ * restarts a sequence at its first step on every pass. Append the reverse
140
+ * steps to the sequence to alternate.
133
141
  */
134
142
  export type RepeatConfig =
135
143
  | number
@@ -548,6 +556,14 @@ export interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
548
556
  * Props of a Motion primitive for a given underlying component `C` and a
549
557
  * concrete variants map `V`: the component's own props (minus `style`, which
550
558
  * we replace with an animated style) intersected with the Motion props.
559
+ *
560
+ * `style` is taken from `AnimatedProps<…>`, not from `C` directly, because a
561
+ * Motion primitive renders `Animated.createAnimatedComponent(C)` — so the
562
+ * styles it accepts are the ones that animated component accepts. Reanimated
563
+ * 4.5 brands what `useAnimatedStyle` returns, and `ComponentProps<C>['style']`
564
+ * rejects a branded value, which would have made the sanctioned escape hatch
565
+ * (Principle 2: own your `useAnimatedStyle` over Inertia-driven shared values,
566
+ * then pass it to a `Motion.*` primitive) require a cast at every call site.
551
567
  */
552
568
  export type MotionComponentProps<
553
569
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -555,7 +571,11 @@ export type MotionComponentProps<
555
571
  V extends VariantsMap<ComponentProps<C>> = VariantsMap<ComponentProps<C>>,
556
572
  > = Omit<ComponentProps<C>, 'style'> &
557
573
  MotionProps<ComponentProps<C>, V> & {
558
- style?: ComponentProps<C>['style']
574
+ style?: AnimatedProps<ComponentProps<C>> extends {
575
+ style?: infer S
576
+ }
577
+ ? S
578
+ : ComponentProps<C>['style']
559
579
  ref?: Ref<unknown>
560
580
  }
561
581
 
@@ -0,0 +1,21 @@
1
+ import { Extrapolation } from 'react-native-reanimated'
2
+
3
+ /**
4
+ * Extrapolation behavior at the edges of the input range. Mirrors
5
+ * Reanimated's enum so consumers don't need a separate import.
6
+ *
7
+ * - `'clamp'` (default) — output stays pinned at the first/last value
8
+ * outside the input range. Matches Framer Motion's default.
9
+ * - `'identity'` — return the input unchanged outside the range.
10
+ * - `'extend'` — continue the linear slope beyond the range.
11
+ */
12
+ export type ExtrapolationMode = 'clamp' | 'identity' | 'extend'
13
+
14
+ /** Map the public mode name to Reanimated's `Extrapolation` enum. */
15
+ export function mapExtrapolation(
16
+ mode: ExtrapolationMode | undefined,
17
+ ): Extrapolation {
18
+ if (mode === 'identity') return Extrapolation.IDENTITY
19
+ if (mode === 'extend') return Extrapolation.EXTEND
20
+ return Extrapolation.CLAMP
21
+ }
@@ -8,6 +8,7 @@ export {
8
8
  } from './useColorCascade'
9
9
  export {
10
10
  useColorTransition,
11
+ type ColorStyle,
11
12
  type ColorStyleKey,
12
13
  type UseColorTransitionOptions,
13
14
  } from './useColorTransition'
@@ -36,6 +37,8 @@ export {
36
37
  useShadow,
37
38
  type BoxShadowLayer,
38
39
  type ShadowConfig,
40
+ type ShadowStyle,
39
41
  type UseShadowOptions,
40
42
  } from './useShadow'
43
+ export { useTranslateStyle, type TranslateStyle } from './useTranslateStyle'
41
44
  export { useVariants } from './useVariants'
@@ -4,7 +4,7 @@ import {
4
4
  useAnimatedStyle,
5
5
  type SharedValue,
6
6
  } from 'react-native-reanimated'
7
- import type { ColorStyleKey } from './useColorTransition'
7
+ import type { ColorStyle, ColorStyleKey } from './useColorTransition'
8
8
 
9
9
  /**
10
10
  * One layer in a color cascade: its own `progress` shared value (0→1) and the
@@ -70,7 +70,7 @@ export function useColorCascade(
70
70
  rest: string,
71
71
  layers: readonly ColorCascadeLayer[],
72
72
  options?: UseColorCascadeOptions,
73
- ): ReturnType<typeof useAnimatedStyle> {
73
+ ): ColorStyle {
74
74
  const key = options?.key ?? 'backgroundColor'
75
75
 
76
76
  // Resolve the layer chain into two flat arrays the worklet closes over — the
@@ -121,5 +121,5 @@ export function useColorCascade(
121
121
  )
122
122
  }
123
123
  return { [key]: acc }
124
- })
124
+ }) as unknown as ColorStyle
125
125
  }
@@ -20,6 +20,21 @@ export type ColorStyleKey =
20
20
  | 'tintColor'
21
21
  | 'shadowColor'
22
22
 
23
+ /**
24
+ * A one-key colour style fragment, as returned by {@link useColorTransition}
25
+ * and {@link useColorCascade}.
26
+ *
27
+ * Deliberately **not** `ReturnType<typeof useAnimatedStyle>`. Reanimated 4.5
28
+ * brands that value (`AnimatedStyleHandle`), and a branded type is rejected
29
+ * inside a `StyleProp` array — so every call site would need a cast, which is
30
+ * the defect `InterpolatedStyle` was introduced to remove in `0.0.9`. The brand
31
+ * is compile-time only, so the runtime value is unchanged.
32
+ *
33
+ * Optional per key because which slot is filled is chosen at run time from
34
+ * `options.key`; exactly one is ever present.
35
+ */
36
+ export type ColorStyle = { [K in ColorStyleKey]?: string }
37
+
23
38
  export interface UseColorTransitionOptions {
24
39
  /**
25
40
  * Which style slot the interpolated color is emitted under. Defaults to
@@ -56,7 +71,7 @@ export function useColorTransition(
56
71
  progress: SharedValue<number>,
57
72
  range: readonly [string, string],
58
73
  options?: UseColorTransitionOptions,
59
- ): ReturnType<typeof useAnimatedStyle> {
74
+ ): ColorStyle {
60
75
  // Resolve the slot key once on the JS thread so the worklet body
61
76
  // consumes a single string literal — consistent with the JS-thread
62
77
  // resolver principle that keeps `Object.keys`-style walks off the UI
@@ -68,5 +83,5 @@ export function useColorTransition(
68
83
  return useAnimatedStyle(() => {
69
84
  'worklet'
70
85
  return { [key]: interpolateColor(progress.value, [0, 1], [from, to]) }
71
- })
86
+ }) as unknown as ColorStyle
72
87
  }
@@ -1,11 +1,11 @@
1
- import { useCallback, useMemo } from 'react'
1
+ import { useCallback, useEffect, useMemo } from 'react'
2
2
  import { useSharedValue, type SharedValue } from 'react-native-reanimated'
3
3
  import {
4
4
  resolveNamedTransitionProp,
5
5
  useNamedTransitions,
6
6
  useShouldReduceMotion,
7
7
  } from '../config'
8
- import { isFocusVisible } from '../gestures'
8
+ import { installFocusVisibility, isFocusVisible } from '../gestures'
9
9
  import { isTopLevelTransition, resolveTransition } from '../transitions'
10
10
  import {
11
11
  type GestureLayerTransitions,
@@ -110,6 +110,13 @@ export function useGesture(
110
110
  // the callbacks below only ever see concrete configs.
111
111
  const resolved = resolveNamedTransitionProp(transition, useNamedTransitions())
112
112
 
113
+ // The web modality listeners behind `focusVisible` attach on mount — see
114
+ // `focusVisibility.ts` for why mount time is early enough and import time
115
+ // is not an option.
116
+ useEffect(() => {
117
+ installFocusVisibility()
118
+ }, [])
119
+
113
120
  const setLayer = useCallback(
114
121
  (sv: SharedValue<number>, layer: LayerName, target: 0 | 1) => {
115
122
  const cfg = shouldReduceMotion
@@ -1,14 +1,14 @@
1
1
  import { useMemo } from 'react'
2
2
  import type { ImageStyle, TextStyle, ViewStyle } from 'react-native'
3
3
  import {
4
- Extrapolation,
5
4
  interpolate,
6
5
  interpolateColor,
7
6
  useAnimatedStyle,
8
7
  type SharedValue,
9
8
  } from 'react-native-reanimated'
9
+ import { warnOnce } from '../internal/warnOnce'
10
+ import { mapExtrapolation, type ExtrapolationMode } from './extrapolation'
10
11
  import type { ColorStyleKey } from './useColorTransition'
11
- import type { ExtrapolationMode } from './useTransform'
12
12
 
13
13
  /**
14
14
  * Numeric style keys `useInterpolatedStyle` can emit directly (not lifted into
@@ -158,12 +158,6 @@ function evenlySpaced(count: number): number[] {
158
158
  return out
159
159
  }
160
160
 
161
- function mapExtrapolation(mode: ExtrapolationMode | undefined): Extrapolation {
162
- if (mode === 'identity') return Extrapolation.IDENTITY
163
- if (mode === 'extend') return Extrapolation.EXTEND
164
- return Extrapolation.CLAMP
165
- }
166
-
167
161
  /**
168
162
  * Order-preserving structural signature of the map + options. Unlike
169
163
  * `stableSig` (which sorts keys), this walks `map` in insertion order because
@@ -202,8 +196,9 @@ function buildEntries(
202
196
  ? [0, 1]
203
197
  : evenlySpaced(output.length)
204
198
 
205
- if (__DEV__ && explicitInput && explicitInput.length !== output.length) {
206
- console.warn(
199
+ if (explicitInput && explicitInput.length !== output.length) {
200
+ warnOnce(
201
+ `interpolated-style-range:${String(key)}:${explicitInput.length}:${output.length}`,
207
202
  `[inertia] useInterpolatedStyle: inputRange has ${explicitInput.length} stops but the "${String(
208
203
  key,
209
204
  )}" output has ${output.length}. They must match — interpolation results are undefined otherwise.`,
@@ -313,6 +308,13 @@ export function useInterpolatedStyle<K extends keyof InterpolatedStyleMap>(
313
308
  // The worklet builds a `Record<string, unknown>` by design — the emitted keys
314
309
  // are only known from `entries` at run time. `InterpolatedStyle<K>` is the
315
310
  // static statement of that same shape, so the cast is where the two meet.
311
+ //
312
+ // Through `unknown` because Reanimated 4.5 brands what `useAnimatedStyle`
313
+ // returns (`AnimatedStyleHandle`), and a branded type no longer overlaps a
314
+ // plain style object. Erasing the brand is the point of this hook: the
315
+ // returned value has to drop into a `StyleProp<ViewStyle>` array without the
316
+ // consumer casting. The brand is compile-time only, so the runtime value is
317
+ // unchanged — it is still the animated style Reanimated produced.
316
318
  return useAnimatedStyle(() => {
317
319
  'worklet'
318
320
  const out: Record<string, unknown> = {}
@@ -343,7 +345,5 @@ export function useInterpolatedStyle<K extends keyof InterpolatedStyleMap>(
343
345
  }
344
346
  if (transform.length > 0) out.transform = transform
345
347
  return out
346
- }) as InterpolatedStyle<K>
348
+ }) as unknown as InterpolatedStyle<K>
347
349
  }
348
-
349
- declare const __DEV__: boolean
@@ -1,3 +1,4 @@
1
+ import { useMemo } from 'react'
1
2
  import {
2
3
  interpolate,
3
4
  interpolateColor,
@@ -9,6 +10,7 @@ import {
9
10
  resolveBoxShadowInput,
10
11
  type BoxShadowLayer,
11
12
  } from '../internal/boxShadow'
13
+ import { stableSig } from '../transitions/sig'
12
14
 
13
15
  export type { BoxShadowLayer }
14
16
 
@@ -41,6 +43,22 @@ export interface ShadowConfig {
41
43
  boxShadow?: string | readonly BoxShadowLayer[]
42
44
  }
43
45
 
46
+ /**
47
+ * The style fragment {@link useShadow} returns: the native shadow keys plus the
48
+ * CSS `boxShadow`, each present only when the paired configs drive it.
49
+ *
50
+ * Deliberately **not** `ReturnType<typeof useAnimatedStyle>` — see
51
+ * {@link ColorStyle} for why the brand is erased here.
52
+ */
53
+ export type ShadowStyle = {
54
+ shadowColor?: string
55
+ shadowOffset?: { width: number; height: number }
56
+ shadowOpacity?: number
57
+ shadowRadius?: number
58
+ elevation?: number
59
+ boxShadow?: string
60
+ }
61
+
44
62
  export interface UseShadowOptions {
45
63
  /** Shadow state at `progress === 0`. */
46
64
  from: ShadowConfig
@@ -95,7 +113,7 @@ export function useShadow({
95
113
  from,
96
114
  to,
97
115
  progress,
98
- }: UseShadowOptions): ReturnType<typeof useAnimatedStyle> {
116
+ }: UseShadowOptions): ShadowStyle {
99
117
  // Resolve presence + endpoints once on the JS thread so the worklet body
100
118
  // consumes flat literals — consistent with the JS-thread resolver
101
119
  // principle that keeps `Object.keys`-style walks off the UI thread.
@@ -113,13 +131,22 @@ export function useShadow({
113
131
  // boxShadow layers: parse/pair once on the JS thread into flat records so
114
132
  // the worklet only interpolates numbers/colors and concatenates — no
115
133
  // frame-time parsing. `[]` when neither side provides the key.
116
- const boxShadowPairs =
117
- from.boxShadow !== undefined || to.boxShadow !== undefined
118
- ? pairBoxShadowLayers(
119
- resolveBoxShadowInput(from.boxShadow),
120
- resolveBoxShadowInput(to.boxShadow),
121
- )
122
- : []
134
+ //
135
+ // Memoised on the inputs' structure: the pairs array is the one non-scalar
136
+ // the worklet captures, and `from` / `to` are usually inline literals, so
137
+ // without the memo a fresh array each render would rebuild the worklet.
138
+ const boxShadowSig = stableSig([from.boxShadow, to.boxShadow])
139
+ const boxShadowPairs = useMemo(
140
+ () =>
141
+ from.boxShadow !== undefined || to.boxShadow !== undefined
142
+ ? pairBoxShadowLayers(
143
+ resolveBoxShadowInput(from.boxShadow),
144
+ resolveBoxShadowInput(to.boxShadow),
145
+ )
146
+ : [],
147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
148
+ [boxShadowSig],
149
+ )
123
150
 
124
151
  const opacityFrom = from.shadowOpacity ?? 0
125
152
  const opacityTo = to.shadowOpacity ?? 0
@@ -187,5 +214,5 @@ export function useShadow({
187
214
  out.boxShadow = css
188
215
  }
189
216
  return out
190
- })
217
+ }) as unknown as ShadowStyle
191
218
  }