@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +131 -0
  4. package/dist/gestureLayer/index.d.mts +119 -0
  5. package/dist/gestureLayer/index.d.ts +119 -0
  6. package/dist/gestureLayer/index.js +346 -0
  7. package/dist/gestureLayer/index.js.map +1 -0
  8. package/dist/gestureLayer/index.mjs +344 -0
  9. package/dist/gestureLayer/index.mjs.map +1 -0
  10. package/dist/index.d.mts +507 -0
  11. package/dist/index.d.ts +507 -0
  12. package/dist/index.js +1546 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/index.mjs +1515 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/motion/Image.d.mts +12 -0
  17. package/dist/motion/Image.d.ts +12 -0
  18. package/dist/motion/Image.js +1138 -0
  19. package/dist/motion/Image.js.map +1 -0
  20. package/dist/motion/Image.mjs +1132 -0
  21. package/dist/motion/Image.mjs.map +1 -0
  22. package/dist/motion/Pressable.d.mts +15 -0
  23. package/dist/motion/Pressable.d.ts +15 -0
  24. package/dist/motion/Pressable.js +1138 -0
  25. package/dist/motion/Pressable.js.map +1 -0
  26. package/dist/motion/Pressable.mjs +1132 -0
  27. package/dist/motion/Pressable.mjs.map +1 -0
  28. package/dist/motion/ScrollView.d.mts +12 -0
  29. package/dist/motion/ScrollView.d.ts +12 -0
  30. package/dist/motion/ScrollView.js +1138 -0
  31. package/dist/motion/ScrollView.js.map +1 -0
  32. package/dist/motion/ScrollView.mjs +1132 -0
  33. package/dist/motion/ScrollView.mjs.map +1 -0
  34. package/dist/motion/Text.d.mts +11 -0
  35. package/dist/motion/Text.d.ts +11 -0
  36. package/dist/motion/Text.js +1138 -0
  37. package/dist/motion/Text.js.map +1 -0
  38. package/dist/motion/Text.mjs +1132 -0
  39. package/dist/motion/Text.mjs.map +1 -0
  40. package/dist/motion/View.d.mts +11 -0
  41. package/dist/motion/View.d.ts +11 -0
  42. package/dist/motion/View.js +1138 -0
  43. package/dist/motion/View.js.map +1 -0
  44. package/dist/motion/View.mjs +1132 -0
  45. package/dist/motion/View.mjs.map +1 -0
  46. package/dist/testing/index.d.mts +57 -0
  47. package/dist/testing/index.d.ts +57 -0
  48. package/dist/testing/index.js +19 -0
  49. package/dist/testing/index.js.map +1 -0
  50. package/dist/testing/index.mjs +16 -0
  51. package/dist/testing/index.mjs.map +1 -0
  52. package/dist/touch/index.d.mts +146 -0
  53. package/dist/touch/index.d.ts +146 -0
  54. package/dist/touch/index.js +166 -0
  55. package/dist/touch/index.js.map +1 -0
  56. package/dist/touch/index.mjs +164 -0
  57. package/dist/touch/index.mjs.map +1 -0
  58. package/dist/types-cU43dEmH.d.mts +339 -0
  59. package/dist/types-cU43dEmH.d.ts +339 -0
  60. package/dist/useGesture-BnY65PlQ.d.ts +84 -0
  61. package/dist/useGesture-DxtXdz-K.d.mts +84 -0
  62. package/jest-preset.js +33 -0
  63. package/jest-setup.js +213 -0
  64. package/llms.txt +142 -0
  65. package/package.json +151 -0
  66. package/src/__type-tests__/animate.test-d.tsx +88 -0
  67. package/src/__type-tests__/variants.test-d.tsx +67 -0
  68. package/src/config/MotionConfig.tsx +30 -0
  69. package/src/config/MotionConfigContext.ts +53 -0
  70. package/src/config/index.ts +9 -0
  71. package/src/gestureLayer/index.ts +21 -0
  72. package/src/gestureLayer/useGestureLayer.ts +285 -0
  73. package/src/gestures/focusVisibility.ts +61 -0
  74. package/src/gestures/index.ts +1 -0
  75. package/src/index.ts +72 -0
  76. package/src/layout/index.ts +16 -0
  77. package/src/layout/resolveLayout.ts +54 -0
  78. package/src/layout/sharedRegistry.ts +111 -0
  79. package/src/layout/useSharedLayout.ts +289 -0
  80. package/src/motion/Image.tsx +9 -0
  81. package/src/motion/Pressable.tsx +12 -0
  82. package/src/motion/ScrollView.tsx +9 -0
  83. package/src/motion/Text.tsx +8 -0
  84. package/src/motion/View.tsx +8 -0
  85. package/src/motion/createMotionComponent.tsx +1306 -0
  86. package/src/motion/index.ts +26 -0
  87. package/src/motion/installCheck.ts +65 -0
  88. package/src/presence/Presence.tsx +165 -0
  89. package/src/presence/PresenceContext.ts +28 -0
  90. package/src/presence/index.ts +6 -0
  91. package/src/testing/index.ts +78 -0
  92. package/src/touch/index.ts +18 -0
  93. package/src/touch/useTouchDrag.ts +289 -0
  94. package/src/transitions/easing.ts +53 -0
  95. package/src/transitions/index.ts +7 -0
  96. package/src/transitions/keys.ts +32 -0
  97. package/src/transitions/resolve.ts +169 -0
  98. package/src/transitions/resolveSequence.ts +87 -0
  99. package/src/transitions/runtime.ts +63 -0
  100. package/src/transitions/sig.ts +40 -0
  101. package/src/transitions/spring.ts +42 -0
  102. package/src/types.ts +383 -0
  103. package/src/values/index.ts +26 -0
  104. package/src/values/useAnimation.ts +69 -0
  105. package/src/values/useBooleanSpring.ts +33 -0
  106. package/src/values/useColorTransition.ts +72 -0
  107. package/src/values/useGesture.ts +144 -0
  108. package/src/values/useMotionValue.ts +33 -0
  109. package/src/values/useScroll.ts +72 -0
  110. package/src/values/useShadow.ts +116 -0
  111. package/src/values/useSpring.ts +98 -0
  112. package/src/values/useTransform.ts +132 -0
  113. package/src/values/useVariants.ts +60 -0
