react-native-screen-transitions 3.2.0 → 3.3.0-beta.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 (152) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/shared/components/create-transition-aware-component.js +8 -2
  3. package/lib/commonjs/shared/components/create-transition-aware-component.js.map +1 -1
  4. package/lib/commonjs/shared/components/{root-transition-aware.js → screen-container.js} +28 -12
  5. package/lib/commonjs/shared/components/screen-container.js.map +1 -0
  6. package/lib/commonjs/shared/configs/presets.js +3 -3
  7. package/lib/commonjs/shared/configs/presets.js.map +1 -1
  8. package/lib/commonjs/shared/configs/specs.js +6 -1
  9. package/lib/commonjs/shared/configs/specs.js.map +1 -1
  10. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +36 -188
  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 +334 -0
  13. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -0
  14. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +47 -4
  15. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  16. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js +3 -3
  17. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js.map +1 -1
  18. package/lib/commonjs/shared/hooks/lifecycle/use-open-transition.js +25 -3
  19. package/lib/commonjs/shared/hooks/lifecycle/use-open-transition.js.map +1 -1
  20. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js +61 -2
  21. package/lib/commonjs/shared/hooks/navigation/use-screen-state.js.map +1 -1
  22. package/lib/commonjs/shared/hooks/use-backdrop-pointer-events.js +32 -0
  23. package/lib/commonjs/shared/hooks/use-backdrop-pointer-events.js.map +1 -0
  24. package/lib/commonjs/shared/providers/gestures.provider.js +4 -2
  25. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  26. package/lib/commonjs/shared/providers/screen/screen-composer.js +2 -2
  27. package/lib/commonjs/shared/providers/screen/screen-composer.js.map +1 -1
  28. package/lib/commonjs/shared/providers/screen/styles.provider.js +41 -32
  29. package/lib/commonjs/shared/providers/screen/styles.provider.js.map +1 -1
  30. package/lib/commonjs/shared/utils/animation/{start-screen-transition.js → animate-to-progress.js} +11 -7
  31. package/lib/commonjs/shared/utils/animation/animate-to-progress.js.map +1 -0
  32. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js +71 -0
  33. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  34. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js +56 -0
  35. package/lib/commonjs/shared/utils/gesture/determine-snap-target.js.map +1 -0
  36. package/lib/commonjs/shared/utils/gesture/validate-snap-points.js +31 -0
  37. package/lib/commonjs/shared/utils/gesture/validate-snap-points.js.map +1 -0
  38. package/lib/commonjs/shared/utils/gesture/velocity.js +11 -0
  39. package/lib/commonjs/shared/utils/gesture/velocity.js.map +1 -1
  40. package/lib/module/shared/components/create-transition-aware-component.js +8 -2
  41. package/lib/module/shared/components/create-transition-aware-component.js.map +1 -1
  42. package/lib/module/shared/components/screen-container.js +64 -0
  43. package/lib/module/shared/components/screen-container.js.map +1 -0
  44. package/lib/module/shared/configs/presets.js +3 -3
  45. package/lib/module/shared/configs/presets.js.map +1 -1
  46. package/lib/module/shared/configs/specs.js +5 -0
  47. package/lib/module/shared/configs/specs.js.map +1 -1
  48. package/lib/module/shared/hooks/gestures/use-build-gestures.js +36 -187
  49. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  50. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js +328 -0
  51. package/lib/module/shared/hooks/gestures/use-screen-gesture-handlers.js.map +1 -0
  52. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +47 -4
  53. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  54. package/lib/module/shared/hooks/lifecycle/use-close-transition.js +3 -3
  55. package/lib/module/shared/hooks/lifecycle/use-close-transition.js.map +1 -1
  56. package/lib/module/shared/hooks/lifecycle/use-open-transition.js +25 -3
  57. package/lib/module/shared/hooks/lifecycle/use-open-transition.js.map +1 -1
  58. package/lib/module/shared/hooks/navigation/use-screen-state.js +63 -4
  59. package/lib/module/shared/hooks/navigation/use-screen-state.js.map +1 -1
  60. package/lib/module/shared/hooks/use-backdrop-pointer-events.js +28 -0
  61. package/lib/module/shared/hooks/use-backdrop-pointer-events.js.map +1 -0
  62. package/lib/module/shared/providers/gestures.provider.js +4 -2
  63. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  64. package/lib/module/shared/providers/screen/screen-composer.js +2 -2
  65. package/lib/module/shared/providers/screen/screen-composer.js.map +1 -1
  66. package/lib/module/shared/providers/screen/styles.provider.js +41 -32
  67. package/lib/module/shared/providers/screen/styles.provider.js.map +1 -1
  68. package/lib/module/shared/utils/animation/{start-screen-transition.js → animate-to-progress.js} +9 -5
  69. package/lib/module/shared/utils/animation/animate-to-progress.js.map +1 -0
  70. package/lib/module/shared/utils/gesture/check-gesture-activation.js +70 -0
  71. package/lib/module/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  72. package/lib/module/shared/utils/gesture/determine-snap-target.js +52 -0
  73. package/lib/module/shared/utils/gesture/determine-snap-target.js.map +1 -0
  74. package/lib/module/shared/utils/gesture/validate-snap-points.js +26 -0
  75. package/lib/module/shared/utils/gesture/validate-snap-points.js.map +1 -0
  76. package/lib/module/shared/utils/gesture/velocity.js +11 -0
  77. package/lib/module/shared/utils/gesture/velocity.js.map +1 -1
  78. package/lib/typescript/shared/components/create-transition-aware-component.d.ts.map +1 -1
  79. package/lib/typescript/shared/components/screen-container.d.ts +6 -0
  80. package/lib/typescript/shared/components/screen-container.d.ts.map +1 -0
  81. package/lib/typescript/shared/configs/specs.d.ts +1 -0
  82. package/lib/typescript/shared/configs/specs.d.ts.map +1 -1
  83. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +1 -1
  84. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  85. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts +34 -0
  86. package/lib/typescript/shared/hooks/gestures/use-screen-gesture-handlers.d.ts.map +1 -0
  87. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts +5 -1
  88. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
  89. package/lib/typescript/shared/hooks/lifecycle/use-open-transition.d.ts.map +1 -1
  90. package/lib/typescript/shared/hooks/navigation/use-screen-state.d.ts +14 -0
  91. package/lib/typescript/shared/hooks/navigation/use-screen-state.d.ts.map +1 -1
  92. package/lib/typescript/shared/hooks/use-backdrop-pointer-events.d.ts +15 -0
  93. package/lib/typescript/shared/hooks/use-backdrop-pointer-events.d.ts.map +1 -0
  94. package/lib/typescript/shared/providers/gestures.provider.d.ts +1 -0
  95. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  96. package/lib/typescript/shared/providers/screen/styles.provider.d.ts.map +1 -1
  97. package/lib/typescript/shared/types/animation.types.d.ts +28 -2
  98. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  99. package/lib/typescript/shared/types/screen.types.d.ts +26 -0
  100. package/lib/typescript/shared/types/screen.types.d.ts.map +1 -1
  101. package/lib/typescript/shared/utils/animation/animate-to-progress.d.ts +19 -0
  102. package/lib/typescript/shared/utils/animation/animate-to-progress.d.ts.map +1 -0
  103. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts +24 -0
  104. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts.map +1 -1
  105. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts +26 -0
  106. package/lib/typescript/shared/utils/gesture/determine-snap-target.d.ts.map +1 -0
  107. package/lib/typescript/shared/utils/gesture/validate-snap-points.d.ts +13 -0
  108. package/lib/typescript/shared/utils/gesture/validate-snap-points.d.ts.map +1 -0
  109. package/lib/typescript/shared/utils/gesture/velocity.d.ts +1 -0
  110. package/lib/typescript/shared/utils/gesture/velocity.d.ts.map +1 -1
  111. package/package.json +29 -2
  112. package/src/shared/__tests__/determine-snap-target.test.ts +268 -0
  113. package/src/shared/__tests__/gesture-activation.test.ts +247 -0
  114. package/src/shared/__tests__/validate-snap-points.test.ts +125 -0
  115. package/src/shared/components/create-transition-aware-component.tsx +11 -1
  116. package/src/shared/components/screen-container.tsx +65 -0
  117. package/src/shared/configs/presets.ts +3 -3
  118. package/src/shared/configs/specs.ts +6 -0
  119. package/src/shared/hooks/gestures/use-build-gestures.tsx +33 -253
  120. package/src/shared/hooks/gestures/use-screen-gesture-handlers.ts +436 -0
  121. package/src/shared/hooks/gestures/use-scroll-registry.tsx +52 -1
  122. package/src/shared/hooks/lifecycle/use-close-transition.ts +3 -3
  123. package/src/shared/hooks/lifecycle/use-open-transition.ts +27 -3
  124. package/src/shared/hooks/navigation/use-screen-state.tsx +106 -2
  125. package/src/shared/hooks/use-backdrop-pointer-events.ts +32 -0
  126. package/src/shared/providers/gestures.provider.tsx +3 -2
  127. package/src/shared/providers/screen/screen-composer.tsx +2 -2
  128. package/src/shared/providers/screen/styles.provider.tsx +40 -34
  129. package/src/shared/types/animation.types.ts +29 -2
  130. package/src/shared/types/screen.types.ts +29 -0
  131. package/src/shared/utils/animation/{start-screen-transition.ts → animate-to-progress.ts} +19 -7
  132. package/src/shared/utils/gesture/check-gesture-activation.ts +78 -0
  133. package/src/shared/utils/gesture/determine-snap-target.ts +75 -0
  134. package/src/shared/utils/gesture/validate-snap-points.ts +37 -0
  135. package/src/shared/utils/gesture/velocity.ts +10 -0
  136. package/lib/commonjs/shared/components/root-transition-aware.js.map +0 -1
  137. package/lib/commonjs/shared/hooks/use-stack-pointer-events.js +0 -23
  138. package/lib/commonjs/shared/hooks/use-stack-pointer-events.js.map +0 -1
  139. package/lib/commonjs/shared/utils/animation/start-screen-transition.js.map +0 -1
  140. package/lib/module/shared/components/root-transition-aware.js +0 -48
  141. package/lib/module/shared/components/root-transition-aware.js.map +0 -1
  142. package/lib/module/shared/hooks/use-stack-pointer-events.js +0 -20
  143. package/lib/module/shared/hooks/use-stack-pointer-events.js.map +0 -1
  144. package/lib/module/shared/utils/animation/start-screen-transition.js.map +0 -1
  145. package/lib/typescript/shared/components/root-transition-aware.d.ts +0 -6
  146. package/lib/typescript/shared/components/root-transition-aware.d.ts.map +0 -1
  147. package/lib/typescript/shared/hooks/use-stack-pointer-events.d.ts +0 -10
  148. package/lib/typescript/shared/hooks/use-stack-pointer-events.d.ts.map +0 -1
  149. package/lib/typescript/shared/utils/animation/start-screen-transition.d.ts +0 -13
  150. package/lib/typescript/shared/utils/animation/start-screen-transition.d.ts.map +0 -1
  151. package/src/shared/components/root-transition-aware.tsx +0 -49
  152. package/src/shared/hooks/use-stack-pointer-events.ts +0 -15
