@rootnative/inertia 0.0.0-alpha.7 → 0.0.2

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 (42) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-ALRHDFZE.mjs → chunk-2DHKV2XY.mjs} +1 -1
  4. package/dist/chunk-34Q4UM6V.js +8 -0
  5. package/dist/{chunk-CWLFUYIY.mjs → chunk-HFDZAHWP.mjs} +1 -1
  6. package/dist/{chunk-7UDYEFBU.js → chunk-IWLU3VDE.js} +361 -288
  7. package/dist/chunk-KYJROYCG.js +8 -0
  8. package/dist/{chunk-DWCLIBYO.mjs → chunk-LYMLQ6WQ.mjs} +362 -289
  9. package/dist/{chunk-TDSO63CJ.js → chunk-NKA6XR77.js} +2 -2
  10. package/dist/{chunk-6SMPIOIC.mjs → chunk-TUMNV4P7.mjs} +1 -1
  11. package/dist/{chunk-2HYD2ZBK.js → chunk-TUYHTHVV.js} +2 -2
  12. package/dist/{chunk-NXDJZD6A.mjs → chunk-V6BTXHZK.mjs} +1 -1
  13. package/dist/chunk-X5J5M3K3.js +8 -0
  14. package/dist/{chunk-JVBXPF2G.mjs → chunk-ZN3PNGHQ.mjs} +1 -1
  15. package/dist/index.d.mts +229 -43
  16. package/dist/index.d.ts +229 -43
  17. package/dist/index.js +199 -23
  18. package/dist/index.mjs +191 -18
  19. package/dist/motion/Image.js +3 -3
  20. package/dist/motion/Image.mjs +2 -2
  21. package/dist/motion/Pressable.js +3 -3
  22. package/dist/motion/Pressable.mjs +2 -2
  23. package/dist/motion/ScrollView.js +3 -3
  24. package/dist/motion/ScrollView.mjs +2 -2
  25. package/dist/motion/Text.js +3 -3
  26. package/dist/motion/Text.mjs +2 -2
  27. package/dist/motion/View.js +3 -3
  28. package/dist/motion/View.mjs +2 -2
  29. package/llms.txt +6 -1
  30. package/package.json +1 -1
  31. package/src/index.ts +10 -0
  32. package/src/motion/createMotionComponent.tsx +624 -501
  33. package/src/values/index.ts +13 -0
  34. package/src/values/useAnimation.ts +15 -1
  35. package/src/values/useAnimator.ts +72 -0
  36. package/src/values/useColorCascade.ts +107 -0
  37. package/src/values/useInterpolatedStyle.ts +319 -0
  38. package/src/values/useMotionValue.ts +20 -2
  39. package/src/values/useSpring.ts +12 -0
  40. package/dist/chunk-3UTJJ4A3.js +0 -8
  41. package/dist/chunk-4QGXK6TF.js +0 -8
  42. package/dist/chunk-Z7HIOFKQ.js +0 -8
@@ -1,5 +1,11 @@
1
1
  export { useAnimation } from './useAnimation'
2
+ export { useAnimator, type Animator } from './useAnimator'
2
3
  export { useBooleanSpring } from './useBooleanSpring'
