@onlynative/inertia 0.0.1-alpha.2 → 0.0.1-alpha.4

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 (61) hide show
  1. package/README.md +44 -3
  2. package/dist/index.d.mts +259 -3
  3. package/dist/index.d.ts +259 -3
  4. package/dist/index.js +1866 -161
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1864 -165
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/motion/Image.d.mts +1 -1
  9. package/dist/motion/Image.d.ts +1 -1
  10. package/dist/motion/Image.js +1696 -146
  11. package/dist/motion/Image.js.map +1 -1
  12. package/dist/motion/Image.mjs +1698 -148
  13. package/dist/motion/Image.mjs.map +1 -1
  14. package/dist/motion/Pressable.d.mts +1 -1
  15. package/dist/motion/Pressable.d.ts +1 -1
  16. package/dist/motion/Pressable.js +1696 -146
  17. package/dist/motion/Pressable.js.map +1 -1
  18. package/dist/motion/Pressable.mjs +1698 -148
  19. package/dist/motion/Pressable.mjs.map +1 -1
  20. package/dist/motion/ScrollView.d.mts +1 -1
  21. package/dist/motion/ScrollView.d.ts +1 -1
  22. package/dist/motion/ScrollView.js +1696 -146
  23. package/dist/motion/ScrollView.js.map +1 -1
  24. package/dist/motion/ScrollView.mjs +1698 -148
  25. package/dist/motion/ScrollView.mjs.map +1 -1
  26. package/dist/motion/Text.d.mts +1 -1
  27. package/dist/motion/Text.d.ts +1 -1
  28. package/dist/motion/Text.js +1696 -146
  29. package/dist/motion/Text.js.map +1 -1
  30. package/dist/motion/Text.mjs +1698 -148
  31. package/dist/motion/Text.mjs.map +1 -1
  32. package/dist/motion/View.d.mts +1 -1
  33. package/dist/motion/View.d.ts +1 -1
  34. package/dist/motion/View.js +1696 -146
  35. package/dist/motion/View.js.map +1 -1
  36. package/dist/motion/View.mjs +1698 -148
  37. package/dist/motion/View.mjs.map +1 -1
  38. package/dist/{types-DeZZzE_e.d.mts → types-CjztO3RW.d.mts} +89 -20
  39. package/dist/{types-DeZZzE_e.d.ts → types-CjztO3RW.d.ts} +89 -20
  40. package/llms.txt +54 -6
  41. package/package.json +1 -1
  42. package/src/__type-tests__/animate.test-d.tsx +88 -0
  43. package/src/index.ts +16 -1
  44. package/src/layout/index.ts +1 -0
  45. package/src/layout/resolveLayout.ts +54 -0
  46. package/src/motion/createMotionComponent.tsx +292 -153
  47. package/src/motion/installCheck.ts +69 -0
  48. package/src/transitions/easing.ts +3 -1
  49. package/src/transitions/index.ts +3 -0
  50. package/src/transitions/keys.ts +32 -0
  51. package/src/transitions/resolve.ts +1 -24
  52. package/src/transitions/sig.ts +40 -0
  53. package/src/transitions/spring.ts +41 -0
  54. package/src/types.ts +96 -18
  55. package/src/values/index.ts +14 -0
  56. package/src/values/useAnimation.ts +69 -0
  57. package/src/values/useGesture.ts +144 -0
  58. package/src/values/useMotionValue.ts +33 -0
  59. package/src/values/useScroll.ts +72 -0
  60. package/src/values/useSpring.ts +93 -0
  61. package/src/values/useTransform.ts +132 -0