@@ -1,12 +1,19 @@
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 {
4
+ runOnUI,
5
+ type SharedValue,
6
+ useDerivedValue,
7
+ } from "react-native-reanimated";
8
+ import { DefaultSnapSpec } from "../../configs/specs";
4
9
  import {
5
10
  type BaseDescriptor,
6
11
  useKeys,
7
12
  } from "../../providers/screen/keys.provider";
13
+ import { AnimationStore } from "../../stores/animation.store";
8
14
  import type { ScreenTransitionConfig } from "../../types/screen.types";
9
15
  import type { BaseStackNavigation } from "../../types/stack.types";
16
+ import { animateToProgress } from "../../utils/animation/animate-to-progress";
10
17
  import { useSharedValueState } from "../reanimated/use-shared-value-state";
11
18
  import { type StackContextValue, useStack } from "./use-stack";
12
19
 
@@ -47,6 +54,21 @@ export interface ScreenState<
47
54
  * Navigation object for this screen.
48
55
  */
49
56
  navigation: TNavigation;
57
+
58
+ /**
59
+ * Programmatically snap to a specific snap point by index.
60
+ * Only works if the screen has snapPoints defined.
61
+ *
62
+ * @param index - The index of the snap point to snap to (0-based)
63
+ */
64
+ snapTo: (index: number) => void;
65
+
66
+ /**
67
+ * Animated value representing the current snap point index.
68
+ * Interpolates between indices during gestures (e.g., 0.5 means halfway between snap 0 and 1).
69
+ * Returns -1 if no snap points are defined.
70
+ */
71
+ animatedSnapIndex: SharedValue<number>;
50
72
  }
