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

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 (87) hide show
  1. package/README.md +104 -9
  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/animation/snap-to.js +62 -0
  5. package/lib/commonjs/shared/animation/snap-to.js.map +1 -0
  6. package/lib/commonjs/shared/constants.js +36 -10
  7. package/lib/commonjs/shared/constants.js.map +1 -1
  8. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +25 -18
  9. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  10. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +0 -25
  11. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  12. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js +74 -64
  13. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -1
  14. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js +2 -61
  15. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js.map +1 -1
  16. package/lib/commonjs/shared/index.js +7 -0
  17. package/lib/commonjs/shared/index.js.map +1 -1
  18. package/lib/commonjs/shared/stores/animation.store.js +2 -1
  19. package/lib/commonjs/shared/stores/animation.store.js.map +1 -1
  20. package/lib/commonjs/shared/utils/animation/animate-to-progress.js +8 -2
  21. package/lib/commonjs/shared/utils/animation/animate-to-progress.js.map +1 -1
  22. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js +90 -23
  23. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  24. package/lib/commonjs/shared/utils/logger.js +22 -0
  25. package/lib/commonjs/shared/utils/logger.js.map +1 -0
  26. package/lib/module/blank-stack/components/adjusted-screen.js +1 -1
  27. package/lib/module/blank-stack/components/adjusted-screen.js.map +1 -1
  28. package/lib/module/shared/animation/snap-to.js +59 -0
  29. package/lib/module/shared/animation/snap-to.js.map +1 -0
  30. package/lib/module/shared/constants.js +34 -9
  31. package/lib/module/shared/constants.js.map +1 -1
  32. package/lib/module/shared/hooks/animation/use-screen-animation.js +25 -18
  33. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  34. package/lib/module/shared/hooks/gestures/use-build-gestures.js +0 -25
  35. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  36. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js +69 -59
  37. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -1
  38. package/lib/module/shared/hooks/navigation/use-screen-state.js +4 -63
  39. package/lib/module/shared/hooks/navigation/use-screen-state.js.map +1 -1
  40. package/lib/module/shared/index.js +1 -0
  41. package/lib/module/shared/index.js.map +1 -1
  42. package/lib/module/shared/stores/animation.store.js +2 -1
  43. package/lib/module/shared/stores/animation.store.js.map +1 -1
  44. package/lib/module/shared/utils/animation/animate-to-progress.js +8 -2
  45. package/lib/module/shared/utils/animation/animate-to-progress.js.map +1 -1
  46. package/lib/module/shared/utils/gesture/check-gesture-activation.js +90 -23
  47. package/lib/module/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  48. package/lib/module/shared/utils/logger.js +17 -0
  49. package/lib/module/shared/utils/logger.js.map +1 -0
  50. package/lib/typescript/blank-stack/components/adjusted-screen.d.ts.map +1 -1
  51. package/lib/typescript/shared/animation/snap-to.d.ts +18 -0
  52. package/lib/typescript/shared/animation/snap-to.d.ts.map +1 -0
  53. package/lib/typescript/shared/constants.d.ts +9 -0
  54. package/lib/typescript/shared/constants.d.ts.map +1 -1
  55. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  56. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  57. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts +1 -16
  58. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts.map +1 -1
  59. package/lib/typescript/shared/hooks/navigation/use-screen-state.d.ts +0 -14
  60. package/lib/typescript/shared/hooks/navigation/use-screen-state.d.ts.map +1 -1
  61. package/lib/typescript/shared/index.d.ts +1 -0
  62. package/lib/typescript/shared/index.d.ts.map +1 -1
  63. package/lib/typescript/shared/stores/animation.store.d.ts +1 -0
  64. package/lib/typescript/shared/stores/animation.store.d.ts.map +1 -1
  65. package/lib/typescript/shared/types/animation.types.d.ts +9 -0
  66. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  67. package/lib/typescript/shared/utils/animation/animate-to-progress.d.ts.map +1 -1
  68. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts +4 -5
  69. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts.map +1 -1
  70. package/lib/typescript/shared/utils/logger.d.ts +6 -0
  71. package/lib/typescript/shared/utils/logger.d.ts.map +1 -0
  72. package/package.json +1 -1
  73. package/src/blank-stack/components/adjusted-screen.tsx +1 -1
  74. package/src/shared/__tests__/derivations.test.ts +1 -0
  75. package/src/shared/__tests__/gesture-activation.test.ts +29 -56
  76. package/src/shared/animation/snap-to.ts +62 -0
  77. package/src/shared/constants.ts +36 -9
  78. package/src/shared/hooks/animation/use-screen-animation.tsx +32 -21
  79. package/src/shared/hooks/gestures/use-build-gestures.tsx +2 -34
  80. package/src/shared/hooks/gestures/use-screen-gesture-handlers.ts +94 -92
  81. package/src/shared/hooks/navigation/use-screen-state.tsx +2 -106
  82. package/src/shared/index.ts +1 -0
  83. package/src/shared/stores/animation.store.ts +2 -0
  84. package/src/shared/types/animation.types.ts +10 -0
  85. package/src/shared/utils/animation/animate-to-progress.ts +7 -2
  86. package/src/shared/utils/gesture/check-gesture-activation.ts +74 -23
  87. package/src/shared/utils/logger.ts +15 -0
