react-native-screen-transitions 3.2.1 → 3.3.0-beta.1

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 (173) hide show
  1. package/README.md +105 -10
  2. package/lib/commonjs/blank-stack/components/adjusted-screen.js +2 -2
  3. package/lib/commonjs/blank-stack/components/adjusted-screen.js.map +1 -1
  4. package/lib/commonjs/shared/components/create-transition-aware-component.js +8 -2
  5. package/lib/commonjs/shared/components/create-transition-aware-component.js.map +1 -1
  6. package/lib/commonjs/shared/components/{root-transition-aware.js → screen-container.js} +28 -12
  7. package/lib/commonjs/shared/components/screen-container.js.map +1 -0
  8. package/lib/commonjs/shared/configs/presets.js +3 -3
  9. package/lib/commonjs/shared/configs/presets.js.map +1 -1
  10. package/lib/commonjs/shared/configs/specs.js +6 -1
  11. package/lib/commonjs/shared/configs/specs.js.map +1 -1
  12. package/lib/commonjs/shared/constants.js +36 -10
  13. package/lib/commonjs/shared/constants.js.map +1 -1
  14. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +25 -18
  15. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  16. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +25 -202
  17. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  18. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js +342 -0
  19. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -0
  20. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +47 -4
  21. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  22. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js +3 -3
  23. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js.map +1 -1
  24. package/lib/commonjs/shared/hooks/lifecycle/use-open-transition.js +25 -3
  25. package/lib/commonjs/shared/hooks/lifecycle/use-open-transition.js.map +1 -1
  26. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js +33 -2
  27. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js.map +1 -1
  28. package/lib/commonjs/shared/hooks/use-backdrop-pointer-events.js +32 -0
  29. package/lib/commonjs/shared/hooks/use-backdrop-pointer-events.js.map +1 -0
  30. package/lib/commonjs/shared/providers/gestures.provider.js +4 -2
  31. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  32. package/lib/commonjs/shared/providers/screen/screen-composer.js +2 -2
  33. package/lib/commonjs/shared/providers/screen/screen-composer.js.map +1 -1
  34. package/lib/commonjs/shared/utils/animation/{start-screen-transition.js → animate-to-progress.js} +16 -8
  35. package/lib/commonjs/shared/utils/animation/animate-to-progress.js.map +1 -0
  36. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js +138 -0
  37. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  38. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js +56 -0
  39. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js.map +1 -0
  40. package/lib/commonjs/shared/utils/gesture/validate-snap-points.js +31 -0
  41. package/lib/commonjs/shared/utils/gesture/validate-snap-points.js.map +1 -0
  42. package/lib/commonjs/shared/utils/gesture/velocity.js +11 -0
  43. package/lib/commonjs/shared/utils/gesture/velocity.js.map +1 -1
  44. package/lib/commonjs/shared/utils/logger.js +22 -0
  45. package/lib/commonjs/shared/utils/logger.js.map +1 -0
  46. package/lib/module/blank-stack/components/adjusted-screen.js +1 -1
  47. package/lib/module/blank-stack/components/adjusted-screen.js.map +1 -1
  48. package/lib/module/shared/components/create-transition-aware-component.js +8 -2
  49. package/lib/module/shared/components/create-transition-aware-component.js.map +1 -1
  50. package/lib/module/shared/components/screen-container.js +64 -0
  51. package/lib/module/shared/components/screen-container.js.map +1 -0
  52. package/lib/module/shared/configs/presets.js +3 -3
  53. package/lib/module/shared/configs/presets.js.map +1 -1
  54. package/lib/module/shared/configs/specs.js +5 -0
  55. package/lib/module/shared/configs/specs.js.map +1 -1
  56. package/lib/module/shared/constants.js +34 -9
  57. package/lib/module/shared/constants.js.map +1 -1
  58. package/lib/module/shared/hooks/animation/use-screen-animation.js +25 -18
  59. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  60. package/lib/module/shared/hooks/gestures/use-build-gestures.js +25 -201
  61. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  62. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js +336 -0
  63. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -0
  64. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +47 -4
  65. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  66. package/lib/module/shared/hooks/lifecycle/use-close-transition.js +3 -3
  67. package/lib/module/shared/hooks/lifecycle/use-close-transition.js.map +1 -1
  68. package/lib/module/shared/hooks/lifecycle/use-open-transition.js +25 -3
  69. package/lib/module/shared/hooks/lifecycle/use-open-transition.js.map +1 -1
  70. package/lib/module/shared/hooks/navigation/use-screen-state.js +35 -4
  71. package/lib/module/shared/hooks/navigation/use-screen-state.js.map +1 -1
  72. package/lib/module/shared/hooks/use-backdrop-pointer-events.js +28 -0
  73. package/lib/module/shared/hooks/use-backdrop-pointer-events.js.map +1 -0
  74. package/lib/module/shared/providers/gestures.provider.js +4 -2
  75. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  76. package/lib/module/shared/providers/screen/screen-composer.js +2 -2
  77. package/lib/module/shared/providers/screen/screen-composer.js.map +1 -1
  78. package/lib/module/shared/utils/animation/{start-screen-transition.js → animate-to-progress.js} +14 -6
  79. package/lib/module/shared/utils/animation/animate-to-progress.js.map +1 -0
  80. package/lib/module/shared/utils/gesture/check-gesture-activation.js +137 -0
  81. package/lib/module/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  82. package/lib/module/shared/utils/gesture/determine-snap-target.js +52 -0
  83. package/lib/module/shared/utils/gesture/determine-snap-target.js.map +1 -0
  84. package/lib/module/shared/utils/gesture/validate-snap-points.js +26 -0
  85. package/lib/module/shared/utils/gesture/validate-snap-points.js.map +1 -0
  86. package/lib/module/shared/utils/gesture/velocity.js +11 -0
  87. package/lib/module/shared/utils/gesture/velocity.js.map +1 -1
  88. package/lib/module/shared/utils/logger.js +17 -0
  89. package/lib/module/shared/utils/logger.js.map +1 -0
  90. package/lib/typescript/blank-stack/components/adjusted-screen.d.ts.map +1 -1
  91. package/lib/typescript/shared/components/create-transition-aware-component.d.ts.map +1 -1
  92. package/lib/typescript/shared/components/screen-container.d.ts +6 -0
  93. package/lib/typescript/shared/components/screen-container.d.ts.map +1 -0
  94. package/lib/typescript/shared/configs/specs.d.ts +1 -0
  95. package/lib/typescript/shared/configs/specs.d.ts.map +1 -1
  96. package/lib/typescript/shared/constants.d.ts +9 -0
  97. package/lib/typescript/shared/constants.d.ts.map +1 -1
  98. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  99. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +1 -1
  100. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  101. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts +19 -0
  102. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts.map +1 -0
  103. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts +5 -1
  104. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
  105. package/lib/typescript/shared/hooks/lifecycle/use-open-transition.d.ts.map +1 -1
  106. package/lib/typescript/shared/hooks/navigation/use-screen-state.d.ts +7 -0
  107. package/lib/typescript/shared/hooks/navigation/use-screen-state.d.ts.map +1 -1
  108. package/lib/typescript/shared/hooks/use-backdrop-pointer-events.d.ts +15 -0
  109. package/lib/typescript/shared/hooks/use-backdrop-pointer-events.d.ts.map +1 -0
  110. package/lib/typescript/shared/providers/gestures.provider.d.ts +1 -0
  111. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  112. package/lib/typescript/shared/types/animation.types.d.ts +37 -2
  113. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  114. package/lib/typescript/shared/types/screen.types.d.ts +26 -0
  115. package/lib/typescript/shared/types/screen.types.d.ts.map +1 -1
  116. package/lib/typescript/shared/utils/animation/animate-to-progress.d.ts +19 -0
  117. package/lib/typescript/shared/utils/animation/animate-to-progress.d.ts.map +1 -0
  118. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts +23 -0
  119. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts.map +1 -1
  120. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts +26 -0
  121. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts.map +1 -0
  122. package/lib/typescript/shared/utils/gesture/validate-snap-points.d.ts +13 -0
  123. package/lib/typescript/shared/utils/gesture/validate-snap-points.d.ts.map +1 -0
  124. package/lib/typescript/shared/utils/gesture/velocity.d.ts +1 -0
  125. package/lib/typescript/shared/utils/gesture/velocity.d.ts.map +1 -1
  126. package/lib/typescript/shared/utils/logger.d.ts +6 -0
  127. package/lib/typescript/shared/utils/logger.d.ts.map +1 -0
  128. package/package.json +3 -2
  129. package/src/blank-stack/components/adjusted-screen.tsx +1 -1
  130. package/src/shared/__tests__/derivations.test.ts +1 -0
  131. package/src/shared/__tests__/determine-snap-target.test.ts +268 -0
  132. package/src/shared/__tests__/gesture-activation.test.ts +220 -0
  133. package/src/shared/__tests__/validate-snap-points.test.ts +125 -0
  134. package/src/shared/components/create-transition-aware-component.tsx +11 -1
  135. package/src/shared/components/screen-container.tsx +65 -0
  136. package/src/shared/configs/presets.ts +3 -3
  137. package/src/shared/configs/specs.ts +6 -0
  138. package/src/shared/constants.ts +36 -9
  139. package/src/shared/hooks/animation/use-screen-animation.tsx +32 -21
  140. package/src/shared/hooks/gestures/use-build-gestures.tsx +23 -275
  141. package/src/shared/hooks/gestures/use-screen-gesture-handlers.ts +434 -0
  142. package/src/shared/hooks/gestures/use-scroll-registry.tsx +52 -1
  143. package/src/shared/hooks/lifecycle/use-close-transition.ts +3 -3
  144. package/src/shared/hooks/lifecycle/use-open-transition.ts +27 -3
  145. package/src/shared/hooks/navigation/use-screen-state.tsx +59 -2
  146. package/src/shared/hooks/use-backdrop-pointer-events.ts +32 -0
  147. package/src/shared/providers/gestures.provider.tsx +3 -2
  148. package/src/shared/providers/screen/screen-composer.tsx +2 -2
  149. package/src/shared/types/animation.types.ts +39 -2
  150. package/src/shared/types/screen.types.ts +29 -0
  151. package/src/shared/utils/animation/{start-screen-transition.ts → animate-to-progress.ts} +23 -8
  152. package/src/shared/utils/gesture/check-gesture-activation.ts +129 -0
  153. package/src/shared/utils/gesture/determine-snap-target.ts +75 -0
  154. package/src/shared/utils/gesture/validate-snap-points.ts +37 -0
  155. package/src/shared/utils/gesture/velocity.ts +10 -0
  156. package/src/shared/utils/logger.ts +15 -0
  157. package/lib/commonjs/shared/components/root-transition-aware.js.map +0 -1
  158. package/lib/commonjs/shared/hooks/use-stack-pointer-events.js +0 -23
  159. package/lib/commonjs/shared/hooks/use-stack-pointer-events.js.map +0 -1
  160. package/lib/commonjs/shared/utils/animation/start-screen-transition.js.map +0 -1
  161. package/lib/module/shared/components/root-transition-aware.js +0 -48
  162. package/lib/module/shared/components/root-transition-aware.js.map +0 -1
  163. package/lib/module/shared/hooks/use-stack-pointer-events.js +0 -20
  164. package/lib/module/shared/hooks/use-stack-pointer-events.js.map +0 -1
  165. package/lib/module/shared/utils/animation/start-screen-transition.js.map +0 -1
  166. package/lib/typescript/shared/components/root-transition-aware.d.ts +0 -6
  167. package/lib/typescript/shared/components/root-transition-aware.d.ts.map +0 -1
  168. package/lib/typescript/shared/hooks/use-stack-pointer-events.d.ts +0 -10
  169. package/lib/typescript/shared/hooks/use-stack-pointer-events.d.ts.map +0 -1
  170. package/lib/typescript/shared/utils/animation/start-screen-transition.d.ts +0 -13
  171. package/lib/typescript/shared/utils/animation/start-screen-transition.d.ts.map +0 -1
  172. package/src/shared/components/root-transition-aware.tsx +0 -49
  173. package/src/shared/hooks/use-stack-pointer-events.ts +0 -15
