@rootnative/inertia 0.0.0-alpha.0 → 0.0.0-alpha.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 (79) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -0
  3. package/dist/gestureLayer/index.d.mts +48 -7
  4. package/dist/gestureLayer/index.d.ts +48 -7
  5. package/dist/gestureLayer/index.js +107 -30
  6. package/dist/gestureLayer/index.mjs +107 -30
  7. package/dist/index.d.mts +127 -16
  8. package/dist/index.d.ts +127 -16
  9. package/dist/index.js +177 -62
  10. package/dist/index.mjs +175 -63
  11. package/dist/motion/Image.d.mts +1 -1
  12. package/dist/motion/Image.d.ts +1 -1
  13. package/dist/motion/Image.js +90 -50
  14. package/dist/motion/Image.mjs +90 -50
  15. package/dist/motion/Pressable.d.mts +1 -1
  16. package/dist/motion/Pressable.d.ts +1 -1
  17. package/dist/motion/Pressable.js +90 -50
  18. package/dist/motion/Pressable.mjs +90 -50
  19. package/dist/motion/ScrollView.d.mts +1 -1
  20. package/dist/motion/ScrollView.d.ts +1 -1
  21. package/dist/motion/ScrollView.js +90 -50
  22. package/dist/motion/ScrollView.mjs +90 -50
  23. package/dist/motion/Text.d.mts +1 -1
  24. package/dist/motion/Text.d.ts +1 -1
  25. package/dist/motion/Text.js +90 -50
  26. package/dist/motion/Text.mjs +90 -50
  27. package/dist/motion/View.d.mts +1 -1
  28. package/dist/motion/View.d.ts +1 -1
  29. package/dist/motion/View.js +90 -50
  30. package/dist/motion/View.mjs +90 -50
  31. package/dist/testing/index.js +0 -2
  32. package/dist/testing/index.mjs +0 -2
  33. package/dist/touch/index.d.mts +1 -1
  34. package/dist/touch/index.d.ts +1 -1
  35. package/dist/touch/index.js +0 -2
  36. package/dist/touch/index.mjs +0 -2
  37. package/dist/{types-cU43dEmH.d.mts → types-BzEgiUdJ.d.mts} +58 -9
  38. package/dist/{types-cU43dEmH.d.ts → types-BzEgiUdJ.d.ts} +58 -9
  39. package/dist/{useGesture-BnY65PlQ.d.ts → useGesture-BRLgiNOC.d.ts} +6 -4
  40. package/dist/{useGesture-DxtXdz-K.d.mts → useGesture-C0GBS7d2.d.mts} +6 -4
  41. package/llms.txt +9 -5
  42. package/package.json +2 -1
  43. package/src/config/MotionConfig.tsx +55 -9
  44. package/src/config/MotionConfigContext.ts +21 -1
  45. package/src/config/index.ts +7 -1
  46. package/src/config/namedTransitions.ts +80 -0
  47. package/src/gestureLayer/index.ts +1 -0
  48. package/src/gestureLayer/useGestureLayer.ts +92 -8
  49. package/src/index.ts +20 -2
  50. package/src/motion/createMotionComponent.tsx +36 -6
  51. package/src/transitions/cubicBezier.ts +126 -0
  52. package/src/transitions/index.ts +1 -0
  53. package/src/types.ts +63 -8
  54. package/src/values/useAnimation.ts +12 -6
  55. package/src/values/useBooleanSpring.ts +5 -3
  56. package/src/values/useGesture.ts +25 -7
  57. package/src/values/useMotionValue.ts +11 -0
  58. package/src/values/useScroll.ts +8 -1
  59. package/src/values/useSpring.ts +46 -9
  60. package/dist/gestureLayer/index.js.map +0 -1
  61. package/dist/gestureLayer/index.mjs.map +0 -1
  62. package/dist/index.js.map +0 -1
  63. package/dist/index.mjs.map +0 -1
  64. package/dist/motion/Image.js.map +0 -1
  65. package/dist/motion/Image.mjs.map +0 -1
  66. package/dist/motion/Pressable.js.map +0 -1
  67. package/dist/motion/Pressable.mjs.map +0 -1
  68. package/dist/motion/ScrollView.js.map +0 -1
  69. package/dist/motion/ScrollView.mjs.map +0 -1
  70. package/dist/motion/Text.js.map +0 -1
  71. package/dist/motion/Text.mjs.map +0 -1
  72. package/dist/motion/View.js.map +0 -1
  73. package/dist/motion/View.mjs.map +0 -1
  74. package/dist/testing/index.js.map +0 -1
  75. package/dist/testing/index.mjs.map +0 -1
  76. package/dist/touch/index.js.map +0 -1
  77. package/dist/touch/index.mjs.map +0 -1
  78. package/src/__type-tests__/animate.test-d.tsx +0 -88
  79. package/src/__type-tests__/variants.test-d.tsx +0 -67
