react-native-screen-transitions 3.0.0-rc.5 → 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 (96) 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/shared/components/create-transition-aware-component.js +2 -0
  5. package/lib/commonjs/shared/components/create-transition-aware-component.js.map +1 -1
  6. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js +8 -4
  7. package/lib/commonjs/shared/hooks/animation/use-screen-animation.js.map +1 -1
  8. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js +29 -5
  9. package/lib/commonjs/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  10. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture.js +26 -0
  11. package/lib/commonjs/shared/hooks/gestures/use-screen-gesture.js.map +1 -0
  12. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js +32 -60
  13. package/lib/commonjs/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  14. package/lib/commonjs/shared/index.js +7 -0
  15. package/lib/commonjs/shared/index.js.map +1 -1
  16. package/lib/commonjs/shared/providers/gestures.provider.js +8 -18
  17. package/lib/commonjs/shared/providers/gestures.provider.js.map +1 -1
  18. package/lib/commonjs/shared/utils/bounds/helpers/interpolate-style.js +30 -0
  19. package/lib/commonjs/shared/utils/bounds/helpers/interpolate-style.js.map +1 -0
  20. package/lib/commonjs/shared/utils/bounds/index.js +29 -1
  21. package/lib/commonjs/shared/utils/bounds/index.js.map +1 -1
  22. package/lib/commonjs/shared/utils/create-provider.js +16 -0
  23. package/lib/commonjs/shared/utils/create-provider.js.map +1 -1
  24. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js +4 -0
  25. package/lib/commonjs/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  26. package/lib/module/blank-stack/components/overlay.js +1 -1
  27. package/lib/module/blank-stack/components/overlay.js.map +1 -1
  28. package/lib/module/shared/components/create-transition-aware-component.js +2 -0
  29. package/lib/module/shared/components/create-transition-aware-component.js.map +1 -1
  30. package/lib/module/shared/hooks/animation/use-screen-animation.js +8 -4
  31. package/lib/module/shared/hooks/animation/use-screen-animation.js.map +1 -1
  32. package/lib/module/shared/hooks/gestures/use-build-gestures.js +30 -6
  33. package/lib/module/shared/hooks/gestures/use-build-gestures.js.map +1 -1
  34. package/lib/module/shared/hooks/gestures/use-screen-gesture.js +22 -0
  35. package/lib/module/shared/hooks/gestures/use-screen-gesture.js.map +1 -0
  36. package/lib/module/shared/hooks/gestures/use-scroll-registry.js +32 -60
  37. package/lib/module/shared/hooks/gestures/use-scroll-registry.js.map +1 -1
  38. package/lib/module/shared/index.js +1 -0
  39. package/lib/module/shared/index.js.map +1 -1
  40. package/lib/module/shared/providers/gestures.provider.js +9 -19
  41. package/lib/module/shared/providers/gestures.provider.js.map +1 -1
  42. package/lib/module/shared/utils/bounds/helpers/interpolate-style.js +26 -0
  43. package/lib/module/shared/utils/bounds/helpers/interpolate-style.js.map +1 -0
  44. package/lib/module/shared/utils/bounds/index.js +29 -1
  45. package/lib/module/shared/utils/bounds/index.js.map +1 -1
  46. package/lib/module/shared/utils/create-provider.js +17 -1
  47. package/lib/module/shared/utils/create-provider.js.map +1 -1
  48. package/lib/module/shared/utils/gesture/check-gesture-activation.js +4 -4
  49. package/lib/module/shared/utils/gesture/check-gesture-activation.js.map +1 -1
  50. package/lib/typescript/blank-stack/types.d.ts +2 -14
  51. package/lib/typescript/blank-stack/types.d.ts.map +1 -1
  52. package/lib/typescript/shared/components/create-transition-aware-component.d.ts +1 -0
  53. package/lib/typescript/shared/components/create-transition-aware-component.d.ts.map +1 -1
  54. package/lib/typescript/shared/hooks/animation/use-screen-animation.d.ts.map +1 -1
  55. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts +1 -0
  56. package/lib/typescript/shared/hooks/gestures/use-build-gestures.d.ts.map +1 -1
  57. package/lib/typescript/shared/hooks/gestures/use-screen-gesture.d.ts +15 -0
  58. package/lib/typescript/shared/hooks/gestures/use-screen-gesture.d.ts.map +1 -0
  59. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts +1 -0
  60. package/lib/typescript/shared/hooks/gestures/use-scroll-registry.d.ts.map +1 -1
  61. package/lib/typescript/shared/index.d.ts +4 -2
  62. package/lib/typescript/shared/index.d.ts.map +1 -1
  63. package/lib/typescript/shared/providers/gestures.provider.d.ts +2 -6
  64. package/lib/typescript/shared/providers/gestures.provider.d.ts.map +1 -1
  65. package/lib/typescript/shared/types/animation.types.d.ts +44 -0
  66. package/lib/typescript/shared/types/animation.types.d.ts.map +1 -1
  67. package/lib/typescript/shared/types/bounds.types.d.ts +6 -0
  68. package/lib/typescript/shared/types/bounds.types.d.ts.map +1 -1
  69. package/lib/typescript/shared/types/core.types.d.ts +7 -0
  70. package/lib/typescript/shared/types/core.types.d.ts.map +1 -1
  71. package/lib/typescript/shared/utils/bounds/helpers/interpolate-style.d.ts +17 -0
  72. package/lib/typescript/shared/utils/bounds/helpers/interpolate-style.d.ts.map +1 -0
  73. package/lib/typescript/shared/utils/bounds/index.d.ts.map +1 -1
  74. package/lib/typescript/shared/utils/create-provider.d.ts +5 -1
  75. package/lib/typescript/shared/utils/create-provider.d.ts.map +1 -1
  76. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts +49 -1
  77. package/lib/typescript/shared/utils/gesture/check-gesture-activation.d.ts.map +1 -1
  78. package/package.json +1 -1
  79. package/src/blank-stack/components/overlay.tsx +1 -1
  80. package/src/blank-stack/types.ts +2 -15
  81. package/src/shared/__tests__/derivations.test.ts +155 -0
  82. package/src/shared/__tests__/gesture-activation.test.ts +251 -0
  83. package/src/shared/components/create-transition-aware-component.tsx +2 -1
  84. package/src/shared/hooks/animation/use-screen-animation.tsx +8 -2
  85. package/src/shared/hooks/gestures/use-build-gestures.tsx +35 -8
  86. package/src/shared/hooks/gestures/use-screen-gesture.ts +19 -0
  87. package/src/shared/hooks/gestures/use-scroll-registry.tsx +39 -59
  88. package/src/shared/index.ts +2 -0
  89. package/src/shared/providers/gestures.provider.tsx +15 -27
  90. package/src/shared/types/animation.types.ts +49 -0
  91. package/src/shared/types/bounds.types.ts +11 -0
  92. package/src/shared/types/core.types.ts +8 -0
  93. package/src/shared/utils/bounds/helpers/interpolate-style.ts +38 -0
  94. package/src/shared/utils/bounds/index.ts +31 -1
  95. package/src/shared/utils/create-provider.tsx +31 -1
  96. package/src/shared/utils/gesture/check-gesture-activation.ts +4 -4
