@onlynative/inertia 0.0.1-alpha.7 → 0.0.1-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +1 -1
  2. package/dist/gestureLayer/index.d.mts +119 -0
  3. package/dist/gestureLayer/index.d.ts +119 -0
  4. package/dist/gestureLayer/index.js +1745 -0
  5. package/dist/gestureLayer/index.js.map +1 -0
  6. package/dist/gestureLayer/index.mjs +1743 -0
  7. package/dist/gestureLayer/index.mjs.map +1 -0
  8. package/dist/index.d.mts +114 -74
  9. package/dist/index.d.ts +114 -74
  10. package/dist/index.js +279 -41
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +279 -44
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/motion/Image.d.mts +1 -1
  15. package/dist/motion/Image.d.ts +1 -1
  16. package/dist/motion/Image.js +178 -4
  17. package/dist/motion/Image.js.map +1 -1
  18. package/dist/motion/Image.mjs +180 -6
  19. package/dist/motion/Image.mjs.map +1 -1
  20. package/dist/motion/Pressable.d.mts +1 -1
  21. package/dist/motion/Pressable.d.ts +1 -1
  22. package/dist/motion/Pressable.js +178 -4
  23. package/dist/motion/Pressable.js.map +1 -1
  24. package/dist/motion/Pressable.mjs +180 -6
  25. package/dist/motion/Pressable.mjs.map +1 -1
  26. package/dist/motion/ScrollView.d.mts +1 -1
  27. package/dist/motion/ScrollView.d.ts +1 -1
  28. package/dist/motion/ScrollView.js +178 -4
  29. package/dist/motion/ScrollView.js.map +1 -1
  30. package/dist/motion/ScrollView.mjs +180 -6
  31. package/dist/motion/ScrollView.mjs.map +1 -1
  32. package/dist/motion/Text.d.mts +1 -1
  33. package/dist/motion/Text.d.ts +1 -1
  34. package/dist/motion/Text.js +178 -4
  35. package/dist/motion/Text.js.map +1 -1
  36. package/dist/motion/Text.mjs +180 -6
  37. package/dist/motion/Text.mjs.map +1 -1
  38. package/dist/motion/View.d.mts +1 -1
  39. package/dist/motion/View.d.ts +1 -1
  40. package/dist/motion/View.js +178 -4
  41. package/dist/motion/View.js.map +1 -1
  42. package/dist/motion/View.mjs +180 -6
  43. package/dist/motion/View.mjs.map +1 -1
  44. package/dist/touch/index.d.mts +146 -0
  45. package/dist/touch/index.d.ts +146 -0
  46. package/dist/touch/index.js +166 -0
  47. package/dist/touch/index.js.map +1 -0
  48. package/dist/touch/index.mjs +164 -0
  49. package/dist/touch/index.mjs.map +1 -0
  50. package/dist/{types-NmNeJjo1.d.mts → types-BwyvoH2V.d.mts} +24 -4
  51. package/dist/{types-NmNeJjo1.d.ts → types-BwyvoH2V.d.ts} +24 -4
  52. package/dist/useGesture-BPPp9LhV.d.ts +84 -0
  53. package/dist/useGesture-BnBF4OtT.d.mts +84 -0
  54. package/llms.txt +12 -3
  55. package/package.json +15 -1
  56. package/src/gestureLayer/index.ts +21 -0
  57. package/src/gestureLayer/useGestureLayer.ts +285 -0
  58. package/src/index.ts +7 -0
  59. package/src/layout/index.ts +15 -0
  60. package/src/layout/sharedRegistry.ts +108 -0
  61. package/src/layout/useSharedLayout.ts +289 -0
  62. package/src/motion/createMotionComponent.tsx +60 -4
  63. package/src/touch/index.ts +18 -0
  64. package/src/touch/useTouchDrag.ts +289 -0
  65. package/src/types.ts +23 -3
  66. package/src/values/index.ts +11 -0
  67. package/src/values/useBooleanSpring.ts +33 -0
  68. package/src/values/useColorTransition.ts +72 -0
  69. package/src/values/useShadow.ts +116 -0
