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

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 (55) hide show
  1. package/dist/gestureLayer/index.d.mts +2 -2
  2. package/dist/gestureLayer/index.d.ts +2 -2
  3. package/dist/gestureLayer/index.js +3 -1402
  4. package/dist/gestureLayer/index.js.map +1 -1
  5. package/dist/gestureLayer/index.mjs +3 -1402
  6. package/dist/gestureLayer/index.mjs.map +1 -1
  7. package/dist/index.d.mts +3 -3
  8. package/dist/index.d.ts +3 -3
  9. package/dist/index.js +69 -1461
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +70 -1462
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/motion/Image.d.mts +1 -1
  14. package/dist/motion/Image.d.ts +1 -1
  15. package/dist/motion/Image.js +68 -1460
  16. package/dist/motion/Image.js.map +1 -1
  17. package/dist/motion/Image.mjs +70 -1462
  18. package/dist/motion/Image.mjs.map +1 -1
  19. package/dist/motion/Pressable.d.mts +1 -1
  20. package/dist/motion/Pressable.d.ts +1 -1
  21. package/dist/motion/Pressable.js +68 -1460
  22. package/dist/motion/Pressable.js.map +1 -1
  23. package/dist/motion/Pressable.mjs +70 -1462
  24. package/dist/motion/Pressable.mjs.map +1 -1
  25. package/dist/motion/ScrollView.d.mts +1 -1
  26. package/dist/motion/ScrollView.d.ts +1 -1
  27. package/dist/motion/ScrollView.js +68 -1460
  28. package/dist/motion/ScrollView.js.map +1 -1
  29. package/dist/motion/ScrollView.mjs +70 -1462
  30. package/dist/motion/ScrollView.mjs.map +1 -1
  31. package/dist/motion/Text.d.mts +1 -1
  32. package/dist/motion/Text.d.ts +1 -1
  33. package/dist/motion/Text.js +68 -1460
  34. package/dist/motion/Text.js.map +1 -1
  35. package/dist/motion/Text.mjs +70 -1462
  36. package/dist/motion/Text.mjs.map +1 -1
  37. package/dist/motion/View.d.mts +1 -1
  38. package/dist/motion/View.d.ts +1 -1
  39. package/dist/motion/View.js +68 -1460
  40. package/dist/motion/View.js.map +1 -1
  41. package/dist/motion/View.mjs +70 -1462
  42. package/dist/motion/View.mjs.map +1 -1
  43. package/dist/touch/index.d.mts +1 -1
  44. package/dist/touch/index.d.ts +1 -1
  45. package/dist/{types-BwyvoH2V.d.mts → types-cU43dEmH.d.mts} +42 -15
  46. package/dist/{types-BwyvoH2V.d.ts → types-cU43dEmH.d.ts} +42 -15
  47. package/dist/{useGesture-BPPp9LhV.d.ts → useGesture-B7A_1DVg.d.ts} +1 -1
  48. package/dist/{useGesture-BnBF4OtT.d.mts → useGesture-cimMrzC1.d.mts} +1 -1
  49. package/jest-setup.js +4 -0
  50. package/package.json +8 -2
  51. package/src/__type-tests__/variants.test-d.tsx +67 -0
  52. package/src/layout/sharedRegistry.ts +7 -4
  53. package/src/motion/createMotionComponent.tsx +63 -33
  54. package/src/motion/installCheck.ts +7 -11
  55. package/src/types.ts +58 -19
@@ -1,6 +1,6 @@
1
1
  import { PanResponderInstance } from 'react-native';
2
2
  import { useAnimatedStyle, SharedValue } from 'react-native-reanimated';
3
- import { T as TransitionConfig } from '../types-BwyvoH2V.mjs';
3
+ import { T as TransitionConfig } from '../types-cU43dEmH.mjs';
4
4
  import 'react';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { PanResponderInstance } from 'react-native';
2
2
  import { useAnimatedStyle, SharedValue } from 'react-native-reanimated';
3
- import { T as TransitionConfig } from '../types-BwyvoH2V.js';
3
+ import { T as TransitionConfig } from '../types-cU43dEmH.js';
4
4
  import 'react';
5
5
 
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { ComponentType } from 'react';
1
+ import { ComponentType, ComponentProps, Ref, ReactElement } from 'react';
2
2
  import { StyleProp } from 'react-native';
3
3
 
