react-native-screen-transitions 3.3.0-beta.2 → 3.3.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +95 -31
  2. package/lib/commonjs/shared/animation/snap-to.js +17 -10
  3. package/lib/commonjs/shared/animation/snap-to.js.map +1 -1
  4. package/lib/commonjs/shared/components/create-transition-aware-component.js +20 -18
  5. package/lib/commonjs/shared/components/create-transition-aware-component.js.map +1 -1
  6. package/lib/commonjs/shared/components/screen-container.js +68 -9
  7. package/lib/commonjs/shared/components/screen-container.js.map +1 -1
  8. package/lib/commonjs/shared/constants.js +8 -1
  9. package/lib/commonjs/shared/constants.js.map +1 -1
  10. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +49 -39
  11. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  12. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js +110 -61
  13. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -1
  14. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +67 -70
  15. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  16. package/lib/commonjs/shared/providers/gestures.provider.js +113 -25
  17. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  18. package/lib/commonjs/shared/types/ownership.types.js +71 -0
  19. package/lib/commonjs/shared/types/ownership.types.js.map +1 -0
  20. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js +72 -128
  21. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  22. package/lib/commonjs/shared/utils/gesture/compute-claimed-directions.js +81 -0
  23. package/lib/commonjs/shared/utils/gesture/compute-claimed-directions.js.map +1 -0
  24. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js +1 -1
  25. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js.map +1 -1
  26. package/lib/commonjs/shared/utils/gesture/find-collapse-target.js +48 -0
  27. package/lib/commonjs/shared/utils/gesture/find-collapse-target.js.map +1 -0
  28. package/lib/commonjs/shared/utils/gesture/resolve-ownership.js +87 -0
  29. package/lib/commonjs/shared/utils/gesture/resolve-ownership.js.map +1 -0
  30. package/lib/commonjs/shared/utils/gesture/velocity.js +16 -5
  31. package/lib/commonjs/shared/utils/gesture/velocity.js.map +1 -1
  32. package/lib/module/shared/animation/snap-to.js +16 -10
  33. package/lib/module/shared/animation/snap-to.js.map +1 -1
  34. package/lib/module/shared/components/create-transition-aware-component.js +20 -18
  35. package/lib/module/shared/components/create-transition-aware-component.js.map +1 -1
  36. package/lib/module/shared/components/screen-container.js +68 -10
  37. package/lib/module/shared/components/screen-container.js.map +1 -1
  38. package/lib/module/shared/constants.js +7 -0
  39. package/lib/module/shared/constants.js.map +1 -1
  40. package/lib/module/shared/hooks/gestures/use-build-gestures.js +49 -39
  41. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  42. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js +112 -63
  43. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -1
  44. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +68 -70
  45. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  46. package/lib/module/shared/providers/gestures.provider.js +113 -25
  47. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  48. package/lib/module/shared/types/ownership.types.js +67 -0
  49. package/lib/module/shared/types/ownership.types.js.map +1 -0
  50. package/lib/module/shared/utils/gesture/check-gesture-activation.js +70 -126
  51. package/lib/module/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  52. package/lib/module/shared/utils/gesture/compute-claimed-directions.js +77 -0
  53. package/lib/module/shared/utils/gesture/compute-claimed-directions.js.map +1 -0
  54. package/lib/module/shared/utils/gesture/determine-snap-target.js +1 -1
  55. package/lib/module/shared/utils/gesture/determine-snap-target.js.map +1 -1
  56. package/lib/module/shared/utils/gesture/find-collapse-target.js +44 -0
  57. package/lib/module/shared/utils/gesture/find-collapse-target.js.map +1 -0
  58. package/lib/module/shared/utils/gesture/resolve-ownership.js +83 -0
  59. package/lib/module/shared/utils/gesture/resolve-ownership.js.map +1 -0
  60. package/lib/module/shared/utils/gesture/velocity.js +16 -5
  61. package/lib/module/shared/utils/gesture/velocity.js.map +1 -1
  62. package/lib/typescript/shared/animation/snap-to.d.ts.map +1 -1
  63. package/lib/typescript/shared/components/create-transition-aware-component.d.ts.map +1 -1
  64. package/lib/typescript/shared/components/screen-container.d.ts.map +1 -1
  65. package/lib/typescript/shared/constants.d.ts +6 -0
  66. package/lib/typescript/shared/constants.d.ts.map +1 -1
  67. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +15 -3
  68. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  69. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts +52 -2
  70. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts.map +1 -1
  71. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts +11 -6
  72. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
  73. package/lib/typescript/shared/hooks/use-backdrop-pointer-events.d.ts +1 -1
  74. package/lib/typescript/shared/hooks/use-backdrop-pointer-events.d.ts.map +1 -1
  75. package/lib/typescript/shared/providers/gestures.provider.d.ts +28 -3
  76. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  77. package/lib/typescript/shared/types/ownership.types.d.ts +52 -0
  78. package/lib/typescript/shared/types/ownership.types.d.ts.map +1 -0
  79. package/lib/typescript/shared/types/screen.types.d.ts +22 -1
  80. package/lib/typescript/shared/types/screen.types.d.ts.map +1 -1
  81. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts +23 -19
  82. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts.map +1 -1
  83. package/lib/typescript/shared/utils/gesture/compute-claimed-directions.d.ts +23 -0
  84. package/lib/typescript/shared/utils/gesture/compute-claimed-directions.d.ts.map +1 -0
  85. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts +5 -1
  86. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts.map +1 -1
  87. package/lib/typescript/shared/utils/gesture/find-collapse-target.d.ts +17 -0
  88. package/lib/typescript/shared/utils/gesture/find-collapse-target.d.ts.map +1 -0
  89. package/lib/typescript/shared/utils/gesture/resolve-ownership.d.ts +36 -0
  90. package/lib/typescript/shared/utils/gesture/resolve-ownership.d.ts.map +1 -0
  91. package/lib/typescript/shared/utils/gesture/velocity.d.ts.map +1 -1
  92. package/package.json +121 -120
  93. package/src/shared/animation/snap-to.ts +17 -11
  94. package/src/shared/components/create-transition-aware-component.tsx +28 -25
  95. package/src/shared/components/screen-container.tsx +79 -12
  96. package/src/shared/constants.ts +7 -0
  97. package/src/shared/hooks/gestures/use-build-gestures.tsx +80 -44
  98. package/src/shared/hooks/gestures/use-screen-gesture-handlers.ts +147 -71
  99. package/src/shared/hooks/gestures/use-scroll-registry.tsx +94 -86
  100. package/src/shared/hooks/use-backdrop-pointer-events.ts +1 -1
  101. package/src/shared/providers/gestures.provider.tsx +168 -25
  102. package/src/shared/types/ownership.types.ts +77 -0
  103. package/src/shared/types/screen.types.ts +24 -1
  104. package/src/shared/utils/gesture/check-gesture-activation.ts +82 -116
  105. package/src/shared/utils/gesture/compute-claimed-directions.ts +93 -0
  106. package/src/shared/utils/gesture/determine-snap-target.ts +6 -2
  107. package/src/shared/utils/gesture/find-collapse-target.ts +42 -0
  108. package/src/shared/utils/gesture/resolve-ownership.ts +110 -0
  109. package/src/shared/utils/gesture/velocity.ts +16 -6
  110. package/src/shared/__tests__/bounds.store.test.ts +0 -394
  111. package/src/shared/__tests__/derivations.test.ts +0 -156
  112. package/src/shared/__tests__/determine-dismissal.test.ts +0 -111
  113. package/src/shared/__tests__/determine-snap-target.test.ts +0 -268
  114. package/src/shared/__tests__/geometry.test.ts +0 -130
  115. package/src/shared/__tests__/gesture-activation.test.ts +0 -471
  116. package/src/shared/__tests__/gesture.velocity.test.ts +0 -131
  117. package/src/shared/__tests__/history.store.test.ts +0 -550
  118. package/src/shared/__tests__/sync-routes-with-removed.test.ts +0 -137
  119. package/src/shared/__tests__/validate-snap-points.test.ts +0 -125
