@onlynative/inertia 0.0.1-alpha.2 → 0.0.1-alpha.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/README.md +29 -2
  2. package/dist/index.d.mts +2 -2
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +170 -58
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +171 -59
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/motion/Image.d.mts +1 -1
  9. package/dist/motion/Image.d.ts +1 -1
  10. package/dist/motion/Image.js +170 -58
  11. package/dist/motion/Image.js.map +1 -1
  12. package/dist/motion/Image.mjs +171 -59
  13. package/dist/motion/Image.mjs.map +1 -1
  14. package/dist/motion/Pressable.d.mts +1 -1
  15. package/dist/motion/Pressable.d.ts +1 -1
  16. package/dist/motion/Pressable.js +170 -58
  17. package/dist/motion/Pressable.js.map +1 -1
  18. package/dist/motion/Pressable.mjs +171 -59
  19. package/dist/motion/Pressable.mjs.map +1 -1
  20. package/dist/motion/ScrollView.d.mts +1 -1
  21. package/dist/motion/ScrollView.d.ts +1 -1
  22. package/dist/motion/ScrollView.js +170 -58
  23. package/dist/motion/ScrollView.js.map +1 -1
  24. package/dist/motion/ScrollView.mjs +171 -59
  25. package/dist/motion/ScrollView.mjs.map +1 -1
  26. package/dist/motion/Text.d.mts +1 -1
  27. package/dist/motion/Text.d.ts +1 -1
  28. package/dist/motion/Text.js +170 -58
  29. package/dist/motion/Text.js.map +1 -1
  30. package/dist/motion/Text.mjs +171 -59
  31. package/dist/motion/Text.mjs.map +1 -1
  32. package/dist/motion/View.d.mts +1 -1
  33. package/dist/motion/View.d.ts +1 -1
  34. package/dist/motion/View.js +170 -58
  35. package/dist/motion/View.js.map +1 -1
  36. package/dist/motion/View.mjs +171 -59
  37. package/dist/motion/View.mjs.map +1 -1
  38. package/dist/{types-DeZZzE_e.d.mts → types-DAhX3fC2.d.mts} +40 -16
  39. package/dist/{types-DeZZzE_e.d.ts → types-DAhX3fC2.d.ts} +40 -16
  40. package/llms.txt +25 -2
  41. package/package.json +1 -1
  42. package/src/motion/createMotionComponent.tsx +258 -97
  43. package/src/motion/installCheck.ts +69 -0
  44. package/src/types.ts +44 -16
@@ -7,6 +7,7 @@ import {
7
7
  useState,
8
8
  } from 'react'
