@rootnative/inertia 0.0.0-alpha.0 → 0.0.0-alpha.1

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 (69) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -0
  3. package/dist/gestureLayer/index.d.mts +6 -5
  4. package/dist/gestureLayer/index.d.ts +6 -5
  5. package/dist/gestureLayer/index.js +89 -27
  6. package/dist/gestureLayer/index.js.map +1 -1
  7. package/dist/gestureLayer/index.mjs +89 -27
  8. package/dist/gestureLayer/index.mjs.map +1 -1
  9. package/dist/index.d.mts +127 -16
  10. package/dist/index.d.ts +127 -16
  11. package/dist/index.js +177 -60
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +175 -61
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/motion/Image.d.mts +1 -1
  16. package/dist/motion/Image.d.ts +1 -1
  17. package/dist/motion/Image.js +90 -48
  18. package/dist/motion/Image.js.map +1 -1
  19. package/dist/motion/Image.mjs +90 -48
  20. package/dist/motion/Image.mjs.map +1 -1
  21. package/dist/motion/Pressable.d.mts +1 -1
  22. package/dist/motion/Pressable.d.ts +1 -1
  23. package/dist/motion/Pressable.js +90 -48
  24. package/dist/motion/Pressable.js.map +1 -1
  25. package/dist/motion/Pressable.mjs +90 -48
  26. package/dist/motion/Pressable.mjs.map +1 -1
  27. package/dist/motion/ScrollView.d.mts +1 -1
  28. package/dist/motion/ScrollView.d.ts +1 -1
  29. package/dist/motion/ScrollView.js +90 -48
  30. package/dist/motion/ScrollView.js.map +1 -1
  31. package/dist/motion/ScrollView.mjs +90 -48
  32. package/dist/motion/ScrollView.mjs.map +1 -1
  33. package/dist/motion/Text.d.mts +1 -1
  34. package/dist/motion/Text.d.ts +1 -1
  35. package/dist/motion/Text.js +90 -48
  36. package/dist/motion/Text.js.map +1 -1
  37. package/dist/motion/Text.mjs +90 -48
  38. package/dist/motion/Text.mjs.map +1 -1
  39. package/dist/motion/View.d.mts +1 -1
  40. package/dist/motion/View.d.ts +1 -1
  41. package/dist/motion/View.js +90 -48
  42. package/dist/motion/View.js.map +1 -1
  43. package/dist/motion/View.mjs +90 -48
  44. package/dist/motion/View.mjs.map +1 -1
  45. package/dist/touch/index.d.mts +1 -1
  46. package/dist/touch/index.d.ts +1 -1
  47. package/dist/{types-cU43dEmH.d.mts → types-BzEgiUdJ.d.mts} +58 -9
  48. package/dist/{types-cU43dEmH.d.ts → types-BzEgiUdJ.d.ts} +58 -9
  49. package/dist/{useGesture-BnY65PlQ.d.ts → useGesture-BRLgiNOC.d.ts} +6 -4
  50. package/dist/{useGesture-DxtXdz-K.d.mts → useGesture-C0GBS7d2.d.mts} +6 -4
  51. package/llms.txt +8 -4
  52. package/package.json +1 -1
  53. package/src/__type-tests__/motion-value.test-d.ts +39 -0
  54. package/src/config/MotionConfig.tsx +55 -9
  55. package/src/config/MotionConfigContext.ts +21 -1
  56. package/src/config/index.ts +7 -1
  57. package/src/config/namedTransitions.ts +80 -0
  58. package/src/gestureLayer/useGestureLayer.ts +32 -8
  59. package/src/index.ts +20 -2
  60. package/src/motion/createMotionComponent.tsx +36 -6
  61. package/src/transitions/cubicBezier.ts +126 -0
  62. package/src/transitions/index.ts +1 -0
  63. package/src/types.ts +63 -8
  64. package/src/values/useAnimation.ts +12 -6
  65. package/src/values/useBooleanSpring.ts +5 -3
  66. package/src/values/useGesture.ts +25 -7
  67. package/src/values/useMotionValue.ts +11 -0
  68. package/src/values/useScroll.ts +8 -1
  69. package/src/values/useSpring.ts +46 -9
@@ -5,10 +5,22 @@ import {
5
5
  useSharedValue,
6
6
  type AnimatedStyle,
7
7
  } from 'react-native-reanimated'