@@ -1,41 +1,107 @@
1
- /** biome-ignore-all lint/style/noNonNullAssertion: <Will always consume context from GestureProvider> */
1
+ /**
2
+ * Connects ScrollViews to the gesture ownership system.
3
+ * Finds the gesture owner for the scroll axis and coordinates with their panGesture.
4
+ */
2
5
 
3
6
  import { useMemo } from "react";
4
- import type { GestureResponderEvent, LayoutChangeEvent } from "react-native";
7
+ import type { LayoutChangeEvent } from "react-native";
8
+ import { Gesture, type GestureType } from "react-native-gesture-handler";
9
+ import type { SharedValue } from "react-native-reanimated";
5
10
  import { useAnimatedScrollHandler } from "react-native-reanimated";
6
11
  import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes";
7
- import { useGestureContext } from "../../providers/gestures.provider";
12
+ import {
13
+ type GestureContextType,
14
+ type ScrollConfig,
15
+ useGestureContext,
16
+ } from "../../providers/gestures.provider";
8
17
  import useStableCallback from "../use-stable-callback";
9
18
 
19
+ /** Walks up context tree to find the screen that owns this scroll axis. */
20
+ function findGestureOwnerForAxis(
21
+ context: GestureContextType | null | undefined,
22
+ axis: "vertical" | "horizontal",
23
+ ): {
24
+ scrollConfig: SharedValue<ScrollConfig | null> | null;
25
+ panGesture: GestureType | null;
26
+ } {
27
+ let current = context;
28
+ const startIsolated = context?.isIsolated;
29
+
30
+ while (current) {
31
+ if (startIsolated !== undefined && current.isIsolated !== startIsolated) {
32
+ break;
33
+ }
34
+
35
+ const ownsAxis =
36
+ axis === "vertical"
37
+ ? current.claimedDirections?.vertical ||
38
+ current.claimedDirections?.["vertical-inverted"]
39
+ : current.claimedDirections?.horizontal ||
40
+ current.claimedDirections?.["horizontal-inverted"];
41
+
42
+ if (ownsAxis) {
43
+ return {
44
+ scrollConfig: current.scrollConfig,
45
+ panGesture: current.panGesture,
46
+ };
47
+ }
48
+
49
+ current = current.ancestorContext;
50
+ }
51
+
52
+ return { scrollConfig: null, panGesture: null };
53
+ }
54
+
10
55
  interface ScrollProgressHookProps {
11
56
  onScroll?: (event: ReanimatedScrollEvent) => void;
12
57
  onContentSizeChange?: (width: number, height: number) => void;
13
58
  onLayout?: (event: LayoutChangeEvent) => void;
14
- onTouchStart?: (event: GestureResponderEvent) => void;
15
- onTouchEnd?: (event: GestureResponderEvent) => void;
59
+ direction?: "vertical" | "horizontal";
16
60
  }