@@ -62,17 +62,57 @@ type RepeatConfig = number | 'infinite' | {
62
62
  type PerPropertyTransition<S> = {
63
63
  [K in keyof S]?: TransitionConfig;
64
64
  };
65
- type Transition<S> = TransitionConfig | PerPropertyTransition<S>;
65
+ /**
66
+ * Per-gesture-layer transition map. Each `gesture` sub-state animates a
67
+ * progress value 0↔1 with its own transition; the worklet composites the
68
+ * layers in priority order (`hovered → focused → focusVisible → pressed`).
69
+ *
70
+ * Keys live on the same `transition` object as `PerPropertyTransition` because
71
+ * the only other place they could go (nested inside `gesture` itself) would
72
+ * collide with the primitive's inferred style keys.
73
+ */
74
+ interface GestureLayerTransitions {
75
+ pressed?: TransitionConfig;
76
+ focused?: TransitionConfig;
77
+ focusVisible?: TransitionConfig;
78
+ hovered?: TransitionConfig;
79
+ }
80
+ type Transition<S> = TransitionConfig | (PerPropertyTransition<S> & GestureLayerTransitions);
81
+ /**
82
+ * Transform shorthands that Inertia exposes on `animate` but that don't
83
+ * appear on RN's typed ViewStyle as top-level keys. RN keeps `scale`,
84
+ * `rotate`, `rotateX`, and `rotateY` inside the `transform` array; only
85
+ * `scaleX`/`scaleY` and `translateX`/`translateY` are surfaced as
86
+ * (deprecated) top-level shortcuts. Inertia's runtime treats these as
87
+ * transform-group keys (see `TRANSFORM_KEYS` in `createMotionComponent`),
88
+ * so they're documented as first-class animatables in `CLAUDE.md` and must
89
+ * be reachable from `animate` without dropping into the `transform: [...]`
90
+ * array form. Rotation values are degrees as numbers — the runtime appends
91
+ * `'deg'` before handing the transform to Reanimated.
92
+ */
93
+ type AnimatableTransformExtras = {
94
+ scale?: AnimatableValue<number>;
95
+ rotate?: AnimatableValue<number>;
96
+ rotateX?: AnimatableValue<number>;
97
+ rotateY?: AnimatableValue<number>;
98
+ };
66
99
  /**
67
100
  * The animation state shape inferred from the underlying component's style
68
101
  * prop. We narrow to the value side of `style` so consumers see ViewStyle on
69
102
  * `Motion.View`, TextStyle on `Motion.Text`, etc. — no shared union.
103
+ *
104
+ * Some components (notably `Pressable`) type `style` as a union of
105
+ * `StyleProp<T>` and a callback `(state) => StyleProp<T>`. If we infer `S`
106
+ * directly from `StyleProp<infer S>`, the callback branch widens `S` to
107
+ * `unknown`, which collapses the animate map to `| {}` and silently
108
+ * accepts any key. Excluding functions first keeps inference tight.
70
109
  */
110
+ type _StyleValue<T> = Exclude<T, (...args: any[]) => any>;
71
111
  type AnimateStyle<C> = C extends {
72
- style?: StyleProp<infer S>;
73
- } ? {
112
+ style?: infer Raw;
113
+ } ? _StyleValue<Raw> extends StyleProp<infer S> ? {
74
114
  [K in keyof S]?: AnimatableValue<S[K]>;
75
- } : never;
115
+ } & AnimatableTransformExtras : never : never;
76
116
  interface AnimationCallbackInfo<S> {
77
117
  /**
78
118
  * The animatable key that just settled — typically a `keyof S` (e.g.
@@ -109,18 +149,26 @@ type VariantsMap<C> = Record<string, AnimateStyle<C>>;
109
149
  * - `hovered` — web-only. Typed for cross-platform call sites; the runtime is
110
150
  * a no-op on native.
111
151
  *
112
- * When a sub-state is active, its values override the base `animate` target
113
- * per-property. Priority on overlap (highest first):
114
- * `pressed` > `focusVisible` > `focused` > `hovered`. `focusVisible` layers
115
- * above `focused` so declaring both yields a state-layer on any focus and a
116
- * ring on keyboard focus only.
152
+ * Sub-states layer additively. Each declared sub-state owns an independent
153
+ * progress value (0↔1) that animates in/out with its own transition; the
154
+ * worklet composites layers in priority order (lowest-to-highest):
155
+ * `hovered focused focusVisible pressed`. Per-property the chain is
117
156
  *
118
- * Sub-states stack as **single-state selection**, not blended interpolation:
119
- * the highest-priority active key's value wins per-property, with one
120
- * transition between target values. Mid-transition cross-fades between
121
- * sub-states (e.g. release-while-still-hovered) follow the standard `transition`
122
- * for that property the resolver does not run multiple parallel
123
- * interpolations the way a hand-rolled chained-`interpolateColor` would.
157
+ * v = base
158
+ * v = lerp(v, hovered.value, progressHovered) // if declared
159
+ * v = lerp(v, focused.value, progressFocused) // if declared
160
+ * v = lerp(v, focusVisible.value, progressFocusVisible) // if declared
161
+ * v = lerp(v, pressed.value, progressPressed) // if declared
162
+ *
163
+ * (Color-valued keys use `interpolateColor` instead of `lerp`.) When a single
164
+ * sub-state is active, this collapses to "the highest-priority declared layer
165
+ * wins". When multiple are mid-transition (e.g. release-while-still-hovered)
166
+ * each layer fades independently — a press layer fading out at 50ms while a
167
+ * hover layer holds at full opacity matches MD3 state-layer semantics.
168
+ *
169
+ * Configure per-layer fade timing via `transition.<stateName>` on the parent
170
+ * primitive (see `GestureLayerTransitions`); without it, layers default to
171
+ * the parent transition or the library default spring.
124
172
  */
125
173
  interface GestureSubStates<C> {
126
174
  pressed?: AnimateStyle<C>;
@@ -173,10 +221,11 @@ interface MotionProps<C> {
173
221
  */
174
222
  controller?: VariantController;
175
223
  /**
176
- * Gesture-driven sub-states (`pressed`, `focused`, `hovered`). When omitted,
177
- * no handlers are mounted on the underlying component. Sub-state values
178
- * merge over `animate` per-property while the corresponding gesture is
179
- * active.
224
+ * Gesture-driven sub-states (`pressed`, `focused`, `focusVisible`,
225
+ * `hovered`). When omitted, no handlers are mounted on the underlying
226
+ * component. Each declared sub-state animates as an independent layer
227
+ * fading in/out over the base `animate` target — see `GestureSubStates`
228
+ * for the composition model and per-layer transition wiring.
180
229
  */
181
230
  gesture?: GestureSubStates<C>;
182
231
  /**
@@ -184,6 +233,26 @@ interface MotionProps<C> {
184
233
  * precedence over the top-level transition.
185
234
  */
186
235
  transition?: Transition<AnimateStyle<C>>;
236
+ /**
237
+ * Auto-layout animation. When the component's position or size changes
238
+ * because of a parent layout change (a flex sibling growing, a list
239
+ * reordering, a column toggling its width), interpolate between the old
240
+ * and new layout instead of snapping.
241
+ *
242
+ * - `true` — animate with the library's default spring.
243
+ * - `TransitionConfig` — spring (react-spring vocab) or timing config; the
244
+ * resolver bridges to Reanimated's `LinearTransition` builder.
245
+ * - omitted / `false` — no layout animation (default).
246
+ *
247
+ * Only `'spring'` / `'timing'` / `'no-animation'` map to layout transitions
248
+ * — decay is downgraded to spring (no clear target). Reduced motion gates
249
+ * the prop the same way it gates `animate`.
250
+ *
251
+ * `layoutId` for shared element transitions across screens is deferred:
252
+ * Reanimated 4 dropped the underlying `sharedTransitionTag` API and a
253
+ * Inertia-side measure-based registry is the in-flight design.
254
+ */
255
+ layout?: boolean | TransitionConfig;
187
256
  /**
188
257
  * Fired once per logical animation completion. See `AnimationCallbackInfo`
189
258
  * for the payload shape — transform parents fire once, not per axis.
@@ -199,4 +268,4 @@ type MotionComponent<C extends ComponentType<any>> = ComponentType<Omit<React.Co
199
268
  style?: React.ComponentProps<C>['style'];
200
269
  }>;
201
270
 
202
- export type { AnimatableValue as A, DecayTransition as D, GestureSubStates as G, MotionComponent as M, NoAnimationTransition as N, PerPropertyTransition as P, RepeatConfig as R, SequenceStep as S, TransitionConfig as T, VariantController as V, AnimateStyle as a, AnimationCallbackInfo as b, MotionProps as c, SpringTransition as d, TimingTransition as e, Transition as f, VariantsMap as g };
271
+ export type { AnimatableValue as A, DecayTransition as D, GestureLayerTransitions as G, MotionComponent as M, NoAnimationTransition as N, PerPropertyTransition as P, RepeatConfig as R, SpringTransition as S, TransitionConfig as T, VariantController as V, AnimateStyle as a, AnimationCallbackInfo as b, GestureSubStates as c, MotionProps as d, SequenceStep as e, TimingTransition as f, Transition as g, VariantsMap as h };
@@ -62,17 +62,57 @@ type RepeatConfig = number | 'infinite' | {
62
62
  type PerPropertyTransition<S> = {
63
63
  [K in keyof S]?: TransitionConfig;
64
64
  };
65
- type Transition<S> = TransitionConfig | PerPropertyTransition<S>;
65
+ /**
66
+ * Per-gesture-layer transition map. Each `gesture` sub-state animates a
67
+ * progress value 0↔1 with its own transition; the worklet composites the
68
+ * layers in priority order (`hovered → focused → focusVisible → pressed`).
69
+ *
70
+ * Keys live on the same `transition` object as `PerPropertyTransition` because
71
+ * the only other place they could go (nested inside `gesture` itself) would
72
+ * collide with the primitive's inferred style keys.
73
+ */
74
+ interface GestureLayerTransitions {
75
+ pressed?: TransitionConfig;
76
+ focused?: TransitionConfig;
77
+ focusVisible?: TransitionConfig;
78
+ hovered?: TransitionConfig;
79
+ }
80
+ type Transition<S> = TransitionConfig | (PerPropertyTransition<S> & GestureLayerTransitions);
81
+ /**
82
+ * Transform shorthands that Inertia exposes on `animate` but that don't
83
+ * appear on RN's typed ViewStyle as top-level keys. RN keeps `scale`,
84
+ * `rotate`, `rotateX`, and `rotateY` inside the `transform` array; only
85
+ * `scaleX`/`scaleY` and `translateX`/`translateY` are surfaced as
86
+ * (deprecated) top-level shortcuts. Inertia's runtime treats these as
87
+ * transform-group keys (see `TRANSFORM_KEYS` in `createMotionComponent`),
88
+ * so they're documented as first-class animatables in `CLAUDE.md` and must
89
+ * be reachable from `animate` without dropping into the `transform: [...]`
90
+ * array form. Rotation values are degrees as numbers — the runtime appends
91
+ * `'deg'` before handing the transform to Reanimated.
92
+ */
93
+ type AnimatableTransformExtras = {
94
+ scale?: AnimatableValue<number>;
95
+ rotate?: AnimatableValue<number>;
96
+ rotateX?: AnimatableValue<number>;
97
+ rotateY?: AnimatableValue<number>;
98
+ };
66
99
  /**
67
100
  * The animation state shape inferred from the underlying component's style
68
101
  * prop. We narrow to the value side of `style` so consumers see ViewStyle on
69
102
  * `Motion.View`, TextStyle on `Motion.Text`, etc. — no shared union.
103
+ *
104
+ * Some components (notably `Pressable`) type `style` as a union of
105
+ * `StyleProp<T>` and a callback `(state) => StyleProp<T>`. If we infer `S`
106
+ * directly from `StyleProp<infer S>`, the callback branch widens `S` to
107
+ * `unknown`, which collapses the animate map to `| {}` and silently
108
+ * accepts any key. Excluding functions first keeps inference tight.
70
109
  */
110
+ type _StyleValue<T> = Exclude<T, (...args: any[]) => any>;
71
111
  type AnimateStyle<C> = C extends {
72
- style?: StyleProp<infer S>;
73
- } ? {
112
+ style?: infer Raw;
113
+ } ? _StyleValue<Raw> extends StyleProp<infer S> ? {
74
114
  [K in keyof S]?: AnimatableValue<S[K]>;
75
- } : never;
115
+ } & AnimatableTransformExtras : never : never;
76
116
  interface AnimationCallbackInfo<S> {
77
117
  /**
78
118
  * The animatable key that just settled — typically a `keyof S` (e.g.
@@ -109,18 +149,26 @@ type VariantsMap<C> = Record<string, AnimateStyle<C>>;
109
149
  * - `hovered` — web-only. Typed for cross-platform call sites; the runtime is
110
150
  * a no-op on native.
111
151
  *
112
- * When a sub-state is active, its values override the base `animate` target
113
- * per-property. Priority on overlap (highest first):
114
- * `pressed` > `focusVisible` > `focused` > `hovered`. `focusVisible` layers
115
- * above `focused` so declaring both yields a state-layer on any focus and a
116
- * ring on keyboard focus only.
152
+ * Sub-states layer additively. Each declared sub-state owns an independent
153
+ * progress value (0↔1) that animates in/out with its own transition; the
154
+ * worklet composites layers in priority order (lowest-to-highest):
155
+ * `hovered focused focusVisible pressed`. Per-property the chain is
117
156
  *
118
- * Sub-states stack as **single-state selection**, not blended interpolation:
119
- * the highest-priority active key's value wins per-property, with one
120
- * transition between target values. Mid-transition cross-fades between
121
- * sub-states (e.g. release-while-still-hovered) follow the standard `transition`
122
- * for that property the resolver does not run multiple parallel
123
- * interpolations the way a hand-rolled chained-`interpolateColor` would.
157
+ * v = base
158
+ * v = lerp(v, hovered.value, progressHovered) // if declared
159
+ * v = lerp(v, focused.value, progressFocused) // if declared
160
+ * v = lerp(v, focusVisible.value, progressFocusVisible) // if declared
161
+ * v = lerp(v, pressed.value, progressPressed) // if declared
162
+ *
163
+ * (Color-valued keys use `interpolateColor` instead of `lerp`.) When a single
164
+ * sub-state is active, this collapses to "the highest-priority declared layer
165
+ * wins". When multiple are mid-transition (e.g. release-while-still-hovered)
166
+ * each layer fades independently — a press layer fading out at 50ms while a
167
+ * hover layer holds at full opacity matches MD3 state-layer semantics.
168
+ *
169
+ * Configure per-layer fade timing via `transition.<stateName>` on the parent
170
+ * primitive (see `GestureLayerTransitions`); without it, layers default to
171
+ * the parent transition or the library default spring.
124
172
  */
125
173
  interface GestureSubStates<C> {
126
174
  pressed?: AnimateStyle<C>;
@@ -173,10 +221,11 @@ interface MotionProps<C> {
173
221
  */
174
222
  controller?: VariantController;
175
223
  /**
176
- * Gesture-driven sub-states (`pressed`, `focused`, `hovered`). When omitted,
177
- * no handlers are mounted on the underlying component. Sub-state values
178
- * merge over `animate` per-property while the corresponding gesture is
179
- * active.
224
+ * Gesture-driven sub-states (`pressed`, `focused`, `focusVisible`,
225
+ * `hovered`). When omitted, no handlers are mounted on the underlying
226
+ * component. Each declared sub-state animates as an independent layer
227
+ * fading in/out over the base `animate` target — see `GestureSubStates`
228
+ * for the composition model and per-layer transition wiring.
180
229
  */
181
230
  gesture?: GestureSubStates<C>;
182
231
  /**
@@ -184,6 +233,26 @@ interface MotionProps<C> {
184
233
  * precedence over the top-level transition.
185
234
  */
186
235
  transition?: Transition<AnimateStyle<C>>;
236
+ /**
237
+ * Auto-layout animation. When the component's position or size changes
238
+ * because of a parent layout change (a flex sibling growing, a list
239
+ * reordering, a column toggling its width), interpolate between the old
240
+ * and new layout instead of snapping.
241
+ *
242
+ * - `true` — animate with the library's default spring.
243
+ * - `TransitionConfig` — spring (react-spring vocab) or timing config; the
244
+ * resolver bridges to Reanimated's `LinearTransition` builder.
245
+ * - omitted / `false` — no layout animation (default).
246
+ *
247
+ * Only `'spring'` / `'timing'` / `'no-animation'` map to layout transitions
248
+ * — decay is downgraded to spring (no clear target). Reduced motion gates
249
+ * the prop the same way it gates `animate`.
250
+ *
251
+ * `layoutId` for shared element transitions across screens is deferred:
252
+ * Reanimated 4 dropped the underlying `sharedTransitionTag` API and a
253
+ * Inertia-side measure-based registry is the in-flight design.
254
+ */
255
+ layout?: boolean | TransitionConfig;
187
256
  /**
188
257
  * Fired once per logical animation completion. See `AnimationCallbackInfo`
189
258
  * for the payload shape — transform parents fire once, not per axis.
@@ -199,4 +268,4 @@ type MotionComponent<C extends ComponentType<any>> = ComponentType<Omit<React.Co
199
268
  style?: React.ComponentProps<C>['style'];
200
269
  }>;
201
270
 
202
- export type { AnimatableValue as A, DecayTransition as D, GestureSubStates as G, MotionComponent as M, NoAnimationTransition as N, PerPropertyTransition as P, RepeatConfig as R, SequenceStep as S, TransitionConfig as T, VariantController as V, AnimateStyle as a, AnimationCallbackInfo as b, MotionProps as c, SpringTransition as d, TimingTransition as e, Transition as f, VariantsMap as g };
271
+ export type { AnimatableValue as A, DecayTransition as D, GestureLayerTransitions as G, MotionComponent as M, NoAnimationTransition as N, PerPropertyTransition as P, RepeatConfig as R, SpringTransition as S, TransitionConfig as T, VariantController as V, AnimateStyle as a, AnimationCallbackInfo as b, GestureSubStates as c, MotionProps as d, SequenceStep as e, TimingTransition as f, Transition as g, VariantsMap as h };
package/llms.txt CHANGED
@@ -13,7 +13,18 @@ Enable the Reanimated Babel plugin per its install guide.
13
13
  ## Imports
14
14
 
15
15
  ```ts
16
- import { Motion, Presence, MotionConfig, useVariants } from '@onlynative/inertia'
16
+ import {
17
+ Motion,
18
+ Presence,
19
+ MotionConfig,
20
+ useGesture,
21
+ useVariants,
22
+ useMotionValue,
23
+ useAnimation,
24
+ useSpring,
25
+ useTransform,
26
+ useScroll,
27
+ } from '@onlynative/inertia'
17
28
  // or for tree-shaking:
18
29
  import { MotionView } from '@onlynative/inertia/view'
19
30
  import { MotionText } from '@onlynative/inertia/text'
@@ -27,12 +38,20 @@ import { MotionScrollView } from '@onlynative/inertia/scroll-view'
27
38
  - `Motion.View` / `Motion.Text` / `Motion.Image` / `Motion.Pressable` / `Motion.ScrollView` — animatable primitives. Per-primitive style inference (no shared `ViewStyle & TextStyle & ImageStyle` fallback).
28
39
  - `<Presence>` — mount / unmount transitions; children need explicit `key`s. Exiting children get `pointerEvents: 'none'` automatically.
29
40
  - `<MotionConfig reducedMotion="user" | "never" | "always">` — gates motion against the OS reduce-motion setting (default `"user"`).
41
+ - `useGesture(transition?)` — hook-form of the `gesture` prop. Returns 0↔1 progress shared values (`pressed`, `focused`, `focusVisible`, `hovered`) plus a `handlers` bag to spread on a `Pressable`. Use when one gesture needs to drive multiple animated siblings (focus rings, MD3 state-layer halos, multi-element compositions).
30
42
  - `useVariants(variants, initial?)` — returns `{ current, transitionTo }` controller for the `controller` prop.
43
+ - `useMotionValue(initial)` — thin pass-through over `useSharedValue<T>`. Returns a `SharedValue<T>` directly so it interops with every Reanimated API without unwrapping.
44
+ - `useAnimation(target, transition?)` — drive a `SharedValue<number>` toward `target` with any `TransitionConfig` (spring / timing / decay / no-animation, plus `repeat`). The general-purpose value-layer hook for boolean-state progress, indeterminate loops, and anywhere the same value needs to feed multiple `useAnimatedStyle` blocks.
45
+ - `useSpring(target, config?)` — spring-only shorthand. Animates a `SharedValue<number>` toward `target` with react-spring vocab. `target` may be a plain number (effect-driven) or a `SharedValue<number>` (UI-thread reaction); the latter is the gesture-smoothing path.
46
+ - `useTransform(value, inputRange, outputRange, options?)` / `useTransform(transformer)` — interpolate a numeric shared value onto a number or color range, or derive any value from any number of shared values via a worklet. Non-worklet transformers are auto-wrapped.
47
+ - `useScroll()` — returns `{ scrollX, scrollY, onScroll }` for use with `Motion.ScrollView`. Scroll events fire on the UI thread.
31
48
  - `createMotionComponent<C>(C)` — wrap any component with the same Motion prop surface, inferring style from `C`.
32
49
 
33
50
  ## Motion props
34
51
 
35
- `initial` (mount-only, non-reactive after first render — pass `false` to skip), `animate`, `exit`, `variants`, `controller`, `gesture`, `transition`, `onAnimationEnd`.
52
+ `initial` (mount-only, non-reactive after first render — pass `false` to skip), `animate`, `exit`, `variants`, `controller`, `gesture`, `transition`, `layout`, `onAnimationEnd`.
53
+
54
+ `layout` accepts `true` (default spring) or a `TransitionConfig` (spring / timing) and animates position + size changes that come from outside the `animate` flow (siblings reordering, dimensions toggling). `'decay'` downgrades to spring; `'no-animation'` and reduced motion both skip the animation.
36
55
 
37
56
  ## Transitions
38
57
 
@@ -62,18 +81,47 @@ Spring uses react-spring vocabulary (`tension`, `friction`, `mass`). Reanimated'
62
81
  ## Gestures
63
82
 
64
83
  ```tsx
65
- <Motion.View gesture={{ pressed: { scale: 0.96 }, focused: {...}, hovered: {...} }} />
84
+ <Motion.View
85
+ gesture={{
86
+ hovered: { backgroundColor: '#0001' },
87
+ focused: { backgroundColor: '#0002' },
88
+ pressed: { backgroundColor: '#0003' },
89
+ }}
90
+ transition={{ type: 'timing', duration: 120 }}
91
+ />
66
92
  ```
67
93
 
68
- Sub-state priority: `hovered < focused < pressed`. `hovered` is a no-op on native.
94
+ Sub-states layer **additively** in priority order: `hovered focused focusVisible → pressed`. Each declared sub-state owns an independent progress (0↔1) that fades in/out with its own transition; the worklet composites layers via `lerp` (numerics) / `interpolateColor` (colors). MD3 release-while-hovered behaves correctly — the press layer fades out without disturbing the hover layer.
95
+
96
+ Per-layer transitions via `transition.<stateName>`:
97
+
98
+ ```tsx
99
+ transition={{
100
+ pressed: { type: 'timing', duration: 50 },
101
+ hovered: { type: 'timing', duration: 90 },
102
+ }}
103
+ ```
104
+
105
+ `focusVisible` engages only on keyboard focus (W3C `:focus-visible`) so click-focus on web doesn't flash a ring; on native it tracks `focused`. `hovered` is a no-op on native.
106
+
107
+ ## Caveats
108
+
109
+ - `Motion.Pressable` does **not** support function-form `style={({ pressed }) => ...}`. Reanimated's animated-component wrapper silently drops it. Drive press/focus/hover styling through `gesture` instead, or compute conditional styles once in render.
110
+ - `initial` is read once on mount and intentionally non-reactive. To reset after a state change, change the component `key`, remount via `<Presence>`, or drive the value through a controller. Pass `initial={false}` to skip the initial-mount animation.
69
111
 
70
112
  ## Animatable properties (alpha)
71
113
 
72
- Numeric: `opacity`, `translateX`, `translateY`, `scale`, `scaleX`, `scaleY`, `rotate`, `width`, `height`, `borderRadius`.
114
+ Numeric: `opacity`, `translateX`, `translateY`, `scale`, `scaleX`, `scaleY`, `rotate`, `rotateX`, `rotateY`, `width`, `height`, `borderRadius`. Rotation values are degrees; the factory wraps them as `'${value}deg'`. `rotateX` / `rotateY` need a sibling `perspective` style entry to render in 3D.
73
115
 
74
116
  Color: `backgroundColor`, `borderColor`, `color`, `tintColor` (Image only). Hex, `rgb()` / `rgba()`, `hsl()` / `hsla()`, and named colors all work; the target string is forwarded straight through `withSpring` / `withTiming` and Reanimated handles RGBA interpolation natively.
75
117
 
76
- Layout (`layout` / `layoutId`) and SVG path morphing are not in alpha.
118
+ Auto-layout transitions ship via the `layout` prop (`true` / `TransitionConfig`) on every `Motion.*` primitive — see Layout. Shared element transitions (`layoutId`) are deferred: Reanimated 4 dropped the `sharedTransitionTag` API the previous design relied on.
119
+
120
+ ## Optional adapter packages
121
+
122
+ - `@onlynative/inertia-gradients` — `MotionLinearGradient` over `expo-linear-gradient`. Animatable: `colors`, `start`, `end`, `locations`.
123
+ - `@onlynative/inertia-svg` — `MotionPath` over `react-native-svg`. Animatable: `d` (path morphing on structurally-compatible paths), `fill`, `stroke`, `strokeWidth`, opacities, `strokeDashoffset`. Source and target paths must share the same command sequence after implicit-repeat expansion; remount with `key` to switch shape.
124
+ - `@onlynative/inertia-gestures` — `useDrag`, `useSwipe`, `usePan` over `react-native-gesture-handler`.
77
125
 
78
126
  ## Docs
79
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlynative/inertia",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.1-alpha.4",
4
4
  "description": "Declarative animation primitives for React Native, built on react-native-reanimated.",
5
5
  "license": "MIT",
6
6
  "author": "OnlyNative",
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Phase-1 acceptance: per-primitive style inference must reject keys that
3
+ * don't exist on the underlying component's style prop, at compile time.
4
+ *
5
+ * These assertions run as part of `tsc --noEmit` (typecheck CI step). They
6
+ * are not Jest tests — Jest's Babel transform strips `@ts-expect-error`,
7
+ * so a runtime check can't enforce a compile-time gate. If any
8
+ * `@ts-expect-error` here becomes unused (i.e. the line below it stops
9
+ * being a type error), tsc fails with "Unused '@ts-expect-error'
10
+ * directive" and Phase-1 has regressed.
11
+ *
12
+ * The file is excluded from the tsup build via the explicit entry list in
13
+ * tsup.config.ts, and from Jest via the `__tests__`-only testMatch glob.
14
+ *
15
+ * We assert against `AnimateStyle<...>` directly (rather than mounting JSX)
16
+ * because Prettier reflows multi-line JSX, which would push the actual
17
+ * error onto a different line than the `@ts-expect-error` directive.
18
+ * Direct value-to-type assignment lines stay one line, regardless of
19
+ * Prettier print width.
20
+ */
21
+
22
+ import type { ComponentProps } from 'react'
23
+ import type { Image, Pressable, ScrollView, Text, View } from 'react-native'
24
+ import type { AnimateStyle } from '../types'
25
+
26
+ type ViewAnimate = AnimateStyle<ComponentProps<typeof View>>
27
+ type TextAnimate = AnimateStyle<ComponentProps<typeof Text>>
28
+ type ImageAnimate = AnimateStyle<ComponentProps<typeof Image>>
29
+ type PressableAnimate = AnimateStyle<ComponentProps<typeof Pressable>>
30
+ type ScrollViewAnimate = AnimateStyle<ComponentProps<typeof ScrollView>>
31
+
32
+ // ─── Motion.View / ViewStyle ────────────────────────────────────────────────
33
+
34
+ const _viewAccepts: ViewAnimate = { opacity: 1, translateX: 10, scale: 1.1 }
35
+ const _viewAcceptsRotate: ViewAnimate = { rotate: 45, rotateX: 30, rotateY: 60 }
36
+ // @ts-expect-error rotate is a numeric degree value; strings like '45deg' aren't accepted
37
+ const _viewRejectsRotateString: ViewAnimate = { rotate: '45deg' }
38
+ // @ts-expect-error tintColor is ImageStyle-only and must be rejected on View
39
+ const _viewRejectsTintColor: ViewAnimate = { tintColor: '#0a84ff' }
40
+ // @ts-expect-error fontSize is TextStyle-only and must be rejected on View
41
+ const _viewRejectsFontSize: ViewAnimate = { fontSize: 20 }
42
+ // @ts-expect-error completely unknown keys must be rejected
43
+ const _viewRejectsUnknown: ViewAnimate = { nonsenseKey: 1 }
44
+
45
+ // ─── Motion.Text / TextStyle ────────────────────────────────────────────────
46
+
47
+ const _textAccepts: TextAnimate = { opacity: 1, color: '#000', fontSize: 16 }
48
+ // @ts-expect-error tintColor is ImageStyle-only and must be rejected on Text
49
+ const _textRejectsTintColor: TextAnimate = { tintColor: '#0a84ff' }
50
+
51
+ // ─── Motion.Image / ImageStyle ──────────────────────────────────────────────
52
+
53
+ const _imageAccepts: ImageAnimate = { tintColor: '#0a84ff', opacity: 0.5 }
54
+ // @ts-expect-error fontSize is TextStyle-only and must be rejected on Image
55
+ const _imageRejectsFontSize: ImageAnimate = { fontSize: 20 }
56
+
57
+ // ─── Motion.Pressable / Motion.ScrollView ───────────────────────────────────
58
+ //
59
+ // Both wrap View internally; their style surface is ViewStyle. Same rule as
60
+ // Motion.View — no tintColor. Pressable's `style` is a union with a
61
+ // `(state) => StyleProp<ViewStyle>` callback; the `_StyleValue` helper in
62
+ // `types.ts` strips the function variant so inference stays tight.
63
+
64
+ const _pressableAccepts: PressableAnimate = { opacity: 1, scale: 0.96 }
65
+ // @ts-expect-error tintColor is ImageStyle-only and must be rejected on Pressable
66
+ const _pressableRejectsTintColor: PressableAnimate = { tintColor: '#0a84ff' }
67
+
68
+ const _scrollViewAccepts: ScrollViewAnimate = { opacity: 1, translateY: 10 }
69
+ // @ts-expect-error tintColor is ImageStyle-only and must be rejected on ScrollView
70
+ const _scrollViewRejectsTintColor: ScrollViewAnimate = { tintColor: '#0a84ff' }
71
+
72
+ // Silence "declared but never read" — these exist purely as type assertions.
73
+ export type _PhaseOneTypeAssertions = [
74
+ typeof _viewAccepts,
75
+ typeof _viewAcceptsRotate,
76
+ typeof _viewRejectsRotateString,
77
+ typeof _viewRejectsTintColor,
78
+ typeof _viewRejectsFontSize,
79
+ typeof _viewRejectsUnknown,
80
+ typeof _textAccepts,
81
+ typeof _textRejectsTintColor,
82
+ typeof _imageAccepts,
83
+ typeof _imageRejectsFontSize,
84
+ typeof _pressableAccepts,
85
+ typeof _pressableRejectsTintColor,
86
+ typeof _scrollViewAccepts,
87
+ typeof _scrollViewRejectsTintColor,
88
+ ]
package/src/index.ts CHANGED
@@ -27,7 +27,22 @@ export {
27
27
  resolveAnimatableValue,
28
28
  ensureWorkletEasing,
29
29
  } from './transitions'
30
- export { useVariants } from './values'
30
+ export {
31
+ useAnimation,
32
+ useGesture,
33
+ useMotionValue,
34
+ useScroll,
35
+ useSpring,
36
+ useTransform,
37
+ useVariants,
38
+ } from './values'
39
+ export type {
40
+ ExtrapolationMode,
41
+ UseGestureHandlers,
42
+ UseGestureResult,
43
+ UseScrollResult,
44
+ UseTransformOptions,
45
+ } from './values'
31
46
  export type {
32
47
  AnimatableValue,
33
48
  AnimateStyle,
@@ -0,0 +1 @@
1
+ export { resolveLayoutTransition, type LayoutProp } from './resolveLayout'
@@ -0,0 +1,54 @@
1
+ import { LinearTransition } from 'react-native-reanimated'
2
+ import { ensureWorkletEasing } from '../transitions/easing'
3
+ import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
4
+ import { type TransitionConfig } from '../types'
5
+
6
+ export type LayoutProp = boolean | TransitionConfig | undefined
7
+
8
+ /**
9
+ * Resolve the public `layout` prop into a Reanimated `LinearTransition` builder.
10
+ *
11
+ * - `undefined` / `false` / `{ type: 'no-animation' }` → `undefined` (no layout
12
+ * animation; layout changes snap).
13
+ * - `true` → default spring with the library's tuned tension / friction / mass.
14
+ * - `{ type: 'spring', ... }` → spring with react-spring vocabulary, bridged
15
+ * into `springify().damping().stiffness().mass()` via `springToReanimated`.
16
+ * - `{ type: 'timing', ... }` → `.duration().easing()`. User easing fns are
17
+ * auto-wrapped as worklets (Reanimated 3.9+ validates this).
18
+ * - `{ type: 'decay', ... }` → silently downgrades to spring; decay doesn't
19
+ * have a clear target for a layout transition.
20
+ *
21
+ * When `reducedMotion` is true the caller should pass `undefined` so the
22
+ * underlying component renders without a layout animation at all. We choose
23
+ * this over `LinearTransition.duration(0)` because Reanimated still runs the
24
+ * commit-tracking machinery in that case; the snap path is genuinely cheaper.
25
+ */
26
+ export function resolveLayoutTransition(
27
+ layout: LayoutProp,
28
+ ): LinearTransition | undefined {
29
+ if (!layout) return undefined
30
+
31
+ const cfg: TransitionConfig = layout === true ? { type: 'spring' } : layout
32
+
33
+ if (cfg.type === 'no-animation') return undefined
34
+
35
+ if (cfg.type === 'timing') {
36
+ let builder = LinearTransition.duration(cfg.duration ?? 300)
37
+ const easing = ensureWorkletEasing(cfg.easing)
38
+ if (easing) builder = builder.easing(easing)
39
+ if (cfg.delay) builder = builder.delay(cfg.delay)
40
+ return builder
41
+ }
42
+
43
+ const spring = cfg.type === 'decay' ? ({ type: 'spring' } as const) : cfg
44
+ const { stiffness, damping, mass } = springToReanimated({
45
+ ...DEFAULT_SPRING,
46
+ ...spring,
47
+ })
48
+ let builder = LinearTransition.springify()
49
+ .stiffness(stiffness)
50
+ .damping(damping)
51
+ .mass(mass)
52
+ if ('delay' in spring && spring.delay) builder = builder.delay(spring.delay)
53
+ return builder
54
+ }