@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.
- package/README.md +44 -3
- package/dist/index.d.mts +259 -3
- package/dist/index.d.ts +259 -3
- package/dist/index.js +1866 -161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1864 -165
- package/dist/index.mjs.map +1 -1
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +1696 -146
- package/dist/motion/Image.js.map +1 -1
- package/dist/motion/Image.mjs +1698 -148
- package/dist/motion/Image.mjs.map +1 -1
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +1696 -146
- package/dist/motion/Pressable.js.map +1 -1
- package/dist/motion/Pressable.mjs +1698 -148
- package/dist/motion/Pressable.mjs.map +1 -1
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +1696 -146
- package/dist/motion/ScrollView.js.map +1 -1
- package/dist/motion/ScrollView.mjs +1698 -148
- package/dist/motion/ScrollView.mjs.map +1 -1
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +1696 -146
- package/dist/motion/Text.js.map +1 -1
- package/dist/motion/Text.mjs +1698 -148
- package/dist/motion/Text.mjs.map +1 -1
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +1696 -146
- package/dist/motion/View.js.map +1 -1
- package/dist/motion/View.mjs +1698 -148
- package/dist/motion/View.mjs.map +1 -1
- package/dist/{types-DeZZzE_e.d.mts → types-CjztO3RW.d.mts} +89 -20
- package/dist/{types-DeZZzE_e.d.ts → types-CjztO3RW.d.ts} +89 -20
- package/llms.txt +54 -6
- package/package.json +1 -1
- package/src/__type-tests__/animate.test-d.tsx +88 -0
- package/src/index.ts +16 -1
- package/src/layout/index.ts +1 -0
- package/src/layout/resolveLayout.ts +54 -0
- package/src/motion/createMotionComponent.tsx +292 -153
- package/src/motion/installCheck.ts +69 -0
- package/src/transitions/easing.ts +3 -1
- package/src/transitions/index.ts +3 -0
- package/src/transitions/keys.ts +32 -0
- package/src/transitions/resolve.ts +1 -24
- package/src/transitions/sig.ts +40 -0
- package/src/transitions/spring.ts +41 -0
- package/src/types.ts +96 -18
- package/src/values/index.ts +14 -0
- package/src/values/useAnimation.ts +69 -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/useSpring.ts +93 -0
- package/src/values/useTransform.ts +132 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
declare const __DEV__: boolean
|
|
2
|
+
declare const process: { env?: Record<string, string | undefined> }
|
|
3
|
+
declare const require: (path: string) => unknown
|
|
4
|
+
|
|
5
|
+
let alreadyChecked = false
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Surface a clear, actionable error at first `createMotionComponent` call when
|
|
9
|
+
* the consumer's Reanimated install is broken. Production builds, repeat calls,
|
|
10
|
+
* and Jest test runs are all skipped — the check is purely a dev-time
|
|
11
|
+
* paper-cut sander for the two failure modes we can detect from JS:
|
|
12
|
+
*
|
|
13
|
+
* 1. `react-native-reanimated` resolves but is on a v3.x line we don't
|
|
14
|
+
* support (the plugin name and worklet runtime both changed at v4).
|
|
15
|
+
* 2. The worklets babel plugin (`react-native-worklets/plugin` in v4) isn't
|
|
16
|
+
* wired into `babel.config.js`, so `'worklet'` directives are dead strings
|
|
17
|
+
* and the first `withSpring` / `withTiming` call would crash on the UI
|
|
18
|
+
* thread with a generic "non-worklet function called" error.
|
|
19
|
+
*
|
|
20
|
+
* The "Reanimated isn't installed at all" case isn't handled here — Metro
|
|
21
|
+
* fails to resolve the static `import 'react-native-reanimated'` at the top
|
|
22
|
+
* of `createMotionComponent.tsx` long before this check runs.
|
|
23
|
+
*/
|
|
24
|
+
export function ensureReanimatedInstalled(): void {
|
|
25
|
+
if (!__DEV__ || alreadyChecked) return
|
|
26
|
+
// The standard `react-native-reanimated/mock` doesn't run the worklets
|
|
27
|
+
// babel plugin, so the marker probe would false-positive every test run.
|
|
28
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
alreadyChecked = true
|
|
32
|
+
|
|
33
|
+
let version: string | undefined
|
|
34
|
+
try {
|
|
35
|
+
const pkg = require('react-native-reanimated/package.json') as {
|
|
36
|
+
version?: string
|
|
37
|
+
}
|
|
38
|
+
version = pkg.version
|
|
39
|
+
} catch {
|
|
40
|
+
// package.json subpath blocked by `exports` field — skip the version
|
|
41
|
+
// probe rather than emit a misleading error.
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (version) {
|
|
45
|
+
const major = parseInt(version.split('.')[0] ?? '0', 10)
|
|
46
|
+
if (major < 4) {
|
|
47
|
+
console.error(
|
|
48
|
+
`[inertia] react-native-reanimated v${version} is installed, but @onlynative/inertia requires v4.0.0 or later. ` +
|
|
49
|
+
`Upgrade with \`pnpm add react-native-reanimated@^4\` (or your package manager's equivalent).`,
|
|
50
|
+
)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The worklets plugin rewrites any function carrying a top-of-body
|
|
56
|
+
// `'worklet'` directive to expose a `__workletHash` property at runtime.
|
|
57
|
+
// Its absence means the plugin didn't run.
|
|
58
|
+
const probe = function probe() {
|
|
59
|
+
'worklet'
|
|
60
|
+
return 0
|
|
61
|
+
} as { __workletHash?: number }
|
|
62
|
+
if (typeof probe.__workletHash !== 'number') {
|
|
63
|
+
console.error(
|
|
64
|
+
`[inertia] The Reanimated worklets babel plugin is not configured. ` +
|
|
65
|
+
`Add \`'react-native-worklets/plugin'\` as the LAST entry in the \`plugins\` array of your \`babel.config.js\`, ` +
|
|
66
|
+
`then restart Metro with a fresh cache: \`npx expo start -c\` or \`npx react-native start --reset-cache\`.`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
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
|
package/src/transitions/index.ts
CHANGED
|
@@ -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,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable string signature for an arbitrary value — used as a dep-array
|
|
3
|
+
* member so a fresh object literal each render doesn't re-fire an effect
|
|
4
|
+
* unless something structurally changed. Functions serialize as `null`
|
|
5
|
+
* (their identity isn't useful in a sig); `undefined` collapses to an empty
|
|
6
|
+
* string so omitted props compare equal across renders.
|
|
7
|
+
*/
|
|
8
|
+
export function stableSig(value: unknown): string {
|
|
9
|
+
if (value === undefined) return ''
|
|
10
|
+
try {
|
|
11
|
+
return stableStringify(value)
|
|
12
|
+
} catch {
|
|
13
|
+
return String(value)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* JSON.stringify with keys sorted at every level so a sig is invariant under
|
|
19
|
+
* property-declaration order. Functions and `undefined` both serialize as
|
|
20
|
+
* `null` — we accept the latter's information loss (rare in practice) in
|
|
21
|
+
* exchange for not crashing on circular function-bearing graphs.
|
|
22
|
+
*/
|
|
23
|
+
function stableStringify(v: unknown): string {
|
|
24
|
+
if (v === null || typeof v !== 'object') {
|
|
25
|
+
if (typeof v === 'function' || v === undefined) return 'null'
|
|
26
|
+
return JSON.stringify(v)
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(v)) {
|
|
29
|
+
return '[' + v.map(stableStringify).join(',') + ']'
|
|
30
|
+
}
|
|
31
|
+
const obj = v as Record<string, unknown>
|
|
32
|
+
const keys = Object.keys(obj).sort()
|
|
33
|
+
return (
|
|
34
|
+
'{' +
|
|
35
|
+
keys
|
|
36
|
+
.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]))
|
|
37
|
+
.join(',') +
|
|
38
|
+
'}'
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type SpringTransition } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default spring physics, expressed in react-spring vocabulary.
|
|
5
|
+
*
|
|
6
|
+
* `tension: 170` / `friction: 26` / `mass: 1` was picked over Reanimated's
|
|
7
|
+
* raw `stiffness: 100` / `damping: 10` default because the raw default
|
|
8
|
+
* overshoots noticeably for the small (~100px) translates that dominate
|
|
9
|
+
* UI work — buttons, sheets, popovers. These numbers settle in ~350ms with
|
|
10
|
+
* a single, almost-imperceptible overshoot, which matches the perceptual
|
|
11
|
+
* target the rest of the library is tuned against.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_SPRING: Required<
|
|
14
|
+
Pick<SpringTransition, 'tension' | 'friction' | 'mass'>
|
|
15
|
+
> = {
|
|
16
|
+
tension: 170,
|
|
17
|
+
friction: 26,
|
|
18
|
+
mass: 1,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert public react-spring vocabulary (`tension` / `friction` / `mass`)
|
|
23
|
+
* to Reanimated's raw `stiffness` / `damping` / `mass`. This is the single
|
|
24
|
+
* place the mapping lives; resolvers, value hooks, and any future surface
|
|
25
|
+
* that needs a Reanimated spring config import from here.
|
|
26
|
+
*
|
|
27
|
+
* The mapping is identity (tension ≡ stiffness, friction ≡ damping) — the
|
|
28
|
+
* names differ but the underlying physics constants are the same. We don't
|
|
29
|
+
* surface the raw names publicly because the react-spring vocabulary is
|
|
30
|
+
* what designers and prior-art consumers expect.
|
|
31
|
+
*/
|
|
32
|
+
export function springToReanimated(t: SpringTransition) {
|
|
33
|
+
return {
|
|
34
|
+
stiffness: t.tension ?? DEFAULT_SPRING.tension,
|
|
35
|
+
damping: t.friction ?? DEFAULT_SPRING.friction,
|
|
36
|
+
mass: t.mass ?? DEFAULT_SPRING.mass,
|
|
37
|
+
velocity: t.velocity,
|
|
38
|
+
restSpeedThreshold: t.restSpeedThreshold,
|
|
39
|
+
restDisplacementThreshold: t.restDisplacementThreshold,
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -75,15 +75,63 @@ export type PerPropertyTransition<S> = {
|
|
|
75
75
|
[K in keyof S]?: TransitionConfig
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Per-gesture-layer transition map. Each `gesture` sub-state animates a
|
|
80
|
+
* progress value 0↔1 with its own transition; the worklet composites the
|
|
81
|
+
* layers in priority order (`hovered → focused → focusVisible → pressed`).
|
|
82
|
+
*
|
|
83
|
+
* Keys live on the same `transition` object as `PerPropertyTransition` because
|
|
84
|
+
* the only other place they could go (nested inside `gesture` itself) would
|
|
85
|
+
* collide with the primitive's inferred style keys.
|
|
86
|
+
*/
|
|
87
|
+
export interface GestureLayerTransitions {
|
|
88
|
+
pressed?: TransitionConfig
|
|
89
|
+
focused?: TransitionConfig
|
|
90
|
+
focusVisible?: TransitionConfig
|
|
91
|
+
hovered?: TransitionConfig
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type Transition<S> =
|
|
95
|
+
| TransitionConfig
|
|
96
|
+
| (PerPropertyTransition<S> & GestureLayerTransitions)
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Transform shorthands that Inertia exposes on `animate` but that don't
|
|
100
|
+
* appear on RN's typed ViewStyle as top-level keys. RN keeps `scale`,
|
|
101
|
+
* `rotate`, `rotateX`, and `rotateY` inside the `transform` array; only
|
|
102
|
+
* `scaleX`/`scaleY` and `translateX`/`translateY` are surfaced as
|
|
103
|
+
* (deprecated) top-level shortcuts. Inertia's runtime treats these as
|
|
104
|
+
* transform-group keys (see `TRANSFORM_KEYS` in `createMotionComponent`),
|
|
105
|
+
* so they're documented as first-class animatables in `CLAUDE.md` and must
|
|
106
|
+
* be reachable from `animate` without dropping into the `transform: [...]`
|
|
107
|
+
* array form. Rotation values are degrees as numbers — the runtime appends
|
|
108
|
+
* `'deg'` before handing the transform to Reanimated.
|
|
109
|
+
*/
|
|
110
|
+
type AnimatableTransformExtras = {
|
|
111
|
+
scale?: AnimatableValue<number>
|
|
112
|
+
rotate?: AnimatableValue<number>
|
|
113
|
+
rotateX?: AnimatableValue<number>
|
|
114
|
+
rotateY?: AnimatableValue<number>
|
|
115
|
+
}
|
|
79
116
|
|
|
80
117
|
/**
|
|
81
118
|
* The animation state shape inferred from the underlying component's style
|
|
82
119
|
* prop. We narrow to the value side of `style` so consumers see ViewStyle on
|
|
83
120
|
* `Motion.View`, TextStyle on `Motion.Text`, etc. — no shared union.
|
|
121
|
+
*
|
|
122
|
+
* Some components (notably `Pressable`) type `style` as a union of
|
|
123
|
+
* `StyleProp<T>` and a callback `(state) => StyleProp<T>`. If we infer `S`
|
|
124
|
+
* directly from `StyleProp<infer S>`, the callback branch widens `S` to
|
|
125
|
+
* `unknown`, which collapses the animate map to `| {}` and silently
|
|
126
|
+
* accepts any key. Excluding functions first keeps inference tight.
|
|
84
127
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
type _StyleValue<T> = Exclude<T, (...args: any[]) => any>
|
|
130
|
+
|
|
131
|
+
export type AnimateStyle<C> = C extends { style?: infer Raw }
|
|
132
|
+
? _StyleValue<Raw> extends StyleProp<infer S>
|
|
133
|
+
? { [K in keyof S]?: AnimatableValue<S[K]> } & AnimatableTransformExtras
|
|
134
|
+
: never
|
|
87
135
|
: never
|
|
88
136
|
|
|
89
137
|
export interface AnimationCallbackInfo<S> {
|
|
@@ -124,18 +172,26 @@ export type VariantsMap<C> = Record<string, AnimateStyle<C>>
|
|
|
124
172
|
* - `hovered` — web-only. Typed for cross-platform call sites; the runtime is
|
|
125
173
|
* a no-op on native.
|
|
126
174
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
175
|
+
* Sub-states layer additively. Each declared sub-state owns an independent
|
|
176
|
+
* progress value (0↔1) that animates in/out with its own transition; the
|
|
177
|
+
* worklet composites layers in priority order (lowest-to-highest):
|
|
178
|
+
* `hovered → focused → focusVisible → pressed`. Per-property the chain is
|
|
179
|
+
*
|
|
180
|
+
* v = base
|
|
181
|
+
* v = lerp(v, hovered.value, progressHovered) // if declared
|
|
182
|
+
* v = lerp(v, focused.value, progressFocused) // if declared
|
|
183
|
+
* v = lerp(v, focusVisible.value, progressFocusVisible) // if declared
|
|
184
|
+
* v = lerp(v, pressed.value, progressPressed) // if declared
|
|
132
185
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
186
|
+
* (Color-valued keys use `interpolateColor` instead of `lerp`.) When a single
|
|
187
|
+
* sub-state is active, this collapses to "the highest-priority declared layer
|
|
188
|
+
* wins". When multiple are mid-transition (e.g. release-while-still-hovered)
|
|
189
|
+
* each layer fades independently — a press layer fading out at 50ms while a
|
|
190
|
+
* hover layer holds at full opacity matches MD3 state-layer semantics.
|
|
191
|
+
*
|
|
192
|
+
* Configure per-layer fade timing via `transition.<stateName>` on the parent
|
|
193
|
+
* primitive (see `GestureLayerTransitions`); without it, layers default to
|
|
194
|
+
* the parent transition or the library default spring.
|
|
139
195
|
*/
|
|
140
196
|
export interface GestureSubStates<C> {
|
|
141
197
|
pressed?: AnimateStyle<C>
|
|
@@ -193,10 +249,11 @@ export interface MotionProps<C> {
|
|
|
193
249
|
*/
|
|
194
250
|
controller?: VariantController
|
|
195
251
|
/**
|
|
196
|
-
* Gesture-driven sub-states (`pressed`, `focused`, `
|
|
197
|
-
* no handlers are mounted on the underlying
|
|
198
|
-
*
|
|
199
|
-
*
|
|
252
|
+
* Gesture-driven sub-states (`pressed`, `focused`, `focusVisible`,
|
|
253
|
+
* `hovered`). When omitted, no handlers are mounted on the underlying
|
|
254
|
+
* component. Each declared sub-state animates as an independent layer
|
|
255
|
+
* fading in/out over the base `animate` target — see `GestureSubStates`
|
|
256
|
+
* for the composition model and per-layer transition wiring.
|
|
200
257
|
*/
|
|
201
258
|
gesture?: GestureSubStates<C>
|
|
202
259
|
/**
|
|
@@ -204,6 +261,26 @@ export interface MotionProps<C> {
|
|
|
204
261
|
* precedence over the top-level transition.
|
|
205
262
|
*/
|
|
206
263
|
transition?: Transition<AnimateStyle<C>>
|
|
264
|
+
/**
|
|
265
|
+
* Auto-layout animation. When the component's position or size changes
|
|
266
|
+
* because of a parent layout change (a flex sibling growing, a list
|
|
267
|
+
* reordering, a column toggling its width), interpolate between the old
|
|
268
|
+
* and new layout instead of snapping.
|
|
269
|
+
*
|
|
270
|
+
* - `true` — animate with the library's default spring.
|
|
271
|
+
* - `TransitionConfig` — spring (react-spring vocab) or timing config; the
|
|
272
|
+
* resolver bridges to Reanimated's `LinearTransition` builder.
|
|
273
|
+
* - omitted / `false` — no layout animation (default).
|
|
274
|
+
*
|
|
275
|
+
* Only `'spring'` / `'timing'` / `'no-animation'` map to layout transitions
|
|
276
|
+
* — decay is downgraded to spring (no clear target). Reduced motion gates
|
|
277
|
+
* the prop the same way it gates `animate`.
|
|
278
|
+
*
|
|
279
|
+
* `layoutId` for shared element transitions across screens is deferred:
|
|
280
|
+
* Reanimated 4 dropped the underlying `sharedTransitionTag` API and a
|
|
281
|
+
* Inertia-side measure-based registry is the in-flight design.
|
|
282
|
+
*/
|
|
283
|
+
layout?: boolean | TransitionConfig
|
|
207
284
|
/**
|
|
208
285
|
* Fired once per logical animation completion. See `AnimationCallbackInfo`
|
|
209
286
|
* for the payload shape — transform parents fire once, not per axis.
|
|
@@ -216,6 +293,7 @@ export interface MotionProps<C> {
|
|
|
216
293
|
* underlying component's props (minus `style`, which we replace with an
|
|
217
294
|
* animated style) with the Motion-specific props above.
|
|
218
295
|
*/
|
|
296
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
219
297
|
export type MotionComponent<C extends ComponentType<any>> = ComponentType<
|
|
220
298
|
Omit<React.ComponentProps<C>, 'style'> &
|
|
221
299
|
MotionProps<React.ComponentProps<C>> & {
|
package/src/values/index.ts
CHANGED
|
@@ -1 +1,15 @@
|
|
|
1
|
+
export { useAnimation } from './useAnimation'
|
|
2
|
+
export {
|
|
3
|
+
useGesture,
|
|
4
|
+
type UseGestureHandlers,
|
|
5
|
+
type UseGestureResult,
|
|
6
|
+
} from './useGesture'
|
|
7
|
+
export { useMotionValue } from './useMotionValue'
|
|
8
|
+
export { useSpring } from './useSpring'
|
|
9
|
+
export {
|
|
10
|
+
useTransform,
|
|
11
|
+
type ExtrapolationMode,
|
|
12
|
+
type UseTransformOptions,
|
|
13
|
+
} from './useTransform'
|
|
14
|
+
export { useScroll, type UseScrollResult } from './useScroll'
|
|
1
15
|
export { useVariants } from './useVariants'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated'
|
|
3
|
+
import { useShouldReduceMotion } from '../config'
|
|
4
|
+
import { resolveTransition, stableSig } from '../transitions'
|
|
5
|
+
import { type TransitionConfig } from '../types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Drive a `SharedValue<number>` toward `target` with **any** transition shape
|
|
9
|
+
* — spring, timing, decay, or no-animation. The general-purpose value-layer
|
|
10
|
+
* hook: reach for it when you need raw `useSharedValue + useEffect + withX`
|
|
11
|
+
* outside the declarative `animate` flow.
|
|
12
|
+
*
|
|
13
|
+
* Re-runs whenever `target` changes shape (`target` is in the dep array) or
|
|
14
|
+
* the transition signature changes (kept stable via JSON-style hashing).
|
|
15
|
+
* Reduced motion (via `<MotionConfig reducedMotion>`) collapses the
|
|
16
|
+
* transition to `no-animation` so the value snaps instead of interpolating.
|
|
17
|
+
*
|
|
18
|
+
* **Spring shorthand.** Prefer [`useSpring`](./useSpring) when you only want
|
|
19
|
+
* spring physics — it accepts the same `tension`/`friction`/`mass` config and
|
|
20
|
+
* also supports a `SharedValue<number>` as the target (UI-thread reactive
|
|
21
|
+
* source). `useAnimation` is JS-thread-driven only.
|
|
22
|
+
*
|
|
23
|
+
* **Loops.** Repeat is part of `TransitionConfig` and flows through
|
|
24
|
+
* untouched — `useAnimation(1, { type: 'timing', duration: 1800, repeat: {
|
|
25
|
+
* count: 'infinite', alternate: false } })` produces an indeterminate-style
|
|
26
|
+
* progress driver.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* // Toggle progress (Switch / Checkbox / Radio).
|
|
31
|
+
* const progress = useAnimation(isChecked ? 1 : 0, {
|
|
32
|
+
* type: 'spring',
|
|
33
|
+
* tension: 380,
|
|
34
|
+
* friction: 33,
|
|
35
|
+
* })
|
|
36
|
+
*
|
|
37
|
+
* // Float a TextField label when the value becomes non-empty.
|
|
38
|
+
* const floated = useAnimation(hasValue ? 1 : 0, {
|
|
39
|
+
* type: 'timing',
|
|
40
|
+
* duration: 150,
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* // Indeterminate progress slider (loops forever, snaps back).
|
|
44
|
+
* const slide = useAnimation(1, {
|
|
45
|
+
* type: 'timing',
|
|
46
|
+
* duration: 1800,
|
|
47
|
+
* repeat: { count: 'infinite', alternate: false },
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function useAnimation(
|
|
52
|
+
target: number,
|
|
53
|
+
transition?: TransitionConfig,
|
|
54
|
+
): SharedValue<number> {
|
|
55
|
+
const output = useSharedValue<number>(target)
|
|
56
|
+
const shouldReduceMotion = useShouldReduceMotion()
|
|
57
|
+
const cfgSig = stableSig(transition)
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const cfg = shouldReduceMotion
|
|
61
|
+
? ({ type: 'no-animation' } as const)
|
|
62
|
+
: (transition ?? ({ type: 'spring' } as const))
|
|
63
|
+
output.value = resolveTransition(cfg, target) as never
|
|
64
|
+
// `output` is identity-stable per hook instance.
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
}, [target, cfgSig, shouldReduceMotion])
|
|
67
|
+
|
|
68
|
+
return output
|
|
69
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react'
|
|
2
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated'
|
|
3
|
+
import { useShouldReduceMotion } from '../config'
|
|
4
|
+
import { isFocusVisible } from '../gestures'
|
|
5
|
+
import { isTopLevelTransition, resolveTransition } from '../transitions'
|
|
6
|
+
import { type GestureLayerTransitions, type TransitionConfig } from '../types'
|
|
7
|
+
|
|
8
|
+
type LayerName = 'pressed' | 'focused' | 'focusVisible' | 'hovered'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handler bag returned by `useGesture`. Spread on a `Pressable` to drive the
|
|
12
|
+
* shared values returned alongside.
|
|
13
|
+
*
|
|
14
|
+
* Hover handlers use `Pressable`'s own `onHoverIn` / `onHoverOut` names (web
|
|
15
|
+
* only — no-ops on native). `onFocus` consults `isFocusVisible()` before
|
|
16
|
+
* raising the keyboard-only `focusVisible` layer; `focused` always raises.
|
|
17
|
+
*/
|
|
18
|
+
export interface UseGestureHandlers {
|
|
19
|
+
onPressIn: () => void
|
|
20
|
+
onPressOut: () => void
|
|
21
|
+
onHoverIn: () => void
|
|
22
|
+
onHoverOut: () => void
|
|
23
|
+
onFocus: () => void
|
|
24
|
+
onBlur: () => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseGestureResult {
|
|
28
|
+
/** 0↔1 progress for the pressed layer. */
|
|
29
|
+
pressed: SharedValue<number>
|
|
30
|
+
/** 0↔1 progress for the focused layer (any focus modality). */
|
|
31
|
+
focused: SharedValue<number>
|
|
32
|
+
/** 0↔1 progress for the focusVisible layer (keyboard focus only). */
|
|
33
|
+
focusVisible: SharedValue<number>
|
|
34
|
+
/** 0↔1 progress for the hovered layer (web only — stays at 0 on native). */
|
|
35
|
+
hovered: SharedValue<number>
|
|
36
|
+
/** Handlers to spread on the receiving `Pressable`. */
|
|
37
|
+
handlers: UseGestureHandlers
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a gesture-layer controller. The hook-form of the `gesture` prop —
|
|
42
|
+
* reach for it when you need to drive multiple animated views from the same
|
|
43
|
+
* gesture state (a focus ring + state-layer halo + content tint all on one
|
|
44
|
+
* Pressable), which the prop-form's "animate the receiver's own style" model
|
|
45
|
+
* can't express.
|
|
46
|
+
*
|
|
47
|
+
* Returns four 0↔1 shared values (one per layer) and a handler bag to spread
|
|
48
|
+
* on a `Pressable`. The shared values are stable across renders — feed them
|
|
49
|
+
* into any number of `useAnimatedStyle` blocks anywhere in the tree.
|
|
50
|
+
*
|
|
51
|
+
* Transitions follow the same shape as the `gesture` prop's accompanying
|
|
52
|
+
* `transition`: pass a single `TransitionConfig` to use for every layer, or a
|
|
53
|
+
* `GestureLayerTransitions` map to give each layer its own. Layers without an
|
|
54
|
+
* explicit transition fall back to the library default spring.
|
|
55
|
+
*
|
|
56
|
+
* Reduced motion (via `<MotionConfig reducedMotion>`) collapses every
|
|
57
|
+
* transition to `no-animation` so state changes snap instead of interpolating
|
|
58
|
+
* — same behaviour the gesture prop applies.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* import { useAnimatedStyle } from 'react-native-reanimated'
|
|
63
|
+
* import { useGesture } from '@onlynative/inertia'
|
|
64
|
+
*
|
|
65
|
+
* function Card() {
|
|
66
|
+
* const { pressed, focused, hovered, handlers } = useGesture({
|
|
67
|
+
* pressed: { type: 'timing', duration: 100 },
|
|
68
|
+
* hovered: { type: 'timing', duration: 150 },
|
|
69
|
+
* focused: { type: 'timing', duration: 200 },
|
|
70
|
+
* })
|
|
71
|
+
*
|
|
72
|
+
* const ringStyle = useAnimatedStyle(() => ({ opacity: focused.value }))
|
|
73
|
+
* const haloStyle = useAnimatedStyle(() => ({
|
|
74
|
+
* opacity: Math.max(
|
|
75
|
+
* hovered.value * 0.08,
|
|
76
|
+
* focused.value * 0.10,
|
|
77
|
+
* pressed.value * 0.10,
|
|
78
|
+
* ),
|
|
79
|
+
* }))
|
|
80
|
+
*
|
|
81
|
+
* return (
|
|
82
|
+
* <Pressable {...handlers}>
|
|
83
|
+
* <Animated.View style={ringStyle} />
|
|
84
|
+
* <Animated.View style={haloStyle} />
|
|
85
|
+
* </Pressable>
|
|
86
|
+
* )
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function useGesture(
|
|
91
|
+
transition?: TransitionConfig | GestureLayerTransitions,
|
|
92
|
+
): UseGestureResult {
|
|
93
|
+
const pressed = useSharedValue(0)
|
|
94
|
+
const focused = useSharedValue(0)
|
|
95
|
+
const focusVisible = useSharedValue(0)
|
|
96
|
+
const hovered = useSharedValue(0)
|
|
97
|
+
const shouldReduceMotion = useShouldReduceMotion()
|
|
98
|
+
|
|
99
|
+
const setLayer = useCallback(
|
|
100
|
+
(sv: SharedValue<number>, layer: LayerName, target: 0 | 1) => {
|
|
101
|
+
const cfg = shouldReduceMotion
|
|
102
|
+
? ({ type: 'no-animation' } as const)
|
|
103
|
+
: (layerTransition(layer, transition) ?? ({ type: 'spring' } as const))
|
|
104
|
+
sv.value = resolveTransition(cfg, target) as never
|
|
105
|
+
},
|
|
106
|
+
// The transition is intentionally read on every call rather than cooked
|
|
107
|
+
// into the dep array — a fresh literal each render would otherwise
|
|
108
|
+
// rebuild the handler bag and break composing consumers that key off
|
|
109
|
+
// handler identity. `transition` is read inside the callback closure;
|
|
110
|
+
// shared values are stable so the only dep that matters is the reduce-
|
|
111
|
+
// motion flag.
|
|
112
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
|
+
[shouldReduceMotion],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const handlers = useMemo<UseGestureHandlers>(
|
|
117
|
+
() => ({
|
|
118
|
+
onPressIn: () => setLayer(pressed, 'pressed', 1),
|
|
119
|
+
onPressOut: () => setLayer(pressed, 'pressed', 0),
|
|
120
|
+
onHoverIn: () => setLayer(hovered, 'hovered', 1),
|
|
121
|
+
onHoverOut: () => setLayer(hovered, 'hovered', 0),
|
|
122
|
+
onFocus: () => {
|
|
123
|
+
setLayer(focused, 'focused', 1)
|
|
124
|
+
if (isFocusVisible()) setLayer(focusVisible, 'focusVisible', 1)
|
|
125
|
+
},
|
|
126
|
+
onBlur: () => {
|
|
127
|
+
setLayer(focused, 'focused', 0)
|
|
128
|
+
setLayer(focusVisible, 'focusVisible', 0)
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
[setLayer, pressed, focused, focusVisible, hovered],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return { pressed, focused, focusVisible, hovered, handlers }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function layerTransition(
|
|
138
|
+
layer: LayerName,
|
|
139
|
+
transition: TransitionConfig | GestureLayerTransitions | undefined,
|
|
140
|
+
): TransitionConfig | undefined {
|
|
141
|
+
if (!transition) return undefined
|
|
142
|
+
if (isTopLevelTransition(transition)) return transition
|
|
143
|
+
return (transition as GestureLayerTransitions)[layer]
|
|
144
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create an animatable value owned by JS but readable from worklets.
|
|
5
|
+
*
|
|
6
|
+
* This is the escape-hatch primitive that the rest of the value-layer hooks
|
|
7
|
+
* (`useSpring`, `useTransform`, `useScroll`) compose against. It is a thin
|
|
8
|
+
* pass-through over Reanimated's `useSharedValue`: a `SharedValue<T>` with
|
|
9
|
+
* `.value` for direct reads/writes (UI-thread reads in worklets, JS-thread
|
|
10
|
+
* writes from event handlers / effects).
|
|
11
|
+
*
|
|
12
|
+
* We intentionally do not introduce a `MotionValue` wrapper class around the
|
|
13
|
+
* shared value. The simplest object that interops with `useAnimatedStyle`,
|
|
14
|
+
* `useDerivedValue`, and every other Reanimated API _is_ the shared value
|
|
15
|
+
* itself; adding a `{ get, set, value }` shell would force consumers to
|
|
16
|
+
* unwrap it at every Reanimated boundary and break worklet capture.
|
|
17
|
+
*
|
|
18
|
+
* Worklet read:
|
|
19
|
+
* ```ts
|
|
20
|
+
* const x = useMotionValue(0)
|
|
21
|
+
* useAnimatedStyle(() => ({ transform: [{ translateX: x.value }] }))
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* JS write:
|
|
25
|
+
* ```ts
|
|
26
|
+
* onPress={() => { x.value = 100 }}
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useMotionValue<T extends number | string>(
|
|
30
|
+
initial: T,
|
|
31
|
+
): SharedValue<T> {
|
|
32
|
+
return useSharedValue<T>(initial)
|
|
33
|
+
}
|