react-native-screen-transitions 3.2.0-beta.3 → 3.2.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 (138) hide show
  1. package/README.md +327 -672
  2. package/lib/commonjs/shared/components/screen-lifecycle.js +9 -133
  3. package/lib/commonjs/shared/components/screen-lifecycle.js.map +1 -1
  4. package/lib/commonjs/shared/constants.js +1 -0
  5. package/lib/commonjs/shared/constants.js.map +1 -1
  6. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +3 -0
  7. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  8. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js +127 -0
  9. package/lib/commonjs/shared/hooks/lifecycle/use-close-transition.js.map +1 -0
  10. package/lib/commonjs/shared/hooks/lifecycle/use-open-transition.js +35 -0
  11. package/lib/commonjs/shared/hooks/lifecycle/use-open-transition.js.map +1 -0
  12. package/lib/commonjs/shared/hooks/lifecycle/use-screen-events.js +58 -0
  13. package/lib/commonjs/shared/hooks/lifecycle/use-screen-events.js.map +1 -0
  14. package/lib/commonjs/shared/hooks/navigation/use-history.js +24 -0
  15. package/lib/commonjs/shared/hooks/navigation/use-history.js.map +1 -0
  16. package/lib/commonjs/shared/index.js +7 -0
  17. package/lib/commonjs/shared/index.js.map +1 -1
  18. package/lib/commonjs/shared/providers/screen/keys.provider.js +0 -4
  19. package/lib/commonjs/shared/providers/screen/keys.provider.js.map +1 -1
  20. package/lib/commonjs/shared/providers/screen/screen-composer.js +7 -5
  21. package/lib/commonjs/shared/providers/screen/screen-composer.js.map +1 -1
  22. package/lib/commonjs/shared/providers/screen/styles.provider.js +41 -32
  23. package/lib/commonjs/shared/providers/screen/styles.provider.js.map +1 -1
  24. package/lib/commonjs/shared/providers/stack/direct.provider.js +9 -0
  25. package/lib/commonjs/shared/providers/stack/direct.provider.js.map +1 -1
  26. package/lib/commonjs/shared/providers/stack/managed.provider.js +9 -0
  27. package/lib/commonjs/shared/providers/stack/managed.provider.js.map +1 -1
  28. package/lib/commonjs/shared/stores/animation.store.js +3 -13
  29. package/lib/commonjs/shared/stores/animation.store.js.map +1 -1
  30. package/lib/commonjs/shared/stores/history.store.js +185 -0
  31. package/lib/commonjs/shared/stores/history.store.js.map +1 -0
  32. package/lib/commonjs/shared/types/stack.types.js.map +1 -1
  33. package/lib/commonjs/shared/utils/animation/start-screen-transition.js +5 -1
  34. package/lib/commonjs/shared/utils/animation/start-screen-transition.js.map +1 -1
  35. package/lib/commonjs/shared/utils/bounds/index.js +19 -4
  36. package/lib/commonjs/shared/utils/bounds/index.js.map +1 -1
  37. package/lib/module/shared/components/screen-lifecycle.js +9 -132
  38. package/lib/module/shared/components/screen-lifecycle.js.map +1 -1
  39. package/lib/module/shared/constants.js +1 -0
  40. package/lib/module/shared/constants.js.map +1 -1
  41. package/lib/module/shared/hooks/animation/use-screen-animation.js +3 -0
  42. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  43. package/lib/module/shared/hooks/lifecycle/use-close-transition.js +122 -0
  44. package/lib/module/shared/hooks/lifecycle/use-close-transition.js.map +1 -0
  45. package/lib/module/shared/hooks/lifecycle/use-open-transition.js +32 -0
  46. package/lib/module/shared/hooks/lifecycle/use-open-transition.js.map +1 -0
  47. package/lib/module/shared/hooks/lifecycle/use-screen-events.js +54 -0
  48. package/lib/module/shared/hooks/lifecycle/use-screen-events.js.map +1 -0
  49. package/lib/module/shared/hooks/navigation/use-history.js +20 -0
  50. package/lib/module/shared/hooks/navigation/use-history.js.map +1 -0
  51. package/lib/module/shared/index.js +1 -0
  52. package/lib/module/shared/index.js.map +1 -1
  53. package/lib/module/shared/providers/screen/keys.provider.js +0 -4
  54. package/lib/module/shared/providers/screen/keys.provider.js.map +1 -1
  55. package/lib/module/shared/providers/screen/screen-composer.js +7 -5
  56. package/lib/module/shared/providers/screen/screen-composer.js.map +1 -1
  57. package/lib/module/shared/providers/screen/styles.provider.js +41 -32
  58. package/lib/module/shared/providers/screen/styles.provider.js.map +1 -1
  59. package/lib/module/shared/providers/stack/direct.provider.js +10 -1
  60. package/lib/module/shared/providers/stack/direct.provider.js.map +1 -1
  61. package/lib/module/shared/providers/stack/managed.provider.js +10 -1
  62. package/lib/module/shared/providers/stack/managed.provider.js.map +1 -1
  63. package/lib/module/shared/stores/animation.store.js +4 -14
  64. package/lib/module/shared/stores/animation.store.js.map +1 -1
  65. package/lib/module/shared/stores/history.store.js +181 -0
  66. package/lib/module/shared/stores/history.store.js.map +1 -0
  67. package/lib/module/shared/types/stack.types.js.map +1 -1
  68. package/lib/module/shared/utils/animation/start-screen-transition.js +5 -1
  69. package/lib/module/shared/utils/animation/start-screen-transition.js.map +1 -1
  70. package/lib/module/shared/utils/bounds/index.js +19 -4
  71. package/lib/module/shared/utils/bounds/index.js.map +1 -1
  72. package/lib/typescript/blank-stack/types.d.ts +0 -3
  73. package/lib/typescript/blank-stack/types.d.ts.map +1 -1
  74. package/lib/typescript/component-stack/types.d.ts +0 -3
  75. package/lib/typescript/component-stack/types.d.ts.map +1 -1
  76. package/lib/typescript/shared/components/screen-lifecycle.d.ts +4 -1
  77. package/lib/typescript/shared/components/screen-lifecycle.d.ts.map +1 -1
  78. package/lib/typescript/shared/constants.d.ts.map +1 -1
  79. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  80. package/lib/typescript/shared/hooks/lifecycle/use-close-transition.d.ts +13 -0
  81. package/lib/typescript/shared/hooks/lifecycle/use-close-transition.d.ts.map +1 -0
  82. package/lib/typescript/shared/hooks/lifecycle/use-open-transition.d.ts +11 -0
  83. package/lib/typescript/shared/hooks/lifecycle/use-open-transition.d.ts.map +1 -0
  84. package/lib/typescript/shared/hooks/lifecycle/use-screen-events.d.ts +7 -0
  85. package/lib/typescript/shared/hooks/lifecycle/use-screen-events.d.ts.map +1 -0
  86. package/lib/typescript/shared/hooks/navigation/use-history.d.ts +37 -0
  87. package/lib/typescript/shared/hooks/navigation/use-history.d.ts.map +1 -0
  88. package/lib/typescript/shared/index.d.ts +3 -2
  89. package/lib/typescript/shared/index.d.ts.map +1 -1
  90. package/lib/typescript/shared/providers/screen/keys.provider.d.ts +0 -6
  91. package/lib/typescript/shared/providers/screen/keys.provider.d.ts.map +1 -1
  92. package/lib/typescript/shared/providers/screen/screen-composer.d.ts.map +1 -1
  93. package/lib/typescript/shared/providers/screen/styles.provider.d.ts.map +1 -1
  94. package/lib/typescript/shared/providers/stack/direct.provider.d.ts.map +1 -1
  95. package/lib/typescript/shared/providers/stack/managed.provider.d.ts.map +1 -1
  96. package/lib/typescript/shared/stores/animation.store.d.ts +3 -4
  97. package/lib/typescript/shared/stores/animation.store.d.ts.map +1 -1
  98. package/lib/typescript/shared/stores/history.store.d.ts +82 -0
  99. package/lib/typescript/shared/stores/history.store.d.ts.map +1 -0
  100. package/lib/typescript/shared/types/animation.types.d.ts +8 -0
  101. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  102. package/lib/typescript/shared/types/bounds.types.d.ts +1 -1
  103. package/lib/typescript/shared/types/bounds.types.d.ts.map +1 -1
  104. package/lib/typescript/shared/types/stack.types.d.ts +1 -0
  105. package/lib/typescript/shared/types/stack.types.d.ts.map +1 -1
  106. package/lib/typescript/shared/utils/animation/start-screen-transition.d.ts.map +1 -1
  107. package/lib/typescript/shared/utils/bounds/index.d.ts.map +1 -1
  108. package/package.json +28 -2
  109. package/src/blank-stack/types.ts +0 -8
  110. package/src/component-stack/types.ts +0 -9
  111. package/src/shared/__tests__/history.store.test.ts +550 -0
  112. package/src/shared/components/screen-lifecycle.tsx +13 -149
  113. package/src/shared/constants.ts +1 -0
  114. package/src/shared/hooks/animation/use-screen-animation.tsx +4 -0
  115. package/src/shared/hooks/lifecycle/use-close-transition.ts +147 -0
  116. package/src/shared/hooks/lifecycle/use-open-transition.ts +30 -0
  117. package/src/shared/hooks/lifecycle/use-screen-events.ts +62 -0
  118. package/src/shared/hooks/navigation/use-history.ts +63 -0
  119. package/src/shared/index.ts +1 -0
  120. package/src/shared/providers/screen/keys.provider.tsx +0 -16
  121. package/src/shared/providers/screen/screen-composer.tsx +6 -10
  122. package/src/shared/providers/screen/styles.provider.tsx +40 -34
  123. package/src/shared/providers/stack/direct.provider.tsx +11 -1
  124. package/src/shared/providers/stack/managed.provider.tsx +11 -1
  125. package/src/shared/stores/animation.store.ts +6 -20
  126. package/src/shared/stores/history.store.ts +201 -0
  127. package/src/shared/types/animation.types.ts +9 -0
  128. package/src/shared/types/bounds.types.ts +1 -0
  129. package/src/shared/types/stack.types.ts +1 -0
  130. package/src/shared/utils/animation/start-screen-transition.ts +4 -1
  131. package/src/shared/utils/bounds/index.ts +29 -3
  132. package/lib/commonjs/shared/utils/read-shared-value.js +0 -17
  133. package/lib/commonjs/shared/utils/read-shared-value.js.map +0 -1
  134. package/lib/module/shared/utils/read-shared-value.js +0 -14
  135. package/lib/module/shared/utils/read-shared-value.js.map +0 -1
  136. package/lib/typescript/shared/utils/read-shared-value.d.ts +0 -7
  137. package/lib/typescript/shared/utils/read-shared-value.d.ts.map +0 -1
  138. package/src/shared/utils/read-shared-value.ts +0 -15
