@rootnative/inertia 0.0.9 → 0.0.10

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 (100) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/README.md +7 -7
  3. package/dist/applyBounds-BYGiHrYO.d.mts +18 -0
  4. package/dist/applyBounds-BYGiHrYO.d.ts +18 -0
  5. package/dist/{chunk-E3ALFSH2.js → chunk-2UGWAB6N.js} +78 -70
  6. package/dist/{chunk-ZBHQPVWE.js → chunk-3ZNHPT5Z.js} +2 -2
  7. package/dist/{chunk-IHVMNOS4.js → chunk-46766TBM.js} +2 -2
  8. package/dist/{chunk-MUT6BTZS.js → chunk-4EZ5DBL3.js} +10 -7
  9. package/dist/{chunk-MFAFB4K7.mjs → chunk-6FCATUMY.mjs} +8 -10
  10. package/dist/{chunk-6YIUETBF.mjs → chunk-7BH3CAO5.mjs} +1 -1
  11. package/dist/{chunk-JBK5ZLMH.mjs → chunk-CUHOCONB.mjs} +1 -1
  12. package/dist/{chunk-M4766VUV.mjs → chunk-DRA2K53O.mjs} +1 -1
  13. package/dist/{chunk-L4JFECXU.mjs → chunk-F4HMCXAX.mjs} +6 -3
  14. package/dist/{chunk-W7NTRSPD.mjs → chunk-HEAWMZEM.mjs} +105 -66
  15. package/dist/{chunk-IX6SEOSK.mjs → chunk-JENYDTXH.mjs} +52 -44
  16. package/dist/chunk-KFHZMFAX.js +23 -0
  17. package/dist/{chunk-6AN5X3YE.js → chunk-KXU6VH2Y.js} +10 -11
  18. package/dist/{chunk-WNVHPMBI.js → chunk-N33GSDNQ.js} +106 -65
  19. package/dist/chunk-OH46TGEA.mjs +20 -0
  20. package/dist/{chunk-QNLSCOR5.mjs → chunk-OLOSU2BD.mjs} +1 -1
  21. package/dist/chunk-PLKXCJIB.js +8 -0
  22. package/dist/{chunk-GSUC4HWP.mjs → chunk-QXKPIENP.mjs} +1 -1
  23. package/dist/chunk-UBHQJFUW.js +8 -0
  24. package/dist/{chunk-6NOCY764.js → chunk-VKXSL3ZL.js} +2 -2
  25. package/dist/chunk-XKNEQDAT.js +8 -0
  26. package/dist/{chunk-EN4PN7X3.mjs → chunk-Z23AMUSW.mjs} +1 -1
  27. package/dist/gestureLayer/index.d.mts +3 -3
  28. package/dist/gestureLayer/index.d.ts +3 -3
  29. package/dist/gestureLayer/index.js +12 -18
  30. package/dist/gestureLayer/index.mjs +5 -11
  31. package/dist/index.d.mts +87 -45
  32. package/dist/index.d.ts +87 -45
  33. package/dist/index.js +114 -91
  34. package/dist/index.mjs +66 -59
  35. package/dist/motion/FlatList.d.mts +1 -1
  36. package/dist/motion/FlatList.d.ts +1 -1
  37. package/dist/motion/FlatList.js +5 -5
  38. package/dist/motion/FlatList.mjs +4 -4
  39. package/dist/motion/Image.d.mts +1 -1
  40. package/dist/motion/Image.d.ts +1 -1
  41. package/dist/motion/Image.js +5 -5
  42. package/dist/motion/Image.mjs +4 -4
  43. package/dist/motion/Pressable.d.mts +1 -1
  44. package/dist/motion/Pressable.d.ts +1 -1
  45. package/dist/motion/Pressable.js +5 -5
  46. package/dist/motion/Pressable.mjs +4 -4
  47. package/dist/motion/ScrollView.d.mts +1 -1
  48. package/dist/motion/ScrollView.d.ts +1 -1
  49. package/dist/motion/ScrollView.js +5 -5
  50. package/dist/motion/ScrollView.mjs +4 -4
  51. package/dist/motion/Text.d.mts +1 -1
  52. package/dist/motion/Text.d.ts +1 -1
  53. package/dist/motion/Text.js +5 -5
  54. package/dist/motion/Text.mjs +4 -4
  55. package/dist/motion/View.d.mts +1 -1
  56. package/dist/motion/View.d.ts +1 -1
  57. package/dist/motion/View.js +5 -5
  58. package/dist/motion/View.mjs +4 -4
  59. package/dist/touch/index.d.mts +2 -1
  60. package/dist/touch/index.d.ts +2 -1
  61. package/dist/touch/index.js +16 -31
  62. package/dist/touch/index.mjs +11 -29
  63. package/dist/{types-DyJpG64F.d.mts → types-08mG8fNk.d.mts} +10 -3
  64. package/dist/{types-DyJpG64F.d.ts → types-08mG8fNk.d.ts} +10 -3
  65. package/dist/{useGesture-DbH46EBp.d.ts → useGesture-D1lHeQQ6.d.ts} +1 -1
  66. package/dist/{useGesture-CnZQnYHH.d.mts → useGesture-DjszaHWz.d.mts} +1 -1
  67. package/package.json +4 -2
  68. package/src/config/namedTransitions.ts +7 -9
  69. package/src/gestureLayer/index.ts +3 -0
  70. package/src/gestureLayer/useGestureLayer.ts +6 -9
  71. package/src/gestures/focusVisibility.ts +19 -17
  72. package/src/gestures/index.ts +1 -1
  73. package/src/index.ts +9 -0
  74. package/src/layout/resolveLayout.ts +4 -1
  75. package/src/layout/useSharedLayout.ts +2 -1
  76. package/src/motion/createMotionComponent.tsx +92 -44
  77. package/src/presence/Presence.tsx +5 -7
  78. package/src/touch/applyBounds.ts +30 -0
  79. package/src/touch/index.ts +1 -0
  80. package/src/touch/useTouchDrag.ts +15 -45
  81. package/src/transitions/constants.ts +14 -0
  82. package/src/transitions/easing.ts +23 -11
  83. package/src/transitions/index.ts +5 -0
  84. package/src/transitions/keys.ts +0 -2
  85. package/src/transitions/resolve.ts +86 -11
  86. package/src/transitions/resolveSequence.ts +3 -1
  87. package/src/transitions/runtime.ts +4 -7
  88. package/src/transitions/spring.ts +7 -2
  89. package/src/types.ts +9 -2
  90. package/src/values/extrapolation.ts +21 -0
  91. package/src/values/index.ts +1 -0
  92. package/src/values/useGesture.ts +9 -2
  93. package/src/values/useInterpolatedStyle.ts +5 -12
  94. package/src/values/useShadow.ts +18 -7
  95. package/src/values/useSpring.ts +34 -25
  96. package/src/values/useTransform.ts +24 -20
  97. package/src/values/useTranslateStyle.ts +31 -0
  98. package/dist/chunk-24NUL23E.js +0 -8
  99. package/dist/chunk-4XBX7KOD.js +0 -8
  100. package/dist/chunk-Z5USXWTE.js +0 -8
