react-native-screen-transitions 3.0.0-rc.2 → 3.0.0-rc.3

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 +421 -371
  2. package/lib/commonjs/blank-stack/components/{Overlay.js → overlay.js} +7 -5
  3. package/lib/commonjs/blank-stack/components/overlay.js.map +1 -0
  4. package/lib/commonjs/blank-stack/components/{Screens.js → screens.js} +8 -10
  5. package/lib/commonjs/blank-stack/components/screens.js.map +1 -0
  6. package/lib/commonjs/blank-stack/components/stack-view.js +95 -0
  7. package/lib/commonjs/blank-stack/components/stack-view.js.map +1 -0
  8. package/lib/commonjs/blank-stack/index.js +1 -8
  9. package/lib/commonjs/blank-stack/index.js.map +1 -1
  10. package/lib/commonjs/blank-stack/navigators/{createBlankStackNavigator.js → create-blank-stack-navigator.js} +3 -3
  11. package/lib/commonjs/blank-stack/navigators/create-blank-stack-navigator.js.map +1 -0
  12. package/lib/commonjs/blank-stack/utils/with-stack-navigation/helpers/compose-descriptors.js +1 -11
  13. package/lib/commonjs/blank-stack/utils/with-stack-navigation/helpers/compose-descriptors.js.map +1 -1
  14. package/lib/commonjs/blank-stack/utils/with-stack-navigation/hooks/use-closing-route-keys.js +1 -12
  15. package/lib/commonjs/blank-stack/utils/with-stack-navigation/hooks/use-closing-route-keys.js.map +1 -1
  16. package/lib/commonjs/blank-stack/utils/with-stack-navigation/hooks/use-stack-navigation-state.js.map +1 -1
  17. package/lib/commonjs/blank-stack/utils/with-stack-navigation/index.js +49 -55
  18. package/lib/commonjs/blank-stack/utils/with-stack-navigation/index.js.map +1 -1
  19. package/lib/commonjs/blank-stack/utils/with-stack-navigation/{_types.js → types.js} +1 -1
  20. package/lib/commonjs/blank-stack/utils/with-stack-navigation/types.js.map +1 -0
  21. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +38 -22
  22. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  23. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  24. package/lib/commonjs/shared/providers/flags.provider.js +25 -0
  25. package/lib/commonjs/shared/providers/flags.provider.js.map +1 -0
  26. package/lib/commonjs/shared/providers/register-bounds.provider.js +71 -45
  27. package/lib/commonjs/shared/providers/register-bounds.provider.js.map +1 -1
  28. package/lib/commonjs/shared/stores/bounds.store.js +91 -47
  29. package/lib/commonjs/shared/stores/bounds.store.js.map +1 -1
  30. package/lib/commonjs/shared/utils/bounds/helpers/is-bounds-equal.js +1 -1
  31. package/lib/commonjs/shared/utils/bounds/helpers/is-bounds-equal.js.map +1 -1
  32. package/lib/commonjs/shared/utils/bounds/index.js +4 -5
  33. package/lib/commonjs/shared/utils/bounds/index.js.map +1 -1
  34. package/lib/commonjs/shared/utils/create-provider.js +20 -1
  35. package/lib/commonjs/shared/utils/create-provider.js.map +1 -1
  36. package/lib/commonjs/shared/utils/reset-stores-for-screen.js +2 -0
  37. package/lib/commonjs/shared/utils/reset-stores-for-screen.js.map +1 -1
  38. package/lib/module/blank-stack/components/{Overlay.js → overlay.js} +7 -5
  39. package/lib/module/blank-stack/components/overlay.js.map +1 -0
  40. package/lib/module/blank-stack/components/{Screens.js → screens.js} +8 -10
  41. package/lib/module/blank-stack/components/screens.js.map +1 -0
  42. package/lib/module/blank-stack/components/stack-view.js +90 -0
  43. package/lib/module/blank-stack/components/stack-view.js.map +1 -0
  44. package/lib/module/blank-stack/index.js +1 -2
  45. package/lib/module/blank-stack/index.js.map +1 -1
  46. package/lib/module/blank-stack/navigators/{createBlankStackNavigator.js → create-blank-stack-navigator.js} +2 -2
  47. package/lib/module/blank-stack/navigators/create-blank-stack-navigator.js.map +1 -0
  48. package/lib/module/blank-stack/utils/with-stack-navigation/helpers/compose-descriptors.js +1 -11
  49. package/lib/module/blank-stack/utils/with-stack-navigation/helpers/compose-descriptors.js.map +1 -1
  50. package/lib/module/blank-stack/utils/with-stack-navigation/hooks/use-closing-route-keys.js +1 -12
  51. package/lib/module/blank-stack/utils/with-stack-navigation/hooks/use-closing-route-keys.js.map +1 -1
  52. package/lib/module/blank-stack/utils/with-stack-navigation/hooks/use-stack-navigation-state.js.map +1 -1
  53. package/lib/module/blank-stack/utils/with-stack-navigation/index.js +48 -54
  54. package/lib/module/blank-stack/utils/with-stack-navigation/index.js.map +1 -1
  55. package/lib/module/blank-stack/utils/with-stack-navigation/types.js +4 -0
  56. package/lib/module/blank-stack/utils/with-stack-navigation/types.js.map +1 -0
  57. package/lib/module/shared/hooks/animation/use-screen-animation.js +38 -22
  58. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  59. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  60. package/lib/module/shared/providers/flags.provider.js +19 -0
  61. package/lib/module/shared/providers/flags.provider.js.map +1 -0
  62. package/lib/module/shared/providers/register-bounds.provider.js +71 -45
  63. package/lib/module/shared/providers/register-bounds.provider.js.map +1 -1
  64. package/lib/module/shared/stores/bounds.store.js +91 -47
  65. package/lib/module/shared/stores/bounds.store.js.map +1 -1
  66. package/lib/module/shared/utils/bounds/helpers/is-bounds-equal.js +1 -1
  67. package/lib/module/shared/utils/bounds/helpers/is-bounds-equal.js.map +1 -1
  68. package/lib/module/shared/utils/bounds/index.js +4 -5
  69. package/lib/module/shared/utils/bounds/index.js.map +1 -1
  70. package/lib/module/shared/utils/create-provider.js +20 -1
  71. package/lib/module/shared/utils/create-provider.js.map +1 -1
  72. package/lib/module/shared/utils/reset-stores-for-screen.js +2 -0
  73. package/lib/module/shared/utils/reset-stores-for-screen.js.map +1 -1
  74. package/lib/typescript/blank-stack/components/{Overlay.d.ts → overlay.d.ts} +1 -1
  75. package/lib/typescript/blank-stack/components/overlay.d.ts.map +1 -0
  76. package/lib/typescript/blank-stack/components/{Screens.d.ts → screens.d.ts} +1 -1
  77. package/lib/typescript/blank-stack/components/{Screens.d.ts.map → screens.d.ts.map} +1 -1
  78. package/lib/typescript/blank-stack/components/stack-view.d.ts +3 -0
  79. package/lib/typescript/blank-stack/components/stack-view.d.ts.map +1 -0
  80. package/lib/typescript/blank-stack/index.d.ts +1 -2
  81. package/lib/typescript/blank-stack/index.d.ts.map +1 -1
  82. package/lib/typescript/blank-stack/navigators/{createBlankStackNavigator.d.ts → create-blank-stack-navigator.d.ts} +1 -1
  83. package/lib/typescript/blank-stack/navigators/create-blank-stack-navigator.d.ts.map +1 -0
  84. package/lib/typescript/blank-stack/types.d.ts +4 -0
  85. package/lib/typescript/blank-stack/types.d.ts.map +1 -1
  86. package/lib/typescript/blank-stack/utils/with-stack-navigation/helpers/compose-descriptors.d.ts.map +1 -1
  87. package/lib/typescript/blank-stack/utils/with-stack-navigation/hooks/use-closing-route-keys.d.ts.map +1 -1
  88. package/lib/typescript/blank-stack/utils/with-stack-navigation/hooks/use-stack-navigation-state.d.ts +1 -1
  89. package/lib/typescript/blank-stack/utils/with-stack-navigation/hooks/use-stack-navigation-state.d.ts.map +1 -1
  90. package/lib/typescript/blank-stack/utils/with-stack-navigation/index.d.ts +3 -5
  91. package/lib/typescript/blank-stack/utils/with-stack-navigation/index.d.ts.map +1 -1
  92. package/lib/typescript/blank-stack/utils/with-stack-navigation/{_types.d.ts → types.d.ts} +1 -1
  93. package/lib/typescript/blank-stack/utils/with-stack-navigation/types.d.ts.map +1 -0
  94. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  95. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  96. package/lib/typescript/shared/index.d.ts +20 -20
  97. package/lib/typescript/shared/providers/flags.provider.d.ts +10 -0
  98. package/lib/typescript/shared/providers/flags.provider.d.ts.map +1 -0
  99. package/lib/typescript/shared/providers/register-bounds.provider.d.ts.map +1 -1
  100. package/lib/typescript/shared/stores/bounds.store.d.ts +23 -11
  101. package/lib/typescript/shared/stores/bounds.store.d.ts.map +1 -1
  102. package/lib/typescript/shared/types/bounds.types.d.ts +2 -2
  103. package/lib/typescript/shared/types/bounds.types.d.ts.map +1 -1
  104. package/lib/typescript/shared/utils/bounds/index.d.ts.map +1 -1
  105. package/lib/typescript/shared/utils/create-provider.d.ts +2 -2
  106. package/lib/typescript/shared/utils/create-provider.d.ts.map +1 -1
  107. package/lib/typescript/shared/utils/reset-stores-for-screen.d.ts.map +1 -1
  108. package/package.json +2 -1
  109. package/src/blank-stack/components/{Overlay.tsx → overlay.tsx} +4 -3
  110. package/src/blank-stack/components/{Screens.tsx → screens.tsx} +7 -9
  111. package/src/blank-stack/components/stack-view.tsx +104 -0
  112. package/src/blank-stack/index.ts +1 -2
  113. package/src/blank-stack/navigators/{createBlankStackNavigator.tsx → create-blank-stack-navigator.tsx} +1 -1
  114. package/src/blank-stack/types.ts +5 -7
  115. package/src/blank-stack/utils/with-stack-navigation/helpers/compose-descriptors.ts +1 -8
  116. package/src/blank-stack/utils/with-stack-navigation/hooks/use-closing-route-keys.tsx +1 -12
  117. package/src/blank-stack/utils/with-stack-navigation/hooks/use-stack-navigation-state.tsx +1 -1
  118. package/src/blank-stack/utils/with-stack-navigation/index.tsx +42 -62
  119. package/src/shared/__tests__/bounds.store.test.ts +398 -167
  120. package/src/shared/__tests__/determine-dismissal.test.ts +2 -12
  121. package/src/shared/__tests__/geometry.test.ts +1 -1
  122. package/src/shared/__tests__/gesture.velocity.test.ts +2 -10
  123. package/src/shared/hooks/animation/use-screen-animation.tsx +55 -29
  124. package/src/shared/hooks/gestures/use-build-gestures.tsx +4 -1
  125. package/src/shared/providers/flags.provider.tsx +21 -0
  126. package/src/shared/providers/register-bounds.provider.tsx +85 -54
  127. package/src/shared/stores/bounds.store.ts +90 -54
  128. package/src/shared/types/bounds.types.ts +2 -2
  129. package/src/shared/utils/bounds/helpers/is-bounds-equal.ts +1 -1
  130. package/src/shared/utils/bounds/index.ts +7 -10
  131. package/src/shared/utils/create-provider.tsx +35 -1
  132. package/src/shared/utils/reset-stores-for-screen.ts +2 -0
  133. package/lib/commonjs/blank-stack/components/Overlay.js.map +0 -1
  134. package/lib/commonjs/blank-stack/components/Screens.js.map +0 -1
  135. package/lib/commonjs/blank-stack/components/StackView.js +0 -93
  136. package/lib/commonjs/blank-stack/components/StackView.js.map +0 -1
  137. package/lib/commonjs/blank-stack/navigators/createBlankStackNavigator.js.map +0 -1
  138. package/lib/commonjs/blank-stack/utils/with-stack-navigation/_types.js.map +0 -1
  139. package/lib/module/blank-stack/components/Overlay.js.map +0 -1
  140. package/lib/module/blank-stack/components/Screens.js.map +0 -1
  141. package/lib/module/blank-stack/components/StackView.js +0 -88
  142. package/lib/module/blank-stack/components/StackView.js.map +0 -1
  143. package/lib/module/blank-stack/navigators/createBlankStackNavigator.js.map +0 -1
  144. package/lib/module/blank-stack/utils/with-stack-navigation/_types.js +0 -4
  145. package/lib/module/blank-stack/utils/with-stack-navigation/_types.js.map +0 -1
  146. package/lib/typescript/blank-stack/components/Overlay.d.ts.map +0 -1
  147. package/lib/typescript/blank-stack/components/StackView.d.ts +0 -2
  148. package/lib/typescript/blank-stack/components/StackView.d.ts.map +0 -1
  149. package/lib/typescript/blank-stack/navigators/createBlankStackNavigator.d.ts.map +0 -1
  150. package/lib/typescript/blank-stack/utils/with-stack-navigation/_types.d.ts.map +0 -1
  151. package/src/blank-stack/components/StackView.tsx +0 -108
  152. /package/src/blank-stack/utils/with-stack-navigation/{_types.ts → types.ts} +0 -0
