react-native-screen-transitions 3.3.0-rc.3 → 3.3.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 (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -7
  3. package/lib/commonjs/blank-stack/components/stack-view.js +2 -1
  4. package/lib/commonjs/blank-stack/components/stack-view.js.map +1 -1
  5. package/lib/commonjs/blank-stack/components/stack-view.native.js +2 -1
  6. package/lib/commonjs/blank-stack/components/stack-view.native.js.map +1 -1
  7. package/lib/commonjs/component-stack/components/stack-view.js +2 -1
  8. package/lib/commonjs/component-stack/components/stack-view.js.map +1 -1
  9. package/lib/commonjs/shared/components/overlay/helpers/get-active-overlay.js +3 -2
  10. package/lib/commonjs/shared/components/overlay/helpers/get-active-overlay.js.map +1 -1
  11. package/lib/commonjs/shared/components/overlay/variations/float-overlay.js +46 -9
  12. package/lib/commonjs/shared/components/overlay/variations/float-overlay.js.map +1 -1
  13. package/lib/commonjs/shared/components/overlay/variations/overlay-host.js +7 -7
  14. package/lib/commonjs/shared/components/overlay/variations/overlay-host.js.map +1 -1
  15. package/lib/commonjs/shared/components/overlay/variations/screen-overlay.js +23 -3
  16. package/lib/commonjs/shared/components/overlay/variations/screen-overlay.js.map +1 -1
  17. package/lib/commonjs/shared/components/screen-container.js +2 -1
  18. package/lib/commonjs/shared/components/screen-container.js.map +1 -1
  19. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +7 -1
  20. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  21. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +12 -12
  22. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  23. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js +5 -2
  24. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js.map +1 -1
  25. package/lib/commonjs/shared/hooks/navigation/use-optimistic-focused-index.js +20 -0
  26. package/lib/commonjs/shared/hooks/navigation/use-optimistic-focused-index.js.map +1 -0
  27. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js +2 -6
  28. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js.map +1 -1
  29. package/lib/commonjs/shared/hooks/reanimated/use-shared-value-state.js +4 -1
  30. package/lib/commonjs/shared/hooks/reanimated/use-shared-value-state.js.map +1 -1
  31. package/lib/commonjs/shared/providers/gestures.provider.js +17 -6
  32. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  33. package/lib/commonjs/shared/providers/layout-anchor.provider.js +7 -5
  34. package/lib/commonjs/shared/providers/layout-anchor.provider.js.map +1 -1
  35. package/lib/commonjs/shared/providers/screen/styles.provider.js +1 -6
  36. package/lib/commonjs/shared/providers/screen/styles.provider.js.map +1 -1
  37. package/lib/commonjs/shared/providers/stack/direct.provider.js +2 -1
  38. package/lib/commonjs/shared/providers/stack/direct.provider.js.map +1 -1
  39. package/lib/commonjs/shared/providers/stack/managed.provider.js +2 -1
  40. package/lib/commonjs/shared/providers/stack/managed.provider.js.map +1 -1
  41. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js +9 -2
  42. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js.map +1 -1
  43. package/lib/commonjs/shared/utils/gesture/find-collapse-target.js +11 -1
  44. package/lib/commonjs/shared/utils/gesture/find-collapse-target.js.map +1 -1
  45. package/lib/commonjs/shared/utils/gesture/validate-snap-points.js +11 -2
  46. package/lib/commonjs/shared/utils/gesture/validate-snap-points.js.map +1 -1
  47. package/lib/commonjs/shared/utils/overlay/visibility.js +19 -0
  48. package/lib/commonjs/shared/utils/overlay/visibility.js.map +1 -0
  49. package/lib/module/blank-stack/components/stack-view.js +2 -1
  50. package/lib/module/blank-stack/components/stack-view.js.map +1 -1
  51. package/lib/module/blank-stack/components/stack-view.native.js +2 -1
  52. package/lib/module/blank-stack/components/stack-view.native.js.map +1 -1
  53. package/lib/module/component-stack/components/stack-view.js +2 -1
  54. package/lib/module/component-stack/components/stack-view.js.map +1 -1
  55. package/lib/module/shared/components/overlay/helpers/get-active-overlay.js +4 -2
  56. package/lib/module/shared/components/overlay/helpers/get-active-overlay.js.map +1 -1
  57. package/lib/module/shared/components/overlay/variations/float-overlay.js +47 -11
  58. package/lib/module/shared/components/overlay/variations/float-overlay.js.map +1 -1
  59. package/lib/module/shared/components/overlay/variations/overlay-host.js +7 -7
  60. package/lib/module/shared/components/overlay/variations/overlay-host.js.map +1 -1
  61. package/lib/module/shared/components/overlay/variations/screen-overlay.js +24 -5
  62. package/lib/module/shared/components/overlay/variations/screen-overlay.js.map +1 -1
  63. package/lib/module/shared/components/screen-container.js +2 -1
  64. package/lib/module/shared/components/screen-container.js.map +1 -1
  65. package/lib/module/shared/hooks/gestures/use-build-gestures.js +7 -1
  66. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  67. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +12 -12
  68. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  69. package/lib/module/shared/hooks/lifecycle/use-close-transition.js +5 -2
  70. package/lib/module/shared/hooks/lifecycle/use-close-transition.js.map +1 -1
  71. package/lib/module/shared/hooks/navigation/use-optimistic-focused-index.js +17 -0
  72. package/lib/module/shared/hooks/navigation/use-optimistic-focused-index.js.map +1 -0
  73. package/lib/module/shared/hooks/navigation/use-screen-state.js +2 -6
  74. package/lib/module/shared/hooks/navigation/use-screen-state.js.map +1 -1
  75. package/lib/module/shared/hooks/reanimated/use-shared-value-state.js +4 -1
  76. package/lib/module/shared/hooks/reanimated/use-shared-value-state.js.map +1 -1
  77. package/lib/module/shared/providers/gestures.provider.js +17 -6
  78. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  79. package/lib/module/shared/providers/layout-anchor.provider.js +7 -5
  80. package/lib/module/shared/providers/layout-anchor.provider.js.map +1 -1
  81. package/lib/module/shared/providers/screen/styles.provider.js +1 -6
  82. package/lib/module/shared/providers/screen/styles.provider.js.map +1 -1
  83. package/lib/module/shared/providers/stack/direct.provider.js +2 -1
  84. package/lib/module/shared/providers/stack/direct.provider.js.map +1 -1
  85. package/lib/module/shared/providers/stack/managed.provider.js +2 -1
  86. package/lib/module/shared/providers/stack/managed.provider.js.map +1 -1
  87. package/lib/module/shared/utils/gesture/determine-snap-target.js +9 -2
  88. package/lib/module/shared/utils/gesture/determine-snap-target.js.map +1 -1
  89. package/lib/module/shared/utils/gesture/find-collapse-target.js +11 -1
  90. package/lib/module/shared/utils/gesture/find-collapse-target.js.map +1 -1
  91. package/lib/module/shared/utils/gesture/validate-snap-points.js +11 -2
  92. package/lib/module/shared/utils/gesture/validate-snap-points.js.map +1 -1
  93. package/lib/module/shared/utils/overlay/visibility.js +12 -0
  94. package/lib/module/shared/utils/overlay/visibility.js.map +1 -0
  95. package/lib/typescript/blank-stack/components/stack-view.d.ts.map +1 -1
  96. package/lib/typescript/blank-stack/components/stack-view.native.d.ts.map +1 -1
  97. package/lib/typescript/component-stack/components/stack-view.d.ts.map +1 -1
  98. package/lib/typescript/shared/components/overlay/helpers/get-active-overlay.d.ts +1 -1
  99. package/lib/typescript/shared/components/overlay/helpers/get-active-overlay.d.ts.map +1 -1
  100. package/lib/typescript/shared/components/overlay/variations/float-overlay.d.ts.map +1 -1
  101. package/lib/typescript/shared/components/overlay/variations/overlay-host.d.ts +7 -0
  102. package/lib/typescript/shared/components/overlay/variations/overlay-host.d.ts.map +1 -1
  103. package/lib/typescript/shared/components/overlay/variations/screen-overlay.d.ts.map +1 -1
  104. package/lib/typescript/shared/components/screen-container.d.ts.map +1 -1
  105. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  106. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
  107. package/lib/typescript/shared/hooks/lifecycle/use-close-transition.d.ts.map +1 -1
  108. package/lib/typescript/shared/hooks/navigation/use-optimistic-focused-index.d.ts +7 -0
  109. package/lib/typescript/shared/hooks/navigation/use-optimistic-focused-index.d.ts.map +1 -0
  110. package/lib/typescript/shared/hooks/navigation/use-screen-state.d.ts.map +1 -1
  111. package/lib/typescript/shared/hooks/reanimated/use-shared-value-state.d.ts.map +1 -1
  112. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  113. package/lib/typescript/shared/providers/layout-anchor.provider.d.ts +1 -1
  114. package/lib/typescript/shared/providers/layout-anchor.provider.d.ts.map +1 -1
  115. package/lib/typescript/shared/providers/screen/styles.provider.d.ts.map +1 -1
  116. package/lib/typescript/shared/providers/stack/direct.provider.d.ts.map +1 -1
  117. package/lib/typescript/shared/providers/stack/managed.provider.d.ts.map +1 -1
  118. package/lib/typescript/shared/types/screen.types.d.ts +16 -1
  119. package/lib/typescript/shared/types/screen.types.d.ts.map +1 -1
  120. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts.map +1 -1
  121. package/lib/typescript/shared/utils/gesture/find-collapse-target.d.ts.map +1 -1
  122. package/lib/typescript/shared/utils/gesture/validate-snap-points.d.ts.map +1 -1
  123. package/lib/typescript/shared/utils/overlay/visibility.d.ts +11 -0
  124. package/lib/typescript/shared/utils/overlay/visibility.d.ts.map +1 -0
  125. package/package.json +8 -2
  126. package/src/blank-stack/components/stack-view.native.tsx +2 -1
  127. package/src/blank-stack/components/stack-view.tsx +2 -1
  128. package/src/component-stack/components/stack-view.tsx +2 -1
  129. package/src/shared/components/overlay/helpers/get-active-overlay.ts +3 -2
  130. package/src/shared/components/overlay/variations/float-overlay.tsx +53 -8
  131. package/src/shared/components/overlay/variations/overlay-host.tsx +16 -6
  132. package/src/shared/components/overlay/variations/screen-overlay.tsx +35 -3
  133. package/src/shared/components/screen-container.tsx +15 -9
  134. package/src/shared/hooks/gestures/use-build-gestures.tsx +5 -1
  135. package/src/shared/hooks/gestures/use-scroll-registry.tsx +10 -6
  136. package/src/shared/hooks/lifecycle/use-close-transition.ts +6 -3
  137. package/src/shared/hooks/navigation/use-optimistic-focused-index.ts +19 -0
  138. package/src/shared/hooks/navigation/use-screen-state.tsx +4 -7
  139. package/src/shared/hooks/reanimated/use-shared-value-state.ts +4 -1
  140. package/src/shared/providers/gestures.provider.tsx +49 -22
  141. package/src/shared/providers/layout-anchor.provider.tsx +28 -25
  142. package/src/shared/providers/screen/styles.provider.tsx +1 -7
  143. package/src/shared/providers/stack/direct.provider.tsx +2 -2
  144. package/src/shared/providers/stack/managed.provider.tsx +2 -2
  145. package/src/shared/types/screen.types.ts +17 -1
  146. package/src/shared/utils/gesture/determine-snap-target.ts +15 -4
  147. package/src/shared/utils/gesture/find-collapse-target.ts +11 -1
  148. package/src/shared/utils/gesture/validate-snap-points.ts +15 -2
  149. package/src/shared/utils/overlay/visibility.ts +23 -0
@@ -25,6 +25,8 @@ export const ScreenContainer = memo(({ children }: Props) => {
25
25
  const { pointerEvents, backdropBehavior } = useBackdropPointerEvents();
26
26
  const gestureContext = useGestureContext();
27
27
 
28
+ const BackdropComponent = current.options.backdropComponent;
29
+
28
30
  const isBackdropActive =
29
31
  backdropBehavior === "dismiss" || backdropBehavior === "collapse";
30
32
 
@@ -101,15 +103,19 @@ export const ScreenContainer = memo(({ children }: Props) => {
101
103
 
102
104
  return (
103
105
  <View style={styles.container} pointerEvents={pointerEvents}>
104
- <Pressable
105
- style={StyleSheet.absoluteFillObject}
106
- pointerEvents={isBackdropActive ? "auto" : "none"}
107
- onPress={isBackdropActive ? handleBackdropPress : undefined}
108
- >
109
- <Animated.View
110
- style={[StyleSheet.absoluteFillObject, animatedBackdropStyle]}
111
- />
112
- </Pressable>
106
+ {BackdropComponent ? (
107
+ <BackdropComponent />
108
+ ) : (
109
+ <Pressable
110
+ style={StyleSheet.absoluteFillObject}
111
+ pointerEvents={isBackdropActive ? "auto" : "none"}
112
+ onPress={isBackdropActive ? handleBackdropPress : undefined}
113
+ >
114
+ <Animated.View
115
+ style={[StyleSheet.absoluteFillObject, animatedBackdropStyle]}
116
+ />
117
+ </Pressable>
118
+ )}
113
119
  <GestureDetector gesture={gestureContext!.panGesture}>
114
120
  <Animated.View
115
121
  style={[styles.content, animatedContentStyle]}
@@ -12,6 +12,7 @@ import { GestureStore, type GestureStoreMap } from "../../stores/gesture.store";
12
12
  import type { ClaimedDirections, Direction } from "../../types/ownership.types";
13
13
  import { claimsAnyDirection } from "../../utils/gesture/compute-claimed-directions";
14
14
  import { resolveOwnership } from "../../utils/gesture/resolve-ownership";
15
+ import { validateSnapPoints } from "../../utils/gesture/validate-snap-points";
15
16
  import { useScreenGestureHandlers } from "./use-screen-gesture-handlers";
16
17
 
17
18
  const DIRECTIONS: Direction[] = [
@@ -94,7 +95,10 @@ export const useBuildGestures = ({
94
95
  const canDismiss = Boolean(
95
96
  isFirstScreen ? false : current.options.gestureEnabled,
96
97
  );
97
- const hasSnapPoints = Array.isArray(snapPoints) && snapPoints.length > 0;
98
+ const { hasSnapPoints } = useMemo(
99
+ () => validateSnapPoints({ snapPoints, canDismiss }),
100
+ [snapPoints, canDismiss],
101
+ );
98
102
  const gestureEnabled = canDismiss || hasSnapPoints;
99
103
 
100
104
  const ownershipStatus = useMemo(
@@ -123,18 +123,22 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
123
123
  const setIsTouched = () => {
124
124
  "worklet";
125
125
  for (const scrollConfig of scrollConfigs) {
126
- if (scrollConfig.value) {
127
- scrollConfig.value = { ...scrollConfig.value, isTouched: true };
128
- }
126
+ scrollConfig.modify((v) => {
127
+ "worklet";
128
+ if (v) v.isTouched = true;
129
+ return v;
130
+ });
129
131
  }
130
132
  };
131
133
 
132
134
  const clearIsTouched = () => {
133
135
  "worklet";
134
136
  for (const scrollConfig of scrollConfigs) {
135
- if (scrollConfig.value) {
136
- scrollConfig.value = { ...scrollConfig.value, isTouched: false };
137
- }
137
+ scrollConfig.modify((v) => {
138
+ "worklet";
139
+ if (v) v.isTouched = false;
140
+ return v;
141
+ });
138
142
  }
139
143
  };
140
144
 
@@ -47,9 +47,12 @@ const useManagedClose = ({
47
47
  const transitionSpec = current.options.transitionSpec;
48
48
 
49
49
  useAnimatedReaction(
50
- () => closingRouteKeysShared.value,
51
- (keys) => {
52
- if (!keys?.includes(routeKey)) return;
50
+ () => {
51
+ const keys = closingRouteKeysShared.value;
52
+ return keys?.includes(routeKey) ?? false;
53
+ },
54
+ (isClosing, wasClosing) => {
55
+ if (!isClosing || wasClosing) return;
53
56
 
54
57
  runOnJS(activate)();
55
58
  animateToProgress({
@@ -0,0 +1,19 @@
1
+ import { type DerivedValue, useDerivedValue } from "react-native-reanimated";
2
+ import { useSharedValueState } from "../reanimated/use-shared-value-state";
3
+
4
+ /**
5
+ * Returns a JS-focused index derived from optimisticFocusedIndex and clamped to route count.
6
+ * Keeps callers aligned on focus behavior during transitions with closing screens.
7
+ */
8
+ export function useOptimisticFocusedIndex(
9
+ optimisticFocusedIndex: DerivedValue<number>,
10
+ routeCount: number,
11
+ ): number {
12
+ return useSharedValueState(
13
+ useDerivedValue(() => {
14
+ const globalIndex = optimisticFocusedIndex.get();
15
+ if (routeCount <= 0) return 0;
16
+ return Math.max(0, Math.min(globalIndex, routeCount - 1));
17
+ }),
18
+ );
19
+ }
@@ -1,6 +1,5 @@
1
1
  import type { Route } from "@react-navigation/native";
2
2
  import { useCallback, useMemo } from "react";
3
- import { useDerivedValue } from "react-native-reanimated";
4
3
  import { snapDescriptorToIndex } from "../../animation/snap-to";
5
4
  import {
6
5
  type BaseDescriptor,
@@ -8,7 +7,7 @@ import {
8
7
  } from "../../providers/screen/keys.provider";
9
8
  import type { ScreenTransitionConfig } from "../../types/screen.types";
10
9
  import type { BaseStackNavigation } from "../../types/stack.types";
11
- import { useSharedValueState } from "../reanimated/use-shared-value-state";
10
+ import { useOptimisticFocusedIndex } from "./use-optimistic-focused-index";
12
11
  import { type StackContextValue, useStack } from "./use-stack";
13
12
 
14
13
  export interface ScreenState<
@@ -74,11 +73,9 @@ export function useScreenState<
74
73
  [routeKeys, current.route.key],
75
74
  );
76
75
 
77
- const focusedIndex = useSharedValueState(
78
- useDerivedValue(() => {
79
- const globalIndex = optimisticFocusedIndex.get();
80
- return Math.max(0, Math.min(globalIndex, routeKeys.length - 1));
81
- }),
76
+ const focusedIndex = useOptimisticFocusedIndex(
77
+ optimisticFocusedIndex,
78
+ routeKeys.length,
82
79
  );
83
80
 
84
81
  const focusedScene = useMemo(() => {
@@ -26,7 +26,10 @@ export function useSharedValueState<T>(sharedValue: SharedValue<T>): T {
26
26
 
27
27
  useAnimatedReaction(
28
28
  () => sharedValue.value,
29
- (value) => runOnJS(setState)(value),
29
+ (value, previousValue) => {
30
+ if (Object.is(value, previousValue)) return;
31
+ runOnJS(setState)(value);
32
+ },
30
33
  );
31
34
 
32
35
  return state;
@@ -22,6 +22,7 @@ import type { ClaimedDirections, Direction } from "../types/ownership.types";
22
22
  import { StackType } from "../types/stack.types";
23
23
  import createProvider from "../utils/create-provider";
24
24
  import { computeClaimedDirections } from "../utils/gesture/compute-claimed-directions";
25
+ import { validateSnapPoints } from "../utils/gesture/validate-snap-points";
25
26
  import { useKeys } from "./screen/keys.provider";
26
27
  import { useStackCoreContext } from "./stack/core.provider";
27
28
 
@@ -159,30 +160,43 @@ export const {
159
160
  } = createProvider("ScreenGesture", { guarded: false })<
160
161
  ScreenGestureProviderProps,
161
162
  GestureContextType
162
- >(({ children }) => {
163
+ >(({ children }): { value: GestureContextType; children: React.ReactNode } => {
163
164
  const { current } = useKeys();
164
165
  const { flags } = useStackCoreContext();
165
- const ancestorContext = useGestureContext();
166
-
167
- const hasGestures = current.options.gestureEnabled === true;
166
+ const ancestorContext: GestureContextType | null = useGestureContext();
168
167
  const isIsolated = flags.STACK_TYPE === StackType.COMPONENT;
168
+ const routeKey = current.route.key;
169
169
 
170
- const hasSnapPoints =
171
- Array.isArray(current.options.snapPoints) &&
172
- current.options.snapPoints.length > 0;
170
+ const isFirstScreen = useNavigationState((state) => {
171
+ const index = state.routes.findIndex((route) => route.key === routeKey);
172
+ return index === 0;
173
+ });
174
+
175
+ const canDismiss = Boolean(
176
+ isFirstScreen ? false : current.options.gestureEnabled,
177
+ );
178
+
179
+ const { hasSnapPoints } = useMemo(
180
+ () =>
181
+ validateSnapPoints({
182
+ snapPoints: current.options.snapPoints,
183
+ canDismiss,
184
+ }),
185
+ [current.options.snapPoints, canDismiss],
186
+ );
187
+
188
+ const gestureEnabled = canDismiss || hasSnapPoints;
173
189
 
174
190
  const claimedDirections = useMemo(
175
191
  () =>
176
192
  computeClaimedDirections(
177
- hasGestures,
193
+ gestureEnabled,
178
194
  current.options.gestureDirection,
179
195
  hasSnapPoints,
180
196
  ),
181
- [hasGestures, current.options.gestureDirection, hasSnapPoints],
197
+ [gestureEnabled, current.options.gestureDirection, hasSnapPoints],
182
198
  );
183
199
 
184
- const routeKey = current.route.key;
185
-
186
200
  // Check if this screen is the current (topmost) route in its navigator
187
201
  const isCurrentRoute = useNavigationState(
188
202
  (state) => state.routes[state.index]?.key === routeKey,
@@ -208,17 +222,30 @@ export const {
208
222
  isIsolated,
209
223
  });
210
224
 
211
- const value: GestureContextType = {
212
- panGesture,
213
- panGestureRef,
214
- scrollConfig,
215
- gestureAnimationValues,
216
- ancestorContext,
217
- gestureEnabled: hasGestures,
218
- isIsolated,
219
- claimedDirections,
220
- childDirectionClaims,
221
- };
225
+ const value = useMemo<GestureContextType>(
226
+ () => ({
227
+ panGesture,
228
+ panGestureRef,
229
+ scrollConfig,
230
+ gestureAnimationValues,
231
+ ancestorContext,
232
+ gestureEnabled,
233
+ isIsolated,
234
+ claimedDirections,
235
+ childDirectionClaims,
236
+ }),
237
+ [
238
+ panGesture,
239
+ panGestureRef,
240
+ scrollConfig,
241
+ gestureAnimationValues,
242
+ ancestorContext,
243
+ gestureEnabled,
244
+ isIsolated,
245
+ claimedDirections,
246
+ childDirectionClaims,
247
+ ],
248
+ );
222
249
 
223
250
  return {
224
251
  value,
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode, useCallback, useMemo } from "react";
2
2
  import { useWindowDimensions, type View } from "react-native";
3
3
  import {
4
4
  type AnimatedRef,
@@ -44,35 +44,38 @@ const { LayoutAnchorProvider, useLayoutAnchorContext } = createProvider(
44
44
  ({ anchorRef, children }) => {
45
45
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
46
46
 
47
- const correctMeasurement = (
48
- measured: MeasuredDimensions,
49
- ): MeasuredDimensions => {
50
- "worklet";
51
- const anchor = measure(anchorRef);
52
- if (!anchor) return measured;
47
+ const correctMeasurement = useCallback(
48
+ (measured: MeasuredDimensions): MeasuredDimensions => {
49
+ "worklet";
50
+ const anchor = measure(anchorRef);
51
+ if (!anchor) return measured;
53
52
 
54
- // Compute scale factor by comparing anchor size to expected screen size.
55
- // Anchor should be full-screen (absoluteFill), so any difference is from scale.
56
- const scaleX = anchor.width > 0 ? anchor.width / screenWidth : 1;
57
- const scaleY = anchor.height > 0 ? anchor.height / screenHeight : 1;
53
+ // Compute scale factor by comparing anchor size to expected screen size.
54
+ // Anchor should be full-screen (absoluteFill), so any difference is from scale.
55
+ const scaleX = anchor.width > 0 ? anchor.width / screenWidth : 1;
56
+ const scaleY = anchor.height > 0 ? anchor.height / screenHeight : 1;
58
57
 
59
- // Get element position relative to anchor (removes translation)
60
- const relativeX = measured.pageX - anchor.pageX;
61
- const relativeY = measured.pageY - anchor.pageY;
58
+ // Get element position relative to anchor (removes translation)
59
+ const relativeX = measured.pageX - anchor.pageX;
60
+ const relativeY = measured.pageY - anchor.pageY;
62
61
 
63
- // Reverse scale: divide relative position and dimensions by scale factor
64
- return {
65
- x: measured.x,
66
- y: measured.y,
67
- width: scaleX !== 1 ? measured.width / scaleX : measured.width,
68
- height: scaleY !== 1 ? measured.height / scaleY : measured.height,
69
- pageX: scaleX !== 1 ? relativeX / scaleX : relativeX,
70
- pageY: scaleY !== 1 ? relativeY / scaleY : relativeY,
71
- };
72
- };
62
+ // Reverse scale: divide relative position and dimensions by scale factor
63
+ return {
64
+ x: measured.x,
65
+ y: measured.y,
66
+ width: scaleX !== 1 ? measured.width / scaleX : measured.width,
67
+ height: scaleY !== 1 ? measured.height / scaleY : measured.height,
68
+ pageX: scaleX !== 1 ? relativeX / scaleX : relativeX,
69
+ pageY: scaleY !== 1 ? relativeY / scaleY : relativeY,
70
+ };
71
+ },
72
+ [anchorRef, screenWidth, screenHeight],
73
+ );
74
+
75
+ const value = useMemo(() => ({ correctMeasurement }), [correctMeasurement]);
73
76
 
74
77
  return {
75
- value: { correctMeasurement },
78
+ value,
76
79
  children,
77
80
  };
78
81
  },
@@ -37,7 +37,7 @@ export function ScreenStylesProvider({ children }: Props) {
37
37
  const stylesMap = useDerivedValue<TransitionInterpolatedStyle>(() => {
38
38
  "worklet";
39
39
  const props = screenInterpolatorProps.value;
40
- const { current, next, progress, stackProgress } = props;
40
+ const { current, next, progress } = props;
41
41
  const isDragging = current.gesture.isDragging;
42
42
  const isNextClosing = !!next?.closing;
43
43
 
@@ -51,9 +51,6 @@ export function ScreenStylesProvider({ children }: Props) {
51
51
 
52
52
  const isInGestureMode = isDragging || isGesturingDuringCloseAnimation.value;
53
53
 
54
- const hasPushedScreenWhileClosing =
55
- !isInGestureMode && isNextClosing && stackProgress > progress;
56
-
57
54
  // Select interpolator
58
55
  // - If in gesture mode, use current screen's interpolator since we're driving
59
56
  // the animation from this screen (dragging back to dismiss next).
@@ -65,7 +62,6 @@ export function ScreenStylesProvider({ children }: Props) {
65
62
 
66
63
  // Build effective props with corrected progress
67
64
  // - Gesture mode: use current.progress only (avoids jumps during drag)
68
- // - Pushed while closing: use stackProgress (includes new screen)
69
65
  // - Normal: use derived progress as-is
70
66
 
71
67
  let effectiveProgress = progress;
@@ -74,8 +70,6 @@ export function ScreenStylesProvider({ children }: Props) {
74
70
  if (isInGestureMode) {
75
71
  effectiveProgress = current.progress;
76
72
  effectiveNext = undefined;
77
- } else if (hasPushedScreenWhileClosing) {
78
- effectiveProgress = stackProgress;
79
73
  }
80
74
 
81
75
  try {
@@ -20,6 +20,7 @@ import {
20
20
  type AnimationStoreMap,
21
21
  } from "../../stores/animation.store";
22
22
  import { HistoryStore } from "../../stores/history.store";
23
+ import { isFloatOverlayVisible } from "../../utils/overlay/visibility";
23
24
  import { useStackCoreContext } from "./core.provider";
24
25
 
25
26
  export interface DirectStackScene {
@@ -116,8 +117,7 @@ function useDirectStackValue(
116
117
  const options = descriptor.options;
117
118
  if (
118
119
  options?.enableTransitions === true &&
119
- options?.overlayMode === "float" &&
120
- options?.overlayShown === true
120
+ isFloatOverlayVisible(options)
121
121
  ) {
122
122
  shouldShowFloatOverlay = true;
123
123
  }
@@ -22,6 +22,7 @@ import type {
22
22
  BaseStackScene,
23
23
  BaseStackState,
24
24
  } from "../../types/stack.types";
25
+ import { isFloatOverlayVisible } from "../../utils/overlay/visibility";
25
26
  import { useStackCoreContext } from "./core.provider";
26
27
  import { useLocalRoutes } from "./helpers/use-local-routes";
27
28
 
@@ -115,8 +116,7 @@ function useManagedStackValue<
115
116
  animationMaps[i] = AnimationStore.getAll(route.key);
116
117
 
117
118
  if (!shouldShowFloatOverlay) {
118
- shouldShowFloatOverlay =
119
- options?.overlayMode === "float" && options?.overlayShown === true;
119
+ shouldShowFloatOverlay = isFloatOverlayVisible(options);
120
120
  }
121
121
 
122
122
  if (!stopLimit) {
@@ -77,7 +77,10 @@ export type ScreenTransitionConfig = {
77
77
  transitionSpec?: TransitionSpec;
78
78
 
79
79
  /**
80
- * Whether the gesture is enabled.
80
+ * Controls whether swipe-to-dismiss is enabled.
81
+ *
82
+ * For screens with `snapPoints`, gesture-driven snapping between non-dismiss
83
+ * snap points remains available even when this is `false`.
81
84
  */
82
85
  gestureEnabled?: boolean;
83
86
 
@@ -210,4 +213,17 @@ export type ScreenTransitionConfig = {
210
213
  * @default 'block' (or 'passthrough' for component stacks)
211
214
  */
212
215
  backdropBehavior?: "block" | "passthrough" | "dismiss" | "collapse";
216
+
217
+ /**
218
+ * Custom component to render as the backdrop layer.
219
+ * When provided, replaces the default backdrop entirely — including press handling.
220
+ *
221
+ * Use `useScreenAnimation()` inside the component to access animation values.
222
+ * Use your navigation method of choice (e.g. `router.back()`) to handle dismissal.
223
+ *
224
+ * `backdropBehavior` still controls container-level pointer events when this is set.
225
+ *
226
+ * @default undefined
227
+ */
228
+ backdropComponent?: React.FC;
213
229
  };
@@ -43,10 +43,21 @@ export function determineSnapTarget({
43
43
  // Project where we'd end up with velocity
44
44
  const projectedProgress = currentProgress - velocityInProgress;
45
45
 
46
+ const sanitizedSnapPoints = snapPoints.filter((point) =>
47
+ canDismiss ? Number.isFinite(point) : Number.isFinite(point) && point > 0,
48
+ );
49
+
46
50
  // Build all possible targets: dismiss (0) only if allowed, plus all snap points
47
- const allTargets = canDismiss
48
- ? [0, ...snapPoints].sort((a, b) => a - b)
49
- : [...snapPoints].sort((a, b) => a - b);
51
+ const allTargets = Array.from(
52
+ new Set(canDismiss ? [0, ...sanitizedSnapPoints] : sanitizedSnapPoints),
53
+ ).sort((a, b) => a - b);
54
+
55
+ if (allTargets.length === 0) {
56
+ return {
57
+ targetProgress: currentProgress,
58
+ shouldDismiss: false,
59
+ };
60
+ }
50
61
 
51
62
  // Find the target whose zone contains the projected progress
52
63
  // Zones are split at midpoints between adjacent targets
@@ -74,6 +85,6 @@ export function determineSnapTarget({
74
85
 
75
86
  return {
76
87
  targetProgress,
77
- shouldDismiss: targetProgress === 0,
88
+ shouldDismiss: canDismiss && targetProgress === 0,
78
89
  };
79
90
  }
@@ -22,7 +22,17 @@ export function findCollapseTarget(
22
22
  ): FindCollapseTargetResult {
23
23
  "worklet";
24
24
 
25
- const sorted = [...snapPoints].sort((a, b) => a - b);
25
+ const normalized = snapPoints.filter((point) =>
26
+ canDismiss ? Number.isFinite(point) : Number.isFinite(point) && point > 0,
27
+ );
28
+
29
+ if (normalized.length === 0) {
30
+ return canDismiss
31
+ ? { target: 0, shouldDismiss: true }
32
+ : { target: currentProgress, shouldDismiss: false };
33
+ }
34
+
35
+ const sorted = [...normalized].sort((a, b) => a - b);
26
36
  const minSnap = sorted[0];
27
37
 
28
38
  // Find next lower snap point
@@ -14,7 +14,7 @@ export const validateSnapPoints = ({
14
14
  snapPoints,
15
15
  canDismiss,
16
16
  }: ValidateSnapPointsOptions): ValidateSnapPointsResult => {
17
- if (!snapPoints) {
17
+ if (!snapPoints || snapPoints.length === 0) {
18
18
  return {
19
19
  hasSnapPoints: false,
20
20
  snapPoints: [],
@@ -23,7 +23,20 @@ export const validateSnapPoints = ({
23
23
  };
24
24
  }
25
25
 
26
- const sortedSnaps = snapPoints.slice().sort((a, b) => a - b);
26
+ const normalizedSnaps = snapPoints.filter((point) =>
27
+ canDismiss ? Number.isFinite(point) : Number.isFinite(point) && point > 0,
28
+ );
29
+
30
+ if (normalizedSnaps.length === 0) {
31
+ return {
32
+ hasSnapPoints: false,
33
+ snapPoints: [],
34
+ minSnapPoint: -1,
35
+ maxSnapPoint: -1,
36
+ };
37
+ }
38
+
39
+ const sortedSnaps = normalizedSnaps.slice().sort((a, b) => a - b);
27
40
  // Clamp to snap point bounds (dismiss at 0 only if allowed)
28
41
  const minProgress = canDismiss ? 0 : sortedSnaps[0];
29
42
  const maxProgress = sortedSnaps[sortedSnaps.length - 1];
@@ -0,0 +1,23 @@
1
+ import type { OverlayMode } from "../../types/overlay.types";
2
+
3
+ type OverlayOptionsLike = {
4
+ overlay?: unknown;
5
+ overlayMode?: OverlayMode;
6
+ overlayShown?: boolean;
7
+ };
8
+
9
+ export const isOverlayVisible = (options?: OverlayOptionsLike): boolean => {
10
+ return Boolean(options?.overlay) && options?.overlayShown !== false;
11
+ };
12
+
13
+ export const isFloatOverlayVisible = (
14
+ options?: OverlayOptionsLike,
15
+ ): boolean => {
16
+ return isOverlayVisible(options) && options?.overlayMode !== "screen";
17
+ };
18
+
19
+ export const isScreenOverlayVisible = (
20
+ options?: OverlayOptionsLike,
21
+ ): boolean => {
22
+ return isOverlayVisible(options) && options?.overlayMode === "screen";
23
+ };