@@ -0,0 +1,62 @@
1
+ import { runOnUI } from "react-native-reanimated";
2
+ import { DefaultSnapSpec } from "../configs/specs";
3
+ import { AnimationStore } from "../stores/animation.store";
4
+ import { HistoryStore } from "../stores/history.store";
5
+ import { animateToProgress } from "../utils/animation/animate-to-progress";
6
+
7
+ /**
8
+ * Programmatically snap the currently focused screen to a specific snap point.
9
+ *
10
+ * @param index - The index of the snap point to snap to (0-based, sorted ascending)
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * import { snapTo } from 'react-native-screen-transitions';
15
+ *
16
+ * // Snap to the first (smallest) snap point
17
+ * snapTo(0);
18
+ *
19
+ * // Snap to the last (largest) snap point
20
+ * snapTo(2); // if there are 3 snap points
21
+ * ```
22
+ */
23
+ export function snapTo(index: number): void {
24
+ const recent = HistoryStore.getMostRecent();
25
+
26
+ if (!recent) {
27
+ console.warn("snapTo: No screen in history");
28
+ return;
29
+ }
30
+
31
+ const { descriptor } = recent;
32
+ const snapPoints = descriptor.options?.snapPoints;
33
+
34
+ if (!snapPoints || snapPoints.length === 0) {
35
+ console.warn("snapTo: No snapPoints defined on current screen");
36
+ return;
37
+ }
38
+
39
+ const sorted = [...snapPoints].sort((a, b) => a - b);
40
+
41
+ if (index < 0 || index >= sorted.length) {
42
+ console.warn(
43
+ `snapTo: index ${index} out of bounds (0-${sorted.length - 1})`,
44
+ );
45
+ return;
46
+ }
47
+
48
+ const targetProgress = sorted[index];
49
+ const animations = AnimationStore.getAll(descriptor.route.key);
50
+
51
+ runOnUI(() => {
52
+ "worklet";
53
+ animateToProgress({
54
+ target: targetProgress,
55
+ animations,
56
+ spec: {
57
+ open: descriptor.options.transitionSpec?.expand ?? DefaultSnapSpec,
58
+ close: descriptor.options.transitionSpec?.collapse ?? DefaultSnapSpec,
59
+ },
60
+ });
61
+ })();
62
+ }
@@ -4,6 +4,7 @@ import type { MeasuredDimensions } from "react-native-reanimated";
4
4
  import type { ScreenTransitionState } from "./types/animation.types";
5
5
  import type { ActivationArea } from "./types/gesture.types";
6
6
  import type { Layout } from "./types/screen.types";
7
+ import type { BaseStackRoute } from "./types/stack.types";
7
8
 
8
9
  /**
9
10
  * Masked view integration
@@ -16,6 +17,35 @@ export const CONTAINER_STYLE_ID = "_ROOT_CONTAINER";
16
17
  */
17
18
  export const NO_STYLES = Object.freeze({});
