@onlynative/inertia 0.0.1-alpha.3 → 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 (60) hide show
  1. package/README.md +19 -5
  2. package/dist/index.d.mts +259 -3
  3. package/dist/index.d.ts +259 -3
  4. package/dist/index.js +1711 -118
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1709 -122
  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 +1502 -64
  11. package/dist/motion/Image.js.map +1 -1
  12. package/dist/motion/Image.mjs +1504 -66
  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 +1502 -64
  17. package/dist/motion/Pressable.js.map +1 -1
  18. package/dist/motion/Pressable.mjs +1504 -66
  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 +1502 -64
  23. package/dist/motion/ScrollView.js.map +1 -1
  24. package/dist/motion/ScrollView.mjs +1504 -66
  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 +1502 -64
  29. package/dist/motion/Text.js.map +1 -1
  30. package/dist/motion/Text.mjs +1504 -66
  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 +1502 -64
  35. package/dist/motion/View.js.map +1 -1
  36. package/dist/motion/View.mjs +1504 -66
  37. package/dist/motion/View.mjs.map +1 -1
  38. package/dist/{types-DAhX3fC2.d.mts → types-CjztO3RW.d.mts} +49 -4
  39. package/dist/{types-DAhX3fC2.d.ts → types-CjztO3RW.d.ts} +49 -4
  40. package/llms.txt +29 -4
  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 +38 -60
  47. package/src/transitions/easing.ts +3 -1
  48. package/src/transitions/index.ts +3 -0
  49. package/src/transitions/keys.ts +32 -0
  50. package/src/transitions/resolve.ts +1 -24
  51. package/src/transitions/sig.ts +40 -0
  52. package/src/transitions/spring.ts +41 -0
  53. package/src/types.ts +52 -2
  54. package/src/values/index.ts +14 -0
  55. package/src/values/useAnimation.ts +69 -0
  56. package/src/values/useGesture.ts +144 -0
  57. package/src/values/useMotionValue.ts +33 -0
  58. package/src/values/useScroll.ts +72 -0
  59. package/src/values/useSpring.ts +93 -0
  60. package/src/values/useTransform.ts +132 -0
@@ -78,16 +78,41 @@ interface GestureLayerTransitions {
78
78
  hovered?: TransitionConfig;
79
79
  }
80
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
+ };
81
99
  /**
82
100
  * The animation state shape inferred from the underlying component's style
83
101
  * prop. We narrow to the value side of `style` so consumers see ViewStyle on
84
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.
85
109
  */
