@rootnative/inertia 0.0.3 → 0.0.4

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 (74) hide show
  1. package/CHANGELOG.md +41 -5
  2. package/README.md +5 -3
  3. package/dist/{chunk-SGUHE5CX.mjs → chunk-4ASOELLA.mjs} +1 -1
  4. package/dist/{chunk-CSODMRJ7.mjs → chunk-55W7BNO2.mjs} +475 -83
  5. package/dist/chunk-5SRD2WCO.js +8 -0
  6. package/dist/{chunk-PTRF47DA.js → chunk-7A6WTXNK.js} +10 -3
  7. package/dist/{chunk-3XTVY34H.js → chunk-CENTULW6.js} +2 -2
  8. package/dist/{chunk-4PEHWDAZ.js → chunk-D5KAHWMC.js} +7 -7
  9. package/dist/{chunk-O22NXXCZ.mjs → chunk-J5Q46OPW.mjs} +1 -1
  10. package/dist/{chunk-RGNX6UZN.mjs → chunk-K3CLP74S.mjs} +2 -2
  11. package/dist/{chunk-BP3Y2SHQ.js → chunk-MYE5NMXW.js} +495 -101
  12. package/dist/{chunk-OQV66TBQ.mjs → chunk-NAOXQJ7U.mjs} +1 -1
  13. package/dist/{chunk-R63GIUNU.mjs → chunk-QKC3APP4.mjs} +1 -1
  14. package/dist/{chunk-46P57VMY.mjs → chunk-RF7J5FRM.mjs} +1 -1
  15. package/dist/{chunk-CY7Y64C3.mjs → chunk-TU2XFPAK.mjs} +10 -4
  16. package/dist/chunk-TVFNJ25Z.js +8 -0
  17. package/dist/chunk-VLDIFL3F.js +8 -0
  18. package/dist/{chunk-7AOERN53.js → chunk-WVRCKOPO.js} +3 -3
  19. package/dist/{chunk-W5MC3P4N.js → chunk-XMEVPEMH.js} +2 -2
  20. package/dist/{chunk-BQQTHG2V.mjs → chunk-YKGY22SB.mjs} +1 -1
  21. package/dist/gestureLayer/index.d.mts +2 -2
  22. package/dist/gestureLayer/index.d.ts +2 -2
  23. package/dist/gestureLayer/index.js +10 -10
  24. package/dist/gestureLayer/index.mjs +3 -3
  25. package/dist/index.d.mts +4 -4
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +48 -169
  28. package/dist/index.mjs +17 -137
  29. package/dist/motion/Image.d.mts +1 -1
  30. package/dist/motion/Image.d.ts +1 -1
  31. package/dist/motion/Image.js +5 -5
  32. package/dist/motion/Image.mjs +4 -4
  33. package/dist/motion/Pressable.d.mts +1 -1
  34. package/dist/motion/Pressable.d.ts +1 -1
  35. package/dist/motion/Pressable.js +5 -5
  36. package/dist/motion/Pressable.mjs +4 -4
  37. package/dist/motion/ScrollView.d.mts +1 -1
  38. package/dist/motion/ScrollView.d.ts +1 -1
  39. package/dist/motion/ScrollView.js +5 -5
  40. package/dist/motion/ScrollView.mjs +4 -4
  41. package/dist/motion/Text.d.mts +1 -1
  42. package/dist/motion/Text.d.ts +1 -1
  43. package/dist/motion/Text.js +5 -5
  44. package/dist/motion/Text.mjs +4 -4
  45. package/dist/motion/View.d.mts +1 -1
  46. package/dist/motion/View.d.ts +1 -1
  47. package/dist/motion/View.js +5 -5
  48. package/dist/motion/View.mjs +4 -4
  49. package/dist/touch/index.d.mts +1 -1
  50. package/dist/touch/index.d.ts +1 -1
  51. package/dist/touch/index.js +3 -3
  52. package/dist/touch/index.mjs +1 -1
  53. package/dist/{types-G1dBU2IQ.d.mts → types-37pN82yh.d.mts} +49 -14
  54. package/dist/{types-G1dBU2IQ.d.ts → types-37pN82yh.d.ts} +49 -14
  55. package/dist/{useGesture-D1e4ixJo.d.ts → useGesture-Cl4uzZIr.d.ts} +1 -1
  56. package/dist/{useGesture-RFY6kTL3.d.mts → useGesture-_IKkS8Dv.d.mts} +1 -1
  57. package/llms.txt +3 -1
  58. package/package.json +1 -1
  59. package/src/index.ts +2 -1
  60. package/src/internal/boxShadow.ts +177 -6
  61. package/src/internal/nonWorkletWarning.ts +4 -6
  62. package/src/internal/warnOnce.ts +28 -0
  63. package/src/layout/index.ts +10 -0
  64. package/src/layout/measureWindow.ts +135 -0
  65. package/src/layout/sharedRegistry.ts +107 -18
  66. package/src/layout/useSharedLayout.ts +256 -91
  67. package/src/motion/createMotionComponent.tsx +378 -39
  68. package/src/motion/installCheck.ts +1 -1
  69. package/src/transitions/index.ts +1 -1
  70. package/src/types.ts +52 -12
  71. package/src/values/useColorTransition.ts +1 -1
  72. package/dist/chunk-FNVFV4EY.js +0 -8
  73. package/dist/chunk-FWQOXA43.js +0 -8
  74. package/dist/chunk-KBP4LR75.js +0 -8