4
+ export {
5
+ useColorCascade,
6
+ type ColorCascadeLayer,
7
+ type UseColorCascadeOptions,
8
+ } from './useColorCascade'
3
9
  export {
4
10
  useColorTransition,
5
11
  type ColorStyleKey,
@@ -10,6 +16,13 @@ export {
10
16
  type UseGestureHandlers,
11
17
  type UseGestureResult,
12
18
  } from './useGesture'
19
+ export {
20
+ useInterpolatedStyle,
21
+ type InterpolatedStyleMap,
22
+ type NumericStyleKey,
23
+ type TransformKey,
24
+ type UseInterpolatedStyleOptions,
25
+ } from './useInterpolatedStyle'
13
26
  export { useMotionValue } from './useMotionValue'
14
27
  export { useSpring } from './useSpring'
15
28
  export {
@@ -1,5 +1,9 @@
1
1
  import { useEffect } from 'react'
2
- import { useSharedValue, type SharedValue } from 'react-native-reanimated'
2
+ import {
3
+ cancelAnimation,
4
+ useSharedValue,
5
+ type SharedValue,
6
+ } from 'react-native-reanimated'
3
7
  import {
4
8
  resolveNamedTransition,
5
9
  useNamedTransitions,
@@ -71,5 +75,15 @@ export function useAnimation(
71
75
  // eslint-disable-next-line react-hooks/exhaustive-deps
72
76
  }, [target, cfgSig, shouldReduceMotion])
73
77
 
78
+ // Cancel the in-flight animation on unmount so an infinite-repeat or
79
+ // still-settling `withX` doesn't keep ticking against an orphaned value.
80
+ // `output` is identity-stable per hook instance and owned here.
81
+ useEffect(
82
+ () => () => cancelAnimation(output),
83
+ // `output` is identity-stable per hook instance (Reanimated guarantee).
84
+ // eslint-disable-next-line react-hooks/exhaustive-deps
85
+ [],
86
+ )
87
+
74
88
  return output
75
89
  }
@@ -0,0 +1,72 @@
1
+ import { useCallback } from 'react'
2
+ import { type SharedValue } from 'react-native-reanimated'
3
+ import {
4
+ resolveNamedTransition,
5
+ useNamedTransitions,
6
+ useShouldReduceMotion,
7
+ } from '../config'
8
+ import { resolveTransition } from '../transitions'
9
+ import { type TransitionInput } from '../types'
10
+
11
+ /**
12
+ * Imperative setter that drives a `SharedValue<number>` toward `to`, resolving
13
+ * the transition through the **same context** the declarative surface uses. It
14
+ * is the imperative escape hatch that closes the two footguns of writing
15
+ * `value.value = resolveTransition(config, to)` by hand from an event handler:
16
+ *
17
+ * 1. **Named transitions resolve.** A `TransitionName` registered on the
18
+ * nearest `<MotionConfig transitions>` works here just as it does on the
19
+ * `transition` prop or in `useAnimation`. Raw `resolveTransition` can't
20
+ * reach the registry (names resolve via context), so imperative call sites
21
+ * otherwise rebuild configs the provider already owns.
22
+ * 2. **Reduced motion is respected.** Writes route through the same
23
+ * `no-animation` downgrade `useAnimation` applies under
24
+ * `<MotionConfig reducedMotion>`. Hand-rolled `resolveTransition` writes
25
+ * silently bypass that setting — a correctness bug this hook fixes.
26
+ *
27
+ * The returned callback is stable across renders (it reads the registry and
28
+ * the reduced-motion flag live inside the body), so it can be dropped straight
29
+ * into memoized handlers.
30
+ *
31
+ * This is not a new animation API — it starts animations in Inertia's existing
32
+ * transition vocabulary, so it does not conflict with the "no imperative-only
33
+ * APIs that bypass the declarative surface" scope rule. It is the hooks-layer
34
+ * equivalent of `useMotionValue` + `resolveTransition`, minus the footguns.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * const hovered = useMotionValue(0)
39
+ * const animate = useAnimator()
40
+ *
41
+ * const onHoverIn = () => animate(hovered, 1, 'state-hover')
42
+ * const onHoverOut = () => animate(hovered, 0, 'state-hover')
43
+ * ```
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * // Inline config works too; default is spring when omitted.
48
+ * animate(progress, 1, { type: 'timing', duration: 150 })
49
+ * animate(progress, 0) // spring
50
+ * ```
51
+ */
52
+ export type Animator = (
53
+ value: SharedValue<number>,
54
+ to: number,
55
+ transition?: TransitionInput,
56
+ ) => void
57
+
58
+ export function useAnimator(): Animator {
59
+ const registry = useNamedTransitions()
60
+ const shouldReduceMotion = useShouldReduceMotion()
61
+
62
+ return useCallback(
63
+ (value, to, transition) => {
64
+ const resolved = resolveNamedTransition(transition, registry)
65
+ const cfg = shouldReduceMotion
66
+ ? ({ type: 'no-animation' } as const)
67
+ : (resolved ?? ({ type: 'spring' } as const))
68
+ value.value = resolveTransition(cfg, to) as never
69
+ },
70
+ [registry, shouldReduceMotion],
71
+ )
72
+ }
@@ -0,0 +1,107 @@
1
+ import { useMemo } from 'react'
2
+ import {
3
+ interpolateColor,
4
+ useAnimatedStyle,
5
+ type SharedValue,
6
+ } from 'react-native-reanimated'
7
+ import type { ColorStyleKey } from './useColorTransition'
8
+
9
+ /**
10
+ * One layer in a color cascade: its own `progress` shared value (0→1) and the
11
+ * color it blends toward as that progress rises. Layers are ordered lowest
12
+ * priority first; a later layer wins over an earlier one at equal progress.
13
+ */
14
+ export interface ColorCascadeLayer {
15
+ /** 0→1 driver for this layer. Drive it upstream (spring / boolean / gesture). */
16
+ progress: SharedValue<number>
17
+ /** The color this layer blends toward as `progress` moves 0→1. */
18
+ color: string
19
+ }
20
+
21
+ export interface UseColorCascadeOptions {
22
+ /**
23
+ * Which style slot the composited color is emitted under. Defaults to
24
+ * `backgroundColor` — identical to `useColorTransition`. Override for ring
25
+ * colors (`borderColor`), text (`color`), image tints (`tintColor`), etc.
26
+ */
27
+ key?: ColorStyleKey
28
+ }
29
+
30
+ /**
31
+ * Priority-ordered layered color crossfade: each layer owns an independent
32
+ * `progress` value and blends the accumulated color below it toward its own
33
+ * color as that progress moves 0→1. Later layers win over earlier ones — the
34
+ * array is priority order, **lowest first** (matching the `gesture` prop's
35
+ * fixed-priority cascade, Decision 5).
36
+ *
37
+ * Equivalent to the hand-chained nested-`interpolateColor` shape
38
+ * `focus(error(hover(rest)))`, collapsed into one hook and one worklet:
39
+ *
40
+ * ```tsx
41
+ * const borderStyle = useColorCascade(
42
+ * colors.border,
43
+ * [
44
+ * { progress: hovered, color: colors.borderHover },
45
+ * { progress: errored, color: colors.borderError },
46
+ * { progress: focused, color: colors.borderFocus },
47
+ * ],
48
+ * { key: 'borderColor' },
49
+ * )
50
+ *
51
+ * return <Motion.View style={[styles.field, borderStyle]} />
52
+ * ```
53
+ *
54
+ * This is a pure interpolator — it does not animate on its own. Drive each
55
+ * layer's `progress` upstream with a `useSpring`, `useBooleanSpring`, gesture
56
+ * progress, or anything else producing a 0→1 shared value.
57
+ *
58
+ * For the single-layer case (`rest` ⇄ one active color), reach for
59
+ * [`useColorTransition`](./useColorTransition) — it is the fast path and this
60
+ * hook is not a replacement for it. For a mixed numeric + color cascade, or
61
+ * function-valued layers, drop to a hand-rolled `useAnimatedStyle`.
62
+ *
63
+ * The layer chain is resolved once on the JS thread and memoized on a
64
+ * structural signature (colors + key), so a fresh-but-equal `layers` array
65
+ * each render produces no new UI-thread closure (CLAUDE.md principle 8). The
66
+ * `progress` shared values are read live in the worklet, so their identity is
67
+ * intentionally excluded from the signature.
68
+ */
69
+ export function useColorCascade(
70
+ rest: string,
71
+ layers: readonly ColorCascadeLayer[],
72
+ options?: UseColorCascadeOptions,
73
+ ): ReturnType<typeof useAnimatedStyle> {
74
+ const key = options?.key ?? 'backgroundColor'
75
+
76
+ // Resolve the layer chain into two flat, memoized arrays the worklet closes
77
+ // over — the static colors and the live progress shared values. Both are
78
+ // memoized on the same structural signature so a fresh-but-equal `layers`
79
+ // literal each render yields the same array references; Reanimated then sees
80
+ // an unchanged closure dependency and does not rebuild the UI-thread worklet
81
+ // (CLAUDE.md principle 8). The `progress` shared values are identity-stable
82
+ // per hook instance, so signing the colors + count is enough to detect a
83
+ // real change to the chain.
84
+ const sig = `${key}|${rest}|${layers.length}|${layers
85
+ .map((l) => l.color)
86
+ .join(',')}`
87
+ // eslint-disable-next-line react-hooks/exhaustive-deps
88
+ const colors = useMemo(() => layers.map((l) => l.color), [sig])
89
+ // eslint-disable-next-line react-hooks/exhaustive-deps
90
+ const progressValues = useMemo(() => layers.map((l) => l.progress), [sig])
91
+
92
+ return useAnimatedStyle(() => {
93
+ 'worklet'
94
+ // Fold the layers bottom-up: each layer blends the accumulated color below
95
+ // it toward its own color as its progress rises, so a higher-priority
96
+ // layer at full progress overrides everything beneath it.
97
+ let acc = rest
98
+ for (let i = 0; i < colors.length; i++) {
99
+ acc = interpolateColor(
100
+ progressValues[i]!.value,
101
+ [0, 1],
102
+ [acc, colors[i]!],
103
+ )
104
+ }
105
+ return { [key]: acc }
106
+ })
107
+ }
@@ -0,0 +1,319 @@
1
+ import { useMemo } from 'react'
2
+ import {
3
+ Extrapolation,
4
+ interpolate,
5
+ interpolateColor,
6
+ useAnimatedStyle,
7
+ type SharedValue,
8
+ } from 'react-native-reanimated'
9
+ import type { ColorStyleKey } from './useColorTransition'
10
+ import type { ExtrapolationMode } from './useTransform'
11
+
12
+ /**
13
+ * Numeric style keys `useInterpolatedStyle` can emit directly (not lifted into
14
+ * the transform array). Mirrors the flat numeric surface of the `animate`
15
+ * prop.
16
+ */
17
+ export type NumericStyleKey =
18
+ | 'opacity'
19
+ | 'width'
20
+ | 'height'
21
+ | 'borderRadius'
22
+ | 'shadowOpacity'
23
+ | 'shadowRadius'
24
+ | 'elevation'
25
+ | 'top'
26
+ | 'left'
27
+ | 'right'
28
+ | 'bottom'
29
+ | 'fontSize'
30
+ | 'lineHeight'
31
+ | 'letterSpacing'
32
+ | 'borderWidth'
33
+
34
+ /**
35
+ * Transform keys, lifted into a `transform: [...]` array in the order they
36
+ * appear in the map — the same key-order convention the `animate` prop uses.
37
+ * `rotate` / `rotateX` / `rotateY` take numeric degrees and emit
38
+ * `'<n>deg'` strings.
39
+ */
40
+ export type TransformKey =
41
+ | 'translateX'
42
+ | 'translateY'
43
+ | 'scale'
44
+ | 'scaleX'
45
+ | 'scaleY'
46
+ | 'rotate'
47
+ | 'rotateX'
48
+ | 'rotateY'
49
+
50
+ const TRANSFORM_KEYS = new Set<string>([
51
+ 'translateX',
52
+ 'translateY',
53
+ 'scale',
54
+ 'scaleX',
55
+ 'scaleY',
56
+ 'rotate',
57
+ 'rotateX',
58
+ 'rotateY',
59
+ ])
60
+
61
+ const ROTATION_KEYS = new Set<string>(['rotate', 'rotateX', 'rotateY'])
62
+
63
+ const COLOR_KEYS = new Set<string>([
64
+ 'backgroundColor',
65
+ 'color',
66
+ 'borderColor',
67
+ 'borderTopColor',
68
+ 'borderRightColor',
69
+ 'borderBottomColor',
70
+ 'borderLeftColor',
71
+ 'tintColor',
72
+ 'shadowColor',
73
+ ])
74
+
75
+ /**
76
+ * Interpolation map: each entry maps `progress` onto an output range for one
77
+ * style or transform key. Numeric / transform keys take number stops; color
78
+ * keys take color-string stops. Mixing stop types per key is a compile error.
79
+ */
80
+ export type InterpolatedStyleMap = {
81
+ [K in NumericStyleKey | TransformKey]?: readonly number[]
82
+ } & {
83
+ [K in ColorStyleKey]?: readonly string[]
84
+ }
85
+
86
+ export interface UseInterpolatedStyleOptions {
87
+ /**
88
+ * Input range mapped onto every key's output range. Defaults to `[0, 1]`
89
+ * for 2-stop outputs, and to evenly-spaced stops across `[0, 1]` for
90
+ * longer outputs. When provided, it applies to all keys; a key whose
91
+ * output length differs from `inputRange.length` throws in dev.
92
+ */
93
+ inputRange?: readonly number[]
94
+ /**
95
+ * Edge behavior outside the input range. Defaults to `'clamp'`, matching
96
+ * `useColorTransition`. Applies to numeric keys; color interpolation
97
+ * always clamps (Reanimated's `interpolateColor` has no extrapolation
98
+ * option).
99
+ */
100
+ extrapolate?: ExtrapolationMode
101
+ }
102
+
103
+ /** One key's pre-resolved plan; the worklet consumes these flat records. */
104
+ interface NumericEntry {
105
+ kind: 'numeric'
106
+ key: string
107
+ input: number[]
108
+ output: number[]
109
+ }
110
+ interface RotationEntry {
111
+ kind: 'rotation'
112
+ key: string
113
+ input: number[]
114
+ output: number[]
115
+ }
116
+ interface TransformNumericEntry {
117
+ kind: 'transform-numeric'
118
+ key: string
119
+ input: number[]
120
+ output: number[]
121
+ }
122
+ interface ColorEntry {
123
+ kind: 'color'
124
+ key: string
125
+ input: number[]
126
+ output: string[]
127
+ }
128
+ type Entry = NumericEntry | RotationEntry | TransformNumericEntry | ColorEntry
129
+
130
+ function evenlySpaced(count: number): number[] {
131
+ if (count <= 1) return [0]
132
+ const out: number[] = []
133
+ for (let i = 0; i < count; i++) out.push(i / (count - 1))
134
+ return out
135
+ }
136
+
137
+ function mapExtrapolation(mode: ExtrapolationMode | undefined): Extrapolation {
138
+ if (mode === 'identity') return Extrapolation.IDENTITY
139
+ if (mode === 'extend') return Extrapolation.EXTEND
140
+ return Extrapolation.CLAMP
141
+ }
142
+
143
+ /**
144
+ * Order-preserving structural signature of the map + options. Unlike
145
+ * `stableSig` (which sorts keys), this walks `map` in insertion order because
146
+ * transform lifting depends on it.
147
+ */
148
+ function buildSignature(
149
+ map: InterpolatedStyleMap,
150
+ options: UseInterpolatedStyleOptions | undefined,
151
+ ): string {
152
+ let sig = ''
153
+ for (const key of Object.keys(map)) {
154
+ const output = (map as Record<string, readonly (number | string)[]>)[key]
155
+ sig += `${key}:${JSON.stringify(output)}|`
156
+ }
157
+ sig += `#ir:${JSON.stringify(options?.inputRange)}|ex:${options?.extrapolate ?? ''}`
158
+ return sig
159
+ }
160
+
161
+ /**
162
+ * Resolve the map into flat per-key plans the worklet consumes. Walks `map`
163
+ * in insertion order so transform axes emit in author order.
164
+ */
165
+ function buildEntries(
166
+ map: InterpolatedStyleMap,
167
+ options: UseInterpolatedStyleOptions | undefined,
168
+ ): Entry[] {
169
+ const explicitInput = options?.inputRange
170
+ const entries: Entry[] = []
171
+ for (const key of Object.keys(map) as (keyof InterpolatedStyleMap)[]) {
172
+ const output = map[key] as readonly (number | string)[] | undefined
173
+ if (output === undefined || output.length === 0) continue
174
+
175
+ const input = explicitInput
176
+ ? (explicitInput as number[])
177
+ : output.length === 2
178
+ ? [0, 1]
179
+ : evenlySpaced(output.length)
180
+
181
+ if (__DEV__ && explicitInput && explicitInput.length !== output.length) {
182
+ console.warn(
183
+ `[inertia] useInterpolatedStyle: inputRange has ${explicitInput.length} stops but the "${String(
184
+ key,
185
+ )}" output has ${output.length}. They must match — interpolation results are undefined otherwise.`,
186
+ )
187
+ }
188
+
189
+ const isColor =
190
+ COLOR_KEYS.has(key as string) && typeof output[0] === 'string'
191
+ if (isColor) {
192
+ entries.push({
193
+ kind: 'color',
194
+ key: key as string,
195
+ input,
196
+ output: output as string[],
197
+ })
198
+ } else if (ROTATION_KEYS.has(key as string)) {
199
+ entries.push({
200
+ kind: 'rotation',
201
+ key: key as string,
202
+ input,
203
+ output: output as number[],
204
+ })
205
+ } else if (TRANSFORM_KEYS.has(key as string)) {
206
+ entries.push({
207
+ kind: 'transform-numeric',
208
+ key: key as string,
209
+ input,
210
+ output: output as number[],
211
+ })
212
+ } else {
213
+ entries.push({
214
+ kind: 'numeric',
215
+ key: key as string,
216
+ input,
217
+ output: output as number[],
218
+ })
219
+ }
220
+ }
221
+ return entries
222
+ }
223
+
224
+ /**
225
+ * Map one `progress` shared value onto N style props via `interpolate` /
226
+ * `interpolateColor`, returning an animated style fragment that composes in a
227
+ * style array on any Reanimated-aware host (`Motion.*`, a hand-rolled
228
+ * `Animated.View`). The style-fragment counterpart to `useTransform`'s
229
+ * output-range form, in the same family as `useColorTransition` / `useShadow`.
230
+ *
231
+ * ```tsx
232
+ * const collapseStyle = useInterpolatedStyle(collapseProgress, {
233
+ * height: [expandedHeight, collapsedHeight],
234
+ * fontSize: [expanded.fontSize, collapsed.fontSize],
235
+ * })
236
+ *
237
+ * const labelStyle = useInterpolatedStyle(floatProgress, {
238
+ * translateY: [restingOffset, 0],
239
+ * scale: [restingScale, 1],
240
+ * })
241
+ *
242
+ * return <Motion.View style={[base, collapseStyle, labelStyle]} />
243
+ * ```
244
+ *
245
+ * This is a pure interpolator — it does not animate on its own. Drive
246
+ * `progress` upstream with a `useSpring`, `useBooleanSpring`, gesture
247
+ * progress, or scroll-derived `useTransform`.
248
+ *
249
+ * - Numeric / transform keys route through `interpolate`; color-string stops
250
+ * on a color key route through `interpolateColor` (a multi-stop
251
+ * `useColorTransition` without touching that hook).
252
+ * - Transform keys (`translateX`, `scale`, `rotate`, …) are lifted into a
253
+ * single `transform` array in the order they appear in the map. `rotate*`
254
+ * keys take numeric degrees and emit `'<n>deg'` strings, consistent with
255
+ * the `animate` surface.
256
+ * - `options.inputRange` defaults to `[0, 1]` for 2-stop outputs and to
257
+ * evenly-spaced stops otherwise. `options.extrapolate` defaults to
258
+ * `'clamp'`.
259
+ *
260
+ * For function-valued entries or multi-source composition, drop to a
261
+ * hand-rolled `useAnimatedStyle` (or `useTransform`'s worklet form) — this
262
+ * hook stays fully declarative and hashable so unchanged maps produce zero
263
+ * new UI-thread closures.
264
+ */
265
+ export function useInterpolatedStyle(
266
+ progress: SharedValue<number>,
267
+ map: InterpolatedStyleMap,
268
+ options?: UseInterpolatedStyleOptions,
269
+ ): ReturnType<typeof useAnimatedStyle> {
270
+ const extrapolate = mapExtrapolation(options?.extrapolate)
271
+
272
+ // Order-preserving signature: the map's key order is load-bearing (transform
273
+ // lifting emits axes in author order), so `stableSig` (which sorts keys) is
274
+ // wrong here — sign the ordered key/output pairs plus the options directly.
275
+ const sig = buildSignature(map, options)
276
+
277
+ // Resolve every key's plan once on the JS thread so the worklet body only
278
+ // consumes flat arrays — consistent with the JS-thread resolver principle
279
+ // that keeps `Object.keys`-style walks off the UI thread (CLAUDE.md
280
+ // principle 8). Memoized on `sig` so a fresh-but-equal map literal each
281
+ // render yields the same `entries` reference — Reanimated then sees an
282
+ // unchanged closure dependency and does not rebuild the UI-thread worklet.
283
+ // eslint-disable-next-line react-hooks/exhaustive-deps
284
+ const entries = useMemo<Entry[]>(() => buildEntries(map, options), [sig])
285
+
286
+ return useAnimatedStyle(() => {
287
+ 'worklet'
288
+ const out: Record<string, unknown> = {}
289
+ const transform: Record<string, unknown>[] = []
290
+ for (const e of entries) {
291
+ if (e.kind === 'color') {
292
+ out[e.key] = interpolateColor(progress.value, e.input, e.output)
293
+ } else if (e.kind === 'numeric') {
294
+ out[e.key] = interpolate(progress.value, e.input, e.output, {
295
+ extrapolateLeft: extrapolate,
296
+ extrapolateRight: extrapolate,
297
+ })
298
+ } else if (e.kind === 'transform-numeric') {
299
+ transform.push({
300
+ [e.key]: interpolate(progress.value, e.input, e.output, {
301
+ extrapolateLeft: extrapolate,
302
+ extrapolateRight: extrapolate,
303
+ }),
304
+ })
305
+ } else {
306
+ // rotation — emit a deg string
307
+ const deg = interpolate(progress.value, e.input, e.output, {
308
+ extrapolateLeft: extrapolate,
309
+ extrapolateRight: extrapolate,
310
+ })
311
+ transform.push({ [e.key]: `${deg}deg` })
312
+ }
313
+ }
314
+ if (transform.length > 0) out.transform = transform
315
+ return out
316
+ })
317
+ }
318
+
319
+ declare const __DEV__: boolean
@@ -1,4 +1,9 @@
1
- import { useSharedValue, type SharedValue } from 'react-native-reanimated'
1
+ import { useEffect } from 'react'
2
+ import {
3
+ cancelAnimation,
4
+ useSharedValue,
5
+ type SharedValue,
6
+ } from 'react-native-reanimated'
2
7
 
3
8
  /**
4
9
  * Create an animatable value owned by JS but readable from worklets.
@@ -40,5 +45,18 @@ export function useMotionValue<T extends number | string>(
40
45
  export function useMotionValue<T extends number | string>(
41
46
  initial: T,
42
47
  ): SharedValue<T> {
43
- return useSharedValue<T>(initial)
48
+ const sv = useSharedValue<T>(initial)
49
+ // Cancel any in-flight animation when the owning component unmounts, so a
50
+ // mid-flight (or infinite-repeat) `withX` driving this value stops ticking
51
+ // its worklet once the value is orphaned. The shared value is
52
+ // identity-stable per hook instance and owned by this component, so
53
+ // cancelling on unmount is always safe. Mid-life cancellation stays the
54
+ // consumer's job via the `/reanimated` interop `cancelAnimation`.
55
+ useEffect(
56
+ () => () => cancelAnimation(sv),
57
+ // `sv` is identity-stable per hook instance (Reanimated guarantee).
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ [],
60
+ )
61
+ return sv
44
62
  }
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useMemo } from 'react'
2
2
  import {
3
+ cancelAnimation,
3
4
  useAnimatedReaction,
4
5
  useSharedValue,
5
6
  withSpring,
@@ -95,6 +96,17 @@ export function useSpring(
95
96
  [isSharedTarget, reanimConfig],
96
97
  )
97
98
 
99
+ // Stop the in-flight spring when the owning component unmounts so its
100
+ // worklet doesn't keep ticking against an orphaned value. `output` is
101
+ // identity-stable per hook instance and owned here, so cancelling on
102
+ // unmount is always safe.
103
+ useEffect(
104
+ () => () => cancelAnimation(output),
105
+ // `output` is identity-stable per hook instance (Reanimated guarantee).
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
+ [],
108
+ )
109
+
98
110
  return output
99
111
  }
100
112
 
@@ -1,8 +0,0 @@
1
- 'use strict';
2
-
3
- var chunk7UDYEFBU_js = require('./chunk-7UDYEFBU.js');
4
- var reactNative = require('react-native');
5
-
6
- var MotionImage = chunk7UDYEFBU_js.createMotionComponent(reactNative.Image);
7
-
8
- exports.MotionImage = MotionImage;
@@ -1,8 +0,0 @@
1
- 'use strict';
2
-
3
- var chunk7UDYEFBU_js = require('./chunk-7UDYEFBU.js');
4
- var reactNative = require('react-native');
5
-
6
- var MotionView = chunk7UDYEFBU_js.createMotionComponent(reactNative.View);
7
-
8
- exports.MotionView = MotionView;
@@ -1,8 +0,0 @@
1
- 'use strict';
2
-
3
- var chunk7UDYEFBU_js = require('./chunk-7UDYEFBU.js');
4
- var reactNative = require('react-native');
5
-
6
- var MotionText = chunk7UDYEFBU_js.createMotionComponent(reactNative.Text);
7
-
8
- exports.MotionText = MotionText;