react-native-screen-transitions 3.0.0-rc.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +228 -96
  2. package/lib/commonjs/blank-stack/components/overlay.js +1 -1
  3. package/lib/commonjs/blank-stack/components/overlay.js.map +1 -1
  4. package/lib/commonjs/blank-stack/components/screens.js +6 -2
  5. package/lib/commonjs/blank-stack/components/screens.js.map +1 -1
  6. package/lib/commonjs/blank-stack/components/stack-view.js +2 -5
  7. package/lib/commonjs/blank-stack/components/stack-view.js.map +1 -1
  8. package/lib/commonjs/blank-stack/utils/with-stack-navigation/index.js +11 -10
  9. package/lib/commonjs/blank-stack/utils/with-stack-navigation/index.js.map +1 -1
  10. package/lib/commonjs/shared/components/controllers/native-stack-lifecycle.js +2 -0
  11. package/lib/commonjs/shared/components/controllers/native-stack-lifecycle.js.map +1 -1
  12. package/lib/commonjs/shared/components/create-transition-aware-component.js +2 -0
  13. package/lib/commonjs/shared/components/create-transition-aware-component.js.map +1 -1
  14. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +8 -4
  15. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  16. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +29 -5
  17. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  18. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture.js +26 -0
  19. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture.js.map +1 -0
  20. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +32 -60
  21. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  22. package/lib/commonjs/shared/index.js +7 -0
  23. package/lib/commonjs/shared/index.js.map +1 -1
  24. package/lib/commonjs/shared/providers/gestures.provider.js +21 -42
  25. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  26. package/lib/commonjs/shared/utils/bounds/helpers/interpolate-style.js +30 -0
  27. package/lib/commonjs/shared/utils/bounds/helpers/interpolate-style.js.map +1 -0
  28. package/lib/commonjs/shared/utils/bounds/index.js +29 -1
  29. package/lib/commonjs/shared/utils/bounds/index.js.map +1 -1
  30. package/lib/commonjs/shared/utils/create-provider.js +16 -0
  31. package/lib/commonjs/shared/utils/create-provider.js.map +1 -1
  32. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js +4 -0
  33. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  34. package/lib/module/blank-stack/components/overlay.js +1 -1
  35. package/lib/module/blank-stack/components/overlay.js.map +1 -1
  36. package/lib/module/blank-stack/components/screens.js +6 -2
  37. package/lib/module/blank-stack/components/screens.js.map +1 -1
  38. package/lib/module/blank-stack/components/stack-view.js +2 -5
  39. package/lib/module/blank-stack/components/stack-view.js.map +1 -1
  40. package/lib/module/blank-stack/utils/with-stack-navigation/index.js +11 -10
  41. package/lib/module/blank-stack/utils/with-stack-navigation/index.js.map +1 -1
  42. package/lib/module/shared/components/controllers/native-stack-lifecycle.js +1 -0
  43. package/lib/module/shared/components/controllers/native-stack-lifecycle.js.map +1 -1
  44. package/lib/module/shared/components/create-transition-aware-component.js +2 -0
  45. package/lib/module/shared/components/create-transition-aware-component.js.map +1 -1
  46. package/lib/module/shared/hooks/animation/use-screen-animation.js +8 -4
  47. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  48. package/lib/module/shared/hooks/gestures/use-build-gestures.js +30 -6
  49. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  50. package/lib/module/shared/hooks/gestures/use-screen-gesture.js +22 -0
  51. package/lib/module/shared/hooks/gestures/use-screen-gesture.js.map +1 -0
  52. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +32 -60
  53. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  54. package/lib/module/shared/index.js +1 -0
  55. package/lib/module/shared/index.js.map +1 -1
  56. package/lib/module/shared/providers/gestures.provider.js +19 -41
  57. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  58. package/lib/module/shared/utils/bounds/helpers/interpolate-style.js +26 -0
  59. package/lib/module/shared/utils/bounds/helpers/interpolate-style.js.map +1 -0
  60. package/lib/module/shared/utils/bounds/index.js +29 -1
  61. package/lib/module/shared/utils/bounds/index.js.map +1 -1
  62. package/lib/module/shared/utils/create-provider.js +17 -1
  63. package/lib/module/shared/utils/create-provider.js.map +1 -1
  64. package/lib/module/shared/utils/gesture/check-gesture-activation.js +4 -4
  65. package/lib/module/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  66. package/lib/typescript/blank-stack/components/screens.d.ts +1 -3
  67. package/lib/typescript/blank-stack/components/screens.d.ts.map +1 -1
  68. package/lib/typescript/blank-stack/components/stack-view.d.ts.map +1 -1
  69. package/lib/typescript/blank-stack/types.d.ts +2 -14
  70. package/lib/typescript/blank-stack/types.d.ts.map +1 -1
  71. package/lib/typescript/blank-stack/utils/with-stack-navigation/index.d.ts.map +1 -1
  72. package/lib/typescript/shared/components/controllers/native-stack-lifecycle.d.ts.map +1 -1
  73. package/lib/typescript/shared/components/create-transition-aware-component.d.ts +1 -0
  74. package/lib/typescript/shared/components/create-transition-aware-component.d.ts.map +1 -1
  75. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  76. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +1 -0
  77. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  78. package/lib/typescript/shared/hooks/gestures/use-screen-gesture.d.ts +15 -0
  79. package/lib/typescript/shared/hooks/gestures/use-screen-gesture.d.ts.map +1 -0
  80. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts +1 -0
  81. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
  82. package/lib/typescript/shared/index.d.ts +4 -2
  83. package/lib/typescript/shared/index.d.ts.map +1 -1
  84. package/lib/typescript/shared/providers/gestures.provider.d.ts +6 -13
  85. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  86. package/lib/typescript/shared/types/animation.types.d.ts +44 -0
  87. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  88. package/lib/typescript/shared/types/bounds.types.d.ts +6 -0
  89. package/lib/typescript/shared/types/bounds.types.d.ts.map +1 -1
  90. package/lib/typescript/shared/types/core.types.d.ts +7 -0
  91. package/lib/typescript/shared/types/core.types.d.ts.map +1 -1
  92. package/lib/typescript/shared/utils/bounds/helpers/interpolate-style.d.ts +17 -0
  93. package/lib/typescript/shared/utils/bounds/helpers/interpolate-style.d.ts.map +1 -0
  94. package/lib/typescript/shared/utils/bounds/index.d.ts.map +1 -1
  95. package/lib/typescript/shared/utils/create-provider.d.ts +5 -1
  96. package/lib/typescript/shared/utils/create-provider.d.ts.map +1 -1
  97. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts +49 -1
  98. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts.map +1 -1
  99. package/package.json +1 -1
  100. package/src/blank-stack/components/overlay.tsx +1 -1
  101. package/src/blank-stack/components/screens.tsx +4 -4
  102. package/src/blank-stack/components/stack-view.tsx +4 -11
  103. package/src/blank-stack/types.ts +2 -15
  104. package/src/blank-stack/utils/with-stack-navigation/index.tsx +17 -3
  105. package/src/shared/__tests__/derivations.test.ts +155 -0
  106. package/src/shared/__tests__/gesture-activation.test.ts +251 -0
  107. package/src/shared/components/controllers/native-stack-lifecycle.tsx +4 -2
  108. package/src/shared/components/create-transition-aware-component.tsx +2 -1
  109. package/src/shared/hooks/animation/use-screen-animation.tsx +8 -2
  110. package/src/shared/hooks/gestures/use-build-gestures.tsx +35 -8
  111. package/src/shared/hooks/gestures/use-screen-gesture.ts +19 -0
  112. package/src/shared/hooks/gestures/use-scroll-registry.tsx +39 -59
  113. package/src/shared/index.ts +2 -0
  114. package/src/shared/providers/gestures.provider.tsx +35 -75
  115. package/src/shared/types/animation.types.ts +49 -0
  116. package/src/shared/types/bounds.types.ts +11 -0
  117. package/src/shared/types/core.types.ts +8 -0
  118. package/src/shared/utils/bounds/helpers/interpolate-style.ts +38 -0
  119. package/src/shared/utils/bounds/index.ts +31 -1
  120. package/src/shared/utils/create-provider.tsx +31 -1
  121. package/src/shared/utils/gesture/check-gesture-activation.ts +4 -4