9
9
  import Animated, {
10
+ interpolateColor,
10
11
  runOnJS,
11
12
  useAnimatedStyle,
12
13
  useSharedValue,
@@ -15,11 +16,13 @@ import Animated, {
15
16
  import { useShouldReduceMotion } from '../config'
16
17
  import { isFocusVisible } from '../gestures'
17
18
  import { usePresence } from '../presence'
18
- import { resolveAnimatableValue } from '../transitions'
19
+ import { resolveAnimatableValue, resolveTransition } from '../transitions'
20
+ import { ensureReanimatedInstalled } from './installCheck'
19
21
  import {
20
22
  type AnimatableValue,
21
23
  type AnimateStyle,
22
24
  type AnimationCallbackInfo,
25
+ type GestureLayerTransitions,
23
26
  type GestureSubStates,
24
27
  type MotionComponent,
25
28
  type MotionProps,
@@ -84,6 +87,16 @@ type AnimatableKey = (typeof ALL_KEYS)[number]
84
87
  type TransformKey = (typeof TRANSFORM_KEYS)[number]
85
88
 
86
89
  const TRANSFORM_KEY_SET = new Set<AnimatableKey>(TRANSFORM_KEYS)
90
+ const COLOR_KEY_SET = new Set<AnimatableKey>(COLOR_KEYS)
91
+
92
+ const GESTURE_LAYER_NAMES = [
93
+ 'hovered',
94
+ 'focused',
95
+ 'focusVisible',
96
+ 'pressed',
97
+ ] as const
98
+ type GestureLayerName = (typeof GESTURE_LAYER_NAMES)[number]
99
+ const GESTURE_LAYER_NAME_SET = new Set<string>(GESTURE_LAYER_NAMES)
87
100
 
88
101
  // Stable style object applied while a Motion primitive is mid-exit so taps
89
102
  // fall through. Hoisted so every render shares the same reference and
@@ -140,9 +153,24 @@ function transitionFor<S>(
140
153
  ): TransitionConfig | undefined {
141
154
  if (!transition) return undefined
142
155
  if (isTopLevelTransition(transition)) return transition
156
+ // Gesture-layer keys (`pressed`, `hovered`, …) live on the same map as
157
+ // per-property keys; skip them when looking up a property transition so a
158
+ // user who wires `transition.pressed` doesn't accidentally apply that to a
159
+ // style key named `pressed` (none currently exist, but keep the lookup
160
+ // honest).
161
+ if (GESTURE_LAYER_NAME_SET.has(prop as string)) return undefined
143
162
  return (transition as PerPropertyTransition<S>)[prop]
144
163
  }
145
164
 
165
+ function gestureLayerTransitionFor<S>(
166
+ layer: GestureLayerName,
167
+ transition: Transition<S> | undefined,
168
+ ): TransitionConfig | undefined {
169
+ if (!transition) return undefined
170
+ if (isTopLevelTransition(transition)) return transition
171
+ return (transition as GestureLayerTransitions)[layer]
172
+ }
173
+
146
174
  /**
147
175
  * Factory that wraps a React Native primitive as a `Motion.*` component.
148
176
  *
@@ -154,10 +182,17 @@ function transitionFor<S>(
154
182
  * borderRadius) and color properties (backgroundColor, borderColor, color,
155
183
  * tintColor) applied via Reanimated shared values + `useAnimatedStyle`.
156
184
  */
185
+ // `ComponentType<any>` is React's canonical "accept any component" idiom.
186
+ // `unknown` doesn't work — props need to widen to whatever `C` actually accepts
187
+ // at the call site. The two `any` uses below are deliberate.
188
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
157
189
  export function createMotionComponent<C extends ComponentType<any>>(
158
190
  Component: C,
159
191
  ): MotionComponent<C> {
192
+ ensureReanimatedInstalled()
193
+
160
194
  const AnimatedComponent = Animated.createAnimatedComponent(
195
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
161
196
  Component as ComponentType<any>,
162
197
  )
163
198
 
@@ -220,10 +255,10 @@ export function createMotionComponent<C extends ComponentType<any>>(
220
255
  >)
221
256
  : undefined
222
257
 
223
- // Gesture sub-state activation tracked as JS state so changes invalidate
224
- // the merged-target signature and re-run the animation effect. The cost
225
- // is four useState slots regardless of whether `gesture` is set; that's
226
- // tiny and lets us stay rules-of-hooks-clean.
258
+ // Gesture sub-state activation tracked as JS state. Activation flips drive
259
+ // the per-layer progress shared values (0↔1); they intentionally do NOT
260
+ // re-run the value-driving effect gesture sub-state targets live on the
261
+ // worklet's composition chain, not on the base `animate` SV.
227
262
  const [pressed, setPressed] = useState(false)
228
263
  const [focused, setFocused] = useState(false)
229
264
  const [focusVisible, setFocusVisible] = useState(false)
@@ -233,7 +268,8 @@ export function createMotionComponent<C extends ComponentType<any>>(
233
268
  // variants in play the union across all variants is what matters — a key
234
269
  // touched by any variant must be active so the worklet picks it up when
235
270
  // the controller transitions. Gesture sub-states join the same union so
236
- // pressed/focused/hovered targets can drive any key they declare.
271
+ // pressed/focused/focusVisible/hovered targets can drive any key they
272
+ // declare even when the base `animate` doesn't touch it.
237
273
  const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
238
274
  if (activeKeysRef.current === null) {
239
275
  const touched = new Set<AnimatableKey>()
@@ -285,24 +321,46 @@ export function createMotionComponent<C extends ComponentType<any>>(
285
321
  )
286
322
  })
287
323
 
288
- // Merge gesture sub-state targets over the base `animate` record. Keys
289
- // touched by any sub-state always appear in the merged record (falling
290
- // back to `animateRecord` or `DEFAULT_RESTING`) so releasing a gesture
291
- // animates back to a defined value rather than getting skipped.
324
+ // One progress SV per gesture layer, allocated unconditionally for hook
325
+ // stability. Each layer's progress animates 0↔1 with its own transition
326
+ // when its activation flips; the worklet reads them when compositing.
327
+ // Initial value is 0 — even if a sub-state is somehow active on mount,
328
+ // the activation effect below will animate it to 1 on the next tick.
329
+ const pressedProgress = useSharedValue(0)
330
+ const focusedProgress = useSharedValue(0)
331
+ const focusVisibleProgress = useSharedValue(0)
332
+ const hoveredProgress = useSharedValue(0)
333
+
334
+ // Mirror gesture targets into a UI-runtime-resident shared value so the
335
+ // animated-style worklet can read the latest layer values without having
336
+ // to capture `gesture` directly (which would re-register the worklet on
337
+ // every render where the consumer passes a fresh literal). The signature
338
+ // dependency means we only push to the SV when targets actually change —
339
+ // the SV ref itself is stable across renders.
292
340
  //
293
- // While exiting, exit values override everything gesture / animate
294
- // targets are inert because the component is on its way out.
295
- const mergedRecord =
341
+ // The resolved value is a layer-keyed map of primitive endpoints (numbers
342
+ // or color strings); sequence/`{ to }` step shapes on a sub-state collapse
343
+ // to their final endpoint via `targetEndValue` because a gesture layer
344
+ // describes a steady target, not a keyframe sequence.
345
+ const gestureSV = useSharedValue<ResolvedGestureLayers | null>(
346
+ resolveGestureLayers(gesture),
347
+ )
348
+ const gestureTargetsSig = stableSig(gesture)
349
+ useEffect(() => {
350
+ gestureSV.value = resolveGestureLayers(gesture)
351
+ // eslint-disable-next-line react-hooks/exhaustive-deps
352
+ }, [gestureTargetsSig])
353
+
354
+ // The base record drives the per-key shared values. Gesture sub-state
355
+ // targets are intentionally NOT merged here — they layer on top in the
356
+ // worklet. Exit values still take precedence over `animate` while exiting
357
+ // because the base SV is what <Presence> waits on to settle.
358
+ const baseRecord =
296
359
  isExiting && exitRecord
297
360
  ? { ...animateRecord, ...exitRecord }
298
- : mergeGestureTargets(animateRecord, gesture, {
299
- pressed,
300
- focused,
301
- focusVisible,
302
- hovered,
303
- })
304
- const mergedSig =
305
- stableSig(mergedRecord) +
361
+ : animateRecord
362
+ const baseSig =
363
+ stableSig(baseRecord) +
306
364
  (isExiting ? '|exit' : '') +
307
365
  (shouldReduceMotion ? '|rm' : '')
308
366
  const transitionSig = stableSig(transition)
@@ -337,7 +395,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
337
395
  // the factory skip the coalescing branch entirely.
338
396
  let transformPending = 0
339
397
  for (const k of ALL_KEYS) {
340
- if (TRANSFORM_KEY_SET.has(k) && mergedRecord[k] !== undefined) {
398
+ if (TRANSFORM_KEY_SET.has(k) && baseRecord[k] !== undefined) {
341
399
  transformPending++
342
400
  }
343
401
  }
@@ -345,7 +403,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
345
403
  transformPending > 0 ? { remaining: transformPending } : undefined
346
404
 
347
405
  for (const key of ALL_KEYS) {
348
- const target = mergedRecord[key]
406
+ const target = baseRecord[key]
349
407
  if (target === undefined) continue
350
408
  // Reduced-motion overrides every per-key transition (and any nested
351
409
  // sequence-step transition) with `no-animation`, which the resolver
@@ -382,15 +440,117 @@ export function createMotionComponent<C extends ComponentType<any>>(
382
440
  safeToRemoveRef.current?.()
383
441
  }
384
442
  // eslint-disable-next-line react-hooks/exhaustive-deps
385
- }, [mergedSig, transitionSig])
443
+ }, [baseSig, transitionSig])
444
+
445
+ // Per-layer progress: when a sub-state activation flips, animate its
446
+ // progress SV 0↔1 with the layer's own transition (or the parent
447
+ // transition / library default, in priority order). On exit we snap every
448
+ // layer to 0 instantly so the unmount-bound base SV isn't fighting a
449
+ // stale layer contribution mid-fade.
450
+ //
451
+ // The `declared` flag short-circuits the effect when the consumer hasn't
452
+ // wired the corresponding sub-state — so a Motion primitive without a
453
+ // `gesture` prop (or with only some sub-states declared) makes zero extra
454
+ // `withSpring` / `withTiming` calls on mount.
455
+ useGestureLayerProgress(
456
+ pressedProgress,
457
+ pressed,
458
+ gesture?.pressed != null,
459
+ 'pressed',
460
+ transition,
461
+ isExiting,
462
+ shouldReduceMotion,
463
+ )
464
+ useGestureLayerProgress(
465
+ focusedProgress,
466
+ focused,
467
+ gesture?.focused != null,
468
+ 'focused',
469
+ transition,
470
+ isExiting,
471
+ shouldReduceMotion,
472
+ )
473
+ useGestureLayerProgress(
474
+ focusVisibleProgress,
475
+ focusVisible,
476
+ gesture?.focusVisible != null,
477
+ 'focusVisible',
478
+ transition,
479
+ isExiting,
480
+ shouldReduceMotion,
481
+ )
482
+ useGestureLayerProgress(
483
+ hoveredProgress,
484
+ hovered,
485
+ gesture?.hovered != null,
486
+ 'hovered',
487
+ transition,
488
+ isExiting,
489
+ shouldReduceMotion,
490
+ )
386
491
 
387
492
  const animatedStyle = useAnimatedStyle(() => {
388
493
  const activeKeys = activeKeysRef.current!
389
494
  const hasTransform = hasTransformRef.current
390
495
  const out: Record<string, unknown> = {}
391
496
  const transform: Array<Record<string, unknown>> = []
497
+
498
+ // Read each progress SV exactly once so the chain below sees a coherent
499
+ // snapshot for this frame. Reading them on the UI thread is cheap.
500
+ const ph = hoveredProgress.value
501
+ const pf = focusedProgress.value
502
+ const pfv = focusVisibleProgress.value
503
+ const pp = pressedProgress.value
504
+
505
+ const layers = gestureSV.value
506
+ // Locals are suffixed `Layer` so they don't shadow the outer `pressed` /
507
+ // `focused` / `focusVisible` / `hovered` JS-state booleans — Reanimated's
508
+ // worklet closure tracker would otherwise pick those up as captured
509
+ // dependencies and re-register the worklet on every activation flip.
510
+ const hoveredLayer = layers ? layers.hovered : null
511
+ const focusedLayer = layers ? layers.focused : null
512
+ const focusVisibleLayer = layers ? layers.focusVisible : null
513
+ const pressedLayer = layers ? layers.pressed : null
514
+
392
515
  for (const key of activeKeys) {
393
- const v = sharedValues[key].value
516
+ let v = sharedValues[key].value
517
+ const isColor = COLOR_KEY_SET.has(key)
518
+
519
+ // Composite gesture layers in priority order (lowest first). Each
520
+ // active layer pulls the value toward its pre-resolved primitive
521
+ // endpoint by `progress`; numeric keys lerp, color keys go through
522
+ // Reanimated's RGBA `interpolateColor`. We skip layers with progress
523
+ // 0 to avoid an `interpolateColor(0, ...)` call that would parse the
524
+ // target color string for no visible effect.
525
+ if (hoveredLayer && ph > 0 && hoveredLayer[key] !== undefined) {
526
+ const t = hoveredLayer[key]
527
+ v = isColor
528
+ ? interpolateColor(ph, [0, 1], [v as string, t as string])
529
+ : (v as number) + ((t as number) - (v as number)) * ph
530
+ }
531
+ if (focusedLayer && pf > 0 && focusedLayer[key] !== undefined) {
532
+ const t = focusedLayer[key]
533
+ v = isColor
534
+ ? interpolateColor(pf, [0, 1], [v as string, t as string])
535
+ : (v as number) + ((t as number) - (v as number)) * pf
536
+ }
537
+ if (
538
+ focusVisibleLayer &&
539
+ pfv > 0 &&
540
+ focusVisibleLayer[key] !== undefined
541
+ ) {
542
+ const t = focusVisibleLayer[key]
543
+ v = isColor
544
+ ? interpolateColor(pfv, [0, 1], [v as string, t as string])
545
+ : (v as number) + ((t as number) - (v as number)) * pfv
546
+ }
547
+ if (pressedLayer && pp > 0 && pressedLayer[key] !== undefined) {
548
+ const t = pressedLayer[key]
549
+ v = isColor
550
+ ? interpolateColor(pp, [0, 1], [v as string, t as string])
551
+ : (v as number) + ((t as number) - (v as number)) * pp
552
+ }
553
+
394
554
  if (TRANSFORM_KEY_SET.has(key)) {
395
555
  transform.push(
396
556
  key === 'rotate' ? { rotate: `${v}deg` } : { [key]: v },
@@ -770,78 +930,72 @@ function stableStringify(v: unknown): string {
770
930
  }
771
931
 
772
932
  /**
773
- * Merge gesture sub-state targets over the base `animate` record. Keys touched
774
- * by any declared sub-state are always present in the result so releasing a
775
- * gesture animates the property back to a defined value (the base `animate`
776
- * value when present, otherwise `DEFAULT_RESTING`). Sub-states layer in
777
- * priority order (lowest first):
778
- * `hovered` < `focused` < `focusVisible` < `pressed`.
933
+ * Per-layer resolved targets: each declared gesture sub-state collapses to a
934
+ * map of primitive endpoints (numbers or color strings), already passed
935
+ * through `targetEndValue` so the worklet can use them directly without
936
+ * inspecting `AnimatableValue` shapes on the UI thread.
779
937
  */
780
- function mergeGestureTargets(
781
- base: Partial<Record<AnimatableKey, AnimatableValue<number | string>>>,
938
+ type ResolvedGestureLayers = {
939
+ pressed?: Record<string, number | string>
940
+ focused?: Record<string, number | string>
941
+ focusVisible?: Record<string, number | string>
942
+ hovered?: Record<string, number | string>
943
+ }
944
+
945
+ function resolveGestureLayers(
782
946
  gesture: GestureSubStates<unknown> | undefined,
783
- active: {
784
- pressed: boolean
785
- focused: boolean
786
- focusVisible: boolean
787
- hovered: boolean
788
- },
789
- ): Partial<Record<AnimatableKey, AnimatableValue<number | string>>> {
790
- if (!gesture) return base
791
- const merged: Partial<
792
- Record<AnimatableKey, AnimatableValue<number | string>>
793
- > = {
794
- ...base,
795
- }
796
- const subStates = [
797
- gesture.hovered,
798
- gesture.focused,
799
- gesture.focusVisible,
800
- gesture.pressed,
801
- ] as Array<
802
- Partial<Record<AnimatableKey, AnimatableValue<number | string>>> | undefined
803
- >
804
- for (const sub of subStates) {
805
- if (!sub) continue
806
- for (const k of ALL_KEYS) {
807
- if (k in sub && !(k in merged)) {
808
- merged[k] = DEFAULT_RESTING[k]
809
- }
947
+ ): ResolvedGestureLayers | null {
948
+ if (!gesture) return null
949
+ const out: ResolvedGestureLayers = {}
950
+ for (const layer of GESTURE_LAYER_NAMES) {
951
+ const subState = gesture[layer]
952
+ if (!subState) continue
953
+ const resolved: Record<string, number | string> = {}
954
+ for (const key of ALL_KEYS) {
955
+ const raw = (subState as Record<string, unknown>)[key]
956
+ if (raw === undefined) continue
957
+ const t = targetEndValue(raw as AnimatableValue<number | string>)
958
+ if (t !== undefined) resolved[key] = t
810
959
  }
960
+ out[layer] = resolved
811
961
  }
812
- if (active.hovered && gesture.hovered) {
813
- Object.assign(
814
- merged,
815
- gesture.hovered as Partial<
816
- Record<AnimatableKey, AnimatableValue<number | string>>
817
- >,
818
- )
819
- }
820
- if (active.focused && gesture.focused) {
821
- Object.assign(
822
- merged,
823
- gesture.focused as Partial<
824
- Record<AnimatableKey, AnimatableValue<number | string>>
825
- >,
826
- )
827
- }
828
- if (active.focusVisible && gesture.focusVisible) {
829
- Object.assign(
830
- merged,
831
- gesture.focusVisible as Partial<
832
- Record<AnimatableKey, AnimatableValue<number | string>>
833
- >,
834
- )
835
- }
836
- if (active.pressed && gesture.pressed) {
837
- Object.assign(
838
- merged,
839
- gesture.pressed as Partial<
840
- Record<AnimatableKey, AnimatableValue<number | string>>
841
- >,
842
- )
843
- }
844
- return merged
962
+ return out
963
+ }
964
+
965
+ /**
966
+ * Drive a single gesture layer's progress shared value 0↔1 with its own
967
+ * transition. Resolution priority for the layer config:
968
+ * `transition.<layerName>` → top-level `transition` → library default spring.
969
+ * On exit, snap to 0 instantly so the unmount-bound base SV finishes its exit
970
+ * animation without a stale layer pulling the value off-target.
971
+ *
972
+ * The hook is invoked unconditionally (one call per layer) so hook order
973
+ * stays stable even when `gesture` adds or removes sub-states across renders.
974
+ */
975
+ function useGestureLayerProgress<S>(
976
+ progress: SharedValue<number>,
977
+ active: boolean,
978
+ declared: boolean,
979
+ layer: GestureLayerName,
980
+ transition: Transition<S> | undefined,
981
+ isExiting: boolean,
982
+ shouldReduceMotion: boolean,
983
+ ): void {
984
+ const layerCfgSig = stableSig(gestureLayerTransitionFor(layer, transition))
985
+ useEffect(() => {
986
+ if (!declared) return
987
+ if (isExiting) {
988
+ progress.value = 0
989
+ return
990
+ }
991
+ const target = active ? 1 : 0
992
+ const cfg = shouldReduceMotion
993
+ ? ({ type: 'no-animation' } as const)
994
+ : (gestureLayerTransitionFor(layer, transition) ??
995
+ ({ type: 'spring' } as const))
996
+ progress.value = resolveTransition(cfg, target) as never
997
+ // eslint-disable-next-line react-hooks/exhaustive-deps
998
+ }, [active, declared, isExiting, shouldReduceMotion, layerCfgSig])
845
999
  }
846
1000
 
847
1001
  type GestureHandlers = Record<string, (event: unknown) => void>
@@ -863,6 +1017,13 @@ function useGestureHandlers(
863
1017
  setFocusVisible: (next: boolean) => void,
864
1018
  setHovered: (next: boolean) => void,
865
1019
  ): GestureHandlers {
1020
+ // Deps key on declared-ness, not object identity — a fresh `gesture={...}`
1021
+ // literal each render must not rebuild handlers if the same sub-states are
1022
+ // declared.
1023
+ const hasPressed = gesture?.pressed ? 1 : 0
1024
+ const hasFocused = gesture?.focused ? 1 : 0
1025
+ const hasFocusVisible = gesture?.focusVisible ? 1 : 0
1026
+ const hasHovered = gesture?.hovered ? 1 : 0
866
1027
  return useMemo(() => {
867
1028
  if (!gesture) return {}
868
1029
  const handlers: GestureHandlers = {}
@@ -903,10 +1064,10 @@ function useGestureHandlers(
903
1064
  return handlers
904
1065
  // eslint-disable-next-line react-hooks/exhaustive-deps
905
1066
  }, [
906
- gesture?.pressed ? 1 : 0,
907
- gesture?.focused ? 1 : 0,
908
- gesture?.focusVisible ? 1 : 0,
909
- gesture?.hovered ? 1 : 0,
1067
+ hasPressed,
1068
+ hasFocused,
1069
+ hasFocusVisible,
1070
+ hasHovered,
910
1071
  rest.onTouchStart,
911
1072
  rest.onTouchEnd,
912
1073
  rest.onTouchCancel,
@@ -0,0 +1,69 @@
1
+ declare const __DEV__: boolean
2
+ declare const process: { env?: Record<string, string | undefined> }
3
+ declare const require: (path: string) => unknown
4
+
5
+ let alreadyChecked = false
6
+
7
+ /**
8
+ * Surface a clear, actionable error at first `createMotionComponent` call when
9
+ * the consumer's Reanimated install is broken. Production builds, repeat calls,
10
+ * and Jest test runs are all skipped — the check is purely a dev-time
11
+ * paper-cut sander for the two failure modes we can detect from JS:
12
+ *
13
+ * 1. `react-native-reanimated` resolves but is on a v3.x line we don't
14
+ * support (the plugin name and worklet runtime both changed at v4).
15
+ * 2. The worklets babel plugin (`react-native-worklets/plugin` in v4) isn't
16
+ * wired into `babel.config.js`, so `'worklet'` directives are dead strings
17
+ * and the first `withSpring` / `withTiming` call would crash on the UI
18
+ * thread with a generic "non-worklet function called" error.
19
+ *
20
+ * The "Reanimated isn't installed at all" case isn't handled here — Metro
21
+ * fails to resolve the static `import 'react-native-reanimated'` at the top
22
+ * of `createMotionComponent.tsx` long before this check runs.
23
+ */
24
+ export function ensureReanimatedInstalled(): void {
25
+ if (!__DEV__ || alreadyChecked) return
26
+ // The standard `react-native-reanimated/mock` doesn't run the worklets
27
+ // babel plugin, so the marker probe would false-positive every test run.
28
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') {
29
+ return
30
+ }
31
+ alreadyChecked = true
32
+
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
+ }
43
+
44
+ if (version) {
45
+ const major = parseInt(version.split('.')[0] ?? '0', 10)
46
+ if (major < 4) {
47
+ console.error(
48
+ `[inertia] react-native-reanimated v${version} is installed, but @onlynative/inertia requires v4.0.0 or later. ` +
49
+ `Upgrade with \`pnpm add react-native-reanimated@^4\` (or your package manager's equivalent).`,
50
+ )
51
+ return
52
+ }
53
+ }
54
+
55
+ // The worklets plugin rewrites any function carrying a top-of-body
56
+ // `'worklet'` directive to expose a `__workletHash` property at runtime.
57
+ // Its absence means the plugin didn't run.
58
+ const probe = function probe() {
59
+ 'worklet'
60
+ return 0
61
+ } as { __workletHash?: number }
62
+ if (typeof probe.__workletHash !== 'number') {
63
+ console.error(
64
+ `[inertia] The Reanimated worklets babel plugin is not configured. ` +
65
+ `Add \`'react-native-worklets/plugin'\` as the LAST entry in the \`plugins\` array of your \`babel.config.js\`, ` +
66
+ `then restart Metro with a fresh cache: \`npx expo start -c\` or \`npx react-native start --reset-cache\`.`,
67
+ )
68
+ }
69
+ }
package/src/types.ts CHANGED
@@ -75,7 +75,25 @@ export type PerPropertyTransition<S> = {
75
75
  [K in keyof S]?: TransitionConfig
76
76
  }
77
77
 
78
- export type Transition<S> = TransitionConfig | PerPropertyTransition<S>
78
+ /**
79
+ * Per-gesture-layer transition map. Each `gesture` sub-state animates a
80
+ * progress value 0↔1 with its own transition; the worklet composites the
81
+ * layers in priority order (`hovered → focused → focusVisible → pressed`).
82
+ *
83
+ * Keys live on the same `transition` object as `PerPropertyTransition` because
84
+ * the only other place they could go (nested inside `gesture` itself) would
85
+ * collide with the primitive's inferred style keys.
86
+ */
87
+ export interface GestureLayerTransitions {
88
+ pressed?: TransitionConfig
89
+ focused?: TransitionConfig
90
+ focusVisible?: TransitionConfig
91
+ hovered?: TransitionConfig
92
+ }
93
+
94
+ export type Transition<S> =
95
+ | TransitionConfig
96
+ | (PerPropertyTransition<S> & GestureLayerTransitions)
79
97
 
80
98
  /**
81
99
  * The animation state shape inferred from the underlying component's style
@@ -124,18 +142,26 @@ export type VariantsMap<C> = Record<string, AnimateStyle<C>>
124
142
  * - `hovered` — web-only. Typed for cross-platform call sites; the runtime is
125
143
  * a no-op on native.
126
144
  *
127
- * When a sub-state is active, its values override the base `animate` target
128
- * per-property. Priority on overlap (highest first):
129
- * `pressed` > `focusVisible` > `focused` > `hovered`. `focusVisible` layers
130
- * above `focused` so declaring both yields a state-layer on any focus and a
131
- * ring on keyboard focus only.
145
+ * Sub-states layer additively. Each declared sub-state owns an independent
146
+ * progress value (0↔1) that animates in/out with its own transition; the
147
+ * worklet composites layers in priority order (lowest-to-highest):
148
+ * `hovered focused focusVisible pressed`. Per-property the chain is
149
+ *
150
+ * v = base
151
+ * v = lerp(v, hovered.value, progressHovered) // if declared
152
+ * v = lerp(v, focused.value, progressFocused) // if declared
153
+ * v = lerp(v, focusVisible.value, progressFocusVisible) // if declared
154
+ * v = lerp(v, pressed.value, progressPressed) // if declared
155
+ *
156
+ * (Color-valued keys use `interpolateColor` instead of `lerp`.) When a single
157
+ * sub-state is active, this collapses to "the highest-priority declared layer
158
+ * wins". When multiple are mid-transition (e.g. release-while-still-hovered)
159
+ * each layer fades independently — a press layer fading out at 50ms while a
160
+ * hover layer holds at full opacity matches MD3 state-layer semantics.
132
161
  *
133
- * Sub-states stack as **single-state selection**, not blended interpolation:
134
- * the highest-priority active key's value wins per-property, with one
135
- * transition between target values. Mid-transition cross-fades between
136
- * sub-states (e.g. release-while-still-hovered) follow the standard `transition`
137
- * for that property — the resolver does not run multiple parallel
138
- * interpolations the way a hand-rolled chained-`interpolateColor` would.
162
+ * Configure per-layer fade timing via `transition.<stateName>` on the parent
163
+ * primitive (see `GestureLayerTransitions`); without it, layers default to
164
+ * the parent transition or the library default spring.
139
165
  */
140
166
  export interface GestureSubStates<C> {
141
167
  pressed?: AnimateStyle<C>
@@ -193,10 +219,11 @@ export interface MotionProps<C> {
193
219
  */
194
220
  controller?: VariantController
195
221
  /**
196
- * Gesture-driven sub-states (`pressed`, `focused`, `hovered`). When omitted,
197
- * no handlers are mounted on the underlying component. Sub-state values
198
- * merge over `animate` per-property while the corresponding gesture is
199
- * active.
222
+ * Gesture-driven sub-states (`pressed`, `focused`, `focusVisible`,
223
+ * `hovered`). When omitted, no handlers are mounted on the underlying
224
+ * component. Each declared sub-state animates as an independent layer
225
+ * fading in/out over the base `animate` target — see `GestureSubStates`
226
+ * for the composition model and per-layer transition wiring.
200
227
  */
201
228
  gesture?: GestureSubStates<C>
202
229
  /**
@@ -216,6 +243,7 @@ export interface MotionProps<C> {
216
243
  * underlying component's props (minus `style`, which we replace with an
217
244
  * animated style) with the Motion-specific props above.
218
245
  */
246
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
219
247
  export type MotionComponent<C extends ComponentType<any>> = ComponentType<
220
248
  Omit<React.ComponentProps<C>, 'style'> &
221
249
  MotionProps<React.ComponentProps<C>> & {