@@ -5,6 +5,7 @@ import { type SharedValue, useDerivedValue } from "react-native-reanimated";
5
5
  import { useSafeAreaInsets } from "react-native-safe-area-context";
6
6
  import type { NativeStackScreenTransitionConfig } from "../../../native-stack/types";
7
7
  import { DEFAULT_SCREEN_TRANSITION_STATE } from "../../constants";
8
+ import { useFlagsContext } from "../../providers/flags.provider";
8
9
  import {
9
10
  type TransitionDescriptor,
10
11
  useKeys,
@@ -16,6 +17,7 @@ import type {
16
17
  ScreenTransitionState,
17
18
  } from "../../types/animation.types";
18
19
  import type { ScreenTransitionConfig } from "../../types/core.types";
20
+ import type { GestureDirection } from "../../types/gesture.types";
19
21
  import { derivations } from "../../utils/animation/derivations";
20
22
  import { createBounds } from "../../utils/bounds";
21
23
 
@@ -25,30 +27,45 @@ type BuiltState = {
25
27
  animating: SharedValue<number>;
26
28
  gesture: GestureStoreMap;
27
29
  route: RouteProp<ParamListBase>;
30
+ unwrapped: ScreenTransitionState;
28
31
  };
29
32
 
30
- const unwrap = (
31
- s: BuiltState | undefined,
32
- key: string | undefined,
33
- ): ScreenTransitionState | undefined => {
33
+ const createScreenTransitionState = (
34
+ route: RouteProp<ParamListBase>,
35
+ ): ScreenTransitionState => ({
36
+ progress: 0,
37
+ closing: 0,
38
+ animating: 0,
39
+ gesture: {
40
+ x: 0,
41
+ y: 0,
42
+ normalizedX: 0,
43
+ normalizedY: 0,
44
+ isDismissing: 0,
45
+ isDragging: 0,
46
+ direction: null,
47
+ },
48
+ route,
49
+ });
50
+
51
+ const unwrapInto = (s: BuiltState): ScreenTransitionState => {
34
52
  "worklet";
35
- if (!s || !key) return undefined;
36
-
37
- return {
38
- progress: s.progress.value,
39
- closing: s.closing.value,
40
- animating: s.animating.value,
41
- gesture: {
42
- x: s.gesture.x.value,
43
- y: s.gesture.y.value,
44
- normalizedX: s.gesture.normalizedX.value,
45
- normalizedY: s.gesture.normalizedY.value,
46
- isDismissing: s.gesture.isDismissing.value,
47
- isDragging: s.gesture.isDragging.value,
48
- direction: s.gesture.direction.value,
49
- },
50
- route: s.route,
51
- };
53
+ const out = s.unwrapped;
54
+ out.progress = s.progress.value;
55
+ out.closing = s.closing.value;
56
+ out.animating = s.animating.value;
57
+ out.gesture.x = s.gesture.x.value;
58
+ out.gesture.y = s.gesture.y.value;
59
+ out.gesture.normalizedX = s.gesture.normalizedX.value;
60
+ out.gesture.normalizedY = s.gesture.normalizedY.value;
61
+ out.gesture.isDismissing = s.gesture.isDismissing.value;
62
+ out.gesture.isDragging = s.gesture.isDragging.value;
63
+ out.gesture.direction = s.gesture.direction.value as Omit<
64
+ GestureDirection,
65
+ "bidirectional"
66
+ > | null;
67
+
68
+ return out;
52
69
  };
53
70
 
54
71
  const useBuildScreenTransitionState = (
@@ -65,18 +82,25 @@ const useBuildScreenTransitionState = (
65
82
  animating: AnimationStore.getAnimation(key, "animating"),
66
83
  gesture: GestureStore.getRouteGestures(key),
67
84
  route: descriptor.route,
85
+ unwrapped: createScreenTransitionState(descriptor.route),
68
86
  };
69
87
  }, [key, descriptor?.route]);
70
88
  };