@@ -0,0 +1,434 @@
1
+ import { useMemo } from "react";
2
+ import { useWindowDimensions } from "react-native";
3
+ import type {
4
+ GestureStateChangeEvent,
5
+ GestureTouchEvent,
6
+ GestureUpdateEvent,
7
+ PanGestureHandlerEventPayload,
8
+ } from "react-native-gesture-handler";
9
+ import type { GestureStateManagerType } from "react-native-gesture-handler/lib/typescript/handlers/gestures/gestureStateManager";
10
+ import { type SharedValue, useSharedValue } from "react-native-reanimated";
11
+ import { DefaultSnapSpec } from "../../configs/specs";
12
+ import {
13
+ DEFAULT_GESTURE_ACTIVATION_AREA,
14
+ DEFAULT_GESTURE_DIRECTION,
15
+ DEFAULT_GESTURE_DRIVES_PROGRESS,
16
+ EPSILON,
17
+ FALSE,
18
+ GESTURE_VELOCITY_IMPACT,
19
+ TRUE,
20
+ } from "../../constants";
21
+ import type { ScrollConfig } from "../../providers/gestures.provider";
22
+ import { useKeys } from "../../providers/screen/keys.provider";
23
+ import { AnimationStore } from "../../stores/animation.store";
24
+ import { GestureStore } from "../../stores/gesture.store";
25
+ import { GestureOffsetState } from "../../types/gesture.types";
26
+ import { animateToProgress } from "../../utils/animation/animate-to-progress";
27
+ import {
28
+ applyOffsetRules,
29
+ checkScrollAwareActivation,
30
+ } from "../../utils/gesture/check-gesture-activation";
31
+ import { determineDismissal } from "../../utils/gesture/determine-dismissal";
32
+ import { determineSnapTarget } from "../../utils/gesture/determine-snap-target";
33
+ import { mapGestureToProgress } from "../../utils/gesture/map-gesture-to-progress";
34
+ import { resetGestureValues } from "../../utils/gesture/reset-gesture-values";
35
+ import { validateSnapPoints } from "../../utils/gesture/validate-snap-points";
36
+ import { velocity } from "../../utils/gesture/velocity";
37
+ import { logger } from "../../utils/logger";
38
+ import useStableCallbackValue from "../use-stable-callback-value";
39
+
40
+ interface UseScreenGestureHandlersProps {
41
+ scrollConfig: SharedValue<ScrollConfig | null>;
42
+ ancestorIsDismissing?: SharedValue<number> | null;
43
+ canDismiss: boolean;
44
+ handleDismiss: () => void;
45
+ }
46
+
47
+ export const useScreenGestureHandlers = ({
48
+ scrollConfig,
49
+ ancestorIsDismissing,
50
+ canDismiss,
51
+ handleDismiss,
52
+ }: UseScreenGestureHandlersProps) => {
53
+ const dimensions = useWindowDimensions();
54
+ const { current } = useKeys();
55
+
56
+ const animations = AnimationStore.getAll(current.route.key);
57
+ const gestureAnimationValues = GestureStore.getRouteGestures(
58
+ current.route.key,
59
+ );
60
+
61
+ const {
62
+ gestureDirection = DEFAULT_GESTURE_DIRECTION,
63
+ gestureDrivesProgress = DEFAULT_GESTURE_DRIVES_PROGRESS,
64
+ gestureVelocityImpact = GESTURE_VELOCITY_IMPACT,
65
+ gestureActivationArea = DEFAULT_GESTURE_ACTIVATION_AREA,
66
+ gestureResponseDistance,
67
+ transitionSpec,
68
+ snapPoints: rawSnapPoints,
69
+ } = current.options;
70
+
71
+ const { hasSnapPoints, snapPoints, minSnapPoint, maxSnapPoint } = useMemo(
72
+ () => validateSnapPoints({ snapPoints: rawSnapPoints, canDismiss }),
73
+ [rawSnapPoints, canDismiss],
74
+ );
75
+
76
+ const directions = useMemo(() => {
77
+ if (hasSnapPoints && Array.isArray(gestureDirection)) {
78
+ /**
79
+ * Unsure if this behavior will change in the future, as I cannot find a use case as to why
80
+ * you would want multiple gesture dismisals for a sheet.
81
+ *
82
+ * e.g. When defining a snap point with a gesture of vertical ( default ), the system
83
+ * assumes that the inverse ( vertical-inverted ), will grow the sheet.
84
+ */
85
+ logger.warn(
86
+ `gestureDirection array is not supported with snapPoints. ` +
87
+ `Only the first direction "${gestureDirection[0]}" will be used. ` +
88
+ `Snap points define a single axis of movement, so only one gesture direction is needed.`,
89
+ );
90
+ }
91
+
92
+ // When snap points are defined, use only the first direction from the array
93
+ const effectiveDirection = hasSnapPoints
94
+ ? Array.isArray(gestureDirection)
95
+ ? gestureDirection[0]
96
+ : gestureDirection
97
+ : gestureDirection;
98
+
99
+ const directionsArray = Array.isArray(effectiveDirection)
100
+ ? effectiveDirection
101
+ : [effectiveDirection];
102
+
103
+ const isBidirectional = directionsArray.includes("bidirectional");
104
+
105
+ const hasHorizontalDirection =
106
+ directionsArray.includes("horizontal") ||
107
+ directionsArray.includes("horizontal-inverted");
108
+
109
+ const isSnapAxisInverted = hasHorizontalDirection
110
+ ? directionsArray.includes("horizontal-inverted") &&
111
+ !directionsArray.includes("horizontal")
112
+ : directionsArray.includes("vertical-inverted") &&
113
+ !directionsArray.includes("vertical");
114
+
115
+ const enableBothVertical =
116
+ isBidirectional || (hasSnapPoints && !hasHorizontalDirection);
117
+ const enableBothHorizontal =
118
+ isBidirectional || (hasSnapPoints && hasHorizontalDirection);
119
+
120
+ return {
121
+ vertical: directionsArray.includes("vertical") || enableBothVertical,
122
+ verticalInverted:
123
+ directionsArray.includes("vertical-inverted") || enableBothVertical,
124
+ horizontal:
125
+ directionsArray.includes("horizontal") || enableBothHorizontal,
126
+ horizontalInverted:
127
+ directionsArray.includes("horizontal-inverted") || enableBothHorizontal,
128
+ snapAxisInverted: hasSnapPoints && isSnapAxisInverted,
129
+ };
130
+ }, [gestureDirection, hasSnapPoints]);
131
+
132
+ const snapAxis =
133
+ directions.horizontal || directions.horizontalInverted
134
+ ? "horizontal"
135
+ : "vertical";
136
+
137
+ const initialTouch = useSharedValue({ x: 0, y: 0 });
138
+ const gestureOffsetState = useSharedValue<GestureOffsetState>(
139
+ GestureOffsetState.PENDING,
140
+ );
141
+ const gestureStartProgress = useSharedValue(1);
142
+
143
+ const onTouchesDown = useStableCallbackValue((e: GestureTouchEvent) => {
144
+ "worklet";
145
+ const firstTouch = e.changedTouches[0];
146
+ initialTouch.value = { x: firstTouch.x, y: firstTouch.y };
147
+ gestureOffsetState.value = GestureOffsetState.PENDING;
148
+ });
149
+
150
+ const onTouchesMove = useStableCallbackValue(
151
+ (e: GestureTouchEvent, manager: GestureStateManagerType) => {
152
+ "worklet";
153
+
154
+ // If an ancestor navigator is already dismissing via gesture, block new gestures here.
155
+ if (ancestorIsDismissing?.value) {
156
+ gestureOffsetState.value = GestureOffsetState.FAILED;
157
+ manager.fail();
158
+ return;
159
+ }
160
+
161
+ const touch = e.changedTouches[0];
162
+
163
+ const { isSwipingDown, isSwipingUp, isSwipingRight, isSwipingLeft } =
164
+ applyOffsetRules({
165
+ touch,
166
+ directions,
167
+ manager,
168
+ dimensions,
169
+ gestureOffsetState,
170
+ initialTouch: initialTouch.value,
171
+ activationArea: gestureActivationArea,
172
+ responseDistance: gestureResponseDistance,
173
+ });
174
+
175
+ if (gestureOffsetState.value === GestureOffsetState.FAILED) {
176
+ manager.fail();
177
+ return;
178
+ }
179
+
180
+ // Keep pending until thresholds are met; no eager activation.
181
+ if (gestureAnimationValues.isDragging?.value) {
182
+ manager.activate();
183
+ return;
184
+ }
185
+
186
+ const recognizedDirection =
187
+ isSwipingDown || isSwipingUp || isSwipingRight || isSwipingLeft;
188
+
189
+ const scrollCfg = scrollConfig.value;
190
+ const isTouchingScrollView = scrollCfg?.isTouched ?? false;
191
+
192
+ if (!isTouchingScrollView) {
193
+ // Early return if gesture hasn't met activation criteria
194
+ const canActivate =
195
+ recognizedDirection &&
196
+ gestureOffsetState.value === GestureOffsetState.PASSED &&
197
+ !gestureAnimationValues.isDismissing?.value;
198
+
199
+ if (!canActivate) {
200
+ return;
201
+ }
202
+
203
+ if (isSwipingDown) {
204
+ gestureAnimationValues.direction.value = "vertical";
205
+ } else if (isSwipingUp) {
206
+ gestureAnimationValues.direction.value = "vertical-inverted";
207
+ } else if (isSwipingRight) {
208
+ gestureAnimationValues.direction.value = "horizontal";
209
+ } else if (isSwipingLeft) {
210
+ gestureAnimationValues.direction.value = "horizontal-inverted";
211
+ }
212
+
213
+ manager.activate();
214
+ return;
215
+ }
216
+
217
+ // Touch IS on ScrollView - apply scroll-aware rules
218
+ // Snap mode: determine if sheet can still expand
219
+ const canExpandMore =
220
+ hasSnapPoints && animations.progress.value < maxSnapPoint - EPSILON;
221
+
222
+ const { shouldActivate, direction: activatedDirection } =
223
+ checkScrollAwareActivation({
224
+ swipeInfo: {
225
+ isSwipingDown,
226
+ isSwipingUp,
227
+ isSwipingRight,
228
+ isSwipingLeft,
229
+ },
230
+ directions,
231
+ scrollConfig: scrollCfg,
232
+ hasSnapPoints,
233
+ canExpandMore,
234
+ });
235
+
236
+ if (recognizedDirection && !shouldActivate) {
237
+ manager.fail();
238
+ return;
239
+ }
240
+
241
+ if (
242
+ shouldActivate &&
243
+ gestureOffsetState.value === GestureOffsetState.PASSED &&
244
+ !gestureAnimationValues.isDismissing?.value
245
+ ) {
246
+ gestureAnimationValues.direction.value = activatedDirection;
247
+ manager.activate();
248
+ return;
249
+ }
250
+ },
251
+ );
252
+
253
+ const onStart = useStableCallbackValue(() => {
254
+ "worklet";
255
+ gestureAnimationValues.isDragging.value = TRUE;
256
+ gestureAnimationValues.isDismissing.value = FALSE;
257
+ gestureStartProgress.value = animations.progress.value;
258
+ animations.animating.value = TRUE;
259
+ });
260
+
261
+ const onUpdate = useStableCallbackValue(
262
+ (event: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
263
+ "worklet";
264
+
265
+ const { translationX, translationY } = event;
266
+ const { width, height } = dimensions;
267
+
268
+ gestureAnimationValues.x.value = translationX;
269
+ gestureAnimationValues.y.value = translationY;
270
+ gestureAnimationValues.normalizedX.value = velocity.normalizeTranslation(
271
+ translationX,
272
+ width,
273
+ );
274
+ gestureAnimationValues.normalizedY.value = velocity.normalizeTranslation(
275
+ translationY,
276
+ height,
277
+ );
278
+
279
+ if (hasSnapPoints && gestureDrivesProgress) {
280
+ const isHorizontal = snapAxis === "horizontal";
281
+ const translation = isHorizontal ? translationX : translationY;
282
+ const dimension = isHorizontal ? width : height;
283
+
284
+ // Map translation to progress delta:
285
+ // - Positive translation (down/right) = decrease progress (dismiss)
286
+ // - Negative translation (up/left) = increase progress (expand)
287
+ // Inverted directions flip this behavior
288
+ const baseSign = -1;
289
+ const sign = directions.snapAxisInverted ? -baseSign : baseSign;
290
+ const progressDelta = (sign * translation) / dimension;
291
+
292
+ // Use pre-computed bounds (minSnapPoint already accounts for canDismiss)
293
+ animations.progress.value = Math.max(
294
+ minSnapPoint,
295
+ Math.min(maxSnapPoint, gestureStartProgress.value + progressDelta),
296
+ );
297
+ } else if (gestureDrivesProgress) {
298
+ let maxProgress = 0;
299
+
300
+ // Horizontal swipe right (positive X)
301
+ if (directions.horizontal && translationX > 0) {
302
+ const progress = mapGestureToProgress(translationX, width);
303
+ maxProgress = Math.max(maxProgress, progress);
304
+ }
305
+
306
+ // Horizontal inverted swipe left (negative X)
307
+ if (directions.horizontalInverted && translationX < 0) {
308
+ const progress = mapGestureToProgress(-translationX, width);
309
+ maxProgress = Math.max(maxProgress, progress);
310
+ }
311
+
312
+ // Vertical swipe down (positive Y)
313
+ if (directions.vertical && translationY > 0) {
314
+ const progress = mapGestureToProgress(translationY, height);
315
+ maxProgress = Math.max(maxProgress, progress);
316
+ }
317
+
318
+ // Vertical inverted swipe up (negative Y)
319
+ if (directions.verticalInverted && translationY < 0) {
320
+ const progress = mapGestureToProgress(-translationY, height);
321
+ maxProgress = Math.max(maxProgress, progress);
322
+ }
323
+
324
+ animations.progress.value = Math.max(
325
+ 0,
326
+ Math.min(1, gestureStartProgress.value - maxProgress),
327
+ );
328
+ }
329
+ },
330
+ );
331
+
332
+ const onEnd = useStableCallbackValue(
333
+ (event: GestureStateChangeEvent<PanGestureHandlerEventPayload>) => {
334
+ "worklet";
335
+
336
+ if (hasSnapPoints) {
337
+ const isHorizontal = snapAxis === "horizontal";
338
+ const axisVelocity = isHorizontal ? event.velocityX : event.velocityY;
339
+ const axisDimension = isHorizontal
340
+ ? dimensions.width
341
+ : dimensions.height;
342
+
343
+ // determineSnapTarget expects positive velocity = toward dismiss (decreasing progress)
344
+ // Positive velocity (down/right) = dismiss for non-inverted
345
+ // Inverted directions need velocity flipped
346
+ const snapVelocity = directions.snapAxisInverted
347
+ ? -axisVelocity
348
+ : axisVelocity;
349
+
350
+ const result = determineSnapTarget({
351
+ currentProgress: animations.progress.value,
352
+ snapPoints,
353
+ velocity: snapVelocity,
354
+ dimension: axisDimension,
355
+ canDismiss: canDismiss,
356
+ });
357
+
358
+ const shouldDismiss = result.shouldDismiss;
359
+ const targetProgress = result.targetProgress;
360
+ const isSnapping = !shouldDismiss;
361
+
362
+ const spec = shouldDismiss
363
+ ? transitionSpec?.close
364
+ : transitionSpec?.open;
365
+
366
+ const effectiveSpec = isSnapping
367
+ ? {
368
+ open: transitionSpec?.expand ?? DefaultSnapSpec,
369
+ close: transitionSpec?.collapse ?? DefaultSnapSpec,
370
+ }
371
+ : transitionSpec;
372
+
373
+ resetGestureValues({
374
+ spec,
375
+ gestures: gestureAnimationValues,
376
+ shouldDismiss,
377
+ event,
378
+ dimensions,
379
+ });
380
+
381
+ // For snap transitions, velocity should match gesture direction
382
+ // Positive gesture velocity (down/right) = collapsing (negative progress velocity)
383
+ // Inverted directions flip this
384
+ const velocitySign = directions.snapAxisInverted ? 1 : -1;
385
+ const initialVelocity =
386
+ velocitySign * velocity.normalize(axisVelocity, axisDimension);
387
+
388
+ animateToProgress({
389
+ target: targetProgress,
390
+ onAnimationFinish: shouldDismiss ? handleDismiss : undefined,
391
+ spec: effectiveSpec,
392
+ animations,
393
+ initialVelocity,
394
+ });
395
+ } else {
396
+ const result = determineDismissal({
397
+ event,
398
+ directions,
399
+ dimensions,
400
+ gestureVelocityImpact,
401
+ });
402
+
403
+ const shouldDismiss = result.shouldDismiss;
404
+ const targetProgress = shouldDismiss ? 0 : gestureStartProgress.value;
405
+
406
+ resetGestureValues({
407
+ spec: shouldDismiss ? transitionSpec?.close : transitionSpec?.open,
408
+ gestures: gestureAnimationValues,
409
+ shouldDismiss,
410
+ event,
411
+ dimensions,
412
+ });
413
+
414
+ const initialVelocity = velocity.calculateProgressVelocity({
415
+ animations,
416
+ shouldDismiss,
417
+ event,
418
+ dimensions,
419
+ directions,
420
+ });
421
+
422
+ animateToProgress({
423
+ target: targetProgress,
424
+ onAnimationFinish: shouldDismiss ? handleDismiss : undefined,
425
+ spec: transitionSpec,
426
+ animations,
427
+ initialVelocity,
428
+ });
429
+ }
430
+ },
431
+ );
432
+
433
+ return { onTouchesDown, onTouchesMove, onStart, onUpdate, onEnd };
434
+ };
@@ -1,7 +1,7 @@
1
1
  /** biome-ignore-all lint/style/noNonNullAssertion: <Will always consume context from GestureProvider> */