@@ -1,168 +1,32 @@
1
- import { useLayoutEffect } from "react";
2
- import {
3
- runOnJS,
4
- useAnimatedReaction,
5
- useDerivedValue,
6
- } from "react-native-reanimated";
7
- import { useHighRefreshRate } from "../hooks/animation/use-high-refresh-rate";
8
- import { useSharedValueState } from "../hooks/reanimated/use-shared-value-state";
9
- import useStableCallback from "../hooks/use-stable-callback";
10
- import { useGestureContext } from "../providers/gestures.provider";
11
- import {
12
- type BaseDescriptor,
13
- useKeys,
14
- } from "../providers/screen/keys.provider";
15
- import { useStackCoreContext } from "../providers/stack/core.provider";
16
- import { useManagedStackContext } from "../providers/stack/managed.provider";
1
+ import { useCloseTransition } from "../hooks/lifecycle/use-close-transition";
2
+ import { useOpenTransition } from "../hooks/lifecycle/use-open-transition";
3
+ import { useScreenEvents } from "../hooks/lifecycle/use-screen-events";
4
+ import type { BaseDescriptor } from "../providers/screen/keys.provider";
17
5
  import { AnimationStore } from "../stores/animation.store";
18
- import { StackType } from "../types/stack.types";
19
- import { startScreenTransition } from "../utils/animation/start-screen-transition";
20
- import { resetStoresForScreen } from "../utils/reset-stores-for-screen";
21
6
 