@@ -22,9 +22,17 @@ import {
22
22
  useShouldReduceMotion,
23
23
  } from '../config'
24
24
  import { isFocusVisible } from '../gestures'
25
+ import {
26
+ normalizeBoxShadow,
27
+ prepareBoxShadowAnimation,
28
+ type AnimatedBoxShadowLayer,
29
+ type SplitBoxShadow,
30
+ } from '../internal/boxShadow'
31
+ import { warnOnce } from '../internal/warnOnce'
25
32
  import {
26
33
  resolveLayoutTransition,
27
34
  type LayoutProp,
35
+ type SharedStyleSnapshot,
28
36
  useSharedLayout,
29
37
  } from '../layout'
30
38
  import { usePresence } from '../presence'
@@ -33,6 +41,7 @@ import {
33
41
  resolveAnimatableValue,
34
42
  resolveTransition,
35
43
  stableSig,
44
+ type CallbackFactory,
36
45
  } from '../transitions'
37
46
  import { ensureReanimatedInstalled } from './installCheck'
38
47
  import {
@@ -42,6 +51,7 @@ import {
42
51
  type GestureLayerTransitions,
43
52
  type GestureSubStates,
44
53
  type MotionComponent,
54
+ type BoxShadowInput,
45
55
  type MotionProps,
46
56
  type PerPropertyTransition,
47
57
  type Transition,
@@ -108,6 +118,42 @@ const COLOR_KEYS = [
108
118
  // these keys directly.
109
119
  const SHADOW_OFFSET_KEYS = ['shadowOffsetWidth', 'shadowOffsetHeight'] as const
110
120
 
121
+ // Keys whose shared value holds a structure rather than a scalar. `boxShadow`
122
+ // is the only one: its slot carries an array of layer objects, which
123
+ // Reanimated's animation drivers recurse into, animating each leaf number and
124
+ // color independently (`arrayOnStart` → `objectOnStart` in Reanimated's
125
+ // `animation/util.ts`).
126
+ //
127
+ // Passing the CSS string through instead would NOT work — a box-shadow string
128
+ // isn't a color, so it lands in Reanimated's prefix-number-suffix branch,
129
+ // which is built for values like '100%' and would pull a single number out of
130
+ // a four-value shadow. All string parsing therefore happens on the JS thread,
131
+ // once per change, in `internal/boxShadow.ts`.
132
+ const STRUCTURED_KEYS = ['boxShadow'] as const
133
+
134
+ // Keys a shared-element transition (`layoutId`) carries from the source
135
+ // element to the target, crossfading them alongside the rect FLIP. Everything
136
+ // here is either a scalar or a color, so the worklet's existing lerp /
137
+ // `interpolateColor` branches handle it with no new machinery.
138
+ //
139
+ // Transform keys are deliberately absent: they are the FLIP's job, and
140
+ // carrying them too would apply the same displacement twice.
141
+ //
142
+ // So is `shadowColor`, despite being a color key. Carrying one of the four
143
+ // native shadow props crossfades a shadow's *color* over geometry that snapped
144
+ // — a worse result than letting the whole shadow snap together. (It would also
145
+ // pull `shadowColor` into the active set on any `layoutId` element whose style
146
+ // declares it, tripping the `boxShadow`-with-native-`shadow*` warning for a
147
+ // consumer who never asked for either.)
148
+ const SHARED_STYLE_KEYS = [
149
+ 'opacity',
150
+ 'borderRadius',
151
+ 'backgroundColor',
152
+ 'borderColor',
153
+ 'color',
154
+ 'tintColor',
155
+ ] as const
156
+
111
157
  /**
112
158
  * Per-effect transform-group coordinator. Counts how many transform-axis
113
159
  * terminal callbacks are still pending; when the last one fires, the
@@ -121,6 +167,7 @@ const ALL_KEYS = [
121
167
  ...NUMERIC_TOP_LEVEL_KEYS,
122
168
  ...COLOR_KEYS,
123
169
  ...SHADOW_OFFSET_KEYS,
170
+ ...STRUCTURED_KEYS,
124
171
  ] as const
125
172
  type AnimatableKey = (typeof ALL_KEYS)[number]
126
173
  type TransformKey = (typeof TRANSFORM_KEYS)[number]
@@ -129,6 +176,20 @@ type ShadowOffsetKey = (typeof SHADOW_OFFSET_KEYS)[number]
129
176
  const TRANSFORM_KEY_SET = new Set<AnimatableKey>(TRANSFORM_KEYS)
130
177
  const COLOR_KEY_SET = new Set<AnimatableKey>(COLOR_KEYS)
131
178
  const SHADOW_OFFSET_KEY_SET = new Set<AnimatableKey>(SHADOW_OFFSET_KEYS)
179
+ const STRUCTURED_KEY_SET = new Set<AnimatableKey>(STRUCTURED_KEYS)
180
+ const SHARED_STYLE_KEY_SET = new Set<AnimatableKey>(SHARED_STYLE_KEYS)
181
+
182
+ /**
183
+ * What a per-key shared value can hold. Scalars for every key except
184
+ * `boxShadow`, whose slot carries the layer array (see `STRUCTURED_KEYS`).
185
+ */
186
+ type AnimatableSlotValue = number | string | readonly AnimatedBoxShadowLayer[]
187
+
188
+ /**
189
+ * Resting `boxShadow` — no shadow at all. Frozen and hoisted so every
190
+ * primitive shares one reference and an untouched slot never allocates.
191
+ */
192
+ const NO_BOX_SHADOW: readonly AnimatedBoxShadowLayer[] = Object.freeze([])
132
193
 
133
194
  const GESTURE_LAYER_NAMES = [
134
195
  'hovered',
@@ -144,7 +205,7 @@ const GESTURE_LAYER_NAME_SET = new Set<string>(GESTURE_LAYER_NAMES)
144
205
  // Reanimated's style merging treats it as a no-op when present.
145
206
  const EXITING_POINTER_EVENTS_STYLE = { pointerEvents: 'none' } as const
146
207
 
147
- const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
208
+ const DEFAULT_RESTING: Record<AnimatableKey, AnimatableSlotValue> = {
148
209
  translateX: 0,
149
210
  translateY: 0,
150
211
  scale: 1,
@@ -171,6 +232,7 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
171
232
  shadowColor: 'transparent',
172
233
  shadowOffsetWidth: 0,
173
234
  shadowOffsetHeight: 0,
235
+ boxShadow: NO_BOX_SHADOW,
174
236
  }
175
237
 
176
238
  // Both lookups run after the factory has resolved registered transition
@@ -398,6 +460,24 @@ export function createMotionComponent<C extends ComponentType<any>>(
398
460
  // identity only changes on the renders that actually add a key, so the
399
461
  // `useAnimatedStyle` worklet (which reads `.current` each frame) sees the
400
462
  // expansion without churning frame-to-frame.
463
+ const hasLayoutId = layoutId !== undefined
464
+
465
+ // The static `style`, flattened at most once per render and only when
466
+ // something asks for it. Two readers: the shared-element key scan just
467
+ // below, and the resting-value pass further down. A plain `animate`-only
468
+ // instance triggers neither and never pays for the flatten.
469
+ let flatStyle: Record<string, unknown> | undefined
470
+ let flatStyleRead = false
471
+ const getFlatStyle = () => {
472
+ if (!flatStyleRead) {
473
+ flatStyleRead = true
474
+ flatStyle = StyleSheet.flatten(style as never) as
475
+ | Record<string, unknown>
476
+ | undefined
477
+ }
478
+ return flatStyle
479
+ }
480
+
401
481
  const touched = new Set<AnimatableKey>()
402
482
  collectTouchedKeys(touched, animateRecord)
403
483
  if (initialRecord) collectTouchedKeys(touched, initialRecord)
@@ -420,9 +500,31 @@ export function createMotionComponent<C extends ComponentType<any>>(
420
500
  }
421
501
  if (exitRecord) collectTouchedKeys(touched, exitRecord)
422
502
 
503
+ // Shared-element style carry: a `layoutId` element crossfades the carried
504
+ // keys from its counterpart, so those keys have to be in the active set
505
+ // for the worklet to emit them at all.
506
+ //
507
+ // Only keys this element already has a value for join — from a record
508
+ // above, or from the static style. Never invented: activating a key with
509
+ // no source value rests it at `DEFAULT_RESTING`, and for `color` on a
510
+ // `Motion.Text` that inherits its colour from a parent, that default is
511
+ // `'transparent'` — invisible text on an element the consumer only asked
512
+ // to move. Same failure mode as the `0.0.3` P0, reached from the other
513
+ // side.
514
+ if (hasLayoutId) {
515
+ const flat = getFlatStyle()
516
+ if (flat) {
517
+ for (const key of SHARED_STYLE_KEYS) {
518
+ if (touched.has(key)) continue
519
+ if (styleValueFor(flat, key) !== undefined) touched.add(key)
520
+ }
521
+ }
522
+ }
523
+
423
524
  const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
424
525
  const hasTransformRef = useRef<boolean>(false)
425
526
  const hasShadowOffsetRef = useRef<boolean>(false)
527
+ const hasBoxShadowRef = useRef<boolean>(false)
426
528
  // Expand the active set only when this render touched a key we haven't
427
529
  // recorded yet. When nothing new appears we keep the existing array
428
530
  // identity so the worklet's captured ref doesn't see a fresh value.
@@ -446,6 +548,27 @@ export function createMotionComponent<C extends ComponentType<any>>(
446
548
  hasShadowOffsetRef.current = activeKeysRef.current.some((k) =>
447
549
  SHADOW_OFFSET_KEY_SET.has(k),
448
550
  )
551
+ hasBoxShadowRef.current = activeKeysRef.current.includes('boxShadow')
552
+ // Animating `boxShadow` alongside the native `shadow*` keys puts two
553
+ // shadow systems on one element; whichever the underlying view resolves
554
+ // last wins, and the result reads as a bug rather than a choice.
555
+ // Checked here rather than per render — the active key set is what
556
+ // decides it, and this block only runs when that set grows.
557
+ if (
558
+ __DEV__ &&
559
+ hasBoxShadowRef.current &&
560
+ activeKeysRef.current.some(
561
+ (k) => k !== 'boxShadow' && k.startsWith('shadow'),
562
+ )
563
+ ) {
564
+ warnOnce(
565
+ 'boxShadow-with-native-shadow',
566
+ '[inertia] `boxShadow` is animated alongside the native `shadow*` ' +
567
+ 'keys on the same element, which applies two shadow systems at ' +
568
+ 'once. Pick one: `boxShadow` for the cross-platform CSS form, or ' +
569
+ 'the native `shadow*` keys.',
570
+ )
571
+ }
449
572
  }
450
573
 
451
574
  // Which keys a record actually drives *right now*. Everything else in
@@ -473,21 +596,52 @@ export function createMotionComponent<C extends ComponentType<any>>(
473
596
  // a style it has no use for.
474
597
  const activeKeys = activeKeysRef.current!
475
598
  const everDriven = everDrivenRef.current
476
- const styleResting: Partial<Record<AnimatableKey, number | string>> = {}
599
+ const styleResting: Partial<Record<AnimatableKey, AnimatableSlotValue>> =
600
+ {}
601
+ // `boxShadow`'s resting value is two halves (animated layers + static
602
+ // inset flags), so it can't ride the scalar map above.
603
+ let styleRestingShadow: SplitBoxShadow | null = null
477
604
  if (activeKeys.some((k) => !everDriven.has(k))) {
478
- const flat = StyleSheet.flatten(style as never) as
479
- | Record<string, unknown>
480
- | undefined
605
+ const flat = getFlatStyle()
481
606
  if (flat) {
482
607
  for (const key of activeKeys) {
483
608
  if (everDriven.has(key)) continue
609
+ if (key === 'boxShadow') {
610
+ const raw = flat.boxShadow as BoxShadowInput | undefined
611
+ if (raw !== undefined) {
612
+ styleRestingShadow = normalizeBoxShadow(raw)
613
+ styleResting.boxShadow = styleRestingShadow.layers
614
+ }
615
+ continue
616
+ }
484
617
  const v = styleValueFor(flat, key)
485
618
  if (v !== undefined) styleResting[key] = v
486
619
  }
487
620
  }
488
621
  }
489
622
 
623
+ // Seed the `boxShadow` slot once, at mount, on the same precedence chain
624
+ // every other key uses (`initial` → `animate` → static style → default).
625
+ // Held in a ref rather than recomputed per render because resolving it
626
+ // may parse a CSS string, and the value is only ever read on the mount
627
+ // pass.
628
+ const boxShadowSeedRef = useRef<SplitBoxShadow | null>(null)
629
+ if (boxShadowSeedRef.current === null) {
630
+ const source =
631
+ initial === false
632
+ ? animateRecord.boxShadow
633
+ : (initialRecord?.boxShadow ?? animateRecord.boxShadow)
634
+ boxShadowSeedRef.current =
635
+ source !== undefined
636
+ ? normalizeBoxShadow(source)
637
+ : (styleRestingShadow ?? {
638
+ layers: [...NO_BOX_SHADOW],
639
+ insets: null,
640
+ })
641
+ }
642
+
490
643
  const sharedValues = useAnimatableSharedValues((key) => {
644
+ if (key === 'boxShadow') return boxShadowSeedRef.current!.layers
491
645
  // Shadow offset synthetics seed from the corresponding axis on the
492
646
  // `shadowOffset: { width, height }` source — the consumer doesn't write
493
647
  // `shadowOffsetWidth` / `shadowOffsetHeight` directly. Fall back to the
@@ -532,12 +686,25 @@ export function createMotionComponent<C extends ComponentType<any>>(
532
686
  // swap, a conditional colour) would stay invisible for any key some
533
687
  // gesture / exit / variant branch happens to mention. Direct assignment,
534
688
  // not an animation: this is a static value, not a target.
689
+ // Static `inset` flags for the layers currently in the `boxShadow` slot,
690
+ // parallel to `sharedValues.boxShadow`. Kept out of the animated payload
691
+ // because Reanimated would drive the boolean down its numeric path and
692
+ // hand native a number mid-flight; see `AnimatedBoxShadowLayer`. `null`
693
+ // whenever no layer is inset, which lets the worklet emit the animated
694
+ // array with no per-frame reassembly at all.
695
+ const boxShadowInsets = useSharedValue<boolean[] | null>(
696
+ boxShadowSeedRef.current.insets,
697
+ )
698
+
535
699
  const styleRestingSig = stableSig(styleResting)
536
700
  useEffect(() => {
537
701
  for (const key of activeKeysRef.current!) {
538
702
  if (everDrivenRef.current.has(key)) continue
539
703
  sharedValues[key].value = (styleResting[key] ??
540
704
  DEFAULT_RESTING[key]) as never
705
+ if (key === 'boxShadow') {
706
+ boxShadowInsets.value = styleRestingShadow?.insets ?? null
707
+ }
541
708
  }
542
709
  // `styleResting` is rebuilt each render; its signature is the real dep.
543
710
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -666,6 +833,33 @@ export function createMotionComponent<C extends ComponentType<any>>(
666
833
  transformPending > 0 ? { remaining: transformPending } : undefined
667
834
 
668
835
  for (const key of ALL_KEYS) {
836
+ // Structured keys don't go through `resolveAnimatableValue` — their
837
+ // target is already an array, which that resolver would read as a
838
+ // keyframe sequence.
839
+ if (key === 'boxShadow') {
840
+ const shadowTarget = baseRecord.boxShadow
841
+ if (shadowTarget === undefined) continue
842
+ const cfg = configFor('boxShadow')
843
+ if (isExiting && reachesTerminalPhase(cfg)) pending++
844
+ driveBoxShadow(
845
+ sharedValues.boxShadow,
846
+ boxShadowInsets,
847
+ shadowTarget,
848
+ cfg,
849
+ makeKeyCallbackFactory(
850
+ 'boxShadow',
851
+ sharedValues.boxShadow,
852
+ // The declared target, not the padded one — the padding is an
853
+ // implementation detail of getting Reanimated to interpolate.
854
+ shadowTarget,
855
+ onAnimationEndRef,
856
+ { stepCount: 1, totalIterations: totalIterationsOf(cfg) },
857
+ isExiting ? onSettle : undefined,
858
+ undefined,
859
+ ),
860
+ )
861
+ continue
862
+ }
669
863
  // Shadow offset synthetics read their target from the nested
670
864
  // `shadowOffset: { width, height }` source on `baseRecord` — the
671
865
  // animate / exit record never has `shadowOffsetWidth` etc. on it
@@ -770,20 +964,44 @@ export function createMotionComponent<C extends ComponentType<any>>(
770
964
  // transform array so they compose with the user's animate transforms —
771
965
  // multiple `translateX` entries sum, multiple `scaleX` entries multiply,
772
966
  // which is exactly the FLIP semantic.
967
+ //
968
+ // The style half rides the same wiring: `readStyles` lets a later mount
969
+ // (or our own unmount) snapshot the carried keys straight off the shared
970
+ // values, and the worklet below crossfades a consumed snapshot out over
971
+ // whatever this element resolves to. Reached through a ref inside the
972
+ // hook, so building it inline here costs nothing downstream.
973
+ const readSharedStyles = () => {
974
+ if (!hasLayoutId) return undefined
975
+ const out: SharedStyleSnapshot = {}
976
+ let any = false
977
+ for (const key of activeKeysRef.current!) {
978
+ if (!SHARED_STYLE_KEY_SET.has(key)) continue
979
+ const v = sharedValues[key].value
980
+ // Structured slots can't be carried, and a key nothing ever gave a
981
+ // value to has nothing to hand over.
982
+ if (typeof v !== 'number' && typeof v !== 'string') continue
983
+ out[key] = v
984
+ any = true
985
+ }
986
+ return any ? out : undefined
987
+ }
988
+
773
989
  const sharedLayout = useSharedLayout({
774
990
  layoutId,
775
991
  userRef: ref,
776
992
  transition: isTopLevelTransition(transition) ? transition : undefined,
777
993
  shouldReduceMotion,
778
994
  userOnLayout,
995
+ readStyles: readSharedStyles,
779
996
  })
780
997
  const flip = sharedLayout.flip
781
- const hasLayoutId = layoutId !== undefined
998
+ const carry = sharedLayout.carry
782
999
 
783
1000
  const animatedStyle = useAnimatedStyle(() => {
784
1001
  const activeKeys = activeKeysRef.current!
785
1002
  const hasTransform = hasTransformRef.current
786
1003
  const hasShadowOffset = hasShadowOffsetRef.current
1004
+ const hasBoxShadow = hasBoxShadowRef.current
787
1005
  const out: Record<string, unknown> = {}
788
1006
  const transform: Array<Record<string, unknown>> = []
789
1007
  // shadow-offset reassembly buffers. The two synthetic axis SVs feed in
@@ -799,6 +1017,12 @@ export function createMotionComponent<C extends ComponentType<any>>(
799
1017
  const pfv = focusVisibleProgress.value
800
1018
  const pp = pressedProgress.value
801
1019
 
1020
+ // Shared-element style carry. Both reads are cheap and the pair rests
1021
+ // at (null, 0), so a primitive that isn't mid-transition — which is
1022
+ // every primitive, almost all of the time — pays one comparison.
1023
+ const carried = carry.snapshot.value
1024
+ const pc = carry.progress.value
1025
+
802
1026
  const layers = gestureSV.value
803
1027
  // Locals are suffixed `Layer` so they don't shadow the outer `pressed` /
804
1028
  // `focused` / `focusVisible` / `hovered` JS-state booleans — Reanimated's
@@ -810,6 +1034,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
810
1034
  const pressedLayer = layers ? layers.pressed : null
811
1035
 
812
1036
  for (const key of activeKeys) {
1037
+ // `boxShadow` is emitted after the loop: its value is a layer array,
1038
+ // which none of the scalar compositing below can act on. Gesture
1039
+ // sub-states are rejected for this key upstream, so there is no
1040
+ // layer contribution to skip past either.
1041
+ if (key === 'boxShadow') continue
813
1042
  let v = sharedValues[key].value
814
1043
  const isColor = COLOR_KEY_SET.has(key)
815
1044
 
@@ -848,6 +1077,23 @@ export function createMotionComponent<C extends ComponentType<any>>(
848
1077
  : (v as number) + ((t as number) - (v as number)) * pp
849
1078
  }
850
1079
 
1080
+ // Shared-element carry sits above every gesture layer: the element
1081
+ // is arriving from somewhere else, and where it came from outranks
1082
+ // how it is being touched right now. `pc` runs 1 → 0, so the first
1083
+ // frame shows the source's value and the last shows this element's
1084
+ // own — including whatever the layers underneath contributed.
1085
+ //
1086
+ // Transform keys never appear in a snapshot (the FLIP owns them), so
1087
+ // this can't double-apply displacement.
1088
+ if (carried !== null && pc > 0) {
1089
+ const s = carried[key]
1090
+ if (s !== undefined) {
1091
+ v = isColor
1092
+ ? interpolateColor(pc, [0, 1], [v as string, s as string])
1093
+ : (v as number) + ((s as number) - (v as number)) * pc
1094
+ }
1095
+ }
1096
+
851
1097
  if (TRANSFORM_KEY_SET.has(key)) {
852
1098
  transform.push(
853
1099
  ROTATION_KEYS.has(key) ? { [key]: `${v}deg` } : { [key]: v },
@@ -876,12 +1122,28 @@ export function createMotionComponent<C extends ComponentType<any>>(
876
1122
  if (hasShadowOffset) {
877
1123
  out.shadowOffset = { width: shadowOffsetW, height: shadowOffsetH }
878
1124
  }
1125
+ if (hasBoxShadow) {
1126
+ const layers = sharedValues.boxShadow
1127
+ .value as readonly AnimatedBoxShadowLayer[]
1128
+ const insets = boxShadowInsets.value
1129
+ // Fast path — no inset layer anywhere, so the animated array is
1130
+ // already exactly what RN wants and crosses over untouched.
1131
+ if (insets === null) {
1132
+ out.boxShadow = layers
1133
+ } else {
1134
+ const withInset = []
1135
+ for (let i = 0; i < layers.length; i++) {
1136
+ withInset.push({ ...layers[i], inset: insets[i] })
1137
+ }
1138
+ out.boxShadow = withInset
1139
+ }
1140
+ }
879
1141
  return out
880
1142
  })
881
1143
 
882
1144
  // Exiting children are tap-deaf: the next press should fall through to
883
1145
  // whatever is underneath, not re-trigger a soon-to-unmount node. This is
884
- // the moti #297 fix and a v0.1 acceptance criterion. RN 0.71+ deprecates
1146
+ // the moti #297 fix and a `0.0.1` acceptance criterion. RN 0.71+ deprecates
885
1147
  // `pointerEvents` as a prop in favor of the style key, so we merge it
886
1148
  // alongside the animated style instead of spreading as a prop.
887
1149
  const mergedStyle = useMemo(
@@ -969,7 +1231,7 @@ function hasMotionProps(props: Record<string, unknown>): boolean {
969
1231
  return false
970
1232
  }
971
1233
 
972
- type SharedValueMap = Record<AnimatableKey, SharedValue<number | string>>
1234
+ type SharedValueMap = Record<AnimatableKey, SharedValue<AnimatableSlotValue>>
973
1235
 
974
1236
  /**
975
1237
  * Allocate one shared value per animatable key in `ALL_KEYS` and return a
@@ -990,36 +1252,39 @@ type SharedValueMap = Record<AnimatableKey, SharedValue<number | string>>
990
1252
  * `withSpring` / `withTiming` call.
991
1253
  */
992
1254
  function useAnimatableSharedValues(
993
- init: (key: AnimatableKey) => number | string,
1255
+ init: (key: AnimatableKey) => AnimatableSlotValue,
994
1256
  ): SharedValueMap {
995
- const translateX = useSharedValue<number | string>(init('translateX'))
996
- const translateY = useSharedValue<number | string>(init('translateY'))
997
- const scale = useSharedValue<number | string>(init('scale'))
998
- const scaleX = useSharedValue<number | string>(init('scaleX'))
999
- const scaleY = useSharedValue<number | string>(init('scaleY'))
1000
- const rotate = useSharedValue<number | string>(init('rotate'))
1001
- const rotateX = useSharedValue<number | string>(init('rotateX'))
1002
- const rotateY = useSharedValue<number | string>(init('rotateY'))
1003
- const opacity = useSharedValue<number | string>(init('opacity'))
1004
- const width = useSharedValue<number | string>(init('width'))
1005
- const height = useSharedValue<number | string>(init('height'))
1006
- const borderRadius = useSharedValue<number | string>(init('borderRadius'))
1007
- const shadowOpacity = useSharedValue<number | string>(init('shadowOpacity'))
1008
- const shadowRadius = useSharedValue<number | string>(init('shadowRadius'))
1009
- const elevation = useSharedValue<number | string>(init('elevation'))
1010
- const backgroundColor = useSharedValue<number | string>(
1257
+ const translateX = useSharedValue<AnimatableSlotValue>(init('translateX'))
1258
+ const translateY = useSharedValue<AnimatableSlotValue>(init('translateY'))
1259
+ const scale = useSharedValue<AnimatableSlotValue>(init('scale'))
1260
+ const scaleX = useSharedValue<AnimatableSlotValue>(init('scaleX'))
1261
+ const scaleY = useSharedValue<AnimatableSlotValue>(init('scaleY'))
1262
+ const rotate = useSharedValue<AnimatableSlotValue>(init('rotate'))
1263
+ const rotateX = useSharedValue<AnimatableSlotValue>(init('rotateX'))
1264
+ const rotateY = useSharedValue<AnimatableSlotValue>(init('rotateY'))
1265
+ const opacity = useSharedValue<AnimatableSlotValue>(init('opacity'))
1266
+ const width = useSharedValue<AnimatableSlotValue>(init('width'))
1267
+ const height = useSharedValue<AnimatableSlotValue>(init('height'))
1268
+ const borderRadius = useSharedValue<AnimatableSlotValue>(init('borderRadius'))
1269
+ const shadowOpacity = useSharedValue<AnimatableSlotValue>(
1270
+ init('shadowOpacity'),
1271
+ )
1272
+ const shadowRadius = useSharedValue<AnimatableSlotValue>(init('shadowRadius'))
1273
+ const elevation = useSharedValue<AnimatableSlotValue>(init('elevation'))
1274
+ const backgroundColor = useSharedValue<AnimatableSlotValue>(
1011
1275
  init('backgroundColor'),
1012
1276
  )
1013
- const borderColor = useSharedValue<number | string>(init('borderColor'))
1014
- const color = useSharedValue<number | string>(init('color'))
1015
- const tintColor = useSharedValue<number | string>(init('tintColor'))
1016
- const shadowColor = useSharedValue<number | string>(init('shadowColor'))
1017
- const shadowOffsetWidth = useSharedValue<number | string>(
1277
+ const borderColor = useSharedValue<AnimatableSlotValue>(init('borderColor'))
1278
+ const color = useSharedValue<AnimatableSlotValue>(init('color'))
1279
+ const tintColor = useSharedValue<AnimatableSlotValue>(init('tintColor'))
1280
+ const shadowColor = useSharedValue<AnimatableSlotValue>(init('shadowColor'))
1281
+ const shadowOffsetWidth = useSharedValue<AnimatableSlotValue>(
1018
1282
  init('shadowOffsetWidth'),
1019
1283
  )
1020
- const shadowOffsetHeight = useSharedValue<number | string>(
1284
+ const shadowOffsetHeight = useSharedValue<AnimatableSlotValue>(
1021
1285
  init('shadowOffsetHeight'),
1022
1286
  )
1287
+ const boxShadow = useSharedValue<AnimatableSlotValue>(init('boxShadow'))
1023
1288
 
1024
1289
  const ref = useRef<SharedValueMap | null>(null)
1025
1290
  if (ref.current === null) {
@@ -1046,6 +1311,7 @@ function useAnimatableSharedValues(
1046
1311
  shadowColor,
1047
1312
  shadowOffsetWidth,
1048
1313
  shadowOffsetHeight,
1314
+ boxShadow,
1049
1315
  }
1050
1316
  }
1051
1317
 
@@ -1084,8 +1350,11 @@ function useAnimatableSharedValues(
1084
1350
  */
1085
1351
  function makeKeyCallbackFactory(
1086
1352
  key: string,
1087
- sharedValue: SharedValue<number | string>,
1088
- target: number | string | undefined,
1353
+ sharedValue: SharedValue<AnimatableSlotValue>,
1354
+ // Widened past the scalar surface for `boxShadow`, whose declared target is
1355
+ // a CSS string or a layer array. Reaches the consumer as
1356
+ // `AnimationCallbackInfo.target`, which is `unknown`.
1357
+ target: number | string | BoxShadowInput | undefined,
1089
1358
  onAnimationEndRef: {
1090
1359
  current: ((info: AnimationCallbackInfo<unknown>) => void) | undefined
1091
1360
  },
@@ -1105,7 +1374,9 @@ function makeKeyCallbackFactory(
1105
1374
  rawPhase: 'step' | 'animation',
1106
1375
  step: number | undefined,
1107
1376
  finished: boolean,
1108
- value: number | string | undefined,
1377
+ // Widened for `boxShadow`, whose settled value is a layer array. Surfaces
1378
+ // on `AnimationCallbackInfo.value`, which is `unknown` either way.
1379
+ value: AnimatableSlotValue | undefined,
1109
1380
  ) => {
1110
1381
  const isLastIteration = state.iteration >= meta.totalIterations - 1
1111
1382
  let phase: 'step' | 'sequence' | 'repeat' | 'animation'
@@ -1191,18 +1462,25 @@ function makeKeyCallbackFactory(
1191
1462
  * nested object decomposes into the `shadowOffsetWidth` / `shadowOffsetHeight`
1192
1463
  * synthetic axes downstream; consumers don't see the synthetics.
1193
1464
  *
1194
- * v0.1 contract: `shadowOffset` accepts a single `{ width, height }` literal
1195
- * (no sequences, no `{ to }` step objects, no array keyframes). Sequence
1196
- * forms on the nested axes can land in v0.2 if real consumers ask for them.
1465
+ * Contract since `0.0.1`: `shadowOffset` accepts a single `{ width, height }`
1466
+ * literal (no sequences, no `{ to }` step objects, no array keyframes).
1467
+ * Sequence forms on the nested axes can land later if real consumers ask.
1197
1468
  */
1469
+ type ScalarKey = Exclude<AnimatableKey, (typeof STRUCTURED_KEYS)[number]>
1470
+
1198
1471
  type InternalAnimateRecord = Partial<
1199
- Record<AnimatableKey, AnimatableValue<number | string>>
1472
+ Record<ScalarKey, AnimatableValue<number | string>>
1200
1473
  > & {
1201
1474
  shadowOffset?: { width?: number; height?: number }
1475
+ // Structured keys depart from `AnimatableValue` — see `STRUCTURED_KEYS`.
1476
+ // Kept out of the mapped half above so the two don't intersect into an
1477
+ // uninhabitable `AnimatableValue<number | string> & BoxShadowInput`.
1478
+ boxShadow?: BoxShadowInput
1202
1479
  }
1203
1480
 
1204
- type InternalInitialRecord = Partial<Record<AnimatableKey, number | string>> & {
1481
+ type InternalInitialRecord = Partial<Record<ScalarKey, number | string>> & {
1205
1482
  shadowOffset?: { width?: number; height?: number }
1483
+ boxShadow?: BoxShadowInput
1206
1484
  }
1207
1485
 
1208
1486
  /**
@@ -1225,6 +1503,46 @@ function shadowOffsetAxisValue(
1225
1503
  return source?.[axis]
1226
1504
  }
1227
1505
 
1506
+ /**
1507
+ * Drive the `boxShadow` slot toward a new target.
1508
+ *
1509
+ * Two things make this different from the scalar path:
1510
+ *
1511
+ * 1. **Both endpoints are padded to a common layer count first.** Reanimated's
1512
+ * array driver iterates the *current* value's indices and pairs each with
1513
+ * `toValue[i]`, so a target with a different layer count either strands
1514
+ * leaves at `toValue: undefined` or never animates the extras. When the
1515
+ * padding changes the slot's shape, we snap it to the padded base before
1516
+ * starting the animation — otherwise the animation would begin from a
1517
+ * differently-shaped value and the pairing would be lost again.
1518
+ * 2. **`inset` travels separately**, in its own slot, because Reanimated
1519
+ * would drive the boolean down the numeric path.
1520
+ */
1521
+ function driveBoxShadow(
1522
+ slot: SharedValue<AnimatableSlotValue>,
1523
+ insetSlot: SharedValue<boolean[] | null>,
1524
+ target: BoxShadowInput,
1525
+ cfg: TransitionConfig | undefined,
1526
+ factory: CallbackFactory | undefined,
1527
+ ): void {
1528
+ const currentLayers = Array.isArray(slot.value)
1529
+ ? (slot.value as readonly AnimatedBoxShadowLayer[])
1530
+ : NO_BOX_SHADOW
1531
+ const { from, to, insets } = prepareBoxShadowAnimation(
1532
+ { layers: [...currentLayers], insets: insetSlot.value },
1533
+ target,
1534
+ )
1535
+ insetSlot.value = insets
1536
+ if (from.length !== currentLayers.length) slot.value = from
1537
+ // `resolveTransition` is typed for the scalar surface it was written for;
1538
+ // Reanimated itself accepts the structured target and recurses into it.
1539
+ slot.value = resolveTransition(
1540
+ cfg,
1541
+ to as unknown as number,
1542
+ factory?.('animation', undefined),
1543
+ ) as AnimatableSlotValue
1544
+ }
1545
+
1228
1546
  /**
1229
1547
  * Read an animatable key's resting value out of a **flattened style object**.
1230
1548
  *
@@ -1460,6 +1778,27 @@ function resolveGestureLayers(
1460
1778
  if (!subState) continue
1461
1779
  const resolved: Record<string, number | string> = {}
1462
1780
  for (const key of ALL_KEYS) {
1781
+ // Structured keys have no place in the layer cascade: compositing them
1782
+ // would mean per-layer, per-field interpolation on the UI thread for
1783
+ // every primitive, whether or not it animates a shadow. Rejected for
1784
+ // now rather than half-supported — a silently-ignored `boxShadow` in a
1785
+ // `gesture` sub-state is exactly the class of bug this library exists
1786
+ // to avoid, so say so.
1787
+ if (STRUCTURED_KEY_SET.has(key)) {
1788
+ if (
1789
+ __DEV__ &&
1790
+ (subState as Record<string, unknown>)[key] !== undefined
1791
+ ) {
1792
+ warnOnce(
1793
+ `gesture-structured-${layer}-${key}`,
1794
+ `[inertia] \`${key}\` is not supported inside \`gesture.${layer}\` ` +
1795
+ 'and will be ignored. Drive it from `animate` (optionally via a ' +
1796
+ 'variant keyed off the same state), or interpolate it yourself ' +
1797
+ 'with `useShadow`.',
1798
+ )
1799
+ }
1800
+ continue
1801
+ }
1463
1802
  // Shadow offset synthetics decompose from the nested `shadowOffset:
1464
1803
  // { width, height }` source on the sub-state, the same as on `animate`.
1465
1804
  if (SHADOW_OFFSET_KEY_SET.has(key)) {
@@ -41,7 +41,7 @@ export function ensureReanimatedInstalled(): void {
41
41
  const major = parseInt(version.split('.')[0] ?? '0', 10)
42
42
  if (major < 4) {
43
43
  console.error(
44
- `[inertia] react-native-reanimated v${version} is installed, but @rootnative/inertia requires v4.0.0 or later. ` +
44
+ `[inertia] react-native-reanimated ${version} is installed, but @rootnative/inertia requires 4.0.0 or later. ` +
45
45
  `Upgrade with \`pnpm add react-native-reanimated@^4\` (or your package manager's equivalent).`,
46
46
  )
47
47
  return
@@ -1,4 +1,4 @@
1
- export { resolveTransition } from './resolve'
1
+ export { resolveTransition, type CallbackFactory } from './resolve'
2
2
  export { resolveAnimatableValue } from './resolveSequence'
3
3
  export { cubicBezier } from './cubicBezier'
4
4
  export { ensureWorkletEasing } from './easing'