@rootnative/inertia 0.0.1 → 0.0.3

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 (44) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/dist/{chunk-TDSO63CJ.js → chunk-3XTVY34H.js} +2 -2
  3. package/dist/{chunk-NXDJZD6A.mjs → chunk-46P57VMY.mjs} +1 -1
  4. package/dist/{chunk-7UDYEFBU.js → chunk-BP3Y2SHQ.js} +477 -298
  5. package/dist/{chunk-6SMPIOIC.mjs → chunk-BQQTHG2V.mjs} +1 -1
  6. package/dist/{chunk-DWCLIBYO.mjs → chunk-CSODMRJ7.mjs} +478 -299
  7. package/dist/chunk-FNVFV4EY.js +8 -0
  8. package/dist/chunk-FWQOXA43.js +8 -0
  9. package/dist/chunk-KBP4LR75.js +8 -0
  10. package/dist/{chunk-ALRHDFZE.mjs → chunk-O22NXXCZ.mjs} +1 -1
  11. package/dist/{chunk-CWLFUYIY.mjs → chunk-OQV66TBQ.mjs} +1 -1
  12. package/dist/{chunk-JVBXPF2G.mjs → chunk-SGUHE5CX.mjs} +1 -1
  13. package/dist/{chunk-2HYD2ZBK.js → chunk-W5MC3P4N.js} +2 -2
  14. package/dist/index.d.mts +231 -43
  15. package/dist/index.d.ts +231 -43
  16. package/dist/index.js +212 -23
  17. package/dist/index.mjs +204 -18
  18. package/dist/motion/Image.js +3 -3
  19. package/dist/motion/Image.mjs +2 -2
  20. package/dist/motion/Pressable.js +3 -3
  21. package/dist/motion/Pressable.mjs +2 -2
  22. package/dist/motion/ScrollView.js +3 -3
  23. package/dist/motion/ScrollView.mjs +2 -2
  24. package/dist/motion/Text.js +3 -3
  25. package/dist/motion/Text.mjs +2 -2
  26. package/dist/motion/View.js +3 -3
  27. package/dist/motion/View.mjs +2 -2
  28. package/llms.txt +5 -0
  29. package/package.json +1 -1
  30. package/src/index.ts +10 -0
  31. package/src/layout/index.ts +1 -0
  32. package/src/layout/sharedRegistry.ts +51 -2
  33. package/src/motion/createMotionComponent.tsx +781 -502
  34. package/src/presence/Presence.tsx +73 -10
  35. package/src/values/index.ts +13 -0
  36. package/src/values/useAnimation.ts +15 -1
  37. package/src/values/useAnimator.ts +83 -0
  38. package/src/values/useColorCascade.ts +125 -0
  39. package/src/values/useInterpolatedStyle.ts +319 -0
  40. package/src/values/useMotionValue.ts +20 -2
  41. package/src/values/useSpring.ts +12 -0
  42. package/dist/chunk-3UTJJ4A3.js +0 -8
  43. package/dist/chunk-4QGXK6TF.js +0 -8
  44. package/dist/chunk-Z7HIOFKQ.js +0 -8
package/dist/index.mjs CHANGED
@@ -1,21 +1,21 @@
1
1
  export { useGesture } from './chunk-RGNX6UZN.mjs';
2
- import { MotionView } from './chunk-6SMPIOIC.mjs';
3
- export { MotionView } from './chunk-6SMPIOIC.mjs';
4
- import { MotionText } from './chunk-JVBXPF2G.mjs';
5
- export { MotionText } from './chunk-JVBXPF2G.mjs';
6
- import { MotionImage } from './chunk-ALRHDFZE.mjs';
7
- export { MotionImage } from './chunk-ALRHDFZE.mjs';
8
- import { MotionPressable } from './chunk-NXDJZD6A.mjs';
9
- export { MotionPressable } from './chunk-NXDJZD6A.mjs';
10
- import { MotionScrollView } from './chunk-CWLFUYIY.mjs';
11
- export { MotionScrollView } from './chunk-CWLFUYIY.mjs';
12
- export { Presence, createMotionComponent, usePresence } from './chunk-DWCLIBYO.mjs';
2
+ import { MotionView } from './chunk-BQQTHG2V.mjs';
3
+ export { MotionView } from './chunk-BQQTHG2V.mjs';
4
+ import { MotionText } from './chunk-SGUHE5CX.mjs';
5
+ export { MotionText } from './chunk-SGUHE5CX.mjs';
6
+ import { MotionImage } from './chunk-O22NXXCZ.mjs';
7
+ export { MotionImage } from './chunk-O22NXXCZ.mjs';
8
+ import { MotionPressable } from './chunk-46P57VMY.mjs';
9
+ export { MotionPressable } from './chunk-46P57VMY.mjs';
10
+ import { MotionScrollView } from './chunk-OQV66TBQ.mjs';
11
+ export { MotionScrollView } from './chunk-OQV66TBQ.mjs';
12
+ export { Presence, createMotionComponent, usePresence } from './chunk-CSODMRJ7.mjs';
13
13
  import { useShouldReduceMotion, resolveNamedTransition, useNamedTransitions, lookupNamedTransition } from './chunk-R63GIUNU.mjs';