@@ -50,7 +50,9 @@ export function resolveAnimatableValue<V extends number | string>(
50
50
  resolveStep(step, stepBase, factory?.('step', i)),
51
51
  )
52
52
  const seq = withSequence(...(animations as never[]))
53
- return applyRepeat(seq, base ? repeatOf(base) : undefined)
53
+ return applyRepeat(seq, base ? repeatOf(base) : undefined, {
54
+ sequence: true,
55
+ })
54
56
  }
55
57
  const step = value as SequenceStep<V>
56
58
  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
@@ -31,8 +31,6 @@ export interface SpringTransition {
31
31
  friction?: number
32
32
  mass?: number
33
33
  velocity?: number
34
- restSpeedThreshold?: number
35
- restDisplacementThreshold?: number
36
34
  delay?: number
37
35
  repeat?: RepeatConfig
38
36
  }
@@ -130,6 +128,15 @@ export type TransitionInput = TransitionConfig | TransitionName
130
128
 
131
129
  /**
132
130
  * Repeat config — one shape, not three flags. Default `alternate: true`.
131
+ *
132
+ * `count` is the total number of iterations, including the first pass, so
133
+ * `repeat: 2` plays the animation twice. A count below `1` runs the animation
134
+ * once and warns in dev.
135
+ *
136
+ * `alternate` reverses direction on every other pass for a single-value
137
+ * animation. It has no effect on a sequence (an array target): Reanimated
138
+ * restarts a sequence at its first step on every pass. Append the reverse
139
+ * steps to the sequence to alternate.
133
140
  */
134
141
  export type RepeatConfig =
135
142
  | number
@@ -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
+ }
@@ -38,4 +38,5 @@ export {
38
38
  type ShadowConfig,
39
39
  type UseShadowOptions,
40
40
  } from './useShadow'
41
+ export { useTranslateStyle } from './useTranslateStyle'
41
42
  export { useVariants } from './useVariants'
