react-native-screen-transitions 3.4.0-alpha.5 → 3.4.0-alpha.6

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 +968 -11
  2. package/lib/commonjs/shared/components/screen-container/deferred-visibility-host.js +3 -1
  3. package/lib/commonjs/shared/components/screen-container/deferred-visibility-host.js.map +1 -1
  4. package/lib/commonjs/shared/components/screen-container/hooks/use-backdrop-pointer-events.js.map +1 -1
  5. package/lib/commonjs/shared/components/screen-container/index.js +10 -2
  6. package/lib/commonjs/shared/components/screen-container/index.js.map +1 -1
  7. package/lib/commonjs/shared/components/screen-container/layers/backdrop.js +4 -6
  8. package/lib/commonjs/shared/components/screen-container/layers/backdrop.js.map +1 -1
  9. package/lib/commonjs/shared/components/screen-container/layers/content.js +3 -6
  10. package/lib/commonjs/shared/components/screen-container/layers/content.js.map +1 -1
  11. package/lib/commonjs/shared/constants.js +2 -0
  12. package/lib/commonjs/shared/constants.js.map +1 -1
  13. package/lib/commonjs/shared/providers/screen/animation/helpers/hydrate-transition-state.js +33 -1
  14. package/lib/commonjs/shared/providers/screen/animation/helpers/hydrate-transition-state.js.map +1 -1
  15. package/lib/commonjs/shared/providers/screen/animation/helpers/pipeline.js +2 -0
  16. package/lib/commonjs/shared/providers/screen/animation/helpers/pipeline.js.map +1 -1
  17. package/lib/commonjs/shared/providers/screen/animation/helpers/use-build-transition-state.js +1 -0
  18. package/lib/commonjs/shared/providers/screen/animation/helpers/use-build-transition-state.js.map +1 -1
  19. package/lib/commonjs/shared/utils/bounds/zoom/build.js +5 -5
  20. package/lib/commonjs/shared/utils/bounds/zoom/build.js.map +1 -1
  21. package/lib/module/shared/components/screen-container/deferred-visibility-host.js +3 -1
  22. package/lib/module/shared/components/screen-container/deferred-visibility-host.js.map +1 -1
  23. package/lib/module/shared/components/screen-container/hooks/use-backdrop-pointer-events.js.map +1 -1
  24. package/lib/module/shared/components/screen-container/index.js +10 -2
  25. package/lib/module/shared/components/screen-container/index.js.map +1 -1
  26. package/lib/module/shared/components/screen-container/layers/backdrop.js +4 -6
  27. package/lib/module/shared/components/screen-container/layers/backdrop.js.map +1 -1
  28. package/lib/module/shared/components/screen-container/layers/content.js +3 -6
  29. package/lib/module/shared/components/screen-container/layers/content.js.map +1 -1
  30. package/lib/module/shared/constants.js +2 -0
  31. package/lib/module/shared/constants.js.map +1 -1
  32. package/lib/module/shared/providers/screen/animation/helpers/hydrate-transition-state.js +32 -1
  33. package/lib/module/shared/providers/screen/animation/helpers/hydrate-transition-state.js.map +1 -1
  34. package/lib/module/shared/providers/screen/animation/helpers/pipeline.js +2 -0
  35. package/lib/module/shared/providers/screen/animation/helpers/pipeline.js.map +1 -1
  36. package/lib/module/shared/providers/screen/animation/helpers/use-build-transition-state.js +1 -0
  37. package/lib/module/shared/providers/screen/animation/helpers/use-build-transition-state.js.map +1 -1
  38. package/lib/module/shared/utils/bounds/zoom/build.js +5 -5
  39. package/lib/module/shared/utils/bounds/zoom/build.js.map +1 -1
  40. package/lib/typescript/shared/components/screen-container/deferred-visibility-host.d.ts +2 -1
  41. package/lib/typescript/shared/components/screen-container/deferred-visibility-host.d.ts.map +1 -1
  42. package/lib/typescript/shared/components/screen-container/hooks/use-backdrop-pointer-events.d.ts +1 -1
  43. package/lib/typescript/shared/components/screen-container/hooks/use-backdrop-pointer-events.d.ts.map +1 -1
  44. package/lib/typescript/shared/components/screen-container/index.d.ts.map +1 -1
  45. package/lib/typescript/shared/components/screen-container/layers/backdrop.d.ts +5 -1
  46. package/lib/typescript/shared/components/screen-container/layers/backdrop.d.ts.map +1 -1
  47. package/lib/typescript/shared/components/screen-container/layers/content.d.ts +3 -1
  48. package/lib/typescript/shared/components/screen-container/layers/content.d.ts.map +1 -1
  49. package/lib/typescript/shared/constants.d.ts.map +1 -1
  50. package/lib/typescript/shared/providers/screen/animation/helpers/hydrate-transition-state.d.ts +16 -0
  51. package/lib/typescript/shared/providers/screen/animation/helpers/hydrate-transition-state.d.ts.map +1 -1
  52. package/lib/typescript/shared/providers/screen/animation/helpers/pipeline.d.ts.map +1 -1
  53. package/lib/typescript/shared/providers/screen/animation/helpers/use-build-transition-state.d.ts +1 -0
  54. package/lib/typescript/shared/providers/screen/animation/helpers/use-build-transition-state.d.ts.map +1 -1
  55. package/lib/typescript/shared/types/animation.types.d.ts +13 -0
  56. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  57. package/lib/typescript/shared/types/screen.types.d.ts +2 -1
  58. package/lib/typescript/shared/types/screen.types.d.ts.map +1 -1
  59. package/lib/typescript/shared/utils/bounds/zoom/build.d.ts.map +1 -1
  60. package/package.json +1 -1
  61. package/src/shared/components/screen-container/deferred-visibility-host.tsx +19 -12
  62. package/src/shared/components/screen-container/hooks/use-backdrop-pointer-events.ts +1 -2
  63. package/src/shared/components/screen-container/index.tsx +13 -4
  64. package/src/shared/components/screen-container/layers/backdrop.tsx +9 -3
  65. package/src/shared/components/screen-container/layers/content.tsx +46 -42
  66. package/src/shared/constants.ts +2 -0
  67. package/src/shared/providers/screen/animation/helpers/hydrate-transition-state.ts +49 -1
  68. package/src/shared/providers/screen/animation/helpers/pipeline.ts +2 -0
  69. package/src/shared/providers/screen/animation/helpers/use-build-transition-state.ts +2 -0
  70. package/src/shared/types/animation.types.ts +15 -0
  71. package/src/shared/types/screen.types.ts +3 -1
  72. package/src/shared/utils/bounds/zoom/build.ts +25 -5