4
4
  /**
@@ -210,8 +210,15 @@ interface VariantController<K extends string = string> {
210
210
  }
211
211
  /**
212
212
  * Props injected onto every Motion primitive.
213
+ *
214
+ * The second type parameter `V` is the concrete `variants` map. It is inferred
215
+ * from the `variants` prop at each JSX use (see `MotionComponent`), which is
216
+ * what lets `animate` narrow to the variant key union and reject typos. When
217
+ * no `variants` prop is passed, `V` falls back to `VariantsMap<C>` — whose key
218
+ * type is the open `string`, so `animate` still accepts any string and nothing
219
+ * regresses for the variant-less case.
213
220
  */
214
- interface MotionProps<C> {
221
+ interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
215
222
  /**
216
223
  * Initial values applied on mount. Read once on mount and intentionally
217
224
  * non-reactive — to reset after a state change, change the component `key`,
@@ -222,10 +229,11 @@ interface MotionProps<C> {
222
229
  initial?: AnimateStyle<C> | false;
223
230
  /**
224
231
  * The animation target. A style object, a variant key (when `variants` is
225
- * supplied), or an array of sequence steps. Variant keys autocomplete when
226
- * `variants` is annotated `as const`.
232
+ * supplied), or an array of sequence steps. When `variants` is set, the
233
+ * string form is narrowed to the map's keys, so a key typo is a compile
234
+ * error and the keys autocomplete — no `as const` required.
227
235
  */
228
- animate?: AnimateStyle<C> | string;
236
+ animate?: AnimateStyle<C> | (keyof V & string);
229
237
  /**
230
238
  * Values applied while the component exits via `<Presence>`.
231
239
  */
@@ -234,13 +242,13 @@ interface MotionProps<C> {
234
242
  * Named animation states. With `variants` set, `animate` accepts a key from
235
243
  * this map.
236
244
  */
237
- variants?: VariantsMap<C>;
245
+ variants?: V;
238
246
  /**
239
247
  * Imperative controller from `useVariants(...)`. When supplied, `animate`
240
248
  * is read from `controller.current` and re-applied whenever the controller
241
249
  * transitions. `animate` and `controller` should not both be set.
242
250
  */
243
- controller?: VariantController;
251
+ controller?: VariantController<keyof V & string>;
244
252
  /**
245
253
  * Gesture-driven sub-states (`pressed`, `focused`, `focusVisible`,
246
254
  * `hovered`). When omitted, no handlers are mounted on the underlying
@@ -282,8 +290,11 @@ interface MotionProps<C> {
282
290
  * recorded rect to its natural position via a FLIP transform stack.
283
291
  *
284
292
  * Reanimated 4 removed the `sharedTransitionTag` API — `layoutId` is the
285
- * Inertia-side measure-based replacement. Rects are stored in window
286
- * coordinates so the source and target can live on different screens.
293
+ * Inertia-side measure-based replacement. Rects are recorded in
294
+ * parent-relative coordinates (from `onLayout`), which composes when the
295
+ * source and target screens share an outer content container (the common
296
+ * stack-navigator case); nested-parent layouts need the v2
297
+ * window-coordinate path.
287
298
  *
288
299
  * The same `transition` prop drives the FLIP animation (spring by
289
300
  * default; `'timing'` honored; `'decay'` downgrades to spring; reduced
@@ -301,12 +312,28 @@ interface MotionProps<C> {
301
312
  onAnimationEnd?: (info: AnimationCallbackInfo<AnimateStyle<C>>) => void;
302
313
  }
303
314
  /**
304
- * The component type produced by `createMotionComponent`. Combines the
305
- * underlying component's props (minus `style`, which we replace with an
306
- * animated style) with the Motion-specific props above.
315
+ * Props of a Motion primitive for a given underlying component `C` and a
316
+ * concrete variants map `V`: the component's own props (minus `style`, which
317
+ * we replace with an animated style) intersected with the Motion props.
318
+ */
319
+ type MotionComponentProps<C extends ComponentType<any>, V extends VariantsMap<ComponentProps<C>> = VariantsMap<ComponentProps<C>>> = Omit<ComponentProps<C>, 'style'> & MotionProps<ComponentProps<C>, V> & {
320
+ style?: ComponentProps<C>['style'];
321
+ ref?: Ref<unknown>;
322
+ };
323
+ /**
324
+ * The component type produced by `createMotionComponent`.
325
+ *
326
+ * It is a **generic call signature**, not a plain `ComponentType`: the variant
327
+ * map `V` is inferred from the `variants` prop at each JSX use. That inference
328
+ * is what narrows `animate`'s string form to the variant keys, so
329
+ * `<Motion.View variants={{ open, closed }} animate="opne" />` is a compile
330
+ * error and `open` / `closed` autocomplete. With no `variants` prop, `V` falls
331
+ * back to the open `VariantsMap`, so `animate` still accepts any string and the
332
+ * variant-less call site is unchanged.
307
333
  */
308
- type MotionComponent<C extends ComponentType<any>> = ComponentType<Omit<React.ComponentProps<C>, 'style'> & MotionProps<React.ComponentProps<C>> & {
309
- style?: React.ComponentProps<C>['style'];
310
- }>;
334
+ interface MotionComponent<C extends ComponentType<any>> {
335
+ <V extends VariantsMap<ComponentProps<C>> = VariantsMap<ComponentProps<C>>>(props: MotionComponentProps<C, V>): ReactElement | null;
336
+ displayName?: string;
337
+ }
311
338
 
312
339
  export type { AnimatableValue as A, DecayTransition as D, EasingInput as E, GestureSubStates as G, MotionComponent as M, NoAnimationTransition as N, PerPropertyTransition as P, RepeatConfig as R, SpringTransition as S, TransitionConfig as T, VariantController as V, AnimateStyle as a, AnimationCallbackInfo as b, MotionProps as c, SequenceStep as d, TimingTransition as e, Transition as f, VariantsMap as g, GestureLayerTransitions as h };
@@ -1,4 +1,4 @@
1
- import { ComponentType } from 'react';
1
+ import { ComponentType, ComponentProps, Ref, ReactElement } from 'react';
2
2
  import { StyleProp } from 'react-native';
3
3
 
4
4
  /**
@@ -210,8 +210,15 @@ interface VariantController<K extends string = string> {
210
210
  }
211
211
  /**
212
212
  * Props injected onto every Motion primitive.
213
+ *
214
+ * The second type parameter `V` is the concrete `variants` map. It is inferred
215
+ * from the `variants` prop at each JSX use (see `MotionComponent`), which is
216
+ * what lets `animate` narrow to the variant key union and reject typos. When
217
+ * no `variants` prop is passed, `V` falls back to `VariantsMap<C>` — whose key
218
+ * type is the open `string`, so `animate` still accepts any string and nothing
219
+ * regresses for the variant-less case.
213
220
  */
214
- interface MotionProps<C> {
221
+ interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
215
222
  /**
216
223
  * Initial values applied on mount. Read once on mount and intentionally
217
224
  * non-reactive — to reset after a state change, change the component `key`,
@@ -222,10 +229,11 @@ interface MotionProps<C> {
222
229
  initial?: AnimateStyle<C> | false;
223
230
  /**
224
231
  * The animation target. A style object, a variant key (when `variants` is
225
- * supplied), or an array of sequence steps. Variant keys autocomplete when
226
- * `variants` is annotated `as const`.
232
+ * supplied), or an array of sequence steps. When `variants` is set, the
233
+ * string form is narrowed to the map's keys, so a key typo is a compile
234
+ * error and the keys autocomplete — no `as const` required.
227
235
  */
228
- animate?: AnimateStyle<C> | string;
236
+ animate?: AnimateStyle<C> | (keyof V & string);
229
237
  /**
230
238
  * Values applied while the component exits via `<Presence>`.
231
239
  */
@@ -234,13 +242,13 @@ interface MotionProps<C> {
234
242
  * Named animation states. With `variants` set, `animate` accepts a key from
235
243
  * this map.
236
244
  */
237
- variants?: VariantsMap<C>;
245
+ variants?: V;
238
246
  /**
239
247
  * Imperative controller from `useVariants(...)`. When supplied, `animate`
240
248
  * is read from `controller.current` and re-applied whenever the controller
241
249
  * transitions. `animate` and `controller` should not both be set.
242
250
  */
243
- controller?: VariantController;
251
+ controller?: VariantController<keyof V & string>;
244
252
  /**
245
253
  * Gesture-driven sub-states (`pressed`, `focused`, `focusVisible`,
246
254
  * `hovered`). When omitted, no handlers are mounted on the underlying
@@ -282,8 +290,11 @@ interface MotionProps<C> {
282
290
  * recorded rect to its natural position via a FLIP transform stack.
283
291
  *
284
292
  * Reanimated 4 removed the `sharedTransitionTag` API — `layoutId` is the
285
- * Inertia-side measure-based replacement. Rects are stored in window
286
- * coordinates so the source and target can live on different screens.
293
+ * Inertia-side measure-based replacement. Rects are recorded in
294
+ * parent-relative coordinates (from `onLayout`), which composes when the
295
+ * source and target screens share an outer content container (the common
296
+ * stack-navigator case); nested-parent layouts need the v2
297
+ * window-coordinate path.
287
298
  *
288
299
  * The same `transition` prop drives the FLIP animation (spring by
289
300
  * default; `'timing'` honored; `'decay'` downgrades to spring; reduced
@@ -301,12 +312,28 @@ interface MotionProps<C> {
301
312
  onAnimationEnd?: (info: AnimationCallbackInfo<AnimateStyle<C>>) => void;
302
313
  }
303
314
  /**
304
- * The component type produced by `createMotionComponent`. Combines the
305
- * underlying component's props (minus `style`, which we replace with an
306
- * animated style) with the Motion-specific props above.
315
+ * Props of a Motion primitive for a given underlying component `C` and a
316
+ * concrete variants map `V`: the component's own props (minus `style`, which
317
+ * we replace with an animated style) intersected with the Motion props.
318
+ */
319
+ type MotionComponentProps<C extends ComponentType<any>, V extends VariantsMap<ComponentProps<C>> = VariantsMap<ComponentProps<C>>> = Omit<ComponentProps<C>, 'style'> & MotionProps<ComponentProps<C>, V> & {
320
+ style?: ComponentProps<C>['style'];
321
+ ref?: Ref<unknown>;
322
+ };
323
+ /**
324
+ * The component type produced by `createMotionComponent`.
325
+ *
326
+ * It is a **generic call signature**, not a plain `ComponentType`: the variant
327
+ * map `V` is inferred from the `variants` prop at each JSX use. That inference
328
+ * is what narrows `animate`'s string form to the variant keys, so
329
+ * `<Motion.View variants={{ open, closed }} animate="opne" />` is a compile
330
+ * error and `open` / `closed` autocomplete. With no `variants` prop, `V` falls
331
+ * back to the open `VariantsMap`, so `animate` still accepts any string and the
332
+ * variant-less call site is unchanged.
307
333
  */
308
- type MotionComponent<C extends ComponentType<any>> = ComponentType<Omit<React.ComponentProps<C>, 'style'> & MotionProps<React.ComponentProps<C>> & {
309
- style?: React.ComponentProps<C>['style'];
310
- }>;
334
+ interface MotionComponent<C extends ComponentType<any>> {
335
+ <V extends VariantsMap<ComponentProps<C>> = VariantsMap<ComponentProps<C>>>(props: MotionComponentProps<C, V>): ReactElement | null;
336
+ displayName?: string;
337
+ }
311
338
 
312
339
  export type { AnimatableValue as A, DecayTransition as D, EasingInput as E, GestureSubStates as G, MotionComponent as M, NoAnimationTransition as N, PerPropertyTransition as P, RepeatConfig as R, SpringTransition as S, TransitionConfig as T, VariantController as V, AnimateStyle as a, AnimationCallbackInfo as b, MotionProps as c, SequenceStep as d, TimingTransition as e, Transition as f, VariantsMap as g, GestureLayerTransitions as h };
@@ -1,5 +1,5 @@
1
1
  import { SharedValue } from 'react-native-reanimated';
2
- import { T as TransitionConfig, h as GestureLayerTransitions } from './types-BwyvoH2V.js';
2
+ import { T as TransitionConfig, h as GestureLayerTransitions } from './types-cU43dEmH.js';
3
3
 
4
4
  /**
5
5
  * Handler bag returned by `useGesture`. Spread on a `Pressable` to drive the
@@ -1,5 +1,5 @@
1
1
  import { SharedValue } from 'react-native-reanimated';
2
- import { T as TransitionConfig, h as GestureLayerTransitions } from './types-BwyvoH2V.mjs';
2
+ import { T as TransitionConfig, h as GestureLayerTransitions } from './types-cU43dEmH.mjs';
3
3
 
4
4
  /**
5
5
  * Handler bag returned by `useGesture`. Spread on a `Pressable` to drive the
package/jest-setup.js CHANGED
@@ -104,6 +104,10 @@ jest.mock('react-native-reanimated', () => {
104
104
  }
105
105
  },
106
106
  useReducedMotion: () => false,
107
+ // Inertia's dev-time install check reads this to detect a too-old
108
+ // Reanimated. The check is skipped under NODE_ENV=test, but the named
109
+ // import must still resolve for consumers' test suites.
110
+ reanimatedVersion: '4.0.0',
107
111
  isWorkletFunction: () => false,
108
112
  cancelAnimation: () => {},
109
113
  runOnJS: (fn) => fn,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlynative/inertia",
3
- "version": "0.0.1-alpha.8",
3
+ "version": "0.0.1-alpha.9",
4
4
  "description": "Declarative animation primitives for React Native, built on react-native-reanimated.",
5
5
  "license": "MIT",
6
6
  "author": "OnlyNative",
@@ -112,7 +112,13 @@
112
112
  "peerDependencies": {
113
113
  "react": ">=19.0.0",
114
114
  "react-native": ">=0.81.0",
115
- "react-native-reanimated": ">=4.0.0"
115
+ "react-native-reanimated": ">=4.0.0",
116
+ "react-native-worklets": ">=0.5.0"
117
+ },
118
+ "peerDependenciesMeta": {
119
+ "react-native-worklets": {
120
+ "optional": true
121
+ }
116
122
  },
117
123
  "devDependencies": {
118
124
  "@react-native/babel-preset": "^0.81.5",
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Compile-time gate for variant-key narrowing on `animate`.
3
+ *
4
+ * CLAUDE.md Principle 5 / line 43: "Variant string keys must autocomplete on
5
+ * `animate`." That requires the `variants` map to be inferred at the JSX call
6
+ * site and `animate`'s string form to be narrowed to its keys — so a key typo
7
+ * is a type error, not a silent runtime no-op. These assertions run under
8
+ * `tsc --noEmit` (the typecheck CI step); if a `@ts-expect-error` here stops
9
+ * being an error, tsc fails with "Unused '@ts-expect-error' directive" and the
10
+ * differentiator has regressed.
11
+ *
12
+ * Unlike `animate.test-d.tsx`, these MUST mount JSX: the narrowing depends on
13
+ * `V` being inferred from the `variants` prop at the call site, which only
14
+ * happens through the generic component call signature.
15
+ */
16
+
17
+ import { Motion } from '../motion'
18
+
19
+ const variants = {
20
+ open: { opacity: 1, translateY: 0 },
21
+ closed: { opacity: 0, translateY: 100 },
22
+ } as const
23
+
24
+ // ─── Variant key narrowing ──────────────────────────────────────────────────
25
+
26
+ // A declared key is accepted (and `open` / `closed` autocomplete here).
27
+ const _acceptsKnownKey = <Motion.View variants={variants} animate="open" />
28
+ const _acceptsOtherKey = <Motion.View variants={variants} animate="closed" />
29
+
30
+ // A typo'd key is a compile error rather than a silent no-op.
31
+ // @ts-expect-error 'opne' is not a key of `variants`
32
+ const _rejectsTypoKey = <Motion.View variants={variants} animate="opne" />
33
+
34
+ // The style-object form still works alongside `variants` (escape hatch for a
35
+ // one-off target that isn't a named state).
36
+ const _acceptsStyleObject = (
37
+ <Motion.View variants={variants} animate={{ opacity: 0.5 }} />
38
+ )
39
+
40
+ // `as const` is NOT required — an inline object literal narrows just as well.
41
+ const inlineVariants = { a: { opacity: 1 }, b: { opacity: 0 } }
42
+ const _inlineVariantsNarrow = (
43
+ <Motion.View variants={inlineVariants} animate="a" />
44
+ )
45
+ const _inlineVariantsReject = (
46
+ // @ts-expect-error 'c' is not a key of the inline variants map
47
+ <Motion.View variants={inlineVariants} animate="c" />
48
+ )
49
+
50
+ // ─── No variants → string stays open (back-compat) ──────────────────────────
51
+
52
+ // Without `variants`, the string form is unconstrained, so this must NOT error
53
+ // (the variant-less call site is unchanged by the narrowing machinery).
54
+ const _noVariantsAnyString = <Motion.View animate="whatever" />
55
+ const _noVariantsStyleObject = <Motion.View animate={{ translateX: 10 }} />
56
+
57
+ // Silence "declared but never read" — these exist purely as type assertions.
58
+ export type _VariantTypeAssertions = [
59
+ typeof _acceptsKnownKey,
60
+ typeof _acceptsOtherKey,
61
+ typeof _rejectsTypoKey,
62
+ typeof _acceptsStyleObject,
63
+ typeof _inlineVariantsNarrow,
64
+ typeof _inlineVariantsReject,
65
+ typeof _noVariantsAnyString,
66
+ typeof _noVariantsStyleObject,
67
+ ]
@@ -14,12 +14,15 @@
14
14
  * it becomes the FLIP source rect; the entry is removed so a third
15
15
  * mount with the same id doesn't re-animate from a stale snapshot.
16
16
  *
17
- * Rects are stored in **window coordinates** (what `measureInWindow`
18
- * returns). Cross-screen transitions need this parent-relative
19
- * coordinates are different on each screen and wouldn't compose.
17
+ * Rects are stored in **parent-relative coordinates** (what `onLayout`'s
18
+ * `nativeEvent.layout` reports). This composes for the common case where the
19
+ * source and target share an outer content container — e.g. a typical stack
20
+ * navigator. Nested-parent setups, where the two parents sit at different
21
+ * window offsets, need a window-coordinate path (`measureInWindow`); that is
22
+ * punted to v2 per the roadmap.
20
23
  */
21
24
 
22
- /** Window-relative rect of a measured element. */
25
+ /** Parent-relative rect of a measured element (from `onLayout`). */
23
26
  export interface SharedRect {
24
27
  x: number
25
28
  y: number
@@ -300,43 +300,73 @@ export function createMotionComponent<C extends ComponentType<any>>(
300
300
  const [focusVisible, setFocusVisible] = useState(false)
301
301
  const [hovered, setHovered] = useState(false)
302
302
 
303
- // The set of keys this instance animates is locked at first render. With
304
- // variants in play the union across all variants is what matters — a key
305
- // touched by any variant must be active so the worklet picks it up when
306
- // the controller transitions. Gesture sub-states join the same union so
307
- // pressed/focused/focusVisible/hovered targets can drive any key they
308
- // declare even when the base `animate` doesn't touch it.
309
- const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
310
- if (activeKeysRef.current === null) {
311
- const touched = new Set<AnimatableKey>()
312
- collectTouchedKeys(touched, animateRecord)
313
- if (initialRecord) collectTouchedKeys(touched, initialRecord)
314
- if (variants) {
315
- for (const variant of Object.values(variants) as object[]) {
316
- if (!variant) continue
317
- collectTouchedKeys(touched, variant as Record<string, unknown>)
318
- }
303
+ // The set of keys this instance animates is a *monotonically growing*
304
+ // union, recomputed every render and expanded when a render introduces a
305
+ // key not seen before. It never shrinks. Two requirements meet here:
306
+ //
307
+ // 1. Variants and gesture sub-states contribute the union across *all*
308
+ // their branches up front a key touched by any variant must be
309
+ // active so the worklet picks it up when the controller transitions
310
+ // to a branch the base `animate` never mentions.
311
+ // 2. A literal `animate` object is reactive: a parent that changes
312
+ // `animate={{ opacity: 1 }}` to `animate={{ opacity: 1, scale: 2 }}`
313
+ // after mount must get `scale` animating. Freezing the set at first
314
+ // render silently dropped the new key (its SV updated, but the
315
+ // worklet which iterates this set — never read it).
316
+ //
317
+ // Growing-only keeps the worklet stable: the `activeKeysRef.current` array
318
+ // identity only changes on the renders that actually add a key, so the
319
+ // `useAnimatedStyle` worklet (which reads `.current` each frame) sees the
320
+ // expansion without churning frame-to-frame.
321
+ const touched = new Set<AnimatableKey>()
322
+ collectTouchedKeys(touched, animateRecord)
323
+ if (initialRecord) collectTouchedKeys(touched, initialRecord)
324
+ if (variants) {
325
+ for (const variant of Object.values(variants) as object[]) {
326
+ if (!variant) continue
327
+ collectTouchedKeys(touched, variant as Record<string, unknown>)
319
328
  }
320
- if (gesture) {
321
- for (const subState of [
322
- gesture.pressed,
323
- gesture.focused,
324
- gesture.focusVisible,
325
- gesture.hovered,
326
- ] as Array<object | undefined>) {
327
- if (!subState) continue
328
- collectTouchedKeys(touched, subState as Record<string, unknown>)
329
+ }
330
+ if (gesture) {
331
+ for (const subState of [
332
+ gesture.pressed,
333
+ gesture.focused,
334
+ gesture.focusVisible,
335
+ gesture.hovered,
336
+ ] as Array<object | undefined>) {
337
+ if (!subState) continue
338
+ collectTouchedKeys(touched, subState as Record<string, unknown>)
339
+ }
340
+ }
341
+ if (exitRecord) collectTouchedKeys(touched, exitRecord)
342
+
343
+ const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
344
+ const hasTransformRef = useRef<boolean>(false)
345
+ const hasShadowOffsetRef = useRef<boolean>(false)
346
+ // Expand the active set only when this render touched a key we haven't
347
+ // recorded yet. When nothing new appears we keep the existing array
348
+ // identity so the worklet's captured ref doesn't see a fresh value.
349
+ const prevActive = activeKeysRef.current
350
+ let grew = prevActive === null
351
+ if (!grew && prevActive) {
352
+ for (const k of touched) {
353
+ if (!prevActive.includes(k)) {
354
+ grew = true
355
+ break
329
356
  }
330
357
  }
331
- if (exitRecord) collectTouchedKeys(touched, exitRecord)
332
- activeKeysRef.current = ALL_KEYS.filter((k) => touched.has(k))
333
358
  }
334
- const hasTransformRef = useRef<boolean>(
335
- activeKeysRef.current.some((k) => TRANSFORM_KEY_SET.has(k)),
336
- )
337
- const hasShadowOffsetRef = useRef<boolean>(
338
- activeKeysRef.current.some((k) => SHADOW_OFFSET_KEY_SET.has(k)),
339
- )
359
+ if (grew) {
360
+ const merged = new Set<AnimatableKey>(prevActive ?? [])
361
+ for (const k of touched) merged.add(k)
362
+ activeKeysRef.current = ALL_KEYS.filter((k) => merged.has(k))
363
+ hasTransformRef.current = activeKeysRef.current.some((k) =>
364
+ TRANSFORM_KEY_SET.has(k),
365
+ )
366
+ hasShadowOffsetRef.current = activeKeysRef.current.some((k) =>
367
+ SHADOW_OFFSET_KEY_SET.has(k),
368
+ )
369
+ }
340
370
 
341
371
  const sharedValues = useAnimatableSharedValues((key) => {
342
372
  // Shadow offset synthetics seed from the corresponding axis on the
@@ -1,6 +1,7 @@
1
+ import { reanimatedVersion } from 'react-native-reanimated'
2
+
1
3
  declare const __DEV__: boolean
2
4
  declare const process: { env?: Record<string, string | undefined> }
3
- declare const require: (path: string) => unknown
4
5
 
5
6
  let alreadyChecked = false
6
7
 
@@ -30,16 +31,11 @@ export function ensureReanimatedInstalled(): void {
30
31
  }
31
32
  alreadyChecked = true
32
33
 
33
- let version: string | undefined
34
- try {
35
- const pkg = require('react-native-reanimated/package.json') as {
36
- version?: string
37
- }
38
- version = pkg.version
39
- } catch {
40
- // package.json subpath blocked by `exports` field — skip the version
41
- // probe rather than emit a misleading error.
42
- }
34
+ // Read the version off Reanimated's own runtime export rather than reaching
35
+ // into its `package.json`. A `require('.../package.json')` here would make
36
+ // esbuild emit a `__require` shim that throws on web bundlers (Expo web), and
37
+ // Reanimated's `exports` field may block the subpath anyway.
38
+ const version: string | undefined = reanimatedVersion
43
39
 
44
40
  if (version) {
45
41
  const major = parseInt(version.split('.')[0] ?? '0', 10)
package/src/types.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { type ComponentType } from 'react'
1
+ import {
2
+ type ComponentProps,
3
+ type ComponentType,
4
+ type ReactElement,
5
+ type Ref,
6
+ } from 'react'
2
7
  import { type StyleProp } from 'react-native'
3
8
 
4
9
  /**
@@ -239,8 +244,15 @@ export interface VariantController<K extends string = string> {
239
244
 
240
245
  /**
241
246
  * Props injected onto every Motion primitive.
247
+ *
248
+ * The second type parameter `V` is the concrete `variants` map. It is inferred
249
+ * from the `variants` prop at each JSX use (see `MotionComponent`), which is
250
+ * what lets `animate` narrow to the variant key union and reject typos. When
251
+ * no `variants` prop is passed, `V` falls back to `VariantsMap<C>` — whose key
252
+ * type is the open `string`, so `animate` still accepts any string and nothing
253
+ * regresses for the variant-less case.
242
254
  */
243
- export interface MotionProps<C> {
255
+ export interface MotionProps<C, V extends VariantsMap<C> = VariantsMap<C>> {
244
256
  /**
245
257
  * Initial values applied on mount. Read once on mount and intentionally
246
258
  * non-reactive — to reset after a state change, change the component `key`,
@@ -251,10 +263,11 @@ export interface MotionProps<C> {
251
263
  initial?: AnimateStyle<C> | false
252
264
  /**
253
265
  * The animation target. A style object, a variant key (when `variants` is
254
- * supplied), or an array of sequence steps. Variant keys autocomplete when
255
- * `variants` is annotated `as const`.
266
+ * supplied), or an array of sequence steps. When `variants` is set, the
267
+ * string form is narrowed to the map's keys, so a key typo is a compile
268
+ * error and the keys autocomplete — no `as const` required.
256
269
  */
257
- animate?: AnimateStyle<C> | string
270
+ animate?: AnimateStyle<C> | (keyof V & string)
258
271
  /**
259
272
  * Values applied while the component exits via `<Presence>`.
260
273
  */
@@ -263,13 +276,13 @@ export interface MotionProps<C> {
263
276
  * Named animation states. With `variants` set, `animate` accepts a key from
264
277
  * this map.
265
278
  */
266
- variants?: VariantsMap<C>
279
+ variants?: V
267
280
  /**
268
281
  * Imperative controller from `useVariants(...)`. When supplied, `animate`
269
282
  * is read from `controller.current` and re-applied whenever the controller
270
283
  * transitions. `animate` and `controller` should not both be set.
271
284
  */
272
- controller?: VariantController
285
+ controller?: VariantController<keyof V & string>
273
286
  /**
274
287
  * Gesture-driven sub-states (`pressed`, `focused`, `focusVisible`,
275
288
  * `hovered`). When omitted, no handlers are mounted on the underlying
@@ -311,8 +324,11 @@ export interface MotionProps<C> {
311
324
  * recorded rect to its natural position via a FLIP transform stack.
312
325
  *
313
326
  * Reanimated 4 removed the `sharedTransitionTag` API — `layoutId` is the
314
- * Inertia-side measure-based replacement. Rects are stored in window
315
- * coordinates so the source and target can live on different screens.
327
+ * Inertia-side measure-based replacement. Rects are recorded in
328
+ * parent-relative coordinates (from `onLayout`), which composes when the
329
+ * source and target screens share an outer content container (the common
330
+ * stack-navigator case); nested-parent layouts need the v2
331
+ * window-coordinate path.
316
332
  *
317
333
  * The same `transition` prop drives the FLIP animation (spring by
318
334
  * default; `'timing'` honored; `'decay'` downgrades to spring; reduced
@@ -331,14 +347,37 @@ export interface MotionProps<C> {
331
347
  }
332
348
 
333
349
  /**
334
- * The component type produced by `createMotionComponent`. Combines the
335
- * underlying component's props (minus `style`, which we replace with an
336
- * animated style) with the Motion-specific props above.
350
+ * Props of a Motion primitive for a given underlying component `C` and a
351
+ * concrete variants map `V`: the component's own props (minus `style`, which
352
+ * we replace with an animated style) intersected with the Motion props.
337
353
  */
338
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
339
- export type MotionComponent<C extends ComponentType<any>> = ComponentType<
340
- Omit<React.ComponentProps<C>, 'style'> &
341
- MotionProps<React.ComponentProps<C>> & {
342
- style?: React.ComponentProps<C>['style']
343
- }
344
- >
354
+ export type MotionComponentProps<
355
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
356
+ C extends ComponentType<any>,
357
+ V extends VariantsMap<ComponentProps<C>> = VariantsMap<ComponentProps<C>>,
358
+ > = Omit<ComponentProps<C>, 'style'> &
359
+ MotionProps<ComponentProps<C>, V> & {
360
+ style?: ComponentProps<C>['style']
361
+ ref?: Ref<unknown>
362
+ }
363
+
364
+ /**
365
+ * The component type produced by `createMotionComponent`.
366
+ *
367
+ * It is a **generic call signature**, not a plain `ComponentType`: the variant
368
+ * map `V` is inferred from the `variants` prop at each JSX use. That inference
369
+ * is what narrows `animate`'s string form to the variant keys, so
370
+ * `<Motion.View variants={{ open, closed }} animate="opne" />` is a compile
371
+ * error and `open` / `closed` autocomplete. With no `variants` prop, `V` falls
372
+ * back to the open `VariantsMap`, so `animate` still accepts any string and the
373
+ * variant-less call site is unchanged.
374
+ */
375
+ export interface MotionComponent<
376
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
377
+ C extends ComponentType<any>,
378
+ > {
379
+ <V extends VariantsMap<ComponentProps<C>> = VariantsMap<ComponentProps<C>>>(
380
+ props: MotionComponentProps<C, V>,
381
+ ): ReactElement | null
382
+ displayName?: string
383
+ }