react-native-screen-transitions 3.3.0-rc.2 → 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 (213) 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/animation/resolve-snap-target.js +48 -0
  10. package/lib/commonjs/shared/animation/resolve-snap-target.js.map +1 -0
  11. package/lib/commonjs/shared/animation/snap-to.js +35 -34
  12. package/lib/commonjs/shared/animation/snap-to.js.map +1 -1
  13. package/lib/commonjs/shared/components/create-transition-aware-component.js +15 -7
  14. package/lib/commonjs/shared/components/create-transition-aware-component.js.map +1 -1
  15. package/lib/commonjs/shared/components/overlay/helpers/get-active-overlay.js +3 -2
  16. package/lib/commonjs/shared/components/overlay/helpers/get-active-overlay.js.map +1 -1
  17. package/lib/commonjs/shared/components/overlay/variations/float-overlay.js +46 -9
  18. package/lib/commonjs/shared/components/overlay/variations/float-overlay.js.map +1 -1
  19. package/lib/commonjs/shared/components/overlay/variations/overlay-host.js +7 -7
  20. package/lib/commonjs/shared/components/overlay/variations/overlay-host.js.map +1 -1
  21. package/lib/commonjs/shared/components/overlay/variations/screen-overlay.js +23 -3
  22. package/lib/commonjs/shared/components/overlay/variations/screen-overlay.js.map +1 -1
  23. package/lib/commonjs/shared/components/screen-container.js +2 -1
  24. package/lib/commonjs/shared/components/screen-container.js.map +1 -1
  25. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +6 -6
  26. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  27. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +7 -1
  28. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  29. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js +37 -8
  30. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -1
  31. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +12 -13
  32. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  33. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js +5 -2
  34. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js.map +1 -1
  35. package/lib/commonjs/shared/hooks/lifecycle/use-screen-events.js +14 -4
  36. package/lib/commonjs/shared/hooks/lifecycle/use-screen-events.js.map +1 -1
  37. package/lib/commonjs/shared/hooks/navigation/use-optimistic-focused-index.js +20 -0
  38. package/lib/commonjs/shared/hooks/navigation/use-optimistic-focused-index.js.map +1 -0
  39. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js +11 -8
  40. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js.map +1 -1
  41. package/lib/commonjs/shared/hooks/reanimated/use-shared-value-state.js +4 -1
  42. package/lib/commonjs/shared/hooks/reanimated/use-shared-value-state.js.map +1 -1
  43. package/lib/commonjs/shared/providers/gestures.provider.js +17 -6
  44. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  45. package/lib/commonjs/shared/providers/layout-anchor.provider.js +7 -5
  46. package/lib/commonjs/shared/providers/layout-anchor.provider.js.map +1 -1
  47. package/lib/commonjs/shared/providers/register-bounds.provider.js +25 -6
  48. package/lib/commonjs/shared/providers/register-bounds.provider.js.map +1 -1
  49. package/lib/commonjs/shared/providers/screen/styles.provider.js +1 -6
  50. package/lib/commonjs/shared/providers/screen/styles.provider.js.map +1 -1
  51. package/lib/commonjs/shared/providers/stack/direct.provider.js +15 -16
  52. package/lib/commonjs/shared/providers/stack/direct.provider.js.map +1 -1
  53. package/lib/commonjs/shared/providers/stack/managed.provider.js +19 -16
  54. package/lib/commonjs/shared/providers/stack/managed.provider.js.map +1 -1
  55. package/lib/commonjs/shared/stores/bounds.store.js +46 -0
  56. package/lib/commonjs/shared/stores/bounds.store.js.map +1 -1
  57. package/lib/commonjs/shared/utils/bounds/index.js +6 -4
  58. package/lib/commonjs/shared/utils/bounds/index.js.map +1 -1
  59. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js +9 -2
  60. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js.map +1 -1
  61. package/lib/commonjs/shared/utils/gesture/find-collapse-target.js +11 -1
  62. package/lib/commonjs/shared/utils/gesture/find-collapse-target.js.map +1 -1
  63. package/lib/commonjs/shared/utils/gesture/validate-snap-points.js +11 -2
  64. package/lib/commonjs/shared/utils/gesture/validate-snap-points.js.map +1 -1
  65. package/lib/commonjs/shared/utils/overlay/visibility.js +19 -0
  66. package/lib/commonjs/shared/utils/overlay/visibility.js.map +1 -0
  67. package/lib/module/blank-stack/components/stack-view.js +2 -1
  68. package/lib/module/blank-stack/components/stack-view.js.map +1 -1
  69. package/lib/module/blank-stack/components/stack-view.native.js +2 -1
  70. package/lib/module/blank-stack/components/stack-view.native.js.map +1 -1
  71. package/lib/module/component-stack/components/stack-view.js +2 -1
  72. package/lib/module/component-stack/components/stack-view.js.map +1 -1
  73. package/lib/module/shared/animation/resolve-snap-target.js +44 -0
  74. package/lib/module/shared/animation/resolve-snap-target.js.map +1 -0
  75. package/lib/module/shared/animation/snap-to.js +34 -34
  76. package/lib/module/shared/animation/snap-to.js.map +1 -1
  77. package/lib/module/shared/components/create-transition-aware-component.js +16 -8
  78. package/lib/module/shared/components/create-transition-aware-component.js.map +1 -1
  79. package/lib/module/shared/components/overlay/helpers/get-active-overlay.js +4 -2
  80. package/lib/module/shared/components/overlay/helpers/get-active-overlay.js.map +1 -1
  81. package/lib/module/shared/components/overlay/variations/float-overlay.js +47 -11
  82. package/lib/module/shared/components/overlay/variations/float-overlay.js.map +1 -1
  83. package/lib/module/shared/components/overlay/variations/overlay-host.js +7 -7
  84. package/lib/module/shared/components/overlay/variations/overlay-host.js.map +1 -1
  85. package/lib/module/shared/components/overlay/variations/screen-overlay.js +24 -5
  86. package/lib/module/shared/components/overlay/variations/screen-overlay.js.map +1 -1
  87. package/lib/module/shared/components/screen-container.js +2 -1
  88. package/lib/module/shared/components/screen-container.js.map +1 -1
  89. package/lib/module/shared/hooks/animation/use-screen-animation.js +6 -6
  90. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  91. package/lib/module/shared/hooks/gestures/use-build-gestures.js +7 -1
  92. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  93. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js +37 -8
  94. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -1
  95. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +12 -13
  96. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  97. package/lib/module/shared/hooks/lifecycle/use-close-transition.js +5 -2
  98. package/lib/module/shared/hooks/lifecycle/use-close-transition.js.map +1 -1
  99. package/lib/module/shared/hooks/lifecycle/use-screen-events.js +13 -4
  100. package/lib/module/shared/hooks/lifecycle/use-screen-events.js.map +1 -1
  101. package/lib/module/shared/hooks/navigation/use-optimistic-focused-index.js +17 -0
  102. package/lib/module/shared/hooks/navigation/use-optimistic-focused-index.js.map +1 -0
  103. package/lib/module/shared/hooks/navigation/use-screen-state.js +12 -9
  104. package/lib/module/shared/hooks/navigation/use-screen-state.js.map +1 -1
  105. package/lib/module/shared/hooks/reanimated/use-shared-value-state.js +4 -1
  106. package/lib/module/shared/hooks/reanimated/use-shared-value-state.js.map +1 -1
  107. package/lib/module/shared/providers/gestures.provider.js +17 -6
  108. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  109. package/lib/module/shared/providers/layout-anchor.provider.js +7 -5
  110. package/lib/module/shared/providers/layout-anchor.provider.js.map +1 -1
  111. package/lib/module/shared/providers/register-bounds.provider.js +25 -6
  112. package/lib/module/shared/providers/register-bounds.provider.js.map +1 -1
  113. package/lib/module/shared/providers/screen/styles.provider.js +1 -6
  114. package/lib/module/shared/providers/screen/styles.provider.js.map +1 -1
  115. package/lib/module/shared/providers/stack/direct.provider.js +15 -16
  116. package/lib/module/shared/providers/stack/direct.provider.js.map +1 -1
  117. package/lib/module/shared/providers/stack/managed.provider.js +19 -16
  118. package/lib/module/shared/providers/stack/managed.provider.js.map +1 -1
  119. package/lib/module/shared/stores/bounds.store.js +46 -0
  120. package/lib/module/shared/stores/bounds.store.js.map +1 -1
  121. package/lib/module/shared/utils/bounds/index.js +6 -4
  122. package/lib/module/shared/utils/bounds/index.js.map +1 -1
  123. package/lib/module/shared/utils/gesture/determine-snap-target.js +9 -2
  124. package/lib/module/shared/utils/gesture/determine-snap-target.js.map +1 -1
  125. package/lib/module/shared/utils/gesture/find-collapse-target.js +11 -1
  126. package/lib/module/shared/utils/gesture/find-collapse-target.js.map +1 -1
  127. package/lib/module/shared/utils/gesture/validate-snap-points.js +11 -2
  128. package/lib/module/shared/utils/gesture/validate-snap-points.js.map +1 -1
  129. package/lib/module/shared/utils/overlay/visibility.js +12 -0
  130. package/lib/module/shared/utils/overlay/visibility.js.map +1 -0
  131. package/lib/typescript/blank-stack/components/stack-view.d.ts.map +1 -1
  132. package/lib/typescript/blank-stack/components/stack-view.native.d.ts.map +1 -1
  133. package/lib/typescript/component-stack/components/stack-view.d.ts.map +1 -1
  134. package/lib/typescript/shared/animation/resolve-snap-target.d.ts +3 -0
  135. package/lib/typescript/shared/animation/resolve-snap-target.d.ts.map +1 -0
  136. package/lib/typescript/shared/animation/snap-to.d.ts +2 -0
  137. package/lib/typescript/shared/animation/snap-to.d.ts.map +1 -1
  138. package/lib/typescript/shared/components/create-transition-aware-component.d.ts +1 -0
  139. package/lib/typescript/shared/components/create-transition-aware-component.d.ts.map +1 -1
  140. package/lib/typescript/shared/components/overlay/helpers/get-active-overlay.d.ts +1 -1
  141. package/lib/typescript/shared/components/overlay/helpers/get-active-overlay.d.ts.map +1 -1
  142. package/lib/typescript/shared/components/overlay/variations/float-overlay.d.ts.map +1 -1
  143. package/lib/typescript/shared/components/overlay/variations/overlay-host.d.ts +7 -0
  144. package/lib/typescript/shared/components/overlay/variations/overlay-host.d.ts.map +1 -1
  145. package/lib/typescript/shared/components/overlay/variations/screen-overlay.d.ts.map +1 -1
  146. package/lib/typescript/shared/components/screen-container.d.ts.map +1 -1
  147. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  148. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  149. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts +1 -1
  150. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts.map +1 -1
  151. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts +0 -2
  152. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
  153. package/lib/typescript/shared/hooks/lifecycle/use-close-transition.d.ts.map +1 -1
  154. package/lib/typescript/shared/hooks/lifecycle/use-screen-events.d.ts.map +1 -1
  155. package/lib/typescript/shared/hooks/navigation/use-optimistic-focused-index.d.ts +7 -0
  156. package/lib/typescript/shared/hooks/navigation/use-optimistic-focused-index.d.ts.map +1 -0
  157. package/lib/typescript/shared/hooks/navigation/use-screen-state.d.ts +6 -0
  158. package/lib/typescript/shared/hooks/navigation/use-screen-state.d.ts.map +1 -1
  159. package/lib/typescript/shared/hooks/reanimated/use-shared-value-state.d.ts.map +1 -1
  160. package/lib/typescript/shared/index.d.ts +4 -0
  161. package/lib/typescript/shared/index.d.ts.map +1 -1
  162. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  163. package/lib/typescript/shared/providers/layout-anchor.provider.d.ts +1 -1
  164. package/lib/typescript/shared/providers/layout-anchor.provider.d.ts.map +1 -1
  165. package/lib/typescript/shared/providers/register-bounds.provider.d.ts +1 -0
  166. package/lib/typescript/shared/providers/register-bounds.provider.d.ts.map +1 -1
  167. package/lib/typescript/shared/providers/screen/styles.provider.d.ts.map +1 -1
  168. package/lib/typescript/shared/providers/stack/direct.provider.d.ts.map +1 -1
  169. package/lib/typescript/shared/providers/stack/managed.provider.d.ts.map +1 -1
  170. package/lib/typescript/shared/stores/bounds.store.d.ts +2 -0
  171. package/lib/typescript/shared/stores/bounds.store.d.ts.map +1 -1
  172. package/lib/typescript/shared/types/screen.types.d.ts +36 -1
  173. package/lib/typescript/shared/types/screen.types.d.ts.map +1 -1
  174. package/lib/typescript/shared/utils/bounds/index.d.ts.map +1 -1
  175. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts.map +1 -1
  176. package/lib/typescript/shared/utils/gesture/find-collapse-target.d.ts.map +1 -1
  177. package/lib/typescript/shared/utils/gesture/validate-snap-points.d.ts.map +1 -1
  178. package/lib/typescript/shared/utils/overlay/visibility.d.ts +11 -0
  179. package/lib/typescript/shared/utils/overlay/visibility.d.ts.map +1 -0
  180. package/package.json +8 -2
  181. package/src/blank-stack/components/stack-view.native.tsx +2 -1
  182. package/src/blank-stack/components/stack-view.tsx +2 -1
  183. package/src/component-stack/components/stack-view.tsx +2 -1
  184. package/src/shared/animation/resolve-snap-target.ts +53 -0
  185. package/src/shared/animation/snap-to.ts +47 -38
  186. package/src/shared/components/create-transition-aware-component.tsx +34 -10
  187. package/src/shared/components/overlay/helpers/get-active-overlay.ts +3 -2
  188. package/src/shared/components/overlay/variations/float-overlay.tsx +53 -8
  189. package/src/shared/components/overlay/variations/overlay-host.tsx +16 -6
  190. package/src/shared/components/overlay/variations/screen-overlay.tsx +35 -3
  191. package/src/shared/components/screen-container.tsx +15 -9
  192. package/src/shared/hooks/animation/use-screen-animation.tsx +8 -8
  193. package/src/shared/hooks/gestures/use-build-gestures.tsx +5 -1
  194. package/src/shared/hooks/gestures/use-screen-gesture-handlers.ts +63 -16
  195. package/src/shared/hooks/gestures/use-scroll-registry.tsx +10 -9
  196. package/src/shared/hooks/lifecycle/use-close-transition.ts +6 -3
  197. package/src/shared/hooks/lifecycle/use-screen-events.ts +15 -4
  198. package/src/shared/hooks/navigation/use-optimistic-focused-index.ts +19 -0
  199. package/src/shared/hooks/navigation/use-screen-state.tsx +24 -8
  200. package/src/shared/hooks/reanimated/use-shared-value-state.ts +4 -1
  201. package/src/shared/providers/gestures.provider.tsx +49 -22
  202. package/src/shared/providers/layout-anchor.provider.tsx +28 -25
  203. package/src/shared/providers/register-bounds.provider.tsx +43 -6
  204. package/src/shared/providers/screen/styles.provider.tsx +1 -7
  205. package/src/shared/providers/stack/direct.provider.tsx +18 -19
  206. package/src/shared/providers/stack/managed.provider.tsx +22 -19
  207. package/src/shared/stores/bounds.store.ts +56 -0
  208. package/src/shared/types/screen.types.ts +39 -1
  209. package/src/shared/utils/bounds/index.ts +6 -4
  210. package/src/shared/utils/gesture/determine-snap-target.ts +15 -4
  211. package/src/shared/utils/gesture/find-collapse-target.ts +11 -1
  212. package/src/shared/utils/gesture/validate-snap-points.ts +15 -2
  213. package/src/shared/utils/overlay/visibility.ts +23 -0
