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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/gestureLayer/index.d.mts +119 -0
  3. package/dist/gestureLayer/index.d.ts +119 -0
  4. package/dist/gestureLayer/index.js +346 -0
  5. package/dist/gestureLayer/index.js.map +1 -0
  6. package/dist/gestureLayer/index.mjs +344 -0
  7. package/dist/gestureLayer/index.mjs.map +1 -0
  8. package/dist/index.d.mts +114 -74
  9. package/dist/index.d.ts +114 -74
  10. package/dist/index.js +388 -1542
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +388 -1545
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/motion/Image.d.mts +1 -1
  15. package/dist/motion/Image.d.ts +1 -1
  16. package/dist/motion/Image.js +244 -1462
  17. package/dist/motion/Image.js.map +1 -1
  18. package/dist/motion/Image.mjs +247 -1465
  19. package/dist/motion/Image.mjs.map +1 -1
  20. package/dist/motion/Pressable.d.mts +1 -1
  21. package/dist/motion/Pressable.d.ts +1 -1
  22. package/dist/motion/Pressable.js +244 -1462
  23. package/dist/motion/Pressable.js.map +1 -1
  24. package/dist/motion/Pressable.mjs +247 -1465
  25. package/dist/motion/Pressable.mjs.map +1 -1
  26. package/dist/motion/ScrollView.d.mts +1 -1
  27. package/dist/motion/ScrollView.d.ts +1 -1
  28. package/dist/motion/ScrollView.js +244 -1462
  29. package/dist/motion/ScrollView.js.map +1 -1
  30. package/dist/motion/ScrollView.mjs +247 -1465
  31. package/dist/motion/ScrollView.mjs.map +1 -1
  32. package/dist/motion/Text.d.mts +1 -1
  33. package/dist/motion/Text.d.ts +1 -1
  34. package/dist/motion/Text.js +244 -1462
  35. package/dist/motion/Text.js.map +1 -1
  36. package/dist/motion/Text.mjs +247 -1465
  37. package/dist/motion/Text.mjs.map +1 -1
  38. package/dist/motion/View.d.mts +1 -1
  39. package/dist/motion/View.d.ts +1 -1
  40. package/dist/motion/View.js +244 -1462
  41. package/dist/motion/View.js.map +1 -1
  42. package/dist/motion/View.mjs +247 -1465
  43. package/dist/motion/View.mjs.map +1 -1
  44. package/dist/touch/index.d.mts +146 -0
  45. package/dist/touch/index.d.ts +146 -0
  46. package/dist/touch/index.js +166 -0
  47. package/dist/touch/index.js.map +1 -0
  48. package/dist/touch/index.mjs +164 -0
  49. package/dist/touch/index.mjs.map +1 -0
  50. package/dist/{types-NmNeJjo1.d.mts → types-cU43dEmH.d.mts} +64 -17
  51. package/dist/{types-NmNeJjo1.d.ts → types-cU43dEmH.d.ts} +64 -17
  52. package/dist/useGesture-B7A_1DVg.d.ts +84 -0
  53. package/dist/useGesture-cimMrzC1.d.mts +84 -0
  54. package/jest-setup.js +4 -0
  55. package/llms.txt +12 -3
  56. package/package.json +22 -2
  57. package/src/__type-tests__/variants.test-d.tsx +67 -0
  58. package/src/gestureLayer/index.ts +21 -0
  59. package/src/gestureLayer/useGestureLayer.ts +285 -0
  60. package/src/index.ts +7 -0
  61. package/src/layout/index.ts +15 -0
  62. package/src/layout/sharedRegistry.ts +111 -0
  63. package/src/layout/useSharedLayout.ts +289 -0
  64. package/src/motion/createMotionComponent.tsx +123 -37
  65. package/src/motion/installCheck.ts +7 -11
  66. package/src/touch/index.ts +18 -0
  67. package/src/touch/useTouchDrag.ts +289 -0
  68. package/src/types.ts +79 -20
  69. package/src/values/index.ts +11 -0
  70. package/src/values/useBooleanSpring.ts +33 -0
  71. package/src/values/useColorTransition.ts +72 -0
  72. package/src/values/useShadow.ts +116 -0
