@onlynative/inertia 0.0.1-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 (63) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +90 -0
  4. package/dist/index.d.mts +185 -0
  5. package/dist/index.d.ts +185 -0
  6. package/dist/index.js +817 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.mjs +796 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/dist/motion/Image.d.mts +12 -0
  11. package/dist/motion/Image.d.ts +12 -0
  12. package/dist/motion/Image.js +656 -0
  13. package/dist/motion/Image.js.map +1 -0
  14. package/dist/motion/Image.mjs +650 -0
  15. package/dist/motion/Image.mjs.map +1 -0
  16. package/dist/motion/Pressable.d.mts +15 -0
  17. package/dist/motion/Pressable.d.ts +15 -0
  18. package/dist/motion/Pressable.js +656 -0
  19. package/dist/motion/Pressable.js.map +1 -0
  20. package/dist/motion/Pressable.mjs +650 -0
  21. package/dist/motion/Pressable.mjs.map +1 -0
  22. package/dist/motion/ScrollView.d.mts +12 -0
  23. package/dist/motion/ScrollView.d.ts +12 -0
  24. package/dist/motion/ScrollView.js +656 -0
  25. package/dist/motion/ScrollView.js.map +1 -0
  26. package/dist/motion/ScrollView.mjs +650 -0
  27. package/dist/motion/ScrollView.mjs.map +1 -0
  28. package/dist/motion/Text.d.mts +11 -0
  29. package/dist/motion/Text.d.ts +11 -0
  30. package/dist/motion/Text.js +656 -0
  31. package/dist/motion/Text.js.map +1 -0
  32. package/dist/motion/Text.mjs +650 -0
  33. package/dist/motion/Text.mjs.map +1 -0
  34. package/dist/motion/View.d.mts +11 -0
  35. package/dist/motion/View.d.ts +11 -0
  36. package/dist/motion/View.js +656 -0
  37. package/dist/motion/View.js.map +1 -0
  38. package/dist/motion/View.mjs +650 -0
  39. package/dist/motion/View.mjs.map +1 -0
  40. package/dist/types-CmbXx-G3.d.mts +185 -0
  41. package/dist/types-CmbXx-G3.d.ts +185 -0
  42. package/llms.txt +78 -0
  43. package/package.json +120 -0
  44. package/src/config/MotionConfig.tsx +30 -0
  45. package/src/config/MotionConfigContext.ts +53 -0
  46. package/src/config/index.ts +9 -0
  47. package/src/index.ts +49 -0
  48. package/src/motion/Image.tsx +9 -0
  49. package/src/motion/Pressable.tsx +12 -0
  50. package/src/motion/ScrollView.tsx +9 -0
  51. package/src/motion/Text.tsx +8 -0
  52. package/src/motion/View.tsx +8 -0
  53. package/src/motion/createMotionComponent.tsx +850 -0
  54. package/src/motion/index.ts +26 -0
  55. package/src/presence/Presence.tsx +165 -0
  56. package/src/presence/PresenceContext.ts +28 -0
  57. package/src/presence/index.ts +6 -0
  58. package/src/transitions/easing.ts +29 -0
  59. package/src/transitions/index.ts +2 -0
  60. package/src/transitions/resolve.ts +265 -0
  61. package/src/types.ts +207 -0
  62. package/src/values/index.ts +1 -0
  63. package/src/values/useVariants.ts +60 -0