@@ -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,7 +1,5 @@
1
- import { useMemo } from "react";
2
1
  import { StyleSheet, View } from "react-native";
3
2
  import {
4
- Gesture,
5
3
  GestureDetector,
6
4
  type GestureType,
7
5
  } from "react-native-gesture-handler";
@@ -23,18 +21,14 @@ export type ScrollConfig = {
23
21
 
24
22
  export interface GestureContextType {
25
23
  panGesture: GestureType;
24
+ panGestureRef: React.MutableRefObject<GestureType | undefined>;
26
25
  nativeGesture: GestureType;
27
26
  scrollConfig: SharedValue<ScrollConfig | null>;
28
27
  gestureAnimationValues: GestureStoreMap;
29
28
  ancestorContext: GestureContextType | null;
29
+ gestureEnabled: boolean;
30
30
  }
31
31
 
32
- /**
33
- * Provider that creates gesture handling for a screen.
34
- * If the current screen doesn't have gestures enabled but an ancestor does,
35
- * we pass through the ancestor's context so scrollable children can coordinate
36
- * with the ancestor's gestures.
37
- */
38
32
  export const {
39
33
  ScreenGestureProvider,
40
34
  useScreenGestureContext: useGestureContext,
@@ -42,38 +36,32 @@ export const {
42
36
  { children: React.ReactNode },
43
37
  GestureContextType
44
38
  >(({ children }) => {
45
- const ancestorContext = useGestureContext();
46
39
  const { current } = useKeys();
40
+ const ancestorContext = useGestureContext();
47
41
  const scrollConfig = useSharedValue<ScrollConfig | null>(null);
48
42
 
49
- const hasOwnGestures = current.options.gestureEnabled === true;
50
- const shouldPassthrough = !hasOwnGestures && !!ancestorContext;
43
+ const hasGestures = current.options.gestureEnabled === true;
51
44
 
52
- const { panGesture, nativeGesture, gestureAnimationValues } =
45
+ const { panGesture, panGestureRef, nativeGesture, gestureAnimationValues } =
53
46
  useBuildGestures({
54
47
  scrollConfig,
55
48
  ancestorContext,
56
49
  });
57
50
 
58
- const value: GestureContextType = shouldPassthrough
59
- ? ancestorContext
60
- : {
61
- panGesture,
62
- scrollConfig,
63
- nativeGesture,
64
- gestureAnimationValues,
65
- ancestorContext,
66
- };
67
-
68
- // When passing through, use a no-op gesture to avoid conflicts.
69
- // Attaching the same gesture to multiple GestureDetectors causes issues.
70
- const noOpGesture = useMemo(() => Gesture.Pan().enabled(false), []);
71
- const activeGesture = shouldPassthrough ? noOpGesture : panGesture;
51
+ const value: GestureContextType = {
52
+ panGesture,
53
+ panGestureRef,
54
+ scrollConfig,
55
+ nativeGesture,
56
+ gestureAnimationValues,
57
+ ancestorContext,
58
+ gestureEnabled: hasGestures,
59
+ };
72
60
 
73
61
  return {
74
62
  value,
75
63
  children: (
76
- <GestureDetector gesture={activeGesture}>
64
+ <GestureDetector gesture={panGesture}>
77
65
  <View style={styles.container}>{children}</View>
78
66
  </GestureDetector>
79
67
  ),
@@ -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 {