@@ -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.`,
@@ -345,5 +340,3 @@ export function useInterpolatedStyle<K extends keyof InterpolatedStyleMap>(
345
340
  return out
346
341
  }) as InterpolatedStyle<K>
347
342
  }
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
 
@@ -113,13 +115,22 @@ export function useShadow({
113
115
  // boxShadow layers: parse/pair once on the JS thread into flat records so
114
116
  // the worklet only interpolates numbers/colors and concatenates — no
115
117
  // 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
- : []
118
+ //
119
+ // Memoised on the inputs' structure: the pairs array is the one non-scalar
120
+ // the worklet captures, and `from` / `to` are usually inline literals, so
121
+ // without the memo a fresh array each render would rebuild the worklet.
122
+ const boxShadowSig = stableSig([from.boxShadow, to.boxShadow])
123
+ const boxShadowPairs = useMemo(
124
+ () =>
125
+ from.boxShadow !== undefined || to.boxShadow !== undefined
126
+ ? pairBoxShadowLayers(
127
+ resolveBoxShadowInput(from.boxShadow),
128
+ resolveBoxShadowInput(to.boxShadow),
129
+ )
130
+ : [],
131
+ // eslint-disable-next-line react-hooks/exhaustive-deps
132
+ [boxShadowSig],
133
+ )
123
134
 
124
135
  const opacityFrom = from.shadowOpacity ?? 0
125
136
  const opacityTo = to.shadowOpacity ?? 0
@@ -6,7 +6,12 @@ import {
6
6
  withSpring,
7
7
  type SharedValue,
8
8
  } from 'react-native-reanimated'
9
- import { lookupNamedTransition, useNamedTransitions } from '../config'
9
+ import {
10
+ lookupNamedTransition,
11
+ useNamedTransitions,
12
+ useShouldReduceMotion,
13
+ } from '../config'
14
+ import { warnOnce } from '../internal/warnOnce'
10
15
  import { springToReanimated } from '../transitions/spring'
11
16
  import {
12
17
  type NamedTransitions,
@@ -14,8 +19,6 @@ import {
14
19
  type TransitionName,
15
20
  } from '../types'
16
21
 
17
- declare const __DEV__: boolean
18
-
19
22
  /**
20
23
  * Animate a shared value toward `target` with spring physics, using the
21
24
  * library's react-spring vocabulary (`tension` / `friction` / `mass`).
@@ -35,12 +38,16 @@ declare const __DEV__: boolean
35
38
  * must resolve to a spring config — a name registered as timing / decay /
36
39
  * no-animation warns in dev and falls back to the default spring (reach for
37
40
  * `useAnimation` when the named transition's type should be honored).
41
+ *
42
+ * Reduced motion (via `<MotionConfig reducedMotion>`) is honoured: the output
43
+ * is assigned the target directly, with no spring, on both paths.
38
44
  */
39
45
  export function useSpring(
40
46
  target: number | SharedValue<number>,
41
47
  config?: SpringTransition | TransitionName,
42
48
  ): SharedValue<number> {
43
49
  const spring = resolveSpringInput(config, useNamedTransitions())
50
+ const shouldReduceMotion = useShouldReduceMotion()
44
51
  // Reanimated config is rebuilt only when the public config object changes
45
52
  // shape. The worklet path reads this from JS-thread closure capture, which
46
53
  // is fine: it's the resolved config that's invariant across UI-thread
@@ -52,14 +59,7 @@ export function useSpring(
52
59
  // depending on `config` itself would rebuild the Reanimated config on
53
60
  // every render and defeat the memo.
54
61
  // eslint-disable-next-line react-hooks/exhaustive-deps
55
- [
56
- spring?.tension,
57
- spring?.friction,
58
- spring?.mass,
59
- spring?.velocity,
60
- spring?.restSpeedThreshold,
61
- spring?.restDisplacementThreshold,
62
- ],
62
+ [spring?.tension, spring?.friction, spring?.mass, spring?.velocity],
63
63
  )
64
64
 
65
65
  const isSharedTarget = isSharedValue(target)
@@ -72,28 +72,38 @@ export function useSpring(
72
72
  // a stale closure.
73
73
  useEffect(() => {
74
74
  if (isSharedTarget) return
75
- output.value = withSpring(target as number, reanimConfig)
75
+ output.value = shouldReduceMotion
76
+ ? (target as number)
77
+ : withSpring(target as number, reanimConfig)
76
78
  // `output` is identity-stable per hook instance (Reanimated guarantee).
77
79
  // eslint-disable-next-line react-hooks/exhaustive-deps
78
- }, [isSharedTarget, target, reanimConfig])
80
+ }, [isSharedTarget, target, reanimConfig, shouldReduceMotion])
79
81
 
80
82
  // SharedValue path. `useAnimatedReaction` runs the prepare worklet whenever
81
83
  // its returned value changes; we read `.value` off the target SV and pipe
82
84
  // it through `withSpring` on the UI thread. When the target is a plain
83
85
  // number we never declare a source so the reaction is inert (returns
84
86
  // `null`, never fires `react`).
87
+ //
88
+ // `source` is in the dependency list on purpose. An explicit list replaces
89
+ // Reanimated's closure-derived one, so a caller that swaps which shared
90
+ // value drives the spring would otherwise keep the reaction wired to the
91
+ // old one — the same stale-closure class `useColorCascade` fixed in 0.0.4.
92
+ // A plain-number target maps to `null`, so a changing number does not
93
+ // re-register the inert reaction.
94
+ const source = isSharedTarget ? target : null
85
95
  useAnimatedReaction(
86
96
  () => {
87
97
  'worklet'
88
- if (!isSharedTarget) return null
89
- return (target as SharedValue<number>).value
98
+ if (source === null) return null
99
+ return source.value
90
100
  },
91
101
  (next, prev) => {
92
102
  'worklet'
93
103
  if (next === null || next === prev) return
94
- output.value = withSpring(next, reanimConfig)
104
+ output.value = shouldReduceMotion ? next : withSpring(next, reanimConfig)
95
105
  },
96
- [isSharedTarget, reanimConfig],
106
+ [source, reanimConfig, shouldReduceMotion],
97
107
  )
98
108
 
99
109
  // Stop the in-flight spring when the owning component unmounts so its
@@ -122,14 +132,13 @@ function resolveSpringInput(
122
132
  if (typeof config !== 'string') return config
123
133
  const cfg = lookupNamedTransition(config, registry)
124
134
  if (cfg.type === undefined || cfg.type === 'spring') return cfg
125
- if (__DEV__) {
126
- console.warn(
127
- `[inertia] Named transition "${config}" resolves to type ` +
128
- `'${cfg.type}', but useSpring / useBooleanSpring are spring-only — ` +
129
- `falling back to the default spring. Use useAnimation to honor ` +
130
- `non-spring named transitions.`,
131
- )
132
- }
135
+ warnOnce(
136
+ `spring-only:${config}`,
137
+ `[inertia] Named transition "${config}" resolves to type ` +
138
+ `'${cfg.type}', but useSpring / useBooleanSpring are spring-only — ` +
139
+ `falling back to the default spring. Use useAnimation to honor ` +
140
+ `non-spring named transitions.`,
141
+ )
133
142
  return undefined
134
143
  }
135
144
 
@@ -1,5 +1,5 @@
1
+ import { useMemo } from 'react'
1
2
  import {
2
- Extrapolation,
3
3
  interpolate,
4
4
  interpolateColor,
5
5
  useDerivedValue,
@@ -11,17 +11,10 @@ import {
11
11
  // import is always available wherever Inertia is.
12
12
  import { isWorkletFunction } from 'react-native-worklets'
13
13
  import { warnNonWorkletOnce } from '../internal/nonWorkletWarning'
14
+ import { stableSig } from '../transitions/sig'
15
+ import { mapExtrapolation, type ExtrapolationMode } from './extrapolation'
14
16
 
15
- /**
16
- * Extrapolation behavior at the edges of the input range. Mirrors
17
- * Reanimated's enum so consumers don't need a separate import.
18
- *
19
- * - `'clamp'` (default) — output stays pinned at the first/last value
20
- * outside the input range. Matches Framer Motion's default.
21
- * - `'identity'` — return the input unchanged outside the range.
22
- * - `'extend'` — continue the linear slope beyond the range.
23
- */
24
- export type ExtrapolationMode = 'clamp' | 'identity' | 'extend'
17
+ export type { ExtrapolationMode }
25
18
 
26
19
  export interface UseTransformOptions {
27
20
  extrapolateLeft?: ExtrapolationMode
@@ -34,7 +27,10 @@ export interface UseTransformOptions {
34
27
  * ```ts