22
7
  interface Props {
23
8
  children: React.ReactNode;
24
- }
25
-
26
- interface CloseHookParams {
27
9
  current: BaseDescriptor;
28
- animations: ReturnType<typeof AnimationStore.getAll>;
29
- activate: () => void;
30
- deactivate: () => void;
10
+ previous?: BaseDescriptor;
31
11
  }
32
12
 
33
- /**
34
- * Managed close - reacts to closingRouteKeysShared from ManagedStackContext.
35
- * Used by blank-stack and component-stack.
36
- */
37
- const useManagedClose = ({
38
- current,
39
- animations,
40
- activate,
41
- deactivate,
42
- }: CloseHookParams) => {
43
- const { handleCloseRoute, closingRouteKeysShared } = useManagedStackContext();
44
-
45
- const handleCloseEnd = useStableCallback((finished: boolean) => {
46
- if (!finished) return;
47
- handleCloseRoute({ route: current.route });
48
- requestAnimationFrame(() => {
49
- deactivate();
50
- resetStoresForScreen(current);
51
- });
52
- });
53
-
54
- useAnimatedReaction(
55
- () => closingRouteKeysShared.value,
56
- (keys) => {
57
- if (!keys?.includes(current.route.key)) return;
58
-
59
- runOnJS(activate)();
60
- startScreenTransition({
61
- target: "close",
62
- spec: current.options.transitionSpec,
63
- animations,
64
- onAnimationFinish: handleCloseEnd,
65
- });
66
- },
67
- );
68
- };
69
-
70
- /**
71
- * Native stack close - listens to beforeRemove navigation event.
72
- */
73
- const useNativeStackClose = ({
74
- current,
75
- animations,
76
- activate,
77
- deactivate,
78
- }: CloseHookParams) => {
79
- const gestureCtx = useGestureContext();
80
-
81
- const isAncestorDismissingViaGesture = useSharedValueState(
82
- useDerivedValue(() => {
83
- "worklet";
84
- return (
85
- gestureCtx?.ancestorContext?.gestureAnimationValues.isDismissing
86
- ?.value ?? false
87
- );
88
- }),
89
- );
90
-
91
- const handleBeforeRemove = useStableCallback((e: any) => {
92
- const options = current.options as { enableTransitions?: boolean };
93
- const isEnabled = options.enableTransitions;
94
- const navigation = current.navigation;
95
- const isFirstScreen = navigation.getState().index === 0;
96
-
97
- // If transitions are disabled, ancestor is dismissing, or first screen - let native handle it
98
- if (!isEnabled || isAncestorDismissingViaGesture || isFirstScreen) {
99
- animations.closing.set(1);
100
- resetStoresForScreen(current);
101
- return;
102
- }
103
-
104
- e.preventDefault();
105
- activate();
106
-
107
- startScreenTransition({
108
- target: "close",
109
- spec: current.options.transitionSpec,
110
- animations,
111
- onAnimationFinish: (finished: boolean) => {
112
- deactivate();
113
- if (finished) {
114
- navigation.dispatch(e.data.action);
115
- requestAnimationFrame(() => {
116
- resetStoresForScreen(current);
117
- });
118
- }
119
- },
120
- });
121
- });
122
-
123
- // biome-ignore lint/correctness/useExhaustiveDependencies: Only re-subscribe when navigation changes
124
- useLayoutEffect(() => {
125
- return current.navigation.addListener?.("beforeRemove", handleBeforeRemove);
126
- }, [current.navigation]);
127
- };
128
-
129
13
  /**
130
14
  * Unified lifecycle controller for all stack types.
131
15
  */
