react-native-screen-transitions 3.0.0-rc.5 → 3.0.0

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 (96) hide show
  1. package/README.md +228 -96
  2. package/lib/commonjs/blank-stack/components/overlay.js +1 -1
  3. package/lib/commonjs/blank-stack/components/overlay.js.map +1 -1
  4. package/lib/commonjs/shared/components/create-transition-aware-component.js +2 -0
  5. package/lib/commonjs/shared/components/create-transition-aware-component.js.map +1 -1
  6. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +8 -4
  7. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  8. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +29 -5
  9. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  10. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture.js +26 -0
  11. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture.js.map +1 -0
  12. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +32 -60
  13. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  14. package/lib/commonjs/shared/index.js +7 -0
  15. package/lib/commonjs/shared/index.js.map +1 -1
  16. package/lib/commonjs/shared/providers/gestures.provider.js +8 -18
  17. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  18. package/lib/commonjs/shared/utils/bounds/helpers/interpolate-style.js +30 -0
  19. package/lib/commonjs/shared/utils/bounds/helpers/interpolate-style.js.map +1 -0
  20. package/lib/commonjs/shared/utils/bounds/index.js +29 -1
  21. package/lib/commonjs/shared/utils/bounds/index.js.map +1 -1
  22. package/lib/commonjs/shared/utils/create-provider.js +16 -0
  23. package/lib/commonjs/shared/utils/create-provider.js.map +1 -1
  24. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js +4 -0
  25. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  26. package/lib/module/blank-stack/components/overlay.js +1 -1
  27. package/lib/module/blank-stack/components/overlay.js.map +1 -1
  28. package/lib/module/shared/components/create-transition-aware-component.js +2 -0
  29. package/lib/module/shared/components/create-transition-aware-component.js.map +1 -1
  30. package/lib/module/shared/hooks/animation/use-screen-animation.js +8 -4
  31. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  32. package/lib/module/shared/hooks/gestures/use-build-gestures.js +30 -6
  33. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  34. package/lib/module/shared/hooks/gestures/use-screen-gesture.js +22 -0
  35. package/lib/module/shared/hooks/gestures/use-screen-gesture.js.map +1 -0
  36. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +32 -60
  37. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  38. package/lib/module/shared/index.js +1 -0
  39. package/lib/module/shared/index.js.map +1 -1
  40. package/lib/module/shared/providers/gestures.provider.js +9 -19
  41. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  42. package/lib/module/shared/utils/bounds/helpers/interpolate-style.js +26 -0
  43. package/lib/module/shared/utils/bounds/helpers/interpolate-style.js.map +1 -0
  44. package/lib/module/shared/utils/bounds/index.js +29 -1
  45. package/lib/module/shared/utils/bounds/index.js.map +1 -1
  46. package/lib/module/shared/utils/create-provider.js +17 -1
  47. package/lib/module/shared/utils/create-provider.js.map +1 -1
  48. package/lib/module/shared/utils/gesture/check-gesture-activation.js +4 -4
  49. package/lib/module/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  50. package/lib/typescript/blank-stack/types.d.ts +2 -14
  51. package/lib/typescript/blank-stack/types.d.ts.map +1 -1
  52. package/lib/typescript/shared/components/create-transition-aware-component.d.ts +1 -0
  53. package/lib/typescript/shared/components/create-transition-aware-component.d.ts.map +1 -1
  54. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  55. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +1 -0
  56. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  57. package/lib/typescript/shared/hooks/gestures/use-screen-gesture.d.ts +15 -0
  58. package/lib/typescript/shared/hooks/gestures/use-screen-gesture.d.ts.map +1 -0
  59. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts +1 -0
  60. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
  61. package/lib/typescript/shared/index.d.ts +4 -2
  62. package/lib/typescript/shared/index.d.ts.map +1 -1
  63. package/lib/typescript/shared/providers/gestures.provider.d.ts +2 -6
  64. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  65. package/lib/typescript/shared/types/animation.types.d.ts +44 -0
  66. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  67. package/lib/typescript/shared/types/bounds.types.d.ts +6 -0
  68. package/lib/typescript/shared/types/bounds.types.d.ts.map +1 -1
  69. package/lib/typescript/shared/types/core.types.d.ts +7 -0
  70. package/lib/typescript/shared/types/core.types.d.ts.map +1 -1
  71. package/lib/typescript/shared/utils/bounds/helpers/interpolate-style.d.ts +17 -0
  72. package/lib/typescript/shared/utils/bounds/helpers/interpolate-style.d.ts.map +1 -0
  73. package/lib/typescript/shared/utils/bounds/index.d.ts.map +1 -1
  74. package/lib/typescript/shared/utils/create-provider.d.ts +5 -1
  75. package/lib/typescript/shared/utils/create-provider.d.ts.map +1 -1
  76. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts +49 -1
  77. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts.map +1 -1
  78. package/package.json +1 -1
  79. package/src/blank-stack/components/overlay.tsx +1 -1
  80. package/src/blank-stack/types.ts +2 -15
  81. package/src/shared/__tests__/derivations.test.ts +155 -0
  82. package/src/shared/__tests__/gesture-activation.test.ts +251 -0
  83. package/src/shared/components/create-transition-aware-component.tsx +2 -1
  84. package/src/shared/hooks/animation/use-screen-animation.tsx +8 -2
  85. package/src/shared/hooks/gestures/use-build-gestures.tsx +35 -8
  86. package/src/shared/hooks/gestures/use-screen-gesture.ts +19 -0
  87. package/src/shared/hooks/gestures/use-scroll-registry.tsx +39 -59
  88. package/src/shared/index.ts +2 -0
  89. package/src/shared/providers/gestures.provider.tsx +15 -27
  90. package/src/shared/types/animation.types.ts +49 -0
  91. package/src/shared/types/bounds.types.ts +11 -0
  92. package/src/shared/types/core.types.ts +8 -0
  93. package/src/shared/utils/bounds/helpers/interpolate-style.ts +38 -0
  94. package/src/shared/utils/bounds/index.ts +31 -1
  95. package/src/shared/utils/create-provider.tsx +31 -1
  96. package/src/shared/utils/gesture/check-gesture-activation.ts +4 -4