35
28
  * const x = useMotionValue(0)
36
29
  * const y = useMotionValue(0)
37
- * const distance = useTransform(() => Math.sqrt(x.value ** 2 + y.value ** 2))
30
+ * const distance = useTransform(() => {
31
+ * 'worklet'
32
+ * return Math.sqrt(x.value ** 2 + y.value ** 2)
33
+ * })
38
34
  * ```
39
35
  *
40
36
  * The transformer MUST be a worklet — put the `'worklet'` directive as its
@@ -87,6 +83,20 @@ export function useTransform<T>(
87
83
  // `useDerivedValue` exactly once. Keeping the hook call unconditional
88
84
  // satisfies rules-of-hooks; per-call branching (transformer vs
89
85
  // interpolation) is decided once at JS time, never at frame time.
86
+ // Range arrays are memoised on their contents: callers routinely write
87
+ // `useTransform(x, [0, 1], [0, 100])` inline, and a fresh array each render
88
+ // would otherwise change the producer's closure and rebuild the derived
89
+ // value's worklet every render.
90
+ const rangeSig = stableSig([inputRange, outputRange])
91
+ const ranges = useMemo(
92
+ () => ({
93
+ input: (inputRange ?? []) as readonly number[],
94
+ output: (outputRange ?? []) as readonly (number | string)[],
95
+ }),
96
+ // eslint-disable-next-line react-hooks/exhaustive-deps
97
+ [rangeSig],
98
+ )
99
+
90
100
  let producer: () => unknown
91
101
  if (typeof arg1 === 'function') {
92
102
  // Transformer overload. Must be a worklet — the directive-wrapped
@@ -110,8 +120,8 @@ export function useTransform<T>(
110
120
  // Interpolation overload. We pre-resolve everything JS-side so the
111
121
  // worklet body only consumes flat values.
112
122
  const source = arg1
113
- const input = inputRange as readonly number[]
114
- const output = outputRange as readonly (number | string)[]
123
+ const input = ranges.input
124
+ const output = ranges.output
115
125
  const isColor = output.length > 0 && typeof output[0] === 'string'
116
126
  const extrapolateLeft = mapExtrapolation(options?.extrapolateLeft)
117
127
  const extrapolateRight = mapExtrapolation(options?.extrapolateRight)
@@ -140,9 +150,3 @@ export function useTransform<T>(
140
150
  | SharedValue<number>
141
151
  | SharedValue<string>
142
152
  }
143
-
144
- function mapExtrapolation(mode: ExtrapolationMode | undefined): Extrapolation {
145
- if (mode === 'identity') return Extrapolation.IDENTITY
146
- if (mode === 'extend') return Extrapolation.EXTEND
147
- return Extrapolation.CLAMP
148
- }
@@ -0,0 +1,31 @@
1
+ import { useAnimatedStyle, type SharedValue } from 'react-native-reanimated'
2
+
3
+ /**
4
+ * Animated style that translates by two shared values:
5
+ * `transform: [{ translateX: x }, { translateY: y }]`.
6
+ *
7
+ * This is the style fragment every drag-style hook returns (`useTouchDrag`,
8
+ * and `useDrag` / `usePan` / `useSwipe` in `@rootnative/inertia-gestures`).
9
+ * Use it directly when a custom gesture owns its own translation values.
10
+ *
11
+ * The style owns the whole `transform` key. Do not stack a second transform
12
+ * style beside it — `transform` is one key in React Native, so the later
13
+ * style replaces this one instead of merging. Nest another animated view, or
14
+ * build one style from `x` / `y` with `useInterpolatedStyle`, to add a
15
+ * transform.
16
+ *
17
+ * ```tsx
18
+ * const x = useMotionValue(0)
19
+ * const y = useMotionValue(0)
20
+ * const style = useTranslateStyle(x, y)
21
+ * return <Motion.View style={style} />
22
+ * ```
23
+ */
24
+ export function useTranslateStyle(
25
+ x: SharedValue<number>,
26
+ y: SharedValue<number>,
27
+ ): ReturnType<typeof useAnimatedStyle> {
28
+ return useAnimatedStyle(() => ({
29
+ transform: [{ translateX: x.value }, { translateY: y.value }],
30
+ }))
31
+ }
@@ -1,8 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkE3ALFSH2_js = require('./chunk-E3ALFSH2.js');
4
- var reactNative = require('react-native');
5
-
6
- var MotionView = chunkE3ALFSH2_js.createMotionComponent(reactNative.View);
7
-
8
- exports.MotionView = MotionView;
@@ -1,8 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkE3ALFSH2_js = require('./chunk-E3ALFSH2.js');
4
- var reactNative = require('react-native');
5
-
6
- var MotionText = chunkE3ALFSH2_js.createMotionComponent(reactNative.Text);
7
-
8
- exports.MotionText = MotionText;
@@ -1,8 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkE3ALFSH2_js = require('./chunk-E3ALFSH2.js');
4
- var reactNative = require('react-native');
5
-
6
- var MotionImage = chunkE3ALFSH2_js.createMotionComponent(reactNative.Image);
7
-
8
- exports.MotionImage = MotionImage;