2
2
 
3
3
  import { useMemo } from "react";
4
- import type { LayoutChangeEvent } from "react-native";
4
+ import type { GestureResponderEvent, LayoutChangeEvent } from "react-native";
5
5
  import { useAnimatedScrollHandler } from "react-native-reanimated";
6
6
  import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes";
7
7
  import { useGestureContext } from "../../providers/gestures.provider";
@@ -11,6 +11,8 @@ interface ScrollProgressHookProps {
11
11
  onScroll?: (event: ReanimatedScrollEvent) => void;
12
12
  onContentSizeChange?: (width: number, height: number) => void;
13
13
  onLayout?: (event: LayoutChangeEvent) => void;
14
+ onTouchStart?: (event: GestureResponderEvent) => void;
15
+ onTouchEnd?: (event: GestureResponderEvent) => void;
14
16
  }
15
17
 
16
18
  export const useScrollRegistry = (props: ScrollProgressHookProps) => {
@@ -43,6 +45,7 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
43
45
  contentWidth: 0,
44
46
  layoutHeight: 0,
45
47
  layoutWidth: 0,
48
+ isTouched: true,
46
49
  };
47
50
  }
48
51
  v.x = event.contentOffset.x;
@@ -73,6 +76,7 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
73
76
  layoutWidth: 0,