@@ -81,13 +81,9 @@ export type BlankStackOverlayProps = {
81
81
  routes: Route<string>[];
82
82
 
83
83
  /**
84
- * Options passed to the overlay component.
84
+ * Custom metadata from the focused screen's options.
85
85
  */
86
- overlayOptions?: {
87
- title?: string;
88
- subtitle?: string;
89
- [key: string]: unknown;
90
- };
86
+ meta?: Record<string, unknown>;
91
87
 
92
88
  /**
93
89
  * Navigation prop for the overlay.
@@ -134,15 +130,6 @@ export type BlankStackNavigationOptions = BlankStackScreenTransitionConfig & {
134
130
  */
135
131
  overlayShown?: boolean;
136
132
 
137
- /**
138
- * Options passed to the overlay component.
139
- */
140
- overlayOptions?: {
141
- title?: string;
142
- subtitle?: string;
143
- [key: string]: unknown;
144
- };
145
-
146
133
  /**
147
134
  * Whether inactive screens should be suspended from re-rendering. Defaults to `false`.
148
135
  * Defaults to `true` when `enableFreeze()` is run at the top of the application.
@@ -0,0 +1,155 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { ScreenTransitionState } from "../types/animation.types";
3
+ import { derivations } from "../utils/animation/derivations";
4
+
5
+ const createMockState = (
6
+ overrides: Partial<ScreenTransitionState> = {},
7
+ ): ScreenTransitionState => ({
8
+ progress: 1,
9
+ closing: 0,
10
+ animating: 0,
11
+ gesture: {
12
+ isDragging: 0,
13
+ x: 0,
14
+ y: 0,
15
+ normalizedX: 0,
16
+ normalizedY: 0,
17
+ isDismissing: 0,
18
+ direction: null,
19
+ },
20
+ route: { key: "test-route", name: "TestScreen" } as any,
21
+ ...overrides,
22
+ });
23
+
24
+ describe("derivations", () => {
25
+ describe("progress", () => {
26
+ it("returns current progress when no next screen", () => {
27
+ const result = derivations({
28
+ current: createMockState({ progress: 0.5 }),
29
+ });
30
+ expect(result.progress).toBe(0.5);
31
+ });
32
+
33
+ it("combines current + next progress (0-2 range)", () => {
34
+ const result = derivations({
35
+ current: createMockState({ progress: 1 }),
36
+ next: createMockState({ progress: 0.5 }),
37
+ });
38
+ expect(result.progress).toBe(1.5);
39
+ });
40
+
41
+ it("returns 2 when both screens fully transitioned", () => {
42
+ const result = derivations({
43
+ current: createMockState({ progress: 1 }),
44
+ next: createMockState({ progress: 1 }),
45
+ });
46
+ expect(result.progress).toBe(2);
47
+ });
48
+ });
49
+
50
+ describe("focused", () => {
51
+ it("returns true when no next screen", () => {
52
+ const result = derivations({
53
+ current: createMockState(),
54
+ });
55
+ expect(result.focused).toBe(true);
56
+ });
57
+
58
+ it("returns false when next screen exists", () => {
59
+ const result = derivations({
60
+ current: createMockState(),
61
+ next: createMockState(),
62
+ });
63
+ expect(result.focused).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe("active", () => {
68
+ it("returns current when focused (no next)", () => {
69
+ const current = createMockState({ progress: 0.3 });
70
+ const result = derivations({ current });
71
+ expect(result.active).toBe(current);
72
+ });
73
+
74
+ it("returns next when not focused", () => {
75
+ const current = createMockState({ progress: 1 });
76
+ const next = createMockState({ progress: 0.5 });
77
+ const result = derivations({ current, next });
78
+ expect(result.active).toBe(next);
79
+ });
80
+ });
81
+
82
+ describe("isActiveTransitioning", () => {
83
+ it("returns true when active screen is dragging", () => {
84
+ const result = derivations({
85
+ current: createMockState({
86
+ gesture: {
87
+ isDragging: 1,
88
+ x: 0,
89
+ y: 0,
90
+ normalizedX: 0,
91
+ normalizedY: 0,
92
+ isDismissing: 0,
93
+ direction: null,
94
+ },
95
+ }),
96
+ });
97
+ expect(result.isActiveTransitioning).toBe(true);
98
+ });
99
+
100
+ it("returns true when active screen is animating", () => {
101
+ const result = derivations({
102
+ current: createMockState({ animating: 1 }),
103
+ });
104
+ expect(result.isActiveTransitioning).toBe(true);
105
+ });
106
+
107
+ it("returns false when not dragging or animating", () => {
108
+ const result = derivations({
109
+ current: createMockState({ animating: 0 }),
110
+ });
111
+ expect(result.isActiveTransitioning).toBe(false);
112
+ });
113
+ });
114
+
115
+ describe("isDismissing", () => {
116
+ it("returns true when gesture isDismissing", () => {
117
+ const result = derivations({
118
+ current: createMockState({
119
+ gesture: {
120
+ isDragging: 0,
121
+ x: 0,
122
+ y: 0,
123
+ normalizedX: 0,
124
+ normalizedY: 0,
125
+ isDismissing: 1,
126
+ direction: null,
127
+ },
128
+ }),
129
+ });
130
+ expect(result.isDismissing).toBe(true);
131
+ });
132
+
133
+ it("returns true when closing flag is set", () => {
134
+ const result = derivations({
135
+ current: createMockState({ closing: 1 }),
136
+ });
137
+ expect(result.isDismissing).toBe(true);
138
+ });
139
+
140
+ it("returns false when not dismissing or closing", () => {
141
+ const result = derivations({
142
+ current: createMockState(),
143
+ });
144
+ expect(result.isDismissing).toBe(false);
145
+ });
146
+
147
+ it("checks active screen (next) for dismissing state", () => {
148
+ const result = derivations({
149
+ current: createMockState(),
150
+ next: createMockState({ closing: 1 }),
151
+ });
152
+ expect(result.isDismissing).toBe(true);
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,251 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ normalizeSides,
4
+ computeEdgeConstraints,
5
+ calculateSwipeDirs,
6
+ shouldActivateOrFail,
7
+ } from "../utils/gesture/check-gesture-activation";
8
+
9
+ describe("normalizeSides", () => {
10
+ it("returns all sides as 'screen' when no area provided", () => {
11
+ const result = normalizeSides();
12
+ expect(result).toEqual({
13
+ left: "screen",
14
+ right: "screen",
15
+ top: "screen",
16
+ bottom: "screen",
17
+ });
18
+ });
19
+
20
+ it("normalizes string input to all sides", () => {
21
+ const result = normalizeSides("edge");
22
+ expect(result).toEqual({
23
+ left: "edge",
24
+ right: "edge",
25
+ top: "edge",
26
+ bottom: "edge",
27
+ });
28
+ });
29
+
30
+ it("handles per-side object input", () => {
31
+ const result = normalizeSides({
32
+ left: "edge",
33
+ right: "screen",
34
+ top: "edge",
35
+ });
36
+ expect(result).toEqual({
37
+ left: "edge",
38
+ right: "screen",
39
+ top: "edge",
40
+ bottom: "screen", // defaults to screen
41
+ });
42
+ });
43
+
44
+ it("defaults missing sides to screen", () => {
45
+ const result = normalizeSides({ left: "edge" });
46
+ expect(result.left).toBe("edge");
47
+ expect(result.right).toBe("screen");
48
+ expect(result.top).toBe("screen");
49
+ expect(result.bottom).toBe("screen");
50
+ });
51
+ });
52
+
53
+ describe("computeEdgeConstraints", () => {
54
+ const dimensions = { width: 375, height: 812 };
55
+ const allScreen = { left: "screen", right: "screen", top: "screen", bottom: "screen" } as const;
56
+ const allEdge = { left: "edge", right: "edge", top: "edge", bottom: "edge" } as const;
57
+
58
+ it("allows all directions when all sides are 'screen'", () => {
59
+ const result = computeEdgeConstraints({ x: 200, y: 400 }, dimensions, allScreen);
60
+ expect(result.horizontalRight).toBe(true);
61
+ expect(result.horizontalLeft).toBe(true);
62
+ expect(result.verticalDown).toBe(true);
63
+ expect(result.verticalUp).toBe(true);
64
+ });
65
+
66
+ it("restricts to left edge for horizontal-right when edge mode", () => {
67
+ // Touch at x=200 (center) should NOT allow right swipe with edge activation
68
+ const center = computeEdgeConstraints({ x: 200, y: 400 }, dimensions, allEdge);
69
+ expect(center.horizontalRight).toBe(false);
70
+
71
+ // Touch at x=30 (within 50px edge) should allow right swipe
72
+ const edge = computeEdgeConstraints({ x: 30, y: 400 }, dimensions, allEdge);
73
+ expect(edge.horizontalRight).toBe(true);
74
+ });
75
+
76
+ it("restricts to right edge for horizontal-left when edge mode", () => {
77
+ // Touch at x=200 (center) should NOT allow left swipe
78
+ const center = computeEdgeConstraints({ x: 200, y: 400 }, dimensions, allEdge);
79
+ expect(center.horizontalLeft).toBe(false);
80
+
81
+ // Touch at x=350 (within 50px of right edge) should allow left swipe
82
+ const edge = computeEdgeConstraints({ x: 350, y: 400 }, dimensions, allEdge);
83
+ expect(edge.horizontalLeft).toBe(true);
84
+ });
85
+
86
+ it("restricts to top edge for vertical-down when edge mode", () => {
87
+ // Touch at y=400 (center) should NOT allow down swipe
88
+ const center = computeEdgeConstraints({ x: 200, y: 400 }, dimensions, allEdge);
89
+ expect(center.verticalDown).toBe(false);
90
+
91
+ // Touch at y=100 (within 135px edge) should allow down swipe
92
+ const edge = computeEdgeConstraints({ x: 200, y: 100 }, dimensions, allEdge);
93
+ expect(edge.verticalDown).toBe(true);
94
+ });
95
+
96
+ it("uses custom responseDistance", () => {
97
+ // With custom distance of 100px
98
+ const result = computeEdgeConstraints({ x: 80, y: 400 }, dimensions, allEdge, 100);
99
+ expect(result.horizontalRight).toBe(true); // 80 < 100
100
+ });
101
+ });
102
+
103
+ describe("calculateSwipeDirs", () => {
104
+ it("detects horizontal swipe right", () => {
105
+ const result = calculateSwipeDirs(50, 10);
106
+ expect(result.isHorizontalSwipe).toBe(true);
107
+ expect(result.isVerticalSwipe).toBe(false);
108
+ expect(result.isSwipingRight).toBe(true);
109
+ expect(result.isSwipingLeft).toBe(false);
110
+ });
111
+
112
+ it("detects horizontal swipe left", () => {
113
+ const result = calculateSwipeDirs(-50, 10);
114
+ expect(result.isHorizontalSwipe).toBe(true);
115
+ expect(result.isSwipingLeft).toBe(true);
116
+ expect(result.isSwipingRight).toBe(false);
117
+ });
118
+
119
+ it("detects vertical swipe down", () => {
120
+ const result = calculateSwipeDirs(10, 50);
121
+ expect(result.isVerticalSwipe).toBe(true);
122
+ expect(result.isHorizontalSwipe).toBe(false);
123
+ expect(result.isSwipingDown).toBe(true);
124
+ expect(result.isSwipingUp).toBe(false);
125
+ });
126
+
127
+ it("detects vertical swipe up", () => {
128
+ const result = calculateSwipeDirs(10, -50);
129
+ expect(result.isVerticalSwipe).toBe(true);
130
+ expect(result.isSwipingUp).toBe(true);
131
+ expect(result.isSwipingDown).toBe(false);
132
+ });
133
+
134
+ it("horizontal wins when equal deltas", () => {
135
+ // When deltaX === deltaY, neither wins (both false)
136
+ const result = calculateSwipeDirs(50, 50);
137
+ expect(result.isHorizontalSwipe).toBe(false);
138
+ expect(result.isVerticalSwipe).toBe(false);
139
+ });
140
+ });
141
+
142
+ describe("shouldActivateOrFail", () => {
143
+ const baseParams = {
144
+ deltaX: 0,
145
+ deltaY: 0,
146
+ hasHorizontal: true,
147
+ hasVertical: true,
148
+ isHorizontalSwipe: false,
149
+ isVerticalSwipe: false,
150
+ allowedRight: true,
151
+ allowedLeft: true,
152
+ allowedUp: true,
153
+ allowedDown: true,
154
+ horizontalGateRight: true,
155
+ horizontalGateLeft: true,
156
+ verticalGateUp: true,
157
+ verticalGateDown: true,
158
+ isSwipingRight: false,
159
+ isSwipingLeft: false,
160
+ isSwipingUp: false,
161
+ isSwipingDown: false,
162
+ };
163
+
164
+ it("activates on valid horizontal right swipe", () => {
165
+ const result = shouldActivateOrFail({
166
+ ...baseParams,
167
+ deltaX: 15, // above threshold (10)
168
+ deltaY: 5, // within tolerance (15)
169
+ isHorizontalSwipe: true,
170
+ isSwipingRight: true,
171
+ });
172
+ expect(result.shouldActivate).toBe(true);
173
+ expect(result.shouldFail).toBe(false);
174
+ });
175
+
176
+ it("activates on valid vertical down swipe", () => {
177
+ const result = shouldActivateOrFail({
178
+ ...baseParams,
179
+ deltaX: 5, // within tolerance (20)
180
+ deltaY: 15, // above threshold (10)
181
+ isVerticalSwipe: true,
182
+ isSwipingDown: true,
183
+ });
184
+ expect(result.shouldActivate).toBe(true);
185
+ expect(result.shouldFail).toBe(false);
186
+ });
187
+
188
+ it("fails when swiping in disallowed direction", () => {
189
+ const result = shouldActivateOrFail({
190
+ ...baseParams,
191
+ deltaX: 15,
192
+ deltaY: 5,
193
+ isHorizontalSwipe: true,
194
+ isSwipingRight: true,
195
+ allowedRight: false, // direction not allowed
196
+ });
197
+ expect(result.shouldActivate).toBe(false);
198
+ expect(result.shouldFail).toBe(true);
199
+ });
200
+
201
+ it("fails when edge gate blocks the swipe", () => {
202
+ const result = shouldActivateOrFail({
203
+ ...baseParams,
204
+ deltaX: 15,
205
+ deltaY: 5,
206
+ isHorizontalSwipe: true,
207
+ isSwipingRight: true,
208
+ horizontalGateRight: false, // edge gate blocks
209
+ });
210
+ expect(result.shouldActivate).toBe(false);
211
+ expect(result.shouldFail).toBe(true);
212
+ });
213
+
214
+ it("fails when vertical deviation exceeds tolerance during horizontal swipe", () => {
215
+ const result = shouldActivateOrFail({
216
+ ...baseParams,
217
+ deltaX: 15,
218
+ deltaY: 20, // exceeds GESTURE_FAIL_TOLERANCE_X (15)
219
+ isHorizontalSwipe: true,
220
+ isSwipingRight: true,
221
+ });
222
+ expect(result.shouldActivate).toBe(false);
223
+ expect(result.shouldFail).toBe(true);
224
+ });
225
+
226
+ it("does not activate when movement is below threshold", () => {
227
+ const result = shouldActivateOrFail({
228
+ ...baseParams,
229
+ deltaX: 5, // below threshold (10)
230
+ deltaY: 2,
231
+ isHorizontalSwipe: true,
232
+ isSwipingRight: true,
233
+ });
234
+ expect(result.shouldActivate).toBe(false);
235
+ expect(result.shouldFail).toBe(false);
236
+ });
237
+
238
+ it("handles bidirectional gesture activation", () => {
239
+ // Both horizontal and vertical allowed, vertical swipe detected
240
+ const result = shouldActivateOrFail({
241
+ ...baseParams,
242
+ hasHorizontal: true,
243
+ hasVertical: true,
244
+ deltaX: 5,
245
+ deltaY: 15,
246
+ isVerticalSwipe: true,
247
+ isSwipingDown: true,
248
+ });
249
+ expect(result.shouldActivate).toBe(true);
250
+ });
251
+ });
@@ -1,3 +1,4 @@
1
+ /** biome-ignore-all lint/style/noNonNullAssertion: <This helper is usually being used inside a transitionable stack> */
1
2
  import type React from "react";
2
3
  import { type ComponentType, forwardRef, memo } from "react";
3
4
  import type { View } from "react-native";
@@ -26,7 +27,7 @@ export function createTransitionAwareComponent<P extends object>(
26
27
  React.ComponentRef<typeof Wrapped>,
27
28
  TransitionAwareProps<P>
28
29
  >((props: Any, ref) => {
29
- const { nativeGesture } = useGestureContext();
30
+ const { nativeGesture } = useGestureContext()!;
30
31
  const { scrollHandler, onContentSizeChange, onLayout } = useScrollRegistry({
31
32
  onScroll: props.onScroll,
32
33
  onContentSizeChange: props.onContentSizeChange,
@@ -28,11 +28,13 @@ type BuiltState = {
28
28
  animating: SharedValue<number>;
29
29
  gesture: GestureStoreMap;
30
30
  route: RouteProp<ParamListBase>;
31
+ meta?: Record<string, unknown>;
31
32
  unwrapped: ScreenTransitionState;
32
33
  };
33
34
 
34
35
  const createScreenTransitionState = (
35
36
  route: RouteProp<ParamListBase>,
37
+ meta?: Record<string, unknown>,
36
38
  ): ScreenTransitionState => ({
37
39
  progress: 0,
38
40
  closing: 0,
@@ -47,6 +49,7 @@ const createScreenTransitionState = (
47
49
  direction: null,
48
50
  },
49
51
  route,
52
+ meta,
50
53
  });
51
54
 
52
55
  const unwrapInto = (s: BuiltState): ScreenTransitionState => {
@@ -62,6 +65,7 @@ const unwrapInto = (s: BuiltState): ScreenTransitionState => {
62
65
  out.gesture.isDismissing = s.gesture.isDismissing.value;
63
66
  out.gesture.isDragging = s.gesture.isDragging.value;
64
67
  out.gesture.direction = s.gesture.direction.value;
68
+ out.meta = s.meta;
65
69
 
66
70
  return out;
67
71
  };
@@ -70,6 +74,7 @@ const useBuildScreenTransitionState = (
70
74
  descriptor: TransitionDescriptor | undefined,
71
75
  ): BuiltState | undefined => {
72
76
  const key = descriptor?.route.key;
77
+ const meta = descriptor?.options?.meta;
73
78
 
74
79
  return useMemo(() => {
75
80
  if (!key) return undefined;
@@ -80,9 +85,10 @@ const useBuildScreenTransitionState = (
80
85
  animating: AnimationStore.getAnimation(key, "animating"),
81
86
  gesture: GestureStore.getRouteGestures(key),
82
87
  route: descriptor.route,
83
- unwrapped: createScreenTransitionState(descriptor.route),
88
+ meta,
89
+ unwrapped: createScreenTransitionState(descriptor.route, meta),
84
90
  };
85
- }, [key, descriptor?.route]);
91
+ }, [key, descriptor?.route, meta]);
86
92
  };
87
93
 
88
94
  const hasTransitionsEnabled = (
@@ -1,5 +1,5 @@
1
1
  import { StackActions } from "@react-navigation/native";
2
- import { useCallback, useMemo } from "react";
2
+ import { useCallback, useMemo, useRef } from "react";
3
3
  import { useWindowDimensions } from "react-native";
4
4
  import {
5
5
  Gesture,
@@ -48,6 +48,7 @@ export const useBuildGestures = ({
48
48
  ancestorContext,
49
49
  }: BuildGesturesHookProps): {
50
50
  panGesture: GestureType;
51
+ panGestureRef: React.MutableRefObject<GestureType | undefined>;
51
52
  nativeGesture: GestureType;
52
53
  gestureAnimationValues: GestureStoreMap;
53
54
  } => {
@@ -63,6 +64,9 @@ export const useBuildGestures = ({
63
64
  GestureOffsetState.PENDING,
64
65
  );
65
66
 
67
+ // Ref for external gesture coordination (e.g., swipeable lists)
68
+ const panGestureRef = useRef<GestureType | undefined>(undefined);
69
+
66
70
  const gestureAnimationValues = GestureStore.getRouteGestures(
67
71
  current.route.key,
68
72
  );
@@ -325,26 +329,49 @@ export const useBuildGestures = ({
325
329
  },
326
330
  );
327
331
 
332
+ // Memoize gestures to keep stable references - critical for RNGH
333
+ // Child gestures reference ancestor's pan via requireExternalGestureToFail,
334
+ // so the pan gesture MUST be stable or children will reference stale objects
328
335
  return useMemo(() => {
329
- const nativeGesture = Gesture.Native();
330
-
331
336
  const panGesture = Gesture.Pan()
337
+ .withRef(panGestureRef)
332
338
  .enabled(gestureEnabled)
333
339
  .manualActivation(true)
334
340
  .onTouchesDown(onTouchesDown)
335
341
  .onTouchesMove(onTouchesMove)
336
342
  .onStart(onStart)
337
343
  .onUpdate(onUpdate)
338
- .onEnd(onEnd)
339
- .blocksExternalGesture(nativeGesture);
344
+ .onEnd(onEnd);
345
+
346
+ // Native gesture setup depends on whether this screen has gestures
347
+ let nativeGesture: GestureType;
348
+
349
+ if (gestureEnabled) {
350
+ // This screen has gestures - set up normal pan/native relationship
351
+ nativeGesture = Gesture.Native().requireExternalGestureToFail(panGesture);
352
+ panGesture.blocksExternalGesture(nativeGesture);
353
+ } else {
354
+ // This screen has no gestures
355
+ // Find nearest ancestor with gestureEnabled=true (attached pan)
356
+ let activePanAncestor = ancestorContext;
357
+ while (activePanAncestor && !activePanAncestor.gestureEnabled) {
358
+ activePanAncestor = activePanAncestor.ancestorContext;
359
+ }
340
360
 
341
- // Allow ancestors to block child native gestures
342
- if (ancestorContext?.panGesture && nativeGesture) {
343
- ancestorContext.panGesture.blocksExternalGesture(nativeGesture);
361
+ if (activePanAncestor?.panGesture) {
362
+ // Found an ancestor with enabled pan - wait for it
363
+ nativeGesture = Gesture.Native().requireExternalGestureToFail(
364
+ activePanAncestor.panGesture,
365
+ );
366
+ } else {
367
+ // No ancestor with enabled pan - plain native
368
+ nativeGesture = Gesture.Native();
369
+ }
344
370
  }
345
371
 
346
372
  return {
347
373
  panGesture,
374
+ panGestureRef,
348
375
  nativeGesture,
349
376
  gestureAnimationValues,
350
377
  };
@@ -0,0 +1,19 @@
1
+ import { useGestureContext } from "../../providers/gestures.provider";
2
+
3
+ /**
4
+ * Returns a ref to the screen's navigation pan gesture.
5
+ * Use this to coordinate child gestures with the navigation gesture.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const screenGesture = useScreenGesture();
10
+ *
11
+ * const myPanGesture = Gesture.Pan()
12
+ * .waitFor(screenGesture) // Wait for navigation gesture to fail first
13
+ * .onUpdate(...);
14
+ * ```
15
+ */
16
+ export const useScreenGesture = () => {
17
+ const ctx = useGestureContext();
18
+ return ctx?.panGestureRef ?? null;
19
+ };