71
89
 
72
- const hasTransitionsEnabled = (options?: ScreenTransitionConfig) => {
90
+ const hasTransitionsEnabled = (
91
+ options: ScreenTransitionConfig | undefined,
92
+ alwaysOn: boolean,
93
+ ) => {
73
94
  "worklet";
95
+ if (alwaysOn) return true;
74
96
  return !!(options as NativeStackScreenTransitionConfig)?.enableTransitions;
75
97
  };
76
98
 
77
99
  export function _useScreenAnimation() {
78
100
  const dimensions = useWindowDimensions();
79
101
  const insets = useSafeAreaInsets();
102
+ const flags = useFlagsContext();
103
+ const transitionsAlwaysOn = flags?.TRANSITIONS_ALWAYS_ON ?? false;
80
104
 
81
105
  const {
82
106
  current: currentDescriptor,
@@ -93,15 +117,17 @@ export function _useScreenAnimation() {
93
117
  >(() => {
94
118
  "worklet";
95
119
 
96
- const previous = unwrap(prevAnimation, previousDescriptor?.route.key);
120
+ const previous = prevAnimation ? unwrapInto(prevAnimation) : undefined;
97
121
 
98
- const next = hasTransitionsEnabled(nextDescriptor?.options)
99
- ? unwrap(nextAnimation, nextDescriptor?.route.key)
100
- : undefined;
122
+ const next =
123
+ nextAnimation &&
124
+ hasTransitionsEnabled(nextDescriptor?.options, transitionsAlwaysOn)
125
+ ? unwrapInto(nextAnimation)
126
+ : undefined;
101
127
 
102
- const current =
103
- unwrap(currentAnimation, currentDescriptor?.route.key) ??
104
- DEFAULT_SCREEN_TRANSITION_STATE;
128
+ const current = currentAnimation
129
+ ? unwrapInto(currentAnimation)
130
+ : DEFAULT_SCREEN_TRANSITION_STATE;
105
131
 
106
132
  const helpers = derivations({
107
133
  current,
@@ -26,7 +26,10 @@ import { useKeys } from "../../providers/keys.provider";
26
26
  import { AnimationStore } from "../../stores/animation.store";
27
27
  import { GestureStore, type GestureStoreMap } from "../../stores/gesture.store";
28
28
 
29
- import { type GestureDirection, GestureOffsetState } from "../../types/gesture.types";
29
+ import {
30
+ type GestureDirection,
31
+ GestureOffsetState,
32
+ } from "../../types/gesture.types";
30
33
  import { startScreenTransition } from "../../utils/animation/start-screen-transition";
31
34
  import { applyOffsetRules } from "../../utils/gesture/check-gesture-activation";
32
35
  import { determineDismissal } from "../../utils/gesture/determine-dismissal";
@@ -0,0 +1,21 @@
1
+ import createProvider from "../utils/create-provider";
2
+
3
+ interface FlagsValue {
4
+ TRANSITIONS_ALWAYS_ON: boolean;
5
+ }
6
+
7
+ interface FlagsProviderProps {
8
+ TRANSITIONS_ALWAYS_ON?: boolean;
9
+ children: React.ReactNode;
10
+ }
11
+
12
+ const { FlagsProvider, useFlagsContext } = createProvider("Flags", {
13
+ guarded: false,
14
+ })<FlagsProviderProps, FlagsValue>(
15
+ ({ TRANSITIONS_ALWAYS_ON = false, children }) => ({
16
+ value: { TRANSITIONS_ALWAYS_ON },
17
+ children,
18
+ }),
19
+ );
20
+
21
+ export { FlagsProvider, useFlagsContext };
@@ -43,27 +43,26 @@ interface RegisterBoundsContextValue {
43
43
  }
44
44
 
45
45
  /**
46
- * Gets the parent screen's route key for nested navigators.
47
- * Returns undefined if we're not inside a nested navigator.
46
+ * Builds the full ancestor key chain for nested navigators.
47
+ * Returns an array of screen keys from immediate parent to root.
48
+ * [parentKey, grandparentKey, greatGrandparentKey, ...]
48
49
  */
49
- const getParentScreenKey = (current: TransitionDescriptor) => {
50
- const parent = current.navigation.getParent();
51
- if (!parent) return undefined;
52
-
53
- const parentState = parent.getState();
54
- if (!parentState?.routes) return undefined;
55
-
56
- // Check if our route key exists directly in parent's routes
57
- const existsInParent = parentState.routes.some(
58
- (r) => r.key === current.route.key,
59
- );
60
-
61
- // If we don't exist in parent's routes, we're nested inside the focused route
62
- if (!existsInParent && parentState.index !== undefined) {
63
- return parentState.routes[parentState.index]?.key;
50
+ const getAncestorKeys = (current: TransitionDescriptor): string[] => {
51
+ const ancestors: string[] = [];
52
+ let nav = current.navigation.getParent();
53
+
54
+ while (nav) {
55
+ const state = nav.getState();
56
+ if (state?.routes && state.index !== undefined) {
57
+ const focusedRoute = state.routes[state.index];
58
+ if (focusedRoute?.key) {
59
+ ancestors.push(focusedRoute.key);
60
+ }
61
+ }
62
+ nav = nav.getParent();
64
63
  }
65
64
 
66
- return undefined;
65
+ return ancestors;
67
66
  };
68
67
 
69
68
  /**
@@ -73,13 +72,13 @@ const getParentScreenKey = (current: TransitionDescriptor) => {
73
72
  const useInitialLayoutHandler = (params: {
74
73
  sharedBoundTag?: string;
75
74
  currentScreenKey: string;
76
- parentScreenKey?: string;
75
+ ancestorKeys: string[];
77
76
  maybeMeasureAndStore: (options: MaybeMeasureAndStoreParams) => void;
78
77
  }) => {
79
78
  const {
80
79
  sharedBoundTag,
81
80
  currentScreenKey,
82
- parentScreenKey,
81
+ ancestorKeys,
83
82
  maybeMeasureAndStore,
84
83
  } = params;
85
84
 
@@ -87,57 +86,84 @@ const useInitialLayoutHandler = (params: {
87
86
  currentScreenKey,
88
87
  "animating",
89
88
  );
90
- const isParentAnimating = parentScreenKey
91
- ? AnimationStore.getAnimation(parentScreenKey, "animating")
92
- : null;
89
+
90
+ // Check if any ancestor is animating
91
+ const ancestorAnimations = ancestorKeys.map((key) =>
92
+ AnimationStore.getAnimation(key, "animating"),
93
+ );
93
94
 
94
95
  const hasMeasuredOnLayout = useSharedValue(false);
95
96
 
96
97
  return useCallback(() => {
97
98
  "worklet";
98
- if (!sharedBoundTag || hasMeasuredOnLayout.value) return;
99
- if (!isAnimating.value && !isParentAnimating?.value) return;
99
+ if (!sharedBoundTag || hasMeasuredOnLayout.get()) return;
100
+
101
+ // Check if current or any ancestor is animating
102
+ let isAnyAnimating = isAnimating.get();
103
+ for (let i = 0; i < ancestorAnimations.length; i++) {
104
+ if (ancestorAnimations[i].get()) {
105
+ isAnyAnimating = 1;
106
+ break;
107
+ }
108
+ }
109
+
110
+ if (!isAnyAnimating) return;
100
111
 
101
112
  maybeMeasureAndStore({
102
113
  shouldSetSource: false,
103
114
  shouldSetDestination: true,
104
115
  });
105
116
 
106
- hasMeasuredOnLayout.value = true;
117
+ hasMeasuredOnLayout.set(true);
107
118
  }, [
108
119
  sharedBoundTag,
109
120
  hasMeasuredOnLayout,
110
121
  isAnimating,
111
- isParentAnimating,
122
+ ancestorAnimations,
112
123
  maybeMeasureAndStore,
113
124
  ]);
114
125
  };
115
126
 
127
+ /**
128
+ * Measures non-pressable elements when screen becomes blurred.
129
+ * Captures bounds right before transition starts.
130
+ */
116
131
  /**
117
132
  * Measures non-pressable elements when screen becomes blurred.
118
133
  * Captures bounds right before transition starts.
119
134
  */
120
135
  const useBlurMeasurement = (params: {
121
136
  sharedBoundTag?: string;
137
+ ancestorKeys: string[];
122
138
  maybeMeasureAndStore: (options: MaybeMeasureAndStoreParams) => void;
123
139
  }) => {
124
- const { sharedBoundTag, maybeMeasureAndStore } = params;
125
- const isFocused = useRef(true);
140
+ const { current } = useKeys();
141
+ const { sharedBoundTag, ancestorKeys, maybeMeasureAndStore } = params;
126
142
  const hasCapturedSource = useRef(false);
127
143
 
144
+ const ancestorClosing = [current.route.key, ...ancestorKeys].map((key) =>
145
+ AnimationStore.getAnimation(key, "closing"),
146
+ );
147
+
148
+ const maybeMeasureOnBlur = useStableCallbackValue(() => {
149
+ "worklet";
150
+
151
+ // Don't measure if current / any ancestor is closing
152
+ const isOneClosing = ancestorClosing.some((closing) => closing.get());
153
+ if (isOneClosing) return;
154
+
155
+ maybeMeasureAndStore({ shouldSetSource: true });
156
+ });
157
+
128
158
  useFocusEffect(
129
159
  useCallback(() => {
130
- isFocused.current = true;
131
160
  hasCapturedSource.current = false;
132
161
 
133
162
  return () => {
134
- if (!sharedBoundTag) return;
135
- if (hasCapturedSource.current) return;
136
-
137
- isFocused.current = false;
138
- runOnUI(maybeMeasureAndStore)({ shouldSetSource: true });
163
+ if (!sharedBoundTag || hasCapturedSource.current) return;
164
+ runOnUI(maybeMeasureOnBlur)();
139
165
  };
140
- }, [sharedBoundTag, maybeMeasureAndStore]),
166
+ }, [sharedBoundTag, maybeMeasureOnBlur]),
141
167
  );
142
168
 
143
169
  return {
@@ -157,7 +183,7 @@ const useParentSyncReaction = (params: {
157
183
  const { parentContext, maybeMeasureAndStore } = params;
158
184
 
159
185
  useAnimatedReaction(
160
- () => parentContext?.updateSignal.value,
186
+ () => parentContext?.updateSignal.get(),
161
187
  (value) => {
162
188
  "worklet";
163
189
  if (value === 0 || value === undefined) return;
@@ -173,11 +199,12 @@ const { RegisterBoundsProvider, useRegisterBoundsContext } = createProvider(
173
199
  ({ style, onPress, sharedBoundTag, animatedRef, children }) => {
174
200
  const { current } = useKeys();
175
201
  const currentScreenKey = current.route.key;
176
- const parentScreenKey = getParentScreenKey(current);
202
+ const ancestorKeys = useMemo(() => getAncestorKeys(current), [current]);
177
203
 
178
204
  // Context & signals
179
205
  const parentContext: RegisterBoundsContextValue | null =
180
206
  useRegisterBoundsContext();
207
+
181
208
  const ownSignal = useSharedValue(0);
182
209
  const updateSignal: SharedValue<number> =
183
210
  parentContext?.updateSignal ?? ownSignal;
@@ -191,7 +218,7 @@ const { RegisterBoundsProvider, useRegisterBoundsContext } = createProvider(
191
218
  const emitUpdate = useStableCallbackValue(() => {
192
219
  "worklet";
193
220
  const isRoot = !parentContext;
194
- if (isRoot) updateSignal.value = updateSignal.value + 1;
221
+ if (isRoot) updateSignal.set(updateSignal.get() + 1);
195
222
  });
196
223
 
197
224
  const maybeMeasureAndStore = useStableCallbackValue(
@@ -208,28 +235,31 @@ const { RegisterBoundsProvider, useRegisterBoundsContext } = createProvider(
208
235
 
209
236
  emitUpdate();
210
237
 
211
- // Always register occurrence
212
- BoundStore.registerOccurrence(
238
+ BoundStore.registerSnapshot(
213
239
  sharedBoundTag,
214
240
  currentScreenKey,
215
241
  measured,
216
242
  preparedStyles,
243
+ ancestorKeys,
217
244
  );
218
245
 
219
- // Set as source (on press or blur)
220
246
  if (shouldSetSource) {
221
- if (isAnimating.value) {
222
- const existing = BoundStore.getOccurrence(
223
- sharedBoundTag,
224
- currentScreenKey,
225
- );
226
- BoundStore.setLinkSource(
247
+ if (isAnimating.get()) {
248
+ // If animation is already in progress,
249
+ // lets use the existing measuremenets.
250
+ const existing = BoundStore.getSnapshot(
227
251
  sharedBoundTag,
228
252
  currentScreenKey,
229
- existing.bounds,
230
- preparedStyles,
231
- parentScreenKey,
232
253
  );
254
+ if (existing) {
255
+ BoundStore.setLinkSource(
256
+ sharedBoundTag,
257
+ currentScreenKey,
258
+ existing.bounds,
259
+ preparedStyles,
260
+ ancestorKeys,
261
+ );
262
+ }
233
263
  return;
234
264
  }
235
265
  BoundStore.setLinkSource(
@@ -237,7 +267,7 @@ const { RegisterBoundsProvider, useRegisterBoundsContext } = createProvider(
237
267
  currentScreenKey,
238
268
  measured,
239
269
  preparedStyles,
240
- parentScreenKey,
270
+ ancestorKeys,
241
271
  );
242
272
  }
243
273
 
@@ -248,7 +278,7 @@ const { RegisterBoundsProvider, useRegisterBoundsContext } = createProvider(
248
278
  currentScreenKey,
249
279
  measured,
250
280
  preparedStyles,
251
- parentScreenKey,
281
+ ancestorKeys,
252
282
  );
253
283
  }
254
284
 
@@ -259,7 +289,7 @@ const { RegisterBoundsProvider, useRegisterBoundsContext } = createProvider(
259
289
  const handleInitialLayout = useInitialLayoutHandler({
260
290
  sharedBoundTag,
261
291
  currentScreenKey,
262
- parentScreenKey,
292
+ ancestorKeys,
263
293
  maybeMeasureAndStore,
264
294
  });
265
295
 
@@ -267,6 +297,7 @@ const { RegisterBoundsProvider, useRegisterBoundsContext } = createProvider(
267
297
  const { markSourceCaptured } = useBlurMeasurement({
268
298
  sharedBoundTag,
269
299
  maybeMeasureAndStore,
300
+ ancestorKeys,
270
301
  });
271
302
 
272
303
  useParentSyncReaction({ parentContext, maybeMeasureAndStore });
@@ -8,51 +8,42 @@ import type { Any } from "../types/utils.types";
8
8
  type TagID = string;
9
9
  type ScreenKey = string;
10
10
 
11
- export type TagData = {
11
+ export type Snapshot = {
12
12
  bounds: MeasuredDimensions;
13
13
  styles: StyleProps;
14
14
  };
15
15
 
16
16
  type ScreenIdentifier = {
17
17
  screenKey: ScreenKey;
18
- parentScreenKey?: ScreenKey;
18
+ ancestorKeys?: ScreenKey[];
19
19
  };
20
20
 
21
21
  type TagLink = {
22
- source: ScreenIdentifier & TagData;
23
- destination: (ScreenIdentifier & TagData) | null;
22
+ source: ScreenIdentifier & Snapshot;
23
+ destination: (ScreenIdentifier & Snapshot) | null;
24
24
  };
25
25
 
26
26
  type TagState = {
27
- occurrences: Record<ScreenKey, TagData>;
27
+ snapshots: Record<ScreenKey, Snapshot & { ancestorKeys?: ScreenKey[] }>;
28
28
  linkStack: TagLink[];
29
29
  };
30
30
 
31
- /**
32
- * Note on cleanup: We intentionally skip automatic cleanup of old links.
33
- * The linkStack grows by one entry per navigation, but `getActiveLink`
34
- * finds the correct link via screenKey matching regardless of stack size.
35
- * This is unlikely to cause performance issues in typical apps, but if
36
- * memory becomes a concern in apps with heavy navigation (hundreds of
37
- * transitions), we should consider implementing cleanup on screen unmount using
38
- * screenKey filtering.
39
- */
40
-
41
31
  const registry = makeMutable<Record<TagID, TagState>>({});
42
32
 
43
- function registerOccurrence(
33
+ function registerSnapshot(
44
34
  tag: TagID,
45
35
  screenKey: ScreenKey,
46
36
  bounds: MeasuredDimensions,
47
37
  styles: StyleProps = {},
38
+ ancestorKeys?: ScreenKey[],
48
39
  ) {
49
40
  "worklet";
50
41
  registry.modify((state: Any) => {
51
42
  "worklet";
52
43
  if (!state[tag]) {
53
- state[tag] = { occurrences: {}, linkStack: [] };
44
+ state[tag] = { snapshots: {}, linkStack: [] };
54
45
  }
55
- state[tag].occurrences[screenKey] = { bounds, styles };
46
+ state[tag].snapshots[screenKey] = { bounds, styles, ancestorKeys };
56
47
  return state;
57
48
  });
58
49
  }
@@ -62,16 +53,15 @@ function setLinkSource(
62
53
  screenKey: ScreenKey,
63
54
  bounds: MeasuredDimensions,
64
55
  styles: StyleProps = {},
65
- parentScreenKey?: ScreenKey,
56
+ ancestorKeys?: ScreenKey[],
66
57
  ) {
67
58
  "worklet";
68
59
  registry.modify((state: Any) => {
69
60
  "worklet";
70
- if (!state[tag]) state[tag] = { occurrences: {}, linkStack: [] };
61
+ if (!state[tag]) state[tag] = { snapshots: {}, linkStack: [] };
71
62
 
72
- // Push new link onto stack
73
63
  state[tag].linkStack.push({
74
- source: { screenKey, parentScreenKey, bounds, styles },
64
+ source: { screenKey, ancestorKeys, bounds, styles },
75
65
  destination: null,
76
66
  });
77
67
  return state;
@@ -83,7 +73,7 @@ function setLinkDestination(
83
73
  screenKey: ScreenKey,
84
74
  bounds: MeasuredDimensions,
85
75
  styles: StyleProps = {},
86
- parentScreenKey?: ScreenKey,
76
+ ancestorKeys?: ScreenKey[],
87
77
  ) {
88
78
  "worklet";
89
79
  registry.modify((state: Any) => {
@@ -94,7 +84,7 @@ function setLinkDestination(
94
84
  // Find the topmost link without a destination
95
85
  for (let i = stack.length - 1; i >= 0; i--) {
96
86
  if (stack[i].destination === null) {
97
- stack[i].destination = { screenKey, parentScreenKey, bounds, styles };
87
+ stack[i].destination = { screenKey, ancestorKeys, bounds, styles };
98
88
  break;
99
89
  }
100
90
  }
@@ -102,22 +92,53 @@ function setLinkDestination(
102
92
  });
103
93
  }
104
94
 
105
- function getOccurrence(tag: TagID, key: ScreenKey) {
106
- "worklet";
107
- return registry.value[tag]?.occurrences[key] ?? null;
108
- }
109
-
110
- // Helper to check if a screen identifier matches a given key
95
+ /**
96
+ * Helper to check if a screen identifier matches a given key.
97
+ * Checks both direct screenKey match and ancestor chain.
98
+ */
111
99
  function matchesScreenKey(
112
100
  identifier: ScreenIdentifier | null | undefined,
113
101
  key: ScreenKey,
114
102
  ): boolean {
115
103
  "worklet";
116
104
  if (!identifier) return false;
117
- return identifier.screenKey === key || identifier.parentScreenKey === key;
105
+
106
+ // Direct match
107
+ if (identifier.screenKey === key) return true;
108
+
109
+ // Check ancestor chain
110
+ return identifier.ancestorKeys?.includes(key) ?? false;
111
+ }
112
+
113
+ /**
114
+ * Get snapshot by tag and optional key.
115
+ * If key is provided, supports ancestor matching - if the key matches any ancestor
116
+ * of a stored snapshot, that snapshot will be returned.
117
+ * If key is omitted, returns the most recently registered snapshot.
118
+ */
119
+ function getSnapshot(tag: TagID, key: ScreenKey): Snapshot | null {
120
+ "worklet";
121
+ const tagState = registry.value[tag];
122
+ if (!tagState) return null;
123
+
124
+ // Direct match in occurrences
125
+ if (tagState.snapshots[key]) {
126
+ const snap = tagState.snapshots[key];
127
+ return { bounds: snap.bounds, styles: snap.styles };
128
+ }
129
+
130
+ // Ancestor match
131
+ for (const screenKey in tagState.snapshots) {
132
+ const snap = tagState.snapshots[screenKey];
133
+ if (snap.ancestorKeys?.includes(key)) {
134
+ return { bounds: snap.bounds, styles: snap.styles };
135
+ }
136
+ }
137
+
138
+ return null;
118
139
  }
119
140
 
120
- function getActiveLink(tag: TagID, screenKey?: ScreenKey, isClosing?: boolean) {
141
+ function getActiveLink(tag: TagID, screenKey?: ScreenKey): TagLink | null {
121
142
  "worklet";
122
143
  const stack = registry.value[tag]?.linkStack;
123
144
 
@@ -127,40 +148,55 @@ function getActiveLink(tag: TagID, screenKey?: ScreenKey, isClosing?: boolean) {
127
148
 
128
149
  // If screenKey provided, find link involving that screen
129
150
  if (screenKey) {
130
- // When closing (backward nav), we want the link where this screen is the DESTINATION
131
- // When opening (forward nav), we want the link where this screen is the DESTINATION too
132
- // The source is always the "from" screen, destination is the "to" screen
133
-
134
- if (isClosing) {
135
- // Backward: find link where I am the destination (I'm going back to source)
136
- for (let i = stack.length - 1; i >= 0; i--) {
137
- const link = stack[i];
138
- if (matchesScreenKey(link.destination, screenKey)) {
139
- return link;
140
- }
141
- }
142
- }
143
-
144
- // Forward or fallback: find any link involving this screen
145
151
  for (let i = stack.length - 1; i >= 0; i--) {
146
152
  const link = stack[i];
147
- if (
148
- matchesScreenKey(link.source, screenKey) ||
149
- matchesScreenKey(link.destination, screenKey)
150
- ) {
153
+ if (!link.destination) continue;
154
+
155
+ const isSource = matchesScreenKey(link.source, screenKey);
156
+ const isDestination = matchesScreenKey(link.destination, screenKey);
157
+
158
+ if (isSource || isDestination) {
159
+ // If I match the source, I'm closing (going back to where I came from)
151
160
  return link;
152
161
  }
153
162
  }
154
163
  return null;
155
164
  }
156
165
 
157
- return stack[stack.length - 1] ?? null;
166
+ const lastLink = stack[stack.length - 1];
167
+ return lastLink ? lastLink : null;
168
+ }
169
+
170
+ /**
171
+ * Clear all snapshots and links for a screen across all tags.
172
+ * Called when a screen unmounts.
173
+ */
174
+ function clear(screenKey: ScreenKey) {
175
+ "worklet";
176
+ registry.modify((state: Any) => {
177
+ "worklet";
178
+ for (const tag in state) {
179
+ // Remove snapshot
180
+ if (state[tag].snapshots[screenKey]) {
181
+ delete state[tag].snapshots[screenKey];
182
+ }
183
+
184
+ // Remove links involving this screen
185
+ state[tag].linkStack = state[tag].linkStack.filter((link: TagLink) => {
186
+ const sourceMatches = matchesScreenKey(link.source, screenKey);
187
+ const destMatches = matchesScreenKey(link.destination, screenKey);
188
+ return !sourceMatches && !destMatches;
189
+ });
190
+ }
191
+ return state;
192
+ });
158
193
  }
159
194
 
160
195
  export const BoundStore = {
161
- registerOccurrence,
196
+ registerSnapshot,
162
197
  setLinkSource,
163
198
  setLinkDestination,
164
199
  getActiveLink,
165
- getOccurrence,
200
+ getSnapshot,
201
+ clear,
166
202
  };
@@ -1,5 +1,5 @@
1
1
  import type { MeasuredDimensions, StyleProps } from "react-native-reanimated";
2
- import type { TagData } from "../stores/bounds.store";
2
+ import type { Snapshot } from "../stores/bounds.store";
3
3
  import type {
4
4
  BoundsBuilderOptions,
5
5
  BoundsReturnType,
@@ -21,5 +21,5 @@ export type BoundEntry = {
21
21
 
22
22
  export type BoundsAccessor = {
23
23
  <T extends BoundsBuilderOptions>(options: T): BoundsReturnType<T>;
24
- getOccurrence: (id: string, key: string) => TagData;
24
+ getSnapshot: (id: string, key?: string) => Snapshot | null;
25
25
  };