8
- import { useShouldReduceMotion } from '../config'
9
- import { isTopLevelTransition, resolveTransition } from '../transitions'
8
+ import {
9
+ resolveNamedTransitionProp,
10
+ useNamedTransitions,
11
+ useShouldReduceMotion,
12
+ } from '../config'
13
+ import {
14
+ isTopLevelTransition,
15
+ resolveTransition,
16
+ stableSig,
17
+ } from '../transitions'
10
18
  import { useGesture, type UseGestureHandlers } from '../values/useGesture'
11
- import { type GestureLayerTransitions, type TransitionConfig } from '../types'
19
+ import {
20
+ type GestureLayerTransitions,
21
+ type TransitionConfig,
22
+ type TransitionInput,
23
+ } from '../types'
12
24
 
13
25
  /**
14
26
  * A single gesture-layer style — a flat map of style keys to a value. Numeric
@@ -57,10 +69,11 @@ export interface UseGestureLayerOptions {
57
69
  /**
58
70
  * Transition forwarded to the underlying `useGesture` hook. Either a single
59
71
  * `TransitionConfig` for every gesture layer, or a `GestureLayerTransitions`
60
- * map for per-layer fades. Reduced motion collapses every transition to
61
- * `no-animation`.
72
+ * map for per-layer fades. A `TransitionName` registered on the nearest
73
+ * `<MotionConfig transitions>` is accepted in both positions. Reduced
74
+ * motion collapses every transition to `no-animation`.
62
75
  */
63
- transition?: TransitionConfig | GestureLayerTransitions
76
+ transition?: TransitionInput | GestureLayerTransitions
64
77
  }
65
78
 
66
79
  export interface UseGestureLayerResult {
@@ -128,18 +141,29 @@ export function useGestureLayer(
128
141
  states: GestureLayerStates,
129
142
  options: UseGestureLayerOptions = {},
130
143
  ): UseGestureLayerResult {
131
- const { disabled: isDisabled = false, transition } = options
144
+ const { disabled: isDisabled = false, transition: transitionInput } = options
132
145
  const shouldReduceMotion = useShouldReduceMotion()
146
+ // Resolve registered names here (not just in useGesture) because the
147
+ // `disabled` layer reads the top-level transition locally. Identity is
148
+ // preserved when no names are present; when a name resolves, the effect
149
+ // below keys on the structural signature so a per-render resolve of the
150
+ // map form doesn't re-fire it.
151
+ const transition = resolveNamedTransitionProp(
152
+ transitionInput,
153
+ useNamedTransitions(),
154
+ )
133
155
  const gesture = useGesture(transition)
134
156
  const disabledProgress = useSharedValue(0)
135
157
 
158
+ const transitionSig = stableSig(transition)
136
159
  useEffect(() => {
137
160
  const target = isDisabled ? 1 : 0
138
161
  const cfg = shouldReduceMotion
139
162
  ? ({ type: 'no-animation' } as const)
140
163
  : (disabledTransition(transition) ?? ({ type: 'spring' } as const))
141
164
  disabledProgress.value = resolveTransition(cfg, target) as never
142
- }, [isDisabled, shouldReduceMotion, transition, disabledProgress])
165
+ // eslint-disable-next-line react-hooks/exhaustive-deps
166
+ }, [isDisabled, shouldReduceMotion, transitionSig, disabledProgress])
143
167
 
144
168
  // JS-thread precompute: union of keys across all layers, per-key type
145
169
  // (number vs color), and a rest-fallback table. The worklet body reads
package/src/index.ts CHANGED
@@ -18,12 +18,23 @@ export {
18
18
  MotionScrollView,
19
19
  createMotionComponent,
20
20
  } from './motion'
21
- export { MotionConfig, useMotionConfig, useShouldReduceMotion } from './config'
22
- export type { MotionConfigValue, ReducedMotion } from './config'
21
+ export {
22
+ MotionConfig,
23
+ resolveNamedTransition,
24
+ useMotionConfig,
25
+ useNamedTransitions,
26
+ useShouldReduceMotion,
27
+ } from './config'
28
+ export type {
29
+ MotionConfigProps,
30
+ MotionConfigValue,
31
+ ReducedMotion,
32
+ } from './config'
23
33
  export { Presence, usePresence } from './presence'
24
34
  export type { PresenceContextValue } from './presence'