@@ -1,12 +1,24 @@
1
1
  import { useMemo } from "react";
2
+ import { snapDescriptorToIndex } from "../../../animation/snap-to";
3
+ import { useOptimisticFocusedIndex } from "../../../hooks/navigation/use-optimistic-focused-index";
2
4
  import {
3
5
  type StackDescriptor,
4
6
  type StackScene,
5
7
  useStack,
6
8
  } from "../../../hooks/navigation/use-stack";
7
9
  import { useKeys } from "../../../providers/screen/keys.provider";
10
+ import type { OverlayProps } from "../../../types/overlay.types";
11
+ import { isScreenOverlayVisible } from "../../../utils/overlay/visibility";
8
12
  import { OverlayHost } from "./overlay-host";
9
13
 
14
+ type OverlayScreenState = Omit<
15
+ OverlayProps<StackDescriptor["navigation"]>,
16
+ "progress" | "overlayAnimation" | "screenAnimation"
17
+ > & {
18
+ index: number;
19
+ snapTo: (index: number) => void;
20
+ };
21
+
10
22
  /**
11
23
  * Screen overlay component that renders per-screen.
12
24
  * Gets current descriptor from keys context.
@@ -17,7 +29,11 @@ import { OverlayHost } from "./overlay-host";
17
29
  */