@@ -4,11 +4,24 @@ import {
4
4
  useAnimatedStyle,
5
5
  useSharedValue,
6
6
  type AnimatedStyle,
7
+ type SharedValue,
7
8
  } from 'react-native-reanimated'
8
- import { useShouldReduceMotion } from '../config'
9
- import { isTopLevelTransition, resolveTransition } from '../transitions'
9
+ import {
10
+ resolveNamedTransitionProp,
11
+ useNamedTransitions,
12
+ useShouldReduceMotion,
13
+ } from '../config'
14
+ import {
15
+ isTopLevelTransition,
16
+ resolveTransition,
17
+ stableSig,
18
+ } from '../transitions'
10
19
  import { useGesture, type UseGestureHandlers } from '../values/useGesture'
11
- import { type GestureLayerTransitions, type TransitionConfig } from '../types'
20
+ import {
21
+ type GestureLayerTransitions,
22
+ type TransitionConfig,
23
+ type TransitionInput,
24
+ } from '../types'
12
25
 
13
26
  /**
14
27
  * A single gesture-layer style — a flat map of style keys to a value. Numeric
@@ -57,10 +70,32 @@ export interface UseGestureLayerOptions {
57
70
  /**
58
71
  * Transition forwarded to the underlying `useGesture` hook. Either a single
59
72
  * `TransitionConfig` for every gesture layer, or a `GestureLayerTransitions`
60
- * map for per-layer fades. Reduced motion collapses every transition to
61
- * `no-animation`.
73
+ * map for per-layer fades. A `TransitionName` registered on the nearest
74
+ * `<MotionConfig transitions>` is accepted in both positions. Reduced
75
+ * motion collapses every transition to `no-animation`.
62
76
  */
63
- transition?: TransitionConfig | GestureLayerTransitions
77
+ transition?: TransitionInput | GestureLayerTransitions
78
+ }
79
+
80
+ /**
81
+ * The per-state 0↔1 progress shared values behind a `useGestureLayer` call.
82
+ * The gesture entries are the same shared values `useGesture` owns; `disabled`
83
+ * is driven by `options.disabled`. All five are stable across renders — feed
84
+ * them into any `useAnimatedStyle` / `useDerivedValue` (or hooks like
85
+ * `useShadow({ progress })`) to derive extra styles from the same gesture
86
+ * wiring without a second `useGesture` call.
87
+ */
88
+ export interface GestureLayerProgress {
89
+ /** 0↔1 progress for the hovered layer (web only — stays at 0 on native). */
90
+ hovered: SharedValue<number>
91
+ /** 0↔1 progress for the focused layer (any focus modality). */
92
+ focused: SharedValue<number>
93
+ /** 0↔1 progress for the focusVisible layer (keyboard focus only). */
94
+ focusVisible: SharedValue<number>
95
+ /** 0↔1 progress for the pressed layer. */
96
+ pressed: SharedValue<number>
97
+ /** 0↔1 progress for the disabled override (driven by `options.disabled`). */
98
+ disabled: SharedValue<number>
64
99
  }
65
100
 
