@onlynative/inertia 0.0.1-alpha.7 → 0.0.1-alpha.9
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 +1 -1
- 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 +114 -74
- package/dist/index.d.ts +114 -74
- package/dist/index.js +388 -1542
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +388 -1545
- 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 +244 -1462
- package/dist/motion/Image.js.map +1 -1
- package/dist/motion/Image.mjs +247 -1465
- 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 +244 -1462
- package/dist/motion/Pressable.js.map +1 -1
- package/dist/motion/Pressable.mjs +247 -1465
- 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 +244 -1462
- package/dist/motion/ScrollView.js.map +1 -1
- package/dist/motion/ScrollView.mjs +247 -1465
- 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 +244 -1462
- package/dist/motion/Text.js.map +1 -1
- package/dist/motion/Text.mjs +247 -1465
- 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 +244 -1462
- package/dist/motion/View.js.map +1 -1
- package/dist/motion/View.mjs +247 -1465
- package/dist/motion/View.mjs.map +1 -1
- 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-NmNeJjo1.d.mts → types-cU43dEmH.d.mts} +64 -17
- package/dist/{types-NmNeJjo1.d.ts → types-cU43dEmH.d.ts} +64 -17
- package/dist/useGesture-B7A_1DVg.d.ts +84 -0
- package/dist/useGesture-cimMrzC1.d.mts +84 -0
- package/jest-setup.js +4 -0
- package/llms.txt +12 -3
- package/package.json +22 -2
- package/src/__type-tests__/variants.test-d.tsx +67 -0
- package/src/gestureLayer/index.ts +21 -0
- package/src/gestureLayer/useGestureLayer.ts +285 -0
- package/src/index.ts +7 -0
- package/src/layout/index.ts +15 -0
- package/src/layout/sharedRegistry.ts +111 -0
- package/src/layout/useSharedLayout.ts +289 -0
- package/src/motion/createMotionComponent.tsx +123 -37
- package/src/motion/installCheck.ts +7 -11
- package/src/touch/index.ts +18 -0
- package/src/touch/useTouchDrag.ts +289 -0
- package/src/types.ts +79 -20
- package/src/values/index.ts +11 -0
- package/src/values/useBooleanSpring.ts +33 -0
- package/src/values/useColorTransition.ts +72 -0
- package/src/values/useShadow.ts +116 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
PanResponder,
|
|
4
|
+
type PanResponderGestureState,
|
|
5
|
+
type PanResponderInstance,
|
|
6
|
+
} from 'react-native'
|
|
7
|
+
import {
|
|
8
|
+
useAnimatedStyle,
|
|
9
|
+
useSharedValue,
|
|
10
|
+
type SharedValue,
|
|
11
|
+
} from 'react-native-reanimated'
|
|
12
|
+
import { buildReleaseAnimation } from '../transitions'
|
|
13
|
+
import type { TransitionConfig } from '../types'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Same drag-result shape as `useDrag` from `@onlynative/inertia-gestures`,
|
|
17
|
+
* minus the `gesture` field (PanResponder spreads handlers, no
|
|
18
|
+
* `<GestureDetector>` wrapper). The shared values + animatedStyle are
|
|
19
|
+
* interchangeable across both hooks; consumers can swap implementations
|
|
20
|
+
* without touching their `useAnimatedStyle` consumers.
|
|
21
|
+
*/
|
|
22
|
+
export interface UseTouchDragResult {
|
|
23
|
+
/** Spread onto a `View` / `Pressable` to install the pan responder. */
|
|
24
|
+
panHandlers: PanResponderInstance['panHandlers']
|
|
25
|
+
/** Stable animated `transform` style. */
|
|
26
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>
|
|
27
|
+
/** Live x translation, persistent across gestures. */
|
|
28
|
+
dragX: SharedValue<number>
|
|
29
|
+
/** Live y translation, persistent across gestures. */
|
|
30
|
+
dragY: SharedValue<number>
|
|
31
|
+
/** True while the gesture is active. */
|
|
32
|
+
isDragging: SharedValue<boolean>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Release transition shape for PanResponder's JS-thread `onRelease`. Mirrors
|
|
37
|
+
* the gesture-handler adapter's `ReleaseTransition` but with `to` typed as
|
|
38
|
+
* required for spring/timing/no-animation (decay omits it).
|
|
39
|
+
*/
|
|
40
|
+
export type TouchReleaseTransition =
|
|
41
|
+
| (TransitionConfig & { type: 'spring'; to: number })
|
|
42
|
+
| (TransitionConfig & { type: 'timing'; to: number })
|
|
43
|
+
| (TransitionConfig & { type: 'decay' })
|
|
44
|
+
| (TransitionConfig & { type: 'no-animation'; to: number })
|
|
45
|
+
|
|
46
|
+
export interface TouchReleaseInfo {
|
|
47
|
+
x: number
|
|
48
|
+
y: number
|
|
49
|
+
velocity: { x: number; y: number }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TouchReleaseResult {
|
|
53
|
+
x?: TouchReleaseTransition
|
|
54
|
+
y?: TouchReleaseTransition
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface UseTouchDragOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Restrict the drag to one axis. Defaults to `'both'`. When `'x'` is set
|
|
60
|
+
* the y-axis shared value never updates (and vice versa); velocity is
|
|
61
|
+
* still reported on both for `onDragEnd`.
|
|
62
|
+
*/
|
|
63
|
+
axis?: 'x' | 'y' | 'both'
|
|
64
|
+
/**
|
|
65
|
+
* Travel bounds (px from resting). Each side is independently optional.
|
|
66
|
+
* Out-of-bounds values clamp to the limit unless `elastic > 0`.
|
|
67
|
+
*/
|
|
68
|
+
constraints?: {
|
|
69
|
+
left?: number
|
|
70
|
+
right?: number
|
|
71
|
+
top?: number
|
|
72
|
+
bottom?: number
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Rubber-band coefficient applied to overshoot past `constraints`. `0`
|
|
76
|
+
* (default) hard-clamps; `0.2`-`0.4` is a typical Framer-Motion feel.
|
|
77
|
+
*/
|
|
78
|
+
elastic?: number
|
|
79
|
+
/**
|
|
80
|
+
* Fires when the user starts dragging. JS thread.
|
|
81
|
+
*/
|
|
82
|
+
onDragStart?: () => void
|
|
83
|
+
/**
|
|
84
|
+
* Fires when the user releases or the gesture terminates. JS thread.
|
|
85
|
+
*
|
|
86
|
+
* Velocity is in px/sec to match the `@onlynative/inertia-gestures` API
|
|
87
|
+
* (PanResponder's native `vx` / `vy` are px/ms; the hook normalizes).
|
|
88
|
+
*/
|
|
89
|
+
onDragEnd?: (info: TouchReleaseInfo) => void
|
|
90
|
+
/**
|
|
91
|
+
* Optional release-animation callback. Return per-axis release transitions
|
|
92
|
+
* to animate the SVs to a settled position via Inertia's transition
|
|
93
|
+
* resolver — spring snap-to-tick, decay with bounds, timing settle.
|
|
94
|
+
*
|
|
95
|
+
* Unlike the gesture-handler version, this callback runs on the **JS
|
|
96
|
+
* thread** (PanResponder is JS-only). The returned transitions still drive
|
|
97
|
+
* UI-thread animations via Reanimated — only the decision logic is JS-side.
|
|
98
|
+
*/
|
|
99
|
+
onRelease?: (info: TouchReleaseInfo) => TouchReleaseResult | void
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* PanResponder-backed drag hook. Pointer-equivalent of `useDrag` from
|
|
104
|
+
* `@onlynative/inertia-gestures`, with two differences:
|
|
105
|
+
*
|
|
106
|
+
* 1. No `react-native-gesture-handler` peer dep required — PanResponder is
|
|
107
|
+
* built into React Native, so this lives in core.
|
|
108
|
+
* 2. Returns `panHandlers` to spread on a `View` / `Pressable` instead of
|
|
109
|
+
* a `gesture` to plug into `<GestureDetector>`.
|
|
110
|
+
*
|
|
111
|
+
* Use this when:
|
|
112
|
+
* - You need keyboard a11y alongside drag (a slider with arrow-key step,
|
|
113
|
+
* a scrollbar with `PageUp` / `PageDown`). PanResponder composes
|
|
114
|
+
* cleanly with `onKeyDown`; gesture-handler doesn't surface keyboard.
|
|
115
|
+
* - You don't want to take `react-native-gesture-handler` as a dependency
|
|
116
|
+
* (smaller bundle, simpler install).
|
|
117
|
+
*
|
|
118
|
+
* Skip this when:
|
|
119
|
+
* - You're already using `react-native-gesture-handler` elsewhere (use
|
|
120
|
+
* `useDrag` from `@onlynative/inertia-gestures` for consistency and
|
|
121
|
+
* better worklet-thread fidelity on release velocity).
|
|
122
|
+
* - You need momentum semantics like the gesture-handler `usePan` —
|
|
123
|
+
* PanResponder's release velocity is JS-thread and slightly less precise.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```tsx
|
|
127
|
+
* import { useTouchDrag } from '@onlynative/inertia/touch'
|
|
128
|
+
*
|
|
129
|
+
* function Slider({ ticks }: { ticks: number[] }) {
|
|
130
|
+
* const drag = useTouchDrag({
|
|
131
|
+
* axis: 'x',
|
|
132
|
+
* constraints: { left: 0, right: 280 },
|
|
133
|
+
* onRelease: (e) => {
|
|
134
|
+
* const snap = nearestTick(e.x, ticks)
|
|
135
|
+
* return { x: { type: 'spring', to: snap, velocity: e.velocity.x } }
|
|
136
|
+
* },
|
|
137
|
+
* })
|
|
138
|
+
*
|
|
139
|
+
* return (
|
|
140
|
+
* <Motion.View
|
|
141
|
+
* style={[styles.thumb, drag.animatedStyle]}
|
|
142
|
+
* {...drag.panHandlers}
|
|
143
|
+
* />
|
|
144
|
+
* )
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export function useTouchDrag(
|
|
149
|
+
options: UseTouchDragOptions = {},
|
|
150
|
+
): UseTouchDragResult {
|
|
151
|
+
const { axis = 'both', constraints, elastic = 0 } = options
|
|
152
|
+
|
|
153
|
+
const dragX = useSharedValue(0)
|
|
154
|
+
const dragY = useSharedValue(0)
|
|
155
|
+
const startX = useSharedValue(0)
|
|
156
|
+
const startY = useSharedValue(0)
|
|
157
|
+
const isDragging = useSharedValue(false)
|
|
158
|
+
|
|
159
|
+
// Snapshot scalars into local consts so the responder callbacks close over
|
|
160
|
+
// primitives, not the `options` literal — a fresh `options` each render
|
|
161
|
+
// would otherwise force the PanResponder identity to change.
|
|
162
|
+
const lockX = axis !== 'y'
|
|
163
|
+
const lockY = axis !== 'x'
|
|
164
|
+
const left = constraints?.left
|
|
165
|
+
const right = constraints?.right
|
|
166
|
+
const top = constraints?.top
|
|
167
|
+
const bottom = constraints?.bottom
|
|
168
|
+
const elasticCoef = elastic
|
|
169
|
+
const { onDragStart, onDragEnd, onRelease } = options
|
|
170
|
+
|
|
171
|
+
const responder = useMemo(
|
|
172
|
+
() => buildResponder(),
|
|
173
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
174
|
+
[
|
|
175
|
+
lockX,
|
|
176
|
+
lockY,
|
|
177
|
+
left,
|
|
178
|
+
right,
|
|
179
|
+
top,
|
|
180
|
+
bottom,
|
|
181
|
+
elasticCoef,
|
|
182
|
+
onDragStart,
|
|
183
|
+
onDragEnd,
|
|
184
|
+
onRelease,
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// Hoisted out of the inline `useMemo` factory to keep the dep list readable
|
|
189
|
+
// and avoid re-declaring closure helpers each render.
|
|
190
|
+
function buildResponder(): PanResponderInstance {
|
|
191
|
+
const handleEnd = (g: PanResponderGestureState) => {
|
|
192
|
+
isDragging.value = false
|
|
193
|
+
const x = dragX.value
|
|
194
|
+
const y = dragY.value
|
|
195
|
+
// PanResponder velocity is px/ms; multiply to match the
|
|
196
|
+
// `@onlynative/inertia-gestures` API (px/sec from gesture-handler).
|
|
197
|
+
const vx = g.vx * 1000
|
|
198
|
+
const vy = g.vy * 1000
|
|
199
|
+
if (onRelease) {
|
|
200
|
+
const result = onRelease({ x, y, velocity: { x: vx, y: vy } })
|
|
201
|
+
if (result) {
|
|
202
|
+
if (result.x && lockX) {
|
|
203
|
+
const toX = 'to' in result.x ? result.x.to : x
|
|
204
|
+
dragX.value = buildReleaseAnimation(
|
|
205
|
+
result.x,
|
|
206
|
+
toX,
|
|
207
|
+
) as unknown as number
|
|
208
|
+
}
|
|
209
|
+
if (result.y && lockY) {
|
|
210
|
+
const toY = 'to' in result.y ? result.y.to : y
|
|
211
|
+
dragY.value = buildReleaseAnimation(
|
|
212
|
+
result.y,
|
|
213
|
+
toY,
|
|
214
|
+
) as unknown as number
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (onDragEnd) onDragEnd({ x, y, velocity: { x: vx, y: vy } })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return PanResponder.create({
|
|
222
|
+
// Always claim the start so taps that turn into drags don't slip
|
|
223
|
+
// through to a parent ScrollView. Consumers can compose their own
|
|
224
|
+
// capture predicates by wrapping the returned `panHandlers`.
|
|
225
|
+
onStartShouldSetPanResponder: () => true,
|
|
226
|
+
onMoveShouldSetPanResponder: () => true,
|
|
227
|
+
onPanResponderGrant: () => {
|
|
228
|
+
startX.value = dragX.value
|
|
229
|
+
startY.value = dragY.value
|
|
230
|
+
isDragging.value = true
|
|
231
|
+
if (onDragStart) onDragStart()
|
|
232
|
+
},
|
|
233
|
+
onPanResponderMove: (_e, g) => {
|
|
234
|
+
if (lockX) {
|
|
235
|
+
dragX.value = applyBounds(
|
|
236
|
+
startX.value + g.dx,
|
|
237
|
+
left,
|
|
238
|
+
right,
|
|
239
|
+
elasticCoef,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
if (lockY) {
|
|
243
|
+
dragY.value = applyBounds(
|
|
244
|
+
startY.value + g.dy,
|
|
245
|
+
top,
|
|
246
|
+
bottom,
|
|
247
|
+
elasticCoef,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
onPanResponderRelease: (_e, g) => handleEnd(g),
|
|
252
|
+
onPanResponderTerminate: (_e, g) => handleEnd(g),
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
257
|
+
transform: [{ translateX: dragX.value }, { translateY: dragY.value }],
|
|
258
|
+
}))
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
panHandlers: responder.panHandlers,
|
|
262
|
+
animatedStyle,
|
|
263
|
+
dragX,
|
|
264
|
+
dragY,
|
|
265
|
+
isDragging,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clamp `value` to `[min, max]`. When `elastic > 0` the overshoot past a
|
|
271
|
+
* bound is scaled by `elastic`, giving a rubber-band feel. `min` / `max`
|
|
272
|
+
* may be `undefined` to leave that side unbounded.
|
|
273
|
+
*
|
|
274
|
+
* JS-thread (PanResponder callbacks are JS, not worklets).
|
|
275
|
+
*/
|
|
276
|
+
function applyBounds(
|
|
277
|
+
value: number,
|
|
278
|
+
min: number | undefined,
|
|
279
|
+
max: number | undefined,
|
|
280
|
+
elastic: number,
|
|
281
|
+
): number {
|
|
282
|
+
if (min !== undefined && value < min) {
|
|
283
|
+
return elastic > 0 ? min + (value - min) * elastic : min
|
|
284
|
+
}
|
|
285
|
+
if (max !== undefined && value > max) {
|
|
286
|
+
return elastic > 0 ? max + (value - max) * elastic : max
|
|
287
|
+
}
|
|
288
|
+
return value
|
|
289
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ComponentProps,
|
|
3
|
+
type ComponentType,
|
|
4
|
+
type ReactElement,
|
|
5
|
+
type Ref,
|
|
6
|
+
} from 'react'
|
|
2
7
|
import { type StyleProp } from 'react-native'
|
|
3
8
|
|
|
4
9
|
/**
|
|
@@ -239,8 +244,15 @@ export interface VariantController<K extends string = string> {
|
|
|
239
244
|
|
|
240
245
|
/**
|
|
241
246
|
* Props injected onto every Motion primitive.
|
|
247
|
+
*
|
|
248
|
+
* The second type parameter `V` is the concrete `variants` map. It is inferred
|
|
249
|
+
* from the `variants` prop at each JSX use (see `MotionComponent`), which is
|
|
250
|
+
* what lets `animate` narrow to the variant key union and reject typos. When
|
|
251
|
+
* no `variants` prop is passed, `V` falls back to `VariantsMap<C>` — whose key
|
|
252
|
+
* type is the open `string`, so `animate` still accepts any string and nothing
|
|
253
|
+
* regresses for the variant-less case.
|
|
242
254
|
*/
|
|
243
|
-
export interface MotionProps<C> {
|
|
255
|
+
export interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
|
|
244
256
|
/**
|
|
245
257
|
* Initial values applied on mount. Read once on mount and intentionally
|
|
246
258
|
* non-reactive — to reset after a state change, change the component `key`,
|
|
@@ -251,10 +263,11 @@ export interface MotionProps<C> {
|
|
|
251
263
|
initial?: AnimateStyle<C> | false
|
|
252
264
|
/**
|
|
253
265
|
* The animation target. A style object, a variant key (when `variants` is
|
|
254
|
-
* supplied), or an array of sequence steps.
|
|
255
|
-
*
|
|
266
|
+
* supplied), or an array of sequence steps. When `variants` is set, the
|
|
267
|
+
* string form is narrowed to the map's keys, so a key typo is a compile
|
|
268
|
+
* error and the keys autocomplete — no `as const` required.
|
|
256
269
|
*/
|
|
257
|
-
animate?: AnimateStyle<C> | string
|
|
270
|
+
animate?: AnimateStyle<C> | (keyof V & string)
|
|
258
271
|
/**
|
|
259
272
|
* Values applied while the component exits via `<Presence>`.
|
|
260
273
|
*/
|
|
@@ -263,13 +276,13 @@ export interface MotionProps<C> {
|
|
|
263
276
|
* Named animation states. With `variants` set, `animate` accepts a key from
|
|
264
277
|
* this map.
|
|
265
278
|
*/
|
|
266
|
-
variants?:
|
|
279
|
+
variants?: V
|
|
267
280
|
/**
|
|
268
281
|
* Imperative controller from `useVariants(...)`. When supplied, `animate`
|
|
269
282
|
* is read from `controller.current` and re-applied whenever the controller
|
|
270
283
|
* transitions. `animate` and `controller` should not both be set.
|
|
271
284
|
*/
|
|
272
|
-
controller?: VariantController
|
|
285
|
+
controller?: VariantController<keyof V & string>
|
|
273
286
|
/**
|
|
274
287
|
* Gesture-driven sub-states (`pressed`, `focused`, `focusVisible`,
|
|
275
288
|
* `hovered`). When omitted, no handlers are mounted on the underlying
|
|
@@ -298,11 +311,34 @@ export interface MotionProps<C> {
|
|
|
298
311
|
* — decay is downgraded to spring (no clear target). Reduced motion gates
|
|
299
312
|
* the prop the same way it gates `animate`.
|
|
300
313
|
*
|
|
301
|
-
* `layoutId`
|
|
302
|
-
*
|
|
303
|
-
*
|
|
314
|
+
* `layoutId` (below) is a related but distinct mechanism for shared
|
|
315
|
+
* element transitions across screens — `layout` animates this element's
|
|
316
|
+
* own layout changes, `layoutId` animates from a different element's
|
|
317
|
+
* last measured rect to this element's current rect.
|
|
304
318
|
*/
|
|
305
319
|
layout?: boolean | TransitionConfig
|
|
320
|
+
/**
|
|
321
|
+
* Shared-element transition id. When a Motion primitive with `layoutId`
|
|
322
|
+
* unmounts, its last on-screen rect is recorded under that id; the next
|
|
323
|
+
* mount of any Motion primitive with the same id animates from the
|
|
324
|
+
* recorded rect to its natural position via a FLIP transform stack.
|
|
325
|
+
*
|
|
326
|
+
* Reanimated 4 removed the `sharedTransitionTag` API — `layoutId` is the
|
|
327
|
+
* Inertia-side measure-based replacement. Rects are recorded in
|
|
328
|
+
* parent-relative coordinates (from `onLayout`), which composes when the
|
|
329
|
+
* source and target screens share an outer content container (the common
|
|
330
|
+
* stack-navigator case); nested-parent layouts need the v2
|
|
331
|
+
* window-coordinate path.
|
|
332
|
+
*
|
|
333
|
+
* The same `transition` prop drives the FLIP animation (spring by
|
|
334
|
+
* default; `'timing'` honored; `'decay'` downgrades to spring; reduced
|
|
335
|
+
* motion skips the transition). Out of scope for the first iteration:
|
|
336
|
+
* style-prop interpolation (border radius, colors, etc.) — only the
|
|
337
|
+
* rect-to-rect transform is animated. Two simultaneously-mounted
|
|
338
|
+
* primitives sharing the same `layoutId` are undefined behavior; pick a
|
|
339
|
+
* primitive per id at a time.
|
|
340
|
+
*/
|
|
341
|
+
layoutId?: string
|
|
306
342
|
/**
|
|
307
343
|
* Fired once per logical animation completion. See `AnimationCallbackInfo`
|
|
308
344
|
* for the payload shape — transform parents fire once, not per axis.
|
|
@@ -311,14 +347,37 @@ export interface MotionProps<C> {
|
|
|
311
347
|
}
|
|
312
348
|
|
|
313
349
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* animated style) with the Motion
|
|
350
|
+
* Props of a Motion primitive for a given underlying component `C` and a
|
|
351
|
+
* concrete variants map `V`: the component's own props (minus `style`, which
|
|
352
|
+
* we replace with an animated style) intersected with the Motion props.
|
|
317
353
|
*/
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
>
|
|
354
|
+
export type MotionComponentProps<
|
|
355
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
356
|
+
C extends ComponentType<any>,
|
|
357
|
+
V extends VariantsMap<ComponentProps<C>> = VariantsMap<ComponentProps<C>>,
|
|
358
|
+
> = Omit<ComponentProps<C>, 'style'> &
|
|
359
|
+
MotionProps<ComponentProps<C>, V> & {
|
|
360
|
+
style?: ComponentProps<C>['style']
|
|
361
|
+
ref?: Ref<unknown>
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The component type produced by `createMotionComponent`.
|
|
366
|
+
*
|
|
367
|
+
* It is a **generic call signature**, not a plain `ComponentType`: the variant
|
|
368
|
+
* map `V` is inferred from the `variants` prop at each JSX use. That inference
|
|
369
|
+
* is what narrows `animate`'s string form to the variant keys, so
|
|
370
|
+
* `<Motion.View variants={{ open, closed }} animate="opne" />` is a compile
|
|
371
|
+
* error and `open` / `closed` autocomplete. With no `variants` prop, `V` falls
|
|
372
|
+
* back to the open `VariantsMap`, so `animate` still accepts any string and the
|
|
373
|
+
* variant-less call site is unchanged.
|
|
374
|
+
*/
|
|
375
|
+
export interface MotionComponent<
|
|
376
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
377
|
+
C extends ComponentType<any>,
|
|
378
|
+
> {
|
|
379
|
+
<V extends VariantsMap<ComponentProps<C>> = VariantsMap<ComponentProps<C>>>(
|
|
380
|
+
props: MotionComponentProps<C, V>,
|
|
381
|
+
): ReactElement | null
|
|
382
|
+
displayName?: string
|
|
383
|
+
}
|
package/src/values/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export { useAnimation } from './useAnimation'
|
|
2
|
+
export { useBooleanSpring } from './useBooleanSpring'
|
|
3
|
+
export {
|
|
4
|
+
useColorTransition,
|
|
5
|
+
type ColorStyleKey,
|
|
6
|
+
type UseColorTransitionOptions,
|
|
7
|
+
} from './useColorTransition'
|
|
2
8
|
export {
|
|
3
9
|
useGesture,
|
|
4
10
|
type UseGestureHandlers,
|
|
@@ -12,4 +18,9 @@ export {
|
|
|
12
18
|
type UseTransformOptions,
|
|
13
19
|
} from './useTransform'
|
|
14
20
|
export { useScroll, type UseScrollResult } from './useScroll'
|
|
21
|
+
export {
|
|
22
|
+
useShadow,
|
|
23
|
+
type ShadowConfig,
|
|
24
|
+
type UseShadowOptions,
|
|
25
|
+
} from './useShadow'
|
|
15
26
|
export { useVariants } from './useVariants'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type SharedValue } from 'react-native-reanimated'
|
|
2
|
+
import { useSpring } from './useSpring'
|
|
3
|
+
import { type SpringTransition } from '../types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Toggle a 0↔1 progress value with a spring whenever `active` flips.
|
|
7
|
+
*
|
|
8
|
+
* This is the recurring shape behind checkbox checks, accordion expansions,
|
|
9
|
+
* drawer open/closed states, focus rings, and every other binary UI flip
|
|
10
|
+
* that wants spring physics rather than a hard cut. The returned shared
|
|
11
|
+
* value sits at `0` when `active` is `false` and animates toward `1` when
|
|
12
|
+
* `active` flips to `true` (and back again on the reverse flip). Feed it to
|
|
13
|
+
* a `useTransform`, `useShadow`, or a hand-rolled `useAnimatedStyle` to
|
|
14
|
+
* drive whatever the boolean controls visually.
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* const progress = useBooleanSpring(isChecked)
|
|
18
|
+
* const indicatorStyle = useAnimatedStyle(() => ({
|
|
19
|
+
* opacity: progress.value,
|
|
20
|
+
* transform: [{ scale: progress.value }],
|
|
21
|
+
* }))
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The spring config follows the same react-spring vocabulary as the rest of
|
|
25
|
+
* the library (`tension` / `friction` / `mass`); omit it to take the
|
|
26
|
+
* library's defaults.
|
|
27
|
+
*/
|
|
28
|
+
export function useBooleanSpring(
|
|
29
|
+
active: boolean,
|
|
30
|
+
springConfig?: SpringTransition,
|
|
31
|
+
): SharedValue<number> {
|
|
32
|
+
return useSpring(active ? 1 : 0, springConfig)
|
|
33
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
interpolateColor,
|
|
3
|
+
useAnimatedStyle,
|
|
4
|
+
type SharedValue,
|
|
5
|
+
} from 'react-native-reanimated'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Color style keys understood by React Native that this hook can target.
|
|
9
|
+
* Sticks to the keys that exist on the v0.1 animatable surface so the
|
|
10
|
+
* returned style fragment is always a legal RN style.
|
|
11
|
+
*/
|
|
12
|
+
export type ColorStyleKey =
|
|
13
|
+
| 'backgroundColor'
|
|
14
|
+
| 'color'
|
|
15
|
+
| 'borderColor'
|
|
16
|
+
| 'borderTopColor'
|
|
17
|
+
| 'borderRightColor'
|
|
18
|
+
| 'borderBottomColor'
|
|
19
|
+
| 'borderLeftColor'
|
|
20
|
+
| 'tintColor'
|
|
21
|
+
| 'shadowColor'
|
|
22
|
+
|
|
23
|
+
export interface UseColorTransitionOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Which style slot the interpolated color is emitted under. Defaults to
|
|
26
|
+
* `backgroundColor` — the dominant case for state-layer haloes, card
|
|
27
|
+
* fills, and chip surfaces. Override for ring colors (`borderColor`),
|
|
28
|
+
* text colors (`color`), image tints (`tintColor`), etc.
|
|
29
|
+
*/
|
|
30
|
+
key?: ColorStyleKey
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Interpolate a single color channel between `from` and `to` as `progress`
|
|
35
|
+
* moves 0→1, returning an animated style fragment that can be spread onto
|
|
36
|
+
* any Reanimated-aware view.
|
|
37
|
+
*
|
|
38
|
+
* ```tsx
|
|
39
|
+
* const progress = useBooleanSpring(isPressed)
|
|
40
|
+
* const fillStyle = useColorTransition(progress, [colors.surface, colors.pressed])
|
|
41
|
+
* const ringStyle = useColorTransition(progress, [colors.outline, colors.primary], {
|
|
42
|
+
* key: 'borderColor',
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* return <Motion.View style={[styles.chip, fillStyle, ringStyle]} />
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* This is a pure interpolator: it does not animate on its own. Drive
|
|
49
|
+
* `progress` upstream with a `useSpring`, `useBooleanSpring`, gesture
|
|
50
|
+
* progress, or scroll-derived `useTransform`. Values outside `[0, 1]`
|
|
51
|
+
* clamp. For a raw `SharedValue<string>` (e.g. to feed a gradient or
|
|
52
|
+
* compose into a hand-rolled `useAnimatedStyle`), use `useTransform`
|
|
53
|
+
* directly with a color output range.
|
|
54
|
+
*/
|
|
55
|
+
export function useColorTransition(
|
|
56
|
+
progress: SharedValue<number>,
|
|
57
|
+
range: readonly [string, string],
|
|
58
|
+
options?: UseColorTransitionOptions,
|
|
59
|
+
): ReturnType<typeof useAnimatedStyle> {
|
|
60
|
+
// Resolve the slot key once on the JS thread so the worklet body
|
|
61
|
+
// consumes a single string literal — consistent with the JS-thread
|
|
62
|
+
// resolver principle that keeps `Object.keys`-style walks off the UI
|
|
63
|
+
// thread (see CLAUDE.md design principle 8).
|
|
64
|
+
const key = options?.key ?? 'backgroundColor'
|
|
65
|
+
const from = range[0]
|
|
66
|
+
const to = range[1]
|
|
67
|
+
|
|
68
|
+
return useAnimatedStyle(() => {
|
|
69
|
+
'worklet'
|
|
70
|
+
return { [key]: interpolateColor(progress.value, [0, 1], [from, to]) }
|
|
71
|
+
})
|
|
72
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
interpolate,
|
|
3
|
+
interpolateColor,
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
type SharedValue,
|
|
6
|
+
} from 'react-native-reanimated'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shape accepted on either end of a `useShadow` tween. Every field is
|
|
10
|
+
* optional — only keys present on at least one side participate in the
|
|
11
|
+
* output style. Mirrors the flat shadow keys on `Motion.View`'s `animate`
|
|
12
|
+
* surface, plus the nested `shadowOffset` source.
|
|
13
|
+
*/
|
|
14
|
+
export interface ShadowConfig {
|
|
15
|
+
shadowOpacity?: number
|
|
16
|
+
shadowRadius?: number
|
|
17
|
+
shadowOffset?: { width?: number; height?: number }
|
|
18
|
+
/** Android elevation. iOS shadow consumers can leave this off. */
|
|
19
|
+
elevation?: number
|
|
20
|
+
shadowColor?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseShadowOptions {
|
|
24
|
+
/** Shadow state at `progress === 0`. */
|
|
25
|
+
from: ShadowConfig
|
|
26
|
+
/** Shadow state at `progress === 1`. */
|
|
27
|
+
to: ShadowConfig
|
|
28
|
+
/**
|
|
29
|
+
* Driver — typically 0→1. Whatever produces it (a `useSpring`, a gesture
|
|
30
|
+
* progress value, a scroll-derived `useTransform`) is the caller's
|
|
31
|
+
* concern. The hook is a pure interpolator; it does not animate on its
|
|
32
|
+
* own. Values outside `[0, 1]` clamp.
|
|
33
|
+
*/
|
|
34
|
+
progress: SharedValue<number>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Interpolate between two shadow configs as `progress` moves 0→1, returning
|
|
39
|
+
* an animated style fragment that can be spread onto any Reanimated-aware
|
|
40
|
+
* view (including `Motion.*` primitives and a hand-rolled `Animated.View`).
|
|
41
|
+
*
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const progress = useSpring(isElevated ? 1 : 0)
|
|
44
|
+
* const shadowStyle = useShadow({
|
|
45
|
+
* from: { shadowOpacity: 0.08, shadowRadius: 2, shadowOffset: { width: 0, height: 1 }, elevation: 1 },
|
|
46
|
+
* to: { shadowOpacity: 0.24, shadowRadius: 12, shadowOffset: { width: 0, height: 8 }, elevation: 8 },
|
|
47
|
+
* progress,
|
|
48
|
+
* })
|
|
49
|
+
*
|
|
50
|
+
* return <Motion.View style={[styles.card, shadowStyle]} />
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* Only keys present on either `from` or `to` are emitted. A key present on
|
|
54
|
+
* one side and absent on the other tweens from the present value to the
|
|
55
|
+
* absent side's natural zero (`0` for numbers, `'transparent'` for
|
|
56
|
+
* `shadowColor`, `{ width: 0, height: 0 }` for `shadowOffset`). This is a
|
|
57
|
+
* pure interpolator — to "animate" the shadow, drive `progress` with a
|
|
58
|
+
* spring, timing, or gesture upstream.
|
|
59
|
+
*/
|
|
60
|
+
export function useShadow({
|
|
61
|
+
from,
|
|
62
|
+
to,
|
|
63
|
+
progress,
|
|
64
|
+
}: UseShadowOptions): ReturnType<typeof useAnimatedStyle> {
|
|
65
|
+
// Resolve presence + endpoints once on the JS thread so the worklet body
|
|
66
|
+
// consumes flat literals — consistent with the JS-thread resolver
|
|
67
|
+
// principle that keeps `Object.keys`-style walks off the UI thread.
|
|
68
|
+
const hasOpacity =
|
|
69
|
+
from.shadowOpacity !== undefined || to.shadowOpacity !== undefined
|
|
70
|
+
const hasRadius =
|
|
71
|
+
from.shadowRadius !== undefined || to.shadowRadius !== undefined
|
|
72
|
+
const hasElevation =
|
|
73
|
+
from.elevation !== undefined || to.elevation !== undefined
|
|
74
|
+
const hasColor =
|
|
75
|
+
from.shadowColor !== undefined || to.shadowColor !== undefined
|
|
76
|
+
const hasOffset =
|
|
77
|
+
from.shadowOffset !== undefined || to.shadowOffset !== undefined
|
|
78
|
+
|
|
79
|
+
const opacityFrom = from.shadowOpacity ?? 0
|
|
80
|
+
const opacityTo = to.shadowOpacity ?? 0
|
|
81
|
+
const radiusFrom = from.shadowRadius ?? 0
|
|
82
|
+
const radiusTo = to.shadowRadius ?? 0
|
|
83
|
+
const elevationFrom = from.elevation ?? 0
|
|
84
|
+
const elevationTo = to.elevation ?? 0
|
|
85
|
+
const colorFrom = from.shadowColor ?? 'transparent'
|
|
86
|
+
const colorTo = to.shadowColor ?? 'transparent'
|
|
87
|
+
const offsetWFrom = from.shadowOffset?.width ?? 0
|
|
88
|
+
const offsetWTo = to.shadowOffset?.width ?? 0
|
|
89
|
+
const offsetHFrom = from.shadowOffset?.height ?? 0
|
|
90
|
+
const offsetHTo = to.shadowOffset?.height ?? 0
|
|
91
|
+
|
|
92
|
+
return useAnimatedStyle(() => {
|
|
93
|
+
'worklet'
|
|
94
|
+
const t = progress.value
|
|
95
|
+
const out: Record<string, unknown> = {}
|
|
96
|
+
if (hasOpacity) {
|
|
97
|
+
out.shadowOpacity = interpolate(t, [0, 1], [opacityFrom, opacityTo])
|
|
98
|
+
}
|
|
99
|
+
if (hasRadius) {
|
|
100
|
+
out.shadowRadius = interpolate(t, [0, 1], [radiusFrom, radiusTo])
|
|
101
|
+
}
|
|
102
|
+
if (hasElevation) {
|
|
103
|
+
out.elevation = interpolate(t, [0, 1], [elevationFrom, elevationTo])
|
|
104
|
+
}
|
|
105
|
+
if (hasColor) {
|
|
106
|
+
out.shadowColor = interpolateColor(t, [0, 1], [colorFrom, colorTo])
|
|
107
|
+
}
|
|
108
|
+
if (hasOffset) {
|
|
109
|
+
out.shadowOffset = {
|
|
110
|
+
width: interpolate(t, [0, 1], [offsetWFrom, offsetWTo]),
|
|
111
|
+
height: interpolate(t, [0, 1], [offsetHFrom, offsetHTo]),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out
|
|
115
|
+
})
|
|
116
|
+
}
|