@@ -0,0 +1,289 @@
1
+ import {
2
+ type MutableRefObject,
3
+ type Ref,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ } from 'react'
9
+ import { type LayoutChangeEvent } from 'react-native'
10
+ import {
11
+ type SharedValue,
12
+ useSharedValue,
13
+ withSequence,
14
+ withSpring,
15
+ withTiming,
16
+ } from 'react-native-reanimated'
17
+ import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
18
+ import { type SpringTransition, type TransitionConfig } from '../types'
19
+ import {
20
+ consumeLayout,
21
+ registerLayout,
22
+ releaseLayout,
23
+ type SharedRect,
24
+ } from './sharedRegistry'
25
+
26
+ /**
27
+ * Shared values produced by `useSharedLayout`. The worklet inside
28
+ * `createMotionComponent` appends `translateX/Y` and `scaleX/Y` transforms
29
+ * built from these so a shared-layout source rect maps onto the new
30
+ * element's transform stack without conflicting with the user's `animate`
31
+ * transforms — multiple transform entries of the same key compose
32
+ * additively (for translates) and multiplicatively (for scales), which is
33
+ * exactly the FLIP semantic.
34
+ *
35
+ * At rest the values are `(0, 0, 1, 1)` — the identity transform — so when
36
+ * no shared-layout transition is active the worklet's contribution is a
37
+ * no-op.
38
+ */
39
+ export interface SharedLayoutValues {
40
+ dx: SharedValue<number>
41
+ dy: SharedValue<number>
42
+ sx: SharedValue<number>
43
+ sy: SharedValue<number>
44
+ }
45
+
46
+ /** What the host component needs to wire into its rendered tree. */
47
+ export interface SharedLayoutBindings {
48
+ flip: SharedLayoutValues
49
+ /**
50
+ * Composite ref the consumer attaches to the rendered animated
51
+ * component. Forwards the underlying ref to the user-supplied `ref`
52
+ * (when present); kept as a stable callback so a `<Motion.*>` with no
53
+ * `layoutId` doesn't pay anything extra.
54
+ */
55
+ setRef: (node: unknown) => void
56
+ /** onLayout handler the consumer must attach to the animated component. */
57
+ onLayout: (event: LayoutChangeEvent) => void
58
+ }
59
+
60
+ /**
61
+ * Hook backing `<Motion.* layoutId="..." />`.
62
+ *
63
+ * Responsibilities, in order:
64
+ * 1. Allocate FLIP shared values (identity at rest).
65
+ * 2. Track the latest layout rect via the `onLayout` event, and push it
66
+ * into the registry under `layoutId` so this primitive can serve as
67
+ * the source for a future transition.
68
+ * 3. On unmount, hand the last measured rect to `releaseLayout` so the
69
+ * next mount with the same id can consume it.
70
+ * 4. On the first layout commit, consume any pending source rect and
71
+ * drive the FLIP shared values: snap them to the (delta, scale) that
72
+ * visually places the new element at the source position, then
73
+ * animate back to identity. `withSequence(snap, animate)` keeps the
74
+ * animation starting from the snapped delta rather than from zero.
75
+ *
76
+ * Coordinate space note: rects are in parent-relative coordinates (what
77
+ * `onLayout` reports). For the common cross-screen navigator pattern —
78
+ * both screens share an outer content container — parent-relative deltas
79
+ * match what the user perceives. Nested-parent setups where the source
80
+ * and target screens sit under containers at different screen offsets
81
+ * will be off by that offset; v1 documents this and leaves a precise
82
+ * window-coords path for v2.
83
+ *
84
+ * When `layoutId` is `undefined`, every callback is a no-op and the FLIP
85
+ * shared values stay at identity — the host's worklet then skips the
86
+ * transform contribution entirely.
87
+ */
88
+ export function useSharedLayout(options: {
89
+ layoutId: string | undefined
90
+ userRef: Ref<unknown> | undefined
91
+ transition: TransitionConfig | undefined
92
+ shouldReduceMotion: boolean
93
+ userOnLayout: ((event: LayoutChangeEvent) => void) | undefined
94
+ }): SharedLayoutBindings {
95
+ const { layoutId, userRef, transition, shouldReduceMotion, userOnLayout } =
96
+ options
97
+
98
+ const dx = useSharedValue(0)
99
+ const dy = useSharedValue(0)
100
+ const sx = useSharedValue(1)
101
+ const sy = useSharedValue(1)
102
+
103
+ // Most-recent rect for this primitive. Updated on every layout commit;
104
+ // read on unmount to populate the registry as the FLIP source for the
105
+ // next mount with the same id.
106
+ const lastRectRef = useRef<SharedRect | null>(null)
107
+
108
+ // First-layout latch — only the first measurement after a fresh mount
109
+ // can consume a source rect. Subsequent layouts (resizes, prop changes)
110
+ // refresh the registry but never re-trigger a FLIP from an old source.
111
+ const consumedRef = useRef(false)
112
+
113
+ const transitionRef = useRef(transition)
114
+ transitionRef.current = transition
115
+ const reducedMotionRef = useRef(shouldReduceMotion)
116
+ reducedMotionRef.current = shouldReduceMotion
117
+
118
+ const setRef = useCallback(
119
+ (node: unknown) => {
120
+ if (typeof userRef === 'function') userRef(node)
121
+ else if (userRef) (userRef as MutableRefObject<unknown>).current = node
122
+ },
123
+ [userRef],
124
+ )
125
+
126
+ const onLayout = useCallback(
127
+ (event: LayoutChangeEvent) => {
128
+ userOnLayout?.(event)
129
+ if (!layoutId) return
130
+
131
+ const { x, y, width, height } = event.nativeEvent.layout
132
+ const rect: SharedRect = { x, y, width, height }
133
+ lastRectRef.current = rect
134
+
135
+ // First-layout-only: read the registry BEFORE writing our own rect
136
+ // so a previously-released source rect can be consumed cleanly
137
+ // without being overwritten by the current rect first.
138
+ let source: SharedRect | undefined
139
+ if (!consumedRef.current) {
140
+ consumedRef.current = true
141
+ source = consumeLayout(layoutId)
142
+ }
143
+ registerLayout(layoutId, rect)
144
+
145
+ if (source) {
146
+ applyFlip({
147
+ source,
148
+ target: rect,
149
+ dx,
150
+ dy,
151
+ sx,
152
+ sy,
153
+ transition: transitionRef.current,
154
+ shouldReduceMotion: reducedMotionRef.current,
155
+ })
156
+ }
157
+ },
158
+ // dx/dy/sx/sy are stable refs from useSharedValue, but eslint's
159
+ // exhaustive-deps would flag them — including them is harmless and
160
+ // silences the warning.
161
+ [layoutId, userOnLayout, dx, dy, sx, sy],
162
+ )
163
+
164
+ // Reset the first-layout latch when the id changes — a new id is logically
165
+ // a new shared-element identity and should be allowed to consume a source.
166
+ useEffect(() => {
167
+ consumedRef.current = false
168
+ }, [layoutId])
169
+
170
+ // On unmount, hand the latest rect to the registry under this id so the
171
+ // next mount can consume it as a FLIP source.
172
+ useEffect(() => {
173
+ return () => {
174
+ if (!layoutId) return
175
+ const rect = lastRectRef.current
176
+ if (!rect) return
177
+ releaseLayout(layoutId, rect)
178
+ }
179
+ }, [layoutId])
180
+
181
+ return useMemo<SharedLayoutBindings>(
182
+ () => ({
183
+ flip: { dx, dy, sx, sy },
184
+ setRef,
185
+ onLayout,
186
+ }),
187
+ [dx, dy, sx, sy, setRef, onLayout],
188
+ )
189
+ }
190
+
191
+ /**
192
+ * Snap the FLIP shared values so the new element visually overlays its
193
+ * source rect, then animate back to identity. The `withSequence(snap,
194
+ * animate)` shape is what makes the spring start from the snapped delta —
195
+ * a plain `withSpring(0)` from a zero base would animate from-zero, not
196
+ * from-source.
197
+ */
198
+ function applyFlip(args: {
199
+ source: SharedRect
200
+ target: SharedRect
201
+ dx: SharedValue<number>
202
+ dy: SharedValue<number>
203
+ sx: SharedValue<number>
204
+ sy: SharedValue<number>
205
+ transition: TransitionConfig | undefined
206
+ shouldReduceMotion: boolean
207
+ }): void {
208
+ const { source, target, dx, dy, sx, sy, transition, shouldReduceMotion } =
209
+ args
210
+
211
+ // Compute the delta that would visually place the new element at the
212
+ // source rect. The transform origin matters: RN scales around the
213
+ // element's center, so the translation needs to account for the
214
+ // center-of-source vs center-of-target offset, not the top-left offset.
215
+ const sourceCenterX = source.x + source.width / 2
216
+ const sourceCenterY = source.y + source.height / 2
217
+ const targetCenterX = target.x + target.width / 2
218
+ const targetCenterY = target.y + target.height / 2
219
+ const deltaX = sourceCenterX - targetCenterX
220
+ const deltaY = sourceCenterY - targetCenterY
221
+ // Guard against zero-sized targets (degenerate layout) — keep scale at 1
222
+ // so the element at least renders even if the FLIP isn't a perfect match.
223
+ const scaleX = target.width > 0 ? source.width / target.width : 1
224
+ const scaleY = target.height > 0 ? source.height / target.height : 1
225
+
226
+ if (shouldReduceMotion) {
227
+ // Reduced-motion: skip the visual transition entirely. The element
228
+ // appears at its natural position; the source rect is discarded.
229
+ dx.value = 0
230
+ dy.value = 0
231
+ sx.value = 1
232
+ sy.value = 1
233
+ return
234
+ }
235
+
236
+ if (transition?.type === 'no-animation') {
237
+ dx.value = 0
238
+ dy.value = 0
239
+ sx.value = 1
240
+ sy.value = 1
241
+ return
242
+ }
243
+
244
+ if (transition?.type === 'timing') {
245
+ const duration = transition.duration ?? 300
246
+ dx.value = withSequence(
247
+ withTiming(deltaX, { duration: 0 }),
248
+ withTiming(0, { duration }),
249
+ )
250
+ dy.value = withSequence(
251
+ withTiming(deltaY, { duration: 0 }),
252
+ withTiming(0, { duration }),
253
+ )
254
+ sx.value = withSequence(
255
+ withTiming(scaleX, { duration: 0 }),
256
+ withTiming(1, { duration }),
257
+ )
258
+ sy.value = withSequence(
259
+ withTiming(scaleY, { duration: 0 }),
260
+ withTiming(1, { duration }),
261
+ )
262
+ return
263
+ }
264
+
265
+ // Spring path (default, and the fallback for `'decay'` which doesn't
266
+ // have a meaningful target value for a FLIP transition).
267
+ const springCfg: SpringTransition =
268
+ transition?.type === 'spring'
269
+ ? { ...DEFAULT_SPRING, ...transition }
270
+ : { type: 'spring', ...DEFAULT_SPRING }
271
+ const springParams = springToReanimated(springCfg)
272
+
273
+ dx.value = withSequence(
274
+ withTiming(deltaX, { duration: 0 }),
275
+ withSpring(0, springParams),
276
+ )
277
+ dy.value = withSequence(
278
+ withTiming(deltaY, { duration: 0 }),
279
+ withSpring(0, springParams),
280
+ )
281
+ sx.value = withSequence(
282
+ withTiming(scaleX, { duration: 0 }),
283
+ withSpring(1, springParams),
284
+ )
285
+ sy.value = withSequence(
286
+ withTiming(scaleY, { duration: 0 }),
287
+ withSpring(1, springParams),
288
+ )
289
+ }
@@ -13,9 +13,14 @@ import Animated, {
13
13
  useSharedValue,
14
14
  type SharedValue,
15
15
  } from 'react-native-reanimated'