package/README.md CHANGED
@@ -1,14 +1,55 @@
1
1
  # react-native-screen-transitions
2
2
 
3
- Gesture-driven screen transitions, shared bounds, presets, and custom animation hooks for React Native and Expo.
3
+ Customizable screen transitions for React Native. Build gesture-driven, shared element, and fully custom animations with a simple API.
4
4
 
5
- ## Install
5
+ | iOS | Android |
6
+ | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
7
+ | <video src="https://github.com/user-attachments/assets/c0d17b8f-7268-421c-9051-e242f8ddca76" width="300" height="600" controls></video> | <video src="https://github.com/user-attachments/assets/3f8d5fb1-96d2-4fe3-860d-62f6fb5a687e" width="300" controls></video> |
8
+
9
+ ## Features
10
+
11
+ - **Full Animation Control** – Define exactly how screens enter, exit, and respond to gestures
12
+ - **Bounds API + Navigation Zoom** – Build shared element and fullscreen zoom transitions with one bounds helper
13
+ - **Auto Snap Points** – Use `snapPoints: ["auto"]` and read measured content layout inside your interpolator
14
+ - **Gesture-Aware Scrollables** – Transition-aware `ScrollView` and `FlatList` coordinate with dismiss and snap gestures
15
+ - **Backdrop + Surface Slots** – Animate screen content, backdrops, surfaces, and per-element slots from one interpolator
16
+ - **Ready-Made Presets** – Instagram, Apple Music, X (Twitter) style transitions included
17
+
18
+ ## What's New In 3.4
19
+
20
+ - **Auto snap sizing** with `snapPoints: ["auto"]` and `layouts.content`
21
+ - **Explicit deferred first frames** by returning `"defer"` from `screenStyleInterpolator`
22
+ - **Compound bounds components** via `Transition.Boundary.View` and `Transition.Boundary.Pressable`
23
+ - **Custom boundary factories** via `Transition.createBoundaryComponent(..., { alreadyAnimated: true })`
24
+ - **Navigation-style bounds zoom** through `bounds({ id }).navigation.zoom()`
25
+ - **Ancestor targeting** in `useScreenGesture()` and `useScreenAnimation()`
26
+ - **Gesture release tuning** with `gestureReleaseVelocityScale` and `gestureReleaseVelocityMax`
27
+ - **Surface slot support** through `surfaceComponent` and the interpolator `surface` slot
28
+ - **Optional first-screen animation** with `experimental_animateOnInitialMount`
29
+
30
+ ## When to Use This Library
31
+
32
+ | Use Case | This Library | Alternative |
33
+ |----------|--------------|-------------|
34
+ | Custom transitions (slide, zoom, fade variations) | Yes | `@react-navigation/stack` works too |
35
+ | Shared element transitions | **Yes** | Limited options elsewhere |
36
+ | Multi-stop sheets (bottom, top, side) with snap points | **Yes** | Dedicated sheet libraries |
37
+ | Gesture-driven animations (drag to dismiss, elastic) | **Yes** | Requires custom implementation |
38
+ | Instagram/Apple Music/Twitter-style transitions | **Yes** | Custom implementation |
39
+ | Simple push/pop with platform defaults | Overkill | `@react-navigation/native-stack` |
40
+ | Maximum raw performance on low-end devices | Not ideal | `@react-navigation/native-stack` |
41
+
42
+ **Choose this library when** you need custom animations, shared elements, or gesture-driven transitions that go beyond platform defaults.
43
+
44
+ **Choose native-stack when** you want platform-native transitions with zero configuration and maximum performance on low-end Android devices.
45
+
46
+ ## Installation
6
47
 
7
48
  ```bash
8
49
  npm install react-native-screen-transitions
9
50
  ```
10
51
 
11
- Peer dependencies:
52
+ ### Peer Dependencies
12
53
 
13
54
  ```bash
14
55
  npm install react-native-reanimated react-native-gesture-handler \
@@ -17,15 +58,19 @@ npm install react-native-reanimated react-native-gesture-handler \
17
58
  react-native-safe-area-context
18
59
  ```
19
60
 
20
- ## Quickstart
61
+ ---
62
+
63
+ ## Quick Start
64
+
65
+ ### 1. Create a Stack
21
66
 
22
67
  ```tsx
23
- import Transition from "react-native-screen-transitions";
24
68
  import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
69
+ import Transition from "react-native-screen-transitions";
25
70
 
26
71
  const Stack = createBlankStackNavigator();
27
72
 
28
- export function AppStack() {
73
+ function App() {
29
74
  return (
30
75
  <Stack.Navigator>
31
76
  <Stack.Screen name="Home" component={HomeScreen} />
@@ -41,9 +86,921 @@ export function AppStack() {
41
86
  }
42
87
  ```
