@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,53 @@
1
+ // `isWorkletFunction` lives in `react-native-worklets` (the Reanimated 4 peer
2
+ // dep); Reanimated's own re-export is deprecated.
3
+ import { isWorkletFunction } from 'react-native-worklets'
4
+ import { type EasingInput } from '../types'
5
+
6
+ /**
7
+ * Reanimated 3.9+ validates that easing functions used in nested-transition
8
+ * contexts (variants, sequences, per-property maps) are worklets, and crashes
9
+ * with `[Reanimated] The easing function is not a worklet` otherwise. The
10
+ * library accepts plain functions on the public surface; this helper wraps
11
+ * them so consumers don't have to think about the worklet boundary.
12
+ *
13
+ * If the input is already a worklet (has been processed by the worklets babel
14
+ * plugin), it's returned as-is. Otherwise it's wrapped in a function whose
15
+ * body declares the `'worklet'` directive — when our source is processed by
16
+ * the consumer's worklets babel plugin (the default Expo/RN setup), the
17
+ * wrapper becomes a real worklet that captures the user fn via closure.
18
+ *
19
+ * Reanimated 4 changed `Easing.bezier(...)` to return an
20
+ * `EasingFunctionFactory` (`{ factory: () => EasingFunction }`) rather than
21
+ * the function itself. The helper accepts both shapes — `EasingFunction` and
22
+ * `EasingFunctionFactory` — and unwraps the factory automatically so
23
+ * consumers don't have to call `.factory()` manually.
24
+ *
25
+ * The user fn must be pure: no JS-thread captured refs, no shared mutable
26
+ * state, no calls to non-worklet APIs.
27
+ */
28
+ export function ensureWorkletEasing(
29
+ easing: EasingInput | undefined,
30
+ ): ((t: number) => number) | undefined {
31
+ if (!easing) return undefined
32
+ // Reanimated 4 `EasingFunctionFactory` — unwrap via `.factory()` before
33
+ // checking worklet status, so the wrapped fn (not the factory wrapper)
34
+ // ends up in the transition config.
35
+ const fn = isEasingFactory(easing) ? easing.factory() : easing
36
+ if (isWorkletFunction(fn)) return fn
37
+ const wrapped = (t: number) => {
38
+ 'worklet'
39
+ return fn(t)
40
+ }
41
+ return wrapped
42
+ }
43
+
44
+ function isEasingFactory(
45
+ value: EasingInput,
46
+ ): value is { factory: () => (t: number) => number } {
47
+ return (
48
+ typeof value === 'object' &&
49
+ value !== null &&
50
+ 'factory' in value &&
51
+ typeof (value as { factory: unknown }).factory === 'function'
52
+ )
53
+ }
@@ -0,0 +1,7 @@
1
+ export { resolveTransition } from './resolve'
2
+ export { resolveAnimatableValue } from './resolveSequence'
3
+ export { ensureWorkletEasing } from './easing'
4
+ export { isTopLevelTransition, TRANSITION_CONFIG_KEYS } from './keys'
5
+ export { buildReleaseAnimation } from './runtime'
6
+ export { stableSig } from './sig'
7
+ export { DEFAULT_SPRING, springToReanimated } from './spring'
@@ -0,0 +1,32 @@
1
+ import { type TransitionConfig } from '../types'
2
+
3
+ /**
4
+ * Field names that may appear on a `TransitionConfig` (spring / timing /
5
+ * decay / no-animation). Used as a structural discriminator: if every key on
6
+ * an object is in this set, the object is treated as a top-level transition;
7
+ * otherwise it's a per-property / per-layer transition map.
8
+ *
9
+ * Adding a new field to `TransitionConfig` requires adding the name here.
10
+ */
11
+ export const TRANSITION_CONFIG_KEYS = new Set([
12
+ 'type',
13
+ 'tension',
14
+ 'friction',
15
+ 'mass',
16
+ 'velocity',
17
+ 'restSpeedThreshold',
18
+ 'restDisplacementThreshold',
19
+ 'duration',
20
+ 'easing',
21
+ 'delay',
22
+ 'repeat',
23
+ 'deceleration',
24
+ 'clamp',
25
+ ])
26
+
27
+ export function isTopLevelTransition(t: unknown): t is TransitionConfig {
28
+ if (t === null || typeof t !== 'object') return false
29
+ const keys = Object.keys(t as object)
30
+ if (keys.length === 0) return false
31
+ return keys.every((k) => TRANSITION_CONFIG_KEYS.has(k))
32
+ }
@@ -0,0 +1,169 @@
1
+ import {
2
+ Easing,
3
+ withDecay,
4
+ withDelay,
5
+ withRepeat,
6
+ withSpring,
7
+ withTiming,
8
+ } from 'react-native-reanimated'
9
+ import { ensureWorkletEasing } from './easing'
10
+ import { springToReanimated } from './spring'
11
+ import {
12
+ type DecayTransition,
13
+ type RepeatConfig,
14
+ type SpringTransition,
15
+ type TimingTransition,
16
+ type TransitionConfig,
17
+ } from '../types'
18
+
19
+ /**
20
+ * UI-thread callback Reanimated invokes when an animation settles. Must be a
21
+ * worklet — callers either author one with `'worklet'` or build one via
22
+ * `runOnJS(...)` to bridge to JS-thread code.
23
+ */
24
+ export type AnimationCallback = (
25
+ finished?: boolean,
26
+ current?: number | string,
27
+ ) => void
28
+
29
+ /**
30
+ * Per-step callback factory. Resolvers call this with the step's phase and
31
+ * sequence index (or `undefined` for non-sequence animations) and attach the
32
+ * resulting callback to the underlying `withSpring` / `withTiming` /
33
+ * `withDecay` call.
34
+ */
35
+ export type CallbackFactory = (
36
+ phase: 'step' | 'animation',
37
+ step: number | undefined,
38
+ ) => AnimationCallback | undefined
39
+
40
+ const DEFAULT_TIMING_DURATION = 250
41
+
42
+ function buildSpring(
43
+ cfg: SpringTransition,
44
+ toValue: number | string,
45
+ cb?: AnimationCallback,
46
+ ) {
47
+ return withSpring(toValue as number, springToReanimated(cfg), cb as never)
48
+ }
49
+
50
+ function buildTiming(
51
+ cfg: TimingTransition,
52
+ toValue: number | string,
53
+ cb?: AnimationCallback,
54
+ ) {
55
+ return withTiming(
56
+ toValue as number,
57
+ {
58
+ duration: cfg.duration ?? DEFAULT_TIMING_DURATION,
59
+ easing: ensureWorkletEasing(cfg.easing) ?? Easing.inOut(Easing.ease),
60
+ },
61
+ cb as never,
62
+ )
63
+ }
64
+
65
+ function buildDecay(cfg: DecayTransition, cb?: AnimationCallback) {
66
+ return withDecay(
67
+ {
68
+ velocity: cfg.velocity ?? 0,
69
+ deceleration: cfg.deceleration,
70
+ clamp: cfg.clamp,
71
+ },
72
+ cb as never,
73
+ )
74
+ }
75
+
76
+ /**
77
+ * Build a single-step animation (no repeat / no delay / no sequence) for a
78
+ * given config + target. Pulled out so sequence steps can compose without
79
+ * recursing into repeat/delay handling per step. The callback is forwarded
80
+ * to Reanimated; for `no-animation` the callback is fired synchronously
81
+ * since there's nothing to wait for.
82
+ */
83
+ function buildOne(
84
+ cfg: TransitionConfig,
85
+ toValue: number | string,
86
+ cb?: AnimationCallback,
87
+ ): unknown {
88
+ if (cfg.type === 'no-animation') {
89
+ if (cb) cb(true, toValue)
90
+ return toValue
91
+ }
92
+ if (cfg.type === 'decay') return buildDecay(cfg, cb)
93
+ if (cfg.type === 'timing') return buildTiming(cfg, toValue, cb)
94
+ return buildSpring(cfg as SpringTransition, toValue, cb)
95
+ }
96
+
97
+ /**
98
+ * Wrap an animation in `withRepeat` per the unified `repeat` shape:
99
+ * - `number` → finite count, alternating direction
100
+ * - `'infinite'` → endless, alternating direction
101
+ * - `{ count, alternate }`→ explicit; `alternate` defaults to `true`
102
+ */
103
+ export function applyRepeat(
104
+ animation: unknown,
105
+ repeat: RepeatConfig | undefined,
106
+ ) {
107
+ if (repeat === undefined) return animation
108
+ if (repeat === 'infinite') {
109
+ return withRepeat(animation as never, -1, true)
110
+ }
111
+ if (typeof repeat === 'number') {
112
+ return withRepeat(animation as never, repeat, true)
113
+ }
114
+ const count = repeat.count === 'infinite' ? -1 : repeat.count
115
+ const alternate = repeat.alternate ?? true
116
+ return withRepeat(animation as never, count, alternate)
117
+ }
118
+
119
+ function applyDelay(animation: unknown, delay: number | undefined) {
120
+ if (!delay || delay <= 0) return animation
121
+ return withDelay(delay, animation as never)
122
+ }
123
+
124
+ /**
125
+ * Build a Reanimated animation for a single property. Runs on the JS thread
126
+ * once per change and produces a baked `withSpring` / `withTiming` /
127
+ * `withDecay` (optionally wrapped in `withDelay` / `withRepeat`) call. The
128
+ * worklet body only consumes the result.
129
+ *
130
+ * `callback`, when provided, fires once when the underlying single-shot
131
+ * animation settles. Repeat-wrapped animations forward the callback to
132
+ * `withRepeat`, so it fires once per iteration as Reanimated does.
133
+ */
134
+ export function resolveTransition(
135
+ config: TransitionConfig | undefined,
136
+ toValue: number | string,
137
+ callback?: AnimationCallback,
138
+ ): unknown {
139
+ const cfg = config ?? ({ type: 'spring' } as SpringTransition)
140
+ const base = buildOne(cfg, toValue, callback)
141
+ const repeated = applyRepeat(base, repeatOf(cfg))
142
+ return applyDelay(repeated, delayOf(cfg))
143
+ }
144
+
145
+ export function repeatOf(cfg: TransitionConfig): RepeatConfig | undefined {
146
+ if (cfg.type === 'no-animation' || cfg.type === 'decay') return undefined
147
+ return cfg.repeat
148
+ }
149
+
150
+ /**
151
+ * Return `cfg` minus its `repeat` field. Used when peeling top-level repeat
152
+ * off a base transition before passing it down to per-sequence-step
153
+ * resolution — the sequence as a whole is what should repeat, not each step.
154
+ */
155
+ export function stripRepeat(
156
+ cfg: TransitionConfig | undefined,
157
+ ): TransitionConfig | undefined {
158
+ if (!cfg) return cfg
159
+ if (cfg.type === 'no-animation' || cfg.type === 'decay') return cfg
160
+ if (cfg.repeat === undefined) return cfg
161
+ const next = { ...cfg }
162
+ delete next.repeat
163
+ return next
164
+ }
165
+
166
+ function delayOf(cfg: TransitionConfig): number | undefined {
167
+ if (cfg.type === 'no-animation') return undefined
168
+ return cfg.delay
169
+ }
@@ -0,0 +1,87 @@
1
+ import { withSequence } from 'react-native-reanimated'
2
+ import {
3
+ applyRepeat,
4
+ repeatOf,
5
+ resolveTransition,
6
+ stripRepeat,
7
+ type AnimationCallback,
8
+ type CallbackFactory,
9
+ } from './resolve'
10
+ import {
11
+ type AnimatableValue,
12
+ type SequenceStep,
13
+ type TransitionConfig,
14
+ } from '../types'
15
+
16
+ /**
17
+ * True when the value is a `{ to, ...transitionOverride }` sequence step.
18
+ * Plain numbers and plain transition objects fail this check.
19
+ */
20
+ function isStepObject<V>(
21
+ v: SequenceStep<V> | V,
22
+ ): v is Extract<SequenceStep<V>, { to: V }> {
23
+ return (
24
+ typeof v === 'object' &&
25
+ v !== null &&
26
+ !Array.isArray(v) &&
27
+ 'to' in (v as object)
28
+ )
29
+ }
30
+
31
+ /**
32
+ * Resolve a per-property `animate` value into a Reanimated animation.
33
+ *
34
+ * Handles the three shapes of `AnimatableValue`:
35
+ * 1. plain value → single `resolveTransition` call
36
+ * 2. `{ to, ...over }` → single step with the override merged into `base`
37
+ * 3. array of either → `withSequence` of resolved steps, with the
38
+ * top-level `repeat` applied at the **sequence level** (not per step).
39
+ * Per-step `repeat` overrides remain step-local.
40
+ */
41
+ export function resolveAnimatableValue<V extends number | string>(
42
+ value: AnimatableValue<V>,
43
+ base: TransitionConfig | undefined,
44
+ factory?: CallbackFactory,
45
+ ): unknown {
46
+ if (Array.isArray(value)) {
47
+ const steps = value as ReadonlyArray<SequenceStep<V>>
48
+ const stepBase = stripRepeat(base)
49
+ const animations = steps.map((step, i) =>
50
+ resolveStep(step, stepBase, factory?.('step', i)),
51
+ )
52
+ const seq = withSequence(...(animations as never[]))
53
+ return applyRepeat(seq, base ? repeatOf(base) : undefined)
54
+ }
55
+ const step = value as SequenceStep<V>
56
+ const cb = factory?.('animation', undefined)
57
+ if (isStepObject<V>(step)) {
58
+ return resolveStep(step, base, cb)
59
+ }
60
+ return resolveTransition(base, step as V, cb)
61
+ }
62
+
63
+ function resolveStep<V extends number | string>(
64
+ step: SequenceStep<V>,
65
+ base: TransitionConfig | undefined,
66
+ cb?: AnimationCallback,
67
+ ): unknown {
68
+ if (isStepObject<V>(step)) {
69
+ const { to, ...override } = step as { to: V } & Partial<TransitionConfig>
70
+ const merged = mergeTransition(base, override as Partial<TransitionConfig>)
71
+ return resolveTransition(merged, to, cb)
72
+ }
73
+ return resolveTransition(base, step as V, cb)
74
+ }
75
+
76
+ function mergeTransition(
77
+ base: TransitionConfig | undefined,
78
+ override: Partial<TransitionConfig>,
79
+ ): TransitionConfig {
80
+ // If the override declares a `type`, it wins outright — mixing fields from
81
+ // a spring base into a timing override produces garbage. Otherwise inherit
82
+ // the base's type and shallow-merge the rest.
83
+ if (override.type && base && override.type !== base.type) {
84
+ return override as TransitionConfig
85
+ }
86
+ return { ...(base ?? { type: 'spring' }), ...override } as TransitionConfig
87
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ Easing,
3
+ withDecay,
4
+ withSpring,
5
+ withTiming,
6
+ } from 'react-native-reanimated'
7
+ import { springToReanimated } from './spring'
8
+ import { type TransitionConfig } from '../types'
9
+
10
+ const DEFAULT_TIMING_DURATION = 250
11
+
12
+ /**
13
+ * Worklet-safe single-step animation builder. Mirrors a subset of
14
+ * `resolveTransition` for the UI-thread path where the transition config is
15
+ * picked at gesture-release time, not at render time.
16
+ *
17
+ * Supported: spring / timing / decay / no-animation, single-step only.
18
+ * Not supported: sequences, top-level repeat, easing-function
19
+ * auto-worklet-wrapping (pass an already-worklet easing if you need a custom
20
+ * one — most release transitions don't).
21
+ *
22
+ * Use this from gesture worklets (`useDrag` / `usePan` release callbacks, or
23
+ * any custom `Gesture.Pan().onEnd(() => ...)` worklet) to animate a shared
24
+ * value with an Inertia transition without the JS round-trip that would lose
25
+ * the release velocity.
26
+ *
27
+ * For decay transitions, `toValue` is ignored — decay decelerates from the
28
+ * SV's current position via its own physics. Pass `0` if you don't have one.
29
+ */
30
+ export function buildReleaseAnimation(
31
+ transition: TransitionConfig,
32
+ toValue: number,
33
+ ): unknown {
34
+ 'worklet'
35
+ if (transition.type === 'no-animation') return toValue
36
+ if (transition.type === 'decay') {
37
+ const cfg: {
38
+ velocity: number
39
+ deceleration?: number
40
+ clamp?: [number, number]
41
+ } = { velocity: transition.velocity ?? 0 }
42
+ if (transition.deceleration !== undefined) {
43
+ cfg.deceleration = transition.deceleration
44
+ }
45
+ if (transition.clamp !== undefined) cfg.clamp = transition.clamp
46
+ return withDecay(cfg)
47
+ }
48
+ if (transition.type === 'timing') {
49
+ // Reanimated 4's `Easing.bezier(...)` returns an `EasingFunctionFactory`
50
+ // rather than the function itself. Unwrap inline so consumers calling
51
+ // `buildReleaseAnimation` from a gesture worklet don't have to.
52
+ const e = transition.easing
53
+ const easingFn =
54
+ e && typeof e === 'object' && 'factory' in e
55
+ ? e.factory()
56
+ : (e ?? Easing.inOut(Easing.ease))
57
+ return withTiming(toValue, {
58
+ duration: transition.duration ?? DEFAULT_TIMING_DURATION,
59
+ easing: easingFn,
60
+ })
61
+ }
62
+ return withSpring(toValue, springToReanimated(transition))
63
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Stable string signature for an arbitrary value — used as a dep-array
3
+ * member so a fresh object literal each render doesn't re-fire an effect
4
+ * unless something structurally changed. Functions serialize as `null`
5
+ * (their identity isn't useful in a sig); `undefined` collapses to an empty
6
+ * string so omitted props compare equal across renders.
7
+ */
8
+ export function stableSig(value: unknown): string {
9
+ if (value === undefined) return ''
10
+ try {
11
+ return stableStringify(value)
12
+ } catch {
13
+ return String(value)
14
+ }
15
+ }
16
+
17
+ /**
18
+ * JSON.stringify with keys sorted at every level so a sig is invariant under
19
+ * property-declaration order. Functions and `undefined` both serialize as
20
+ * `null` — we accept the latter's information loss (rare in practice) in
21
+ * exchange for not crashing on circular function-bearing graphs.
22
+ */
23
+ function stableStringify(v: unknown): string {
24
+ if (v === null || typeof v !== 'object') {
25
+ if (typeof v === 'function' || v === undefined) return 'null'
26
+ return JSON.stringify(v)
27
+ }
28
+ if (Array.isArray(v)) {
29
+ return '[' + v.map(stableStringify).join(',') + ']'
30
+ }
31
+ const obj = v as Record<string, unknown>
32
+ const keys = Object.keys(obj).sort()
33
+ return (
34
+ '{' +
35
+ keys
36
+ .map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]))
37
+ .join(',') +
38
+ '}'
39
+ )
40
+ }
@@ -0,0 +1,42 @@
1
+ import { type SpringTransition } from '../types'
2
+
3
+ /**
4
+ * Default spring physics, expressed in react-spring vocabulary.
5
+ *
6
+ * `tension: 170` / `friction: 26` / `mass: 1` was picked over Reanimated's
7
+ * raw `stiffness: 100` / `damping: 10` default because the raw default
8
+ * overshoots noticeably for the small (~100px) translates that dominate
9
+ * UI work — buttons, sheets, popovers. These numbers settle in ~350ms with
10
+ * a single, almost-imperceptible overshoot, which matches the perceptual
11
+ * target the rest of the library is tuned against.
12
+ */
13
+ export const DEFAULT_SPRING: Required<
14
+ Pick<SpringTransition, 'tension' | 'friction' | 'mass'>
15
+ > = {
16
+ tension: 170,
17
+ friction: 26,
18
+ mass: 1,
19
+ }
20
+
21
+ /**
22
+ * Convert public react-spring vocabulary (`tension` / `friction` / `mass`)
23
+ * to Reanimated's raw `stiffness` / `damping` / `mass`. This is the single
24
+ * place the mapping lives; resolvers, value hooks, and any future surface
25
+ * that needs a Reanimated spring config import from here.
26
+ *
27
+ * The mapping is identity (tension ≡ stiffness, friction ≡ damping) — the
28
+ * names differ but the underlying physics constants are the same. We don't
29
+ * surface the raw names publicly because the react-spring vocabulary is
30
+ * what designers and prior-art consumers expect.
31
+ */
32
+ export function springToReanimated(t: SpringTransition) {
33
+ 'worklet'
34
+ return {
35
+ stiffness: t.tension ?? DEFAULT_SPRING.tension,
36
+ damping: t.friction ?? DEFAULT_SPRING.friction,
37
+ mass: t.mass ?? DEFAULT_SPRING.mass,
38
+ velocity: t.velocity,
39
+ restSpeedThreshold: t.restSpeedThreshold,
40
+ restDisplacementThreshold: t.restDisplacementThreshold,
41
+ }
42
+ }