16
+ import { type LayoutChangeEvent } from 'react-native'
16
17
  import { useShouldReduceMotion } from '../config'
17
18
  import { isFocusVisible } from '../gestures'
18
- import { resolveLayoutTransition, type LayoutProp } from '../layout'
19
+ import {
20
+ resolveLayoutTransition,
21
+ type LayoutProp,
22
+ useSharedLayout,
23
+ } from '../layout'
19
24
  import { usePresence } from '../presence'
20
25
  import {
21
26
  isTopLevelTransition,
@@ -223,10 +228,31 @@ export function createMotionComponent<C extends ComponentType<any>>(
223
228
  controller,
224
229
  gesture,
225
230
  layout,
231
+ layoutId,
226
232
  onAnimationEnd,
227
233
  style,
234
+ onLayout: userOnLayout,
228
235
  ...rest
229
- } = props as Props & { style?: unknown; layout?: LayoutProp }
236
+ } = props as Props & {
237
+ style?: unknown
238
+ layout?: LayoutProp
239
+ layoutId?: string
240
+ onLayout?: (event: LayoutChangeEvent) => void
241
+ }
242
+
243
+ // Function-form `style={(state) => ...}` is the Pressable render-prop API.
244
+ // Inertia drives press/focus state through `gesture.*` and merges its own
245
+ // animated style; a function passed here lands inside a style array where
246
+ // the underlying component never invokes it, so the resulting styles are
247
+ // silently dropped. Throw loudly in dev rather than ship the footgun.
248
+ if (__DEV__ && typeof style === 'function') {
249
+ throw new Error(
250
+ '[inertia] `style` must be a style object or array of style objects, ' +
251
+ 'not a function. The function-form `style={(state) => ...}` Pressable ' +
252
+ 'API is not supported — use `gesture.pressed` (or `gesture.focused`, ' +
253
+ 'etc.) to drive state-dependent styling instead.',
254
+ )
255
+ }
230
256
 