@@ -0,0 +1,285 @@
1
+ import { useEffect, useMemo } from 'react'
2
+ import {
3
+ interpolateColor,
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ type AnimatedStyle,
7
+ } from 'react-native-reanimated'
8
+ import { useShouldReduceMotion } from '../config'
9
+ import { isTopLevelTransition, resolveTransition } from '../transitions'
10
+ import { useGesture, type UseGestureHandlers } from '../values/useGesture'
11
+ import { type GestureLayerTransitions, type TransitionConfig } from '../types'
12
+
13
+ /**
14
+ * A single gesture-layer style — a flat map of style keys to a value. Numeric
15
+ * values participate in clamped-max composition (the "strongest active layer
16
+ * wins" model used by MD3 state-layer haloes); string values are treated as
17
+ * colors and composed via priority cascade with `interpolateColor`.
18
+ *
19
+ * The hook does not validate that string values are valid colors — passing
20
+ * something like `borderStyle: 'solid'` will crash inside the worklet. Keep
21
+ * string values to color strings.
22
+ */
23
+ export type GestureLayerStyle = {
24
+ [key: string]: number | string | undefined
25
+ }
26
+
27
+ /**
28
+ * Per-state style maps. Every key is optional; missing layers default to
29
+ * `rest` (or `0` / `'transparent'` if `rest` is also absent for that key).
30
+ *
31
+ * - `rest` — base values, applied when no other layer is active.
32
+ * - `hovered` / `focused` / `focusVisible` / `pressed` — gesture-driven
33
+ * states tracked via the underlying `useGesture` hook. Each owns an
34
+ * independent 0↔1 progress that fades the layer in/out per the configured
35
+ * transition.
36
+ * - `disabled` — gated by the JS-side `options.disabled` flag rather than a
37
+ * gesture. Sits at the top of the priority cascade and overrides every
38
+ * gesture layer when active.
39
+ */
40
+ export interface GestureLayerStates {
41
+ rest?: GestureLayerStyle
42
+ hovered?: GestureLayerStyle
43
+ focused?: GestureLayerStyle
44
+ focusVisible?: GestureLayerStyle
45
+ pressed?: GestureLayerStyle
46
+ disabled?: GestureLayerStyle
47
+ }
48
+
49
+ export interface UseGestureLayerOptions {
50
+ /**
51
+ * When `true`, the `disabled` layer becomes active (or `rest` if `disabled`
52
+ * is undefined). Animates via the top-level transition or the library
53
+ * default spring; per-layer transitions (`GestureLayerTransitions`) do not
54
+ * apply to `disabled`.
55
+ */
56
+ disabled?: boolean
57
+ /**
58
+ * Transition forwarded to the underlying `useGesture` hook. Either a single
59
+ * `TransitionConfig` for every gesture layer, or a `GestureLayerTransitions`
60
+ * map for per-layer fades. Reduced motion collapses every transition to
61
+ * `no-animation`.
62
+ */
63
+ transition?: TransitionConfig | GestureLayerTransitions
64
+ }
65
+
66
+ export interface UseGestureLayerResult {
67
+ /**
68
+ * Animated style produced by `useAnimatedStyle` — spread on an
69
+ * `Animated.View` or pass through `<Motion.View style={...} />`.
70
+ */
71
+ style: AnimatedStyle<Record<string, unknown>>
72
+ /** Handlers to spread on the receiving `Pressable`. */
73
+ handlers: UseGestureHandlers
74
+ }
75
+
76
+ /**
77
+ * A "strongest active layer wins" interactive-feedback primitive. Sits one
78
+ * step above `useGesture()` — the consumer supplies the per-state target
79
+ * values, the hook handles the four gesture progress shared values, the
80
+ * disabled override, the worklet, and the transition.
81
+ *
82
+ * Composition model:
83
+ *
84
+ * - **Numeric keys** (opacity, scale, borderWidth, etc.) compose via
85
+ * clamped-max with `rest` as the floor:
86
+ * `out = max(rest, ...for each active gesture layer: lerp(rest, layer, progress))`.
87
+ * This matches the MD3 state-layer halo pattern — multiple states active
88
+ * simultaneously raise the value to the strongest, not the sum.
89
+ * - **Color keys** (any string value) compose via priority cascade with
90
+ * `interpolateColor`, lowest priority first: `hovered → focused →
91
+ * focusVisible → pressed`. Clamped-max doesn't apply to colors; this
92
+ * matches the cascade used by the declarative `gesture` prop.
93
+ * - **Disabled** sits at the top of the cascade for both numeric and color
94
+ * keys — when active, it lerps the composed value toward the `disabled`
95
+ * target.
96
+ *
97
+ * Reach for this when you want MD3 / iOS-translucent state-layer overlays
98
+ * without rewriting the worklet by hand for every consumer; reach for plain
99
+ * `useGesture()` when you need a composition model this hook doesn't
100
+ * express (additive, multiply, per-key custom blends).
101
+ *
102
+ * @example MD3 state-layer halo
103
+ * ```tsx
104
+ * import { useGestureLayer } from '@onlynative/inertia/gesture-layer'
105
+ * import Animated from 'react-native-reanimated'
106
+ * import { Pressable } from 'react-native'
107
+ *
108
+ * function SwitchHalo({ disabled }: { disabled?: boolean }) {
109
+ * const { style, handlers } = useGestureLayer(
110
+ * {
111
+ * rest: { opacity: 0, backgroundColor: 'transparent' },
112
+ * hovered: { opacity: 0.08, backgroundColor: '#000' },
113
+ * focused: { opacity: 0.10, backgroundColor: '#000' },
114
+ * pressed: { opacity: 0.12, backgroundColor: '#000' },
115
+ * },
116
+ * { disabled, transition: { type: 'timing', duration: 150 } },
117
+ * )
118
+ *
119
+ * return (
120
+ * <Pressable {...handlers}>
121
+ * <Animated.View style={style} />
122
+ * </Pressable>
123
+ * )
124
+ * }
125
+ * ```
126
+ */
127
+ export function useGestureLayer(
128
+ states: GestureLayerStates,
129
+ options: UseGestureLayerOptions = {},
130
+ ): UseGestureLayerResult {
131
+ const { disabled: isDisabled = false, transition } = options
132
+ const shouldReduceMotion = useShouldReduceMotion()
133
+ const gesture = useGesture(transition)
134
+ const disabledProgress = useSharedValue(0)
135
+
136
+ useEffect(() => {
137
+ const target = isDisabled ? 1 : 0
138
+ const cfg = shouldReduceMotion
139
+ ? ({ type: 'no-animation' } as const)
140
+ : (disabledTransition(transition) ?? ({ type: 'spring' } as const))
141
+ disabledProgress.value = resolveTransition(cfg, target) as never
142
+ }, [isDisabled, shouldReduceMotion, transition, disabledProgress])
143
+
144
+ // JS-thread precompute: union of keys across all layers, per-key type
145
+ // (number vs color), and a rest-fallback table. The worklet body reads
146
+ // from `meta` instead of probing each layer per frame — the type check
147
+ // only runs when layer identities change.
148
+ const meta = useMemo(() => {
149
+ const layers = {
150
+ rest: states.rest,
151
+ hovered: states.hovered,
152
+ focused: states.focused,
153
+ focusVisible: states.focusVisible,
154
+ pressed: states.pressed,
155
+ disabled: states.disabled,
156
+ }
157
+ const sources = [
158
+ layers.rest,
159
+ layers.hovered,
160
+ layers.focused,
161
+ layers.focusVisible,
162
+ layers.pressed,
163
+ layers.disabled,
164
+ ]
165
+ const keySet = new Set<string>()
166
+ for (const src of sources) {
167
+ if (!src) continue
168
+ for (const k in src) if (src[k] !== undefined) keySet.add(k)
169
+ }
170
+ const keys = Array.from(keySet)
171
+ const types: Record<string, 'number' | 'color'> = {}
172
+ const restValues: Record<string, number | string> = {}
173
+ for (const k of keys) {
174
+ let firstDefined: number | string | undefined
175
+ for (const src of sources) {
176
+ if (src && src[k] !== undefined) {
177
+ firstDefined = src[k]
178
+ break
179
+ }
180
+ }
181
+ const isColor = typeof firstDefined === 'string'
182
+ types[k] = isColor ? 'color' : 'number'
183
+ const restRaw = layers.rest ? layers.rest[k] : undefined
184
+ restValues[k] =
185
+ restRaw !== undefined ? restRaw : isColor ? 'transparent' : 0
186
+ }
187
+ return { layers, keys, types, restValues }
188
+ }, [
189
+ states.rest,
190
+ states.hovered,
191
+ states.focused,
192
+ states.focusVisible,
193
+ states.pressed,
194
+ states.disabled,
195
+ ])
196
+
197
+ const style = useAnimatedStyle(() => {
198
+ const { layers, keys, types, restValues } = meta
199
+ const ph = gesture.hovered.value
200
+ const pf = gesture.focused.value
201
+ const pfv = gesture.focusVisible.value
202
+ const pp = gesture.pressed.value
203
+ const pd = disabledProgress.value
204
+
205
+ const hoveredLayer = layers.hovered
206
+ const focusedLayer = layers.focused
207
+ const focusVisibleLayer = layers.focusVisible
208
+ const pressedLayer = layers.pressed
209
+ const disabledLayer = layers.disabled
210
+
211
+ const out: Record<string, unknown> = {}
212
+
213
+ for (let i = 0; i < keys.length; i++) {
214
+ const k = keys[i]!
215
+ const isColor = types[k] === 'color'
216
+ const rest = restValues[k]!
217
+
218
+ if (isColor) {
219
+ let v = rest as string
220
+ if (hoveredLayer && ph > 0 && hoveredLayer[k] !== undefined) {
221
+ v = interpolateColor(ph, [0, 1], [v, hoveredLayer[k] as string])
222
+ }
223
+ if (focusedLayer && pf > 0 && focusedLayer[k] !== undefined) {
224
+ v = interpolateColor(pf, [0, 1], [v, focusedLayer[k] as string])
225
+ }
226
+ if (
227
+ focusVisibleLayer &&
228
+ pfv > 0 &&
229
+ focusVisibleLayer[k] !== undefined
230
+ ) {
231
+ v = interpolateColor(pfv, [0, 1], [v, focusVisibleLayer[k] as string])
232
+ }
233
+ if (pressedLayer && pp > 0 && pressedLayer[k] !== undefined) {
234
+ v = interpolateColor(pp, [0, 1], [v, pressedLayer[k] as string])
235
+ }
236
+ if (disabledLayer && pd > 0 && disabledLayer[k] !== undefined) {
237
+ v = interpolateColor(pd, [0, 1], [v, disabledLayer[k] as string])
238
+ }
239
+ out[k] = v
240
+ } else {
241
+ const base = rest as number
242
+ let m = base
243
+ if (hoveredLayer && ph > 0 && hoveredLayer[k] !== undefined) {
244
+ const c = base + ((hoveredLayer[k] as number) - base) * ph
245
+ if (c > m) m = c
246
+ }
247
+ if (focusedLayer && pf > 0 && focusedLayer[k] !== undefined) {
248
+ const c = base + ((focusedLayer[k] as number) - base) * pf
249
+ if (c > m) m = c
250
+ }
251
+ if (
252
+ focusVisibleLayer &&
253
+ pfv > 0 &&
254
+ focusVisibleLayer[k] !== undefined
255
+ ) {
256
+ const c = base + ((focusVisibleLayer[k] as number) - base) * pfv
257
+ if (c > m) m = c
258
+ }
259
+ if (pressedLayer && pp > 0 && pressedLayer[k] !== undefined) {
260
+ const c = base + ((pressedLayer[k] as number) - base) * pp
261
+ if (c > m) m = c
262
+ }
263
+ if (disabledLayer && pd > 0 && disabledLayer[k] !== undefined) {
264
+ m = m + ((disabledLayer[k] as number) - m) * pd
265
+ }
266
+ out[k] = m
267
+ }
268
+ }
269
+
270
+ return out
271
+ })
272
+
273
+ return {
274
+ style: style as AnimatedStyle<Record<string, unknown>>,
275
+ handlers: gesture.handlers,
276
+ }
277
+ }
278
+
279
+ function disabledTransition(
280
+ transition: TransitionConfig | GestureLayerTransitions | undefined,
281
+ ): TransitionConfig | undefined {
282
+ if (!transition) return undefined
283
+ if (isTopLevelTransition(transition)) return transition
284
+ return undefined
285
+ }
package/src/index.ts CHANGED
@@ -30,18 +30,25 @@ export {
30
30
  } from './transitions'
