@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
|
@@ -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 '@rootnative/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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useAnimatedScrollHandler,
|
|
3
|
+
useSharedValue,
|
|
4
|
+
type SharedValue,
|
|
5
|
+
} from 'react-native-reanimated'
|
|
6
|
+
import { type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native'
|
|
7
|
+
|
|
8
|
+
export interface UseScrollResult {
|
|
9
|
+
/** Horizontal scroll offset in points. */
|
|
10
|
+
scrollX: SharedValue<number>
|
|
11
|
+
/** Vertical scroll offset in points. */
|
|
12
|
+
scrollY: SharedValue<number>
|
|
13
|
+
/**
|
|
14
|
+
* Handler to pass to a `Motion.ScrollView`'s `onScroll` prop (or any other
|
|
15
|
+
* Reanimated `Animated.ScrollView`). The handler is opaque to JS — it runs
|
|
16
|
+
* as a worklet — but the type narrows to the same shape RN's native
|
|
17
|
+
* `onScroll` prop expects so it composes cleanly.
|
|
18
|
+
*/
|
|
19
|
+
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Track the scroll offset of a `Motion.ScrollView` as shared values.
|
|
24
|
+
*
|
|
25
|
+
* ```tsx
|
|
26
|
+
* const { scrollY, onScroll } = useScroll()
|
|
27
|
+
* const headerOpacity = useTransform(scrollY, [0, 100], [1, 0])
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <>
|
|
31
|
+
* <Motion.View animate={{ opacity: headerOpacity }} />
|
|
32
|
+
* <Motion.ScrollView onScroll={onScroll} scrollEventThrottle={16}>
|
|
33
|
+
* …
|
|
34
|
+
* </Motion.ScrollView>
|
|
35
|
+
* </>
|
|
36
|
+
* )
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Scroll events fire on the UI thread, so `scrollX` / `scrollY` are safe to
|
|
40
|
+
* read from any worklet (`useAnimatedStyle`, `useDerivedValue`,
|
|
41
|
+
* `useTransform`) without a JS-thread bounce.
|
|
42
|
+
*
|
|
43
|
+
* Remember to set `scrollEventThrottle={16}` on the `ScrollView` for 60Hz
|
|
44
|
+
* updates — RN's default is to dispatch on every event, which on iOS still
|
|
45
|
+
* means one per frame, but Android benefits from the explicit cap.
|
|
46
|
+
*/
|
|
47
|
+
export function useScroll(): UseScrollResult {
|
|
48
|
+
const scrollX = useSharedValue(0)
|
|
49
|
+
const scrollY = useSharedValue(0)
|
|
50
|
+
|
|
51
|
+
const handler = useAnimatedScrollHandler({
|
|
52
|
+
onScroll: (event) => {
|
|
53
|
+
'worklet'
|
|
54
|
+
scrollX.value = event.contentOffset.x
|
|
55
|
+
scrollY.value = event.contentOffset.y
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// `useAnimatedScrollHandler` returns an opaque worklet bag whose JS-side
|
|
60
|
+
// type is `(event) => void` but actually carries native event-handler
|
|
61
|
+
// wiring. Cast through `unknown` because the public RN `onScroll` type
|
|
62
|
+
// wants a `NativeSyntheticEvent`-taking function and Reanimated's handler
|
|
63
|
+
// is structurally compatible — the cast is to satisfy the consumer's
|
|
64
|
+
// prop type at the call site.
|
|
65
|
+
return {
|
|
66
|
+
scrollX,
|
|
67
|
+
scrollY,
|
|
68
|
+
onScroll: handler as unknown as (
|
|
69
|
+
event: NativeSyntheticEvent<NativeScrollEvent>,
|
|
70
|
+
) => void,
|
|
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
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
useAnimatedReaction,
|
|
4
|
+
useSharedValue,
|
|
5
|
+
withSpring,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from 'react-native-reanimated'
|
|
8
|
+
import { springToReanimated } from '../transitions/spring'
|
|
9
|
+
import { type SpringTransition } from '../types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Animate a shared value toward `target` with spring physics, using the
|
|
13
|
+
* library's react-spring vocabulary (`tension` / `friction` / `mass`).
|
|
14
|
+
*
|
|
15
|
+
* `target` may be a plain number or a `SharedValue<number>`. The plain-number
|
|
16
|
+
* path drives the spring from a JS `useEffect`, so the animation re-runs on
|
|
17
|
+
* every render where `target` changes. The shared-value path drives the
|
|
18
|
+
* spring from a Reanimated reaction on the UI thread, so values produced by
|
|
19
|
+
* gestures, scroll handlers, or other worklets flow through without bouncing
|
|
20
|
+
* back to JS.
|
|
21
|
+
*
|
|
22
|
+
* Both call sites end up at the same `withSpring` invocation; the split is
|
|
23
|
+
* just about which thread observes the source change.
|
|
24
|
+
*/
|
|
25
|
+
export function useSpring(
|
|
26
|
+
target: number | SharedValue<number>,
|
|
27
|
+
config?: SpringTransition,
|
|
28
|
+
): SharedValue<number> {
|
|
29
|
+
// Reanimated config is rebuilt only when the public config object changes
|
|
30
|
+
// shape. The worklet path reads this from JS-thread closure capture, which
|
|
31
|
+
// is fine: it's the resolved config that's invariant across UI-thread
|
|
32
|
+
// ticks, not a JS-thread reference that would go stale.
|
|
33
|
+
const reanimConfig = useMemo(
|
|
34
|
+
() => springToReanimated(config ?? {}),
|
|
35
|
+
// Keyed on the resolved primitive fields rather than the `config` object
|
|
36
|
+
// identity: callers routinely pass a fresh object literal each render, so
|
|
37
|
+
// depending on `config` itself would rebuild the Reanimated config on
|
|
38
|
+
// every render and defeat the memo.
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
[
|
|
41
|
+
config?.tension,
|
|
42
|
+
config?.friction,
|
|
43
|
+
config?.mass,
|
|
44
|
+
config?.velocity,
|
|
45
|
+
config?.restSpeedThreshold,
|
|
46
|
+
config?.restDisplacementThreshold,
|
|
47
|
+
],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const isSharedTarget = isSharedValue(target)
|
|
51
|
+
const initial = isSharedTarget ? target.value : (target as number)
|
|
52
|
+
const output = useSharedValue<number>(initial)
|
|
53
|
+
|
|
54
|
+
// Plain-number path. The reaction below is a no-op when `target` is a
|
|
55
|
+
// number, so this effect carries the change. Reading `target` directly in
|
|
56
|
+
// the dep array means React drives the schedule; we don't have to babysit
|
|
57
|
+
// a stale closure.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (isSharedTarget) return
|
|
60
|
+
output.value = withSpring(target as number, reanimConfig)
|
|
61
|
+
// `output` is identity-stable per hook instance (Reanimated guarantee).
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, [isSharedTarget, target, reanimConfig])
|
|
64
|
+
|
|
65
|
+
// SharedValue path. `useAnimatedReaction` runs the prepare worklet whenever
|
|
66
|
+
// its returned value changes; we read `.value` off the target SV and pipe
|
|
67
|
+
// it through `withSpring` on the UI thread. When the target is a plain
|
|
68
|
+
// number we never declare a source so the reaction is inert (returns
|
|
69
|
+
// `null`, never fires `react`).
|
|
70
|
+
useAnimatedReaction(
|
|
71
|
+
() => {
|
|
72
|
+
'worklet'
|
|
73
|
+
if (!isSharedTarget) return null
|
|
74
|
+
return (target as SharedValue<number>).value
|
|
75
|
+
},
|
|
76
|
+
(next, prev) => {
|
|
77
|
+
'worklet'
|
|
78
|
+
if (next === null || next === prev) return
|
|
79
|
+
output.value = withSpring(next, reanimConfig)
|
|
80
|
+
},
|
|
81
|
+
[isSharedTarget, reanimConfig],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return output
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isSharedValue(v: unknown): v is SharedValue<number> {
|
|
88
|
+
// SharedValues are plain objects with a single `value` accessor — there is
|
|
89
|
+
// no public constructor or instanceof check. Reading `'value' in v` on any
|
|
90
|
+
// POJO would also pass, but the hook's call site already narrows the type;
|
|
91
|
+
// this guard exists to dispatch between the two implementation paths, not
|
|
92
|
+
// to validate untrusted input.
|
|
93
|
+
return (
|
|
94
|
+
typeof v === 'object' &&
|
|
95
|
+
v !== null &&
|
|
96
|
+
'value' in (v as Record<string, unknown>)
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Extrapolation,
|
|
3
|
+
interpolate,
|
|
4
|
+
interpolateColor,
|
|
5
|
+
useDerivedValue,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from 'react-native-reanimated'
|
|
8
|
+
// `isWorkletFunction` was re-exported from `react-native-reanimated` historically
|
|
9
|
+
// but is deprecated there in favor of importing from `react-native-worklets`.
|
|
10
|
+
// `react-native-worklets` is a required peer of Reanimated 4, so the direct
|
|
11
|
+
// import is always available wherever Inertia is.
|
|
12
|
+
import { isWorkletFunction } from 'react-native-worklets'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extrapolation behavior at the edges of the input range. Mirrors
|
|
16
|
+
* Reanimated's enum so consumers don't need a separate import.
|
|
17
|
+
*
|
|
18
|
+
* - `'clamp'` (default) — output stays pinned at the first/last value
|
|
19
|
+
* outside the input range. Matches Framer Motion's default.
|
|
20
|
+
* - `'identity'` — return the input unchanged outside the range.
|
|
21
|
+
* - `'extend'` — continue the linear slope beyond the range.
|
|
22
|
+
*/
|
|
23
|
+
export type ExtrapolationMode = 'clamp' | 'identity' | 'extend'
|
|
24
|
+
|
|
25
|
+
export interface UseTransformOptions {
|
|
26
|
+
extrapolateLeft?: ExtrapolationMode
|
|
27
|
+
extrapolateRight?: ExtrapolationMode
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Derive a value from one or more shared values via a transformer worklet.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* const x = useMotionValue(0)
|
|
35
|
+
* const y = useMotionValue(0)
|
|
36
|
+
* const distance = useTransform(() => Math.sqrt(x.value ** 2 + y.value ** 2))
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* The transformer must be a worklet (or a plain function we auto-wrap —
|
|
40
|
+
* see the easing wrapper for the rationale). It runs on the UI thread on
|
|
41
|
+
* every frame where any read shared value changes.
|
|
42
|
+
*/
|
|
43
|
+
export function useTransform<T>(transformer: () => T): SharedValue<T>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Interpolate a numeric shared value onto a range of numbers or colors.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* const scroll = useMotionValue(0)
|
|
50
|
+
* const headerOpacity = useTransform(scroll, [0, 100], [1, 0])
|
|
51
|
+
* const headerColor = useTransform(scroll, [0, 100], ['#fff', '#000'])
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* When `outputRange` is numeric, this maps to Reanimated's `interpolate`;
|
|
55
|
+
* when it's a tuple of color strings, it maps to `interpolateColor`. The
|
|
56
|
+
* input range must be monotonically increasing.
|
|
57
|
+
*/
|
|
58
|
+
export function useTransform(
|
|
59
|
+
value: SharedValue<number>,
|
|
60
|
+
inputRange: readonly number[],
|
|
61
|
+
outputRange: readonly number[],
|
|
62
|
+
options?: UseTransformOptions,
|
|
63
|
+
): SharedValue<number>
|
|
64
|
+
export function useTransform(
|
|
65
|
+
value: SharedValue<number>,
|
|
66
|
+
inputRange: readonly number[],
|
|
67
|
+
outputRange: readonly string[],
|
|
68
|
+
options?: UseTransformOptions,
|
|
69
|
+
): SharedValue<string>
|
|
70
|
+
|
|
71
|
+
export function useTransform<T>(
|
|
72
|
+
arg1: (() => T) | SharedValue<number>,
|
|
73
|
+
inputRange?: readonly number[],
|
|
74
|
+
outputRange?: readonly number[] | readonly string[],
|
|
75
|
+
options?: UseTransformOptions,
|
|
76
|
+
): SharedValue<T> | SharedValue<number> | SharedValue<string> {
|
|
77
|
+
// Build the producer worklet on the JS thread, then call
|
|
78
|
+
// `useDerivedValue` exactly once. Keeping the hook call unconditional
|
|
79
|
+
// satisfies rules-of-hooks; per-call branching (transformer vs
|
|
80
|
+
// interpolation) is decided once at JS time, never at frame time.
|
|
81
|
+
let producer: () => unknown
|
|
82
|
+
if (typeof arg1 === 'function') {
|
|
83
|
+
// Transformer overload. The public surface accepts a plain function;
|
|
84
|
+
// Reanimated 3.9+ requires worklets in nested-derivation contexts, so
|
|
85
|
+
// we auto-wrap at JS time the same way `ensureWorkletEasing` does.
|
|
86
|
+
const userFn = arg1 as () => T
|
|
87
|
+
producer = isWorkletFunction(userFn)
|
|
88
|
+
? userFn
|
|
89
|
+
: () => {
|
|
90
|
+
'worklet'
|
|
91
|
+
return userFn()
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// Interpolation overload. We pre-resolve everything JS-side so the
|
|
95
|
+
// worklet body only consumes flat values.
|
|
96
|
+
const source = arg1
|
|
97
|
+
const input = inputRange as readonly number[]
|
|
98
|
+
const output = outputRange as readonly (number | string)[]
|
|
99
|
+
const isColor = output.length > 0 && typeof output[0] === 'string'
|
|
100
|
+
const extrapolateLeft = mapExtrapolation(options?.extrapolateLeft)
|
|
101
|
+
const extrapolateRight = mapExtrapolation(options?.extrapolateRight)
|
|
102
|
+
producer = isColor
|
|
103
|
+
? () => {
|
|
104
|
+
'worklet'
|
|
105
|
+
return interpolateColor(
|
|
106
|
+
source.value,
|
|
107
|
+
input as number[],
|
|
108
|
+
output as string[],
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
: () => {
|
|
112
|
+
'worklet'
|
|
113
|
+
return interpolate(
|
|
114
|
+
source.value,
|
|
115
|
+
input as number[],
|
|
116
|
+
output as number[],
|
|
117
|
+
{ extrapolateLeft, extrapolateRight },
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return useDerivedValue(producer as () => never) as unknown as
|
|
123
|
+
| SharedValue<T>
|
|
124
|
+
| SharedValue<number>
|
|
125
|
+
| SharedValue<string>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mapExtrapolation(mode: ExtrapolationMode | undefined): Extrapolation {
|
|
129
|
+
if (mode === 'identity') return Extrapolation.IDENTITY
|
|
130
|
+
if (mode === 'extend') return Extrapolation.EXTEND
|
|
131
|
+
return Extrapolation.CLAMP
|
|
132
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useMemo, useRef } from 'react'
|
|
2
|
+
import { type VariantController } from '../types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a controller for a variants map. The controller is the imperative
|
|
6
|
+
* escape hatch — pass it to a Motion primitive via `controller={...}` and
|
|
7
|
+
* call `controller.transitionTo('open')` from event handlers, async chains,
|
|
8
|
+
* etc. The hook name mirrors the prop name (`variants`) so the relationship
|
|
9
|
+
* is obvious.
|
|
10
|
+
*
|
|
11
|
+
* The controller is identity-stable across renders. State changes are
|
|
12
|
+
* delivered to subscribers via `subscribe` — Motion primitives subscribe
|
|
13
|
+
* internally and re-resolve `animate` on each transition.
|
|
14
|
+
*/
|
|
15
|
+
export function useVariants<V extends Readonly<Record<string, object>>>(
|
|
16
|
+
variants: V,
|
|
17
|
+
initial?: keyof V & string,
|
|
18
|
+
): VariantController<keyof V & string> {
|
|
19
|
+
// Pin the variants object reference for the lifetime of the controller.
|
|
20
|
+
// Consumers shouldn't recreate the map on every render anyway, but if they
|
|
21
|
+
// do, the controller still works against the first definition's keys.
|
|
22
|
+
const variantsRef = useRef(variants)
|
|
23
|
+
|
|
24
|
+
return useMemo(() => {
|
|
25
|
+
const listeners = new Set<(next: keyof V & string) => void>()
|
|
26
|
+
let current =
|
|
27
|
+
initial ??
|
|
28
|
+
((Object.keys(variantsRef.current)[0] ?? '') as keyof V & string)
|
|
29
|
+
|
|
30
|
+
const controller: VariantController<keyof V & string> = {
|
|
31
|
+
get current() {
|
|
32
|
+
return current
|
|
33
|
+
},
|
|
34
|
+
transitionTo(next) {
|
|
35
|
+
if (next === current) return
|
|
36
|
+
if (!(next in variantsRef.current)) {
|
|
37
|
+
if (__DEV__) {
|
|
38
|
+
console.warn(
|
|
39
|
+
`[inertia] useVariants: unknown variant "${String(next)}". Known keys: ${Object.keys(variantsRef.current).join(', ')}`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
current = next
|
|
45
|
+
for (const fn of listeners) fn(next)
|
|
46
|
+
},
|
|
47
|
+
subscribe(listener) {
|
|
48
|
+
listeners.add(listener)
|
|
49
|
+
return () => {
|
|
50
|
+
listeners.delete(listener)
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
return controller
|
|
55
|
+
// Identity-stable controller — only build once.
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
}, [])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
declare const __DEV__: boolean
|