react-native-screen-transitions 2.0.1 → 2.0.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 (57) hide show
  1. package/package.json +4 -3
  2. package/src/__tests__ /geometry.test.ts +127 -0
  3. package/src/components/bounds-activator.tsx +29 -0
  4. package/src/components/controllers/screen-lifecycle.tsx +72 -0
  5. package/src/components/create-transition-aware-component.tsx +99 -0
  6. package/src/components/root-transition-aware.tsx +56 -0
  7. package/src/configs/index.ts +2 -0
  8. package/src/configs/presets.ts +227 -0
  9. package/src/configs/specs.ts +9 -0
  10. package/src/hooks/animation/use-associated-style.tsx +28 -0
  11. package/src/hooks/animation/use-screen-animation.tsx +142 -0
  12. package/src/hooks/bounds/use-bound-measurer.tsx +71 -0
  13. package/src/hooks/gestures/use-build-gestures.tsx +369 -0
  14. package/src/hooks/gestures/use-scroll-progress.tsx +60 -0
  15. package/src/hooks/use-stable-callback.tsx +15 -0
  16. package/src/index.ts +32 -0
  17. package/src/integrations/native-stack/navigators/createNativeStackNavigator.tsx +112 -0
  18. package/src/integrations/native-stack/utils/debounce.tsx +14 -0
  19. package/src/integrations/native-stack/utils/getModalRoutesKeys.ts +21 -0
  20. package/src/integrations/native-stack/utils/useAnimatedHeaderHeight.tsx +18 -0
  21. package/src/integrations/native-stack/utils/useDismissedRouteError.tsx +30 -0
  22. package/src/integrations/native-stack/utils/useInvalidPreventRemoveError.tsx +31 -0
  23. package/src/integrations/native-stack/views/FontProcessor.native.tsx +12 -0
  24. package/src/integrations/native-stack/views/FontProcessor.tsx +5 -0
  25. package/src/integrations/native-stack/views/FooterComponent.tsx +10 -0
  26. package/src/integrations/native-stack/views/NativeStackView.native.tsx +657 -0
  27. package/src/integrations/native-stack/views/NativeStackView.tsx +214 -0
  28. package/src/integrations/native-stack/views/useHeaderConfigProps.tsx +295 -0
  29. package/src/providers/gestures.tsx +89 -0
  30. package/src/providers/keys.tsx +38 -0
  31. package/src/stores/animations.ts +45 -0
  32. package/src/stores/bounds.ts +71 -0
  33. package/src/stores/gestures.ts +55 -0
  34. package/src/stores/navigator-dismiss-state.ts +17 -0
  35. package/src/stores/utils/reset-stores-for-screen.ts +14 -0
  36. package/src/types/animation.ts +76 -0
  37. package/src/types/bounds.ts +82 -0
  38. package/src/types/core.ts +50 -0
  39. package/src/types/gesture.ts +33 -0
  40. package/src/types/navigator.ts +744 -0
  41. package/src/types/utils.ts +3 -0
  42. package/src/utils/animation/animate.ts +28 -0
  43. package/src/utils/animation/run-transition.ts +49 -0
  44. package/src/utils/bounds/_types/builder.ts +35 -0
  45. package/src/utils/bounds/_types/geometry.ts +17 -0
  46. package/src/utils/bounds/_types/get-bounds.ts +10 -0
  47. package/src/utils/bounds/build-bound-styles.ts +184 -0
  48. package/src/utils/bounds/constants.ts +28 -0
  49. package/src/utils/bounds/flatten-styles.ts +21 -0
  50. package/src/utils/bounds/geometry.ts +113 -0
  51. package/src/utils/bounds/get-bounds.ts +56 -0
  52. package/src/utils/bounds/index.ts +46 -0
  53. package/src/utils/bounds/style-composers.ts +172 -0
  54. package/src/utils/gesture/apply-gesture-activation-criteria.ts +109 -0
  55. package/src/utils/gesture/map-gesture-to-progress.ts +11 -0
  56. package/src/utils/gesture/normalize-gesture-translation.ts +20 -0
  57. package/src/utils/index.ts +1 -0