@@ -1,5 +1,5 @@
1
1
  import { StackActions } from "@react-navigation/native";
2
- import { useCallback, useMemo } from "react";
2
+ import { useCallback, useMemo, useRef } from "react";
3
3
  import { useWindowDimensions } from "react-native";
4
4
  import {
5
5
  Gesture,
@@ -48,6 +48,7 @@ export const useBuildGestures = ({
48
48
  ancestorContext,
49
49
  }: BuildGesturesHookProps): {
50
50
  panGesture: GestureType;
51
+ panGestureRef: React.MutableRefObject<GestureType | undefined>;
51
52
  nativeGesture: GestureType;
52
53
  gestureAnimationValues: GestureStoreMap;
53
54
  } => {
@@ -63,6 +64,9 @@ export const useBuildGestures = ({
63
64
  GestureOffsetState.PENDING,
64
65
  );
65
66
 
67
+ // Ref for external gesture coordination (e.g., swipeable lists)
68
+ const panGestureRef = useRef<GestureType | undefined>(undefined);
69
+
66
70
  const gestureAnimationValues = GestureStore.getRouteGestures(
67
71
  current.route.key,
68
72
  );
@@ -325,26 +329,49 @@ export const useBuildGestures = ({
325
329
  },
326
330
  );
327
331
 
332
+ // Memoize gestures to keep stable references - critical for RNGH
333
+ // Child gestures reference ancestor's pan via requireExternalGestureToFail,
334
+ // so the pan gesture MUST be stable or children will reference stale objects
328
335
  return useMemo(() => {
329
- const nativeGesture = Gesture.Native();
330
-
331
336
  const panGesture = Gesture.Pan()
337
+ .withRef(panGestureRef)
332
338
  .enabled(gestureEnabled)
333
339
  .manualActivation(true)
334
340
  .onTouchesDown(onTouchesDown)
335
341
  .onTouchesMove(onTouchesMove)
336
342
  .onStart(onStart)
337
343
  .onUpdate(onUpdate)
338
- .onEnd(onEnd)
339
- .blocksExternalGesture(nativeGesture);
344
+ .onEnd(onEnd);
345
+
346
+ // Native gesture setup depends on whether this screen has gestures
347
+ let nativeGesture: GestureType;
348
+
349
+ if (gestureEnabled) {
350
+ // This screen has gestures - set up normal pan/native relationship
351
+ nativeGesture = Gesture.Native().requireExternalGestureToFail(panGesture);
352
+ panGesture.blocksExternalGesture(nativeGesture);
353
+ } else {
354
+ // This screen has no gestures
355
+ // Find nearest ancestor with gestureEnabled=true (attached pan)
356
+ let activePanAncestor = ancestorContext;
357
+ while (activePanAncestor && !activePanAncestor.gestureEnabled) {
358
+ activePanAncestor = activePanAncestor.ancestorContext;
359
+ }
340
360
 
341
- // Allow ancestors to block child native gestures
342
- if (ancestorContext?.panGesture && nativeGesture) {
343
- ancestorContext.panGesture.blocksExternalGesture(nativeGesture);
361
+ if (activePanAncestor?.panGesture) {
362
+ // Found an ancestor with enabled pan - wait for it
363
+ nativeGesture = Gesture.Native().requireExternalGestureToFail(
364
+ activePanAncestor.panGesture,
365
+ );
366
+ } else {
367
+ // No ancestor with enabled pan - plain native
368
+ nativeGesture = Gesture.Native();
369
+ }
344
370
  }
345
371
 
346
372
  return {
347
373
  panGesture,
374
+ panGestureRef,
348
375
  nativeGesture,
349
376
  gestureAnimationValues,
350
377
  };
@@ -0,0 +1,19 @@
1
+ import { useGestureContext } from "../../providers/gestures.provider";
2
+
3
+ /**
4
+ * Returns a ref to the screen's navigation pan gesture.
5
+ * Use this to coordinate child gestures with the navigation gesture.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const screenGesture = useScreenGesture();
10
+ *
11
+ * const myPanGesture = Gesture.Pan()
12
+ * .waitFor(screenGesture) // Wait for navigation gesture to fail first
13
+ * .onUpdate(...);
14
+ * ```
15
+ */
16
+ export const useScreenGesture = () => {
17
+ const ctx = useGestureContext();
18
+ return ctx?.panGestureRef ?? null;
19
+ };
@@ -1,3 +1,6 @@
1
+ /** biome-ignore-all lint/style/noNonNullAssertion: <Will always consume context from GestureProvider> */
2
+
3
+ import { useMemo } from "react";
1
4
  import type { LayoutChangeEvent } from "react-native";
2
5
  import { useAnimatedScrollHandler } from "react-native-reanimated";
3
6
  import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes";
@@ -12,13 +15,26 @@ interface ScrollProgressHookProps {
12
15
  }
13
16
 
14
17
  export const useScrollRegistry = (props: ScrollProgressHookProps) => {
15
- const { scrollConfig, ancestorContext } = useGestureContext();
18
+ const context = useGestureContext()!;
19
+ const { scrollConfig, ancestorContext } = context;
20
+
21
+ const ancestorScrollConfigs = useMemo(() => {
22
+ const configs: (typeof scrollConfig)[] = [];
23
+ let current = ancestorContext;
24
+ while (current) {
25
+ if (current.scrollConfig) {
26
+ configs.push(current.scrollConfig);
27
+ }
28
+ current = current.ancestorContext;
29
+ }
30
+ return configs;
31
+ }, [ancestorContext]);
16
32
 
17
33
  const scrollHandler = useAnimatedScrollHandler({
18
34
  onScroll: (event) => {
19
35
  props.onScroll?.(event);
20
36
 
21
- scrollConfig.modify((v: Any) => {
37
+ const updateScrollPosition = (v: Any) => {
22
38
  "worklet";
23
39
  if (v === null) {
24
40
  return {
@@ -33,25 +49,13 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
33
49
  v.x = event.contentOffset.x;
34
50
  v.y = event.contentOffset.y;
35
51
  return v;
36
- });
37
-
38
- if (ancestorContext?.scrollConfig) {
39
- ancestorContext.scrollConfig.modify((v: Any) => {
40
- "worklet";
41
- if (v === null) {
42
- return {
43
- x: event.contentOffset.x,
44
- y: event.contentOffset.y,
45
- contentHeight: 0,
46
- contentWidth: 0,
47
- layoutHeight: 0,
48
- layoutWidth: 0,
49
- };
50
- }
51
- v.x = event.contentOffset.x;
52
- v.y = event.contentOffset.y;
53
- return v;
54
- });
52
+ };
53
+
54
+ scrollConfig.modify(updateScrollPosition);
55
+
56
+ // Sync to ALL ancestors, not just immediate parent
57
+ for (const ancestorConfig of ancestorScrollConfigs) {
58
+ ancestorConfig.modify(updateScrollPosition);
55
59
  }
56
60
  },
57
61
  });
@@ -60,7 +64,7 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
60
64
  (width: number, height: number) => {
61
65
  props.onContentSizeChange?.(width, height);
62
66
 
63
- scrollConfig.modify((v: Any) => {
67
+ const updateContentSize = (v: Any) => {
64
68
  "worklet";
65
69
  if (v === null) {
66
70
  return {
@@ -75,24 +79,12 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
75
79
  v.contentWidth = width;
76
80
  v.contentHeight = height;
77
81
  return v;
78
- });
79
- if (ancestorContext?.scrollConfig) {
80
- ancestorContext.scrollConfig.modify((v: Any) => {
81
- "worklet";
82
- if (v === null) {
83
- return {
84
- x: 0,
85
- y: 0,
86
- layoutHeight: 0,
87
- layoutWidth: 0,
88
- contentWidth: width,
89
- contentHeight: height,
90
- };
91
- }
92
- v.contentWidth = width;
93
- v.contentHeight = height;
94
- return v;
95
- });
82
+ };
83
+
84
+ scrollConfig.modify(updateContentSize);
85
+
86
+ for (const ancestorConfig of ancestorScrollConfigs) {
87
+ ancestorConfig.modify(updateContentSize);
96
88
  }
97
89
  },
98
90
  );
@@ -101,7 +93,7 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
101
93
  props.onLayout?.(event);
102
94
  const { width, height } = event.nativeEvent.layout;
103
95
 
104
- scrollConfig.modify((v: Any) => {
96
+ const updateLayout = (v: Any) => {
105
97
  "worklet";
106
98
  if (v === null) {
107
99
  return {
@@ -116,24 +108,12 @@ export const useScrollRegistry = (props: ScrollProgressHookProps) => {
116
108
  v.layoutHeight = height;
117
109
  v.layoutWidth = width;
118
110
  return v;
119
- });
120
- if (ancestorContext?.scrollConfig) {
121
- ancestorContext.scrollConfig.modify((v: Any) => {
122
- "worklet";
123
- if (v === null) {
124
- return {
125
- x: 0,
126
- y: 0,
127
- contentHeight: 0,
128
- contentWidth: 0,
129
- layoutHeight: height,
130
- layoutWidth: width,
131
- };
132
- }
133
- v.layoutHeight = height;
134
- v.layoutWidth = width;
135
- return v;
136
- });
111
+ };
112
+
113
+ scrollConfig.modify(updateLayout);
114
+
115
+ for (const ancestorConfig of ancestorScrollConfigs) {
116
+ ancestorConfig.modify(updateLayout);
137
117
  }
138
118
  });
139
119
 
@@ -19,6 +19,7 @@ export default {
19
19
  };
20
20
 
21
21
  export { useScreenAnimation } from "./hooks/animation/use-screen-animation";
22
+ export { useScreenGesture } from "./hooks/gestures/use-screen-gesture";
22
23
 
23
24
  export type {
24
25
  AnimationConfig,
@@ -26,4 +27,5 @@ export type {
26
27
  ScreenInterpolationProps,
27
28
  ScreenStyleInterpolator,
28
29
  } from "./types/animation.types";
30
+ export type { BoundEntry, BoundsLink } from "./types/bounds.types";
29
31
  export type { ScreenTransitionConfig } from "./types/core.types";
@@ -1,11 +1,13 @@
1
- import { createContext, useContext, useMemo } from "react";
2
1
  import { StyleSheet, View } from "react-native";
3
- import type { GestureType } from "react-native-gesture-handler";
4
- import { GestureDetector } from "react-native-gesture-handler";
2
+ import {
3
+ GestureDetector,
4
+ type GestureType,
5
+ } from "react-native-gesture-handler";
5
6
  import type { SharedValue } from "react-native-reanimated";
6
7
  import { useSharedValue } from "react-native-reanimated";
7
8
  import { useBuildGestures } from "../hooks/gestures/use-build-gestures";
8
9
  import type { GestureStoreMap } from "../stores/gesture.store";
10
+ import createProvider from "../utils/create-provider";
9
11
  import { useKeys } from "./keys.provider";
10
12
 
11
13
  export type ScrollConfig = {
@@ -19,94 +21,52 @@ export type ScrollConfig = {
19
21
 
20
22
  export interface GestureContextType {
21
23
  panGesture: GestureType;
24
+ panGestureRef: React.MutableRefObject<GestureType | undefined>;
22
25
  nativeGesture: GestureType;
23
26
  scrollConfig: SharedValue<ScrollConfig | null>;
24
27
  gestureAnimationValues: GestureStoreMap;
25
- ancestorContext: GestureContextType | undefined;
28
+ ancestorContext: GestureContextType | null;
29
+ gestureEnabled: boolean;
26
30
  }
27
31
 
28
- type GestureProviderProps = {
29
- children: React.ReactNode;
30
- };
31
-
32
- const GestureContext = createContext<GestureContextType | undefined>(undefined);
33
-
34
- /**
35
- * Provider that creates gesture handling for a screen.
36
- * If the current screen doesn't have gestures enabled but a parent does,
37
- * we pass through the parent's context so scrollable children can coordinate
38
- * with the ancestor's gestures.
39
- */
40
- export const ScreenGestureProvider = ({ children }: GestureProviderProps) => {
41
- const ancestorContext = useContext(GestureContext);
32
+ export const {
33
+ ScreenGestureProvider,
34
+ useScreenGestureContext: useGestureContext,
35
+ } = createProvider("ScreenGesture", { guarded: false })<
36
+ { children: React.ReactNode },
37
+ GestureContextType
38
+ >(({ children }) => {
42
39
  const { current } = useKeys();
43
-
44
- const hasOwnGestures = current.options.gestureEnabled === true;
45
-
46
- // If this screen doesn't have its own gestures but an ancestor does,
47
- // pass through so scrollable children coordinate with that ancestor
48
- if (!hasOwnGestures && ancestorContext) {
49
- return children;
50
- }
51
-
52
- return (
53
- <ScreenGestureProviderInner ancestorContext={ancestorContext}>
54
- {children}
55
- </ScreenGestureProviderInner>
56
- );
57
- };
58
-
59
- const ScreenGestureProviderInner = ({
60
- children,
61
- ancestorContext,
62
- }: GestureProviderProps & {
63
- ancestorContext: GestureContextType | undefined;
64
- }) => {
40
+ const ancestorContext = useGestureContext();
65
41
  const scrollConfig = useSharedValue<ScrollConfig | null>(null);
66
42
 
67
- const { panGesture, nativeGesture, gestureAnimationValues } =
43
+ const hasGestures = current.options.gestureEnabled === true;
44
+
45
+ const { panGesture, panGestureRef, nativeGesture, gestureAnimationValues } =
68
46
  useBuildGestures({
69
47
  scrollConfig,
70
48
  ancestorContext,
71
49
  });
72
50
 
73
- const value: GestureContextType = useMemo(
74
- () => ({
75
- panGesture,
76
- scrollConfig,
77
- nativeGesture,
78
- gestureAnimationValues,
79
- ancestorContext,
80
- }),
81
- [
82
- panGesture,
83
- scrollConfig,
84
- nativeGesture,
85
- gestureAnimationValues,
86
- ancestorContext,
87
- ],
88
- );
89
-
90
- return (
91
- <GestureContext.Provider value={value}>
51
+ const value: GestureContextType = {
52
+ panGesture,
53
+ panGestureRef,
54
+ scrollConfig,
55
+ nativeGesture,
56
+ gestureAnimationValues,
57
+ ancestorContext,
58
+ gestureEnabled: hasGestures,
59
+ };
60
+
61
+ return {
62
+ value,
63
+ children: (
92
64
  <GestureDetector gesture={panGesture}>
93
65
  <View style={styles.container}>{children}</View>
94
66
  </GestureDetector>
95
- </GestureContext.Provider>
96
- );
97
- };
98
-
99
- export const useGestureContext = () => {
100
- const context = useContext(GestureContext);
101
-
102
- if (!context) {
103
- throw new Error(
104
- "useGestureContext must be used within a ScreenGestureProvider",
105
- );
106
- }
107
-
108
- return context;
109
- };
67
+ ),
68
+ };
69
+ });
110
70
 
111
71
  const styles = StyleSheet.create({
112
72
  container: {
@@ -21,10 +21,59 @@ export interface OverlayInterpolationProps {
21
21
  }
22
22
 
23
23
  export type ScreenTransitionState = {
24
+ /**
25
+ * Animation progress for this screen.
26
+ * - `0`: Screen is fully off-screen (entering)
27
+ * - `1`: Screen is fully visible (active)
28
+ *
29
+ * This value animates from 0 to 1 when the screen enters,
30
+ * and from 1 to 0 when it exits.
31
+ */
24
32
  progress: number;
33
+
34
+ /**
35
+ * Whether this screen is in the process of being dismissed.
36
+ * - `0`: Screen is opening or active
37
+ * - `1`: Screen is closing/being dismissed
38
+ *
39
+ * Use this to trigger different animations when navigating back vs forward.
40
+ */
25
41
  closing: number;
42
+
43
+ /**
44
+ * Whether this screen is currently animating.
45
+ * - `0`: No animation in progress
46
+ * - `1`: Animation or gesture is in progress
47
+ */
26
48
  animating: number;
49
+
50
+ /**
51
+ * Live gesture values for this screen.
52
+ * Contains translation (x, y), normalized values (-1 to 1),
53
+ * and flags for dragging/dismissing state.
54
+ */
27
55
  gesture: GestureValues;
56
+
57
+ /**
58
+ * Custom metadata passed from screen options.
59
+ * Use this for conditional animation logic instead of checking route names.
60
+ *
61
+ * @example
62
+ * // In screen options:
63
+ * options={{ meta: { scalesOthers: true } }}
64
+ *
65
+ * // In animation logic:
66
+ * if (props.next?.meta?.scalesOthers) { ... }
67
+ */
68
+ meta?: Record<string, unknown>;
69
+
70
+ /**
71
+ * The route object for this screen.
72
+ *
73
+ * @deprecated Use `meta` instead for conditional animation logic.
74
+ * Pass route params via options: `options={({ route }) => ({ meta: { id: route.params.id } })}`
75
+ * This field may be removed in a future version.
76
+ */
28
77
  route: RouteProp<ParamListBase>;
29
78
  };
30
79
 
@@ -19,7 +19,18 @@ export type BoundEntry = {
19
19
  styles: StyleProps;
20
20
  };
21
21
 
22
+ export type BoundsLink = {
23
+ source: BoundEntry | null;
24
+ destination: BoundEntry | null;
25
+ };
26
+
22
27
  export type BoundsAccessor = {
23
28
  <T extends BoundsBuilderOptions>(options: T): BoundsReturnType<T>;
24
29
  getSnapshot: (id: string, key?: string) => Snapshot | null;
30
+ getLink: (id: string) => BoundsLink | null;
31
+ interpolateStyle: (
32
+ id: string,
33
+ property: keyof StyleProps,
34
+ fallback?: number,
35
+ ) => number;
25
36
  };
@@ -100,4 +100,12 @@ export type ScreenTransitionConfig = {
100
100
  * The area of the screen where the gesture is activated.
101
101
  */
102
102
  gestureActivationArea?: GestureActivationArea;
103
+
104
+ /**
105
+ * Custom metadata passed through to animation props.
106
+ *
107
+ * @example
108
+ * options={{ meta: { scalesOthers: true } }}
109
+ */
110
+ meta?: Record<string, unknown>;
103
111
  };
@@ -0,0 +1,38 @@
1
+ import { interpolate } from "react-native-reanimated";
2
+ import { ENTER_RANGE, EXIT_RANGE } from "../../../constants";
3
+ import type { BoundsLink } from "../../../types/bounds.types";
4
+
5
+ type InterpolateStyleOptions = {
6
+ fallback?: number;
7
+ };
8
+
9
+ /**
10
+ * Interpolates a numeric style property between source and destination bounds.
11
+ *
12
+ * @param link - The bounds link containing source and destination styles
13
+ * @param property - The style property to interpolate (e.g., "borderRadius", "opacity")
14
+ * @param progress - Animation progress value
15
+ * @param entering - Whether the screen is entering (focused) or exiting (unfocused)
16
+ * @param options - Optional configuration
17
+ * @returns The interpolated value
18
+ */
19
+ export function interpolateLinkStyle(
20
+ link: BoundsLink | null,
21
+ property: string,
22
+ progress: number,
23
+ entering: boolean,
24
+ options: InterpolateStyleOptions = {},
25
+ ): number {
26
+ "worklet";
27
+
28
+ const { fallback = 0 } = options;
29
+
30
+ const sourceValue =
31
+ (link?.source?.styles?.[property] as number | undefined) ?? fallback;
32
+ const destValue =
33
+ (link?.destination?.styles?.[property] as number | undefined) ?? fallback;
34
+
35
+ const range = entering ? ENTER_RANGE : EXIT_RANGE;
36
+
37
+ return interpolate(progress, range, [sourceValue, destValue], "clamp");
38
+ }
@@ -11,12 +11,13 @@ import type {
11
11
  ScreenInterpolationProps,
12
12
  ScreenTransitionState,
13
13
  } from "../../types/animation.types";
14
- import type { BoundsAccessor } from "../../types/bounds.types";
14
+ import type { BoundsAccessor, BoundsLink } from "../../types/bounds.types";
15
15
  import type { Layout } from "../../types/core.types";
16
16
  import {
17
17
  computeContentTransformGeometry,
18
18
  computeRelativeGeometry,
19
19
  } from "./helpers/geometry";
20
+ import { interpolateLinkStyle } from "./helpers/interpolate-style";
20
21
  import {
21
22
  composeContentStyle,
22
23
  composeSizeAbsolute,
@@ -207,7 +208,36 @@ export const createBounds = (
207
208
  return BoundStore.getSnapshot(tag, key);
208
209
  };
209
210
 
211
+ const getLink = (tag: string): BoundsLink | null => {
212
+ "worklet";
213
+ const link = BoundStore.getActiveLink(tag, props.current?.route.key);
214
+ if (!link) return null;
215
+ return {
216
+ source: link.source
217
+ ? { bounds: link.source.bounds, styles: link.source.styles }
218
+ : null,
219
+ destination: link.destination
220
+ ? { bounds: link.destination.bounds, styles: link.destination.styles }
221
+ : null,
222
+ };
223
+ };
224
+
225
+ const interpolateStyle = (
226
+ tag: string,
227
+ property: string,
228
+ fallback?: number,
229
+ ): number => {
230
+ "worklet";
231
+ const link = getLink(tag);
232
+ const entering = !props.next;
233
+ return interpolateLinkStyle(link, property, props.progress, entering, {
234
+ fallback,
235
+ });
236
+ };
237
+
210
238
  return Object.assign(boundsFunction, {
211
239
  getSnapshot,
240
+ getLink,
241
+ interpolateStyle,
212
242
  }) as BoundsAccessor;
213
243
  };
@@ -9,8 +9,11 @@ import {
9
9
  type ReactNode,
10
10
  useContext,
11
11
  useMemo,
12
+ useRef,
12
13
  } from "react";
13
14
 
15
+ type InnerProviderComponent = (props: { children: ReactNode }) => ReactNode;
16
+
14
17
  export default function createProvider<
15
18
  ProviderName extends string,
16
19
  Guarded extends boolean = true,
@@ -19,7 +22,13 @@ export default function createProvider<
19
22
  factory: (props: ProviderProps) => {
20
23
  value?: ContextValue;
21
24
  enabled?: boolean;
22
- children?: ReactNode;
25
+ children?:
26
+ | ReactNode
27
+ | ((
28
+ innerProvider: {
29
+ [K in ProviderName as `${K}Provider`]: InnerProviderComponent;
30
+ },
31
+ ) => ReactNode);
23
32
  },
24
33
  ) => {
25
34
  const { guarded = true } = options ?? {};
@@ -45,6 +54,27 @@ export default function createProvider<
45
54
  [enabled, value],
46
55
  );
47
56
 
57
+ // Per-instance ref ensures InnerProvider reads latest value while keeping
58
+ // a stable component reference.
59
+ const valueRef = useRef<ContextValue | null>(memoValue);
60
+ valueRef.current = memoValue;
61
+
62
+ const InnerProvider = useMemo(
63
+ (): InnerProviderComponent =>
64
+ ({ children }) => (
65
+ <Context.Provider value={valueRef.current}>
66
+ {children}
67
+ </Context.Provider>
68
+ ),
69
+ [],
70
+ );
71
+
72
+ if (typeof children === "function") {
73
+ return children({
74
+ [`${name}Provider`]: InnerProvider,
75
+ } as { [K in ProviderName as `${K}Provider`]: InnerProviderComponent });
76
+ }
77
+
48
78
  return <Context.Provider value={memoValue}>{children}</Context.Provider>;
49
79
  };
50
80
 
@@ -72,7 +72,7 @@ const DEFAULT_EDGE_DISTANCE_HORIZONTAL = 50;
72
72
  const DEFAULT_EDGE_DISTANCE_VERTICAL = 135;
73
73
  const DEFAULT_ACTIVATION_AREA = "screen" as const;
74
74
 
75
- function normalizeSides(area?: GestureActivationArea): NormalizedSides {
75
+ export function normalizeSides(area?: GestureActivationArea): NormalizedSides {
76
76
  "worklet";
77
77
  if (!area || typeof area === "string") {
78
78
  const mode: ActivationArea = area ?? DEFAULT_ACTIVATION_AREA;
@@ -88,7 +88,7 @@ function normalizeSides(area?: GestureActivationArea): NormalizedSides {
88
88
  };
89
89
  }
90
90
 
91
- function computeEdgeConstraints(
91
+ export function computeEdgeConstraints(
92
92
  initialTouch: { x: number; y: number },
93
93
  dimensions: Layout,
94
94
  sides: NormalizedSides,
@@ -108,7 +108,7 @@ function computeEdgeConstraints(
108
108
  return { horizontalRight, horizontalLeft, verticalDown, verticalUp } as const;
109
109
  }
110
110
 
111
- function calculateSwipeDirs(deltaX: number, deltaY: number) {
111
+ export function calculateSwipeDirs(deltaX: number, deltaY: number) {
112
112
  "worklet";
113
113
 
114
114
  const isVerticalSwipe = Math.abs(deltaY) > Math.abs(deltaX);
@@ -129,7 +129,7 @@ function calculateSwipeDirs(deltaX: number, deltaY: number) {
129
129
  };
130
130
  }
131
131
 
132
- function shouldActivateOrFail(params: ShouldActivateOrFailProps) {
132
+ export function shouldActivateOrFail(params: ShouldActivateOrFailProps) {
133
133
  "worklet";
134
134
 
135
135
  const {