14
14
  export { MotionConfig, resolveNamedTransition, useMotionConfig, useNamedTransitions, useShouldReduceMotion } from './chunk-R63GIUNU.mjs';
15
15
  import { stableSig, resolveTransition, springToReanimated, warnNonWorkletOnce } from './chunk-CY7Y64C3.mjs';
16
16
  export { buildReleaseAnimation, cubicBezier, ensureWorkletEasing, resolveAnimatableValue, resolveTransition } from './chunk-CY7Y64C3.mjs';
17
- import { useEffect, useMemo, useRef } from 'react';
18
- import { useSharedValue, withSpring, useAnimatedReaction, useAnimatedStyle, interpolateColor, useDerivedValue, Extrapolation, useAnimatedScrollHandler, interpolate } from 'react-native-reanimated';
17
+ import { useEffect, useRef, useCallback, useMemo } from 'react';
18
+ import { useSharedValue, cancelAnimation, withSpring, useAnimatedReaction, useAnimatedStyle, interpolateColor, interpolate, useDerivedValue, Extrapolation, useAnimatedScrollHandler } from 'react-native-reanimated';
19
19
  import { isWorkletFunction } from 'react-native-worklets';
20
20
 
21
21
  // src/motion/index.ts
@@ -35,8 +35,27 @@ function useAnimation(target, transition) {
35
35
  const cfg = shouldReduceMotion ? { type: "no-animation" } : resolved ?? { type: "spring" };
36
36
  output.value = resolveTransition(cfg, target);
37
37
  }, [target, cfgSig, shouldReduceMotion]);
38
+ useEffect(
39
+ () => () => cancelAnimation(output),
40
+ // `output` is identity-stable per hook instance (Reanimated guarantee).
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ []
43
+ );
38
44
  return output;
39
45
  }
46
+ function useAnimator() {
47
+ const registry = useNamedTransitions();
48
+ const shouldReduceMotion = useShouldReduceMotion();
49
+ const registryRef = useRef(registry);
50
+ registryRef.current = registry;
51
+ const reduceMotionRef = useRef(shouldReduceMotion);
52
+ reduceMotionRef.current = shouldReduceMotion;
53
+ return useCallback((value, to, transition) => {
54
+ const resolved = resolveNamedTransition(transition, registryRef.current);
55
+ const cfg = reduceMotionRef.current ? { type: "no-animation" } : resolved ?? { type: "spring" };
56
+ value.value = resolveTransition(cfg, to);
57
+ }, []);
58
+ }
40
59
  function useSpring(target, config) {
41
60
  const spring = resolveSpringInput(config, useNamedTransitions());
42
61
  const reanimConfig = useMemo(
@@ -75,6 +94,12 @@ function useSpring(target, config) {
75
94
  },
76
95
  [isSharedTarget, reanimConfig]
77
96
  );
97
+ useEffect(
98
+ () => () => cancelAnimation(output),
99
+ // `output` is identity-stable per hook instance (Reanimated guarantee).
100
+ // eslint-disable-next-line react-hooks/exhaustive-deps
101
+ []
102
+ );
78
103
  return output;
79
104
  }