17
61
 
62
+ /**
63
+ * Returns scroll handlers and a native gesture for ScrollView coordination.
64
+ * Automatically finds the gesture owner for the scroll axis.
65
+ */
18
66
  export const useScrollRegistry = (props: ScrollProgressHookProps) => {
19
- const context = useGestureContext()!;
20
- const { scrollConfig, ancestorContext } = context;
21
-
22
- const ancestorScrollConfigs = useMemo(() => {
23
- const configs: (typeof scrollConfig)[] = [];
24
- let current = ancestorContext;
25
- while (current) {
26
- if (current.scrollConfig) {
27
- configs.push(current.scrollConfig);
67
+ const context = useGestureContext();
68
+ const scrollDirection = props.direction ?? "vertical";
69
+
70
+ const { scrollConfig, panGesture } = findGestureOwnerForAxis(
71
+ context,
72
+ scrollDirection,
73
+ );
74
+
75
+ const nativeGesture = useMemo(() => {
76
+ if (!panGesture || !scrollConfig) return null;
77
+
78
+ const setIsTouched = () => {
79
+ "worklet";
80
+ if (scrollConfig.value) {
81
+ scrollConfig.value = { ...scrollConfig.value, isTouched: true };
28
82
  }
29
- current = current.ancestorContext;
30
- }
31
- return configs;
32
- }, [ancestorContext]);
83
+ };
84
+
85
+ const clearIsTouched = () => {
86
+ "worklet";
87
+ if (scrollConfig.value) {
88
+ scrollConfig.value = { ...scrollConfig.value, isTouched: false };
89
+ }
90
+ };
91
+
92
+ return Gesture.Native()
93
+ .onTouchesDown(setIsTouched)
94
+ .onTouchesUp(clearIsTouched)
95
+ .onTouchesCancelled(clearIsTouched)
96
+ .requireExternalGestureToFail(panGesture);
97
+ }, [panGesture, scrollConfig]);
33
98
 
34
99
  const scrollHandler = useAnimatedScrollHandler({
35
100
  onScroll: (event) => {
36
101
  props.onScroll?.(event);
102
+ if (!scrollConfig) return;
37
103
 
38
- const updateScrollPosition = (v: any) => {
104
+ const update = (v: any) => {
39
105
  "worklet";
40
106
  if (v === null) {
41
107
  return {
@@ -52,21 +118,16 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
52
118
  v.y = event.contentOffset.y;
53
119
  return v;
54
120
  };
55
-
56
- scrollConfig.modify(updateScrollPosition);
57
-
58
- // Sync to ALL ancestors, not just immediate parent
59
- for (const ancestorConfig of ancestorScrollConfigs) {
60
- ancestorConfig.modify(updateScrollPosition);
61
- }
121
+ scrollConfig.modify(update);
62
122
  },
63
123
  });
64
124
 
65
125
  const onContentSizeChange = useStableCallback(
66
126
  (width: number, height: number) => {
67
127
  props.onContentSizeChange?.(width, height);
128
+ if (!scrollConfig) return;
68
129
 
69
- const updateContentSize = (v: any) => {
130
+ const update = (v: any) => {
70
131
  "worklet";
71
132
  if (v === null) {
72
133
  return {
@@ -83,20 +144,17 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
83
144
  v.contentHeight = height;
84
145
  return v;
85
146
  };
86
-
87
- scrollConfig.modify(updateContentSize);
88
-
89
- for (const ancestorConfig of ancestorScrollConfigs) {
90
- ancestorConfig.modify(updateContentSize);
91
- }
147
+ scrollConfig.modify(update);
92
148
  },
93
149
  );
94
150
 
95
151
  const onLayout = useStableCallback((event: LayoutChangeEvent) => {
96
152
  props.onLayout?.(event);
153
+ if (!scrollConfig) return;
154
+
97
155
  const { width, height } = event.nativeEvent.layout;
98
156
 
99
- const updateLayout = (v: any) => {
157
+ const update = (v: any) => {
100
158
  "worklet";
101
159
  if (v === null) {
102
160
  return {
@@ -113,63 +171,13 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
113
171
  v.layoutWidth = width;
114
172
  return v;
115
173
  };
116
-
117
- scrollConfig.modify(updateLayout);
118
-
119
- for (const ancestorConfig of ancestorScrollConfigs) {
120
- ancestorConfig.modify(updateLayout);
121
- }
122
- });
123
-
124
- const onTouchStart = useStableCallback((event: GestureResponderEvent) => {
125
- props.onTouchStart?.(event);
126
-
127
- const setTouched = (v: any) => {
128
- "worklet";
129
- if (v === null) {
130
- return {
131
- x: 0,
132
- y: 0,
133
- contentHeight: 0,
134
- contentWidth: 0,
135
- layoutHeight: 0,
136
- layoutWidth: 0,
137
- isTouched: true,
138
- };
139
- }
140
- v.isTouched = true;
141
- return v;
142
- };
143
-
144
- scrollConfig.modify(setTouched);
145
-
146
- for (const ancestorConfig of ancestorScrollConfigs) {
147
- ancestorConfig.modify(setTouched);
148
- }
149
- });
150
-
151
- const onTouchEnd = useStableCallback((event: GestureResponderEvent) => {
152
- props.onTouchEnd?.(event);
153
-
154
- const clearTouched = (v: any) => {
155
- "worklet";
156
- if (v === null) return v;
157
- v.isTouched = false;
158
- return v;
159
- };
160
-
161
- scrollConfig.modify(clearTouched);
162
-
163
- for (const ancestorConfig of ancestorScrollConfigs) {
164
- ancestorConfig.modify(clearTouched);
165
- }
174
+ scrollConfig.modify(update);
166
175
  });
167
176
 
168
177
  return {
169
178
  scrollHandler,
170
179
  onContentSizeChange,
171
180
  onLayout,
172
- onTouchStart,
173
- onTouchEnd,
181
+ nativeGesture,
174
182
  };
175
183
  };
@@ -2,7 +2,7 @@ import { useKeys } from "../providers/screen/keys.provider";
2
2
  import { useStackCoreContext } from "../providers/stack/core.provider";
3
3
  import { StackType } from "../types/stack.types";
4
4
 
5
- type BackdropBehavior = "block" | "passthrough" | "dismiss";
5
+ type BackdropBehavior = "block" | "passthrough" | "dismiss" | "collapse";
6
6
 
7
7
  interface BackdropPointerEventsResult {
8
8
  pointerEvents: "box-none" | undefined;
@@ -1,15 +1,29 @@
1
- import { StyleSheet, View } from "react-native";
2
- import {
3
- GestureDetector,
4
- type GestureType,
5
- } from "react-native-gesture-handler";
1
+ /**
2
+ * Gesture System - Core Provider
3
+ *
4
+ * Each screen gets a GestureContext containing:
5
+ * - panGesture: Pan gesture handler for dismiss/snap
6
+ * - scrollConfig: Scroll state for boundary detection
7
+ * - claimedDirections: Which directions this screen handles
8
+ * - childDirectionClaims: Claims registered by descendant screens
9
+ *
10
+ * ScrollView coordination is handled by useScrollRegistry, which finds the
11
+ * gesture owner for the scroll axis and creates appropriate Native gestures.
12
+ */
13
+
14
+ import { useNavigationState } from "@react-navigation/native";
15
+ import { useEffect, useMemo } from "react";
16
+ import type { GestureType } from "react-native-gesture-handler";
6
17
  import type { SharedValue } from "react-native-reanimated";
7
18
  import { useSharedValue } from "react-native-reanimated";
8
19
  import { useBuildGestures } from "../hooks/gestures/use-build-gestures";
9
- import { useBackdropPointerEvents } from "../hooks/use-backdrop-pointer-events";
10
- import type { GestureStoreMap } from "../stores/gesture.store";
20
+ import { GestureStore, type GestureStoreMap } from "../stores/gesture.store";
21
+ import type { ClaimedDirections, Direction } from "../types/ownership.types";
22
+ import { StackType } from "../types/stack.types";
11
23
  import createProvider from "../utils/create-provider";
24
+ import { computeClaimedDirections } from "../utils/gesture/compute-claimed-directions";
12
25
  import { useKeys } from "./screen/keys.provider";
26
+ import { useStackCoreContext } from "./stack/core.provider";
13
27
 
14
28
  export type ScrollConfig = {
15
29
  x: number;
@@ -21,14 +35,118 @@ export type ScrollConfig = {
21
35
  isTouched: boolean;
22
36
  };
23
37
 
38
+ export type DirectionClaim = {
39
+ routeKey: string;
40
+ isDismissing: SharedValue<number>;
41
+ } | null;
42
+
43
+ export type DirectionClaimMap = {
44
+ vertical: DirectionClaim;
45
+ "vertical-inverted": DirectionClaim;
46
+ horizontal: DirectionClaim;
47
+ "horizontal-inverted": DirectionClaim;
48
+ };
49
+
50
+ const NO_CLAIMS: DirectionClaimMap = {
51
+ vertical: null,
52
+ "vertical-inverted": null,
53
+ horizontal: null,
54
+ "horizontal-inverted": null,
55
+ };
56
+
57
+ const DIRECTIONS: Direction[] = [
58
+ "vertical",
59
+ "vertical-inverted",
60
+ "horizontal",
61
+ "horizontal-inverted",
62
+ ];
63
+
64
+ /**
65
+ * Registers direction claims on ancestors that this screen shadows.
66
+ * Only registers claims when this screen is the current (topmost) route
67
+ * in its navigator, preventing unfocused screens from blocking gestures.
68
+ */
69
+ function useRegisterDirectionClaims(
70
+ ancestorContext: GestureContextType | null | undefined,
71
+ claimedDirections: ClaimedDirections,
72
+ routeKey: string,
73
+ isIsolated: boolean,
74
+ isCurrentRoute: boolean,
75
+ ) {
76
+ useEffect(() => {
77
+ // Only register claims when this screen is the current route
78
+ if (!isCurrentRoute || !ancestorContext) {
79
+ return;
80
+ }
81
+
82
+ const gestureValues = GestureStore.getRouteGestures(routeKey);
83
+ const isDismissing = gestureValues.isDismissing;
84
+
85
+ const claimedAncestors: Array<{
86
+ ancestor: GestureContextType;
87
+ directions: Direction[];
88
+ }> = [];
89
+
90
+ let ancestor: GestureContextType | null = ancestorContext;
91
+ while (ancestor) {
92
+ if (ancestor.isIsolated !== isIsolated) break;
93
+
94
+ const shadowedDirections: Direction[] = [];
95
+ for (const dir of DIRECTIONS) {
96
+ if (claimedDirections[dir] && ancestor.claimedDirections?.[dir]) {
97
+ shadowedDirections.push(dir);
98
+ }
99
+ }
100
+
101
+ if (shadowedDirections.length > 0) {
102
+ claimedAncestors.push({ ancestor, directions: shadowedDirections });
103
+ const newClaims = { ...ancestor.childDirectionClaims.value };
104
+ for (const dir of shadowedDirections) {
105
+ newClaims[dir] = { routeKey, isDismissing };
106
+ }
107
+ ancestor.childDirectionClaims.value = newClaims;
108
+ }
109
+
110
+ ancestor = ancestor.ancestorContext;
111
+ }
112
+
113
+ return () => {
114
+ for (const { ancestor, directions } of claimedAncestors) {
115
+ const currentClaims = ancestor.childDirectionClaims.value;
116
+ const newClaims = { ...currentClaims };
117
+ let needsUpdate = false;
118
+
119
+ for (const dir of directions) {
120
+ if (currentClaims[dir]?.routeKey === routeKey) {
121
+ newClaims[dir] = null;
122
+ needsUpdate = true;
123
+ }
124
+ }
125
+
126
+ if (needsUpdate) {
127
+ ancestor.childDirectionClaims.value = newClaims;
128
+ }
129
+ }
130
+ };
131
+ }, [
132
+ ancestorContext,
133
+ claimedDirections,
134
+ routeKey,
135
+ isIsolated,
136
+ isCurrentRoute,
137
+ ]);
138
+ }
139
+
24
140
  export interface GestureContextType {
25
141
  panGesture: GestureType;
26
142
  panGestureRef: React.MutableRefObject<GestureType | undefined>;
27
- nativeGesture: GestureType;
28
143
  scrollConfig: SharedValue<ScrollConfig | null>;
29
144
  gestureAnimationValues: GestureStoreMap;
30
145
  ancestorContext: GestureContextType | null;
31
146
  gestureEnabled: boolean;
147
+ isIsolated: boolean;
148
+ claimedDirections: ClaimedDirections;
149
+ childDirectionClaims: SharedValue<DirectionClaimMap>;
32
150
  }
33
151
 
34
152
  interface ScreenGestureProviderProps {
@@ -43,42 +161,67 @@ export const {
43
161
  GestureContextType
44
162
  >(({ children }) => {
45
163
  const { current } = useKeys();
164
+ const { flags } = useStackCoreContext();
46
165
  const ancestorContext = useGestureContext();
47
- const scrollConfig = useSharedValue<ScrollConfig | null>(null);
48
- const { pointerEvents } = useBackdropPointerEvents();
49
166
 
50
167
  const hasGestures = current.options.gestureEnabled === true;
168
+ const isIsolated = flags.STACK_TYPE === StackType.COMPONENT;
169
+
170
+ const hasSnapPoints =
171
+ Array.isArray(current.options.snapPoints) &&
172
+ current.options.snapPoints.length > 0;
173
+
174
+ const claimedDirections = useMemo(
175
+ () =>
176
+ computeClaimedDirections(
177
+ hasGestures,
178
+ current.options.gestureDirection,
179
+ hasSnapPoints,
180
+ ),
181
+ [hasGestures, current.options.gestureDirection, hasSnapPoints],
182
+ );
183
+
184
+ const routeKey = current.route.key;
51
185
 
52
- const { panGesture, panGestureRef, nativeGesture, gestureAnimationValues } =
186
+ // Check if this screen is the current (topmost) route in its navigator
187
+ const isCurrentRoute = useNavigationState(
188
+ (state) => state.routes[state.index]?.key === routeKey,
189
+ );
190
+
191
+ const scrollConfig = useSharedValue<ScrollConfig | null>(null);
192
+ const childDirectionClaims = useSharedValue<DirectionClaimMap>(NO_CLAIMS);
193
+
194
+ useRegisterDirectionClaims(
195
+ ancestorContext,
196
+ claimedDirections,
197
+ routeKey,
198
+ isIsolated,
199
+ isCurrentRoute,
200
+ );
201
+
202
+ const { panGesture, panGestureRef, gestureAnimationValues } =
53
203
  useBuildGestures({
54
204
  scrollConfig,
55
205
  ancestorContext,
206
+ claimedDirections,
207
+ childDirectionClaims,
208
+ isIsolated,
56
209
  });
57
210
 
58
211
  const value: GestureContextType = {
59
212
  panGesture,
60
213
  panGestureRef,
61
214
  scrollConfig,
62
- nativeGesture,
63
215
  gestureAnimationValues,
64
216
  ancestorContext,
65
217
  gestureEnabled: hasGestures,
218
+ isIsolated,
219
+ claimedDirections,
220
+ childDirectionClaims,
66
221
  };
67
222
 
68
223
  return {
69
224
  value,
70
- children: (
71
- <GestureDetector gesture={panGesture}>
72
- <View style={styles.container} pointerEvents={pointerEvents}>
73
- {children}
74
- </View>
75
- </GestureDetector>
76
- ),
225
+ children,
77
226
  };
78
227
  });
79
-
80
- const styles = StyleSheet.create({
81
- container: {
82
- flex: 1,
83
- },
84
- });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Gesture Ownership System Types
3
+ *
4
+ * Core principles:
5
+ * 1. Gestures dismiss stacks, not screens
6
+ * 2. Ownership is per-direction (4 independent directions)
7
+ * 3. Shadowing: child claiming same direction blocks parent
8
+ * 4. Inheritance: no local claim walks up tree to find owner
9
+ */
10
+
11
+ /**
12
+ * The four independent gesture directions.
13
+ * Each direction is owned independently.
14
+ *
15
+ * Uses the same format as GestureDirection from gesture.types.ts
16
+ * (excluding 'bidirectional' which expands to all four).
17
+ */
18
+ export type Direction =
19
+ | "vertical"
20
+ | "vertical-inverted"
21
+ | "horizontal"
22
+ | "horizontal-inverted";
23
+
24
+ /**
25
+ * All possible directions as an array for iteration.
26
+ */
27
+ export const DIRECTIONS: Direction[] = [
28
+ "vertical",
29
+ "vertical-inverted",
30
+ "horizontal",
31
+ "horizontal-inverted",
32
+ ];
33
+
34
+ /**
35
+ * Map of which directions a screen claims ownership of.
36
+ * A screen claims a direction when:
37
+ * - gestureEnabled is true AND
38
+ * - gestureDirection includes that direction
39
+ *
40
+ * For snap points, both directions on the axis are claimed automatically.
41
+ */
42
+ export type ClaimedDirections = Record<Direction, boolean>;
43
+
44
+ /**
45
+ * Empty claims - used when gestureEnabled is false.
46
+ */
47
+ export const NO_CLAIMS: ClaimedDirections = {
48
+ vertical: false,
49
+ "vertical-inverted": false,
50
+ horizontal: false,
51
+ "horizontal-inverted": false,
52
+ };
53
+
54
+ /**
55
+ * Ownership status for a direction relative to the current screen.
56
+ *
57
+ * - 'self': Current screen owns this direction (should activate)
58
+ * - 'ancestor': An ancestor owns this direction (should fail to bubble up)
59
+ * - 'none': No one owns this direction (should fail, no gesture response)
60
+ */
61
+ export type OwnershipStatus = "self" | "ancestor" | "none";
62
+
63
+ /**
64
+ * Map of ownership status for all four directions.
65
+ * Pre-computed during render for worklet access.
66
+ */
67
+ export type DirectionOwnership = Record<Direction, OwnershipStatus>;
68
+
69
+ /**
70
+ * Empty ownership - used when no gestures are configured anywhere.
71
+ */
72
+ export const NO_OWNERSHIP: DirectionOwnership = {
73
+ vertical: "none",
74
+ "vertical-inverted": "none",
75
+ horizontal: "none",
76
+ "horizontal-inverted": "none",
77
+ };
@@ -77,9 +77,17 @@ export type ScreenTransitionConfig = {
77
77
 
78
78
  /**
79
79
  * How much the gesture's final velocity impacts the dismiss decision.
80
+ * @default 0.3
80
81
  */
81
82
  gestureVelocityImpact?: number;
82
83
 
84
+ /**
85
+ * How much velocity affects snap point targeting. Lower values make snapping
86
+ * feel more deliberate (iOS-like), higher values make it more responsive to flicks.
87
+ * @default 0.1
88
+ */
89
+ snapVelocityImpact?: number;
90
+
83
91
  /**
84
92
  * Distance threshold for gesture recognition throughout the screen.
85
93
  */
@@ -155,14 +163,29 @@ export type ScreenTransitionConfig = {
155
163
  */
156
164
  initialSnapIndex?: number;
157
165
 
166
+ /**
167
+ * Controls whether swiping to expand the sheet works from within a ScrollView.
168
+ *
169
+ * - `true` (Apple Maps style): Swiping up at scroll top expands the sheet
170
+ * - `false` (Instagram style): Expand only works via deadspace (non-scrollable areas)
171
+ *
172
+ * Collapse (swipe down at scroll top) always works regardless of this setting.
173
+ *
174
+ * Only applies to screens with `snapPoints` configured.
175
+ *
176
+ * @default true
177
+ */
178
+ expandViaScrollView?: boolean;
179
+
158
180
  /**
159
181
  * Controls how touches interact with the backdrop area (outside the screen content).
160
182
  *
161
183
  * - `'block'`: Backdrop catches all touches (default for most screens)
162
184
  * - `'passthrough'`: Touches pass through to content behind (default for component stacks)
163
185
  * - `'dismiss'`: Tapping backdrop dismisses the screen
186
+ * - `'collapse'`: Tapping backdrop collapses to next lower snap point (dismisses at min)
164
187
  *
165
188
  * @default 'block' (or 'passthrough' for component stacks)
166
189
  */
167
- backdropBehavior?: "block" | "passthrough" | "dismiss";
190
+ backdropBehavior?: "block" | "passthrough" | "dismiss" | "collapse";
168
191
  };