@rootnative/inertia 0.0.0-alpha.0
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.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/gestureLayer/index.d.mts +119 -0
- package/dist/gestureLayer/index.d.ts +119 -0
- package/dist/gestureLayer/index.js +346 -0
- package/dist/gestureLayer/index.js.map +1 -0
- package/dist/gestureLayer/index.mjs +344 -0
- package/dist/gestureLayer/index.mjs.map +1 -0
- package/dist/index.d.mts +507 -0
- package/dist/index.d.ts +507 -0
- package/dist/index.js +1546 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1515 -0
- package/dist/index.mjs.map +1 -0
- package/dist/motion/Image.d.mts +12 -0
- package/dist/motion/Image.d.ts +12 -0
- package/dist/motion/Image.js +1138 -0
- package/dist/motion/Image.js.map +1 -0
- package/dist/motion/Image.mjs +1132 -0
- package/dist/motion/Image.mjs.map +1 -0
- package/dist/motion/Pressable.d.mts +15 -0
- package/dist/motion/Pressable.d.ts +15 -0
- package/dist/motion/Pressable.js +1138 -0
- package/dist/motion/Pressable.js.map +1 -0
- package/dist/motion/Pressable.mjs +1132 -0
- package/dist/motion/Pressable.mjs.map +1 -0
- package/dist/motion/ScrollView.d.mts +12 -0
- package/dist/motion/ScrollView.d.ts +12 -0
- package/dist/motion/ScrollView.js +1138 -0
- package/dist/motion/ScrollView.js.map +1 -0
- package/dist/motion/ScrollView.mjs +1132 -0
- package/dist/motion/ScrollView.mjs.map +1 -0
- package/dist/motion/Text.d.mts +11 -0
- package/dist/motion/Text.d.ts +11 -0
- package/dist/motion/Text.js +1138 -0
- package/dist/motion/Text.js.map +1 -0
- package/dist/motion/Text.mjs +1132 -0
- package/dist/motion/Text.mjs.map +1 -0
- package/dist/motion/View.d.mts +11 -0
- package/dist/motion/View.d.ts +11 -0
- package/dist/motion/View.js +1138 -0
- package/dist/motion/View.js.map +1 -0
- package/dist/motion/View.mjs +1132 -0
- package/dist/motion/View.mjs.map +1 -0
- package/dist/testing/index.d.mts +57 -0
- package/dist/testing/index.d.ts +57 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +16 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/touch/index.d.mts +146 -0
- package/dist/touch/index.d.ts +146 -0
- package/dist/touch/index.js +166 -0
- package/dist/touch/index.js.map +1 -0
- package/dist/touch/index.mjs +164 -0
- package/dist/touch/index.mjs.map +1 -0
- package/dist/types-cU43dEmH.d.mts +339 -0
- package/dist/types-cU43dEmH.d.ts +339 -0
- package/dist/useGesture-BnY65PlQ.d.ts +84 -0
- package/dist/useGesture-DxtXdz-K.d.mts +84 -0
- package/jest-preset.js +33 -0
- package/jest-setup.js +213 -0
- package/llms.txt +142 -0
- package/package.json +151 -0
- package/src/__type-tests__/animate.test-d.tsx +88 -0
- package/src/__type-tests__/variants.test-d.tsx +67 -0
- package/src/config/MotionConfig.tsx +30 -0
- package/src/config/MotionConfigContext.ts +53 -0
- package/src/config/index.ts +9 -0
- package/src/gestureLayer/index.ts +21 -0
- package/src/gestureLayer/useGestureLayer.ts +285 -0
- package/src/gestures/focusVisibility.ts +61 -0
- package/src/gestures/index.ts +1 -0
- package/src/index.ts +72 -0
- package/src/layout/index.ts +16 -0
- package/src/layout/resolveLayout.ts +54 -0
- package/src/layout/sharedRegistry.ts +111 -0
- package/src/layout/useSharedLayout.ts +289 -0
- package/src/motion/Image.tsx +9 -0
- package/src/motion/Pressable.tsx +12 -0
- package/src/motion/ScrollView.tsx +9 -0
- package/src/motion/Text.tsx +8 -0
- package/src/motion/View.tsx +8 -0
- package/src/motion/createMotionComponent.tsx +1306 -0
- package/src/motion/index.ts +26 -0
- package/src/motion/installCheck.ts +65 -0
- package/src/presence/Presence.tsx +165 -0
- package/src/presence/PresenceContext.ts +28 -0
- package/src/presence/index.ts +6 -0
- package/src/testing/index.ts +78 -0
- package/src/touch/index.ts +18 -0
- package/src/touch/useTouchDrag.ts +289 -0
- package/src/transitions/easing.ts +53 -0
- package/src/transitions/index.ts +7 -0
- package/src/transitions/keys.ts +32 -0
- package/src/transitions/resolve.ts +169 -0
- package/src/transitions/resolveSequence.ts +87 -0
- package/src/transitions/runtime.ts +63 -0
- package/src/transitions/sig.ts +40 -0
- package/src/transitions/spring.ts +42 -0
- package/src/types.ts +383 -0
- package/src/values/index.ts +26 -0
- package/src/values/useAnimation.ts +69 -0
- package/src/values/useBooleanSpring.ts +33 -0
- package/src/values/useColorTransition.ts +72 -0
- package/src/values/useGesture.ts +144 -0
- package/src/values/useMotionValue.ts +33 -0
- package/src/values/useScroll.ts +72 -0
- package/src/values/useShadow.ts +116 -0
- package/src/values/useSpring.ts +98 -0
- package/src/values/useTransform.ts +132 -0
- package/src/values/useVariants.ts +60 -0
package/jest-setup.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Inertia's Jest setup. Loaded automatically by `@rootnative/inertia/jest-preset`,
|
|
2
|
+
// or addable to a hand-rolled config via `setupFiles`.
|
|
3
|
+
//
|
|
4
|
+
// The mocks here cover everything the core, gestures, gradients, and svg
|
|
5
|
+
// packages exercise. Re-create this list locally only if you can't use the
|
|
6
|
+
// preset (e.g. you have a custom transform pipeline that conflicts).
|
|
7
|
+
|
|
8
|
+
// `react-native-worklets` is a required peer of Reanimated 4 and ships its
|
|
9
|
+
// own native module. The source files import `isWorkletFunction` from it
|
|
10
|
+
// directly (Reanimated's re-export is deprecated); under Jest we stub the
|
|
11
|
+
// surface so the source guard works the same way the Reanimated mock does.
|
|
12
|
+
jest.mock('react-native-worklets', () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
isWorkletFunction: () => false,
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
// Override RN's Text mock — the default mockComponent crashes on arrow function
|
|
18
|
+
// components exported by RN 0.81's Flow `component` syntax.
|
|
19
|
+
jest.mock('react-native/Libraries/Text/Text', () => {
|
|
20
|
+
const React = require('react')
|
|
21
|
+
const Text = React.forwardRef(({ children, ...props }, ref) =>
|
|
22
|
+
React.createElement('RCTText', { ...props, ref }, children),
|
|
23
|
+
)
|
|
24
|
+
Text.displayName = 'Text'
|
|
25
|
+
return { __esModule: true, default: Text }
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Reanimated mock — STATIC RENDER, with stable `useSharedValue` semantics.
|
|
29
|
+
//
|
|
30
|
+
// `useAnimatedStyle` invokes the worklet exactly once per render and returns
|
|
31
|
+
// a plain object. It does not subscribe to shared-value mutations — to
|
|
32
|
+
// observe a post-mount target value you must trigger a re-render (the
|
|
33
|
+
// `renderWithMotion` / `flushMotion` helpers in `@rootnative/inertia/testing`
|
|
34
|
+
// do this for you).
|
|
35
|
+
//
|
|
36
|
+
// `useSharedValue` is backed by `useRef` so the same `{ value }` object
|
|
37
|
+
// persists across renders. After a `useEffect` assigns the target, the next
|
|
38
|
+
// render reads it back.
|
|
39
|
+
//
|
|
40
|
+
// What this means for tests:
|
|
41
|
+
// ✅ assert at-rest structure / role / accessibility / static styles
|
|
42
|
+
// ✅ assert post-effect target styles by re-rendering (use
|
|
43
|
+
// `renderWithMotion` from the testing subpath)
|
|
44
|
+
// ❌ frame-level intermediate states are not observable — physics doesn't
|
|
45
|
+
// run; targets snap in one step.
|
|
46
|
+
jest.mock('react-native-reanimated', () => {
|
|
47
|
+
const React = require('react')
|
|
48
|
+
const { Image, ScrollView, Text, View } = require('react-native')
|
|
49
|
+
|
|
50
|
+
const wrap = (Component, displayName) => {
|
|
51
|
+
const Wrapped = React.forwardRef((props, ref) =>
|
|
52
|
+
React.createElement(Component, { ...props, ref }),
|
|
53
|
+
)
|
|
54
|
+
Wrapped.displayName = displayName
|
|
55
|
+
return Wrapped
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const AnimatedView = wrap(View, 'Animated.View')
|
|
59
|
+
const AnimatedText = wrap(Text, 'Animated.Text')
|
|
60
|
+
const AnimatedImage = wrap(Image, 'Animated.Image')
|
|
61
|
+
const AnimatedScrollView = wrap(ScrollView, 'Animated.ScrollView')
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
__esModule: true,
|
|
65
|
+
default: {
|
|
66
|
+
View: AnimatedView,
|
|
67
|
+
Text: AnimatedText,
|
|
68
|
+
Image: AnimatedImage,
|
|
69
|
+
ScrollView: AnimatedScrollView,
|
|
70
|
+
createAnimatedComponent: (c) =>
|
|
71
|
+
wrap(c, `Animated(${c.displayName ?? c.name ?? 'Component'})`),
|
|
72
|
+
},
|
|
73
|
+
View: AnimatedView,
|
|
74
|
+
Text: AnimatedText,
|
|
75
|
+
Image: AnimatedImage,
|
|
76
|
+
ScrollView: AnimatedScrollView,
|
|
77
|
+
useSharedValue: (initial) => {
|
|
78
|
+
const ref = React.useRef(null)
|
|
79
|
+
if (ref.current === null) ref.current = { value: initial }
|
|
80
|
+
return ref.current
|
|
81
|
+
},
|
|
82
|
+
useDerivedValue: (fn) => ({ value: fn() }),
|
|
83
|
+
useAnimatedStyle: (fn) => fn(),
|
|
84
|
+
useAnimatedProps: (fn) => fn(),
|
|
85
|
+
useAnimatedReaction: (prepare, react) => {
|
|
86
|
+
// Best-effort sync invocation: run prepare() once, hand the result to
|
|
87
|
+
// react() so tests can observe the side effect (e.g. `useSpring`
|
|
88
|
+
// forwarding a target into its output shared value). Physics still
|
|
89
|
+
// doesn't run — `withSpring` is the identity in this mock.
|
|
90
|
+
const value = typeof prepare === 'function' ? prepare() : undefined
|
|
91
|
+
if (typeof react === 'function') react(value, undefined)
|
|
92
|
+
},
|
|
93
|
+
useAnimatedScrollHandler: (handlers) => {
|
|
94
|
+
// The real handler is an opaque worklet bag; in tests we return a plain
|
|
95
|
+
// function that invokes the appropriate user handler synchronously so
|
|
96
|
+
// assertions on scroll-driven shared values work without a native event
|
|
97
|
+
// loop. Shape: `useAnimatedScrollHandler({ onScroll })` or
|
|
98
|
+
// `useAnimatedScrollHandler(onScroll)`.
|
|
99
|
+
const onScroll =
|
|
100
|
+
typeof handlers === 'function' ? handlers : handlers?.onScroll
|
|
101
|
+
return (event) => {
|
|
102
|
+
if (typeof onScroll === 'function')
|
|
103
|
+
onScroll(event?.nativeEvent ?? event)
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
useReducedMotion: () => false,
|
|
107
|
+
// Inertia's dev-time install check reads this to detect a too-old
|
|
108
|
+
// Reanimated. The check is skipped under NODE_ENV=test, but the named
|
|
109
|
+
// import must still resolve for consumers' test suites.
|
|
110
|
+
reanimatedVersion: '4.0.0',
|
|
111
|
+
isWorkletFunction: () => false,
|
|
112
|
+
cancelAnimation: () => {},
|
|
113
|
+
runOnJS: (fn) => fn,
|
|
114
|
+
runOnUI: (fn) => fn,
|
|
115
|
+
withSpring: (v) => v,
|
|
116
|
+
withTiming: (v) => v,
|
|
117
|
+
withDecay: (v) => v,
|
|
118
|
+
withDelay: (_d, v) => v,
|
|
119
|
+
withRepeat: (v) => v,
|
|
120
|
+
withSequence: (...args) => args[args.length - 1],
|
|
121
|
+
Easing: {
|
|
122
|
+
// Reanimated 4's `Easing.bezier(...)` returns an `EasingFunctionFactory`
|
|
123
|
+
// (`{ factory: () => EasingFunction }`), not a bare function. We surface
|
|
124
|
+
// both shapes: the returned value is callable (backward-compat with
|
|
125
|
+
// pre-4 call sites that did `Easing.bezier(...)(t)`) AND has `.factory()`
|
|
126
|
+
// so the canonical Reanimated 4 unwrap `Easing.bezier(...).factory()`
|
|
127
|
+
// works under Jest without consumer overrides.
|
|
128
|
+
bezier: () => {
|
|
129
|
+
const fn = () => 0
|
|
130
|
+
fn.factory = () => fn
|
|
131
|
+
return fn
|
|
132
|
+
},
|
|
133
|
+
ease: () => 0,
|
|
134
|
+
linear: (t) => t,
|
|
135
|
+
quad: () => 0,
|
|
136
|
+
cubic: () => 0,
|
|
137
|
+
sin: () => 0,
|
|
138
|
+
circle: () => 0,
|
|
139
|
+
exp: () => 0,
|
|
140
|
+
poly: () => () => 0,
|
|
141
|
+
back: () => () => 0,
|
|
142
|
+
bounce: () => 0,
|
|
143
|
+
elastic: () => () => 0,
|
|
144
|
+
in: (fn) => fn,
|
|
145
|
+
out: (fn) => fn,
|
|
146
|
+
inOut: (fn) => fn,
|
|
147
|
+
step0: () => 0,
|
|
148
|
+
step1: () => 0,
|
|
149
|
+
},
|
|
150
|
+
interpolate: (value, _input, output) =>
|
|
151
|
+
value >= 1 ? output[output.length - 1] : output[0],
|
|
152
|
+
interpolateColor: (value, _input, output) =>
|
|
153
|
+
value >= 1 ? output[output.length - 1] : output[0],
|
|
154
|
+
Extrapolation: { CLAMP: 'clamp', IDENTITY: 'identity', EXTEND: 'extend' },
|
|
155
|
+
// Layout-animation builder stub. The real builder is chainable and records
|
|
156
|
+
// spring / timing config; tests assert against the recorded fields. Each
|
|
157
|
+
// chain call returns a fresh instance with the field set so the resolver's
|
|
158
|
+
// immutable-chain idiom (rebinding `builder = builder.x(...)`) works.
|
|
159
|
+
LinearTransition: (() => {
|
|
160
|
+
class LinearTransitionStub {
|
|
161
|
+
constructor() {
|
|
162
|
+
this.__kind = 'LinearTransition'
|
|
163
|
+
}
|
|
164
|
+
_clone(patch) {
|
|
165
|
+
const next = new LinearTransitionStub()
|
|
166
|
+
Object.assign(next, this, patch)
|
|
167
|
+
return next
|
|
168
|
+
}
|
|
169
|
+
springify(duration) {
|
|
170
|
+
return this._clone({ __mode: 'spring', __duration: duration })
|
|
171
|
+
}
|
|
172
|
+
damping(v) {
|
|
173
|
+
return this._clone({ damping: v })
|
|
174
|
+
}
|
|
175
|
+
stiffness(v) {
|
|
176
|
+
return this._clone({ stiffness: v })
|
|
177
|
+
}
|
|
178
|
+
mass(v) {
|
|
179
|
+
return this._clone({ mass: v })
|
|
180
|
+
}
|
|
181
|
+
dampingRatio(v) {
|
|
182
|
+
return this._clone({ dampingRatio: v })
|
|
183
|
+
}
|
|
184
|
+
duration(v) {
|
|
185
|
+
return this._clone({ __mode: 'timing', __duration: v })
|
|
186
|
+
}
|
|
187
|
+
easing(fn) {
|
|
188
|
+
return this._clone({ easing: fn })
|
|
189
|
+
}
|
|
190
|
+
delay(v) {
|
|
191
|
+
return this._clone({ delay: v })
|
|
192
|
+
}
|
|
193
|
+
reduceMotion(v) {
|
|
194
|
+
return this._clone({ reduceMotion: v })
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const proxy = new Proxy(LinearTransitionStub, {
|
|
198
|
+
get(target, prop) {
|
|
199
|
+
if (prop in target) return target[prop]
|
|
200
|
+
// Forward static-style invocations to a fresh instance so
|
|
201
|
+
// `LinearTransition.springify()` works as well as
|
|
202
|
+
// `new LinearTransition().springify()`.
|
|
203
|
+
return (...args) => {
|
|
204
|
+
const instance = new LinearTransitionStub()
|
|
205
|
+
return instance[prop](...args)
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
return proxy
|
|
210
|
+
})(),
|
|
211
|
+
ReduceMotion: { System: 'system', Always: 'always', Never: 'never' },
|
|
212
|
+
}
|
|
213
|
+
})
|
package/llms.txt
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Inertia
|
|
2
|
+
|
|
3
|
+
> Declarative animation primitives for React Native, built on react-native-reanimated. Inspired by Framer Motion (web) and react-spring (cross-platform). DX-first — animations are props on a component, not imperative shared values, worklets, and `useAnimatedStyle` boilerplate.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @rootnative/inertia react-native-reanimated
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Enable the Reanimated Babel plugin per its install guide.
|
|
12
|
+
|
|
13
|
+
## Imports
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
Motion,
|
|
18
|
+
Presence,
|
|
19
|
+
MotionConfig,
|
|
20
|
+
useGesture,
|
|
21
|
+
useVariants,
|
|
22
|
+
useMotionValue,
|
|
23
|
+
useAnimation,
|
|
24
|
+
useSpring,
|
|
25
|
+
useBooleanSpring,
|
|
26
|
+
useTransform,
|
|
27
|
+
useShadow,
|
|
28
|
+
useColorTransition,
|
|
29
|
+
useScroll,
|
|
30
|
+
} from '@rootnative/inertia'
|
|
31
|
+
// Opt-in MD3 / iOS state-layer overlay helper:
|
|
32
|
+
import { useGestureLayer } from '@rootnative/inertia/gesture-layer'
|
|
33
|
+
// or for tree-shaking the primitives:
|
|
34
|
+
import { MotionView } from '@rootnative/inertia/view'
|
|
35
|
+
import { MotionText } from '@rootnative/inertia/text'
|
|
36
|
+
import { MotionImage } from '@rootnative/inertia/image'
|
|
37
|
+
import { MotionPressable } from '@rootnative/inertia/pressable'
|
|
38
|
+
import { MotionScrollView } from '@rootnative/inertia/scroll-view'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Public API
|
|
42
|
+
|
|
43
|
+
- `Motion.View` / `Motion.Text` / `Motion.Image` / `Motion.Pressable` / `Motion.ScrollView` — animatable primitives. Per-primitive style inference (no shared `ViewStyle & TextStyle & ImageStyle` fallback).
|
|
44
|
+
- `<Presence>` — mount / unmount transitions; children need explicit `key`s. Exiting children get `pointerEvents: 'none'` automatically.
|
|
45
|
+
- `<MotionConfig reducedMotion="user" | "never" | "always">` — gates motion against the OS reduce-motion setting (default `"user"`).
|
|
46
|
+
- `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).
|
|
47
|
+
- `useGestureLayer(states, options?)` — opt-in helper at `@rootnative/inertia/gesture-layer` for the **strongest-active-layer-wins** composition (MD3 state-layer haloes, iOS-translucent overlays). Supply per-state target maps (`rest` / `hovered` / `focused` / `focusVisible` / `pressed` / `disabled`); the hook owns the worklet. Numeric keys compose via clamped-max with `rest` as the floor; color keys compose via priority cascade with `interpolateColor`; `disabled` (JS-side flag) overrides every gesture layer. Returns `{ style, handlers }`. Reach for plain `useGesture` for additive blends or per-key custom rules.
|
|
48
|
+
- `useVariants(variants, initial?)` — returns `{ current, transitionTo }` controller for the `controller` prop.
|
|
49
|
+
- `useMotionValue(initial)` — thin pass-through over `useSharedValue<T>`. Returns a `SharedValue<T>` directly so it interops with every Reanimated API without unwrapping.
|
|
50
|
+
- `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.
|
|
51
|
+
- `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.
|
|
52
|
+
- `useBooleanSpring(active, config?)` — sugar over `useSpring` for the recurring "spring 0↔1 progress from a boolean" shape (checkbox checks, accordion expansions, drawer open/closed). Identical mechanics to `useSpring(active ? 1 : 0, config)` — the named form so the call site reads as the boolean it represents.
|
|
53
|
+
- `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.
|
|
54
|
+
- `useShadow({ from, to, progress })` — pure value-layer interpolator between two `ShadowConfig`s (`shadowOpacity` / `shadowRadius` / `shadowOffset` / `elevation` / `shadowColor`) driven by a `SharedValue<number>` (0→1). Returns an animated style fragment to spread onto `style`; only emits keys present on either side, absent sides default to natural zero. The hook does not animate on its own — drive `progress` with a spring, a scroll-derived `useTransform`, or any other shared value source.
|
|
55
|
+
- `useColorTransition(progress, [from, to], options?)` — pure value-layer interpolator for a single color channel, driven by a `SharedValue<number>` (0→1). Returns an animated style fragment with one color key (default `backgroundColor`; configurable via `options.key` to `color` / `borderColor` / `tintColor` / `shadowColor` / per-side border colors). For raw `SharedValue<string>` output, use `useTransform(progress, [0, 1], [from, to])` instead.
|
|
56
|
+
- `useScroll()` — returns `{ scrollX, scrollY, onScroll }` for use with `Motion.ScrollView`. Scroll events fire on the UI thread.
|
|
57
|
+
- `createMotionComponent<C>(C)` — wrap any component with the same Motion prop surface, inferring style from `C`.
|
|
58
|
+
- `buildReleaseAnimation(transition, toValue)` — worklet-safe single-step animation builder (spring / timing / decay / no-animation). For assigning Inertia-resolved animations to shared values from inside a gesture worklet. Used internally by `@rootnative/inertia-gestures`'s `useDrag({ onRelease })`.
|
|
59
|
+
|
|
60
|
+
## Motion props
|
|
61
|
+
|
|
62
|
+
`initial` (mount-only, non-reactive after first render — pass `false` to skip), `animate`, `exit`, `variants`, `controller`, `gesture`, `transition`, `layout`, `onAnimationEnd`.
|
|
63
|
+
|
|
64
|
+
`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.
|
|
65
|
+
|
|
66
|
+
## Transitions
|
|
67
|
+
|
|
68
|
+
| `type` | Public config | Default? |
|
|
69
|
+
| ---------------- | ---------------------------------------------------------------- | -------- |
|
|
70
|
+
| `'spring'` | `tension`, `friction`, `mass`, `velocity`, `delay`, `repeat` | yes |
|
|
71
|
+
| `'timing'` | `duration`, `easing` (`EasingFunction` or Reanimated 4 `EasingFunctionFactory` — `Easing.bezier(...)` works directly, no `.factory()` call needed), `delay`, `repeat` | |
|
|
72
|
+
| `'decay'` | `velocity`, `deceleration`, `clamp`, `delay` | |
|
|
73
|
+
| `'no-animation'` | — | |
|
|
74
|
+
|
|
75
|
+
Spring uses react-spring vocabulary (`tension`, `friction`, `mass`). Reanimated's raw `stiffness` / `damping` never appear in the public API. Per-property `transition` entries override the top-level config.
|
|
76
|
+
|
|
77
|
+
## Sequences and repeat
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<Motion.View
|
|
81
|
+
animate={{
|
|
82
|
+
translateX: [0, 100, 0],
|
|
83
|
+
opacity: [0, { to: 1, type: 'spring' }, { to: 0, delay: 500 }],
|
|
84
|
+
}}
|
|
85
|
+
transition={{ repeat: 'infinite' }}
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`repeat`: `number` (finite, alternating) | `'infinite'` | `{ count, alternate }`.
|
|
90
|
+
|
|
91
|
+
## Gestures
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
<Motion.View
|
|
95
|
+
gesture={{
|
|
96
|
+
hovered: { backgroundColor: '#0001' },
|
|
97
|
+
focused: { backgroundColor: '#0002' },
|
|
98
|
+
pressed: { backgroundColor: '#0003' },
|
|
99
|
+
}}
|
|
100
|
+
transition={{ type: 'timing', duration: 120 }}
|
|
101
|
+
/>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Sub-states composite as a **priority cascade** in the order `hovered → focused → focusVisible → pressed` (highest-priority active layer's target wins — values are never summed). 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.
|
|
105
|
+
|
|
106
|
+
Per-layer transitions via `transition.<stateName>`:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
transition={{
|
|
110
|
+
pressed: { type: 'timing', duration: 50 },
|
|
111
|
+
hovered: { type: 'timing', duration: 90 },
|
|
112
|
+
}}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`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.
|
|
116
|
+
|
|
117
|
+
## Caveats
|
|
118
|
+
|
|
119
|
+
- Every `Motion.*` primitive **throws in dev** when `style` is a function. Reanimated's animated-component wrapper would silently drop the function-form `style={({ pressed }) => ...}` that RN's `Pressable` accepts, so the throw catches the footgun at the source. Drive press/focus/hover styling through `gesture` instead, or compute conditional styles once in render. Production builds skip the check (the styles would still be dropped — catching it in dev is the safety net).
|
|
120
|
+
- `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.
|
|
121
|
+
|
|
122
|
+
## Animatable properties (alpha)
|
|
123
|
+
|
|
124
|
+
Numeric: `opacity`, `translateX`, `translateY`, `scale`, `scaleX`, `scaleY`, `rotate`, `rotateX`, `rotateY`, `width`, `height`, `borderRadius`, `shadowOpacity`, `shadowRadius`, `elevation`. Rotation values are degrees; the factory wraps them as `'${value}deg'`. `rotateX` / `rotateY` need a sibling `perspective` style entry to render in 3D.
|
|
125
|
+
|
|
126
|
+
Color: `backgroundColor`, `borderColor`, `color`, `tintColor` (Image only), `shadowColor`. 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.
|
|
127
|
+
|
|
128
|
+
Nested object: `shadowOffset: { width, height }`. The only nested-object style on the surface — internally decomposes into two synthetic axis SVs that the worklet recomposes into a single `shadowOffset` prop. v0.1 supports the **single-value form only** (`{ width: 0, height: 4 }`); sequences / array keyframes / per-axis transition splits are out of scope, drop to `useMotionValue` for those.
|
|
129
|
+
|
|
130
|
+
Auto-layout transitions ship via the `layout` prop (`true` / `TransitionConfig`) on every `Motion.*` primitive — see Layout. Shared element transitions ship via the `layoutId` prop: pair the same id on a source and target `Motion.*` and Inertia FLIPs between them on mount (parent-relative coords, rect-only animation, 1s TTL).
|
|
131
|
+
|
|
132
|
+
## Optional adapter packages
|
|
133
|
+
|
|
134
|
+
- `@rootnative/inertia-gradients` — `MotionLinearGradient` over `expo-linear-gradient`. Animatable: `colors`, `start`, `end`, `locations`.
|
|
135
|
+
- `@rootnative/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.
|
|
136
|
+
- `@rootnative/inertia-gestures` — `useDrag`, `useSwipe`, `usePan` over `react-native-gesture-handler`. `useDrag` accepts an `onRelease` worklet that returns per-axis Inertia release transitions (snap-to-tick spring, decay with bounds, etc.) — the release velocity stays on the UI thread, no JS round-trip.
|
|
137
|
+
|
|
138
|
+
## Docs
|
|
139
|
+
|
|
140
|
+
- Full docs: https://rootnative.github.io/inertia/
|
|
141
|
+
- Per-page LLM reference: https://rootnative.github.io/inertia/llms-full.txt
|
|
142
|
+
- Source: https://github.com/rootnative/inertia
|
package/package.json
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rootnative/inertia",
|
|
3
|
+
"version": "0.0.0-alpha.0",
|
|
4
|
+
"description": "Declarative animation primitives for React Native, built on react-native-reanimated.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "RootNative",
|
|
7
|
+
"homepage": "https://github.com/rootnative/inertia",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rootnative/inertia.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rootnative/inertia/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"reanimated",
|
|
19
|
+
"animation",
|
|
20
|
+
"motion",
|
|
21
|
+
"expo",
|
|
22
|
+
"framer-motion",
|
|
23
|
+
"react-spring",
|
|
24
|
+
"declarative"
|
|
25
|
+
],
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"module": "./dist/index.mjs",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"react-native": "./src/index.ts",
|
|
31
|
+
"source": "./src/index.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"react-native": "./src/index.ts",
|
|
36
|
+
"source": "./src/index.ts",
|
|
37
|
+
"import": "./dist/index.mjs",
|
|
38
|
+
"require": "./dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./view": {
|
|
41
|
+
"types": "./dist/motion/View.d.ts",
|
|
42
|
+
"react-native": "./src/motion/View.tsx",
|
|
43
|
+
"source": "./src/motion/View.tsx",
|
|
44
|
+
"import": "./dist/motion/View.mjs",
|
|
45
|
+
"require": "./dist/motion/View.js"
|
|
46
|
+
},
|
|
47
|
+
"./text": {
|
|
48
|
+
"types": "./dist/motion/Text.d.ts",
|
|
49
|
+
"react-native": "./src/motion/Text.tsx",
|
|
50
|
+
"source": "./src/motion/Text.tsx",
|
|
51
|
+
"import": "./dist/motion/Text.mjs",
|
|
52
|
+
"require": "./dist/motion/Text.js"
|
|
53
|
+
},
|
|
54
|
+
"./image": {
|
|
55
|
+
"types": "./dist/motion/Image.d.ts",
|
|
56
|
+
"react-native": "./src/motion/Image.tsx",
|
|
57
|
+
"source": "./src/motion/Image.tsx",
|
|
58
|
+
"import": "./dist/motion/Image.mjs",
|
|
59
|
+
"require": "./dist/motion/Image.js"
|
|
60
|
+
},
|
|
61
|
+
"./pressable": {
|
|
62
|
+
"types": "./dist/motion/Pressable.d.ts",
|
|
63
|
+
"react-native": "./src/motion/Pressable.tsx",
|
|
64
|
+
"source": "./src/motion/Pressable.tsx",
|
|
65
|
+
"import": "./dist/motion/Pressable.mjs",
|
|
66
|
+
"require": "./dist/motion/Pressable.js"
|
|
67
|
+
},
|
|
68
|
+
"./scroll-view": {
|
|
69
|
+
"types": "./dist/motion/ScrollView.d.ts",
|
|
70
|
+
"react-native": "./src/motion/ScrollView.tsx",
|
|
71
|
+
"source": "./src/motion/ScrollView.tsx",
|
|
72
|
+
"import": "./dist/motion/ScrollView.mjs",
|
|
73
|
+
"require": "./dist/motion/ScrollView.js"
|
|
74
|
+
},
|
|
75
|
+
"./testing": {
|
|
76
|
+
"types": "./dist/testing/index.d.ts",
|
|
77
|
+
"react-native": "./src/testing/index.ts",
|
|
78
|
+
"source": "./src/testing/index.ts",
|
|
79
|
+
"import": "./dist/testing/index.mjs",
|
|
80
|
+
"require": "./dist/testing/index.js"
|
|
81
|
+
},
|
|
82
|
+
"./touch": {
|
|
83
|
+
"types": "./dist/touch/index.d.ts",
|
|
84
|
+
"react-native": "./src/touch/index.ts",
|
|
85
|
+
"source": "./src/touch/index.ts",
|
|
86
|
+
"import": "./dist/touch/index.mjs",
|
|
87
|
+
"require": "./dist/touch/index.js"
|
|
88
|
+
},
|
|
89
|
+
"./gesture-layer": {
|
|
90
|
+
"types": "./dist/gestureLayer/index.d.ts",
|
|
91
|
+
"react-native": "./src/gestureLayer/index.ts",
|
|
92
|
+
"source": "./src/gestureLayer/index.ts",
|
|
93
|
+
"import": "./dist/gestureLayer/index.mjs",
|
|
94
|
+
"require": "./dist/gestureLayer/index.js"
|
|
95
|
+
},
|
|
96
|
+
"./jest-preset": "./jest-preset.js",
|
|
97
|
+
"./jest-setup": "./jest-setup.js",
|
|
98
|
+
"./package.json": "./package.json"
|
|
99
|
+
},
|
|
100
|
+
"files": [
|
|
101
|
+
"dist",
|
|
102
|
+
"src",
|
|
103
|
+
"jest-preset.js",
|
|
104
|
+
"jest-setup.js",
|
|
105
|
+
"llms.txt",
|
|
106
|
+
"README.md",
|
|
107
|
+
"LICENSE",
|
|
108
|
+
"CHANGELOG.md",
|
|
109
|
+
"!**/__tests__",
|
|
110
|
+
"!**/*.test.*"
|
|
111
|
+
],
|
|
112
|
+
"peerDependencies": {
|
|
113
|
+
"react": ">=19.0.0",
|
|
114
|
+
"react-native": ">=0.81.0",
|
|
115
|
+
"react-native-reanimated": ">=4.0.0",
|
|
116
|
+
"react-native-worklets": ">=0.5.0"
|
|
117
|
+
},
|
|
118
|
+
"peerDependenciesMeta": {
|
|
119
|
+
"react-native-worklets": {
|
|
120
|
+
"optional": true
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"devDependencies": {
|
|
124
|
+
"@react-native/babel-preset": "^0.81.5",
|
|
125
|
+
"@size-limit/preset-small-lib": "^11.1.0",
|
|
126
|
+
"@testing-library/react-native": "^13.3.3",
|
|
127
|
+
"@types/jest": "^29.5.14",
|
|
128
|
+
"@types/react": "^19.1.0",
|
|
129
|
+
"jest": "^29.7.0",
|
|
130
|
+
"react": "19.1.0",
|
|
131
|
+
"react-native": "0.81.5",
|
|
132
|
+
"react-native-reanimated": "~4.1.1",
|
|
133
|
+
"react-test-renderer": "19.1.0",
|
|
134
|
+
"size-limit": "^11.1.0",
|
|
135
|
+
"tsup": "^8.3.5",
|
|
136
|
+
"typescript": "^5.7.3"
|
|
137
|
+
},
|
|
138
|
+
"publishConfig": {
|
|
139
|
+
"access": "public"
|
|
140
|
+
},
|
|
141
|
+
"scripts": {
|
|
142
|
+
"build": "tsup",
|
|
143
|
+
"dev": "tsup --watch",
|
|
144
|
+
"typecheck": "tsc --noEmit",
|
|
145
|
+
"test": "jest",
|
|
146
|
+
"size": "size-limit",
|
|
147
|
+
"size:why": "size-limit --why",
|
|
148
|
+
"lint": "eslint .",
|
|
149
|
+
"clean": "rm -rf dist .turbo *.tsbuildinfo"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time gate for variant-key narrowing on `animate`.
|
|
3
|
+
*
|
|
4
|
+
* CLAUDE.md Principle 5 / line 43: "Variant string keys must autocomplete on
|
|
5
|
+
* `animate`." That requires the `variants` map to be inferred at the JSX call
|
|
6
|
+
* site and `animate`'s string form to be narrowed to its keys — so a key typo
|
|
7
|
+
* is a type error, not a silent runtime no-op. These assertions run under
|
|
8
|
+
* `tsc --noEmit` (the typecheck CI step); if a `@ts-expect-error` here stops
|
|
9
|
+
* being an error, tsc fails with "Unused '@ts-expect-error' directive" and the
|
|
10
|
+
* differentiator has regressed.
|
|
11
|
+
*
|
|
12
|
+
* Unlike `animate.test-d.tsx`, these MUST mount JSX: the narrowing depends on
|
|
13
|
+
* `V` being inferred from the `variants` prop at the call site, which only
|
|
14
|
+
* happens through the generic component call signature.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Motion } from '../motion'
|
|
18
|
+
|
|
19
|
+
const variants = {
|
|
20
|
+
open: { opacity: 1, translateY: 0 },
|
|
21
|
+
closed: { opacity: 0, translateY: 100 },
|
|
22
|
+
} as const
|
|
23
|
+
|
|
24
|
+
// ─── Variant key narrowing ──────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
// A declared key is accepted (and `open` / `closed` autocomplete here).
|
|
27
|
+
const _acceptsKnownKey = <Motion.View variants={variants} animate="open" />
|
|
28
|
+
const _acceptsOtherKey = <Motion.View variants={variants} animate="closed" />
|
|
29
|
+
|
|
30
|
+
// A typo'd key is a compile error rather than a silent no-op.
|
|
31
|
+
// @ts-expect-error 'opne' is not a key of `variants`
|
|
32
|
+
const _rejectsTypoKey = <Motion.View variants={variants} animate="opne" />
|
|
33
|
+
|
|
34
|
+
// The style-object form still works alongside `variants` (escape hatch for a
|
|
35
|
+
// one-off target that isn't a named state).
|
|
36
|
+
const _acceptsStyleObject = (
|
|
37
|
+
<Motion.View variants={variants} animate={{ opacity: 0.5 }} />
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// `as const` is NOT required — an inline object literal narrows just as well.
|
|
41
|
+
const inlineVariants = { a: { opacity: 1 }, b: { opacity: 0 } }
|
|
42
|
+
const _inlineVariantsNarrow = (
|
|
43
|
+
<Motion.View variants={inlineVariants} animate="a" />
|
|
44
|
+
)
|
|
45
|
+
const _inlineVariantsReject = (
|
|
46
|
+
// @ts-expect-error 'c' is not a key of the inline variants map
|
|
47
|
+
<Motion.View variants={inlineVariants} animate="c" />
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// ─── No variants → string stays open (back-compat) ──────────────────────────
|
|
51
|
+
|
|
52
|
+
// Without `variants`, the string form is unconstrained, so this must NOT error
|
|
53
|
+
// (the variant-less call site is unchanged by the narrowing machinery).
|
|
54
|
+
const _noVariantsAnyString = <Motion.View animate="whatever" />
|
|
55
|
+
const _noVariantsStyleObject = <Motion.View animate={{ translateX: 10 }} />
|
|
56
|
+
|
|
57
|
+
// Silence "declared but never read" — these exist purely as type assertions.
|
|
58
|
+
export type _VariantTypeAssertions = [
|
|
59
|
+
typeof _acceptsKnownKey,
|
|
60
|
+
typeof _acceptsOtherKey,
|
|
61
|
+
typeof _rejectsTypoKey,
|
|
62
|
+
typeof _acceptsStyleObject,
|
|
63
|
+
typeof _inlineVariantsNarrow,
|
|
64
|
+
typeof _inlineVariantsReject,
|
|
65
|
+
typeof _noVariantsAnyString,
|
|
66
|
+
typeof _noVariantsStyleObject,
|
|
67
|
+
]
|