231
257
  // <Presence> contract: when an ancestor flips `isPresent` to false the
232
258
  // child stays rendered until `safeToRemove` is called, giving the exit
@@ -274,43 +300,73 @@ export function createMotionComponent<C extends ComponentType<any>>(
274
300
  const [focusVisible, setFocusVisible] = useState(false)
275
301
  const [hovered, setHovered] = useState(false)
276
302
 
277
- // The set of keys this instance animates is locked at first render. With
278
- // variants in play the union across all variants is what matters — a key
279
- // touched by any variant must be active so the worklet picks it up when
280
- // the controller transitions. Gesture sub-states join the same union so
281
- // pressed/focused/focusVisible/hovered targets can drive any key they
282
- // declare even when the base `animate` doesn't touch it.
283
- const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
284
- if (activeKeysRef.current === null) {
285
- const touched = new Set<AnimatableKey>()
286
- collectTouchedKeys(touched, animateRecord)
287
- if (initialRecord) collectTouchedKeys(touched, initialRecord)
288
- if (variants) {
289
- for (const variant of Object.values(variants) as object[]) {
290
- if (!variant) continue
291
- collectTouchedKeys(touched, variant as Record<string, unknown>)
292
- }
303
+ // The set of keys this instance animates is a *monotonically growing*
304
+ // union, recomputed every render and expanded when a render introduces a
305
+ // key not seen before. It never shrinks. Two requirements meet here:
306
+ //
307
+ // 1. Variants and gesture sub-states contribute the union across *all*
308
+ // their branches up front a key touched by any variant must be
309
+ // active so the worklet picks it up when the controller transitions
310
+ // to a branch the base `animate` never mentions.
311
+ // 2. A literal `animate` object is reactive: a parent that changes
312
+ // `animate={{ opacity: 1 }}` to `animate={{ opacity: 1, scale: 2 }}`
313
+ // after mount must get `scale` animating. Freezing the set at first
314
+ // render silently dropped the new key (its SV updated, but the
315
+ // worklet which iterates this set — never read it).
316
+ //
317
+ // Growing-only keeps the worklet stable: the `activeKeysRef.current` array
318
+ // identity only changes on the renders that actually add a key, so the
319
+ // `useAnimatedStyle` worklet (which reads `.current` each frame) sees the
320
+ // expansion without churning frame-to-frame.
321
+ const touched = new Set<AnimatableKey>()
322
+ collectTouchedKeys(touched, animateRecord)
323
+ if (initialRecord) collectTouchedKeys(touched, initialRecord)
324
+ if (variants) {
325
+ for (const variant of Object.values(variants) as object[]) {
326
+ if (!variant) continue
327
+ collectTouchedKeys(touched, variant as Record<string, unknown>)
293
328
  }
294
- if (gesture) {
295
- for (const subState of [
296
- gesture.pressed,
297
- gesture.focused,
298
- gesture.focusVisible,
299
- gesture.hovered,
300
- ] as Array<object | undefined>) {
301
- if (!subState) continue
302
- collectTouchedKeys(touched, subState as Record<string, unknown>)
329
+ }
330
+ if (gesture) {
331
+ for (const subState of [
332
+ gesture.pressed,
333
+ gesture.focused,
334
+ gesture.focusVisible,
335
+ gesture.hovered,
336
+ ] as Array<object | undefined>) {
337
+ if (!subState) continue
338
+ collectTouchedKeys(touched, subState as Record<string, unknown>)
339
+ }
340
+ }
341
+ if (exitRecord) collectTouchedKeys(touched, exitRecord)
342
+
343
+ const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
344
+ const hasTransformRef = useRef<boolean>(false)
345
+ const hasShadowOffsetRef = useRef<boolean>(false)
346
+ // Expand the active set only when this render touched a key we haven't
347
+ // recorded yet. When nothing new appears we keep the existing array
348
+ // identity so the worklet's captured ref doesn't see a fresh value.
349
+ const prevActive = activeKeysRef.current
350
+ let grew = prevActive === null
351
+ if (!grew && prevActive) {
352
+ for (const k of touched) {
353
+ if (!prevActive.includes(k)) {
354
+ grew = true
355
+ break
303
356
  }
304
357
  }
305
- if (exitRecord) collectTouchedKeys(touched, exitRecord)
306
- activeKeysRef.current = ALL_KEYS.filter((k) => touched.has(k))
307
358
  }
308
- const hasTransformRef = useRef<boolean>(
309
- activeKeysRef.current.some((k) => TRANSFORM_KEY_SET.has(k)),
310
- )
311
- const hasShadowOffsetRef = useRef<boolean>(
312
- activeKeysRef.current.some((k) => SHADOW_OFFSET_KEY_SET.has(k)),
313
- )
359
+ if (grew) {
360
+ const merged = new Set<AnimatableKey>(prevActive ?? [])
361
+ for (const k of touched) merged.add(k)
362
+ activeKeysRef.current = ALL_KEYS.filter((k) => merged.has(k))
363
+ hasTransformRef.current = activeKeysRef.current.some((k) =>
364
+ TRANSFORM_KEY_SET.has(k),
365
+ )
366
+ hasShadowOffsetRef.current = activeKeysRef.current.some((k) =>
367
+ SHADOW_OFFSET_KEY_SET.has(k),
368
+ )
369
+ }
314
370
 
315
371
  const sharedValues = useAnimatableSharedValues((key) => {
316
372
  // Shadow offset synthetics seed from the corresponding axis on the
@@ -531,6 +587,23 @@ export function createMotionComponent<C extends ComponentType<any>>(
531
587
  shouldReduceMotion,
532
588
  )
533
589
 
590
+ // Shared-element transition wiring. `useSharedLayout` allocates FLIP
591
+ // shared values (identity at rest), measures via the merged `onLayout`,
592
+ // and on first-mount snaps the FLIP transform to a source rect popped
593
+ // from the registry. The worklet below appends those entries to the
594
+ // transform array so they compose with the user's animate transforms —
595
+ // multiple `translateX` entries sum, multiple `scaleX` entries multiply,
596
+ // which is exactly the FLIP semantic.
597
+ const sharedLayout = useSharedLayout({
598
+ layoutId,
599
+ userRef: ref,
600
+ transition: isTopLevelTransition(transition) ? transition : undefined,
601
+ shouldReduceMotion,
602
+ userOnLayout,
603
+ })
604
+ const flip = sharedLayout.flip
605
+ const hasLayoutId = layoutId !== undefined
606
+
534
607
  const animatedStyle = useAnimatedStyle(() => {
535
608
  const activeKeys = activeKeysRef.current!
536
609
  const hasTransform = hasTransformRef.current
@@ -611,7 +684,19 @@ export function createMotionComponent<C extends ComponentType<any>>(
611
684
  out[key] = v
612
685
  }
613
686
  }
614
- if (hasTransform) out.transform = transform
687
+ // Shared-element FLIP transforms append after the user's transform
688
+ // entries so they compose multiplicatively in the same `transform`
689
+ // array — separate style entries with `transform` keys would
690
+ // last-write-wins, which is what we explicitly avoid here. At rest
691
+ // (dx, dy, sx, sy) = (0, 0, 1, 1) so the contribution is a no-op
692
+ // when no shared-element transition is active.
693
+ if (hasLayoutId) {
694
+ transform.push({ translateX: flip.dx.value })
695
+ transform.push({ translateY: flip.dy.value })
696
+ transform.push({ scaleX: flip.sx.value })
697
+ transform.push({ scaleY: flip.sy.value })
698
+ }
699
+ if (hasTransform || hasLayoutId) out.transform = transform
615
700
  if (hasShadowOffset) {
616
701
  out.shadowOffset = { width: shadowOffsetW, height: shadowOffsetH }
617
702
  }
@@ -654,9 +739,10 @@ export function createMotionComponent<C extends ComponentType<any>>(
654
739
 
655
740
  return (
656
741
  <AnimatedComponent
657
- ref={ref as never}
742
+ ref={sharedLayout.setRef as never}
658
743
  {...(rest as object)}
659
744
  {...gestureHandlers}
745
+ onLayout={sharedLayout.onLayout}
660
746
  layout={layoutTransition}
661
747
  style={mergedStyle}
662
748
  />
@@ -1,6 +1,7 @@
1
+ import { reanimatedVersion } from 'react-native-reanimated'
2
+
1
3
  declare const __DEV__: boolean
2
4
  declare const process: { env?: Record<string, string | undefined> }
3
- declare const require: (path: string) => unknown
4
5
 
5
6
  let alreadyChecked = false
6
7
 
@@ -30,16 +31,11 @@ export function ensureReanimatedInstalled(): void {
30
31
  }
31
32
  alreadyChecked = true
32
33
 
33
- let version: string | undefined
34
- try {
35
- const pkg = require('react-native-reanimated/package.json') as {
36
- version?: string
37
- }
38
- version = pkg.version
39
- } catch {
40
- // package.json subpath blocked by `exports` field — skip the version
41
- // probe rather than emit a misleading error.
42
- }
34
+ // Read the version off Reanimated's own runtime export rather than reaching
35
+ // into its `package.json`. A `require('.../package.json')` here would make
36
+ // esbuild emit a `__require` shim that throws on web bundlers (Expo web), and
37
+ // Reanimated's `exports` field may block the subpath anyway.
38
+ const version: string | undefined = reanimatedVersion
43
39
 
44
40
  if (version) {
45
41
  const major = parseInt(version.split('.')[0] ?? '0', 10)
@@ -0,0 +1,18 @@
1
+ /**
2
+ * PanResponder-backed drag hook. Lives in core because `PanResponder` is
3
+ * built into React Native — no extra peer dependency. Use this when you
4
+ * need keyboard a11y alongside drag, or when you don't want to take
5
+ * `react-native-gesture-handler` as a dependency.
6
+ *
7
+ * For pointer-only drag in a project that already uses gesture-handler,
8
+ * prefer `useDrag` from `@onlynative/inertia-gestures` — its UI-thread
9
+ * release path is more precise.
10
+ */
11
+ export { useTouchDrag } from './useTouchDrag'
12
+ export type {
13
+ TouchReleaseInfo,
14
+ TouchReleaseResult,
15
+ TouchReleaseTransition,
16
+ UseTouchDragOptions,
17
+ UseTouchDragResult,
18
+ } from './useTouchDrag'