110
+ type _StyleValue<T> = Exclude<T, (...args: any[]) => any>;
86
111
  type AnimateStyle<C> = C extends {
87
- style?: StyleProp<infer S>;
88
- } ? {
112
+ style?: infer Raw;
113
+ } ? _StyleValue<Raw> extends StyleProp<infer S> ? {
89
114
  [K in keyof S]?: AnimatableValue<S[K]>;
90
- } : never;
115
+ } & AnimatableTransformExtras : never : never;
91
116
  interface AnimationCallbackInfo<S> {
92
117
  /**
93
118
  * The animatable key that just settled — typically a `keyof S` (e.g.
@@ -208,6 +233,26 @@ interface MotionProps<C> {
208
233
  * precedence over the top-level transition.
209
234
  */
210
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;
211
256
  /**
212
257
  * Fired once per logical animation completion. See `AnimationCallbackInfo`
213
258
  * for the payload shape — transform parents fire once, not per axis.
@@ -223,4 +268,4 @@ type MotionComponent<C extends ComponentType<any>> = ComponentType<Omit<React.Co
223
268
  style?: React.ComponentProps<C>['style'];
224
269
  }>;
225
270
 
226
- 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 };
@@ -78,16 +78,41 @@ interface GestureLayerTransitions {
78
78
  hovered?: TransitionConfig;
79
79
  }
80
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
+ };
81
99
  /**
82
100
  * The animation state shape inferred from the underlying component's style
83
101
  * prop. We narrow to the value side of `style` so consumers see ViewStyle on
84
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.
85
109
  */
110
+ type _StyleValue<T> = Exclude<T, (...args: any[]) => any>;
86
111
  type AnimateStyle<C> = C extends {
87
- style?: StyleProp<infer S>;
88
- } ? {
112
+ style?: infer Raw;
113
+ } ? _StyleValue<Raw> extends StyleProp<infer S> ? {
89
114
  [K in keyof S]?: AnimatableValue<S[K]>;
90
- } : never;
115
+ } & AnimatableTransformExtras : never : never;
91
116
  interface AnimationCallbackInfo<S> {
92
117
  /**
93
118
  * The animatable key that just settled — typically a `keyof S` (e.g.
@@ -208,6 +233,26 @@ interface MotionProps<C> {
208
233
  * precedence over the top-level transition.
209
234
  */
210
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;
211
256
  /**
212
257
  * Fired once per logical animation completion. See `AnimationCallbackInfo`
213
258
  * for the payload shape — transform parents fire once, not per axis.
@@ -223,4 +268,4 @@ type MotionComponent<C extends ComponentType<any>> = ComponentType<Omit<React.Co
223
268
  style?: React.ComponentProps<C>['style'];
224
269
  }>;
225
270
 
226
- 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
 
@@ -92,11 +111,17 @@ transition={{
92
111
 
93
112
  ## Animatable properties (alpha)
94
113
 
95
- 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.
96
115
 
97
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.
98
117
 
99
- 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`.
100
125
 
101
126
  ## Docs
102
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlynative/inertia",
3
- "version": "0.0.1-alpha.3",
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
+ }
@@ -15,8 +15,14 @@ import Animated, {
15
15
  } from 'react-native-reanimated'
16
16
  import { useShouldReduceMotion } from '../config'
17
17
  import { isFocusVisible } from '../gestures'
18
+ import { resolveLayoutTransition, type LayoutProp } from '../layout'
18
19
  import { usePresence } from '../presence'
19
- import { resolveAnimatableValue, resolveTransition } from '../transitions'
20
+ import {
21
+ isTopLevelTransition,
22
+ resolveAnimatableValue,
23
+ resolveTransition,
24
+ stableSig,
25
+ } from '../transitions'
20
26
  import { ensureReanimatedInstalled } from './installCheck'
21
27
  import {
22
28
  type AnimatableValue,
@@ -45,8 +51,17 @@ const TRANSFORM_KEYS = [
45
51
  'scaleX',
46
52
  'scaleY',
47
53
  'rotate',
54
+ 'rotateX',
55
+ 'rotateY',
48
56
  ] as const
49
57
 
58
+ // Rotation keys land in the transform array as `{ rotate: '45deg' }` / `{
59
+ // rotateX: '45deg' }` etc. — Reanimated needs the unit-suffixed string form.
60
+ // We hold the underlying shared value as a plain number (degrees) and wrap
61
+ // in the worklet so the resolver pipeline stays uniform with the other
62
+ // numeric transform keys.
63
+ const ROTATION_KEYS = new Set<string>(['rotate', 'rotateX', 'rotateY'])
64
+
50
65
  const NUMERIC_TOP_LEVEL_KEYS = [
51
66
  'opacity',
52
67
  'width',
@@ -110,6 +125,8 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
110
125
  scaleX: 1,
111
126
  scaleY: 1,
112
127
  rotate: 0,
128
+ rotateX: 0,
129
+ rotateY: 0,
113
130
  opacity: 1,
114
131
  width: 0,
115
132
  height: 0,
@@ -124,29 +141,6 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
124
141
  tintColor: 'transparent',
125
142
  }
126
143
 
127
- const TRANSITION_KEYS = new Set([
128
- 'type',
129
- 'tension',
130
- 'friction',
131
- 'mass',
132
- 'velocity',
133
- 'restSpeedThreshold',
134
- 'restDisplacementThreshold',
135
- 'duration',
136
- 'easing',
137
- 'delay',
138
- 'repeat',
139
- 'deceleration',
140
- 'clamp',
141
- ])
142
-
143
- function isTopLevelTransition(t: unknown): t is TransitionConfig {
144
- if (t === null || typeof t !== 'object') return false
145
- const keys = Object.keys(t as object)
146
- if (keys.length === 0) return false
147
- return keys.every((k) => TRANSITION_KEYS.has(k))
148
- }
149
-
150
144
  function transitionFor<S>(
151
145
  prop: keyof S,
152
146
  transition: Transition<S> | undefined,
@@ -207,10 +201,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
207
201
  variants,
208
202
  controller,
209
203
  gesture,
204
+ layout,
210
205
  onAnimationEnd,
211
206
  style,
212
207
  ...rest
213
- } = props as Props & { style?: unknown }
208
+ } = props as Props & { style?: unknown; layout?: LayoutProp }
214
209
 
215
210
  // <Presence> contract: when an ancestor flips `isPresent` to false the
216
211
  // child stays rendered until `safeToRemove` is called, giving the exit
@@ -553,7 +548,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
553
548
 
554
549
  if (TRANSFORM_KEY_SET.has(key)) {
555
550
  transform.push(
556
- key === 'rotate' ? { rotate: `${v}deg` } : { [key]: v },
551
+ ROTATION_KEYS.has(key) ? { [key]: `${v}deg` } : { [key]: v },
557
552
  )
558
553
  } else {
559
554
  out[key] = v
@@ -585,11 +580,24 @@ export function createMotionComponent<C extends ComponentType<any>>(
585
580
  setHovered,
586
581
  )
587
582
 
583
+ // Resolve the `layout` prop into a Reanimated `LinearTransition` builder.
584
+ // Memoized on the value's stable signature so a fresh `layout={true}` or
585
+ // `layout={{ ... }}` literal each render doesn't rebuild the builder. When
586
+ // reduced motion is active we pass `undefined` — see `resolveLayout` for
587
+ // why we don't pass a duration-0 builder instead.
588
+ const layoutSig = stableSig(layout)
589
+ const layoutTransition = useMemo(
590
+ () => (shouldReduceMotion ? undefined : resolveLayoutTransition(layout)),
591
+ // eslint-disable-next-line react-hooks/exhaustive-deps
592
+ [layoutSig, shouldReduceMotion],
593
+ )
594
+
588
595
  return (
589
596
  <AnimatedComponent
590
597
  ref={ref as never}
591
598
  {...(rest as object)}
592
599
  {...gestureHandlers}
600
+ layout={layoutTransition}
593
601
  style={mergedStyle}
594
602
  />
595
603
  )
@@ -629,6 +637,8 @@ function useAnimatableSharedValues(
629
637
  const scaleX = useSharedValue<number | string>(init('scaleX'))
630
638
  const scaleY = useSharedValue<number | string>(init('scaleY'))
631
639
  const rotate = useSharedValue<number | string>(init('rotate'))
640
+ const rotateX = useSharedValue<number | string>(init('rotateX'))
641
+ const rotateY = useSharedValue<number | string>(init('rotateY'))
632
642
  const opacity = useSharedValue<number | string>(init('opacity'))
633
643
  const width = useSharedValue<number | string>(init('width'))
634
644
  const height = useSharedValue<number | string>(init('height'))
@@ -649,6 +659,8 @@ function useAnimatableSharedValues(
649
659
  scaleX,
650
660
  scaleY,
651
661
  rotate,
662
+ rotateX,
663
+ rotateY,
652
664
  opacity,
653
665
  width,
654
666
  height,
@@ -895,40 +907,6 @@ function restValue(
895
907
  return undefined
896
908
  }
897
909
 
898
- function stableSig(value: unknown): string {
899
- if (value === undefined) return ''
900
- try {
901
- return stableStringify(value)
902
- } catch {
903
- return String(value)
904
- }
905
- }
906
-
907
- /**
908
- * JSON.stringify with keys sorted at every level — gives a stable signature
909
- * regardless of property declaration order. Functions serialize as `null` so a
910
- * change in easing-fn reference is invisible here; that's fine for v0.1
911
- * (easing swaps are rare and the worklet wrapper handles correctness).
912
- */
913
- function stableStringify(v: unknown): string {
914
- if (v === null || typeof v !== 'object') {
915
- if (typeof v === 'function' || v === undefined) return 'null'
916
- return JSON.stringify(v)
917
- }
918
- if (Array.isArray(v)) {
919
- return '[' + v.map(stableStringify).join(',') + ']'
920
- }
921
- const obj = v as Record<string, unknown>
922
- const keys = Object.keys(obj).sort()
923
- return (
924
- '{' +
925
- keys
926
- .map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]))
927
- .join(',') +
928
- '}'
929
- )
930
- }
931
-
932
910
  /**
933
911
  * Per-layer resolved targets: each declared gesture sub-state collapses to a
934
912
  * map of primitive endpoints (numbers or color strings), already passed
@@ -1,4 +1,6 @@
1
- import { isWorkletFunction } from 'react-native-reanimated'
1
+ // `isWorkletFunction` lives in `react-native-worklets` (the Reanimated 4 peer
2
+ // dep); Reanimated's own re-export is deprecated.
3
+ import { isWorkletFunction } from 'react-native-worklets'
2
4
 
3
5
  /**
4
6
  * Reanimated 3.9+ validates that easing functions used in nested-transition
@@ -1,2 +1,5 @@
1
1
  export { resolveTransition, resolveAnimatableValue } from './resolve'
2
2
  export { ensureWorkletEasing } from './easing'
3
+ export { isTopLevelTransition, TRANSITION_CONFIG_KEYS } from './keys'
4
+ export { stableSig } from './sig'
5
+ export { DEFAULT_SPRING, springToReanimated } from './spring'
@@ -0,0 +1,32 @@
1
+ import { type TransitionConfig } from '../types'
2
+
3
+ /**
4
+ * Field names that may appear on a `TransitionConfig` (spring / timing /
5
+ * decay / no-animation). Used as a structural discriminator: if every key on
6
+ * an object is in this set, the object is treated as a top-level transition;
7
+ * otherwise it's a per-property / per-layer transition map.
8
+ *
9
+ * Adding a new field to `TransitionConfig` requires adding the name here.
10
+ */
11
+ export const TRANSITION_CONFIG_KEYS = new Set([
12
+ 'type',
13
+ 'tension',
14
+ 'friction',
15
+ 'mass',
16
+ 'velocity',
17
+ 'restSpeedThreshold',
18
+ 'restDisplacementThreshold',
19
+ 'duration',
20
+ 'easing',
21
+ 'delay',
22
+ 'repeat',
23
+ 'deceleration',
24
+ 'clamp',
25
+ ])
26
+
27
+ export function isTopLevelTransition(t: unknown): t is TransitionConfig {
28
+ if (t === null || typeof t !== 'object') return false
29
+ const keys = Object.keys(t as object)
30
+ if (keys.length === 0) return false
31
+ return keys.every((k) => TRANSITION_CONFIG_KEYS.has(k))
32
+ }
@@ -8,6 +8,7 @@ import {
8
8
  withTiming,
9
9
  } from 'react-native-reanimated'