66
101
  export interface UseGestureLayerResult {
@@ -71,6 +106,12 @@ export interface UseGestureLayerResult {
71
106
  style: AnimatedStyle<Record<string, unknown>>
72
107
  /** Handlers to spread on the receiving `Pressable`. */
73
108
  handlers: UseGestureHandlers
109
+ /**
110
+ * Per-state progress shared values — the inputs the composed `style` is
111
+ * derived from. Read-only by convention: writing to them fights the
112
+ * handlers.
113
+ */
114
+ states: GestureLayerProgress
74
115
  }
75
116
 
76
117
  /**
@@ -94,6 +135,13 @@ export interface UseGestureLayerResult {
94
135
  * keys — when active, it lerps the composed value toward the `disabled`
95
136
  * target.
96
137
  *
138
+ * Alongside `style` and `handlers`, the hook returns `states` — the per-state
139
+ * 0↔1 progress shared values the style is derived from (the four gesture
140
+ * layers plus the disabled override). Feed them into other progress-driven
141
+ * hooks (`useShadow`, `useColorTransition`) or a custom `useAnimatedStyle`
142
+ * to derive extra styles from the same gesture wiring — no second
143
+ * `useGesture` call, no duplicated handlers.
144
+ *
97
145
  * Reach for this when you want MD3 / iOS-translucent state-layer overlays
98
146
  * without rewriting the worklet by hand for every consumer; reach for plain
99
147
  * `useGesture()` when you need a composition model this hook doesn't
@@ -123,23 +171,41 @@ export interface UseGestureLayerResult {
123
171
  * )
124
172
  * }
125
173
  * ```
174
+ *
175
+ * @example Elevation crossfade driven by the hover progress
176
+ * ```tsx
177
+ * const { style, handlers, states } = useGestureLayer(layers, options)
178
+ * const shadow = useShadow({ from: restShadow, to: raisedShadow, progress: states.hovered })
179
+ * // <Animated.View style={[style, shadow]} />
180
+ * ```
126
181
  */
127
182
  export function useGestureLayer(
128
183
  states: GestureLayerStates,
129
184
  options: UseGestureLayerOptions = {},
130
185
  ): UseGestureLayerResult {
131
- const { disabled: isDisabled = false, transition } = options
186
+ const { disabled: isDisabled = false, transition: transitionInput } = options
132
187
  const shouldReduceMotion = useShouldReduceMotion()
188
+ // Resolve registered names here (not just in useGesture) because the
189
+ // `disabled` layer reads the top-level transition locally. Identity is
190
+ // preserved when no names are present; when a name resolves, the effect
191
+ // below keys on the structural signature so a per-render resolve of the
192
+ // map form doesn't re-fire it.
193
+ const transition = resolveNamedTransitionProp(
194
+ transitionInput,
195
+ useNamedTransitions(),
196
+ )
133
197
  const gesture = useGesture(transition)
134
198
  const disabledProgress = useSharedValue(0)
135
199
 
200
+ const transitionSig = stableSig(transition)
136
201
  useEffect(() => {
137
202
  const target = isDisabled ? 1 : 0
138
203
  const cfg = shouldReduceMotion
139
204
  ? ({ type: 'no-animation' } as const)
140
205
  : (disabledTransition(transition) ?? ({ type: 'spring' } as const))
141
206
  disabledProgress.value = resolveTransition(cfg, target) as never
142
- }, [isDisabled, shouldReduceMotion, transition, disabledProgress])
207
+ // eslint-disable-next-line react-hooks/exhaustive-deps
208
+ }, [isDisabled, shouldReduceMotion, transitionSig, disabledProgress])
143
209
 
144
210
  // JS-thread precompute: union of keys across all layers, per-key type
145
211
  // (number vs color), and a rest-fallback table. The worklet body reads
@@ -270,9 +336,27 @@ export function useGestureLayer(
270
336
  return out
271
337
  })
272
338
 
339
+ const progress = useMemo<GestureLayerProgress>(
340
+ () => ({
341
+ hovered: gesture.hovered,
342
+ focused: gesture.focused,
343
+ focusVisible: gesture.focusVisible,
344
+ pressed: gesture.pressed,
345
+ disabled: disabledProgress,
346
+ }),
347
+ [
348
+ gesture.hovered,
349
+ gesture.focused,
350
+ gesture.focusVisible,
351
+ gesture.pressed,
352
+ disabledProgress,
353
+ ],
354
+ )
355
+
273
356
  return {
274
357
  style: style as AnimatedStyle<Record<string, unknown>>,
275
358
  handlers: gesture.handlers,
359
+ states: progress,
276
360
  }
277
361
  }
278
362
 
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
  }