@rootnative/inertia 0.0.3 → 0.0.5

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 (75) hide show
  1. package/CHANGELOG.md +92 -5
  2. package/README.md +9 -3
  3. package/dist/{chunk-OQV66TBQ.mjs → chunk-27MKBTSG.mjs} +1 -1
  4. package/dist/{chunk-3XTVY34H.js → chunk-27U6766G.js} +2 -2
  5. package/dist/{chunk-4PEHWDAZ.js → chunk-4NAMD62I.js} +7 -7
  6. package/dist/chunk-67GHRCF6.js +8 -0
  7. package/dist/{chunk-SGUHE5CX.mjs → chunk-COEJVWRZ.mjs} +1 -1
  8. package/dist/chunk-GPOMFIIU.js +8 -0
  9. package/dist/{chunk-BQQTHG2V.mjs → chunk-GTJ6VAH5.mjs} +1 -1
  10. package/dist/chunk-K63LXGKS.js +8 -0
  11. package/dist/{chunk-46P57VMY.mjs → chunk-OAUWLPQH.mjs} +1 -1
  12. package/dist/{chunk-7AOERN53.js → chunk-OBRGJAST.js} +3 -3
  13. package/dist/{chunk-RGNX6UZN.mjs → chunk-OW5XTGVN.mjs} +2 -2
  14. package/dist/{chunk-PTRF47DA.js → chunk-PM6CVGXJ.js} +11 -3
  15. package/dist/{chunk-W5MC3P4N.js → chunk-QQVDZKSD.js} +2 -2
  16. package/dist/{chunk-CY7Y64C3.mjs → chunk-REYL77RE.mjs} +11 -4
  17. package/dist/{chunk-CSODMRJ7.mjs → chunk-RIVVBABB.mjs} +597 -134
  18. package/dist/{chunk-BP3Y2SHQ.js → chunk-VQ5D35UA.js} +617 -152
  19. package/dist/{chunk-O22NXXCZ.mjs → chunk-XSK5MNUH.mjs} +1 -1
  20. package/dist/{chunk-R63GIUNU.mjs → chunk-Z3HCJ43H.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-BQgLJeQG.d.mts} +85 -14
  54. package/dist/{types-G1dBU2IQ.d.ts → types-BQgLJeQG.d.ts} +85 -14
  55. package/dist/{useGesture-D1e4ixJo.d.ts → useGesture-B0_CzOUz.d.ts} +1 -1
  56. package/dist/{useGesture-RFY6kTL3.d.mts → useGesture-DfaTx-3t.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 +497 -74
  68. package/src/motion/installCheck.ts +1 -1
  69. package/src/transitions/index.ts +1 -1
  70. package/src/transitions/resolveSequence.ts +12 -0
  71. package/src/types.ts +155 -12
  72. package/src/values/useColorTransition.ts +1 -1
  73. package/dist/chunk-FNVFV4EY.js +0 -8
  74. package/dist/chunk-FWQOXA43.js +0 -8
  75. 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,
@@ -81,6 +91,78 @@ const NUMERIC_TOP_LEVEL_KEYS = [
81
91
  'shadowOpacity',
82
92
  'shadowRadius',
83
93
  'elevation',
94
+ // ─── Added in 0.0.5 ──────────────────────────────────────────────────────
95
+ //
96
+ // Every key below rides the generic numeric path: one shared value, resolved
97
+ // on the JS thread, emitted by the worklet through the same `out[key] = v`
98
+ // branch as `opacity`. None needs a new interpolation mode, so the cost is
99
+ // the shared value plus a `DEFAULT_RESTING` entry.
100
+ //
101
+ // The load-bearing constraint is the resting default, not the plumbing. A
102
+ // key declared only in `gesture` / `exit` / a non-active variant rests at
103
+ // `DEFAULT_RESTING` unless the static `style` names it, and the animated
104
+ // style merges *after* `style` — so a non-identity default silently stomps
105
+ // the layout. That is the `0.0.3` P0 class, and it is why the sizing and
106
+ // spacing keys below default to `0` only in the sense RN itself does (an
107
+ // unset `padding` *is* 0); `minWidth`/`maxWidth`/`aspectRatio` are
108
+ // deliberately excluded — their "unset" is not a number, so no default can
109
+ // stand in for it without changing layout on activation.
110
+ //
111
+ // Per-corner radii. Animating one corner is common (a card expanding into a
112
+ // sheet keeps its top corners and squares off the bottom); `borderRadius`
113
+ // alone can't express it.
114
+ 'borderTopLeftRadius',
115
+ 'borderTopRightRadius',
116
+ 'borderBottomLeftRadius',
117
+ 'borderBottomRightRadius',
118
+ // Border widths. `borderWidth` is the common case; the per-edge keys matter
119
+ // for underline-style focus affordances (`borderBottomWidth` on a field).
120
+ 'borderWidth',
121
+ 'borderTopWidth',
122
+ 'borderRightWidth',
123
+ 'borderBottomWidth',
124
+ 'borderLeftWidth',
125
+ // Absolute-position insets. Bread-and-butter for drawers, tooltips, and
126
+ // anything pinned to an edge that slides in.
127
+ 'top',
128
+ 'right',
129
+ 'bottom',
130
+ 'left',
131
+ // Spacing. Padding animates the box from the inside (a button growing its
132
+ // hit area on press); margin shifts it within its parent's flow.
133
+ 'padding',
134
+ 'paddingTop',
135
+ 'paddingRight',
136
+ 'paddingBottom',
137
+ 'paddingLeft',
138
+ 'paddingHorizontal',
139
+ 'paddingVertical',
140
+ 'margin',
141
+ 'marginTop',
142
+ 'marginRight',
143
+ 'marginBottom',
144
+ 'marginLeft',
145
+ 'marginHorizontal',
146
+ 'marginVertical',
147
+ // Flex sizing. `flex` / `flexGrow` interpolate the split between siblings —
148
+ // the idiomatic RN way to animate a pane resize without measuring.
149
+ 'flex',
150
+ 'flexGrow',
151
+ 'flexShrink',
152
+ // Text metrics. `TextStyle`-only at the type level (`AnimateStyle<C>` gates
153
+ // them per primitive), but the runtime table is shared across primitives —
154
+ // an unused slot is one ref, same as `tintColor` has always been.
155
+ 'fontSize',
156
+ 'letterSpacing',
157
+ 'lineHeight',
158
+ // Stacking. Integer-valued in practice, and interpolating it mid-flight is
159
+ // rarely what you want, but a `no-animation` transition on `zIndex` is the
160
+ // clean way to reorder at a step boundary in a sequence.
161
+ 'zIndex',
162
+ // Gap. Row/column gaps animate a list's density without touching children.
163
+ 'gap',
164
+ 'rowGap',
165
+ 'columnGap',
84
166
  ] as const
85
167
 
86
168
  // Color-valued keys. Reanimated's value setter detects color strings and
@@ -108,6 +190,42 @@ const COLOR_KEYS = [
108
190
  // these keys directly.
109
191
  const SHADOW_OFFSET_KEYS = ['shadowOffsetWidth', 'shadowOffsetHeight'] as const
110
192
 
193
+ // Keys whose shared value holds a structure rather than a scalar. `boxShadow`
194
+ // is the only one: its slot carries an array of layer objects, which
195
+ // Reanimated's animation drivers recurse into, animating each leaf number and
196
+ // color independently (`arrayOnStart` → `objectOnStart` in Reanimated's
197
+ // `animation/util.ts`).
198
+ //
199
+ // Passing the CSS string through instead would NOT work — a box-shadow string
200
+ // isn't a color, so it lands in Reanimated's prefix-number-suffix branch,
201
+ // which is built for values like '100%' and would pull a single number out of
202
+ // a four-value shadow. All string parsing therefore happens on the JS thread,
203
+ // once per change, in `internal/boxShadow.ts`.
204
+ const STRUCTURED_KEYS = ['boxShadow'] as const
205
+
206
+ // Keys a shared-element transition (`layoutId`) carries from the source
207
+ // element to the target, crossfading them alongside the rect FLIP. Everything
208
+ // here is either a scalar or a color, so the worklet's existing lerp /
209
+ // `interpolateColor` branches handle it with no new machinery.
210
+ //
211
+ // Transform keys are deliberately absent: they are the FLIP's job, and
212
+ // carrying them too would apply the same displacement twice.
213
+ //
214
+ // So is `shadowColor`, despite being a color key. Carrying one of the four
215
+ // native shadow props crossfades a shadow's *color* over geometry that snapped
216
+ // — a worse result than letting the whole shadow snap together. (It would also
217
+ // pull `shadowColor` into the active set on any `layoutId` element whose style
218
+ // declares it, tripping the `boxShadow`-with-native-`shadow*` warning for a
219
+ // consumer who never asked for either.)
220
+ const SHARED_STYLE_KEYS = [
221
+ 'opacity',
222
+ 'borderRadius',
223
+ 'backgroundColor',
224
+ 'borderColor',
225
+ 'color',
226
+ 'tintColor',
227
+ ] as const
228
+
111
229
  /**
112
230
  * Per-effect transform-group coordinator. Counts how many transform-axis
113
231
  * terminal callbacks are still pending; when the last one fires, the
@@ -121,6 +239,7 @@ const ALL_KEYS = [
121
239
  ...NUMERIC_TOP_LEVEL_KEYS,
122
240
  ...COLOR_KEYS,
123
241
  ...SHADOW_OFFSET_KEYS,
242
+ ...STRUCTURED_KEYS,
124
243
  ] as const
125
244
  type AnimatableKey = (typeof ALL_KEYS)[number]
126
245
  type TransformKey = (typeof TRANSFORM_KEYS)[number]
@@ -129,6 +248,20 @@ type ShadowOffsetKey = (typeof SHADOW_OFFSET_KEYS)[number]
129
248
  const TRANSFORM_KEY_SET = new Set<AnimatableKey>(TRANSFORM_KEYS)
130
249
  const COLOR_KEY_SET = new Set<AnimatableKey>(COLOR_KEYS)
131
250
  const SHADOW_OFFSET_KEY_SET = new Set<AnimatableKey>(SHADOW_OFFSET_KEYS)
251
+ const STRUCTURED_KEY_SET = new Set<AnimatableKey>(STRUCTURED_KEYS)
252
+ const SHARED_STYLE_KEY_SET = new Set<AnimatableKey>(SHARED_STYLE_KEYS)
253
+
254
+ /**
255
+ * What a per-key shared value can hold. Scalars for every key except
256
+ * `boxShadow`, whose slot carries the layer array (see `STRUCTURED_KEYS`).
257
+ */
258
+ type AnimatableSlotValue = number | string | readonly AnimatedBoxShadowLayer[]
259
+
260
+ /**
261
+ * Resting `boxShadow` — no shadow at all. Frozen and hoisted so every
262
+ * primitive shares one reference and an untouched slot never allocates.
263
+ */
264
+ const NO_BOX_SHADOW: readonly AnimatedBoxShadowLayer[] = Object.freeze([])
132
265
 
133
266
  const GESTURE_LAYER_NAMES = [
134
267
  'hovered',
@@ -144,7 +277,7 @@ const GESTURE_LAYER_NAME_SET = new Set<string>(GESTURE_LAYER_NAMES)
144
277
  // Reanimated's style merging treats it as a no-op when present.
145
278
  const EXITING_POINTER_EVENTS_STYLE = { pointerEvents: 'none' } as const
146
279
 
147
- const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
280
+ const DEFAULT_RESTING: Record<AnimatableKey, AnimatableSlotValue> = {
148
281
  translateX: 0,
149
282
  translateY: 0,
150
283
  scale: 1,
@@ -160,6 +293,54 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
160
293
  shadowOpacity: 0,
161
294
  shadowRadius: 0,
162
295
  elevation: 0,
296
+ // 0.0.5 keys. Every one of these is 0 because that is RN's own unset value
297
+ // for it — an element with no `paddingTop` has 0 padding, no `borderWidth`
298
+ // has none, no `top` is unpinned at 0 within its positioning context. So a
299
+ // key that reaches its resting default lands where the element already was.
300
+ //
301
+ // The exception worth naming: `flex` / `flexGrow` / `flexShrink` do NOT have
302
+ // 0 as a universal identity (`flexShrink` defaults to 1 on the web, and RN's
303
+ // own default differs by axis). They are still 0 here, because the style-scan
304
+ // in the resting pass reads the real value off the static `style` whenever it
305
+ // is declared — and a consumer animating flex without declaring it is asking
306
+ // for the from-0 growth, which is the readable interpretation.
307
+ borderTopLeftRadius: 0,
308
+ borderTopRightRadius: 0,
309
+ borderBottomLeftRadius: 0,
310
+ borderBottomRightRadius: 0,
311
+ borderWidth: 0,
312
+ borderTopWidth: 0,
313
+ borderRightWidth: 0,
314
+ borderBottomWidth: 0,
315
+ borderLeftWidth: 0,
316
+ top: 0,
317
+ right: 0,
318
+ bottom: 0,
319
+ left: 0,
320
+ padding: 0,
321
+ paddingTop: 0,
322
+ paddingRight: 0,
323
+ paddingBottom: 0,
324
+ paddingLeft: 0,
325
+ paddingHorizontal: 0,
326
+ paddingVertical: 0,
327
+ margin: 0,
328
+ marginTop: 0,
329
+ marginRight: 0,
330
+ marginBottom: 0,
331
+ marginLeft: 0,
332
+ marginHorizontal: 0,
333
+ marginVertical: 0,
334
+ flex: 0,
335
+ flexGrow: 0,
336
+ flexShrink: 0,
337
+ fontSize: 0,
338
+ letterSpacing: 0,
339
+ lineHeight: 0,
340
+ zIndex: 0,
341
+ gap: 0,
342
+ rowGap: 0,
343
+ columnGap: 0,
163
344
  // 'transparent' is the only safe universal default for colors: it works as
164
345
  // an initial seed for any color animation (no jarring opaque flash on mount
165
346
  // when `initial` is omitted) and rgba(0,0,0,0) interpolates cleanly into
@@ -171,6 +352,7 @@ const DEFAULT_RESTING: Record<AnimatableKey, number | string> = {
171
352
  shadowColor: 'transparent',
172
353
  shadowOffsetWidth: 0,
173
354
  shadowOffsetHeight: 0,
355
+ boxShadow: NO_BOX_SHADOW,
174
356
  }
175
357
 
176
358
  // Both lookups run after the factory has resolved registered transition
@@ -398,6 +580,24 @@ export function createMotionComponent<C extends ComponentType<any>>(
398
580
  // identity only changes on the renders that actually add a key, so the
399
581
  // `useAnimatedStyle` worklet (which reads `.current` each frame) sees the
400
582
  // expansion without churning frame-to-frame.
583
+ const hasLayoutId = layoutId !== undefined
584
+
585
+ // The static `style`, flattened at most once per render and only when
586
+ // something asks for it. Two readers: the shared-element key scan just
587
+ // below, and the resting-value pass further down. A plain `animate`-only
588
+ // instance triggers neither and never pays for the flatten.
589
+ let flatStyle: Record<string, unknown> | undefined
590
+ let flatStyleRead = false
591
+ const getFlatStyle = () => {
592
+ if (!flatStyleRead) {
593
+ flatStyleRead = true
594
+ flatStyle = StyleSheet.flatten(style as never) as
595
+ | Record<string, unknown>
596
+ | undefined
597
+ }
598
+ return flatStyle
599
+ }
600
+
401
601
  const touched = new Set<AnimatableKey>()
402
602
  collectTouchedKeys(touched, animateRecord)
403
603
  if (initialRecord) collectTouchedKeys(touched, initialRecord)
@@ -420,9 +620,31 @@ export function createMotionComponent<C extends ComponentType<any>>(
420
620
  }
421
621
  if (exitRecord) collectTouchedKeys(touched, exitRecord)
422
622
 
623
+ // Shared-element style carry: a `layoutId` element crossfades the carried
624
+ // keys from its counterpart, so those keys have to be in the active set
625
+ // for the worklet to emit them at all.
626
+ //
627
+ // Only keys this element already has a value for join — from a record
628
+ // above, or from the static style. Never invented: activating a key with
629
+ // no source value rests it at `DEFAULT_RESTING`, and for `color` on a
630
+ // `Motion.Text` that inherits its colour from a parent, that default is
631
+ // `'transparent'` — invisible text on an element the consumer only asked
632
+ // to move. Same failure mode as the `0.0.3` P0, reached from the other
633
+ // side.
634
+ if (hasLayoutId) {
635
+ const flat = getFlatStyle()
636
+ if (flat) {
637
+ for (const key of SHARED_STYLE_KEYS) {
638
+ if (touched.has(key)) continue
639
+ if (styleValueFor(flat, key) !== undefined) touched.add(key)
640
+ }
641
+ }
642
+ }
643
+
423
644
  const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
424
645
  const hasTransformRef = useRef<boolean>(false)
425
646
  const hasShadowOffsetRef = useRef<boolean>(false)
647
+ const hasBoxShadowRef = useRef<boolean>(false)
426
648
  // Expand the active set only when this render touched a key we haven't
427
649
  // recorded yet. When nothing new appears we keep the existing array
428
650
  // identity so the worklet's captured ref doesn't see a fresh value.
@@ -446,6 +668,27 @@ export function createMotionComponent<C extends ComponentType<any>>(
446
668
  hasShadowOffsetRef.current = activeKeysRef.current.some((k) =>
447
669
  SHADOW_OFFSET_KEY_SET.has(k),
448
670
  )
671
+ hasBoxShadowRef.current = activeKeysRef.current.includes('boxShadow')
672
+ // Animating `boxShadow` alongside the native `shadow*` keys puts two
673
+ // shadow systems on one element; whichever the underlying view resolves
674
+ // last wins, and the result reads as a bug rather than a choice.
675
+ // Checked here rather than per render — the active key set is what
676
+ // decides it, and this block only runs when that set grows.
677
+ if (
678
+ __DEV__ &&
679
+ hasBoxShadowRef.current &&
680
+ activeKeysRef.current.some(
681
+ (k) => k !== 'boxShadow' && k.startsWith('shadow'),
682
+ )
683
+ ) {
684
+ warnOnce(
685
+ 'boxShadow-with-native-shadow',
686
+ '[inertia] `boxShadow` is animated alongside the native `shadow*` ' +
687
+ 'keys on the same element, which applies two shadow systems at ' +
688
+ 'once. Pick one: `boxShadow` for the cross-platform CSS form, or ' +
689
+ 'the native `shadow*` keys.',
690
+ )
691
+ }
449
692
  }
450
693
 
451
694
  // Which keys a record actually drives *right now*. Everything else in
@@ -473,21 +716,52 @@ export function createMotionComponent<C extends ComponentType<any>>(
473
716
  // a style it has no use for.
474
717
  const activeKeys = activeKeysRef.current!
475
718
  const everDriven = everDrivenRef.current
476
- const styleResting: Partial<Record<AnimatableKey, number | string>> = {}
719
+ const styleResting: Partial<Record<AnimatableKey, AnimatableSlotValue>> =
720
+ {}
721
+ // `boxShadow`'s resting value is two halves (animated layers + static
722
+ // inset flags), so it can't ride the scalar map above.
723
+ let styleRestingShadow: SplitBoxShadow | null = null
477
724
  if (activeKeys.some((k) => !everDriven.has(k))) {
478
- const flat = StyleSheet.flatten(style as never) as
479
- | Record<string, unknown>
480
- | undefined
725
+ const flat = getFlatStyle()
481
726
  if (flat) {
482
727
  for (const key of activeKeys) {
483
728
  if (everDriven.has(key)) continue
729
+ if (key === 'boxShadow') {
730
+ const raw = flat.boxShadow as BoxShadowInput | undefined
731
+ if (raw !== undefined) {
732
+ styleRestingShadow = normalizeBoxShadow(raw)
733
+ styleResting.boxShadow = styleRestingShadow.layers
734
+ }
735
+ continue
736
+ }
484
737
  const v = styleValueFor(flat, key)
485
738
  if (v !== undefined) styleResting[key] = v
486
739
  }
487
740
  }
488
741
  }
489
742
 
743
+ // Seed the `boxShadow` slot once, at mount, on the same precedence chain
744
+ // every other key uses (`initial` → `animate` → static style → default).
745
+ // Held in a ref rather than recomputed per render because resolving it
746
+ // may parse a CSS string, and the value is only ever read on the mount
747
+ // pass.
748
+ const boxShadowSeedRef = useRef<SplitBoxShadow | null>(null)
749
+ if (boxShadowSeedRef.current === null) {
750
+ const source =
751
+ initial === false
752
+ ? animateRecord.boxShadow
753
+ : (initialRecord?.boxShadow ?? animateRecord.boxShadow)
754
+ boxShadowSeedRef.current =
755
+ source !== undefined
756
+ ? normalizeBoxShadow(source)
757
+ : (styleRestingShadow ?? {
758
+ layers: [...NO_BOX_SHADOW],
759
+ insets: null,
760
+ })
761
+ }
762
+
490
763
  const sharedValues = useAnimatableSharedValues((key) => {
764
+ if (key === 'boxShadow') return boxShadowSeedRef.current!.layers
491
765
  // Shadow offset synthetics seed from the corresponding axis on the
492
766
  // `shadowOffset: { width, height }` source — the consumer doesn't write
493
767
  // `shadowOffsetWidth` / `shadowOffsetHeight` directly. Fall back to the
@@ -532,12 +806,25 @@ export function createMotionComponent<C extends ComponentType<any>>(
532
806
  // swap, a conditional colour) would stay invisible for any key some
533
807
  // gesture / exit / variant branch happens to mention. Direct assignment,
534
808
  // not an animation: this is a static value, not a target.
809
+ // Static `inset` flags for the layers currently in the `boxShadow` slot,
810
+ // parallel to `sharedValues.boxShadow`. Kept out of the animated payload
811
+ // because Reanimated would drive the boolean down its numeric path and
812
+ // hand native a number mid-flight; see `AnimatedBoxShadowLayer`. `null`
813
+ // whenever no layer is inset, which lets the worklet emit the animated
814
+ // array with no per-frame reassembly at all.
815
+ const boxShadowInsets = useSharedValue<boolean[] | null>(
816
+ boxShadowSeedRef.current.insets,
817
+ )
818
+
535
819
  const styleRestingSig = stableSig(styleResting)
536
820
  useEffect(() => {
537
821
  for (const key of activeKeysRef.current!) {
538
822
  if (everDrivenRef.current.has(key)) continue
539
823
  sharedValues[key].value = (styleResting[key] ??
540
824
  DEFAULT_RESTING[key]) as never
825
+ if (key === 'boxShadow') {
826
+ boxShadowInsets.value = styleRestingShadow?.insets ?? null
827
+ }
541
828
  }
542
829
  // `styleResting` is rebuilt each render; its signature is the real dep.
543
830
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -666,6 +953,33 @@ export function createMotionComponent<C extends ComponentType<any>>(
666
953
  transformPending > 0 ? { remaining: transformPending } : undefined
667
954
 
668
955
  for (const key of ALL_KEYS) {
956
+ // Structured keys don't go through `resolveAnimatableValue` — their
957
+ // target is already an array, which that resolver would read as a
958
+ // keyframe sequence.
959
+ if (key === 'boxShadow') {
960
+ const shadowTarget = baseRecord.boxShadow
961
+ if (shadowTarget === undefined) continue
962
+ const cfg = configFor('boxShadow')
963
+ if (isExiting && reachesTerminalPhase(cfg)) pending++
964
+ driveBoxShadow(
965
+ sharedValues.boxShadow,
966
+ boxShadowInsets,
967
+ shadowTarget,
968
+ cfg,
969
+ makeKeyCallbackFactory(
970
+ 'boxShadow',
971
+ sharedValues.boxShadow,
972
+ // The declared target, not the padded one — the padding is an
973
+ // implementation detail of getting Reanimated to interpolate.
974
+ shadowTarget,
975
+ onAnimationEndRef,
976
+ { stepCount: 1, totalIterations: totalIterationsOf(cfg) },
977
+ isExiting ? onSettle : undefined,
978
+ undefined,
979
+ ),
980
+ )
981
+ continue
982
+ }
669
983
  // Shadow offset synthetics read their target from the nested
670
984
  // `shadowOffset: { width, height }` source on `baseRecord` — the
671
985
  // animate / exit record never has `shadowOffsetWidth` etc. on it
@@ -770,20 +1084,44 @@ export function createMotionComponent<C extends ComponentType<any>>(
770
1084
  // transform array so they compose with the user's animate transforms —
771
1085
  // multiple `translateX` entries sum, multiple `scaleX` entries multiply,
772
1086
  // which is exactly the FLIP semantic.
1087
+ //
1088
+ // The style half rides the same wiring: `readStyles` lets a later mount
1089
+ // (or our own unmount) snapshot the carried keys straight off the shared
1090
+ // values, and the worklet below crossfades a consumed snapshot out over
1091
+ // whatever this element resolves to. Reached through a ref inside the
1092
+ // hook, so building it inline here costs nothing downstream.
1093
+ const readSharedStyles = () => {
1094
+ if (!hasLayoutId) return undefined
1095
+ const out: SharedStyleSnapshot = {}
1096
+ let any = false
1097
+ for (const key of activeKeysRef.current!) {
1098
+ if (!SHARED_STYLE_KEY_SET.has(key)) continue
1099
+ const v = sharedValues[key].value
1100
+ // Structured slots can't be carried, and a key nothing ever gave a
1101
+ // value to has nothing to hand over.
1102
+ if (typeof v !== 'number' && typeof v !== 'string') continue
1103
+ out[key] = v
1104
+ any = true
1105
+ }
1106
+ return any ? out : undefined
1107
+ }
1108
+
773
1109
  const sharedLayout = useSharedLayout({
774
1110
  layoutId,
775
1111
  userRef: ref,
776
1112
  transition: isTopLevelTransition(transition) ? transition : undefined,
777
1113
  shouldReduceMotion,
778
1114
  userOnLayout,
1115
+ readStyles: readSharedStyles,
779
1116
  })
780
1117
  const flip = sharedLayout.flip
781
- const hasLayoutId = layoutId !== undefined
1118
+ const carry = sharedLayout.carry
782
1119
 
783
1120
  const animatedStyle = useAnimatedStyle(() => {
784
1121
  const activeKeys = activeKeysRef.current!
785
1122
  const hasTransform = hasTransformRef.current
786
1123
  const hasShadowOffset = hasShadowOffsetRef.current
1124
+ const hasBoxShadow = hasBoxShadowRef.current
787
1125
  const out: Record<string, unknown> = {}
788
1126
  const transform: Array<Record<string, unknown>> = []
789
1127
  // shadow-offset reassembly buffers. The two synthetic axis SVs feed in
@@ -799,6 +1137,12 @@ export function createMotionComponent<C extends ComponentType<any>>(
799
1137
  const pfv = focusVisibleProgress.value
800
1138
  const pp = pressedProgress.value
801
1139
 
1140
+ // Shared-element style carry. Both reads are cheap and the pair rests
1141
+ // at (null, 0), so a primitive that isn't mid-transition — which is
1142
+ // every primitive, almost all of the time — pays one comparison.
1143
+ const carried = carry.snapshot.value
1144
+ const pc = carry.progress.value
1145
+
802
1146
  const layers = gestureSV.value
803
1147
  // Locals are suffixed `Layer` so they don't shadow the outer `pressed` /
804
1148
  // `focused` / `focusVisible` / `hovered` JS-state booleans — Reanimated's
@@ -810,6 +1154,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
810
1154
  const pressedLayer = layers ? layers.pressed : null
811
1155
 
812
1156
  for (const key of activeKeys) {
1157
+ // `boxShadow` is emitted after the loop: its value is a layer array,
1158
+ // which none of the scalar compositing below can act on. Gesture
1159
+ // sub-states are rejected for this key upstream, so there is no
1160
+ // layer contribution to skip past either.
1161
+ if (key === 'boxShadow') continue
813
1162
  let v = sharedValues[key].value
814
1163
  const isColor = COLOR_KEY_SET.has(key)
815
1164
 
@@ -848,6 +1197,23 @@ export function createMotionComponent<C extends ComponentType<any>>(
848
1197
  : (v as number) + ((t as number) - (v as number)) * pp
849
1198
  }
850
1199
 
1200
+ // Shared-element carry sits above every gesture layer: the element
1201
+ // is arriving from somewhere else, and where it came from outranks
1202
+ // how it is being touched right now. `pc` runs 1 → 0, so the first
1203
+ // frame shows the source's value and the last shows this element's
1204
+ // own — including whatever the layers underneath contributed.
1205
+ //
1206
+ // Transform keys never appear in a snapshot (the FLIP owns them), so
1207
+ // this can't double-apply displacement.
1208
+ if (carried !== null && pc > 0) {
1209
+ const s = carried[key]
1210
+ if (s !== undefined) {
1211
+ v = isColor
1212
+ ? interpolateColor(pc, [0, 1], [v as string, s as string])
1213
+ : (v as number) + ((s as number) - (v as number)) * pc
1214
+ }
1215
+ }
1216
+
851
1217
  if (TRANSFORM_KEY_SET.has(key)) {
852
1218
  transform.push(
853
1219
  ROTATION_KEYS.has(key) ? { [key]: `${v}deg` } : { [key]: v },
@@ -876,12 +1242,28 @@ export function createMotionComponent<C extends ComponentType<any>>(
876
1242
  if (hasShadowOffset) {
877
1243
  out.shadowOffset = { width: shadowOffsetW, height: shadowOffsetH }
878
1244
  }
1245
+ if (hasBoxShadow) {
1246
+ const layers = sharedValues.boxShadow
1247
+ .value as readonly AnimatedBoxShadowLayer[]
1248
+ const insets = boxShadowInsets.value
1249
+ // Fast path — no inset layer anywhere, so the animated array is
1250
+ // already exactly what RN wants and crosses over untouched.
1251
+ if (insets === null) {
1252
+ out.boxShadow = layers
1253
+ } else {
1254
+ const withInset = []
1255
+ for (let i = 0; i < layers.length; i++) {
1256
+ withInset.push({ ...layers[i], inset: insets[i] })
1257
+ }
1258
+ out.boxShadow = withInset
1259
+ }
1260
+ }
879
1261
  return out
880
1262
  })
881
1263
 
882
1264
  // Exiting children are tap-deaf: the next press should fall through to
883
1265
  // 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
1266
+ // the moti #297 fix and a `0.0.1` acceptance criterion. RN 0.71+ deprecates
885
1267
  // `pointerEvents` as a prop in favor of the style key, so we merge it
886
1268
  // alongside the animated style instead of spreading as a prop.
887
1269
  const mergedStyle = useMemo(
@@ -969,7 +1351,7 @@ function hasMotionProps(props: Record<string, unknown>): boolean {
969
1351
  return false
970
1352
  }
971
1353
 
972
- type SharedValueMap = Record<AnimatableKey, SharedValue<number | string>>
1354
+ type SharedValueMap = Record<AnimatableKey, SharedValue<AnimatableSlotValue>>
973
1355
 
974
1356
  /**
975
1357
  * Allocate one shared value per animatable key in `ALL_KEYS` and return a
@@ -983,70 +1365,38 @@ type SharedValueMap = Record<AnimatableKey, SharedValue<number | string>>
983
1365
  * are stable across renders (Reanimated's `useSharedValue` is a `useRef`
984
1366
  * under the hood), so snapshotting the wrapping object once is safe.
985
1367
  *
986
- * Hooks are called in a stable, lexical order fine for rules-of-hooks.
987
- * Unused shared values are cheap; the worklet skips them via
988
- * `activeKeysRef`. Color keys are seeded with the initial color string so
989
- * Reanimated's value setter recognizes the slot as a color from the first
990
- * `withSpring` / `withTiming` call.
1368
+ * Hooks are called by iterating `ALL_KEYS`, which is a module-level `as const`
1369
+ * array its length and order are fixed at module evaluation, so the hook
1370
+ * sequence is identical on every render of every instance. That satisfies
1371
+ * rules-of-hooks exactly as a hand-written list does, and it is what keeps this
1372
+ * function from growing a line per key: the 0.0.5 additions took the table from
1373
+ * 23 keys to 63, and 63 near-identical `useSharedValue` lines is both a bigger
1374
+ * bundle and a standing invitation to add a key in one list but not the other
1375
+ * (the failure mode is a `sharedValues[key]` of `undefined` inside the worklet,
1376
+ * which is exactly how the first draft of that change broke).
1377
+ *
1378
+ * The ESLint exhaustive-deps rule can't see through the loop, hence the
1379
+ * disable — the invariant it would check for us is asserted above instead.
1380
+ *
1381
+ * Unused shared values are cheap; the worklet skips them via `activeKeysRef`.
1382
+ * Color keys are seeded with the initial color string so Reanimated's value
1383
+ * setter recognizes the slot as a color from the first `withSpring` /
1384
+ * `withTiming` call.
991
1385
  */
992
1386
  function useAnimatableSharedValues(
993
- init: (key: AnimatableKey) => number | string,
1387
+ init: (key: AnimatableKey) => AnimatableSlotValue,
994
1388
  ): 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>(
1011
- init('backgroundColor'),
1012
- )
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>(
1018
- init('shadowOffsetWidth'),
1019
- )
1020
- const shadowOffsetHeight = useSharedValue<number | string>(
1021
- init('shadowOffsetHeight'),
1022
- )
1389
+ const entries: [AnimatableKey, SharedValue<AnimatableSlotValue>][] = []
1390
+ for (const key of ALL_KEYS) {
1391
+ // eslint-disable-next-line react-hooks/rules-of-hooks
1392
+ entries.push([key, useSharedValue<AnimatableSlotValue>(init(key))])
1393
+ }
1023
1394
 
1024
1395
  const ref = useRef<SharedValueMap | null>(null)
1025
1396
  if (ref.current === null) {
1026
- ref.current = {
1027
- translateX,
1028
- translateY,
1029
- scale,
1030
- scaleX,
1031
- scaleY,
1032
- rotate,
1033
- rotateX,
1034
- rotateY,
1035
- opacity,
1036
- width,
1037
- height,
1038
- borderRadius,
1039
- shadowOpacity,
1040
- shadowRadius,
1041
- elevation,
1042
- backgroundColor,
1043
- borderColor,
1044
- color,
1045
- tintColor,
1046
- shadowColor,
1047
- shadowOffsetWidth,
1048
- shadowOffsetHeight,
1049
- }
1397
+ const map = {} as SharedValueMap
1398
+ for (const [key, sv] of entries) map[key] = sv
1399
+ ref.current = map
1050
1400
  }
1051
1401
 
1052
1402
  // Cancel every in-flight per-key animation when the primitive unmounts, so
@@ -1084,8 +1434,11 @@ function useAnimatableSharedValues(
1084
1434
  */
1085
1435
  function makeKeyCallbackFactory(
1086
1436
  key: string,
1087
- sharedValue: SharedValue<number | string>,
1088
- target: number | string | undefined,
1437
+ sharedValue: SharedValue<AnimatableSlotValue>,
1438
+ // Widened past the scalar surface for `boxShadow`, whose declared target is
1439
+ // a CSS string or a layer array. Reaches the consumer as
1440
+ // `AnimationCallbackInfo.target`, which is `unknown`.
1441
+ target: number | string | BoxShadowInput | undefined,
1089
1442
  onAnimationEndRef: {
1090
1443
  current: ((info: AnimationCallbackInfo<unknown>) => void) | undefined
1091
1444
  },
@@ -1105,7 +1458,9 @@ function makeKeyCallbackFactory(
1105
1458
  rawPhase: 'step' | 'animation',
1106
1459
  step: number | undefined,
1107
1460
  finished: boolean,
1108
- value: number | string | undefined,
1461
+ // Widened for `boxShadow`, whose settled value is a layer array. Surfaces
1462
+ // on `AnimationCallbackInfo.value`, which is `unknown` either way.
1463
+ value: AnimatableSlotValue | undefined,
1109
1464
  ) => {
1110
1465
  const isLastIteration = state.iteration >= meta.totalIterations - 1
1111
1466
  let phase: 'step' | 'sequence' | 'repeat' | 'animation'
@@ -1191,18 +1546,25 @@ function makeKeyCallbackFactory(
1191
1546
  * nested object decomposes into the `shadowOffsetWidth` / `shadowOffsetHeight`
1192
1547
  * synthetic axes downstream; consumers don't see the synthetics.
1193
1548
  *
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.
1549
+ * Contract since `0.0.1`: `shadowOffset` accepts a single `{ width, height }`
1550
+ * literal (no sequences, no `{ to }` step objects, no array keyframes).
1551
+ * Sequence forms on the nested axes can land later if real consumers ask.
1197
1552
  */
1553
+ type ScalarKey = Exclude<AnimatableKey, (typeof STRUCTURED_KEYS)[number]>
1554
+
1198
1555
  type InternalAnimateRecord = Partial<
1199
- Record<AnimatableKey, AnimatableValue<number | string>>
1556
+ Record<ScalarKey, AnimatableValue<number | string>>
1200
1557
  > & {
1201
1558
  shadowOffset?: { width?: number; height?: number }
1559
+ // Structured keys depart from `AnimatableValue` — see `STRUCTURED_KEYS`.
1560
+ // Kept out of the mapped half above so the two don't intersect into an
1561
+ // uninhabitable `AnimatableValue<number | string> & BoxShadowInput`.
1562
+ boxShadow?: BoxShadowInput
1202
1563
  }
1203
1564
 
1204
- type InternalInitialRecord = Partial<Record<AnimatableKey, number | string>> & {
1565
+ type InternalInitialRecord = Partial<Record<ScalarKey, number | string>> & {
1205
1566
  shadowOffset?: { width?: number; height?: number }
1567
+ boxShadow?: BoxShadowInput
1206
1568
  }
1207
1569
 
1208
1570
  /**
@@ -1225,6 +1587,46 @@ function shadowOffsetAxisValue(
1225
1587
  return source?.[axis]
1226
1588
  }
1227
1589
 
1590
+ /**
1591
+ * Drive the `boxShadow` slot toward a new target.
1592
+ *
1593
+ * Two things make this different from the scalar path:
1594
+ *
1595
+ * 1. **Both endpoints are padded to a common layer count first.** Reanimated's
1596
+ * array driver iterates the *current* value's indices and pairs each with
1597
+ * `toValue[i]`, so a target with a different layer count either strands
1598
+ * leaves at `toValue: undefined` or never animates the extras. When the
1599
+ * padding changes the slot's shape, we snap it to the padded base before
1600
+ * starting the animation — otherwise the animation would begin from a
1601
+ * differently-shaped value and the pairing would be lost again.
1602
+ * 2. **`inset` travels separately**, in its own slot, because Reanimated
1603
+ * would drive the boolean down the numeric path.
1604
+ */
1605
+ function driveBoxShadow(
1606
+ slot: SharedValue<AnimatableSlotValue>,
1607
+ insetSlot: SharedValue<boolean[] | null>,
1608
+ target: BoxShadowInput,
1609
+ cfg: TransitionConfig | undefined,
1610
+ factory: CallbackFactory | undefined,
1611
+ ): void {
1612
+ const currentLayers = Array.isArray(slot.value)
1613
+ ? (slot.value as readonly AnimatedBoxShadowLayer[])
1614
+ : NO_BOX_SHADOW
1615
+ const { from, to, insets } = prepareBoxShadowAnimation(
1616
+ { layers: [...currentLayers], insets: insetSlot.value },
1617
+ target,
1618
+ )
1619
+ insetSlot.value = insets
1620
+ if (from.length !== currentLayers.length) slot.value = from
1621
+ // `resolveTransition` is typed for the scalar surface it was written for;
1622
+ // Reanimated itself accepts the structured target and recurses into it.
1623
+ slot.value = resolveTransition(
1624
+ cfg,
1625
+ to as unknown as number,
1626
+ factory?.('animation', undefined),
1627
+ ) as AnimatableSlotValue
1628
+ }
1629
+
1228
1630
  /**
1229
1631
  * Read an animatable key's resting value out of a **flattened style object**.
1230
1632
  *
@@ -1460,6 +1862,27 @@ function resolveGestureLayers(
1460
1862
  if (!subState) continue
1461
1863
  const resolved: Record<string, number | string> = {}
1462
1864
  for (const key of ALL_KEYS) {
1865
+ // Structured keys have no place in the layer cascade: compositing them
1866
+ // would mean per-layer, per-field interpolation on the UI thread for
1867
+ // every primitive, whether or not it animates a shadow. Rejected for
1868
+ // now rather than half-supported — a silently-ignored `boxShadow` in a
1869
+ // `gesture` sub-state is exactly the class of bug this library exists
1870
+ // to avoid, so say so.
1871
+ if (STRUCTURED_KEY_SET.has(key)) {
1872
+ if (
1873
+ __DEV__ &&
1874
+ (subState as Record<string, unknown>)[key] !== undefined
1875
+ ) {
1876
+ warnOnce(
1877
+ `gesture-structured-${layer}-${key}`,
1878
+ `[inertia] \`${key}\` is not supported inside \`gesture.${layer}\` ` +
1879
+ 'and will be ignored. Drive it from `animate` (optionally via a ' +
1880
+ 'variant keyed off the same state), or interpolate it yourself ' +
1881
+ 'with `useShadow`.',
1882
+ )
1883
+ }
1884
+ continue
1885
+ }
1463
1886
  // Shadow offset synthetics decompose from the nested `shadowOffset:
1464
1887
  // { width, height }` source on the sub-state, the same as on `animate`.
1465
1888
  if (SHADOW_OFFSET_KEY_SET.has(key)) {