74
77
  contentWidth: width,
75
78
  contentHeight: height,
79
+ isTouched: false,
76
80
  };
77
81
  }
78
82
  v.contentWidth = width;
@@ -102,6 +106,7 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
102
106
  contentWidth: 0,
103
107
  layoutHeight: height,
104
108
  layoutWidth: width,
109
+ isTouched: false,
105
110
  };
106
111
  }
107
112
  v.layoutHeight = height;
@@ -116,9 +121,55 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
116
121
  }
117
122
  });
118
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
+ }
166
+ });
167
+
119
168
  return {
120
169
  scrollHandler,
121
170
  onContentSizeChange,
122
171
  onLayout,
172
+ onTouchStart,
173
+ onTouchEnd,
123
174
  };
124
175
  };
@@ -10,7 +10,7 @@ import { useStackCoreContext } from "../../providers/stack/core.provider";
10
10
  import { useManagedStackContext } from "../../providers/stack/managed.provider";
11
11
  import type { AnimationStoreMap } from "../../stores/animation.store";
12
12
  import { StackType } from "../../types/stack.types";
13
- import { startScreenTransition } from "../../utils/animation/start-screen-transition";
13
+ import { animateToProgress } from "../../utils/animation/animate-to-progress";
14
14
  import { resetStoresForScreen } from "../../utils/reset-stores-for-screen";