51
73
 
52
74
  /**
@@ -77,6 +99,84 @@ export function useScreenState<
77
99
  return scenes[focusedIndex] ?? scenes[scenes.length - 1];
78
100
  }, [scenes, focusedIndex]);
79
101
 
102
+ const currentOptions = current.options;
103
+ const snapPoints = currentOptions?.snapPoints;
104
+ const animations = useMemo(
105
+ () => AnimationStore.getAll(current.route.key),
106
+ [current.route.key],
107
+ );
108
+
109
+ // Pre-sort snap points for the derived value (avoids sorting in worklet)
110
+ const sortedSnapPoints = useMemo(
111
+ () => (snapPoints ? [...snapPoints].sort((a, b) => a - b) : []),
112
+ [snapPoints],
113
+ );
114
+
115
+ const animatedSnapIndex = useDerivedValue(() => {
116
+ if (sortedSnapPoints.length === 0) {
117
+ return -1;
118
+ }
119
+
120
+ const progress = animations.progress.value;
121
+
122
+ // Below first snap point
123
+ if (progress <= sortedSnapPoints[0]) {
124
+ return 0;
125
+ }
126
+
127
+ // Above last snap point
128
+ if (progress >= sortedSnapPoints[sortedSnapPoints.length - 1]) {
129
+ return sortedSnapPoints.length - 1;
130
+ }
131
+
132
+ // Find segment and interpolate
133
+ for (let i = 0; i < sortedSnapPoints.length - 1; i++) {
134
+ if (progress <= sortedSnapPoints[i + 1]) {
135
+ const t =
136
+ (progress - sortedSnapPoints[i]) /
137
+ (sortedSnapPoints[i + 1] - sortedSnapPoints[i]);
138
+ return i + t;
139
+ }
140
+ }
141
+
142
+ return sortedSnapPoints.length - 1;
143
+ });
144
+
145
+ const snapTo = useCallback(
146
+ (targetIndex: number) => {
147
+ if (!sortedSnapPoints || sortedSnapPoints.length === 0) {
148
+ console.warn("snapTo called but no snapPoints defined");
149
+ return;
150
+ }
151
+
152
+ if (targetIndex < 0 || targetIndex >= sortedSnapPoints.length) {
153
+ console.warn(
154
+ `snapTo index ${targetIndex} out of bounds (0-${sortedSnapPoints.length - 1})`,
155
+ );
156
+ return;
157
+ }
158
+
159
+ const targetProgress = sortedSnapPoints[targetIndex];
160
+
161
+ runOnUI(() => {
162
+ "worklet";
163
+ animateToProgress({
164
+ target: targetProgress,
165
+ animations,
166
+ spec: {
167
+ open:
168
+ focusedScene.descriptor.options.transitionSpec?.expand ??
169
+ DefaultSnapSpec,
170
+ close:
171
+ focusedScene.descriptor.options.transitionSpec?.collapse ??
172
+ DefaultSnapSpec,
173
+ },
174
+ });
175
+ })();
176
+ },
177
+ [sortedSnapPoints, animations, focusedScene],
178
+ );
179
+
80
180
  return useMemo(
81
181
  () => ({
82
182
  index,
@@ -86,6 +186,8 @@ export function useScreenState<
86
186
  focusedIndex,
87
187
  meta: focusedScene?.descriptor?.options?.meta,
88
188
  navigation: current.navigation as TNavigation,
189
+ snapTo,
190
+ animatedSnapIndex,
89
191
  }),
90
192
  [
91
193
  index,
@@ -94,6 +196,8 @@ export function useScreenState<
94
196
  focusedIndex,
95
197
  current.navigation,
96
198
  current.route,
199
+ snapTo,
200
+ animatedSnapIndex,
97
201
  ],
98
202
  );
99
203
  }
@@ -0,0 +1,32 @@
1
+ import { useKeys } from "../providers/screen/keys.provider";
2
+ import { useStackCoreContext } from "../providers/stack/core.provider";
3
+ import { StackType } from "../types/stack.types";
4
+
5
+ type BackdropBehavior = "block" | "passthrough" | "dismiss";
6
+
7
+ interface BackdropPointerEventsResult {
8
+ pointerEvents: "box-none" | undefined;
9
+ backdropBehavior: BackdropBehavior;
10
+ }
11
+
12
+ /**
13
+ * Returns pointer events and backdrop behavior based on screen options.
14
+ *
15
+ * - Explicit `backdropBehavior` option takes precedence
16
+ * - Component stacks default to 'passthrough' (box-none)
17
+ * - Other stacks default to 'block' (undefined = normal touch handling)
18
+ */
19
+ export function useBackdropPointerEvents(): BackdropPointerEventsResult {
20
+ const { current } = useKeys();
21
+ const { flags } = useStackCoreContext();
22
+
23
+ const isComponentStack = flags.STACK_TYPE === StackType.COMPONENT;
24
+ const backdropBehavior: BackdropBehavior =
25
+ current.options.backdropBehavior ??
26
+ (isComponentStack ? "passthrough" : "block");
27
+
28
+ const pointerEvents =
29
+ backdropBehavior === "passthrough" ? "box-none" : undefined;
30
+
31
+ return { pointerEvents, backdropBehavior };
32
+ }
@@ -6,7 +6,7 @@ import {
6
6
  import type { SharedValue } from "react-native-reanimated";
7
7
  import { useSharedValue } from "react-native-reanimated";
8
8
  import { useBuildGestures } from "../hooks/gestures/use-build-gestures";
9
- import { useStackPointerEvents } from "../hooks/use-stack-pointer-events";
9
+ import { useBackdropPointerEvents } from "../hooks/use-backdrop-pointer-events";
10
10
  import type { GestureStoreMap } from "../stores/gesture.store";
11
11
  import createProvider from "../utils/create-provider";
12
12
  import { useKeys } from "./screen/keys.provider";
@@ -18,6 +18,7 @@ export type ScrollConfig = {
18
18
  contentWidth: number;
19
19
  layoutHeight: number;
20
20
  layoutWidth: number;
21
+ isTouched: boolean;
21
22
  };
22
23
 
23
24
  export interface GestureContextType {
@@ -42,9 +43,9 @@ export const {
42
43
  GestureContextType
43
44
  >(({ children }) => {
44
45
  const { current } = useKeys();
45
- const pointerEvents = useStackPointerEvents();
46
46
  const ancestorContext = useGestureContext();
47
47
  const scrollConfig = useSharedValue<ScrollConfig | null>(null);
48
+ const { pointerEvents } = useBackdropPointerEvents();
48
49
 
49
50
  const hasGestures = current.options.gestureEnabled === true;
50
51
 
@@ -1,5 +1,5 @@
1
1
  import type React from "react";
2
- import { RootTransitionAware } from "../../components/root-transition-aware";
2
+ import { ScreenContainer } from "../../components/screen-container";
3
3
  import { ScreenLifecycle } from "../../components/screen-lifecycle";
4
4
  import { ScreenGestureProvider } from "../gestures.provider";
5
5
  import { type BaseDescriptor, KeysProvider } from "./keys.provider";
@@ -23,7 +23,7 @@ export function ScreenComposer<TDescriptor extends BaseDescriptor>({
23
23
  <KeysProvider previous={previous} current={current} next={next}>
24
24
  <ScreenGestureProvider>
25
25
  <ScreenStylesProvider>
26
- <RootTransitionAware>{children}</RootTransitionAware>
26
+ <ScreenContainer>{children}</ScreenContainer>
27
27
  </ScreenStylesProvider>
28
28
  </ScreenGestureProvider>
29
29
  </KeysProvider>
@@ -28,56 +28,62 @@ export function ScreenStylesProvider({ children }: Props) {
28
28
  const { screenInterpolatorProps, nextInterpolator, currentInterpolator } =
29
29
  _useScreenAnimation();
30
30
 
31
- // Track when a gesture is triggered while another screen is closing
32
- const hasTriggeredGestureWhileInFlight = useSharedValue(false);
31
+ /**
32
+ * Tracks when user starts a gesture while another screen is still closing.
33
+ * This persists until both the gesture ends AND the closing animation completes.
34
+ */
35
+ const isGesturingDuringCloseAnimation = useSharedValue(false);
33
36
 
34
37
  const stylesMap = useDerivedValue<TransitionInterpolatedStyle>(() => {
35
38
  "worklet";
36
39
  const props = screenInterpolatorProps.value;
37
- const bounds = createBounds(props);
40
+ const { current, next, progress, stackProgress } = props;
41
+ const isDragging = current.gesture.isDragging;
42
+ const isNextClosing = !!next?.closing;
38
43
 
39
- // Detect when user starts gesture on current screen while next screen is closing
40
- if (props.current.gesture.isDragging && props.next?.closing) {
41
- hasTriggeredGestureWhileInFlight.value = true;
44
+ if (isDragging && isNextClosing) {
45
+ isGesturingDuringCloseAnimation.value = true;
42
46
  }
43
47
 
44
- // Reset the flag when no longer dragging and next screen is done closing
45
- if (
46
- !props.current.gesture.isDragging &&
47
- !props.next?.closing &&
48
- hasTriggeredGestureWhileInFlight.value
49
- ) {
50
- hasTriggeredGestureWhileInFlight.value = false;
48
+ if (!isDragging && !isNextClosing) {
49
+ isGesturingDuringCloseAnimation.value = false;
51
50
  }
52
51
 
53
- // Use current interpolator when gesture triggered while in-flight,
54
- // otherwise use next interpolator if available (normal case)
55
- const shouldUseCurrentInterpolator =
56
- props.current.gesture.isDragging ||
57
- hasTriggeredGestureWhileInFlight.value;
52
+ const isInGestureMode = isDragging || isGesturingDuringCloseAnimation.value;
58
53
 
59
- const interpolator = shouldUseCurrentInterpolator
54
+ const hasPushedScreenWhileClosing =
55
+ !isInGestureMode && isNextClosing && stackProgress > progress;
56
+
57
+ // Select interpolator
58
+ // - If in gesture mode, use current screen's interpolator since we're driving
59
+ // the animation from this screen (dragging back to dismiss next).
60
+ const interpolator = isInGestureMode
60
61
  ? currentInterpolator
61
62
  : (nextInterpolator ?? currentInterpolator);
62
63
 
63
- /**
64
- * Maintainer Note:
65
- * To avoid unnecessary jumps in off directions, we have to snap back to the currents progress.
66
- * While this still introduces a 'snap back' animation, it's still very rare that a user would encounter this unless
67
- * they're spamming things out. Not ideal, but this is the best way to go about dealing with fast rapid gestures.
68
- *
69
- * The alternative was preventing users from actually being able to drag back while animation was still in flight. But there was a significant delay
70
- * when waiting for gestures to register again.
71
- */
72
- const effectiveProps = shouldUseCurrentInterpolator
73
- ? { ...props, progress: props.current.progress, next: undefined }
74
- : props;
64
+ if (!interpolator) return NO_STYLES;
65
+
66
+ // Build effective props with corrected progress
67
+ // - Gesture mode: use current.progress only (avoids jumps during drag)
68
+ // - Pushed while closing: use stackProgress (includes new screen)
69
+ // - Normal: use derived progress as-is
70
+
71
+ let effectiveProgress = progress;
72
+ let effectiveNext = next;
73
+
74
+ if (isInGestureMode) {
75
+ effectiveProgress = current.progress;
76
+ effectiveNext = undefined;
77
+ } else if (hasPushedScreenWhileClosing) {
78
+ effectiveProgress = stackProgress;
79
+ }
75
80
 
76
81
  try {
77
- if (!interpolator) return NO_STYLES;
78
82
  return interpolator({
79
- ...effectiveProps,
80
- bounds,
83
+ ...props,
84
+ progress: effectiveProgress,
85
+ next: effectiveNext,
86
+ bounds: createBounds(props),
81
87
  });
82
88
  } catch (err) {
83
89
  if (__DEV__) {
@@ -182,7 +182,18 @@ export type TransitionInterpolatedStyle = {
182
182
  contentStyle?: StyleProps;
183
183
 
184
184
  /**
185
- * Animated style for a semi-transparent overlay. Styles are only applied when Transition.View is present.
185
+ * Animated style for the semi-transparent backdrop layer behind screen content.
186
+ *
187
+ * @example
188
+ * backdropStyle: {
189
+ * backgroundColor: "black",
190
+ * opacity: interpolate(progress, [0, 1], [0, 0.5]),
191
+ * }
192
+ */
193
+ backdropStyle?: StyleProps;
194
+
195
+ /**
196
+ * @deprecated Use `backdropStyle` instead. Will be removed in next major version.
186
197
  */
187
198
  overlayStyle?: StyleProps;
188
199
 
@@ -198,9 +209,25 @@ export type TransitionInterpolatedStyle = {
198
209
  export type AnimationConfig = WithSpringConfig | WithTimingConfig;
199
210
 
200
211
  /**
201
- * Defines separate animation configurations for opening and closing a screen.
212
+ * Defines separate animation configurations for screen transitions and snap point changes.
202
213
  */
203
214
  export interface TransitionSpec {
215
+ /**
216
+ * Animation config for opening/entering a screen.
217
+ */
204
218
  open?: AnimationConfig;
219
+ /**
220
+ * Animation config for closing/exiting a screen.
221
+ */
205
222
  close?: AnimationConfig;
223
+ /**
224
+ * Animation config for expanding to a higher snap point.
225
+ * Uses lower intensity than `open` to match smaller movement distances.
226
+ */
227
+ expand?: AnimationConfig;
228
+ /**
229
+ * Animation config for collapsing to a lower snap point.
230
+ * Uses lower intensity than `close` to match smaller movement distances.
231
+ */
232
+ collapse?: AnimationConfig;
206
233
  }
@@ -136,4 +136,33 @@ export type ScreenTransitionConfig = {
136
136
  * @default false
137
137
  */
138
138
  experimental_enableHighRefreshRate?: boolean;
139
+
140
+ /**
141
+ * Describes heights where a screen can rest, as fractions of screen height.
142
+ * Pass an array of ascending values from 0 to 1.
143
+ *
144
+ * @example
145
+ * snapPoints={[0.5, 1.0]} // 50% and 100% of screen height
146
+ *
147
+ * @default [1.0]
148
+ */
149
+ snapPoints?: number[];
150
+
151
+ /**
152
+ * The initial snap point index when the screen opens.
153
+ *
154
+ * @default 0
155
+ */
156
+ initialSnapIndex?: number;
157
+
158
+ /**
159
+ * Controls how touches interact with the backdrop area (outside the screen content).
160
+ *
161
+ * - `'block'`: Backdrop catches all touches (default for most screens)
162
+ * - `'passthrough'`: Touches pass through to content behind (default for component stacks)
163
+ * - `'dismiss'`: Tapping backdrop dismisses the screen
164
+ *
165
+ * @default 'block' (or 'passthrough' for component stacks)
166
+ */
167
+ backdropBehavior?: "block" | "passthrough" | "dismiss";
139
168
  };
@@ -4,8 +4,14 @@ import type { AnimationStoreMap } from "../../stores/animation.store";
4
4
  import type { TransitionSpec } from "../../types/animation.types";
5
5
  import { animate } from "./animate";
6
6
 
7
- interface StartScreenTransitionProps {
8
- target: "open" | "close";
7
+ interface AnimateToProgressProps {
8
+ /**
9
+ * Target for the animation:
10
+ * - "open" = animate to progress 1
11
+ * - "close" = animate to progress 0
12
+ * - number = animate to specific progress value (e.g., 0.5 for snap point)
13
+ */
14
+ target: "open" | "close" | number;
9
15
  spec?: TransitionSpec;
10
16
  onAnimationFinish?: (finished: boolean) => void;
11
17
  animations: AnimationStoreMap;
@@ -13,16 +19,22 @@ interface StartScreenTransitionProps {
13
19
  initialVelocity?: number;
14
20
  }
15
21
 
16
- export const startScreenTransition = ({
22
+ export const animateToProgress = ({
17
23
  target,
18
24
  spec,
19
25
  onAnimationFinish,
20
26
  animations,
21
27
  initialVelocity,
22
- }: StartScreenTransitionProps) => {
28
+ }: AnimateToProgressProps) => {
23
29
  "worklet";
24
- const value = target === "open" ? 1 : 0;
25
- const config = target === "open" ? spec?.open : spec?.close;
30
+
31
+ // Determine target value and direction
32
+ const isClosing =
33
+ target === "close" || (typeof target === "number" && target === 0);
34
+ const value = typeof target === "number" ? target : target === "open" ? 1 : 0;
35
+
36
+ // Select spec based on direction (closing uses close spec, otherwise open)
37
+ const config = isClosing ? spec?.close : spec?.open;
26
38
 
27
39
  const isSpringConfig =
28
40
  !!config && !("duration" in config) && !("easing" in config);
@@ -34,7 +46,7 @@ export const startScreenTransition = ({
34
46
 
35
47
  const { progress, animating, closing, entering } = animations;
36
48
 
37
- if (target === "close") {
49
+ if (isClosing) {
38
50
  closing.set(TRUE);
39
51
  entering.set(FALSE);
40
52
  } else {
@@ -308,3 +308,81 @@ export const applyOffsetRules = ({
308
308
  isSwipingLeft,
309
309
  };
310
310
  };
311
+
312
+ interface ScrollAwareActivationParams {
313
+ swipeInfo: {
314
+ isSwipingDown: boolean;
315
+ isSwipingUp: boolean;
316
+ isSwipingRight: boolean;
317
+ isSwipingLeft: boolean;
318
+ };
319
+ directions: Directions;
320
+ scrollX: number;
321
+ scrollY: number;
322
+ maxScrollX: number;
323
+ maxScrollY: number;
324
+ hasSnapPoints?: boolean;
325
+ canExpandMore?: boolean;
326
+ }
327
+
328
+ type GestureDirection =
329
+ | "vertical"
330
+ | "vertical-inverted"
331
+ | "horizontal"
332
+ | "horizontal-inverted";
333
+
334
+ /**
335
+ * Checks if a gesture should activate based on scroll position.
336
+ * Returns the direction to activate for, or null if activation should not occur.
337
+ */
338
+ export function checkScrollAwareActivation({
339
+ swipeInfo,
340
+ directions,
341
+ scrollX,
342
+ scrollY,
343
+ maxScrollX,
344
+ maxScrollY,
345
+ hasSnapPoints,
346
+ canExpandMore,
347
+ }: ScrollAwareActivationParams): {
348
+ shouldActivate: boolean;
349
+ direction: GestureDirection | null;
350
+ } {
351
+ "worklet";
352
+
353
+ const { isSwipingDown, isSwipingUp, isSwipingRight, isSwipingLeft } =
354
+ swipeInfo;
355
+
356
+ if (directions.vertical && isSwipingDown && scrollY <= 0) {
357
+ return { shouldActivate: true, direction: "vertical" };
358
+ }
359
+
360
+ if (directions.horizontal && isSwipingRight && scrollX <= 0) {
361
+ return { shouldActivate: true, direction: "horizontal" };
362
+ }
363
+
364
+ if (directions.verticalInverted && isSwipingUp && scrollY >= maxScrollY) {
365
+ return { shouldActivate: true, direction: "vertical-inverted" };
366
+ }
367
+
368
+ if (directions.horizontalInverted && isSwipingLeft && scrollX >= maxScrollX) {
369
+ return { shouldActivate: true, direction: "horizontal-inverted" };
370
+ }
371
+
372
+ if (hasSnapPoints && canExpandMore) {
373
+ // Vertical sheet: swipe up at scroll top → expand
374
+ const enabledYAxis = directions.vertical || directions.verticalInverted;
375
+ const enabledXAxis = directions.horizontal || directions.horizontalInverted;
376
+
377
+ if (enabledYAxis && isSwipingUp && scrollY <= 0) {
378
+ return { shouldActivate: true, direction: "vertical-inverted" };
379
+ }
380
+
381
+ // Horizontal sheet: swipe left at scroll left → expand
382
+ if (enabledXAxis && isSwipingLeft && scrollX <= 0) {
383
+ return { shouldActivate: true, direction: "horizontal-inverted" };
384
+ }
385
+ }
386
+
387
+ return { shouldActivate: false, direction: null };
388
+ }
@@ -0,0 +1,75 @@
1
+ interface DetermineSnapTargetProps {
2
+ currentProgress: number;
3
+ snapPoints: number[];
4
+ /** Velocity along the snap axis (positive = toward dismiss) */
5
+ velocity: number;
6
+ /** Screen dimension along the snap axis (width or height) */
7
+ dimension: number;
8
+ /** How much velocity affects the snap decision (0-1). Default 0.15 */
9
+ velocityFactor?: number;
10
+ /** Whether dismiss (progress=0) is allowed. Default true */
11
+ canDismiss?: boolean;
12
+ }
13
+
14
+ interface DetermineSnapTargetResult {
15
+ targetProgress: number;
16
+ shouldDismiss: boolean;
17
+ }
18
+
19
+ /**
20
+ * Determines which snap point to animate to based on current progress and velocity.
21
+ *
22
+ * Logic: Snap to whichever point is closest, factoring in velocity.
23
+ * The "zones" between snap points are split at midpoints.
24
+ * Velocity can push you into the next zone.
25
+ */
26
+ export function determineSnapTarget({
27
+ currentProgress,
28
+ snapPoints,
29
+ velocity,
30
+ dimension,
31
+ velocityFactor = 0.15,
32
+ canDismiss = true,
33
+ }: DetermineSnapTargetProps): DetermineSnapTargetResult {
34
+ "worklet";
35
+
36
+ // Convert velocity to progress units (positive = toward dismiss = decreasing progress)
37
+ const velocityInProgress = (velocity / dimension) * velocityFactor;
38
+
39
+ // Project where we'd end up with velocity
40
+ const projectedProgress = currentProgress - velocityInProgress;
41
+
42
+ // Build all possible targets: dismiss (0) only if allowed, plus all snap points
43
+ const allTargets = canDismiss
44
+ ? [0, ...snapPoints].sort((a, b) => a - b)
45
+ : [...snapPoints].sort((a, b) => a - b);
46
+
47
+ // Find the target whose zone contains the projected progress
48
+ // Zones are split at midpoints between adjacent targets
49
+ let targetProgress = allTargets[0];
50
+
51
+ for (let i = 0; i < allTargets.length; i++) {
52
+ const current = allTargets[i];
53
+ const next = allTargets[i + 1];
54
+
55
+ if (next === undefined) {
56
+ // Last target - if we're above the midpoint to here, snap to it
57
+ targetProgress = current;
58
+ break;
59
+ }
60
+
61
+ const midpoint = (current + next) / 2;
62
+
63
+ if (projectedProgress < midpoint) {
64
+ targetProgress = current;
65
+ break;
66
+ }
67
+
68
+ targetProgress = next;
69
+ }
70
+
71
+ return {
72
+ targetProgress,
73
+ shouldDismiss: targetProgress === 0,
74
+ };
75
+ }
@@ -0,0 +1,37 @@
1
+ interface ValidateSnapPointsResult {
2
+ hasSnapPoints: boolean;
3
+ snapPoints: number[];
4
+ minSnapPoint: number;
5
+ maxSnapPoint: number;
6
+ }
7
+
8
+ interface ValidateSnapPointsOptions {
9
+ snapPoints?: number[];
10
+ canDismiss?: boolean;
11
+ }
12
+
13
+ export const validateSnapPoints = ({
14
+ snapPoints,
15
+ canDismiss,
16
+ }: ValidateSnapPointsOptions): ValidateSnapPointsResult => {
17
+ if (!snapPoints) {
18
+ return {
19
+ hasSnapPoints: false,
20
+ snapPoints: [],
21
+ minSnapPoint: -1,
22
+ maxSnapPoint: -1,
23
+ };
24
+ }
25
+
26
+ const sortedSnaps = snapPoints.slice().sort((a, b) => a - b);
27
+ // Clamp to snap point bounds (dismiss at 0 only if allowed)
28
+ const minProgress = canDismiss ? 0 : sortedSnaps[0];
29
+ const maxProgress = sortedSnaps[sortedSnaps.length - 1];
30
+
31
+ return {
32
+ hasSnapPoints: true,
33
+ snapPoints: sortedSnaps,
34
+ minSnapPoint: minProgress,
35
+ maxSnapPoint: maxProgress,
36
+ };
37
+ };
@@ -34,6 +34,15 @@ const normalize = (velocityPixelsPerSecond: number, screenSize: number) => {
34
34
  );
35
35
  };
36
36
 
37
+ /**
38
+ * Normalizes translation to -1...1 range (for gesture tracking).
39
+ * Used to convert pixel translation to normalized gesture values.
40
+ */
41
+ const normalizeTranslation = (translation: number, dimension: number) => {
42
+ "worklet";
43
+ return clamp(translation / Math.max(1, dimension), -1, 1);
44
+ };
45
+
37
46
  /**
38
47
  * Calculates a normalized velocity that moves the current value toward zero.
39
48
  * Used for spring-back animations when dismissing gestures.
@@ -137,6 +146,7 @@ const shouldPassDismissalThreshold = (
137
146
 
138
147
  export const velocity = {
139
148
  normalize,
149
+ normalizeTranslation,
140
150
  calculateRestoreVelocity,
141
151
  calculateProgressVelocity,
142
152
  shouldPassDismissalThreshold,
@@ -1 +0,0 @@
1
- {"version":3,"names":["_react","require","_reactNative","_reactNativeReanimated","_interopRequireWildcard","_constants","_useStackPointerEvents","_styles","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","RootTransitionAware","exports","memo","children","stylesMap","useScreenStyles","pointerEvents","useStackPointerEvents","animatedContentStyle","useAnimatedStyle","value","contentStyle","NO_STYLES","animatedOverlayStyle","overlayStyle","jsxs","View","style","styles","container","jsx","StyleSheet","absoluteFillObject","content","create","flex"],"sourceRoot":"../../../../src","sources":["shared/components/root-transition-aware.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,sBAAA,GAAAC,uBAAA,CAAAH,OAAA;AACA,IAAAI,UAAA,GAAAJ,OAAA;AACA,IAAAK,sBAAA,GAAAL,OAAA;AACA,IAAAM,OAAA,GAAAN,OAAA;AAAsE,IAAAO,WAAA,GAAAP,OAAA;AAAA,SAAAG,wBAAAK,CAAA,EAAAC,CAAA,6BAAAC,OAAA,MAAAC,CAAA,OAAAD,OAAA,IAAAE,CAAA,OAAAF,OAAA,YAAAP,uBAAA,YAAAA,CAAAK,CAAA,EAAAC,CAAA,SAAAA,CAAA,IAAAD,CAAA,IAAAA,CAAA,CAAAK,UAAA,SAAAL,CAAA,MAAAM,CAAA,EAAAC,CAAA,EAAAC,CAAA,KAAAC,SAAA,QAAAC,OAAA,EAAAV,CAAA,iBAAAA,CAAA,uBAAAA,CAAA,yBAAAA,CAAA,SAAAQ,CAAA,MAAAF,CAAA,GAAAL,CAAA,GAAAG,CAAA,GAAAD,CAAA,QAAAG,CAAA,CAAAK,GAAA,CAAAX,CAAA,UAAAM,CAAA,CAAAM,GAAA,CAAAZ,CAAA,GAAAM,CAAA,CAAAO,GAAA,CAAAb,CAAA,EAAAQ,CAAA,gBAAAP,CAAA,IAAAD,CAAA,gBAAAC,CAAA,OAAAa,cAAA,CAAAC,IAAA,CAAAf,CAAA,EAAAC,CAAA,OAAAM,CAAA,IAAAD,CAAA,GAAAU,MAAA,CAAAC,cAAA,KAAAD,MAAA,CAAAE,wBAAA,CAAAlB,CAAA,EAAAC,CAAA,OAAAM,CAAA,CAAAK,GAAA,IAAAL,CAAA,CAAAM,GAAA,IAAAP,CAAA,CAAAE,CAAA,EAAAP,CAAA,EAAAM,CAAA,IAAAC,CAAA,CAAAP,CAAA,IAAAD,CAAA,CAAAC,CAAA,WAAAO,CAAA,KAAAR,CAAA,EAAAC,CAAA;AAM/D,MAAMkB,mBAAmB,GAAAC,OAAA,CAAAD,mBAAA,gBAAG,IAAAE,WAAI,EAAC,CAAC;EAAEC;AAAgB,CAAC,KAAK;EAChE,MAAM;IAAEC;EAAU,CAAC,GAAG,IAAAC,uBAAe,EAAC,CAAC;EACvC,MAAMC,aAAa,GAAG,IAAAC,4CAAqB,EAAC,CAAC;EAE7C,MAAMC,oBAAoB,GAAG,IAAAC,uCAAgB,EAAC,MAAM;IACnD,SAAS;;IACT,OAAOL,SAAS,CAACM,KAAK,CAACC,YAAY,IAAIC,oBAAS;EACjD,CAAC,CAAC;EAEF,MAAMC,oBAAoB,GAAG,IAAAJ,uCAAgB,EAAC,MAAM;IACnD,SAAS;;IACT,OAAOL,SAAS,CAACM,KAAK,CAACI,YAAY,IAAIF,oBAAS;EACjD,CAAC,CAAC;EAEF,oBACC,IAAAhC,WAAA,CAAAmC,IAAA,EAACzC,YAAA,CAAA0C,IAAI;IAACC,KAAK,EAAE,CAACC,MAAM,CAACC,SAAS,CAAE;IAACb,aAAa,EAAEA,aAAc;IAAAH,QAAA,gBAC7D,IAAAvB,WAAA,CAAAwC,GAAA,EAAC7C,sBAAA,CAAAgB,OAAQ,CAACyB,IAAI;MACbC,KAAK,EAAE,CAACI,uBAAU,CAACC,kBAAkB,EAAET,oBAAoB,CAAE;MAC7DP,aAAa,EAAC;IAAM,CACpB,CAAC,eACF,IAAA1B,WAAA,CAAAwC,GAAA,EAAC7C,sBAAA,CAAAgB,OAAQ,CAACyB,IAAI;MACbC,KAAK,EAAE,CAACC,MAAM,CAACK,OAAO,EAAEf,oBAAoB,CAAE;MAC9CF,aAAa,EAAEA,aAAc;MAAAH,QAAA,EAE5BA;IAAQ,CACK,CAAC;EAAA,CACX,CAAC;AAET,CAAC,CAAC;AAEF,MAAMe,MAAM,GAAGG,uBAAU,CAACG,MAAM,CAAC;EAChCL,SAAS,EAAE;IACVM,IAAI,EAAE;EACP,CAAC;EACDF,OAAO,EAAE;IACRE,IAAI,EAAE;EACP;AACD,CAAC,CAAC","ignoreList":[]}