18
30
  export function ScreenOverlay() {
19
31
  const { current } = useKeys<StackDescriptor>();
20
- const { flags } = useStack();
32
+ const { flags, routes, optimisticFocusedIndex, routeKeys } = useStack();
33
+ const focusedIndex = useOptimisticFocusedIndex(
34
+ optimisticFocusedIndex,
35
+ routeKeys.length,
36
+ );
21
37
 
22
38
  const options = current.options;
23
39
 
@@ -29,14 +45,30 @@ export function ScreenOverlay() {
29
45
  [current],
30
46
  );
31
47
 
48
+ const overlayScreenState = useMemo<OverlayScreenState>(
49
+ () => ({
50
+ index: routeKeys.indexOf(current.route.key),
51
+ options: current.options,
52
+ routes,
53
+ focusedRoute: routes[focusedIndex] ?? current.route,
54
+ focusedIndex,
55
+ meta: current.options?.meta,
56
+ navigation: current.navigation,
57
+ snapTo: (index: number) => {
58
+ snapDescriptorToIndex(current, index);
59
+ },
60
+ }),
61
+ [current, routeKeys, routes, focusedIndex],
62
+ );
63
+
32
64
  // Skip screens without enableTransitions (native-stack only)
33
65
  if (!flags.TRANSITIONS_ALWAYS_ON && !options.enableTransitions) {
34
66
  return null;
35
67
  }
36
68
 
37
- if (!options.overlayShown || options.overlayMode !== "screen") {
69
+ if (!isScreenOverlayVisible(options)) {
38
70
  return null;
39
71
  }
40
72
 
41
- return <OverlayHost scene={scene} />;
73
+ return <OverlayHost scene={scene} overlayScreenState={overlayScreenState} />;
42
74
  }
@@ -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]}
@@ -80,11 +80,12 @@ const useBuildScreenTransitionState = (
80
80
  ): BuiltState | undefined => {
81
81
  const key = descriptor?.route?.key;
82
82
  const meta = descriptor?.options?.meta;
83
+ const route = descriptor?.route;
83
84
 
84
85
  return useMemo(() => {
85
- if (!key || !descriptor?.route) return undefined;
86
+ if (!key || !route) return undefined;
86
87
 
87
- const plainRoute = toPlainRoute(descriptor.route);
88
+ const plainRoute = toPlainRoute(route);
88
89
  const plainMeta = meta
89
90
  ? (toPlainValue(meta) as Record<string, unknown>)
90
91
  : undefined;
@@ -99,7 +100,7 @@ const useBuildScreenTransitionState = (
99
100
  meta: plainMeta,
100
101
  unwrapped: createScreenTransitionState(plainRoute, plainMeta),
101
102
  };
102
- }, [key, meta, descriptor]);
103
+ }, [key, meta, route]);
103
104
  };