15
15
  import { useSharedValueState } from "../reanimated/use-shared-value-state";
16
16
  import useStableCallback from "../use-stable-callback";
@@ -49,7 +49,7 @@ const useManagedClose = ({
49
49
  if (!keys?.includes(current.route.key)) return;
50
50
 
51
51
  runOnJS(activate)();
52
- startScreenTransition({
52
+ animateToProgress({
53
53
  target: "close",
54
54
  spec: current.options.transitionSpec,
55
55
  animations,
@@ -96,7 +96,7 @@ const useNativeStackClose = ({
96
96
  e.preventDefault();
97
97
  activate();
98
98
 
99
- startScreenTransition({
99
+ animateToProgress({
100
100
  target: "close",
101
101
  spec: current.options.transitionSpec,
102
102
  animations,
@@ -1,9 +1,30 @@
1
1
  import { useLayoutEffect } from "react";
2
2
  import type { BaseDescriptor } from "../../providers/screen/keys.provider";
3
3
  import type { AnimationStoreMap } from "../../stores/animation.store";
4
- import { startScreenTransition } from "../../utils/animation/start-screen-transition";
4
+ import { animateToProgress } from "../../utils/animation/animate-to-progress";
5
5
  import { useHighRefreshRate } from "../animation/use-high-refresh-rate";
6
6
 
7
+ /**
8
+ * Calculates the initial progress value based on snap points configuration.
9
+ */
10
+ function getInitialProgress({
11
+ snapPoints,
12
+ initialSnapIndex,
13
+ }: {
14
+ snapPoints?: number[];
15
+ initialSnapIndex: number;
16
+ }): number | undefined {
17
+ if (!snapPoints) {
18
+ return undefined;
19
+ }
20
+
21
+ const clampedIndex = Math.min(
22
+ Math.max(0, initialSnapIndex),
23
+ snapPoints.length - 1,
24
+ );
25
+ return snapPoints[clampedIndex];
26
+ }
27
+
7
28
  /**
8
29
  * Handles opening animation on mount.
9
30
  * Returns activate/deactivate functions for high refresh rate.
@@ -17,9 +38,12 @@ export function useOpenTransition(
17
38
 
18
39
  // biome-ignore lint/correctness/useExhaustiveDependencies: Must only run once on mount
19
40
  useLayoutEffect(() => {
41
+ const { snapPoints, initialSnapIndex = 0 } = current.options;
42
+ const targetProgress = getInitialProgress({ snapPoints, initialSnapIndex });
43
+
20
44
  activateHighRefreshRate();
21
- startScreenTransition({
22
- target: "open",
45
+ animateToProgress({
46
+ target: targetProgress ?? "open",
23
47
  spec: current.options.transitionSpec,
24
48
  animations,
25
49
  onAnimationFinish: deactivateHighRefreshRate,
@@ -1,12 +1,15 @@
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 { runOnUI, useDerivedValue } from "react-native-reanimated";
4
+ import { DefaultSnapSpec } from "../../configs/specs";
4
5
  import {
5
6
  type BaseDescriptor,
6
7
  useKeys,
7
8
  } from "../../providers/screen/keys.provider";
9
+ import { AnimationStore } from "../../stores/animation.store";
8
10
  import type { ScreenTransitionConfig } from "../../types/screen.types";
9
11
  import type { BaseStackNavigation } from "../../types/stack.types";
12
+ import { animateToProgress } from "../../utils/animation/animate-to-progress";
10
13
  import { useSharedValueState } from "../reanimated/use-shared-value-state";
11
14
  import { type StackContextValue, useStack } from "./use-stack";
12
15
 
@@ -47,6 +50,14 @@ export interface ScreenState<
47
50
  * Navigation object for this screen.
48
51
  */
49
52
  navigation: TNavigation;
53
+
54
+ /**
55
+ * Programmatically snap to a specific snap point by index.
56
+ * Only works if the screen has snapPoints defined.
57
+ *
58
+ * @param index - The index of the snap point to snap to (0-based)
59
+ */
60
+ snapTo: (index: number) => void;
50
61
  }
51
62
 
52
63
  /**
@@ -77,6 +88,50 @@ export function useScreenState<
77
88
  return scenes[focusedIndex] ?? scenes[scenes.length - 1];
78
89
  }, [scenes, focusedIndex]);
79
90
 
91
+ const currentOptions = current.options;
92
+ const animations = useMemo(
93
+ () => AnimationStore.getAll(current.route.key),
94
+ [current.route.key],
95
+ );
96
+
97
+ const snapTo = useCallback(
98
+ (targetIndex: number) => {
99
+ const points = currentOptions?.snapPoints;
100
+ if (!points || points.length === 0) {
101
+ console.warn("snapTo called but no snapPoints defined");
102
+ return;
103
+ }
104
+
105
+ const sorted = [...points].sort((a, b) => a - b);
106
+
107
+ if (targetIndex < 0 || targetIndex >= sorted.length) {
108
+ console.warn(
109
+ `snapTo index ${targetIndex} out of bounds (0-${sorted.length - 1})`,
110
+ );
111
+ return;
112
+ }
113
+
114
+ const targetProgress = sorted[targetIndex];
115
+
116
+ runOnUI(() => {
117
+ "worklet";
118
+ animateToProgress({
119
+ target: targetProgress,
120
+ animations,
121
+ spec: {
122
+ open:
123
+ focusedScene.descriptor.options.transitionSpec?.expand ??
124
+ DefaultSnapSpec,
125
+ close:
126
+ focusedScene.descriptor.options.transitionSpec?.collapse ??
127
+ DefaultSnapSpec,
128
+ },
129
+ });
130
+ })();
131
+ },
132
+ [currentOptions?.snapPoints, animations, focusedScene],
133
+ );
134
+
80
135
  return useMemo(
81
136
  () => ({
82
137
  index,
@@ -86,6 +141,7 @@ export function useScreenState<
86
141
  focusedIndex,
87
142
  meta: focusedScene?.descriptor?.options?.meta,
88
143
  navigation: current.navigation as TNavigation,
144
+ snapTo,
89
145
  }),
90
146
  [
91
147
  index,
@@ -94,6 +150,7 @@ export function useScreenState<
94
150
  focusedIndex,
95
151
  current.navigation,
96
152
  current.route,
153
+ snapTo,
97
154
  ],
98
155
  );
99
156
  }