80
105
  function resolveSpringInput(config, registry) {
@@ -96,6 +121,36 @@ function isSharedValue(v) {
96
121
  function useBooleanSpring(active, springConfig) {
97
122
  return useSpring(active ? 1 : 0, springConfig);
98
123
  }
124
+ function useColorCascade(rest, layers, options) {
125
+ const key = options?.key ?? "backgroundColor";
126
+ const sig = `${key}|${rest}|${layers.length}|${layers.map((l) => l.color).join(",")}`;
127
+ const colors = useMemo(() => layers.map((l) => l.color), [sig]);
128
+ const progressRef = useRef([]);
129
+ const prevProgress = progressRef.current;
130
+ let progressChanged = prevProgress.length !== layers.length;
131
+ if (!progressChanged) {
132
+ for (let i = 0; i < layers.length; i++) {
133
+ if (prevProgress[i] !== layers[i].progress) {
134
+ progressChanged = true;
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ if (progressChanged) progressRef.current = layers.map((l) => l.progress);
140
+ const progressValues = progressRef.current;
141
+ return useAnimatedStyle(() => {
142
+ "worklet";
143
+ let acc = rest;
144
+ for (let i = 0; i < colors.length; i++) {
145
+ acc = interpolateColor(
146
+ progressValues[i].value,
147
+ [0, 1],
148
+ [acc, colors[i]]
149
+ );
150
+ }
151
+ return { [key]: acc };
152
+ });
153
+ }
99
154
  function useColorTransition(progress, range, options) {
100
155
  const key = options?.key ?? "backgroundColor";
101
156
  const from = range[0];
@@ -105,8 +160,139 @@ function useColorTransition(progress, range, options) {
105
160
  return { [key]: interpolateColor(progress.value, [0, 1], [from, to]) };
106
161
  });
107
162
  }
163
+ var TRANSFORM_KEYS = /* @__PURE__ */ new Set([
164
+ "translateX",
165
+ "translateY",
166
+ "scale",
167
+ "scaleX",
168
+ "scaleY",
169
+ "rotate",
170
+ "rotateX",
171
+ "rotateY"
172
+ ]);
173
+ var ROTATION_KEYS = /* @__PURE__ */ new Set(["rotate", "rotateX", "rotateY"]);
174
+ var COLOR_KEYS = /* @__PURE__ */ new Set([
175
+ "backgroundColor",
176
+ "color",
177
+ "borderColor",
178
+ "borderTopColor",
179
+ "borderRightColor",
180
+ "borderBottomColor",
181
+ "borderLeftColor",
182
+ "tintColor",
183
+ "shadowColor"
184
+ ]);
185
+ function evenlySpaced(count) {
186
+ if (count <= 1) return [0];
187
+ const out = [];
188
+ for (let i = 0; i < count; i++) out.push(i / (count - 1));
189
+ return out;
190
+ }
191
+ function mapExtrapolation(mode) {
192
+ if (mode === "identity") return Extrapolation.IDENTITY;
193
+ if (mode === "extend") return Extrapolation.EXTEND;
194
+ return Extrapolation.CLAMP;
195
+ }
196
+ function buildSignature(map, options) {
197
+ let sig = "";
198
+ for (const key of Object.keys(map)) {
199
+ const output = map[key];
200
+ sig += `${key}:${JSON.stringify(output)}|`;
201
+ }
202
+ sig += `#ir:${JSON.stringify(options?.inputRange)}|ex:${options?.extrapolate ?? ""}`;
203
+ return sig;
204
+ }
205
+ function buildEntries(map, options) {
206
+ const explicitInput = options?.inputRange;
207
+ const entries = [];
208
+ for (const key of Object.keys(map)) {
209
+ const output = map[key];
210
+ if (output === void 0 || output.length === 0) continue;
211
+ const input = explicitInput ? explicitInput : output.length === 2 ? [0, 1] : evenlySpaced(output.length);
212
+ if (__DEV__ && explicitInput && explicitInput.length !== output.length) {
213
+ console.warn(
214
+ `[inertia] useInterpolatedStyle: inputRange has ${explicitInput.length} stops but the "${String(
215
+ key
216
+ )}" output has ${output.length}. They must match \u2014 interpolation results are undefined otherwise.`
217
+ );
218
+ }
219
+ const isColor = COLOR_KEYS.has(key) && typeof output[0] === "string";
220
+ if (isColor) {
221
+ entries.push({
222
+ kind: "color",
223
+ key,
224
+ input,
225
+ output
226
+ });
227
+ } else if (ROTATION_KEYS.has(key)) {
228
+ entries.push({
229
+ kind: "rotation",
230
+ key,
231
+ input,
232
+ output
233
+ });
234
+ } else if (TRANSFORM_KEYS.has(key)) {
235
+ entries.push({
236
+ kind: "transform-numeric",
237
+ key,
238
+ input,
239
+ output
240
+ });
241
+ } else {
242
+ entries.push({
243
+ kind: "numeric",
244
+ key,
245
+ input,
246
+ output
247
+ });
248
+ }
249
+ }
250
+ return entries;
251
+ }
252
+ function useInterpolatedStyle(progress, map, options) {
253
+ const extrapolate = mapExtrapolation(options?.extrapolate);
254
+ const sig = buildSignature(map, options);
255
+ const entries = useMemo(() => buildEntries(map, options), [sig]);
256
+ return useAnimatedStyle(() => {
257
+ "worklet";
258
+ const out = {};
259
+ const transform = [];
260
+ for (const e of entries) {
261
+ if (e.kind === "color") {
262
+ out[e.key] = interpolateColor(progress.value, e.input, e.output);
263
+ } else if (e.kind === "numeric") {
264
+ out[e.key] = interpolate(progress.value, e.input, e.output, {
265
+ extrapolateLeft: extrapolate,
266
+ extrapolateRight: extrapolate
267
+ });
268
+ } else if (e.kind === "transform-numeric") {
269
+ transform.push({
270
+ [e.key]: interpolate(progress.value, e.input, e.output, {
271
+ extrapolateLeft: extrapolate,
272
+ extrapolateRight: extrapolate
273
+ })
274
+ });
275
+ } else {
276
+ const deg = interpolate(progress.value, e.input, e.output, {
277
+ extrapolateLeft: extrapolate,
278
+ extrapolateRight: extrapolate
279
+ });
280
+ transform.push({ [e.key]: `${deg}deg` });
281
+ }
282
+ }
283
+ if (transform.length > 0) out.transform = transform;
284
+ return out;
285
+ });
286
+ }
108
287
  function useMotionValue(initial) {
109
- return useSharedValue(initial);
288
+ const sv = useSharedValue(initial);
289
+ useEffect(
290
+ () => () => cancelAnimation(sv),
291
+ // `sv` is identity-stable per hook instance (Reanimated guarantee).
292
+ // eslint-disable-next-line react-hooks/exhaustive-deps
293
+ []
294
+ );
295
+ return sv;
110
296
  }
111
297
  function useTransform(arg1, inputRange, outputRange, options) {
112
298
  let producer;
@@ -129,8 +315,8 @@ function useTransform(arg1, inputRange, outputRange, options) {
129
315
  const input = inputRange;
130
316
  const output = outputRange;
131
317
  const isColor = output.length > 0 && typeof output[0] === "string";
132
- const extrapolateLeft = mapExtrapolation(options?.extrapolateLeft);
133
- const extrapolateRight = mapExtrapolation(options?.extrapolateRight);
318
+ const extrapolateLeft = mapExtrapolation2(options?.extrapolateLeft);
319
+ const extrapolateRight = mapExtrapolation2(options?.extrapolateRight);
134
320
  producer = isColor ? () => {
135
321
  "worklet";
136
322
  return interpolateColor(
@@ -150,7 +336,7 @@ function useTransform(arg1, inputRange, outputRange, options) {
150
336
  }
151
337
  return useDerivedValue(producer);
152
338
  }
153
- function mapExtrapolation(mode) {
339
+ function mapExtrapolation2(mode) {
154
340
  if (mode === "identity") return Extrapolation.IDENTITY;
155
341
  if (mode === "extend") return Extrapolation.EXTEND;
156
342
  return Extrapolation.CLAMP;
@@ -401,4 +587,4 @@ function useVariants(variants, initial) {
401
587
  }, []);
402
588
  }
403
589
 
404
- export { Motion, useAnimation, useBooleanSpring, useColorTransition, useMotionValue, useScroll, useShadow, useSpring, useTransform, useVariants };
590
+ export { Motion, useAnimation, useAnimator, useBooleanSpring, useColorCascade, useColorTransition, useInterpolatedStyle, useMotionValue, useScroll, useShadow, useSpring, useTransform, useVariants };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunk3UTJJ4A3_js = require('../chunk-3UTJJ4A3.js');
4
- require('../chunk-7UDYEFBU.js');
3
+ var chunkKBP4LR75_js = require('../chunk-KBP4LR75.js');
4
+ require('../chunk-BP3Y2SHQ.js');
5
5
  require('../chunk-7AOERN53.js');
6
6
  require('../chunk-PTRF47DA.js');
7
7
 
@@ -9,5 +9,5 @@ require('../chunk-PTRF47DA.js');
9
9
 
10
10
  Object.defineProperty(exports, "MotionImage", {
11
11
  enumerable: true,
12
- get: function () { return chunk3UTJJ4A3_js.MotionImage; }
12
+ get: function () { return chunkKBP4LR75_js.MotionImage; }
13
13
  });
@@ -1,4 +1,4 @@
1
- export { MotionImage } from '../chunk-ALRHDFZE.mjs';
2
- import '../chunk-DWCLIBYO.mjs';
1
+ export { MotionImage } from '../chunk-O22NXXCZ.mjs';
2
+ import '../chunk-CSODMRJ7.mjs';
3
3
  import '../chunk-R63GIUNU.mjs';
4
4
  import '../chunk-CY7Y64C3.mjs';
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunkTDSO63CJ_js = require('../chunk-TDSO63CJ.js');
4
- require('../chunk-7UDYEFBU.js');
3
+ var chunk3XTVY34H_js = require('../chunk-3XTVY34H.js');
4
+ require('../chunk-BP3Y2SHQ.js');
5
5
  require('../chunk-7AOERN53.js');
6
6
  require('../chunk-PTRF47DA.js');
7
7
 
@@ -9,5 +9,5 @@ require('../chunk-PTRF47DA.js');
9
9
 
10
10
  Object.defineProperty(exports, "MotionPressable", {
11
11
  enumerable: true,
12
- get: function () { return chunkTDSO63CJ_js.MotionPressable; }
12
+ get: function () { return chunk3XTVY34H_js.MotionPressable; }
13
13
  });
@@ -1,4 +1,4 @@
1
- export { MotionPressable } from '../chunk-NXDJZD6A.mjs';
2
- import '../chunk-DWCLIBYO.mjs';
1
+ export { MotionPressable } from '../chunk-46P57VMY.mjs';
2
+ import '../chunk-CSODMRJ7.mjs';
3
3
  import '../chunk-R63GIUNU.mjs';
4
4
  import '../chunk-CY7Y64C3.mjs';
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunk2HYD2ZBK_js = require('../chunk-2HYD2ZBK.js');
4
- require('../chunk-7UDYEFBU.js');
3
+ var chunkW5MC3P4N_js = require('../chunk-W5MC3P4N.js');
4
+ require('../chunk-BP3Y2SHQ.js');
5
5
  require('../chunk-7AOERN53.js');
6
6
  require('../chunk-PTRF47DA.js');
7
7
 
@@ -9,5 +9,5 @@ require('../chunk-PTRF47DA.js');
9
9
 
10
10
  Object.defineProperty(exports, "MotionScrollView", {
11
11
  enumerable: true,
12
- get: function () { return chunk2HYD2ZBK_js.MotionScrollView; }
12
+ get: function () { return chunkW5MC3P4N_js.MotionScrollView; }
13
13
  });
@@ -1,4 +1,4 @@
1
- export { MotionScrollView } from '../chunk-CWLFUYIY.mjs';
2
- import '../chunk-DWCLIBYO.mjs';
1
+ export { MotionScrollView } from '../chunk-OQV66TBQ.mjs';
2
+ import '../chunk-CSODMRJ7.mjs';
3
3
  import '../chunk-R63GIUNU.mjs';
4
4
  import '../chunk-CY7Y64C3.mjs';
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunkZ7HIOFKQ_js = require('../chunk-Z7HIOFKQ.js');
4
- require('../chunk-7UDYEFBU.js');
3
+ var chunkFWQOXA43_js = require('../chunk-FWQOXA43.js');
4
+ require('../chunk-BP3Y2SHQ.js');
5
5
  require('../chunk-7AOERN53.js');
6
6
  require('../chunk-PTRF47DA.js');
7
7
 
@@ -9,5 +9,5 @@ require('../chunk-PTRF47DA.js');
9
9
 
10
10
  Object.defineProperty(exports, "MotionText", {
11
11
  enumerable: true,
12
- get: function () { return chunkZ7HIOFKQ_js.MotionText; }
12
+ get: function () { return chunkFWQOXA43_js.MotionText; }
13
13
  });
@@ -1,4 +1,4 @@
1
- export { MotionText } from '../chunk-JVBXPF2G.mjs';
2
- import '../chunk-DWCLIBYO.mjs';
1
+ export { MotionText } from '../chunk-SGUHE5CX.mjs';
2
+ import '../chunk-CSODMRJ7.mjs';
3
3
  import '../chunk-R63GIUNU.mjs';
4
4
  import '../chunk-CY7Y64C3.mjs';
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var chunk4QGXK6TF_js = require('../chunk-4QGXK6TF.js');
4
- require('../chunk-7UDYEFBU.js');
3
+ var chunkFNVFV4EY_js = require('../chunk-FNVFV4EY.js');
4
+ require('../chunk-BP3Y2SHQ.js');
5
5
  require('../chunk-7AOERN53.js');
6
6
  require('../chunk-PTRF47DA.js');
7
7
 
@@ -9,5 +9,5 @@ require('../chunk-PTRF47DA.js');
9
9
 
10
10
  Object.defineProperty(exports, "MotionView", {
11
11
  enumerable: true,
12
- get: function () { return chunk4QGXK6TF_js.MotionView; }
12
+ get: function () { return chunkFNVFV4EY_js.MotionView; }
13
13
  });
@@ -1,4 +1,4 @@
1
- export { MotionView } from '../chunk-6SMPIOIC.mjs';
2
- import '../chunk-DWCLIBYO.mjs';
1
+ export { MotionView } from '../chunk-BQQTHG2V.mjs';
2
+ import '../chunk-CSODMRJ7.mjs';
3
3
  import '../chunk-R63GIUNU.mjs';
4
4
  import '../chunk-CY7Y64C3.mjs';
package/llms.txt CHANGED
@@ -26,6 +26,8 @@ import {
26
26
  useTransform,
27
27
  useShadow,
28
28
  useColorTransition,
29
+ useColorCascade,
30
+ useInterpolatedStyle,
29
31
  useScroll,
30
32
  } from '@rootnative/inertia'
31
33
  // Opt-in MD3 / iOS state-layer overlay helper:
@@ -50,11 +52,14 @@ import { MotionScrollView } from '@rootnative/inertia/scroll-view'
50
52
  - `useVariants(variants, initial?)` — returns `{ current, transitionTo }` controller for the `controller` prop.
51
53
  - `useMotionValue(initial)` — thin pass-through over `useSharedValue<T>`. Returns a `SharedValue<T>` directly so it interops with every Reanimated API without unwrapping.
52
54
  - `useAnimation(target, transition?)` — drive a `SharedValue<number>` toward `target` with any `TransitionConfig` (spring / timing / decay / no-animation, plus `repeat`) or a registered `TransitionName`. The general-purpose value-layer hook for boolean-state progress, indeterminate loops, and anywhere the same value needs to feed multiple `useAnimatedStyle` blocks.
55
+ - `useAnimator()` — the **imperative** counterpart to `useAnimation`. Returns an identity-stable `(value: SharedValue<number>, to: number, transition?: TransitionConfig | TransitionName) => void` setter to call from event handlers (hover-in, focus, press) rather than from an effect. Resolves registered `TransitionName`s through the nearest `<MotionConfig transitions>` and gates on `<MotionConfig reducedMotion>` (`'no-animation'` downgrade) — the two things a hand-rolled `resolveTransition` write in an event handler silently gets wrong. Default transition is the library spring. Not a new animation API — the hooks-layer equivalent of `useMotionValue` + `resolveTransition`, minus the footguns.
53
56
  - `useSpring(target, config?)` — spring-only shorthand. Animates a `SharedValue<number>` toward `target` with react-spring vocab (or a spring-typed registered `TransitionName`; non-spring names warn and fall back to the default spring). `target` may be a plain number (effect-driven) or a `SharedValue<number>` (UI-thread reaction); the latter is the gesture-smoothing path.
54
57
  - `useBooleanSpring(active, config?)` — sugar over `useSpring` for the recurring "spring 0↔1 progress from a boolean" shape (checkbox checks, accordion expansions, drawer open/closed). Identical mechanics to `useSpring(active ? 1 : 0, config)` — the named form so the call site reads as the boolean it represents.
55
58
  - `useTransform(value, inputRange, outputRange, options?)` / `useTransform(transformer)` — interpolate a numeric shared value onto a number or color range, or derive any value from any number of shared values via a worklet. Non-worklet transformers are auto-wrapped.
56
59
  - `useShadow({ from, to, progress })` — pure value-layer interpolator between two `ShadowConfig`s (`shadowOpacity` / `shadowRadius` / `shadowOffset` / `elevation` / `shadowColor`, plus `boxShadow` as a CSS string or `BoxShadowLayer[]` — the shadow surface on web and RN 0.76+ new-arch; multi-layer, CSS-transition padding semantics, malformed strings throw at setup) driven by a `SharedValue<number>` (0→1). Returns an animated style fragment to spread onto `style`; only emits keys present on either side, absent sides default to natural zero. The hook does not animate on its own — drive `progress` with a spring, a scroll-derived `useTransform`, or any other shared value source.
57
60
  - `useColorTransition(progress, [from, to], options?)` — pure value-layer interpolator for a single color channel, driven by a `SharedValue<number>` (0→1). Returns an animated style fragment with one color key (default `backgroundColor`; configurable via `options.key` to `color` / `borderColor` / `tintColor` / `shadowColor` / per-side border colors). For raw `SharedValue<string>` output, use `useTransform(progress, [0, 1], [from, to])` instead.
61
+ - `useColorCascade(rest, layers, options?)` — pure value-layer interpolator compositing a **priority-ordered** stack of color layers over a base `rest` color; each `layers` entry is `{ progress: SharedValue<number>, color: string }`, later entries win as their progress rises (equivalent to the nested `focus(error(hover(rest)))` `interpolateColor` chain). `options.key` reuses `ColorStyleKey` (default `backgroundColor`). Returns a spreadable animated style fragment; memoized on a colors+key signature. Color-only by design — cascade a numeric key separately via `useInterpolatedStyle` + `useTransform` max. `useColorTransition` stays the single-layer fast path.
62
+ - `useInterpolatedStyle(progress, map, options?)` — pure value-layer interpolator mapping one `SharedValue<number>` onto **N** style props at once, returned as a spreadable animated style fragment (the multi-key, style-fragment counterpart to `useTransform`'s output-range form). Numeric keys (`opacity` / `height` / `fontSize` / …) route through `interpolate`; transform keys (`translateX` / `scale` / `rotate` / …) lift into a `transform` array in map key-order (`rotate*` emit `'<n>deg'`); color keys (`backgroundColor` / `borderColor` / …) route through `interpolateColor` (a multi-stop `useColorTransition`). Stop types are compile-checked. `options.inputRange` (default `[0,1]` / evenly-spaced) and `options.extrapolate` (default `'clamp'`) apply to every key. Memoized on an order-preserving signature. For function-valued or multi-source composition, drop to `useTransform`'s worklet form or a hand-rolled `useAnimatedStyle`.
58
63
  - `useScroll()` — returns `{ scrollX, scrollY, onScroll }` for use with `Motion.ScrollView`. Scroll events fire on the UI thread.
59
64
  - `createMotionComponent<C>(C)` — wrap any component with the same Motion prop surface, inferring style from `C`.
60
65
  - `buildReleaseAnimation(transition, toValue)` — worklet-safe single-step animation builder (spring / timing / decay / no-animation). For assigning Inertia-resolved animations to shared values from inside a gesture worklet. Used internally by `@rootnative/inertia-gestures`'s `useDrag({ onRelease })`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rootnative/inertia",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Declarative animation primitives for React Native, built on react-native-reanimated.",
5
5
  "license": "MIT",
6
6
  "author": "RootNative",
package/src/index.ts CHANGED
@@ -41,9 +41,12 @@ export {
41
41
  } from './transitions'
42
42
  export {
43
43
  useAnimation,
44
+ useAnimator,
44
45
  useBooleanSpring,
46
+ useColorCascade,
45
47
  useColorTransition,
46
48
  useGesture,
49
+ useInterpolatedStyle,
47
50
  useMotionValue,
48
51
  useScroll,
49
52
  useShadow,
@@ -57,13 +60,20 @@ export {
57
60
  // Runtime render-layer interop lives in `@rootnative/inertia/reanimated`.
58
61
  export type { SharedValue } from 'react-native-reanimated'
59
62
  export type {
63
+ Animator,
60
64
  BoxShadowLayer,
65
+ ColorCascadeLayer,
61
66
  ColorStyleKey,
62
67
  ExtrapolationMode,
68
+ InterpolatedStyleMap,
69
+ NumericStyleKey,
63
70
  ShadowConfig,
71
+ TransformKey,
72
+ UseColorCascadeOptions,
64
73
  UseColorTransitionOptions,
65
74
  UseGestureHandlers,
66
75
  UseGestureResult,
76
+ UseInterpolatedStyleOptions,
67
77
  UseScrollResult,
68
78
  UseShadowOptions,
69
79
  UseTransformOptions,
@@ -7,6 +7,7 @@ export {
7
7
  releaseLayout,
8
8
  SHARED_LAYOUT_TTL_MS,
9
9
  __setSharedLayoutClock,
10
+ __sharedRegistrySize,
10
11
  type SharedRect,
11
12
  } from './sharedRegistry'
12
13
  export {
@@ -51,6 +51,37 @@ export const SHARED_LAYOUT_TTL_MS = 1000
51
51
  */
52
52
  let now = (): number => Date.now()
53
53
 
54
+ /**
55
+ * When the last expiry sweep ran, so the scan is amortized to at most once per
56
+ * TTL window rather than running on every write.
57
+ */
58
+ let lastSweep = 0
59
+
60
+ /**
61
+ * Drop every entry whose TTL has passed.
62
+ *
63
+ * Without this the map only ever shrank via `consumeLayout` / `peekSharedLayout`,
64
+ * so a `layoutId` that unmounted and was never remounted left its rect behind
65
+ * for the lifetime of the process. Bounded by distinct-id count, which is small
66
+ * for a handful of hero images but not for per-item ids in a long-lived list
67
+ * (`layoutId={`photo-${item.id}`}`).
68
+ *
69
+ * Sweeping a *live* element's entry is harmless: while mounted it re-registers
70
+ * on every `onLayout`, and on unmount `releaseLayout` re-adds it with a fresh
71
+ * TTL. So the only thing an over-eager sweep can cost is a FLIP source that was
72
+ * already too old to be used.
73
+ *
74
+ * Time-gated so a burst of layout events doesn't turn into a burst of full
75
+ * scans — worst case an expired entry survives one extra TTL window.
76
+ */
77
+ function sweepExpired(at: number): void {
78
+ if (at - lastSweep < SHARED_LAYOUT_TTL_MS) return
79
+ lastSweep = at
80
+ for (const [id, entry] of REGISTRY) {
81
+ if (entry.expiresAt < at) REGISTRY.delete(id)
82
+ }
83
+ }
84
+
54
85
  /**
55
86
  * Update the latest known rect for `id`. Called on every `onLayout` of a
56
87
  * Motion primitive with `layoutId` set so the registry always holds a
@@ -58,7 +89,9 @@ let now = (): number => Date.now()
58
89
  * transition. Resets the TTL each call.
59
90
  */
60
91
  export function registerLayout(id: string, rect: SharedRect): void {
61
- REGISTRY.set(id, { rect, expiresAt: now() + SHARED_LAYOUT_TTL_MS })
92
+ const at = now()
93
+ sweepExpired(at)
94
+ REGISTRY.set(id, { rect, expiresAt: at + SHARED_LAYOUT_TTL_MS })
62
95
  }
63
96
 
64
97
  /**
@@ -67,7 +100,9 @@ export function registerLayout(id: string, rect: SharedRect): void {
67
100
  * is purely intent-documenting at the call site.
68
101
  */
69
102
  export function releaseLayout(id: string, rect: SharedRect): void {
70
- REGISTRY.set(id, { rect, expiresAt: now() + SHARED_LAYOUT_TTL_MS })
103
+ const at = now()
104
+ sweepExpired(at)
105
+ REGISTRY.set(id, { rect, expiresAt: at + SHARED_LAYOUT_TTL_MS })
71
106
  }
72
107
 
73
108
  /**
@@ -88,6 +123,7 @@ export function consumeLayout(id: string): SharedRect | undefined {
88
123
  /** Drop all entries. Tests use this to isolate between cases. */
89
124
  export function clearSharedRegistry(): void {
90
125
  REGISTRY.clear()
126
+ lastSweep = 0
91
127
  }
92
128
 
93
129
  /**
@@ -108,4 +144,17 @@ export function peekSharedLayout(id: string): SharedRect | undefined {
108
144
  */
109
145
  export function __setSharedLayoutClock(fn: (() => number) | undefined): void {
110
146
  now = fn ?? Date.now
147
+ // Swapping the clock makes the previous sweep timestamp meaningless — a
148
+ // stubbed clock usually starts near 0, which would otherwise suppress every
149
+ // sweep until it caught up past the real-time value recorded earlier.
150
+ lastSweep = 0
151
+ }
152
+
153
+ /**
154
+ * Test hook: how many entries the registry is holding, expired or not. Used to
155
+ * assert that released-but-never-consumed rects are actually evicted. Not
156
+ * exported from the package root — reachable only from inside the workspace.
157
+ */
158
+ export function __sharedRegistrySize(): number {
159
+ return REGISTRY.size
111
160
  }