132
- export const ScreenLifecycle = ({ children }: Props) => {
133
- const { flags } = useStackCoreContext();
134
- const { current } = useKeys();
16
+ export const ScreenLifecycle = ({ children, current, previous }: Props) => {
135
17
  const animations = AnimationStore.getAll(current.route.key);
136
- const { activateHighRefreshRate, deactivateHighRefreshRate } =
137
- useHighRefreshRate(current);
138
18
 
139
- const isNativeStack = flags.STACK_TYPE === StackType.NATIVE;
140
-
141
- // biome-ignore lint/correctness/useExhaustiveDependencies: Must only run once on mount
142
- useLayoutEffect(() => {
143
- activateHighRefreshRate();
144
- startScreenTransition({
145
- target: "open",
146
- spec: current.options.transitionSpec,
147
- animations,
148
- onAnimationFinish: deactivateHighRefreshRate,
149
- });
150
- }, []);
19
+ const { activateHighRefreshRate, deactivateHighRefreshRate } =
20
+ useOpenTransition(current, animations);
151
21
 
152
- const closeParams: CloseHookParams = {
22
+ useCloseTransition(
153
23
  current,
154
24
  animations,
155
- activate: activateHighRefreshRate,
156
- deactivate: deactivateHighRefreshRate,
157
- };
25
+ activateHighRefreshRate,
26
+ deactivateHighRefreshRate,
27
+ );
158
28
 
159
- if (isNativeStack) {
160
- // biome-ignore lint/correctness/useHookAtTopLevel: STACK_TYPE is stable per screen instance
161
- useNativeStackClose(closeParams);
162
- } else {
163
- // biome-ignore lint/correctness/useHookAtTopLevel: STACK_TYPE is stable per screen instance
164
- useManagedClose(closeParams);
165
- }
29
+ useScreenEvents(current, previous, animations);
166
30
 
167
31
  return children;
168
32
  };
@@ -24,6 +24,7 @@ export const DEFAULT_SCREEN_TRANSITION_STATE: ScreenTransitionState =
24
24
  progress: 0,
25
25
  closing: 0,
26
26
  animating: 0,
27
+ entering: 1,
27
28
  gesture: {
28
29
  x: 0,
29
30
  y: 0,
@@ -24,6 +24,7 @@ type BuiltState = {
24
24
  progress: SharedValue<number>;
25
25
  closing: SharedValue<number>;
26
26
  animating: SharedValue<number>;
27
+ entering: SharedValue<number>;
27
28
  gesture: GestureStoreMap;
28
29
  route: BaseStackRoute;
29
30
  meta?: Record<string, unknown>;
@@ -37,6 +38,7 @@ const createScreenTransitionState = (
37
38
  progress: 0,
38
39
  closing: 0,
39
40
  animating: 0,
41
+ entering: 1,
40
42
  gesture: {
41
43
  x: 0,
42
44
  y: 0,
@@ -55,6 +57,7 @@ const unwrapInto = (s: BuiltState): ScreenTransitionState => {
55
57
  const out = s.unwrapped;
56
58
  out.progress = s.progress.value;
57
59
  out.closing = s.closing.value;
60
+ out.entering = s.entering.value;
58
61
  out.animating = s.animating.value;
59
62
  out.gesture.x = s.gesture.x.value;
60
63
  out.gesture.y = s.gesture.y.value;
@@ -80,6 +83,7 @@ const useBuildScreenTransitionState = (
80
83
  return {
81
84
  progress: AnimationStore.getAnimation(key, "progress"),
82
85
  closing: AnimationStore.getAnimation(key, "closing"),
86
+ entering: AnimationStore.getAnimation(key, "entering"),
83
87
  animating: AnimationStore.getAnimation(key, "animating"),
84
88
  gesture: GestureStore.getRouteGestures(key),
85
89
  route: descriptor.route,
@@ -0,0 +1,147 @@
1
+ import { useLayoutEffect } from "react";
2
+ import {
3
+ runOnJS,
4
+ useAnimatedReaction,
5
+ useDerivedValue,
6
+ } from "react-native-reanimated";
7
+ import { useGestureContext } from "../../providers/gestures.provider";
8
+ import type { BaseDescriptor } from "../../providers/screen/keys.provider";
9
+ import { useStackCoreContext } from "../../providers/stack/core.provider";
10
+ import { useManagedStackContext } from "../../providers/stack/managed.provider";
11
+ import type { AnimationStoreMap } from "../../stores/animation.store";
12
+ import { StackType } from "../../types/stack.types";
13
+ import { startScreenTransition } from "../../utils/animation/start-screen-transition";
14
+ import { resetStoresForScreen } from "../../utils/reset-stores-for-screen";
15
+ import { useSharedValueState } from "../reanimated/use-shared-value-state";
16
+ import useStableCallback from "../use-stable-callback";
17
+
18
+ export interface CloseHookParams {
19
+ current: BaseDescriptor;
20
+ animations: AnimationStoreMap;
21
+ activate: () => void;
22
+ deactivate: () => void;
23
+ }
24
+
25
+ /**
26
+ * Managed close - reacts to closingRouteKeysShared from ManagedStackContext.
27
+ * Used by blank-stack and component-stack.
28
+ */
29
+ const useManagedClose = ({
30
+ current,
31
+ animations,
32
+ activate,
33
+ deactivate,
34
+ }: CloseHookParams) => {
35
+ const { handleCloseRoute, closingRouteKeysShared } = useManagedStackContext();
36
+
37
+ const handleCloseEnd = useStableCallback((finished: boolean) => {
38
+ if (!finished) return;
39
+ handleCloseRoute({ route: current.route });
40
+ requestAnimationFrame(() => {
41
+ deactivate();
42
+ resetStoresForScreen(current);
43
+ });
44
+ });
45
+
46
+ useAnimatedReaction(
47
+ () => closingRouteKeysShared.value,
48
+ (keys) => {
49
+ if (!keys?.includes(current.route.key)) return;
50
+
51
+ runOnJS(activate)();
52
+ startScreenTransition({
53
+ target: "close",
54
+ spec: current.options.transitionSpec,
55
+ animations,
56
+ onAnimationFinish: handleCloseEnd,
57
+ });
58
+ },
59
+ );
60
+ };
61
+
62
+ /**
63
+ * Native stack close - listens to beforeRemove navigation event.
64
+ */
65
+ const useNativeStackClose = ({
66
+ current,
67
+ animations,
68
+ activate,
69
+ deactivate,
70
+ }: CloseHookParams) => {
71
+ const gestureCtx = useGestureContext();
72
+
73
+ const isAncestorDismissingViaGesture = useSharedValueState(
74
+ useDerivedValue(() => {
75
+ "worklet";
76
+ return (
77
+ gestureCtx?.ancestorContext?.gestureAnimationValues.isDismissing
78
+ ?.value ?? false
79
+ );
80
+ }),
81
+ );
82
+
83
+ const handleBeforeRemove = useStableCallback((e: any) => {
84
+ const options = current.options as { enableTransitions?: boolean };
85
+ const isEnabled = options.enableTransitions;
86
+ const navigation = current.navigation;
87
+ const isFirstScreen = navigation.getState().index === 0;
88
+
89
+ // If transitions are disabled, ancestor is dismissing, or first screen - let native handle it
90
+ if (!isEnabled || isAncestorDismissingViaGesture || isFirstScreen) {
91
+ animations.closing.set(1);
92
+ resetStoresForScreen(current);
93
+ return;
94
+ }
95
+
96
+ e.preventDefault();
97
+ activate();
98
+
99
+ startScreenTransition({
100
+ target: "close",
101
+ spec: current.options.transitionSpec,
102
+ animations,
103
+ onAnimationFinish: (finished: boolean) => {
104
+ deactivate();
105
+ if (finished) {
106
+ navigation.dispatch(e.data.action);
107
+ requestAnimationFrame(() => {
108
+ resetStoresForScreen(current);
109
+ });
110
+ }
111
+ },
112
+ });
113
+ });
114
+
115
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Only re-subscribe when navigation changes
116
+ useLayoutEffect(() => {
117
+ return current.navigation.addListener?.("beforeRemove", handleBeforeRemove);
118
+ }, [current.navigation]);
119
+ };
120
+
121
+ /**
122
+ * Unified close handler that branches on stack type.
123
+ */
124
+ export function useCloseTransition(
125
+ current: BaseDescriptor,
126
+ animations: AnimationStoreMap,
127
+ activate: () => void,
128
+ deactivate: () => void,
129
+ ) {
130
+ const { flags } = useStackCoreContext();
131
+ const isNativeStack = flags.STACK_TYPE === StackType.NATIVE;
132
+
133
+ const closeParams: CloseHookParams = {
134
+ current,
135
+ animations,
136
+ activate,
137
+ deactivate,
138
+ };
139
+
140
+ if (isNativeStack) {
141
+ // biome-ignore lint/correctness/useHookAtTopLevel: STACK_TYPE is stable per screen instance
142
+ useNativeStackClose(closeParams);
143
+ } else {
144
+ // biome-ignore lint/correctness/useHookAtTopLevel: STACK_TYPE is stable per screen instance
145
+ useManagedClose(closeParams);
146
+ }
147
+ }
@@ -0,0 +1,30 @@
1
+ import { useLayoutEffect } from "react";
2
+ import type { BaseDescriptor } from "../../providers/screen/keys.provider";
3
+ import type { AnimationStoreMap } from "../../stores/animation.store";
4
+ import { startScreenTransition } from "../../utils/animation/start-screen-transition";
5
+ import { useHighRefreshRate } from "../animation/use-high-refresh-rate";
6
+
7
+ /**
8
+ * Handles opening animation on mount.
9
+ * Returns activate/deactivate functions for high refresh rate.
10
+ */
11
+ export function useOpenTransition(
12
+ current: BaseDescriptor,
13
+ animations: AnimationStoreMap,
14
+ ) {
15
+ const { activateHighRefreshRate, deactivateHighRefreshRate } =
16
+ useHighRefreshRate(current);
17
+
18
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Must only run once on mount
19
+ useLayoutEffect(() => {
20
+ activateHighRefreshRate();
21
+ startScreenTransition({
22
+ target: "open",
23
+ spec: current.options.transitionSpec,
24
+ animations,
25
+ onAnimationFinish: deactivateHighRefreshRate,
26
+ });
27
+ }, []);
28
+
29
+ return { activateHighRefreshRate, deactivateHighRefreshRate };
30
+ }
@@ -0,0 +1,62 @@
1
+ import { useEffect } from "react";
2
+ import { runOnJS, useAnimatedReaction } from "react-native-reanimated";
3
+ import type { BaseDescriptor } from "../../providers/screen/keys.provider";
4
+ import type { AnimationStoreMap } from "../../stores/animation.store";
5
+ import { HistoryStore } from "../../stores/history.store";
6
+ import useStableCallback from "../use-stable-callback";
7
+
8
+ /**
9
+ * Check if a screen is a leaf (renders visible content) vs a navigator container.
10
+ * Navigator containers have nested state with routes.
11
+ */
12
+ function isLeafScreen(navigation: BaseDescriptor["navigation"]): boolean {
13
+ const state = navigation.getState();
14
+ const currentRoute = state.routes[state.index];
15
+ return !("state" in currentRoute);
16
+ }
17
+
18
+ /**
19
+ * Updates the HistoryStore for navigation history tracking.
20
+ */
21
+ export function useScreenEvents(
22
+ current: BaseDescriptor,
23
+ previous: BaseDescriptor | undefined,
24
+ animations: AnimationStoreMap,
25
+ ) {
26
+ const navigatorKey = current.navigation.getState()?.key ?? "";
27
+
28
+ // Track history via focus listener - waits for nested navigators to initialize
29
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Must only run once on mount
30
+ useEffect(() => {
31
+ // Check on mount (after paint, nested navs initialized)
32
+ if (isLeafScreen(current.navigation)) {
33
+ HistoryStore.focus(current, navigatorKey);
34
+ }
35
+
36
+ // Also listen for focus events
37
+ const unsubscribe = current.navigation.addListener?.("focus", () => {
38
+ if (isLeafScreen(current.navigation)) {
39
+ HistoryStore.focus(current, navigatorKey);
40
+ }
41
+ });
42
+
43
+ return () => unsubscribe?.();
44
+ }, []);
45
+
46
+ // When closing starts, focus previous in history
47
+ const handleBlur = useStableCallback(() => {
48
+ if (previous && isLeafScreen(previous.navigation)) {
49
+ const prevNavigatorKey = previous.navigation.getState()?.key ?? "";
50
+ HistoryStore.focus(previous, prevNavigatorKey);
51
+ }
52
+ });
53
+
54
+ useAnimatedReaction(
55
+ () => animations.closing.get(),
56
+ (closing, prevClosing) => {
57
+ if (closing && !prevClosing) {
58
+ runOnJS(handleBlur)();
59
+ }
60
+ },
61
+ );
62
+ }
@@ -0,0 +1,63 @@
1
+ import { useMemo, useSyncExternalStore } from "react";
2
+ import { type HistoryEntry, HistoryStore } from "../../stores/history.store";
3
+ import type { ScreenKey } from "../../types/screen.types";
4
+
5
+ export interface UseHistoryReturn {
6
+ /**
7
+ * The full history map.
8
+ */
9
+ history: ReadonlyMap<ScreenKey, HistoryEntry>;
10
+
11
+ /**
12
+ * Get N most recent history entries.
13
+ * Most recent first.
14
+ */
15
+ getRecent: (n: number) => HistoryEntry[];
16
+
17
+ /**
18
+ * Get history entries for a specific navigator.
19
+ * Most recent first.
20
+ */
21
+ getByNavigator: (navigatorKey: string) => HistoryEntry[];
22
+
23
+ /**
24
+ * Get the path between two screens (for multi-waypoint interpolation).
25
+ * Returns screen keys in order from 'from' to 'to'.
26
+ */
27
+ getPath: (fromKey: ScreenKey, toKey: ScreenKey) => ScreenKey[];
28
+
29
+ /**
30
+ * Get a specific history entry by screen key.
31
+ */
32
+ get: (screenKey: ScreenKey) => HistoryEntry | undefined;
33
+
34
+ /**
35
+ * Get the most recent history entry (for forward navigation).
36
+ */
37
+ getMostRecent: () => HistoryEntry | undefined;
38
+ }
39
+
40
+ /**
41
+ * Subscribe to history store changes.
42
+ * Returns the full history map and helper methods.
43
+ */
44
+ export function useHistory(): UseHistoryReturn {
45
+ const history = useSyncExternalStore(
46
+ HistoryStore.subscribe,
47
+ HistoryStore.getSnapshot,
48
+ );
49
+
50
+ return useMemo(
51
+ () => ({
52
+ history,
53
+ getRecent: (n: number) => HistoryStore.getRecent(n),
54
+ getByNavigator: (navigatorKey: string) =>
55
+ HistoryStore.getByNavigator(navigatorKey),
56
+ getPath: (fromKey: ScreenKey, toKey: ScreenKey) =>
57
+ HistoryStore.getPath(fromKey, toKey),
58
+ get: (screenKey: ScreenKey) => HistoryStore.get(screenKey),
59
+ getMostRecent: () => HistoryStore.getMostRecent(),
60
+ }),
61
+ [history],
62
+ );
63
+ }
@@ -20,6 +20,7 @@ export default {
20
20
 
21
21
  export { useScreenAnimation } from "./hooks/animation/use-screen-animation";
22
22
  export { useScreenGesture } from "./hooks/gestures/use-screen-gesture";
23
+ export { useHistory } from "./hooks/navigation/use-history";
23
24
  export {
24
25
  type ScreenState,
25
26
  useScreenState,
@@ -1,11 +1,4 @@
1
- import type {
2
- Descriptor,
3
- NavigationProp,
4
- ParamListBase,
5
- RouteProp,
6
- } from "@react-navigation/native";
7
1
  import { createContext, useContext, useMemo } from "react";
8
- import type { ScreenTransitionConfig } from "../../types/screen.types";
9
2
  import type { BaseStackDescriptor } from "../../types/stack.types";
10
3
 
11
4
  /**
@@ -15,15 +8,6 @@ import type { BaseStackDescriptor } from "../../types/stack.types";
15
8
  */
16
9
  export type BaseDescriptor = BaseStackDescriptor;
17
10
 
18
- /**
19
- * React Navigation specific descriptor - extends base with full typing
20
- */
21
- export type TransitionDescriptor = Descriptor<
22
- ScreenTransitionConfig,
23
- NavigationProp<ParamListBase>,
24
- RouteProp<ParamListBase>
25
- >;
26
-
27
11
  interface KeysContextType<TDescriptor extends BaseDescriptor = BaseDescriptor> {
28
12
  previous?: TDescriptor;
29
13
  current: TDescriptor;
@@ -19,18 +19,14 @@ export function ScreenComposer<TDescriptor extends BaseDescriptor>({
19
19
  children,
20
20
  }: Props<TDescriptor>) {
21
21
  return (
22
- <KeysProvider<TDescriptor>
23
- previous={previous}
24
- current={current}
25
- next={next}
26
- >
27
- <ScreenGestureProvider>
28
- <ScreenLifecycle>
22
+ <ScreenLifecycle current={current} previous={previous}>
23
+ <KeysProvider previous={previous} current={current} next={next}>
24
+ <ScreenGestureProvider>
29
25
  <ScreenStylesProvider>
30
26
  <RootTransitionAware>{children}</RootTransitionAware>
31
27
  </ScreenStylesProvider>
32
- </ScreenLifecycle>
33
- </ScreenGestureProvider>
34
- </KeysProvider>
28
+ </ScreenGestureProvider>
29
+ </KeysProvider>
30
+ </ScreenLifecycle>
35
31
  );
36
32
  }
@@ -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__) {