@@ -0,0 +1,214 @@
1
+ import {
2
+ getHeaderTitle,
3
+ Header,
4
+ HeaderBackButton,
5
+ HeaderBackContext,
6
+ SafeAreaProviderCompat,
7
+ Screen,
8
+ useHeaderHeight,
9
+ } from "@react-navigation/elements";
10
+ import {
11
+ type ParamListBase,
12
+ type RouteProp,
13
+ type StackNavigationState,
14
+ useLinkBuilder,
15
+ } from "@react-navigation/native";
16
+ import * as React from "react";
17
+ import { Animated, Image, StyleSheet, View } from "react-native";
18
+
19
+ import type {
20
+ NativeStackDescriptor,
21
+ NativeStackDescriptorMap,
22
+ NativeStackNavigationHelpers,
23
+ } from "../../../types/navigator";
24
+ import { AnimatedHeaderHeightContext } from "../utils/useAnimatedHeaderHeight";
25
+
26
+ type Props = {
27
+ state: StackNavigationState<ParamListBase>;
28
+ // This is used for the native implementation of the stack.
29
+ navigation: NativeStackNavigationHelpers;
30
+ descriptors: NativeStackDescriptorMap;
31
+ describe: (
32
+ route: RouteProp<ParamListBase>,
33
+ placeholder: boolean,
34
+ ) => NativeStackDescriptor;
35
+ };
36
+
37
+ const TRANSPARENT_PRESENTATIONS = [
38
+ "transparentModal",
39
+ "containedTransparentModal",
40
+ ];
41
+
42
+ export function NativeStackView({ state, descriptors, describe }: Props) {
43
+ const parentHeaderBack = React.useContext(HeaderBackContext);
44
+ const { buildHref } = useLinkBuilder();
45
+
46
+ const preloadedDescriptors =
47
+ state.preloadedRoutes.reduce<NativeStackDescriptorMap>((acc, route) => {
48
+ acc[route.key] = acc[route.key] || describe(route, true);
49
+ return acc;
50
+ }, {});
51
+
52
+ return (
53
+ <SafeAreaProviderCompat>
54
+ {state.routes.concat(state.preloadedRoutes).map((route, i) => {
55
+ const isFocused = state.index === i;
56
+ const previousKey = state.routes[i - 1]?.key;
57
+ const nextKey = state.routes[i + 1]?.key;
58
+ const previousDescriptor = previousKey
59
+ ? descriptors[previousKey]
60
+ : undefined;
61
+ const nextDescriptor = nextKey ? descriptors[nextKey] : undefined;
62
+ const { options, navigation, render } =
63
+ descriptors[route.key] ?? preloadedDescriptors[route.key];
64
+
65
+ const headerBack = previousDescriptor
66
+ ? {
67
+ title: getHeaderTitle(
68
+ previousDescriptor.options,
69
+ previousDescriptor.route.name,
70
+ ),
71
+ href: buildHref(
72
+ previousDescriptor.route.name,
73
+ previousDescriptor.route.params,
74
+ ),
75
+ }
76
+ : parentHeaderBack;
77
+
78
+ const canGoBack = headerBack != null;
79
+
80
+ const {
81
+ header,
82
+ headerShown,
83
+ headerBackImageSource,
84
+ headerLeft,
85
+ headerTransparent,
86
+ headerBackTitle,
87
+ presentation,
88
+ contentStyle,
89
+ ...rest
90
+ } = options;
91
+
92
+ const nextPresentation = nextDescriptor?.options.presentation;
93
+
94
+ const isPreloaded =
95
+ preloadedDescriptors[route.key] !== undefined &&
96
+ descriptors[route.key] === undefined;
97
+
98
+ return (
99
+ <Screen
100
+ key={route.key}
101
+ focused={isFocused}
102
+ route={route}
103
+ navigation={navigation}
104
+ headerShown={headerShown}
105
+ headerTransparent={headerTransparent}
106
+ header={
107
+ header !== undefined ? (
108
+ header({
109
+ back: headerBack,
110
+ options,
111
+ route,
112
+ navigation,
113
+ })
114
+ ) : (
115
+ <Header
116
+ {...rest}
117
+ back={headerBack}
118
+ title={getHeaderTitle(options, route.name)}
119
+ headerLeft={
120
+ typeof headerLeft === "function"
121
+ ? ({ label, ...rest }) =>
122
+ headerLeft({
123
+ ...rest,
124
+ label: headerBackTitle ?? label,
125
+ })
126
+ : headerLeft === undefined && canGoBack
127
+ ? ({ tintColor, label, ...rest }) => (
128
+ <HeaderBackButton
129
+ {...rest}
130
+ label={headerBackTitle ?? label}
131
+ tintColor={tintColor}
132
+ backImage={
133
+ headerBackImageSource !== undefined
134
+ ? () => (
135
+ <Image
136
+ source={headerBackImageSource}
137
+ resizeMode="contain"
138
+ tintColor={tintColor}
139
+ style={styles.backImage}
140
+ />
141
+ )
142
+ : undefined
143
+ }
144
+ onPress={navigation.goBack}
145
+ />
146
+ )
147
+ : headerLeft
148
+ }
149
+ headerTransparent={headerTransparent}
150
+ />
151
+ )
152
+ }
153
+ style={[
154
+ StyleSheet.absoluteFill,
155
+ {
156
+ display:
157
+ (isFocused ||
158
+ (nextPresentation != null &&
159
+ TRANSPARENT_PRESENTATIONS.includes(nextPresentation))) &&
160
+ !isPreloaded
161
+ ? "flex"
162
+ : "none",
163
+ },
164
+ presentation != null &&
165
+ TRANSPARENT_PRESENTATIONS.includes(presentation)
166
+ ? { backgroundColor: "transparent" }
167
+ : null,
168
+ ]}
169
+ >
170
+ <HeaderBackContext.Provider value={headerBack}>
171
+ <AnimatedHeaderHeightProvider>
172
+ <View style={[styles.contentContainer, contentStyle]}>
173
+ {render()}
174
+ </View>
175
+ </AnimatedHeaderHeightProvider>
176
+ </HeaderBackContext.Provider>
177
+ </Screen>
178
+ );
179
+ })}
180
+ </SafeAreaProviderCompat>
181
+ );
182
+ }
183
+
184
+ const AnimatedHeaderHeightProvider = ({
185
+ children,
186
+ }: {
187
+ children: React.ReactNode;
188
+ }) => {
189
+ const headerHeight = useHeaderHeight();
190
+ const [animatedHeaderHeight] = React.useState(
191
+ () => new Animated.Value(headerHeight),
192
+ );
193
+
194
+ React.useEffect(() => {
195
+ animatedHeaderHeight.setValue(headerHeight);
196
+ }, [animatedHeaderHeight, headerHeight]);
197
+
198
+ return (
199
+ <AnimatedHeaderHeightContext.Provider value={animatedHeaderHeight}>
200
+ {children}
201
+ </AnimatedHeaderHeightContext.Provider>
202
+ );
203
+ };
204
+
205
+ const styles = StyleSheet.create({
206
+ contentContainer: {
207
+ flex: 1,
208
+ },
209
+ backImage: {
210
+ height: 24,
211
+ width: 24,
212
+ margin: 3,
213
+ },
214
+ });
@@ -0,0 +1,295 @@
1
+ import { getHeaderTitle, HeaderTitle } from "@react-navigation/elements";
2
+ import { type Route, useLocale, useTheme } from "@react-navigation/native";
3
+ import { Platform, StyleSheet, type TextStyle, View } from "react-native";
4
+ import {
5
+ isSearchBarAvailableForCurrentPlatform,
6
+ ScreenStackHeaderBackButtonImage,
7
+ ScreenStackHeaderCenterView,
8
+ ScreenStackHeaderLeftView,
9
+ ScreenStackHeaderRightView,
10
+ ScreenStackHeaderSearchBarView,
11
+ SearchBar,
12
+ } from "react-native-screens";
13
+
14
+ import type { NativeStackNavigationOptions } from "../../../types/navigator";
15
+ import { processFonts } from "./FontProcessor";
16
+
17
+ type Props = NativeStackNavigationOptions & {
18
+ headerTopInsetEnabled: boolean;
19
+ headerHeight: number;
20
+ headerBack: { title?: string | undefined; href: undefined } | undefined;
21
+ route: Route<string>;
22
+ };
23
+
24
+ export function useHeaderConfigProps({
25
+ headerBackImageSource,
26
+ headerBackButtonDisplayMode,
27
+ headerBackButtonMenuEnabled,
28
+ headerBackTitle,
29
+ headerBackTitleStyle,
30
+ headerBackVisible,
31
+ headerShadowVisible,
32
+ headerLargeStyle,
33
+ headerLargeTitle,
34
+ headerLargeTitleShadowVisible,
35
+ headerLargeTitleStyle,
36
+ headerBackground,
37
+ headerLeft,
38
+ headerRight,
39
+ headerShown,
40
+ headerStyle,
41
+ headerBlurEffect,
42
+ headerTintColor,
43
+ headerTitle,
44
+ headerTitleAlign,
45
+ headerTitleStyle,
46
+ headerTransparent,
47
+ headerSearchBarOptions,
48
+ headerTopInsetEnabled,
49
+ headerBack,
50
+ route,
51
+ title,
52
+ }: Props) {
53
+ const { direction } = useLocale();
54
+ const { colors, fonts } = useTheme();
55
+ const tintColor =
56
+ headerTintColor ?? (Platform.OS === "ios" ? colors.primary : colors.text);
57
+
58
+ const headerBackTitleStyleFlattened =
59
+ StyleSheet.flatten([fonts.regular, headerBackTitleStyle]) || {};
60
+ const headerLargeTitleStyleFlattened =
61
+ StyleSheet.flatten([
62
+ Platform.select({ ios: fonts.heavy, default: fonts.medium }),
63
+ headerLargeTitleStyle,
64
+ ]) || {};
65
+ const headerTitleStyleFlattened =
66
+ StyleSheet.flatten([
67
+ Platform.select({ ios: fonts.bold, default: fonts.medium }),
68
+ headerTitleStyle,
69
+ ]) || {};
70
+ const headerStyleFlattened = StyleSheet.flatten(headerStyle) || {};
71
+ const headerLargeStyleFlattened = StyleSheet.flatten(headerLargeStyle) || {};
72
+
73
+ const [backTitleFontFamily, largeTitleFontFamily, titleFontFamily] =
74
+ processFonts([
75
+ headerBackTitleStyleFlattened.fontFamily,
76
+ headerLargeTitleStyleFlattened.fontFamily,
77
+ headerTitleStyleFlattened.fontFamily,
78
+ ]);
79
+
80
+ const backTitleFontSize =
81
+ "fontSize" in headerBackTitleStyleFlattened
82
+ ? headerBackTitleStyleFlattened.fontSize
83
+ : undefined;
84
+
85
+ const titleText = getHeaderTitle({ title, headerTitle }, route.name);
86
+ const titleColor =
87
+ "color" in headerTitleStyleFlattened
88
+ ? headerTitleStyleFlattened.color
89
+ : (headerTintColor ?? colors.text);
90
+ const titleFontSize =
91
+ "fontSize" in headerTitleStyleFlattened
92
+ ? headerTitleStyleFlattened.fontSize
93
+ : undefined;
94
+ const titleFontWeight = headerTitleStyleFlattened.fontWeight;
95
+
96
+ const largeTitleBackgroundColor = headerLargeStyleFlattened.backgroundColor;
97
+ const largeTitleColor =
98
+ "color" in headerLargeTitleStyleFlattened
99
+ ? headerLargeTitleStyleFlattened.color
100
+ : undefined;
101
+ const largeTitleFontSize =
102
+ "fontSize" in headerLargeTitleStyleFlattened
103
+ ? headerLargeTitleStyleFlattened.fontSize
104
+ : undefined;
105
+ const largeTitleFontWeight = headerLargeTitleStyleFlattened.fontWeight;
106
+
107
+ const headerTitleStyleSupported: TextStyle = { color: titleColor };
108
+
109
+ if (headerTitleStyleFlattened.fontFamily != null) {
110
+ headerTitleStyleSupported.fontFamily = headerTitleStyleFlattened.fontFamily;
111
+ }
112
+
113
+ if (titleFontSize != null) {
114
+ headerTitleStyleSupported.fontSize = titleFontSize;
115
+ }
116
+
117
+ if (titleFontWeight != null) {
118
+ headerTitleStyleSupported.fontWeight = titleFontWeight;
119
+ }
120
+
121
+ const headerBackgroundColor =
122
+ headerStyleFlattened.backgroundColor ??
123
+ (headerBackground != null || headerTransparent
124
+ ? "transparent"
125
+ : colors.card);
126
+
127
+ const canGoBack = headerBack != null;
128
+
129
+ const headerLeftElement = headerLeft?.({
130
+ tintColor,
131
+ canGoBack,
132
+ label: headerBackTitle ?? headerBack?.title,
133
+ // `href` is only applicable to web
134
+ href: undefined,
135
+ });
136
+
137
+ const headerRightElement = headerRight?.({
138
+ tintColor,
139
+ canGoBack,
140
+ });
141
+
142
+ const headerTitleElement =
143
+ typeof headerTitle === "function"
144
+ ? headerTitle({
145
+ tintColor,
146
+ children: titleText,
147
+ })
148
+ : null;
149
+
150
+ const hasHeaderSearchBar =
151
+ isSearchBarAvailableForCurrentPlatform && headerSearchBarOptions != null;
152
+
153
+ /**
154
+ * We need to set this in if:
155
+ * - Back button should stay visible when `headerLeft` is specified
156
+ * - If `headerTitle` for Android is specified, so we only need to remove the title and keep the back button
157
+ */
158
+ const backButtonInCustomView =
159
+ headerBackVisible ||
160
+ (Platform.OS === "android" &&
161
+ headerTitleElement != null &&
162
+ headerLeftElement == null);
163
+
164
+ const translucent =
165
+ headerBackground != null ||
166
+ headerTransparent ||
167
+ // When using a SearchBar or large title, the header needs to be translucent for it to work on iOS
168
+ ((hasHeaderSearchBar || headerLargeTitle) &&
169
+ Platform.OS === "ios" &&
170
+ headerTransparent !== false);
171
+
172
+ const isBackButtonDisplayModeAvailable =
173
+ // On iOS 14+
174
+ Platform.OS === "ios" &&
175
+ parseInt(Platform.Version, 10) >= 14 &&
176
+ // Doesn't have custom styling, by default System, see: https://github.com/software-mansion/react-native-screens/pull/2105#discussion_r1565222738
177
+ (backTitleFontFamily == null || backTitleFontFamily === "System") &&
178
+ backTitleFontSize == null &&
179
+ // Back button menu is not disabled
180
+ headerBackButtonMenuEnabled !== false;
181
+
182
+ const isCenterViewRenderedAndroid = headerTitleAlign === "center";
183
+
184
+ const children = (
185
+ <>
186
+ {Platform.OS === "ios" ? (
187
+ <>
188
+ {headerLeftElement != null ? (
189
+ <ScreenStackHeaderLeftView>
190
+ {headerLeftElement}
191
+ </ScreenStackHeaderLeftView>
192
+ ) : null}
193
+ {headerTitleElement != null ? (
194
+ <ScreenStackHeaderCenterView>
195
+ {headerTitleElement}
196
+ </ScreenStackHeaderCenterView>
197
+ ) : null}
198
+ </>
199
+ ) : (
200
+ <>
201
+ {headerLeftElement != null || typeof headerTitle === "function" ? (
202
+ // The style passed to header left, together with title element being wrapped
203
+ // in flex view is reqruied for proper header layout, in particular,
204
+ // for the text truncation to work.
205
+ <ScreenStackHeaderLeftView
206
+ style={!isCenterViewRenderedAndroid ? { flex: 1 } : null}
207
+ >
208
+ {headerLeftElement}
209
+ {headerTitleAlign !== "center" ? (
210
+ typeof headerTitle === "function" ? (
211
+ <View style={{ flex: 1 }}>{headerTitleElement}</View>
212
+ ) : (
213
+ <View style={{ flex: 1 }}>
214
+ <HeaderTitle
215
+ tintColor={tintColor}
216
+ style={headerTitleStyleSupported}
217
+ >
218
+ {titleText}
219
+ </HeaderTitle>
220
+ </View>
221
+ )
222
+ ) : null}
223
+ </ScreenStackHeaderLeftView>
224
+ ) : null}
225
+ {isCenterViewRenderedAndroid ? (
226
+ <ScreenStackHeaderCenterView>
227
+ {typeof headerTitle === "function" ? (
228
+ headerTitleElement
229
+ ) : (
230
+ <HeaderTitle
231
+ tintColor={tintColor}
232
+ style={headerTitleStyleSupported}
233
+ >
234
+ {titleText}
235
+ </HeaderTitle>
236
+ )}
237
+ </ScreenStackHeaderCenterView>
238
+ ) : null}
239
+ </>
240
+ )}
241
+ {headerBackImageSource !== undefined ? (
242
+ <ScreenStackHeaderBackButtonImage source={headerBackImageSource} />
243
+ ) : null}
244
+ {headerRightElement != null ? (
245
+ <ScreenStackHeaderRightView>
246
+ {headerRightElement}
247
+ </ScreenStackHeaderRightView>
248
+ ) : null}
249
+ {hasHeaderSearchBar ? (
250
+ <ScreenStackHeaderSearchBarView>
251
+ <SearchBar {...headerSearchBarOptions} />
252
+ </ScreenStackHeaderSearchBarView>
253
+ ) : null}
254
+ </>
255
+ );
256
+
257
+ return {
258
+ backButtonInCustomView,
259
+ backgroundColor: headerBackgroundColor,
260
+ backTitle: headerBackTitle,
261
+ backTitleVisible: isBackButtonDisplayModeAvailable
262
+ ? undefined
263
+ : headerBackButtonDisplayMode !== "minimal",
264
+ backButtonDisplayMode: isBackButtonDisplayModeAvailable
265
+ ? headerBackButtonDisplayMode
266
+ : undefined,
267
+ backTitleFontFamily,
268
+ backTitleFontSize,
269
+ blurEffect: headerBlurEffect,
270
+ color: tintColor,
271
+ direction,
272
+ disableBackButtonMenu: headerBackButtonMenuEnabled === false,
273
+ hidden: headerShown === false,
274
+ hideBackButton: headerBackVisible === false,
275
+ hideShadow:
276
+ headerShadowVisible === false ||
277
+ headerBackground != null ||
278
+ (headerTransparent && headerShadowVisible !== true),
279
+ largeTitle: headerLargeTitle,
280
+ largeTitleBackgroundColor,
281
+ largeTitleColor,
282
+ largeTitleFontFamily,
283
+ largeTitleFontSize,
284
+ largeTitleFontWeight,
285
+ largeTitleHideShadow: headerLargeTitleShadowVisible === false,
286
+ title: titleText,
287
+ titleColor,
288
+ titleFontFamily,
289
+ titleFontSize,
290
+ titleFontWeight: String(titleFontWeight),
291
+ topInsetEnabled: headerTopInsetEnabled,
292
+ translucent: translucent === true,
293
+ children,
294
+ } as const;
295
+ }
@@ -0,0 +1,89 @@
1
+ import { createContext, useContext, useMemo } from "react";
2
+ import { StyleSheet, View } from "react-native";
3
+ import type { GestureType } from "react-native-gesture-handler";
4
+ import {
5
+ GestureDetector,
6
+ GestureHandlerRootView,
7
+ } from "react-native-gesture-handler";
8
+ import type { SharedValue } from "react-native-reanimated";
9
+ import { useSharedValue } from "react-native-reanimated";
10
+ import { useBuildGestures } from "../hooks/gestures/use-build-gestures";
11
+
12
+ export type ScrollProgress = {
13
+ x: number;
14
+ y: number;
15
+ contentHeight: number;
16
+ contentWidth: number;
17
+ layoutHeight: number;
18
+ layoutWidth: number;
19
+ };
20
+
21
+ export interface GestureContextType {
22
+ panGesture: GestureType;
23
+ nativeGesture: GestureType;
24
+ scrollProgress: SharedValue<ScrollProgress>;
25
+ }
26
+
27
+ type ScreenGestureProviderProps = {
28
+ children: React.ReactNode;
29
+ };
30
+
31
+ const DEFAULT_SCROLL_PROGRESS: ScrollProgress = {
32
+ x: 0,
33
+ y: 0,
34
+ contentHeight: 0,
35
+ contentWidth: 0,
36
+ layoutHeight: 0,
37
+ layoutWidth: 0,
38
+ };
39
+
40
+ const GestureContext = createContext<GestureContextType | undefined>(undefined);
41
+
42
+ export const ScreenGestureProvider = ({
43
+ children,
44
+ }: ScreenGestureProviderProps) => {
45
+ const scrollProgress = useSharedValue<ScrollProgress>(
46
+ DEFAULT_SCROLL_PROGRESS,
47
+ );
48
+
49
+ const { panGesture, nativeGesture } = useBuildGestures({
50
+ scrollProgress,
51
+ });
52
+
53
+ const value = useMemo(
54
+ () => ({
55
+ panGesture,
56
+ scrollProgress,
57
+ nativeGesture,
58
+ }),
59
+ [panGesture, scrollProgress, nativeGesture],
60
+ ) satisfies GestureContextType;
61
+
62
+ return (
63
+ <GestureHandlerRootView>
64
+ <GestureContext.Provider value={value}>
65
+ <GestureDetector gesture={panGesture}>
66
+ <View style={styles.container}>{children}</View>
67
+ </GestureDetector>
68
+ </GestureContext.Provider>
69
+ </GestureHandlerRootView>
70
+ );
71
+ };
72
+
73
+ export const useGestureContext = () => {
74
+ const context = useContext(GestureContext);
75
+
76
+ if (!context) {
77
+ throw new Error(
78
+ "useGestureContext must be used within a ScreenGestureProvider",
79
+ );
80
+ }
81
+
82
+ return context;
83
+ };
84
+
85
+ const styles = StyleSheet.create({
86
+ container: {
87
+ flex: 1,
88
+ },
89
+ });
@@ -0,0 +1,38 @@
1
+ import { createContext, useContext, useMemo } from "react";
2
+ import type { NativeStackDescriptor } from "../types/navigator";
3
+
4
+ interface KeysContextType {
5
+ previous?: NativeStackDescriptor;
6
+ current: NativeStackDescriptor;
7
+ next?: NativeStackDescriptor;
8
+ }
9
+
10
+ const KeysContext = createContext<KeysContextType | undefined>(undefined);
11
+
12
+ interface KeysProviderProps {
13
+ children: React.ReactNode;
14
+ previous?: NativeStackDescriptor;
15
+ current: NativeStackDescriptor;
16
+ next?: NativeStackDescriptor;
17
+ }
18
+
19
+ export const KeysProvider = ({
20
+ children,
21
+ previous,
22
+ current,
23
+ next,
24
+ }: KeysProviderProps) => {
25
+ const value = useMemo(
26
+ () => ({ previous, current, next }),
27
+ [previous, current, next],
28
+ );
29
+ return <KeysContext.Provider value={value}>{children}</KeysContext.Provider>;
30
+ };
31
+
32
+ export const useKeys = (): KeysContextType => {
33
+ const context = useContext(KeysContext);
34
+ if (context === undefined) {
35
+ throw new Error("useKeys must be used within a KeysProvider");
36
+ }
37
+ return context;
38
+ };
@@ -0,0 +1,45 @@
1
+ import { makeMutable, type SharedValue } from "react-native-reanimated";
2
+ import type { ScreenKey } from "../types/navigator";
3
+
4
+ export type AnimationMap = {
5
+ progress: SharedValue<number>;
6
+ closing: SharedValue<number>;
7
+ animating: SharedValue<number>;
8
+ };
9
+
10
+ const store: Record<ScreenKey, AnimationMap> = {};
11
+
12
+ const ensure = (key: ScreenKey) => {
13
+ let bag = store[key];
14
+ if (!bag) {
15
+ bag = {
16
+ progress: makeMutable(0),
17
+ closing: makeMutable(0),
18
+ animating: makeMutable(0),
19
+ };
20
+ store[key] = bag;
21
+ }
22
+ return bag;
23
+ };
24
+
25
+ export function getAnimation(
26
+ key: ScreenKey,
27
+ type: "progress" | "closing" | "animating",
28
+ ): SharedValue<number> {
29
+ return ensure(key)[type];
30
+ }
31
+
32
+ export function getAll(key: ScreenKey) {
33
+ return ensure(key);
34
+ }
35
+
36
+ function clear(routeKey: ScreenKey) {
37
+ "worklet";
38
+ delete store[routeKey];
39
+ }
40
+
41
+ export const Animations = {
42
+ getAnimation,
43
+ clear,
44
+ getAll,
45
+ };