43
88
 
44
- ## Docs
89
+ ### 2. With Expo Router
90
+
91
+ ```tsx
92
+ import { withLayoutContext } from "expo-router";
93
+ import {
94
+ createBlankStackNavigator,
95
+ type BlankStackNavigationOptions,
96
+ } from "react-native-screen-transitions/blank-stack";
97
+
98
+ const { Navigator } = createBlankStackNavigator();
99
+
100
+ export const Stack = withLayoutContext<
101
+ BlankStackNavigationOptions,
102
+ typeof Navigator
103
+ >(Navigator);
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Presets
109
+
110
+ Use built-in presets for common transitions:
111
+
112
+ ```tsx
113
+ <Stack.Screen
114
+ name="Detail"
115
+ options={{
116
+ ...Transition.Presets.SlideFromBottom(),
117
+ }}
118
+ />
119
+ ```
120
+
121
+ | Preset | Description |
122
+ | -------------------------------------- | --------------------------------------- |
123
+ | `SlideFromTop()` | Slides in from top |
124
+ | `SlideFromBottom()` | Slides in from bottom (modal-style) |
125
+ | `ZoomIn()` | Scales in with fade |
126
+ | `DraggableCard()` | Multi-directional drag with scaling |
127
+ | `ElasticCard()` | Elastic drag with overlay |
128
+ | `SharedIGImage({ sharedBoundTag })` | Instagram-style shared image |
129
+ | `SharedAppleMusic({ sharedBoundTag })` | Apple Music-style shared element |
130
+ | `SharedXImage({ sharedBoundTag })` | X (Twitter)-style image transition |
131
+
132
+ ---
133
+
134
+ ## Custom Animations
135
+
136
+ ### The Basics
137
+
138
+ Every screen has a `progress` value that goes from 0 → 1 → 2:
139
+
140
+ ```
141
+ 0 ─────────── 1 ─────────── 2
142
+ entering visible exiting
143
+ ```
144
+
145
+ When navigating from A to B:
146
+ - **Screen B**: progress goes `0 → 1` (entering)
147
+ - **Screen A**: progress goes `1 → 2` (exiting)
148
+
149
+ ### Simple Fade
150
+
151
+ ```tsx
152
+ options={{
153
+ screenStyleInterpolator: ({ progress }) => {
154
+ "worklet";
155
+ return {
156
+ content: {
157
+ style: {
158
+ opacity: interpolate(progress, [0, 1, 2], [0, 1, 0]),
159
+ },
160
+ },
161
+ };
162
+ },
163
+ }}
164
+ ```
165
+
166
+ ### Slide from Right
167
+
168
+ ```tsx
169
+ options={{
170
+ screenStyleInterpolator: ({ progress, layouts: { screen } }) => {
171
+ "worklet";
172
+ return {
173
+ content: {
174
+ style: {
175
+ transform: [{
176
+ translateX: interpolate(
177
+ progress,
178
+ [0, 1, 2],
179
+ [screen.width, 0, -screen.width * 0.3]
180
+ ),
181
+ }],
182
+ },
183
+ },
184
+ };
185
+ },
186
+ }}
187
+ ```
188
+
189
+ ### Slide from Bottom
190
+
191
+ ```tsx
192
+ options={{
193
+ screenStyleInterpolator: ({ progress, layouts: { screen } }) => {
194
+ "worklet";
195
+ return {
196
+ content: {
197
+ style: {
198
+ transform: [{
199
+ translateY: interpolate(progress, [0, 1], [screen.height, 0]),
200
+ }],
201
+ },
202
+ },
203
+ };
204
+ },
205
+ }}
206
+ ```
207
+
208
+ ### Return Styles
209
+
210
+ Your interpolator can return:
211
+
212
+ ```tsx
213
+ return {
214
+ content: { style: { ... } }, // Main screen
215
+ backdrop: { style: { ... } }, // Backdrop / blur / dimming
216
+ surface: { style: { ... }, props: { ... } }, // Custom surface layer
217
+ ["my-id"]: { style: { ... } }, // Specific element via styleId
218
+ };
219
+ ```
220
+
221
+ Return `"defer"` to hide the screen's visual subtree until you have a safe first frame:
222
+
223
+ ```tsx
224
+ screenStyleInterpolator: ({ bounds }) => {
225
+ "worklet";
226
+
227
+ const snapshot = bounds.getSnapshot("hero");
228
+ if (!snapshot) return "defer";
229
+
230
+ return {
231
+ content: {
232
+ style: { opacity: 1 },
233
+ },
234
+ };
235
+ };
236
+ ```
237
+
238
+ ### Animation Specs
239
+
240
+ Control timing with spring configs:
241
+
242
+ ```tsx
243
+ options={{
244
+ screenStyleInterpolator: myInterpolator,
245
+ transitionSpec: {
246
+ open: { stiffness: 1000, damping: 500, mass: 3 }, // Screen enters
247
+ close: { stiffness: 1000, damping: 500, mass: 3 }, // Screen exits
248
+ expand: { stiffness: 300, damping: 30 }, // Snap point increases
249
+ collapse: { stiffness: 300, damping: 30 }, // Snap point decreases
250
+ },
251
+ }}
252
+ ```
253
+
254
+ ---
255
+
256
+ ## Gestures
257
+
258
+ Enable swipe-to-dismiss:
259
+
260
+ ```tsx
261
+ options={{
262
+ gestureEnabled: true,
263
+ gestureDirection: "vertical",
264
+ ...Transition.Presets.SlideFromBottom(),
265
+ }}
266
+ ```
267
+
268
+ ### Gesture Options
269
+
270
+ | Option | Description |
271
+ | ------------------------- | ------------------------------------------------------------------------ |
272
+ | `gestureEnabled` | Enable swipe-to-dismiss (snap sheets: `false` blocks dismiss-to-0 only) |
273
+ | `gestureDirection` | Direction(s) for swipe gesture |
274
+ | `gestureActivationArea` | Where gesture can start |
275
+ | `gestureResponseDistance` | Pixel threshold for activation |
276
+ | `gestureVelocityImpact` | How much velocity affects dismissal (default: 0.3) |
277
+ | `gestureDrivesProgress` | Whether gesture controls animation progress (default: true) |
278
+ | `snapVelocityImpact` | How much velocity affects snap targeting (default: 0.1, lower = iOS-like)|
279
+ | `gestureReleaseVelocityScale` | Multiplier for release velocity used by post-release spring animations |
280
+ | `gestureReleaseVelocityMax` | Max absolute normalized release velocity used by spring animations |
281
+ | `sheetScrollGestureBehavior` | Nested scroll handoff mode: `"expand-and-collapse"` or `"collapse-only"` |
282
+ | `gestureSnapLocked` | Lock gesture-based snap movement to current snap point |
283
+ | `backdropBehavior` | Touch handling for backdrop area |
284
+ | `backdropComponent` | Custom backdrop component (replaces default backdrop + press behavior) |
285
+
286
+ ### Gesture Direction
287
+
288
+ ```tsx
289
+ gestureDirection: "horizontal" // swipe left to dismiss
290
+ gestureDirection: "horizontal-inverted" // swipe right to dismiss
291
+ gestureDirection: "vertical" // swipe down to dismiss
292
+ gestureDirection: "vertical-inverted" // swipe up to dismiss
293
+ gestureDirection: "bidirectional" // any direction
294
+
295
+ // Or combine multiple:
296
+ gestureDirection: ["horizontal", "vertical"]
297
+ ```
298
+
299
+ ### Gesture Activation Area
300
+
301
+ ```tsx
302
+ // Simple - same for all edges
303
+ gestureActivationArea: "edge" // only from screen edges
304
+ gestureActivationArea: "screen" // anywhere on screen
305
+
306
+ // Per-side configuration
307
+ gestureActivationArea: {
308
+ left: "edge",
309
+ right: "screen",
310
+ top: "edge",
311
+ bottom: "screen",
312
+ }
313
+ ```
314
+
315
+ ### With ScrollViews
316
+
317
+ Use transition-aware scrollables so gestures work correctly:
318
+
319
+ ```tsx
320
+ <Transition.ScrollView>
321
+ {/* content */}
322
+ </Transition.ScrollView>
323
+
324
+ <Transition.FlatList data={items} renderItem={...} />
325
+ ```
326
+
327
+ Gesture rules with scrollables:
328
+ - **vertical** – only activates when scrolled to top
329
+ - **vertical-inverted** – only activates when scrolled to bottom
330
+ - **horizontal** – only activates at left/right scroll edges
331
+
332
+ ---
333
+
334
+ ## Snap Points
335
+
336
+ Create multi-stop sheets that snap to defined positions. Works with any gesture direction (bottom sheets, top sheets, side sheets), and supports intrinsic content sizing with `"auto"` snap points.
337
+
338
+ ### Basic Configuration
339
+
340
+ ```tsx
341
+ // Bottom sheet (most common)
342
+ <Stack.Screen
343
+ name="Sheet"
344
+ options={{
345
+ gestureEnabled: true,
346
+ gestureDirection: "vertical",
347
+ snapPoints: [0.5, 1], // 50% and 100% of screen
348
+ initialSnapIndex: 0, // Start at 50%
349
+ backdropBehavior: "dismiss", // Tap backdrop to dismiss
350
+ ...Transition.Presets.SlideFromBottom(),
351
+ }}
352
+ />
353
+
354
+ // Side sheet (same API, different direction)
355
+ <Stack.Screen
356
+ name="SidePanel"
357
+ options={{
358
+ gestureEnabled: true,
359
+ gestureDirection: "horizontal",
360
+ snapPoints: [0.3, 0.7, 1], // 30%, 70%, 100% of screen width
361
+ initialSnapIndex: 1,
362
+ // Add a horizontal screenStyleInterpolator for drawer-style motion
363
+ }}
364
+ />
365
+
366
+ // Auto-sized sheet
367
+ <Stack.Screen
368
+ name="Composer"
369
+ options={{
370
+ gestureEnabled: true,
371
+ gestureDirection: "vertical",
372
+ snapPoints: ["auto", 1],
373
+ initialSnapIndex: 0,
374
+ backdropBehavior: "collapse",
375
+ ...Transition.Presets.SlideFromBottom(),
376
+ }}
377
+ />
378
+ ```
379
+
380
+ ### Options
381
+
382
+ | Option | Description |
383
+ | ------------------ | -------------------------------------------------------------------- |
384
+ | `snapPoints` | Array of fractions (0-1) or `"auto"` values where sheet can rest |
385
+ | `initialSnapIndex` | Index of initial snap point (default: 0) |
386
+ | `gestureSnapLocked` | Locks gesture snapping to current point (programmatic `snapTo` still works) |
387
+ | `sheetScrollGestureBehavior` | Nested scroll handoff mode for snap sheets |
388
+ | `backdropBehavior` | Touch handling: `"block"`, `"passthrough"`, `"dismiss"`, `"collapse"`|
389
+ | `backdropComponent` | Custom backdrop component; replaces default backdrop + tap handling |
390
+
391
+ ### Auto Snap Points
392
+
393
+ When you use `"auto"`, the library measures the content height and exposes it in `layouts.content`:
394
+
395
+ ```tsx
396
+ screenStyleInterpolator: ({ layouts, snapIndex }) => {
397
+ "worklet";
398
+
399
+ const contentHeight = layouts.content?.height ?? 0;
400
+
401
+ return {
402
+ content: {
403
+ style: {
404
+ opacity: interpolate(snapIndex, [0, 1], [0.8, 1]),
405
+ },
406
+ },
407
+ "sheet-height-debug": {
408
+ style: {
409
+ opacity: contentHeight > 0 ? 1 : 0,
410
+ },
411
+ },
412
+ };
413
+ }
414
+ ```
415
+
416
+ #### backdropBehavior Values
417
+
418
+ | Value | Description |
419
+ | --------------- | ---------------------------------------------------------------- |
420
+ | `"block"` | Backdrop catches all touches (default) |
421
+ | `"passthrough"` | Touches pass through to content behind |
422
+ | `"dismiss"` | Tapping backdrop dismisses the screen |
423
+ | `"collapse"` | Tapping backdrop collapses to next lower snap point, then dismisses |
424
+
425
+ #### Custom Backdrop Component
426
+
427
+ Use `backdropComponent` when you want full control over backdrop visuals and interactions.
428
+
429
+ - When provided, it replaces the default backdrop entirely (including default tap behavior)
430
+ - You are responsible for dismiss/collapse actions inside the custom component
431
+ - `backdropBehavior` still controls container-level pointer event behavior
432
+
433
+ ```tsx
434
+ import { router } from "expo-router";
435
+ import { Pressable } from "react-native";
436
+ import Animated, { interpolate, useAnimatedStyle } from "react-native-reanimated";
437
+ import { useScreenAnimation } from "react-native-screen-transitions";
438
+
439
+ function SheetBackdrop() {
440
+ const animation = useScreenAnimation();
441
+
442
+ const style = useAnimatedStyle(() => ({
443
+ opacity: interpolate(animation.value.current.progress, [0, 1], [0, 0.4]),
444
+ backgroundColor: "#000",
445
+ }));
446
+
447
+ return (
448
+ <Pressable style={{ flex: 1 }} onPress={() => router.back()}>
449
+ <Animated.View style={[{ flex: 1 }, style]} />
450
+ </Pressable>
451
+ );
452
+ }
453
+
454
+ <Stack.Screen
455
+ name="Sheet"
456
+ options={{
457
+ snapPoints: [0.5, 1],
458
+ backdropBehavior: "dismiss",
459
+ backdropComponent: SheetBackdrop,
460
+ }}
461
+ />
462
+ ```
463
+
464
+ ### Programmatic Control
465
+
466
+ Control snap points from anywhere in your app:
467
+
468
+ ```tsx
469
+ import { snapTo } from "react-native-screen-transitions";
470
+
471
+ function BottomSheet() {
472
+ // Expand to full height (index 1)
473
+ const expand = () => snapTo(1);
474
+
475
+ // Collapse to half height (index 0)
476
+ const collapse = () => snapTo(0);
477
+
478
+ return (
479
+ <View>
480
+ <Button title="Expand" onPress={expand} />
481
+ <Button title="Collapse" onPress={collapse} />
482
+ </View>
483
+ );
484
+ }
485
+ ```
486
+
487
+ The animated `snapIndex` is available in screen interpolators via `ScreenInterpolationProps`:
488
+
489
+ ```tsx
490
+ screenStyleInterpolator: ({ snapIndex }) => {
491
+ // snapIndex interpolates between snap point indices
492
+ // e.g., 0.5 means halfway between snap point 0 and 1
493
+ return {
494
+ content: {
495
+ style: {
496
+ opacity: interpolate(snapIndex, [0, 1], [0.5, 1]),
497
+ },
498
+ },
499
+ };
500
+ }
501
+ ```
502
+
503
+ ### ScrollView Behavior
504
+
505
+ With `Transition.ScrollView` inside a snap-enabled sheet:
506
+ - **`sheetScrollGestureBehavior: "expand-and-collapse"`**: At boundary, swipe up expands and swipe down collapses (or dismisses at min if enabled)
507
+ - **`sheetScrollGestureBehavior: "collapse-only"`**: Expand works only via deadspace; collapse/dismiss via scroll still works at boundary
508
+ - **Scrolled into content**: Normal scroll behavior
509
+
510
+ ### Snap Animation Specs
511
+
512
+ Customize snap animations separately from enter/exit:
513
+
514
+ ```tsx
515
+ transitionSpec: {
516
+ open: { stiffness: 1000, damping: 500, mass: 3 }, // Screen enter
517
+ close: { stiffness: 1000, damping: 500, mass: 3 }, // Screen exit
518
+ expand: { stiffness: 300, damping: 30 }, // Snap up
519
+ collapse: { stiffness: 300, damping: 30 }, // Snap down
520
+ }
521
+ ```
522
+
523
+ ---
524
+
525
+ ## Shared Elements (Bounds API)
526
+
527
+ Animate elements between screens by tagging them. In 3.4, the recommended API is `Transition.Boundary.*` for explicit bounds ownership, while `sharedBoundTag` on transition-aware components still works for lighter setups.
528
+
529
+ ### 1. Tag the Source
530
+
531
+ ```tsx
532
+ <Transition.Boundary.Pressable
533
+ id="avatar"
534
+ onPress={() => navigation.navigate("Profile")}
535
+ >
536
+ <Image source={avatar} style={{ width: 50, height: 50 }} />
537
+ </Transition.Boundary.Pressable>
538
+ ```
539
+
540
+ ### 2. Tag the Destination
541
+
542
+ ```tsx
543
+ <Transition.Boundary.View id="avatar">
544
+ <Image source={avatar} style={{ width: 200, height: 200 }} />
545
+ </Transition.Boundary.View>
546
+ ```
547
+
548
+ ### 3. Use in Interpolator
549
+
550
+ ```tsx
551
+ screenStyleInterpolator: ({ bounds }) => {
552
+ "worklet";
553
+ return {
554
+ avatar: bounds({ id: "avatar", method: "transform" }),
555
+ };
556
+ };
557
+ ```
558
+
559
+ ### Navigation Zoom
560
+
561
+ For fullscreen, navigation-style shared transitions:
562
+
563
+ ```tsx
564
+ screenStyleInterpolator: ({ bounds }) => {
565
+ "worklet";
566
+ return bounds({ id: "avatar" }).navigation.zoom({
567
+ target: "fullscreen",
568
+ });
569
+ };
570
+ ```
571
+
572
+ ### Bounds Options
573
+
574
+ | Option | Values | Description |
575
+ | ----------- | ---------------------------------- | ----------------------------- |
576
+ | `id` | string | The boundary id to match |
577
+ | `group` | string | Optional group key for paged/detail flows |
578
+ | `method` | `"transform"` `"size"` `"content"` | How to animate |
579
+ | `space` | `"relative"` `"absolute"` | Coordinate space |
580
+ | `scaleMode` | `"match"` `"none"` `"uniform"` | Aspect ratio handling |
581
+ | `raw` | boolean | Return raw values |
582
+
583
+ ---
584
+
585
+ ## Overlays
586
+
587
+ Persistent UI that animates with the stack:
588
+
589
+ ```tsx
590
+ const TabBar = ({ focusedIndex, progress }) => {
591
+ const style = useAnimatedStyle(() => ({
592
+ transform: [{ translateY: interpolate(progress.value, [0, 1], [100, 0]) }],
593
+ }));
594
+ return <Animated.View style={[styles.tabBar, style]} />;
595
+ };
596
+
597
+ <Stack.Screen
598
+ name="Home"
599
+ options={{
600
+ overlay: TabBar,
601
+ overlayShown: true,
602
+ }}
603
+ />
604
+ ```
605
+
606
+ ### Overlay Props
607
+
608
+ | Prop | Description |
609
+ | -------------- | ------------------------------ |
610
+ | `focusedRoute` | Currently focused route |
611
+ | `focusedIndex` | Index of focused screen |
612
+ | `routes` | All routes in the stack |
613
+ | `progress` | Stack progress (derived value) |
614
+ | `navigation` | Navigation prop |
615
+ | `meta` | Custom metadata from options |
616
+
617
+ ---
618
+
619
+ ## Transition Components
620
+
621
+ | Component | Description |
622
+ | ----------------------- | -------------------------------------- |
623
+ | `Transition.View` | Animated view with `sharedBoundTag` |
624
+ | `Transition.Pressable` | Pressable that measures bounds |
625
+ | `Transition.ScrollView` | ScrollView with gesture coordination |
626
+ | `Transition.FlatList` | FlatList with gesture coordination |
627
+ | `Transition.Boundary.View` | Explicit bounds destination/source registration |
628
+ | `Transition.Boundary.Pressable` | Pressable boundary that captures source bounds on press |
629
+ | `Transition.MaskedView` | For reveal effects (requires native) |
630
+
631
+ Helper exports:
632
+
633
+ - `Transition.createBoundaryComponent(Component, { alreadyAnimated?: boolean })`
634
+ - `Transition.createTransitionAwareComponent(Component, { isScrollable?: boolean, alreadyAnimated?: boolean })`
635
+
636
+ ---
637
+
638
+ ## Hooks
639
+
640
+ ### useScreenAnimation
641
+
642
+ Access animation state inside a screen:
643
+
644
+ ```tsx
645
+ import { useScreenAnimation } from "react-native-screen-transitions";
646
+
647
+ function DetailScreen() {
648
+ const animation = useScreenAnimation();
649
+ const parentAnimation = useScreenAnimation("parent");
650
+
651
+ const style = useAnimatedStyle(() => ({
652
+ opacity: parentAnimation.value.current.progress,
653
+ }));
654
+
655
+ return <Animated.View style={style}>...</Animated.View>;
656
+ }
657
+ ```
658
+
659
+ ### useScreenState
660
+
661
+ Get navigation state without animation values:
662
+
663
+ ```tsx
664
+ import { useScreenState } from "react-native-screen-transitions";
665
+
666
+ function DetailScreen() {
667
+ const { index, focusedRoute, routes, navigation } = useScreenState();
668
+ // ...
669
+ }
670
+ ```
671
+
672
+ ### useHistory
673
+
674
+ Access navigation history across the app:
675
+
676
+ ```tsx
677
+ import { useHistory } from "react-native-screen-transitions";
678
+
679
+ function MyComponent() {
680
+ const { getRecent, getPath } = useHistory();
681
+
682
+ const recentScreens = getRecent(5); // Last 5 screens
683
+ const path = getPath(fromKey, toKey); // Path between screens
684
+ }
685
+ ```
686
+
687
+ ### useScreenGesture
688
+
689
+ Coordinate your own pan gestures with the navigation gesture:
690
+
691
+ ```tsx
692
+ import { useScreenGesture } from "react-native-screen-transitions";
693
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
694
+
695
+ function MyScreen() {
696
+ const screenGesture = useScreenGesture();
697
+ const parentGesture = useScreenGesture("parent");
698
+
699
+ const myPanGesture = Gesture.Pan()
700
+ .simultaneousWithExternalGesture(screenGesture, parentGesture)
701
+ .onUpdate((e) => {
702
+ // Your gesture logic
703
+ });
704
+
705
+ return (
706
+ <GestureDetector gesture={myPanGesture}>
707
+ <View />
708
+ </GestureDetector>
709
+ );
710
+ }
711
+ ```
712
+
713
+ Use this when you have custom pan gestures that need to work alongside screen dismiss gestures.
714
+ You can target `"self"`, `"parent"`, `"root"`, or `{ ancestor: number }`.
715
+
716
+ ---
717
+
718
+ ## Advanced Animation Props
719
+
720
+ The full `screenStyleInterpolator` receives these props:
721
+
722
+ | Prop | Description |
723
+ | ---------------- | -------------------------------------------------------- |
724
+ | `progress` | Combined progress (0-2) |
725
+ | `stackProgress` | Accumulated progress across entire stack |
726
+ | `snapIndex` | Animated snap point index (-1 if no snap points) |
727
+ | `focused` | Whether this screen is the topmost in the stack |
728
+ | `current` | Current screen state |
729
+ | `previous` | Previous screen state |
730
+ | `next` | Next screen state |
731
+ | `active` | Screen driving the transition |
732
+ | `inactive` | Screen NOT driving the transition |
733
+ | `layouts.screen` | Screen dimensions |
734
+ | `layouts.content` | Measured content dimensions when available (`"auto"` snap points) |
735
+ | `insets` | Safe area insets |
736
+ | `bounds` | Shared element bounds function |
737
+
738
+ ### Screen State Properties
739
+
740
+ Each screen state (`current`, `previous`, `next`, `active`, `inactive`) contains:
741
+
742
+ | Property | Description |
743
+ | ----------- | ---------------------------------------- |
744
+ | `progress` | Animation progress (0 or 1) |
745
+ | `closing` | Whether closing (0 or 1) |
746
+ | `entering` | Whether entering (0 or 1) |
747
+ | `animating` | Whether animating (0 or 1) |
748
+ | `gesture` | Gesture values (x, y, normalized values) |
749
+ | `meta` | Custom metadata from options |
750
+
751
+ ### Using `meta` for Conditional Logic
752
+
753
+ Pass custom data between screens:
754
+
755
+ ```tsx
756
+ // Screen A
757
+ options={{ meta: { hideTabBar: true } }}
758
+
759
+ // Screen B reads it
760
+ screenStyleInterpolator: (props) => {
761
+ "worklet";
762
+ const hideTabBar = props.inactive?.meta?.hideTabBar;
763
+ // ...
764
+ };
765
+ ```
766
+
767
+ ### Animate Individual Elements
768
+
769
+ Use `styleId` to target specific elements:
770
+
771
+ ```tsx
772
+ // In options
773
+ screenStyleInterpolator: ({ progress }) => {
774
+ "worklet";
775
+ return {
776
+ "hero-image": {
777
+ opacity: interpolate(progress, [0, 1], [0, 1]),
778
+ },
779
+ };
780
+ };
781
+
782
+ // In component
783
+ <Transition.View styleId="hero-image">
784
+ <Image source={...} />
785
+ </Transition.View>
786
+ ```
787
+
788
+ ---
789
+
790
+ ## Stack Types
791
+
792
+ All three stacks share the same animation API. Choose based on your needs:
793
+
794
+ | Stack | Best For |
795
+ | ------------------- | --------------------------------------------------------- |
796
+ | **Blank Stack** | Most apps. Full control, all features. |
797
+ | **Native Stack** | When you need native screen primitives. |
798
+ | **Component Stack** | Embedded flows, isolated from React Navigation. *(Experimental)* |
799
+
800
+ ### Blank Stack
801
+
802
+ The default choice. Uses `react-native-screens` for native screen containers, with animations powered by Reanimated worklets running on the UI thread (not the JS thread).
803
+
804
+ ```tsx
805
+ import { createBlankStackNavigator } from "react-native-screen-transitions/blank-stack";
806
+ ```
807
+
808
+ ### Native Stack
809
+
810
+ Extends `@react-navigation/native-stack`. Requires `enableTransitions: true`.
811
+
812
+ ```tsx
813
+ import { createNativeStackNavigator } from "react-native-screen-transitions/native-stack";
814
+
815
+ <Stack.Screen
816
+ name="Detail"
817
+ options={{
818
+ enableTransitions: true,
819
+ ...Transition.Presets.SlideFromBottom(),
820
+ }}
821
+ />
822
+ ```
823
+
824
+ ### Component Stack (Experimental)
825
+
826
+ > **Note:** This API is experimental and may change based on community feedback.
827
+
828
+ Standalone navigator, not connected to React Navigation. Ideal for embedded flows.
829
+
830
+ ```tsx
831
+ import { createComponentStackNavigator } from "react-native-screen-transitions/component-stack";
832
+
833
+ const Stack = createComponentStackNavigator();
834
+
835
+ <Stack.Navigator initialRouteName="step1">
836
+ <Stack.Screen name="step1" component={Step1} />
837
+ <Stack.Screen name="step2" component={Step2} />
838
+ </Stack.Navigator>
839
+ ```
840
+
841
+ ---
842
+
843
+ ## Caveats & Trade-offs
844
+
845
+ ### Native Stack
846
+
847
+ The Native Stack uses transparent modal presentation to intercept transitions. This has trade-offs:
848
+
849
+ - **Delayed touch events** – Exiting screens may have briefly delayed touch response
850
+ - **beforeRemove listeners** – Relies on navigation lifecycle events
851
+ - **Rapid navigation** – Some edge cases with very fast navigation sequences
852
+
853
+ For most apps, Blank Stack avoids these issues entirely.
854
+
855
+ ### Component Stack (Experimental)
856
+
857
+ - **No deep linking** – Routes aren't part of your URL structure
858
+ - **Isolated state** – Doesn't affect parent navigation
859
+ - **Touch pass-through** – Uses `pointerEvents="box-none"` by default
860
+
861
+ ---
862
+
863
+ ## Experimental Features
864
+
865
+ ### High Refresh Rate
866
+
867
+ Force maximum refresh rate during transitions (for 90Hz/120Hz displays):
868
+
869
+ ```tsx
870
+ options={{
871
+ experimental_enableHighRefreshRate: true,
872
+ }}
873
+ ```
874
+
875
+ ### Animate On Initial Mount
876
+
877
+ Animate the first screen in a navigator instead of snapping it directly to the settled state:
878
+
879
+ ```tsx
880
+ options={{
881
+ experimental_animateOnInitialMount: true,
882
+ }}
883
+ ```
884
+
885
+ ---
886
+
887
+ ## Masked View Setup
888
+
889
+ Required for `SharedIGImage` and `SharedAppleMusic` presets. The masked view creates the "reveal" effect where content expands from the shared element.
890
+
891
+ > **Note**: Requires native code. Will not work in Expo Go.
892
+
893
+ ### Installation
894
+
895
+ ```bash
896
+ # Expo
897
+ npx expo install @react-native-masked-view/masked-view
898
+
899
+ # Bare React Native
900
+ npm install @react-native-masked-view/masked-view
901
+ cd ios && pod install
902
+ ```
903
+
904
+ ### Full Example
905
+
906
+ **1. Source Screen** – Tag pressable elements:
907
+
908
+ ```tsx
909
+ // app/index.tsx
910
+ import { router } from "expo-router";
911
+ import { View } from "react-native";
912
+ import Transition from "react-native-screen-transitions";
913
+
914
+ export default function HomeScreen() {
915
+ return (
916
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
917
+ <Transition.Pressable
918
+ sharedBoundTag="album-art"
919
+ style={{
920
+ width: 200,
921
+ height: 200,
922
+ backgroundColor: "#1DB954",
923
+ borderRadius: 12,
924
+ }}
925
+ onPress={() => {
926
+ router.push({
927
+ pathname: "/details",
928
+ params: { sharedBoundTag: "album-art" },
929
+ });
930
+ }}
931
+ />
932
+ </View>
933
+ );
934
+ }
935
+ ```
936
+
937
+ **2. Destination Screen** – Wrap with MaskedView and match the tag:
938
+
939
+ ```tsx
940
+ // app/details.tsx
941
+ import { useLocalSearchParams } from "expo-router";
942
+ import Transition from "react-native-screen-transitions";
943
+
944
+ export default function DetailsScreen() {
945
+ const { sharedBoundTag } = useLocalSearchParams<{ sharedBoundTag: string }>();
946
+
947
+ return (
948
+ <Transition.MaskedView style={{ flex: 1, backgroundColor: "#121212" }}>
949
+ <Transition.View
950
+ sharedBoundTag={sharedBoundTag}
951
+ style={{
952
+ backgroundColor: "#1DB954",
953
+ width: 400,
954
+ height: 400,
955
+ alignSelf: "center",
956
+ borderRadius: 12,
957
+ }}
958
+ />
959
+ {/* Additional screen content */}
960
+ </Transition.MaskedView>
961
+ );
962
+ }
963
+ ```
964
+
965
+ **3. Layout** – Apply the preset with dynamic tag:
966
+
967
+ ```tsx
968
+ // app/_layout.tsx
969
+ import Transition from "react-native-screen-transitions";
970
+ import { Stack } from "./stack";
971
+
972
+ export default function RootLayout() {
973
+ return (
974
+ <Stack>
975
+ <Stack.Screen name="index" />
976
+ <Stack.Screen
977
+ name="details"
978
+ options={({ route }) => ({
979
+ ...Transition.Presets.SharedAppleMusic({
980
+ sharedBoundTag: route.params?.sharedBoundTag ?? "",
981
+ }),
982
+ })}
983
+ />
984
+ </Stack>
985
+ );
986
+ }
987
+ ```
988
+
989
+ ### How It Works
990
+
991
+ 1. `Transition.Pressable` measures its bounds on press and stores them with the tag
992
+ 2. `Transition.View` on the destination registers as the target for that tag
993
+ 3. `Transition.MaskedView` clips content to the animating shared element bounds
994
+ 4. The preset interpolates position, size, and mask for a seamless expand/collapse effect
995
+
996
+ ---
997
+
998
+ ## Support
999
+
1000
+ This package is developed in my spare time.
1001
+
1002
+ If you'd like to fuel the next release, [buy me a coffee](https://buymeacoffee.com/trpfsu)
1003
+
1004
+ ## License
45
1005
 
46
- - Docs site: `https://eds2002.github.io/react-native-screen-transitions/`
47
- - Stable docs line: `3.x`
48
- - Unreleased docs line: `Next`
49
- - Repository: `https://github.com/eds2002/react-native-screen-transitions`
1006
+ MIT