@@ -0,0 +1,26 @@
1
+ import { MotionImage } from './Image'
2
+ import { MotionPressable } from './Pressable'
3
+ import { MotionScrollView } from './ScrollView'
4
+ import { MotionText } from './Text'
5
+ import { MotionView } from './View'
6
+
7
+ export { createMotionComponent } from './createMotionComponent'
8
+ export {
9
+ MotionView,
10
+ MotionText,
11
+ MotionImage,
12
+ MotionPressable,
13
+ MotionScrollView,
14
+ }
15
+
16
+ /**
17
+ * The `Motion.*` namespace. Each property is a primitive with its style prop
18
+ * inferred from the underlying RN component. There is no shared style fallback.
19
+ */
20
+ export const Motion = {
21
+ View: MotionView,
22
+ Text: MotionText,
23
+ Image: MotionImage,
24
+ Pressable: MotionPressable,
25
+ ScrollView: MotionScrollView,
26
+ } as const
@@ -0,0 +1,165 @@
1
+ import {
2
+ Children,
3
+ isValidElement,
4
+ type Key,
5
+ type ReactElement,
6
+ type ReactNode,
7
+ useCallback,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react'
12
+ import { PresenceContext, type PresenceContextValue } from './PresenceContext'
13
+
14
+ interface RenderEntry {
15
+ key: Key
16
+ element: ReactElement
17
+ isPresent: boolean
18
+ }
19
+
20
+ /**
21
+ * Wrap a list of children with mount / unmount transitions. When a child is
22
+ * removed from the incoming list it stays in the snapshot until its exit
23
+ * animation completes; descendants consume the per-child `<PresenceContext>`
24
+ * to coordinate.
25
+ *
26
+ * Children must be `<Motion.*>` primitives (or any component that consumes
27
+ * `usePresence()` and calls `safeToRemove`). Plain elements without that
28
+ * contract will linger in the snapshot once removed; document that and pick
29
+ * the right primitive.
30
+ *
31
+ * Children also need explicit `key`s so removal is detectable across
32
+ * renders. Without a key, React falls back to positional identity and
33
+ * removal looks like a prop change — Presence has nothing to mark exiting.
34
+ */
35
+ export function Presence({ children }: { children: ReactNode }) {
36
+ const incoming = useMemo(() => {
37
+ const out: ReactElement[] = []
38
+ Children.forEach(children, (child) => {
39
+ if (!isValidElement(child)) return
40
+ if (child.key === null) {
41
+ if (__DEV__) {
42
+ console.warn(
43
+ '[inertia] <Presence> children must have a `key`. Skipping a keyless child.',
44
+ )
45
+ }
46
+ return
47
+ }
48
+ out.push(child)
49
+ })
50
+ return out
51
+ }, [children])
52
+
53
+ // Snapshot of elements removed from `incoming` whose exit animation is
54
+ // still in flight. setExiting is called synchronously during render below
55
+ // (the documented pattern for derived-from-prop-change state), so React
56
+ // re-renders with the new snapshot before committing — no visual frame
57
+ // where the departing child has vanished.
58
+ const [exiting, setExiting] = useState<Map<Key, ReactElement>>(
59
+ () => new Map(),
60
+ )
61
+
62
+ // Tracks the previous render's `incoming` so we can diff. Updated
63
+ // synchronously alongside the setState call.
64
+ const prevIncomingRef = useRef<ReactElement[]>(incoming)
65
+
66
+ if (prevIncomingRef.current !== incoming) {
67
+ const prev = prevIncomingRef.current
68
+ prevIncomingRef.current = incoming
69
+ const incomingKeys = new Set(incoming.map((el) => el.key as Key))
70
+ let next: Map<Key, ReactElement> | null = null
71
+ const ensureMutable = () => {
72
+ if (!next) next = new Map(exiting)
73
+ return next
74
+ }
75
+
76
+ // Departures: in prev but not in current → snapshot for exit.
77
+ for (const oldEl of prev) {
78
+ const key = oldEl.key as Key
79
+ if (!incomingKeys.has(key) && !exiting.has(key)) {
80
+ ensureMutable().set(key, oldEl)
81
+ }
82
+ }
83
+ // Returns: was exiting and reappears → drop the snapshot. The live
84
+ // `incoming` entry takes over with the same key, so React reconciles
85
+ // the underlying Motion instance and the in-flight exit animation
86
+ // interrupts back toward `animate` values.
87
+ for (const el of incoming) {
88
+ const key = el.key as Key
89
+ if (exiting.has(key)) {
90
+ ensureMutable().delete(key)
91
+ }
92
+ }
93
+
94
+ if (next) setExiting(next)
95
+ }
96
+
97
+ const handleRemove = useCallback((key: Key) => {
98
+ setExiting((prev) => {
99
+ if (!prev.has(key)) return prev
100
+ const next = new Map(prev)
101
+ next.delete(key)
102
+ return next
103
+ })
104
+ }, [])
105
+
106
+ // Single combined render list. Putting `incoming` and `exiting` entries in
107
+ // one array (rather than two `.map` calls inside a fragment) ensures React
108
+ // reconciles by `key` across positions — when an entry moves from
109
+ // present-list to exiting-list, the component instance persists.
110
+ const renderList: RenderEntry[] = []
111
+ for (const el of incoming) {
112
+ renderList.push({
113
+ key: el.key as Key,
114
+ element: el,
115
+ isPresent: true,
116
+ })
117
+ }
118
+ for (const [key, el] of exiting) {
119
+ if (!renderList.some((entry) => entry.key === key)) {
120
+ renderList.push({ key, element: el, isPresent: false })
121
+ }
122
+ }
123
+
124
+ return (
125
+ <>
126
+ {renderList.map(({ key, element, isPresent }) => (
127
+ <PresenceItem
128
+ key={key}
129
+ itemKey={key}
130
+ isPresent={isPresent}
131
+ onRemove={handleRemove}
132
+ >
133
+ {element}
134
+ </PresenceItem>
135
+ ))}
136
+ </>
137
+ )
138
+ }
139
+
140
+ function PresenceItem({
141
+ itemKey,
142
+ isPresent,
143
+ onRemove,
144
+ children,
145
+ }: {
146
+ itemKey: Key
147
+ isPresent: boolean
148
+ onRemove: (key: Key) => void
149
+ children: ReactNode
150
+ }) {
151
+ const value = useMemo<PresenceContextValue>(
152
+ () => ({
153
+ isPresent,
154
+ safeToRemove: () => onRemove(itemKey),
155
+ }),
156
+ [isPresent, itemKey, onRemove],
157
+ )
158
+ return (
159
+ <PresenceContext.Provider value={value}>
160
+ {children}
161
+ </PresenceContext.Provider>
162
+ )
163
+ }
164
+
165
+ declare const __DEV__: boolean
@@ -0,0 +1,28 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ /**
4
+ * Per-child contract between `<Presence>` and its descendant Motion
5
+ * primitives. `<Presence>` provides a fresh value to each rendered child;
6
+ * Motion primitives consume it to gate exit animations.
7
+ *
8
+ * - `isPresent`: `true` while the child is in the incoming children list.
9
+ * Flips to `false` when the parent removes it; the child remains rendered
10
+ * until `safeToRemove` is called.
11
+ * - `safeToRemove`: callback the child invokes when its exit animation has
12
+ * settled. `<Presence>` then drops the snapshot entry and unmounts.
13
+ */
14
+ export interface PresenceContextValue {
15
+ isPresent: boolean
16
+ safeToRemove: () => void
17
+ }
18
+
19
+ export const PresenceContext = createContext<PresenceContextValue | null>(null)
20
+
21
+ /**
22
+ * Read the surrounding `<Presence>` contract from a child component. Returns
23
+ * `null` when there is no `<Presence>` ancestor — useful for components that
24
+ * want to support both standalone and Presence-wrapped use without branching.
25
+ */
26
+ export function usePresence(): PresenceContextValue | null {
27
+ return useContext(PresenceContext)
28
+ }
@@ -0,0 +1,6 @@
1
+ export { Presence } from './Presence'
2
+ export {
3
+ PresenceContext,
4
+ usePresence,
5
+ type PresenceContextValue,
6
+ } from './PresenceContext'
@@ -0,0 +1,29 @@
1
+ import { isWorkletFunction } from 'react-native-reanimated'
2
+
3
+ /**
4
+ * Reanimated 3.9+ validates that easing functions used in nested-transition
5
+ * contexts (variants, sequences, per-property maps) are worklets, and crashes
6
+ * with `[Reanimated] The easing function is not a worklet` otherwise. The
7
+ * library accepts plain functions on the public surface; this helper wraps
8
+ * them so consumers don't have to think about the worklet boundary.
9
+ *
10
+ * If the input is already a worklet (has been processed by the worklets babel
11
+ * plugin), it's returned as-is. Otherwise it's wrapped in a function whose
12
+ * body declares the `'worklet'` directive — when our source is processed by
13
+ * the consumer's worklets babel plugin (the default Expo/RN setup), the
14
+ * wrapper becomes a real worklet that captures the user fn via closure.
15
+ *
16
+ * The user fn must be pure: no JS-thread captured refs, no shared mutable
17
+ * state, no calls to non-worklet APIs.
18
+ */
19
+ export function ensureWorkletEasing(
20
+ easing: ((t: number) => number) | undefined,
21
+ ): ((t: number) => number) | undefined {
22
+ if (!easing) return undefined
23
+ if (isWorkletFunction(easing)) return easing
24
+ const wrapped = (t: number) => {
25
+ 'worklet'
26
+ return easing(t)
27
+ }
28
+ return wrapped
29
+ }
@@ -0,0 +1,2 @@
1
+ export { resolveTransition, resolveAnimatableValue } from './resolve'
2
+ export { ensureWorkletEasing } from './easing'
@@ -0,0 +1,265 @@
1
+ import {
2
+ Easing,
3
+ withDecay,
4
+ withDelay,
5
+ withRepeat,
6
+ withSequence,
7
+ withSpring,
8
+ withTiming,
9
+ } from 'react-native-reanimated'
10
+ import { ensureWorkletEasing } from './easing'
11
+ import {
12
+ type AnimatableValue,
13
+ type DecayTransition,
14
+ type RepeatConfig,
15
+ type SequenceStep,
16
+ type SpringTransition,
17
+ type TimingTransition,
18
+ type TransitionConfig,
19
+ } from '../types'
20
+
21
+ /**
22
+ * UI-thread callback Reanimated invokes when an animation settles. Must be a
23
+ * worklet — callers either author one with `'worklet'` or build one via
24
+ * `runOnJS(...)` to bridge to JS-thread code.
25
+ */
26
+ export type AnimationCallback = (
27
+ finished?: boolean,
28
+ current?: number | string,
29
+ ) => void
30
+
31
+ /**
32
+ * Per-step callback factory. Resolvers call this with the step's phase and
33
+ * sequence index (or `undefined` for non-sequence animations) and attach the
34
+ * resulting callback to the underlying `withSpring` / `withTiming` /
35
+ * `withDecay` call.
36
+ */
37
+ export type CallbackFactory = (
38
+ phase: 'step' | 'animation',
39
+ step: number | undefined,
40
+ ) => AnimationCallback | undefined
41
+
42
+ /**
43
+ * Default spring physics, expressed in react-spring vocabulary. Conversion
44
+ * to Reanimated's raw `stiffness` / `damping` lives below; raw config never
45
+ * leaks past this module.
46
+ */
47
+ const DEFAULT_SPRING: Required<
48
+ Pick<SpringTransition, 'tension' | 'friction' | 'mass'>
49
+ > = {
50
+ tension: 170,
51
+ friction: 26,
52
+ mass: 1,
53
+ }
54
+
55
+ const DEFAULT_TIMING_DURATION = 250
56
+
57
+ function springToReanimated(t: SpringTransition) {
58
+ return {
59
+ stiffness: t.tension ?? DEFAULT_SPRING.tension,
60
+ damping: t.friction ?? DEFAULT_SPRING.friction,
61
+ mass: t.mass ?? DEFAULT_SPRING.mass,
62
+ velocity: t.velocity,
63
+ restSpeedThreshold: t.restSpeedThreshold,
64
+ restDisplacementThreshold: t.restDisplacementThreshold,
65
+ }
66
+ }
67
+
68
+ function buildSpring(
69
+ cfg: SpringTransition,
70
+ toValue: number | string,
71
+ cb?: AnimationCallback,
72
+ ) {
73
+ return withSpring(toValue as number, springToReanimated(cfg), cb as never)
74
+ }
75
+
76
+ function buildTiming(
77
+ cfg: TimingTransition,
78
+ toValue: number | string,
79
+ cb?: AnimationCallback,
80
+ ) {
81
+ return withTiming(
82
+ toValue as number,
83
+ {
84
+ duration: cfg.duration ?? DEFAULT_TIMING_DURATION,
85
+ easing: ensureWorkletEasing(cfg.easing) ?? Easing.inOut(Easing.ease),
86
+ },
87
+ cb as never,
88
+ )
89
+ }
90
+
91
+ function buildDecay(cfg: DecayTransition, cb?: AnimationCallback) {
92
+ return withDecay(
93
+ {
94
+ velocity: cfg.velocity ?? 0,
95
+ deceleration: cfg.deceleration,
96
+ clamp: cfg.clamp,
97
+ },
98
+ cb as never,
99
+ )
100
+ }
101
+
102
+ /**
103
+ * Build a single-step animation (no repeat / no delay / no sequence) for a
104
+ * given config + target. Pulled out so sequence steps can compose without
105
+ * recursing into repeat/delay handling per step. The callback is forwarded
106
+ * to Reanimated; for `no-animation` the callback is fired synchronously
107
+ * since there's nothing to wait for.
108
+ */
109
+ function buildOne(
110
+ cfg: TransitionConfig,
111
+ toValue: number | string,
112
+ cb?: AnimationCallback,
113
+ ): unknown {
114
+ if (cfg.type === 'no-animation') {
115
+ if (cb) cb(true, toValue)
116
+ return toValue
117
+ }
118
+ if (cfg.type === 'decay') return buildDecay(cfg, cb)
119
+ if (cfg.type === 'timing') return buildTiming(cfg, toValue, cb)
120
+ return buildSpring(cfg as SpringTransition, toValue, cb)
121
+ }
122
+
123
+ /**
124
+ * Wrap an animation in `withRepeat` per the unified `repeat` shape:
125
+ * - `number` → finite count, alternating direction
126
+ * - `'infinite'` → endless, alternating direction
127
+ * - `{ count, alternate }`→ explicit; `alternate` defaults to `true`
128
+ */
129
+ function applyRepeat(animation: unknown, repeat: RepeatConfig | undefined) {
130
+ if (repeat === undefined) return animation
131
+ if (repeat === 'infinite') {
132
+ return withRepeat(animation as never, -1, true)
133
+ }
134
+ if (typeof repeat === 'number') {
135
+ return withRepeat(animation as never, repeat, true)
136
+ }
137
+ const count = repeat.count === 'infinite' ? -1 : repeat.count
138
+ const alternate = repeat.alternate ?? true
139
+ return withRepeat(animation as never, count, alternate)
140
+ }
141
+
142
+ function applyDelay(animation: unknown, delay: number | undefined) {
143
+ if (!delay || delay <= 0) return animation
144
+ return withDelay(delay, animation as never)
145
+ }
146
+
147
+ /**
148
+ * Build a Reanimated animation for a single property. Runs on the JS thread
149
+ * once per change and produces a baked `withSpring` / `withTiming` /
150
+ * `withDecay` (optionally wrapped in `withDelay` / `withRepeat`) call. The
151
+ * worklet body only consumes the result.
152
+ *
153
+ * `callback`, when provided, fires once when the underlying single-shot
154
+ * animation settles. Repeat-wrapped animations forward the callback to
155
+ * `withRepeat`, so it fires once per iteration as Reanimated does.
156
+ */
157
+ export function resolveTransition(
158
+ config: TransitionConfig | undefined,
159
+ toValue: number | string,
160
+ callback?: AnimationCallback,
161
+ ): unknown {
162
+ const cfg = config ?? ({ type: 'spring' } as SpringTransition)
163
+ const base = buildOne(cfg, toValue, callback)
164
+ const repeated = applyRepeat(base, repeatOf(cfg))
165
+ return applyDelay(repeated, delayOf(cfg))
166
+ }
167
+
168
+ function repeatOf(cfg: TransitionConfig): RepeatConfig | undefined {
169
+ if (cfg.type === 'no-animation' || cfg.type === 'decay') return undefined
170
+ return cfg.repeat
171
+ }
172
+
173
+ /**
174
+ * Return `cfg` minus its `repeat` field. Used when peeling top-level repeat
175
+ * off a base transition before passing it down to per-sequence-step
176
+ * resolution — the sequence as a whole is what should repeat, not each step.
177
+ */
178
+ function stripRepeat(
179
+ cfg: TransitionConfig | undefined,
180
+ ): TransitionConfig | undefined {
181
+ if (!cfg) return cfg
182
+ if (cfg.type === 'no-animation' || cfg.type === 'decay') return cfg
183
+ if (cfg.repeat === undefined) return cfg
184
+ const next = { ...cfg }
185
+ delete next.repeat
186
+ return next
187
+ }
188
+
189
+ function delayOf(cfg: TransitionConfig): number | undefined {
190
+ if (cfg.type === 'no-animation') return undefined
191
+ return cfg.delay
192
+ }
193
+
194
+ /**
195
+ * True when the value is a `{ to, ...transitionOverride }` sequence step.
196
+ * Plain numbers and plain transition objects fail this check.
197
+ */
198
+ function isStepObject<V>(
199
+ v: SequenceStep<V> | V,
200
+ ): v is Extract<SequenceStep<V>, { to: V }> {
201
+ return (
202
+ typeof v === 'object' &&
203
+ v !== null &&
204
+ !Array.isArray(v) &&
205
+ 'to' in (v as object)
206
+ )
207
+ }
208
+
209
+ /**
210
+ * Resolve a per-property `animate` value into a Reanimated animation.
211
+ *
212
+ * Handles the three shapes of `AnimatableValue`:
213
+ * 1. plain value → single `resolveTransition` call
214
+ * 2. `{ to, ...over }` → single step with the override merged into `base`
215
+ * 3. array of either → `withSequence` of resolved steps, with the
216
+ * top-level `repeat` applied at the **sequence level** (not per step).
217
+ * Per-step `repeat` overrides remain step-local.
218
+ */
219
+ export function resolveAnimatableValue<V extends number | string>(
220
+ value: AnimatableValue<V>,
221
+ base: TransitionConfig | undefined,
222
+ factory?: CallbackFactory,
223
+ ): unknown {
224
+ if (Array.isArray(value)) {
225
+ const steps = value as ReadonlyArray<SequenceStep<V>>
226
+ const stepBase = stripRepeat(base)
227
+ const animations = steps.map((step, i) =>
228
+ resolveStep(step, stepBase, factory?.('step', i)),
229
+ )
230
+ const seq = withSequence(...(animations as never[]))
231
+ return applyRepeat(seq, base ? repeatOf(base) : undefined)
232
+ }
233
+ const step = value as SequenceStep<V>
234
+ const cb = factory?.('animation', undefined)
235
+ if (isStepObject<V>(step)) {
236
+ return resolveStep(step, base, cb)
237
+ }
238
+ return resolveTransition(base, step as V, cb)
239
+ }
240
+
241
+ function resolveStep<V extends number | string>(
242
+ step: SequenceStep<V>,
243
+ base: TransitionConfig | undefined,
244
+ cb?: AnimationCallback,
245
+ ): unknown {
246
+ if (isStepObject<V>(step)) {
247
+ const { to, ...override } = step as { to: V } & Partial<TransitionConfig>
248
+ const merged = mergeTransition(base, override as Partial<TransitionConfig>)
249
+ return resolveTransition(merged, to, cb)
250
+ }
251
+ return resolveTransition(base, step as V, cb)
252
+ }
253
+
254
+ function mergeTransition(
255
+ base: TransitionConfig | undefined,
256
+ override: Partial<TransitionConfig>,
257
+ ): TransitionConfig {
258
+ // If the override declares a `type`, it wins outright — mixing fields from
259
+ // a spring base into a timing override produces garbage. Otherwise inherit
260
+ // the base's type and shallow-merge the rest.
261
+ if (override.type && base && override.type !== base.type) {
262
+ return override as TransitionConfig
263
+ }
264
+ return { ...(base ?? { type: 'spring' }), ...override } as TransitionConfig
265
+ }