104
105
 
105
106
  const hasTransitionsEnabled = (
@@ -135,11 +136,10 @@ export function _useScreenAnimation() {
135
136
  return points ? [...points].sort((a, b) => a - b) : [];
136
137
  }, [currentDescriptor?.options?.snapPoints]);
137
138
 
138
- const nextHasTransitions = useMemo(() => {
139
- return nextDescriptor
140
- ? hasTransitionsEnabled(nextDescriptor.options, transitionsAlwaysOn)
141
- : false;
142
- }, [nextDescriptor, transitionsAlwaysOn]);
139
+ const nextRouteKey = nextDescriptor?.route?.key;
140
+ const nextHasTransitions =
141
+ !!nextRouteKey &&
142
+ hasTransitionsEnabled(nextDescriptor?.options, transitionsAlwaysOn);
143
143
 
144
144
  const screenInterpolatorProps = useDerivedValue<
145
145
  Omit<ScreenInterpolationProps, "bounds">
@@ -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(
@@ -110,8 +110,6 @@ export const useScreenGestureHandlers = ({
110
110
  canDismiss,
111
111
  handleDismiss,
112
112
  ownershipStatus,
113
- claimedDirections,
114
- ancestorContext,
115
113
  childDirectionClaims,
116
114
  }: UseScreenGestureHandlersProps) => {
117
115
  const dimensions = useWindowDimensions();
@@ -132,6 +130,7 @@ export const useScreenGestureHandlers = ({
132
130
  transitionSpec,
133
131
  snapPoints: rawSnapPoints,
134
132
  expandViaScrollView = true,
133
+ gestureSnapLocked = false,
135
134
  } = current.options;
136
135
 
137
136
  const { hasSnapPoints, snapPoints, minSnapPoint, maxSnapPoint } = useMemo(
@@ -200,11 +199,25 @@ export const useScreenGestureHandlers = ({
200
199
  ? "horizontal"
201
200
  : "vertical";
202
201
 
202
+ const isExpandGesture = (swipeDirection: Direction): boolean => {
203
+ "worklet";
204
+ if (snapAxis === "horizontal") {
205
+ return directions.snapAxisInverted
206
+ ? swipeDirection === "horizontal"
207
+ : swipeDirection === "horizontal-inverted";
208
+ }
209
+
210
+ return directions.snapAxisInverted
211
+ ? swipeDirection === "vertical"
212
+ : swipeDirection === "vertical-inverted";
213
+ };
214
+
203
215
  const initialTouch = useSharedValue({ x: 0, y: 0 });
204
216
  const gestureOffsetState = useSharedValue<GestureOffsetState>(
205
217
  GestureOffsetState.PENDING,
206
218
  );
207
219
  const gestureStartProgress = useSharedValue(1);
220
+ const lockedSnapPoint = useSharedValue(maxSnapPoint);
208
221
 
209
222
  const onTouchesDown = useStableCallbackValue((e: GestureTouchEvent) => {
210
223
  "worklet";
@@ -280,6 +293,15 @@ export const useScreenGestureHandlers = ({
280
293
  return;
281
294
  }
282
295
 
296
+ if (
297
+ hasSnapPoints &&
298
+ gestureSnapLocked &&
299
+ isExpandGesture(swipeDirection)
300
+ ) {
301
+ manager.fail();
302
+ return;
303
+ }
304
+
283
305
  // Snap sheets can interrupt their own animation; non-snap cannot
284
306
  if (!hasSnapPoints && gestureAnimationValues.isDismissing?.value) {
285
307
  return;
@@ -303,23 +325,19 @@ export const useScreenGestureHandlers = ({
303
325
 
304
326
  // Step 7: Expand check for snap sheets
305
327
  if (hasSnapPoints) {
306
- const isExpandGesture =
307
- (directions.snapAxisInverted && swipeDirection === "vertical") ||
308
- (!directions.snapAxisInverted &&
309
- swipeDirection === "vertical-inverted") ||
310
- (directions.snapAxisInverted && swipeDirection === "horizontal") ||
311
- (!directions.snapAxisInverted &&
312
- swipeDirection === "horizontal-inverted");
313
-
314
- if (isExpandGesture) {
328
+ if (isExpandGesture(swipeDirection)) {
315
329
  if (!expandViaScrollView) {
316
330
  manager.fail();
317
331
  return;
318
332
  }
319
333
 
334
+ const effectiveMaxSnapPoint = gestureSnapLocked
335
+ ? lockedSnapPoint.value
336
+ : maxSnapPoint;
337
+
320
338
  const canExpandMore =
321
- animations.progress.value < maxSnapPoint - EPSILON &&
322
- animations.targetProgress.value < maxSnapPoint - EPSILON;
339
+ animations.progress.value < effectiveMaxSnapPoint - EPSILON &&
340
+ animations.targetProgress.value < effectiveMaxSnapPoint - EPSILON;
323
341
 
324
342
  if (!canExpandMore) {
325
343
  manager.fail();
@@ -336,6 +354,24 @@ export const useScreenGestureHandlers = ({
336
354
 
337
355
  const onStart = useStableCallbackValue(() => {
338
356
  "worklet";
357
+ if (hasSnapPoints && gestureSnapLocked) {
358
+ let nearest = snapPoints[0] ?? animations.progress.value;
359
+ let smallestDistance = Math.abs(animations.progress.value - nearest);
360
+
361
+ for (let i = 1; i < snapPoints.length; i++) {
362
+ const point = snapPoints[i];
363
+ const distance = Math.abs(animations.progress.value - point);
364
+ if (distance < smallestDistance) {
365
+ smallestDistance = distance;
366
+ nearest = point;
367
+ }
368
+ }
369
+
370
+ lockedSnapPoint.value = nearest;
371
+ } else {
372
+ lockedSnapPoint.value = maxSnapPoint;
373
+ }
374
+
339
375
  gestureAnimationValues.isDragging.value = TRUE;
340
376
  gestureAnimationValues.isDismissing.value = FALSE;
341
377
  gestureStartProgress.value = animations.progress.value;
@@ -369,10 +405,21 @@ export const useScreenGestureHandlers = ({
369
405
  const baseSign = -1;
370
406
  const sign = directions.snapAxisInverted ? -baseSign : baseSign;
371
407
  const progressDelta = (sign * translation) / dimension;
408
+ const maxProgressForGesture = gestureSnapLocked
409
+ ? lockedSnapPoint.value
410
+ : maxSnapPoint;
411
+ const minProgressForGesture = gestureSnapLocked
412
+ ? canDismiss
413
+ ? 0
414
+ : lockedSnapPoint.value
415
+ : minSnapPoint;
372
416
 
373
417
  animations.progress.value = Math.max(
374
- minSnapPoint,
375
- Math.min(maxSnapPoint, gestureStartProgress.value + progressDelta),
418
+ minProgressForGesture,
419
+ Math.min(
420
+ maxProgressForGesture,
421
+ gestureStartProgress.value + progressDelta,
422
+ ),
376
423
  );
377
424
  } else if (gestureDrivesProgress) {
378
425
  let maxProgress = 0;
@@ -427,7 +474,7 @@ export const useScreenGestureHandlers = ({
427
474
 
428
475
  const result = determineSnapTarget({
429
476
  currentProgress: animations.progress.value,
430
- snapPoints,
477
+ snapPoints: gestureSnapLocked ? [lockedSnapPoint.value] : snapPoints,
431
478
  velocity: snapVelocity,
432
479
  dimension: axisDimension,
433
480
  velocityFactor: snapVelocityImpact,
@@ -17,7 +17,6 @@ import type { LayoutChangeEvent } from "react-native";
17
17
  import { Gesture, type GestureType } from "react-native-gesture-handler";
18
18
  import type { SharedValue } from "react-native-reanimated";
19
19
  import { useAnimatedScrollHandler } from "react-native-reanimated";
20
- import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes";
21
20
  import {
22
21
  type GestureContextType,
23
22
  type ScrollConfig,
@@ -76,7 +75,6 @@ function findGestureOwnersForAxis(
76
75
  }
77
76
 
78
77
  interface ScrollProgressHookProps {
79
- onScroll?: (event: ReanimatedScrollEvent) => void;
80
78
  onContentSizeChange?: (width: number, height: number) => void;
81
79
  onLayout?: (event: LayoutChangeEvent) => void;
82
80
  direction?: "vertical" | "horizontal";
@@ -125,18 +123,22 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
125
123
  const setIsTouched = () => {
126
124
  "worklet";
127
125
  for (const scrollConfig of scrollConfigs) {
128
- if (scrollConfig.value) {
129
- scrollConfig.value = { ...scrollConfig.value, isTouched: true };
130
- }
126
+ scrollConfig.modify((v) => {
127
+ "worklet";
128
+ if (v) v.isTouched = true;
129
+ return v;
130
+ });
131
131
  }
132
132
  };
133
133
 
134
134
  const clearIsTouched = () => {
135
135
  "worklet";
136
136
  for (const scrollConfig of scrollConfigs) {
137
- if (scrollConfig.value) {
138
- scrollConfig.value = { ...scrollConfig.value, isTouched: false };
139
- }
137
+ scrollConfig.modify((v) => {
138
+ "worklet";
139
+ if (v) v.isTouched = false;
140
+ return v;
141
+ });
140
142
  }
141
143
  };
142
144
 
@@ -156,7 +158,6 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
156
158
 
157
159
  const scrollHandler = useAnimatedScrollHandler({
158
160
  onScroll: (event) => {
159
- props.onScroll?.(event);
160
161
  if (scrollConfigs.length === 0) return;
161
162
 
162
163
  const update = (v: any) => {
@@ -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({
@@ -5,16 +5,27 @@ import type { AnimationStoreMap } from "../../stores/animation.store";
5
5
  import { HistoryStore } from "../../stores/history.store";
6
6
  import useStableCallback from "../use-stable-callback";
7
7
 
8
+ function hasSnapPoints(descriptor: BaseDescriptor): boolean {
9
+ const snapPoints = descriptor.options?.snapPoints;
10
+ return Boolean(snapPoints && snapPoints.length > 0);
11
+ }
12
+
8
13
  /**
9
14
  * Check if a screen is a leaf (renders visible content) vs a navigator container.
10
15
  * Navigator containers have nested state with routes.
11
16
  */
12
17
  function isLeafScreen(navigation: BaseDescriptor["navigation"]): boolean {
13
18
  const state = navigation.getState();
14
- const currentRoute = state.routes[state.index];
19
+ const index = state?.index ?? -1;
20
+ const currentRoute = state?.routes?.[index];
21
+ if (!currentRoute) return false;
15
22
  return !("state" in currentRoute);
16
23
  }
17
24
 
25
+ function shouldTrackInHistory(descriptor: BaseDescriptor): boolean {
26
+ return hasSnapPoints(descriptor) || isLeafScreen(descriptor.navigation);
27
+ }
28
+
18
29
  /**
19
30
  * Updates the HistoryStore for navigation history tracking.
20
31
  */
@@ -29,13 +40,13 @@ export function useScreenEvents(
29
40
  // biome-ignore lint/correctness/useExhaustiveDependencies: Must only run once on mount
30
41
  useEffect(() => {
31
42
  // Check on mount (after paint, nested navs initialized)
32
- if (isLeafScreen(current.navigation)) {
43
+ if (shouldTrackInHistory(current)) {
33
44
  HistoryStore.focus(current, navigatorKey);
34
45
  }
35
46
 
36
47
  // Also listen for focus events
37
48
  const unsubscribe = current.navigation.addListener?.("focus", () => {
38
- if (isLeafScreen(current.navigation)) {
49
+ if (shouldTrackInHistory(current)) {
39
50
  HistoryStore.focus(current, navigatorKey);
40
51
  }
41
52
  });
@@ -45,7 +56,7 @@ export function useScreenEvents(
45
56
 
46
57
  // When closing starts, focus previous in history
47
58
  const handleBlur = useStableCallback(() => {
48
- if (previous && isLeafScreen(previous.navigation)) {
59
+ if (previous && shouldTrackInHistory(previous)) {
49
60
  const prevNavigatorKey = previous.navigation.getState()?.key ?? "";
50
61
  HistoryStore.focus(previous, prevNavigatorKey);
51
62
  }
@@ -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,13 +1,13 @@
1
1
  import type { Route } from "@react-navigation/native";
2
- import { useMemo } from "react";
3
- import { useDerivedValue } from "react-native-reanimated";
2
+ import { useCallback, useMemo } from "react";
3
+ import { snapDescriptorToIndex } from "../../animation/snap-to";
4
4
  import {
5
5
  type BaseDescriptor,
6
6
  useKeys,
7
7
  } from "../../providers/screen/keys.provider";
8
8
  import type { ScreenTransitionConfig } from "../../types/screen.types";
9
9
  import type { BaseStackNavigation } from "../../types/stack.types";
10
- import { useSharedValueState } from "../reanimated/use-shared-value-state";
10
+ import { useOptimisticFocusedIndex } from "./use-optimistic-focused-index";
11
11
  import { type StackContextValue, useStack } from "./use-stack";
12
12
 
13
13
  export interface ScreenState<
@@ -47,6 +47,13 @@ export interface ScreenState<
47
47
  * Navigation object for this screen.
48
48
  */
49
49
  navigation: TNavigation;
50
+
51
+ /**
52
+ * Programmatically snap the focused screen to a snap point index.
53
+ *
54
+ * Scoped to this screen's stack context, avoiding global history ambiguity.
55
+ */
56
+ snapTo: (index: number) => void;
50
57
  }
51
58
 
52
59
  /**
@@ -66,17 +73,24 @@ export function useScreenState<
66
73
  [routeKeys, current.route.key],
67
74
  );
68
75
 
69
- const focusedIndex = useSharedValueState(
70
- useDerivedValue(() => {
71
- const globalIndex = optimisticFocusedIndex.get();
72
- return Math.max(0, Math.min(globalIndex, routeKeys.length - 1));
73
- }),
76
+ const focusedIndex = useOptimisticFocusedIndex(
77
+ optimisticFocusedIndex,
78
+ routeKeys.length,
74
79
  );
75
80
 
76
81
  const focusedScene = useMemo(() => {
77
82
  return scenes[focusedIndex] ?? scenes[scenes.length - 1];
78
83
  }, [scenes, focusedIndex]);
79
84
 
85
+ const snapTo = useCallback(
86
+ (targetIndex: number) => {
87
+ const descriptor = focusedScene?.descriptor;
88
+ if (!descriptor) return;
89
+ snapDescriptorToIndex(descriptor, targetIndex);
90
+ },
91
+ [focusedScene],
92
+ );
93
+
80
94
  return useMemo(
81
95
  () => ({
82
96
  index,
@@ -86,6 +100,7 @@ export function useScreenState<
86
100
  focusedIndex,
87
101
  meta: focusedScene?.descriptor?.options?.meta,
88
102
  navigation: current.navigation as TNavigation,
103
+ snapTo,
89
104
  }),
90
105
  [
91
106
  index,
@@ -94,6 +109,7 @@ export function useScreenState<
94
109
  focusedIndex,
95
110
  current.navigation,
96
111
  current.route,
112
+ snapTo,
97
113
  ],
98
114
  );
99
115
  }
@@ -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,