18
19
 
20
+ /**
21
+ * Default gesture values
22
+ */
23
+ const DEFAULT_GESTURE_VALUES = {
24
+ x: 0,
25
+ y: 0,
26
+ normalizedX: 0,
27
+ normalizedY: 0,
28
+ isDismissing: 0,
29
+ isDragging: 0,
30
+ direction: null,
31
+ } as const;
32
+
33
+ /**
34
+ * Creates a new screen transition state object
35
+ */
36
+ export const createScreenTransitionState = (
37
+ route: BaseStackRoute,
38
+ meta?: Record<string, unknown>,
39
+ ): ScreenTransitionState => ({
40
+ progress: 0,
41
+ closing: 0,
42
+ animating: 0,
43
+ entering: 1,
44
+ gesture: { ...DEFAULT_GESTURE_VALUES },
45
+ route,
46
+ meta,
47
+ });
48
+
19
49
  /**
20
50
  * Default screen transition state
21
51
  */
@@ -25,15 +55,7 @@ export const DEFAULT_SCREEN_TRANSITION_STATE: ScreenTransitionState =
25
55
  closing: 0,
26
56
  animating: 0,
27
57
  entering: 1,
28
- gesture: {
29
- x: 0,
30
- y: 0,
31
- normalizedX: 0,
32
- normalizedY: 0,
33
- isDismissing: 0,
34
- isDragging: 0,
35
- direction: null,
36
- },
58
+ gesture: DEFAULT_GESTURE_VALUES,
37
59
  route: {} as RouteProp<ParamListBase>,
38
60
  });
39
61
 
@@ -79,3 +101,8 @@ export const IS_WEB = Platform.OS === "web";
79
101
 
80
102
  export const TRUE = 1;
81
103
  export const FALSE = 0;
104
+
105
+ /**
106
+ * Small value for floating-point comparisons to handle animation/interpolation imprecision
107
+ */
108
+ export const EPSILON = 1e-5;
@@ -3,7 +3,10 @@ import { useWindowDimensions } from "react-native";
3
3
  import { type SharedValue, useDerivedValue } from "react-native-reanimated";
4
4
  import { useSafeAreaInsets } from "react-native-safe-area-context";
5
5
  import type { NativeStackScreenTransitionConfig } from "../../../native-stack/types";