10
10
  import { ensureWorkletEasing } from './easing'
11
+ import { springToReanimated } from './spring'
11
12
  import {
12
13
  type AnimatableValue,
13
14
  type DecayTransition,
@@ -39,32 +40,8 @@ export type CallbackFactory = (
39
40
  step: number | undefined,
40
41
  ) => AnimationCallback | undefined
41
42
 
42
- /**
43
- * Default spring physics, expressed in react-spring vocabulary. Conversion
44
- * to Reanimated's raw `stiffness` / `damping` lives below; raw config never
45
- * leaks past this module.
46
- */
47
- const DEFAULT_SPRING: Required<
48
- Pick<SpringTransition, 'tension' | 'friction' | 'mass'>
49
- > = {
50
- tension: 170,
51
- friction: 26,
52
- mass: 1,
53
- }
54
-
55
43
  const DEFAULT_TIMING_DURATION = 250
56
44
 
57
- function springToReanimated(t: SpringTransition) {
58
- return {
59
- stiffness: t.tension ?? DEFAULT_SPRING.tension,
60
- damping: t.friction ?? DEFAULT_SPRING.friction,
61
- mass: t.mass ?? DEFAULT_SPRING.mass,
62
- velocity: t.velocity,
63
- restSpeedThreshold: t.restSpeedThreshold,
64
- restDisplacementThreshold: t.restDisplacementThreshold,
65
- }
66
- }
67
-
68
45
  function buildSpring(
69
46
  cfg: SpringTransition,
70
47
  toValue: number | string,