@@ -0,0 +1,30 @@
1
+ import { type ReactNode, useMemo } from 'react'
2
+ import {
3
+ MotionConfigContext,
4
+ type MotionConfigValue,
5
+ type ReducedMotion,
6
+ } from './MotionConfigContext'
7
+
8
+ /**
9
+ * Provider that controls how descendant Motion primitives respond to
10
+ * reduced-motion preferences. Wrap the root of your app once with the
11
+ * default (`reducedMotion="user"`) to respect the OS accessibility setting,
12
+ * or scope a subtree with `'always'` / `'never'` for specific use cases.
13
+ */
14
+ export function MotionConfig({
15
+ reducedMotion = 'user',
16
+ children,
17
+ }: {
18
+ reducedMotion?: ReducedMotion
19
+ children: ReactNode
20
+ }) {
21
+ const value = useMemo<MotionConfigValue>(
22
+ () => ({ reducedMotion }),
23
+ [reducedMotion],
24
+ )
25
+ return (
26
+ <MotionConfigContext.Provider value={value}>
27
+ {children}
28
+ </MotionConfigContext.Provider>
29
+ )
30
+ }
@@ -0,0 +1,53 @@
1
+ import { createContext, useContext } from 'react'
2
+ import { useReducedMotion } from 'react-native-reanimated'
3
+
4
+ /**
5
+ * How descendant Motion primitives should treat reduced-motion preferences.
6
+ *
7
+ * - `'user'` (default): defer to the OS accessibility setting via
8
+ * Reanimated's `useReducedMotion()`. This is the only value that respects
9
+ * user choice and is the right default for app-level wrappers.
10
+ * - `'never'`: animate regardless of OS setting. Use sparingly — e.g. for
11
+ * onboarding transitions you've decided are essential.
12
+ * - `'always'`: never animate, regardless of OS setting. Useful for tests
13
+ * and snapshots.
14
+ */
15
+ export type ReducedMotion = 'user' | 'never' | 'always'
16
+
17
+ export interface MotionConfigValue {
18
+ reducedMotion: ReducedMotion
19
+ }
20
+
21
+ /**
22
+ * Default config used when a Motion primitive is rendered without a
23
+ * `<MotionConfig>` ancestor. `'user'` means respect the OS setting.
24
+ */
25
+ export const DEFAULT_MOTION_CONFIG: MotionConfigValue = {
26
+ reducedMotion: 'user',
27
+ }
28
+
29
+ export const MotionConfigContext = createContext<MotionConfigValue>(
30
+ DEFAULT_MOTION_CONFIG,
31
+ )
32
+
33
+ /**
34
+ * Read the active `<MotionConfig>` from a descendant. Returns the default
35
+ * (`'user'`) when no provider is present.
36
+ */
37
+ export function useMotionConfig(): MotionConfigValue {
38
+ return useContext(MotionConfigContext)
39
+ }
40
+
41
+ /**
42
+ * Resolve the active reduced-motion mode to a boolean. `'user'` consults
43
+ * Reanimated's OS-backed hook; `'always'` / `'never'` shortcut. Motion
44
+ * primitives call this to decide whether to swap transitions for
45
+ * `no-animation`.
46
+ */
47
+ export function useShouldReduceMotion(): boolean {
48
+ const { reducedMotion } = useMotionConfig()
49
+ const osReduced = useReducedMotion()
50
+ if (reducedMotion === 'never') return false
51
+ if (reducedMotion === 'always') return true
52
+ return osReduced
53
+ }
@@ -0,0 +1,9 @@
1
+ export { MotionConfig } from './MotionConfig'
2
+ export {
3
+ MotionConfigContext,
4
+ DEFAULT_MOTION_CONFIG,
5
+ useMotionConfig,
6
+ useShouldReduceMotion,
7
+ type MotionConfigValue,
8
+ type ReducedMotion,
9
+ } from './MotionConfigContext'
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Opt-in subpath: `@rootnative/inertia/gesture-layer`.
3
+ *
4
+ * `useGestureLayer` is a generic interactive-feedback primitive that sits one
5
+ * step above `useGesture()` — consumer supplies per-state target values; the
6
+ * hook handles "strongest active layer wins" composition (clamped-max for
7
+ * numerics, priority cascade for colors), the worklet, and the transition.
8
+ *
9
+ * Reach for it when MD3 state-layer haloes, iOS-translucent overlays, or
10
+ * any "highest engaged state controls the overlay" pattern shows up. For
11
+ * composition models this hook doesn't express (additive, multiply, custom
12
+ * per-key blends), drop to `useGesture()` and write a `useAnimatedStyle`
13
+ * block by hand.
14
+ */
15
+ export { useGestureLayer } from './useGestureLayer'
16
+ export type {
17
+ GestureLayerStates,
18
+ GestureLayerStyle,
19
+ UseGestureLayerOptions,
20
+ UseGestureLayerResult,
21
+ } from './useGestureLayer'
@@ -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 '@rootnative/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
+ }
@@ -0,0 +1,61 @@
1
+ import { Platform } from 'react-native'
2
+
3
+ /**
4
+ * Input-modality tracker for the `focusVisible` gesture sub-state.
5
+ *
6
+ * Implements the W3C `:focus-visible` heuristic: a focus event counts as
7
+ * "visible" only when the most recent user input was keyboard-driven. Mouse,
8
+ * pointer, and touch events flip the modality to `'pointer'`; keyboard events
9
+ * flip it back to `'keyboard'`.
10
+ *
11
+ * On native platforms there is no pointer-vs-keyboard distinction — focus
12
+ * arrives via D-pad, screen reader, or hardware keyboard, all of which are
13
+ * keyboard-equivalent — so `isFocusVisible()` is unconditionally `true`.
14
+ *
15
+ * The web listeners attach lazily on first call (capture phase, so they run
16
+ * before the focus event reaches the focused element) and stay installed for
17
+ * the lifetime of the document. They are passive and idle-cheap; the cost is
18
+ * one boolean read per `onFocus` dispatch.
19
+ */
20
+
21
+ type InputModality = 'keyboard' | 'pointer'
22
+
23
+ // Default to `'keyboard'` so a programmatic / autofocus that happens before
24
+ // any user input still draws a focus ring — matches the W3C polyfill default.
25
+ let modality: InputModality = 'keyboard'
26
+ let installed = false
27
+
28
+ function setKeyboard() {
29
+ modality = 'keyboard'
30
+ }
31
+
32
+ function setPointer() {
33
+ modality = 'pointer'
34
+ }
35
+
36
+ function ensureInstalled(): void {
37
+ if (installed) return
38
+ if (Platform.OS !== 'web') return
39
+ if (typeof document === 'undefined') return
40
+ document.addEventListener('keydown', setKeyboard, true)
41
+ document.addEventListener('mousedown', setPointer, true)
42
+ document.addEventListener('pointerdown', setPointer, true)
43
+ document.addEventListener('touchstart', setPointer, true)
44
+ installed = true
45
+ }
46
+
47
+ /**
48
+ * `true` if the next `onFocus` should be treated as "focus-visible" (keyboard
49
+ * focus). On native, always `true`. On web, reflects the most recent user
50
+ * input modality.
51
+ */
52
+ export function isFocusVisible(): boolean {
53
+ if (Platform.OS !== 'web') return true
54
+ ensureInstalled()
55
+ return modality === 'keyboard'
56
+ }
57
+
58
+ /** @internal — test-only hook to reset module state between cases. */
59
+ export function __resetFocusVisibilityForTests(next: InputModality): void {
60
+ modality = next
61
+ }
@@ -0,0 +1 @@
1
+ export { isFocusVisible } from './focusVisibility'
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `@rootnative/inertia` — declarative animation primitives for React Native.
3
+ *
4
+ * v0.1 surface: `Motion.View` / `Motion.Text` / `Motion.Image` /
5
+ * `Motion.Pressable` / `Motion.ScrollView`, with `initial` / `animate` /
6
+ * `exit` / `transition` / `variants` / `gesture` / `controller` /
7
+ * `onAnimationEnd` props. Sequences, repeats (single-value and
8
+ * sequence-level), spring / timing / decay / no-animation transitions,
9
+ * and `<Presence>` for mount / unmount transitions all in scope.
10
+ * `<MotionConfig reducedMotion>` gates motion against the OS setting.
11
+ */
12
+ export {
13
+ Motion,
14
+ MotionView,
15
+ MotionText,
16
+ MotionImage,
17
+ MotionPressable,
18
+ MotionScrollView,
19
+ createMotionComponent,
20
+ } from './motion'
21
+ export { MotionConfig, useMotionConfig, useShouldReduceMotion } from './config'
22
+ export type { MotionConfigValue, ReducedMotion } from './config'
23
+ export { Presence, usePresence } from './presence'
24
+ export type { PresenceContextValue } from './presence'
25
+ export {
26
+ buildReleaseAnimation,
27
+ resolveTransition,
28
+ resolveAnimatableValue,
29
+ ensureWorkletEasing,
30
+ } from './transitions'
31
+ export {
32
+ useAnimation,
33
+ useBooleanSpring,
34
+ useColorTransition,
35
+ useGesture,
36
+ useMotionValue,
37
+ useScroll,
38
+ useShadow,
39
+ useSpring,
40
+ useTransform,
41
+ useVariants,
42
+ } from './values'
43
+ export type {
44
+ ColorStyleKey,
45
+ ExtrapolationMode,
46
+ ShadowConfig,
47
+ UseColorTransitionOptions,
48
+ UseGestureHandlers,
49
+ UseGestureResult,
50
+ UseScrollResult,
51
+ UseShadowOptions,
52
+ UseTransformOptions,
53
+ } from './values'
54
+ export type {
55
+ AnimatableValue,
56
+ AnimateStyle,
57
+ AnimationCallbackInfo,
58
+ DecayTransition,
59
+ GestureSubStates,
60
+ MotionComponent,
61
+ MotionProps,
62
+ NoAnimationTransition,
63
+ PerPropertyTransition,
64
+ RepeatConfig,
65
+ SequenceStep,
66
+ SpringTransition,
67
+ TimingTransition,
68
+ Transition,
69
+ TransitionConfig,
70
+ VariantController,
71
+ VariantsMap,
72
+ } from './types'
@@ -0,0 +1,16 @@
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,54 @@
1
+ import { LinearTransition } from 'react-native-reanimated'
2
+ import { ensureWorkletEasing } from '../transitions/easing'
3
+ import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
4
+ import { type TransitionConfig } from '../types'
5
+
6
+ export type LayoutProp = boolean | TransitionConfig | undefined
7
+
8
+ /**
9
+ * Resolve the public `layout` prop into a Reanimated `LinearTransition` builder.
10
+ *
11
+ * - `undefined` / `false` / `{ type: 'no-animation' }` → `undefined` (no layout
12
+ * animation; layout changes snap).
13
+ * - `true` → default spring with the library's tuned tension / friction / mass.
14
+ * - `{ type: 'spring', ... }` → spring with react-spring vocabulary, bridged
15
+ * into `springify().damping().stiffness().mass()` via `springToReanimated`.
16
+ * - `{ type: 'timing', ... }` → `.duration().easing()`. User easing fns are
17
+ * auto-wrapped as worklets (Reanimated 3.9+ validates this).
18
+ * - `{ type: 'decay', ... }` → silently downgrades to spring; decay doesn't
19
+ * have a clear target for a layout transition.
20
+ *
21
+ * When `reducedMotion` is true the caller should pass `undefined` so the
22
+ * underlying component renders without a layout animation at all. We choose
23
+ * this over `LinearTransition.duration(0)` because Reanimated still runs the
24
+ * commit-tracking machinery in that case; the snap path is genuinely cheaper.
25
+ */
26
+ export function resolveLayoutTransition(
27
+ layout: LayoutProp,
28
+ ): LinearTransition | undefined {
29
+ if (!layout) return undefined
30
+
31
+ const cfg: TransitionConfig = layout === true ? { type: 'spring' } : layout
32
+
33
+ if (cfg.type === 'no-animation') return undefined
34
+
35
+ if (cfg.type === 'timing') {
36
+ let builder = LinearTransition.duration(cfg.duration ?? 300)
37
+ const easing = ensureWorkletEasing(cfg.easing)
38
+ if (easing) builder = builder.easing(easing)
39
+ if (cfg.delay) builder = builder.delay(cfg.delay)
40
+ return builder
41
+ }
42
+
43
+ const spring = cfg.type === 'decay' ? ({ type: 'spring' } as const) : cfg
44
+ const { stiffness, damping, mass } = springToReanimated({
45
+ ...DEFAULT_SPRING,
46
+ ...spring,
47
+ })
48
+ let builder = LinearTransition.springify()
49
+ .stiffness(stiffness)
50
+ .damping(damping)
51
+ .mass(mass)
52
+ if ('delay' in spring && spring.delay) builder = builder.delay(spring.delay)
53
+ return builder
54
+ }