6
- import { DEFAULT_SCREEN_TRANSITION_STATE } from "../../constants";
6
+ import {
7
+ createScreenTransitionState,
8
+ DEFAULT_SCREEN_TRANSITION_STATE,
9
+ } from "../../constants";
7
10
  import {
8
11
  type BaseDescriptor,
9
12
  useKeys,
@@ -31,26 +34,26 @@ type BuiltState = {
31
34
  unwrapped: ScreenTransitionState;
32
35
  };
33
36
 
34
- const createScreenTransitionState = (
35
- route: BaseStackRoute,
36
- meta?: Record<string, unknown>,
37
- ): ScreenTransitionState => ({
38
- progress: 0,
39
- closing: 0,
40
- animating: 0,
41
- entering: 1,
42
- gesture: {
43
- x: 0,
44
- y: 0,
45
- normalizedX: 0,
46
- normalizedY: 0,
47
- isDismissing: 0,
48
- isDragging: 0,
49
- direction: null,
50
- },
51
- route,
52
- meta,
53
- });
37
+ /**
38
+ * Computes the animated snap index based on progress and snap points.
39
+ * Returns -1 if no snap points, otherwise interpolates between indices.
40
+ */
41
+ const computeSnapIndex = (progress: number, snapPoints: number[]): number => {
42
+ "worklet";
43
+ if (snapPoints.length === 0) return -1;
44
+ if (progress <= snapPoints[0]) return 0;
45
+ if (progress >= snapPoints[snapPoints.length - 1])
46
+ return snapPoints.length - 1;
47
+
48
+ for (let i = 0; i < snapPoints.length - 1; i++) {
49
+ if (progress <= snapPoints[i + 1]) {
50
+ const t =
51
+ (progress - snapPoints[i]) / (snapPoints[i + 1] - snapPoints[i]);
52
+ return i + t;
53
+ }
54
+ }
55
+ return snapPoints.length - 1;
56
+ };
54
57
 
55
58
  const unwrapInto = (s: BuiltState): ScreenTransitionState => {
56
59
  "worklet";
@@ -123,6 +126,11 @@ export function _useScreenAnimation() {
123
126
  const currentRouteKey = currentDescriptor?.route?.key;
124
127
  const currentIndex = routeKeys.indexOf(currentRouteKey);
125
128
 
129
+ const sortedSnapPoints = useMemo(() => {
130
+ const points = currentDescriptor?.options?.snapPoints;
131
+ return points ? [...points].sort((a, b) => a - b) : [];
132
+ }, [currentDescriptor?.options?.snapPoints]);
133
+
126
134
  const screenInterpolatorProps = useDerivedValue<
127
135
  Omit<ScreenInterpolationProps, "bounds">
128
136
  >(() => {
@@ -152,6 +160,8 @@ export function _useScreenAnimation() {
152
160
  const stackProgress =
153
161
  currentIndex >= 0 ? rootStackProgress.value - currentIndex : progress;
154
162
 
163
+ const snapIndex = computeSnapIndex(current.progress, sortedSnapPoints);
164
+
155
165
  return {
156
166
  layouts: { screen: dimensions },
157
167
  insets,
@@ -160,6 +170,7 @@ export function _useScreenAnimation() {
160
170
  next,
161
171
  progress,
162
172
  stackProgress,
173
+ snapIndex,
163
174
  ...helpers,
164
175
  };
165
176
  });
@@ -1,20 +1,12 @@
1
1
  import { StackActions } from "@react-navigation/native";
2
2
  import { useCallback, useMemo, useRef } from "react";
3
- import { useWindowDimensions } from "react-native";
4
3
  import { Gesture, type GestureType } from "react-native-gesture-handler";
5
4
  import type { SharedValue } from "react-native-reanimated";
6
- import {
7
- DEFAULT_GESTURE_ACTIVATION_AREA,
8
- DEFAULT_GESTURE_DIRECTION,
9
- DEFAULT_GESTURE_DRIVES_PROGRESS,
10
- GESTURE_VELOCITY_IMPACT,
11
- } from "../../constants";
12
5
  import type {
13
6
  GestureContextType,
14
7
  ScrollConfig,
15
8
  } from "../../providers/gestures.provider";
16
9
  import { useKeys } from "../../providers/screen/keys.provider";
17
- import { AnimationStore } from "../../stores/animation.store";
18
10
  import { GestureStore, type GestureStoreMap } from "../../stores/gesture.store";
19
11
  import { useScreenGestureHandlers } from "./use-screen-gesture-handlers";
20
12
 
@@ -32,8 +24,6 @@ export const useBuildGestures = ({
32
24
  nativeGesture: GestureType;
33
25
  gestureAnimationValues: GestureStoreMap;
34
26
  } => {
35
- const dimensions = useWindowDimensions();
36
-
37
27
  const { current } = useKeys();
38
28
 
39
29
  const navState = current.navigation.getState();
@@ -48,17 +38,8 @@ export const useBuildGestures = ({
48
38
  const gestureAnimationValues = GestureStore.getRouteGestures(
49
39
  current.route.key,
50
40
  );
51
- const animations = AnimationStore.getAll(current.route.key);
52
-
53
- const {
54
- gestureDirection = DEFAULT_GESTURE_DIRECTION,
55
- gestureVelocityImpact = GESTURE_VELOCITY_IMPACT,
56
- gestureDrivesProgress = DEFAULT_GESTURE_DRIVES_PROGRESS,
57
- gestureActivationArea = DEFAULT_GESTURE_ACTIVATION_AREA,
58
- gestureResponseDistance,
59
- transitionSpec,
60
- snapPoints,
61
- } = current.options;
41
+
42
+ const { snapPoints } = current.options;
62
43
 
63
44
  // Dismiss gesture is controlled by gestureEnabled (disabled for first screen)
64
45
  const canDismiss = Boolean(
@@ -97,26 +78,13 @@ export const useBuildGestures = ({
97
78
 
98
79
  const { onTouchesDown, onTouchesMove, onStart, onUpdate, onEnd } =
99
80
  useScreenGestureHandlers({
100
- dimensions,
101
- animations,
102
- gestureAnimationValues,
103
- gestureDirection,
104
- gestureDrivesProgress,
105
- gestureVelocityImpact,
106
81
  scrollConfig,
107
- gestureActivationArea,
108
- gestureResponseDistance,
109
- snapPoints,
110
82
  canDismiss,
111
- transitionSpec,
112
83
  handleDismiss,
113
84
  ancestorIsDismissing:
114
85
  ancestorContext?.gestureAnimationValues.isDismissing,
115
86
  });
116
87
 
117
- // Memoize gestures to keep stable references - critical for RNGH
118
- // Child gestures reference ancestor's pan via requireExternalGestureToFail,
119
- // so the pan gesture MUST be stable or children will reference stale objects
120
88
  return useMemo(() => {
121
89
  const panGesture = Gesture.Pan()
122
90
  .withRef(panGestureRef)
@@ -1,4 +1,5 @@
1
1
  import { useMemo } from "react";
2
+ import { useWindowDimensions } from "react-native";
2
3
  import type {
3
4
  GestureStateChangeEvent,
4
5
  GestureTouchEvent,
@@ -8,17 +9,20 @@ import type {
8
9
  import type { GestureStateManagerType } from "react-native-gesture-handler/lib/typescript/handlers/gestures/gestureStateManager";
9
10
  import { type SharedValue, useSharedValue } from "react-native-reanimated";
10
11
  import { DefaultSnapSpec } from "../../configs/specs";
11
- import { FALSE, TRUE } from "../../constants";
12
- import type { ScrollConfig } from "../../providers/gestures.provider";
13
- import type { AnimationStoreMap } from "../../stores/animation.store";
14
- import type { GestureStoreMap } from "../../stores/gesture.store";
15
- import type { TransitionSpec } from "../../types/animation.types";
16
12
  import {
17
- type GestureActivationArea,
18
- type GestureDirection,
19
- GestureOffsetState,
20
- } from "../../types/gesture.types";
21
- import type { Layout } from "../../types/screen.types";
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";
22
26
  import { animateToProgress } from "../../utils/animation/animate-to-progress";
23
27
  import {
24
28
  applyOffsetRules,
@@ -30,50 +34,71 @@ import { mapGestureToProgress } from "../../utils/gesture/map-gesture-to-progres
30
34
  import { resetGestureValues } from "../../utils/gesture/reset-gesture-values";
31
35
  import { validateSnapPoints } from "../../utils/gesture/validate-snap-points";
32
36
  import { velocity } from "../../utils/gesture/velocity";
37
+ import { logger } from "../../utils/logger";
33
38
  import useStableCallbackValue from "../use-stable-callback-value";
34
39
 
35
40
  interface UseScreenGestureHandlersProps {
36
- dimensions: Layout;
37
- animations: AnimationStoreMap;
38
- gestureAnimationValues: GestureStoreMap;
39
- gestureDirection: GestureDirection | GestureDirection[];
40
- gestureDrivesProgress: boolean;
41
- gestureVelocityImpact: number;
42
41
  scrollConfig: SharedValue<ScrollConfig | null>;
43
- gestureActivationArea: GestureActivationArea;
44
- gestureResponseDistance?: number;
45
42
  ancestorIsDismissing?: SharedValue<number> | null;
46
- snapPoints?: number[];
47
43
  canDismiss: boolean;
48
- transitionSpec?: TransitionSpec;
49
44
  handleDismiss: () => void;
50
45
  }
51
46
 
52
47
  export const useScreenGestureHandlers = ({
53
- dimensions,
54
- animations,
55
- gestureAnimationValues,
56
- gestureDirection,
57
- gestureDrivesProgress,
58
- gestureVelocityImpact,
59
48
  scrollConfig,
60
- gestureActivationArea,
61
- gestureResponseDistance,
62
49
  ancestorIsDismissing,
63
- snapPoints: rawSnapPoints,
64
50
  canDismiss,
65
- transitionSpec,
66
51
  handleDismiss,
67
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
+
68
71
  const { hasSnapPoints, snapPoints, minSnapPoint, maxSnapPoint } = useMemo(
69
72
  () => validateSnapPoints({ snapPoints: rawSnapPoints, canDismiss }),
70
73
  [rawSnapPoints, canDismiss],
71
74
  );
72
75
 
73
76
  const directions = useMemo(() => {
74
- const directionsArray = Array.isArray(gestureDirection)
75
- ? gestureDirection
76
- : [gestureDirection];
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];
77
102
 
78
103
  const isBidirectional = directionsArray.includes("bidirectional");
79
104
 
@@ -128,7 +153,7 @@ export const useScreenGestureHandlers = ({
128
153
 
129
154
  // If an ancestor navigator is already dismissing via gesture, block new gestures here.
130
155
  if (ancestorIsDismissing?.value) {
131
- gestureOffsetState.set(GestureOffsetState.FAILED);
156
+ gestureOffsetState.value = GestureOffsetState.FAILED;
132
157
  manager.fail();
133
158
  return;
134
159
  }
@@ -176,13 +201,13 @@ export const useScreenGestureHandlers = ({
176
201
  }
177
202
 
178
203
  if (isSwipingDown) {
179
- gestureAnimationValues.direction.set("vertical");
204
+ gestureAnimationValues.direction.value = "vertical";
180
205
  } else if (isSwipingUp) {
181
- gestureAnimationValues.direction.set("vertical-inverted");
206
+ gestureAnimationValues.direction.value = "vertical-inverted";
182
207
  } else if (isSwipingRight) {
183
- gestureAnimationValues.direction.set("horizontal");
208
+ gestureAnimationValues.direction.value = "horizontal";
184
209
  } else if (isSwipingLeft) {
185
- gestureAnimationValues.direction.set("horizontal-inverted");
210
+ gestureAnimationValues.direction.value = "horizontal-inverted";
186
211
  }
187
212
 
188
213
  manager.activate();
@@ -190,18 +215,12 @@ export const useScreenGestureHandlers = ({
190
215
  }
191
216
 
192
217
  // Touch IS on ScrollView - apply scroll-aware rules
193
- const scrollX = scrollCfg?.x ?? 0;
194
- const scrollY = scrollCfg?.y ?? 0;
195
- const maxScrollX = scrollCfg?.contentWidth
196
- ? scrollCfg.contentWidth - scrollCfg.layoutWidth
197
- : 0;
198
- const maxScrollY = scrollCfg?.contentHeight
199
- ? scrollCfg.contentHeight - scrollCfg.layoutHeight
200
- : 0;
201
-
202
218
  // Snap mode: determine if sheet can still expand
219
+ // Also check targetProgress - if we're already animating toward max, scroll should win
203
220
  const canExpandMore =
204
- hasSnapPoints && animations.progress.value < maxSnapPoint - 0.01;
221
+ hasSnapPoints &&
222
+ animations.progress.value < maxSnapPoint - EPSILON &&
223
+ animations.targetProgress.value < maxSnapPoint - EPSILON;
205
224
 
206
225
  const { shouldActivate, direction: activatedDirection } =
207
226
  checkScrollAwareActivation({
@@ -212,10 +231,7 @@ export const useScreenGestureHandlers = ({
212
231
  isSwipingLeft,
213
232
  },
214
233
  directions,
215
- scrollX,
216
- scrollY,
217
- maxScrollX,
218
- maxScrollY,
234
+ scrollConfig: scrollCfg,
219
235
  hasSnapPoints,
220
236
  canExpandMore,
221
237
  });
@@ -242,6 +258,7 @@ export const useScreenGestureHandlers = ({
242
258
  gestureAnimationValues.isDragging.value = TRUE;
243
259
  gestureAnimationValues.isDismissing.value = FALSE;
244
260
  gestureStartProgress.value = animations.progress.value;
261
+ animations.animating.value = TRUE;
245
262
  });
246
263
 
247
264
  const onUpdate = useStableCallbackValue(
@@ -251,7 +268,6 @@ export const useScreenGestureHandlers = ({
251
268
  const { translationX, translationY } = event;
252
269
  const { width, height } = dimensions;
253
270
 
254
- // Update gesture values (shared across all modes)
255
271
  gestureAnimationValues.x.value = translationX;
256
272
  gestureAnimationValues.y.value = translationY;
257
273
  gestureAnimationValues.normalizedX.value = velocity.normalizeTranslation(
@@ -264,7 +280,6 @@ export const useScreenGestureHandlers = ({
264
280
  );
265
281
 
266
282
  if (hasSnapPoints && gestureDrivesProgress) {
267
- // Snap mode: bidirectional tracking on snap axis
268
283
  const isHorizontal = snapAxis === "horizontal";
269
284
  const translation = isHorizontal ? translationX : translationY;
270
285
  const dimension = isHorizontal ? width : height;
@@ -283,43 +298,30 @@ export const useScreenGestureHandlers = ({
283
298
  Math.min(maxSnapPoint, gestureStartProgress.value + progressDelta),
284
299
  );
285
300
  } else if (gestureDrivesProgress) {
286
- // Standard mode: find max progress across allowed directions
287
- const axes = [
288
- {
289
- enabled: directions.horizontal,
290
- translation: translationX,
291
- dimension: width,
292
- sign: 1,
293
- },
294
- {
295
- enabled: directions.horizontalInverted,
296
- translation: translationX,
297
- dimension: width,
298
- sign: -1,
299
- },
300
- {
301
- enabled: directions.vertical,
302
- translation: translationY,
303
- dimension: height,
304
- sign: 1,
305
- },
306
- {
307
- enabled: directions.verticalInverted,
308
- translation: translationY,
309
- dimension: height,
310
- sign: -1,
311
- },
312
- ];
313
-
314
301
  let maxProgress = 0;
315
- for (const axis of axes) {
316
- if (axis.enabled && axis.translation * axis.sign > 0) {
317
- const progress = mapGestureToProgress(
318
- Math.abs(axis.translation),
319
- axis.dimension,
320
- );
321
- maxProgress = Math.max(maxProgress, progress);
322
- }
302
+
303
+ // Horizontal swipe right (positive X)
304
+ if (directions.horizontal && translationX > 0) {
305
+ const progress = mapGestureToProgress(translationX, width);
306
+ maxProgress = Math.max(maxProgress, progress);
307
+ }
308
+
309
+ // Horizontal inverted swipe left (negative X)
310
+ if (directions.horizontalInverted && translationX < 0) {
311
+ const progress = mapGestureToProgress(-translationX, width);
312
+ maxProgress = Math.max(maxProgress, progress);
313
+ }
314
+
315
+ // Vertical swipe down (positive Y)
316
+ if (directions.vertical && translationY > 0) {
317
+ const progress = mapGestureToProgress(translationY, height);
318
+ maxProgress = Math.max(maxProgress, progress);
319
+ }
320
+
321
+ // Vertical inverted swipe up (negative Y)
322
+ if (directions.verticalInverted && translationY < 0) {
323
+ const progress = mapGestureToProgress(-translationY, height);
324
+ maxProgress = Math.max(maxProgress, progress);
323
325
  }
324
326
 
325
327
  animations.progress.value = Math.max(
@@ -394,7 +396,6 @@ export const useScreenGestureHandlers = ({
394
396
  initialVelocity,
395
397
  });
396
398
  } else {
397
- // Standard mode: use determineDismissal
398
399
  const result = determineDismissal({
399
400
  event,
400
401
  directions,
@@ -403,7 +404,8 @@ export const useScreenGestureHandlers = ({
403
404
  });
404
405
 
405
406
  const shouldDismiss = result.shouldDismiss;
406
- const targetProgress = shouldDismiss ? 0 : gestureStartProgress.value;
407
+ // Without snap points, always animate to fully visible (1) when not dismissing
408
+ const targetProgress = shouldDismiss ? 0 : 1;
407
409
 
408
410
  resetGestureValues({
409
411
  spec: shouldDismiss ? transitionSpec?.close : transitionSpec?.open,