31
31
  export {
32
32
  useAnimation,
33
+ useBooleanSpring,
34
+ useColorTransition,
33
35
  useGesture,
34
36
  useMotionValue,
35
37
  useScroll,
38
+ useShadow,
36
39
  useSpring,
37
40
  useTransform,
38
41
  useVariants,
39
42
  } from './values'
40
43
  export type {
44
+ ColorStyleKey,
41
45
  ExtrapolationMode,
46
+ ShadowConfig,
47
+ UseColorTransitionOptions,
42
48
  UseGestureHandlers,
43
49
  UseGestureResult,
44
50
  UseScrollResult,
51
+ UseShadowOptions,
45
52
  UseTransformOptions,
46
53
  } from './values'
47
54
  export type {
@@ -1 +1,16 @@
1
1
  export { resolveLayoutTransition, type LayoutProp } from './resolveLayout'
2
+ export {
3
+ clearSharedRegistry,
4
+ consumeLayout,
5
+ peekSharedLayout,
6
+ registerLayout,
7
+ releaseLayout,
8
+ SHARED_LAYOUT_TTL_MS,
9
+ __setSharedLayoutClock,
10
+ type SharedRect,
11
+ } from './sharedRegistry'
12
+ export {
13
+ useSharedLayout,
14
+ type SharedLayoutBindings,
15
+ type SharedLayoutValues,
16
+ } from './useSharedLayout'
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Module-level registry of last-known on-screen rects for shared-layout
3
+ * elements, indexed by `layoutId`. Backs `<Motion.* layoutId="..." />` —
4
+ * Reanimated 4 dropped the `sharedTransitionTag` API the previous design
5
+ * relied on, so the cross-screen shared-element transition lives in
6
+ * userland now.
7
+ *
8
+ * Lifecycle, per id:
9
+ * 1. While a Motion primitive with `layoutId={id}` is mounted, every
10
+ * `onLayout` updates the rect via `registerLayout`.
11
+ * 2. When that primitive unmounts, the same rect is left behind under a
12
+ * TTL via `releaseLayout` so a subsequent mount can consume it.
13
+ * 3. The next mount calls `consumeLayout(id)` — if a fresh entry exists
14
+ * it becomes the FLIP source rect; the entry is removed so a third
15
+ * mount with the same id doesn't re-animate from a stale snapshot.
16
+ *
17
+ * Rects are stored in **window coordinates** (what `measureInWindow`
18
+ * returns). Cross-screen transitions need this — parent-relative
19
+ * coordinates are different on each screen and wouldn't compose.
20
+ */
21
+
22
+ /** Window-relative rect of a measured element. */
23
+ export interface SharedRect {
24
+ x: number
25
+ y: number
26
+ width: number
27
+ height: number
28
+ }
29
+
30
+ interface Entry {
31
+ rect: SharedRect
32
+ expiresAt: number
33
+ }
34
+
35
+ const REGISTRY = new Map<string, Entry>()
36
+
37
+ /**
38
+ * How long (ms) a released rect remains consumable. Sized to comfortably
39
+ * cover a typical screen transition (slide animation, gesture-driven
40
+ * dismiss) without leaving stale entries lying around if no incoming
41
+ * mount picks them up.
42
+ */
43
+ export const SHARED_LAYOUT_TTL_MS = 1000
44
+
45
+ /**
46
+ * Provide the current monotonic-ish timestamp. Indirected so tests can
47
+ * stub it via `__setSharedLayoutClock` without touching `Date.now` globally.
48
+ */
49
+ let now = (): number => Date.now()
50
+
51
+ /**
52
+ * Update the latest known rect for `id`. Called on every `onLayout` of a
53
+ * Motion primitive with `layoutId` set so the registry always holds a
54
+ * current measurement if that primitive becomes the source of a future
55
+ * transition. Resets the TTL each call.
56
+ */
57
+ export function registerLayout(id: string, rect: SharedRect): void {
58
+ REGISTRY.set(id, { rect, expiresAt: now() + SHARED_LAYOUT_TTL_MS })
59
+ }
60
+
61
+ /**
62
+ * Record the rect for `id` on unmount so the next mount can consume it as
63
+ * the FLIP source. Functionally identical to `registerLayout` — the split
64
+ * is purely intent-documenting at the call site.
65
+ */
66
+ export function releaseLayout(id: string, rect: SharedRect): void {
67
+ REGISTRY.set(id, { rect, expiresAt: now() + SHARED_LAYOUT_TTL_MS })
68
+ }
69
+
70
+ /**
71
+ * Take the recorded rect for `id` if it exists and hasn't expired. The
72
+ * entry is removed in either case — at most one incoming mount consumes
73
+ * a given release, and an expired entry is dropped so it can't poison a
74
+ * later transition. Returns `undefined` when no fresh source is available,
75
+ * in which case the caller should mount without a layout animation.
76
+ */
77
+ export function consumeLayout(id: string): SharedRect | undefined {
78
+ const entry = REGISTRY.get(id)
79
+ if (!entry) return undefined
80
+ REGISTRY.delete(id)
81
+ if (entry.expiresAt < now()) return undefined
82
+ return entry.rect
83
+ }
84
+
85
+ /** Drop all entries. Tests use this to isolate between cases. */
86
+ export function clearSharedRegistry(): void {
87
+ REGISTRY.clear()
88
+ }
89
+
90
+ /**
91
+ * Inspect a registry entry without consuming it. Intended for tests and
92
+ * dev tooling; production code should go through `consumeLayout`.
93
+ */
94
+ export function peekSharedLayout(id: string): SharedRect | undefined {
95
+ const entry = REGISTRY.get(id)
96
+ if (!entry) return undefined
97
+ if (entry.expiresAt < now()) return undefined
98
+ return entry.rect
99
+ }
100
+
101
+ /**
102
+ * Test hook: swap the clock used for TTL calculations. Pass `undefined` to
103
+ * restore `Date.now`. Not exported from the package root — reachable only
104
+ * from inside the workspace.
105
+ */
106
+ export function __setSharedLayoutClock(fn: (() => number) | undefined): void {
107
+ now = fn ?? Date.now
108
+ }