25
35
  export {
26
36
  buildReleaseAnimation,
37
+ cubicBezier,
27
38
  resolveTransition,
28
39
  resolveAnimatableValue,
29
40
  ensureWorkletEasing,
@@ -56,17 +67,24 @@ export type {
56
67
  AnimateStyle,
57
68
  AnimationCallbackInfo,
58
69
  DecayTransition,
70
+ EasingFunction,
71
+ EasingFunctionFactory,
72
+ EasingInput,
59
73
  GestureSubStates,
60
74
  MotionComponent,
61
75
  MotionProps,
76
+ NamedTransitions,
62
77
  NoAnimationTransition,
63
78
  PerPropertyTransition,
79
+ RegisteredTransitions,
64
80
  RepeatConfig,
65
81
  SequenceStep,
66
82
  SpringTransition,
67
83
  TimingTransition,
68
84
  Transition,
69
85
  TransitionConfig,
86
+ TransitionInput,
87
+ TransitionName,
70
88
  VariantController,
71
89
  VariantsMap,
72
90
  } from './types'
@@ -14,7 +14,12 @@ import Animated, {
14
14
  type SharedValue,
15
15
  } from 'react-native-reanimated'
16
16
  import { type LayoutChangeEvent } from 'react-native'
17
- import { useShouldReduceMotion } from '../config'
17
+ import {
18
+ lookupNamedTransition,
19
+ resolveNamedTransitionProp,
20
+ useNamedTransitions,
21
+ useShouldReduceMotion,
22
+ } from '../config'
18
23
  import { isFocusVisible } from '../gestures'
19
24
  import {
20
25
  resolveLayoutTransition,
@@ -167,11 +172,16 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
167
172
  shadowOffsetHeight: 0,
168
173
  }
169
174
 
175
+ // Both lookups run after the factory has resolved registered transition
176
+ // names into concrete configs (`resolveNamedTransitionProp`), so the
177
+ // `TransitionInput` values on the map forms are narrowed to `TransitionConfig`
178
+ // here — a string can no longer reach these call sites at runtime.
170
179
  function transitionFor<S>(
171
180
  prop: keyof S,
172
181
  transition: Transition<S> | undefined,
173
182
  ): TransitionConfig | undefined {
174
183
  if (!transition) return undefined
184
+ if (typeof transition === 'string') return undefined
175
185
  if (isTopLevelTransition(transition)) return transition
176
186
  // Gesture-layer keys (`pressed`, `hovered`, …) live on the same map as
177
187
  // per-property keys; skip them when looking up a property transition so a
@@ -179,7 +189,9 @@ function transitionFor<S>(
179
189
  // style key named `pressed` (none currently exist, but keep the lookup
180
190
  // honest).
181
191
  if (GESTURE_LAYER_NAME_SET.has(prop as string)) return undefined
182
- return (transition as PerPropertyTransition<S>)[prop]
192
+ return (transition as PerPropertyTransition<S>)[prop] as
193
+ | TransitionConfig
194
+ | undefined
183
195
  }
184
196
 
185
197
  function gestureLayerTransitionFor<S>(
@@ -187,8 +199,11 @@ function gestureLayerTransitionFor<S>(
187
199
  transition: Transition<S> | undefined,
188
200
  ): TransitionConfig | undefined {
189
201
  if (!transition) return undefined
202
+ if (typeof transition === 'string') return undefined
190
203
  if (isTopLevelTransition(transition)) return transition
191
- return (transition as GestureLayerTransitions)[layer]
204
+ return (transition as GestureLayerTransitions)[layer] as
205
+ | TransitionConfig
206
+ | undefined
192
207
  }
193
208
 
194
209
  /**
@@ -223,11 +238,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
223
238
  initial,
224
239
  animate,
225
240
  exit,
226
- transition,
241
+ transition: transitionProp,
227
242
  variants,
228
243
  controller,
229
244
  gesture,
230
- layout,
245
+ layout: layoutProp,
231
246
  layoutId,
232
247
  onAnimationEnd,
233
248
  style,
@@ -235,11 +250,26 @@ export function createMotionComponent<C extends ComponentType<any>>(
235
250
  ...rest
236
251
  } = props as Props & {
237
252
  style?: unknown
238
- layout?: LayoutProp
253
+ layout?: LayoutProp | string
239
254
  layoutId?: string
240
255
  onLayout?: (event: LayoutChangeEvent) => void
241
256
  }
242
257
 
258
+ // Resolve registered transition names (from the nearest <MotionConfig
259
+ // transitions>) into concrete configs before anything downstream touches
260
+ // the props. Resolution is JS-thread and identity-preserving when no
261
+ // names are present; when names resolve, the registry entries are stable
262
+ // objects, so every signature-keyed memo below stays warm.
263
+ const namedTransitions = useNamedTransitions()
264
+ const transition = resolveNamedTransitionProp(
265
+ transitionProp as Transition<Record<string, unknown>> | undefined,
266
+ namedTransitions,
267
+ )
268
+ const layout: LayoutProp =
269
+ typeof layoutProp === 'string'
270
+ ? lookupNamedTransition(layoutProp, namedTransitions)
271
+ : layoutProp
272
+
243
273
  // Function-form `style={(state) => ...}` is the Pressable render-prop API.
244
274
  // Inertia drives press/focus state through `gesture.*` and merges its own
245
275
  // animated style; a function passed here lands inside a style array where
@@ -0,0 +1,126 @@
1
+ import { Easing } from 'react-native-reanimated'
2
+ import { type EasingInput } from '../types'
3
+
4
+ /**
5
+ * The CSS easing keywords and their canonical cubic-bezier control points
6
+ * (W3C css-easing-1). `linear` is special-cased to Reanimated's identity
7
+ * easing rather than a degenerate bezier.
8
+ */
9
+ const CSS_KEYWORDS: Record<string, readonly [number, number, number, number]> =
10
+ {
11
+ ease: [0.25, 0.1, 0.25, 1],
12
+ 'ease-in': [0.42, 0, 1, 1],
13
+ 'ease-out': [0, 0, 0.58, 1],
14
+ 'ease-in-out': [0.42, 0, 0.58, 1],
15
+ }
16
+
17
+ const CSS_FUNCTION = /^cubic-bezier\((.*)\)$/
18
+
19
+ /**
20
+ * Build a `timing.easing` value from cubic-bezier control points — as four
21
+ * numbers, as a W3C CSS `cubic-bezier(...)` string, or as a CSS easing
22
+ * keyword. Design systems store easing tokens in the CSS format; this helper
23
+ * makes any such token file directly consumable without hand-translating to
24
+ * `Easing.bezier` calls:
25
+ *
26
+ * ```ts
27
+ * cubicBezier(0.2, 0, 0, 1) // number form
28
+ * cubicBezier('cubic-bezier(0.2, 0, 0, 1)') // CSS token form
29
+ * cubicBezier('ease-out') // CSS keywords, incl. 'linear'
30
+ * ```
31
+ *
32
+ * The return value is exactly what `timing.easing` accepts (Reanimated's
33
+ * bezier factory; the resolver worklet-wraps it via `ensureWorkletEasing`
34
+ * like every other easing input), so it works in the `transition` prop, in
35
+ * named transitions registered on `<MotionConfig transitions>`, and in the
36
+ * value-layer hooks:
37
+ *
38
+ * ```tsx
39
+ * <MotionConfig
40
+ * transitions={{
41
+ * 'state-hover': {
42
+ * type: 'timing',
43
+ * duration: 150,
44
+ * easing: cubicBezier(theme.motion.easingStandard),
45
+ * },
46
+ * }}
47
+ * >
48
+ * ```
49
+ *
50
+ * Invalid input **throws** rather than warning: easing tokens are constructed
51
+ * at theme/module setup, and a malformed token should fail loudly there, not
52
+ * silently animate with the wrong curve. Per the CSS spec, `x1` / `x2` must
53
+ * be within `[0, 1]` (`y1` / `y2` are unrestricted). The stepping keywords
54
+ * (`step-start` / `step-end`) and the CSS `linear(...)` function are not
55
+ * easing curves this helper produces — they're intentionally unsupported.
56
+ */
57
+ export function cubicBezier(
58
+ x1: number,
59
+ y1: number,
60
+ x2: number,
61
+ y2: number,
62
+ ): EasingInput
63
+ export function cubicBezier(css: string): EasingInput
64
+ export function cubicBezier(
65
+ first: number | string,
66
+ y1?: number,
67
+ x2?: number,
68
+ y2?: number,
69
+ ): EasingInput {
70
+ if (typeof first === 'string') return fromCss(first)
71
+ return bezier(first, y1 as number, x2 as number, y2 as number, undefined)
72
+ }
73
+
74
+ function fromCss(input: string): EasingInput {
75
+ const token = input.trim().toLowerCase()
76
+ if (token === 'linear') return Easing.linear
77
+ const keyword = CSS_KEYWORDS[token]
78
+ if (keyword) return bezier(...keyword, input)
79
+ const match = CSS_FUNCTION.exec(token)
80
+ if (!match) {
81
+ throw new Error(
82
+ `[inertia] cubicBezier: unsupported easing token ${JSON.stringify(input)}. ` +
83
+ `Expected four numbers, a 'cubic-bezier(x1, y1, x2, y2)' string, or ` +
84
+ `one of the CSS keywords 'linear' | 'ease' | 'ease-in' | 'ease-out' ` +
85
+ `| 'ease-in-out'.`,
86
+ )
87
+ }
88
+ const parts = match[1]!.split(',').map((p) => Number(p.trim()))
89
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
90
+ throw new Error(
91
+ `[inertia] cubicBezier: could not parse ${JSON.stringify(input)} — ` +
92
+ `expected exactly four finite numbers inside cubic-bezier(...).`,
93
+ )
94
+ }
95
+ return bezier(parts[0]!, parts[1]!, parts[2]!, parts[3]!, input)
96
+ }
97
+
98
+ function bezier(
99
+ x1: number,
100
+ y1: number,
101
+ x2: number,
102
+ y2: number,
103
+ source: string | undefined,
104
+ ): EasingInput {
105
+ const describe = () =>
106
+ source !== undefined
107
+ ? JSON.stringify(source)
108
+ : `cubicBezier(${x1}, ${y1}, ${x2}, ${y2})`
109
+ for (const n of [x1, y1, x2, y2]) {
110
+ if (typeof n !== 'number' || !Number.isFinite(n)) {
111
+ throw new Error(
112
+ `[inertia] cubicBezier: ${describe()} — every control point must be ` +
113
+ `a finite number.`,
114
+ )
115
+ }
116
+ }
117
+ // CSS (and the underlying bezier solver) require the curve to be a function
118
+ // of time: x1 / x2 within [0, 1]. y values are unrestricted (overshoot).
119
+ if (x1 < 0 || x1 > 1 || x2 < 0 || x2 > 1) {
120
+ throw new Error(
121
+ `[inertia] cubicBezier: ${describe()} — x1 and x2 must be within ` +
122
+ `[0, 1] (got x1=${x1}, x2=${x2}).`,
123
+ )
124
+ }
125
+ return Easing.bezier(x1, y1, x2, y2)
126
+ }
@@ -1,5 +1,6 @@
1
1
  export { resolveTransition } from './resolve'
2
2
  export { resolveAnimatableValue } from './resolveSequence'
3
+ export { cubicBezier } from './cubicBezier'
3
4
  export { ensureWorkletEasing } from './easing'
4
5
  export { isTopLevelTransition, TRANSITION_CONFIG_KEYS } from './keys'
5
6
  export { buildReleaseAnimation } from './runtime'
package/src/types.ts CHANGED
@@ -77,6 +77,57 @@ export type TransitionConfig =
77
77
  | DecayTransition
78
78
  | NoAnimationTransition
79
79
 
80
+ /**
81
+ * Augmentation point for typed named transitions. Empty by default — the
82
+ * registry is data fed to `<MotionConfig transitions={...}>` at runtime, so
83
+ * out of the box any string is accepted wherever a `TransitionName` is.
84
+ *
85
+ * Consumers who want their registered names to autocomplete (and typos to be
86
+ * compile errors) augment this interface with their design system's names:
87
+ *
88
+ * ```ts
89
+ * declare module '@rootnative/inertia' {
90
+ * interface RegisteredTransitions {
91
+ * 'state-hover': TransitionConfig
92
+ * 'selection': TransitionConfig
93
+ * }
94
+ * }
95
+ * ```
96
+ *
97
+ * The value type is ignored — only the keys matter. Inertia itself never
98
+ * declares a name here: presets are consumer data, not library surface.
99
+ */
100
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
101
+ export interface RegisteredTransitions {}
102
+
103
+ /**
104
+ * A named transition registered on the nearest `<MotionConfig transitions>`.
105
+ * Accepted anywhere a `TransitionConfig` is: the `transition` prop (top-level
106
+ * and per-property), the `layout` prop, and the value-layer hooks. Resolution
107
+ * happens on the JS thread against the nearest provider; unknown names warn
108
+ * in dev and fall back to the library default spring.
109
+ *
110
+ * Plain `string` until `RegisteredTransitions` is augmented; with an
111
+ * augmentation in place it narrows to the registered keys.
112
+ */
113
+ export type TransitionName = keyof RegisteredTransitions extends never
114
+ ? string
115
+ : keyof RegisteredTransitions & string
116
+
117
+ /**
118
+ * The registry shape accepted by `<MotionConfig transitions={...}>`: named
119
+ * `TransitionConfig`s. Names are consumer vocabulary (design tokens, semantic
120
+ * roles); Inertia ships no presets.
121
+ */
122
+ export type NamedTransitions = Partial<Record<TransitionName, TransitionConfig>>
123
+
124
+ /**
125
+ * A transition given either inline (`TransitionConfig`) or by registered name
126
+ * (`TransitionName`). This is the input type everywhere the public surface
127
+ * accepts a single transition.
128
+ */
129
+ export type TransitionInput = TransitionConfig | TransitionName
130
+
80
131
  /**
81
132
  * Repeat config — one shape, not three flags. Default `alternate: true`.
82
133
  */
@@ -91,7 +142,7 @@ export type RepeatConfig =
91
142
  * here.
92
143
  */
93
144
  export type PerPropertyTransition<S> = {
94
- [K in keyof S]?: TransitionConfig
145
+ [K in keyof S]?: TransitionInput
95
146
  }
96
147
 
97
148
  /**
@@ -104,14 +155,14 @@ export type PerPropertyTransition<S> = {
104
155
  * collide with the primitive's inferred style keys.
105
156
  */
106
157
  export interface GestureLayerTransitions {
107
- pressed?: TransitionConfig
108
- focused?: TransitionConfig
109
- focusVisible?: TransitionConfig
110
- hovered?: TransitionConfig
158
+ pressed?: TransitionInput
159
+ focused?: TransitionInput
160
+ focusVisible?: TransitionInput
161
+ hovered?: TransitionInput
111
162
  }
112
163
 
113
164
  export type Transition<S> =
114
- | TransitionConfig
165
+ | TransitionInput
115
166
  | (PerPropertyTransition<S> & GestureLayerTransitions)
116
167
 
117
168
  /**
@@ -293,7 +344,9 @@ export interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
293
344
  gesture?: GestureSubStates<C>
294
345
  /**
295
346
  * Per-property or top-level transition config. Per-property entries take
296
- * precedence over the top-level transition.
347
+ * precedence over the top-level transition. Anywhere a config object is
348
+ * accepted (top-level, per-property, per gesture layer) a `TransitionName`
349
+ * registered on the nearest `<MotionConfig transitions>` is accepted too.
297
350
  */
298
351
  transition?: Transition<AnimateStyle<C>>
299
352
  /**
@@ -305,6 +358,8 @@ export interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
305
358
  * - `true` — animate with the library's default spring.
306
359
  * - `TransitionConfig` — spring (react-spring vocab) or timing config; the
307
360
  * resolver bridges to Reanimated's `LinearTransition` builder.
361
+ * - `TransitionName` — a name registered on the nearest
362
+ * `<MotionConfig transitions>`.
308
363
  * - omitted / `false` — no layout animation (default).
309
364
  *
310
365
  * Only `'spring'` / `'timing'` / `'no-animation'` map to layout transitions
@@ -316,7 +371,7 @@ export interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
316
371
  * own layout changes, `layoutId` animates from a different element's
317
372
  * last measured rect to this element's current rect.
318
373
  */
319
- layout?: boolean | TransitionConfig
374
+ layout?: boolean | TransitionInput
320
375
  /**
321
376
  * Shared-element transition id. When a Motion primitive with `layoutId`
322
377
  * unmounts, its last on-screen rect is recorded under that id; the next
@@ -1,12 +1,17 @@
1
1
  import { useEffect } from 'react'
2
2
  import { useSharedValue, type SharedValue } from 'react-native-reanimated'
3
- import { useShouldReduceMotion } from '../config'
3
+ import {
4
+ resolveNamedTransition,
5
+ useNamedTransitions,
6
+ useShouldReduceMotion,
7
+ } from '../config'
4
8
  import { resolveTransition, stableSig } from '../transitions'
5
- import { type TransitionConfig } from '../types'
9
+ import { type TransitionInput } from '../types'
6
10
 
7
11
  /**
8
12
  * Drive a `SharedValue<number>` toward `target` with **any** transition shape
9
- * — spring, timing, decay, or no-animation. The general-purpose value-layer
13
+ * — spring, timing, decay, or no-animation or a `TransitionName` registered
14
+ * on the nearest `<MotionConfig transitions>`. The general-purpose value-layer
10
15
  * hook: reach for it when you need raw `useSharedValue + useEffect + withX`
11
16
  * outside the declarative `animate` flow.
12
17
  *
@@ -50,16 +55,17 @@ import { type TransitionConfig } from '../types'
50
55
  */
51
56
  export function useAnimation(
52
57
  target: number,
53
- transition?: TransitionConfig,
58
+ transition?: TransitionInput,
54
59
  ): SharedValue<number> {
55
60
  const output = useSharedValue<number>(target)
56
61
  const shouldReduceMotion = useShouldReduceMotion()
57
- const cfgSig = stableSig(transition)
62
+ const resolved = resolveNamedTransition(transition, useNamedTransitions())
63
+ const cfgSig = stableSig(resolved)
58
64
 
59
65
  useEffect(() => {
60
66
  const cfg = shouldReduceMotion
61
67
  ? ({ type: 'no-animation' } as const)
62
- : (transition ?? ({ type: 'spring' } as const))
68
+ : (resolved ?? ({ type: 'spring' } as const))
63
69
  output.value = resolveTransition(cfg, target) as never
64
70
  // `output` is identity-stable per hook instance.
65
71
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -1,6 +1,6 @@
1
1
  import { type SharedValue } from 'react-native-reanimated'
2
2
  import { useSpring } from './useSpring'
3
- import { type SpringTransition } from '../types'
3
+ import { type SpringTransition, type TransitionName } from '../types'
4
4
 
5
5
  /**
6
6
  * Toggle a 0↔1 progress value with a spring whenever `active` flips.
@@ -23,11 +23,13 @@ import { type SpringTransition } from '../types'
23
23
  *
24
24
  * The spring config follows the same react-spring vocabulary as the rest of
25
25
  * the library (`tension` / `friction` / `mass`); omit it to take the
26
- * library's defaults.
26
+ * library's defaults, or pass a `TransitionName` registered on the nearest
27
+ * `<MotionConfig transitions>` (spring-typed names only — see `useSpring`
28
+ * for the non-spring fallback behaviour).
27
29
  */
28
30
  export function useBooleanSpring(
29
31
  active: boolean,
30
- springConfig?: SpringTransition,
32
+ springConfig?: SpringTransition | TransitionName,
31
33
  ): SharedValue<number> {
32
34
  return useSpring(active ? 1 : 0, springConfig)
33
35
  }
@@ -1,9 +1,17 @@
1
1
  import { useCallback, useMemo } from 'react'
2
2
  import { useSharedValue, type SharedValue } from 'react-native-reanimated'
3
- import { useShouldReduceMotion } from '../config'
3
+ import {
4
+ resolveNamedTransitionProp,
5
+ useNamedTransitions,
6
+ useShouldReduceMotion,
7
+ } from '../config'
4
8
  import { isFocusVisible } from '../gestures'
5
9
  import { isTopLevelTransition, resolveTransition } from '../transitions'
6
- import { type GestureLayerTransitions, type TransitionConfig } from '../types'
10
+ import {
11
+ type GestureLayerTransitions,
12
+ type TransitionConfig,
13
+ type TransitionInput,
14
+ } from '../types'
7
15
 
8
16
  type LayerName = 'pressed' | 'focused' | 'focusVisible' | 'hovered'
9
17
 
@@ -50,8 +58,10 @@ export interface UseGestureResult {
50
58
  *
51
59
  * Transitions follow the same shape as the `gesture` prop's accompanying
52
60
  * `transition`: pass a single `TransitionConfig` to use for every layer, or a
53
- * `GestureLayerTransitions` map to give each layer its own. Layers without an
54
- * explicit transition fall back to the library default spring.
61
+ * `GestureLayerTransitions` map to give each layer its own. A `TransitionName`
62
+ * registered on the nearest `<MotionConfig transitions>` is accepted in both
63
+ * positions (top-level and per-layer). Layers without an explicit transition
64
+ * fall back to the library default spring.
55
65
  *
56
66
  * Reduced motion (via `<MotionConfig reducedMotion>`) collapses every
57
67
  * transition to `no-animation` so state changes snap instead of interpolating
@@ -88,19 +98,23 @@ export interface UseGestureResult {
88
98
  * ```
89
99
  */
90
100
  export function useGesture(
91
- transition?: TransitionConfig | GestureLayerTransitions,
101
+ transition?: TransitionInput | GestureLayerTransitions,
92
102
  ): UseGestureResult {
93
103
  const pressed = useSharedValue(0)
94
104
  const focused = useSharedValue(0)
95
105
  const focusVisible = useSharedValue(0)
96
106
  const hovered = useSharedValue(0)
97
107
  const shouldReduceMotion = useShouldReduceMotion()
108
+ // Registered transition names (top-level string or per-layer string values)
109
+ // resolve against the nearest <MotionConfig transitions> at render time, so
110
+ // the callbacks below only ever see concrete configs.
111
+ const resolved = resolveNamedTransitionProp(transition, useNamedTransitions())
98
112
 
99
113
  const setLayer = useCallback(
100
114
  (sv: SharedValue<number>, layer: LayerName, target: 0 | 1) => {
101
115
  const cfg = shouldReduceMotion
102
116
  ? ({ type: 'no-animation' } as const)
103
- : (layerTransition(layer, transition) ?? ({ type: 'spring' } as const))
117
+ : (layerTransition(layer, resolved) ?? ({ type: 'spring' } as const))
104
118
  sv.value = resolveTransition(cfg, target) as never
105
119
  },
106
120
  // The transition is intentionally read on every call rather than cooked
@@ -134,11 +148,15 @@ export function useGesture(
134
148
  return { pressed, focused, focusVisible, hovered, handlers }
135
149
  }
136
150
 
151
+ // Runs after name resolution, so the `TransitionInput` values on the map form
152
+ // are concrete configs by the time they're read — hence the return cast.
137
153
  function layerTransition(
138
154
  layer: LayerName,
139
155
  transition: TransitionConfig | GestureLayerTransitions | undefined,
140
156
  ): TransitionConfig | undefined {
141
157
  if (!transition) return undefined
142
158
  if (isTopLevelTransition(transition)) return transition
143
- return (transition as GestureLayerTransitions)[layer]
159
+ return (transition as GestureLayerTransitions)[layer] as
160
+ | TransitionConfig
161
+ | undefined
144
162
  }
@@ -26,6 +26,17 @@ import { useSharedValue, type SharedValue } from 'react-native-reanimated'
26
26
  * onPress={() => { x.value = 100 }}
27
27
  * ```
28
28
  */
29
+ // Overloads widen primitive literals: with a bare generic constrained to
30
+ // `number | string`, TS skips literal widening, so `useMotionValue(0)` would
31
+ // infer `SharedValue<0>` and reject every subsequent write. The primitive
32
+ // overloads make `useMotionValue(0)` / `useMotionValue('#fff')` come back as
33
+ // `SharedValue<number>` / `SharedValue<string>`; the generic overload stays
34
+ // last for callers who want an explicit narrower type (e.g. a string union).
35
+ export function useMotionValue(initial: number): SharedValue<number>
36
+ export function useMotionValue(initial: string): SharedValue<string>
37
+ export function useMotionValue<T extends number | string>(
38
+ initial: T,
39
+ ): SharedValue<T>
29
40
  export function useMotionValue<T extends number | string>(
30
41
  initial: T,
31
42
  ): SharedValue<T> {
@@ -25,10 +25,13 @@ export interface UseScrollResult {
25
25
  * ```tsx
26
26
  * const { scrollY, onScroll } = useScroll()
27
27
  * const headerOpacity = useTransform(scrollY, [0, 100], [1, 0])
28
+ * const headerStyle = useAnimatedStyle(() => ({
29
+ * opacity: headerOpacity.value,
30
+ * }))
28
31
  *
29
32
  * return (
30
33
  * <>
31
- * <Motion.View animate={{ opacity: headerOpacity }} />
34
+ * <Animated.View style={headerStyle} />
32
35
  * <Motion.ScrollView onScroll={onScroll} scrollEventThrottle={16}>
33
36
  * …
34
37
  * </Motion.ScrollView>
@@ -36,6 +39,10 @@ export interface UseScrollResult {
36
39
  * )
37
40
  * ```
38
41
  *
42
+ * The derived shared values are consumed through Reanimated interop
43
+ * (`useAnimatedStyle` / `useDerivedValue`) — the declarative `animate` prop
44
+ * takes target values, not shared values.
45
+ *
39
46
  * Scroll events fire on the UI thread, so `scrollX` / `scrollY` are safe to
40
47
  * read from any worklet (`useAnimatedStyle`, `useDerivedValue`,
41
48
  * `useTransform`) without a JS-thread bounce.