*/
import type React from "react";
import { type ComponentType, forwardRef, memo } from "react";
-import type { View } from "react-native";
import { GestureDetector } from "react-native-gesture-handler";
-import Animated, {
- runOnUI,
- useAnimatedRef,
- useComposedEventHandler,
-} from "react-native-reanimated";
-import { RegisterBoundsProvider } from "../providers/register-bounds.provider";
+import Animated, { useComposedEventHandler } from "react-native-reanimated";
import {
ScrollMetadataOwnerProvider,
useScrollGestureCoordination,
@@ -36,7 +30,6 @@ export function createTransitionAwareComponent(
TransitionAwareProps
>((props: any, ref) => {
const {
- remeasureOnFocus: _remeasureOnFocus,
onScroll: userOnScroll,
onMomentumScrollEnd: userOnMomentumScrollEnd,
onScrollEndDrag: userOnScrollEndDrag,
@@ -112,45 +105,27 @@ export function createTransitionAwareComponent
(
const Inner = forwardRef<
React.ComponentRef,
TransitionAwareProps
- >((props, _) => {
+ >((props, ref) => {
const {
children,
style,
- sharedBoundTag,
styleId,
- onPress,
- remeasureOnFocus,
animatedProps: userAnimatedProps,
...rest
} = props as any;
- const animatedRef = useAnimatedRef();
- const associatedId = sharedBoundTag || styleId;
- const associatedStyles = useSlotStyles(associatedId);
- const associatedProps = useSlotProps(associatedId);
+ const associatedStyles = useSlotStyles(styleId);
+ const associatedProps = useSlotProps(styleId);
return (
-
- {({ captureActiveOnPress, handleInitialLayout }) => (
-
- {children}
-
- )}
-
+ {children}
+
);
});
diff --git a/packages/react-native-screen-transitions/src/shared/components/screen-container/hooks/use-backdrop-pointer-events.ts b/packages/react-native-screen-transitions/src/shared/components/screen-container/hooks/use-backdrop-pointer-events.ts
index b66e04d6..ccc88de7 100644
--- a/packages/react-native-screen-transitions/src/shared/components/screen-container/hooks/use-backdrop-pointer-events.ts
+++ b/packages/react-native-screen-transitions/src/shared/components/screen-container/hooks/use-backdrop-pointer-events.ts
@@ -1,8 +1,8 @@
import { useState } from "react";
import { runOnJS, useAnimatedReaction } from "react-native-reanimated";
-import { useDescriptors } from "../../../providers/screen/descriptors";
-import { useScreenOptionsContext } from "../../../providers/screen/options";
-import { useStackCoreContext } from "../../../providers/stack/core.provider";
+import { useDescriptorsStore } from "../../../providers/screen/descriptors";
+import { useScreenOptionsStore } from "../../../providers/screen/options";
+import { useStackCoreStore } from "../../../providers/stack/core.provider";
import type { BackdropBehavior } from "../../../types/screen.types";
import { StackType } from "../../../types/stack.types";
@@ -21,9 +21,11 @@ interface BackdropPointerEventsResult {
* - Other stacks default to 'block' (undefined = normal touch handling)
*/
export function useBackdropPointerEvents(): BackdropPointerEventsResult {
- const { current } = useDescriptors();
- const screenOptions = useScreenOptionsContext();
- const { flags } = useStackCoreContext();
+ const screenOptions = useScreenOptionsStore();
+ const descriptorBackdropBehavior = useDescriptorsStore(
+ (store) => store.options.backdropBehavior,
+ );
+ const stackType = useStackCoreStore((store) => store.flags.STACK_TYPE);
const [runtimeBackdropBehavior, setRuntimeBackdropBehavior] = useState<
BackdropBehavior | undefined
>(undefined);
@@ -39,10 +41,10 @@ export function useBackdropPointerEvents(): BackdropPointerEventsResult {
[screenOptions],
);
- const isComponentStack = flags.STACK_TYPE === StackType.COMPONENT;
+ const isComponentStack = stackType === StackType.COMPONENT;
const backdropBehavior: BackdropBehavior =
runtimeBackdropBehavior ??
- current.options.backdropBehavior ??
+ descriptorBackdropBehavior ??
(isComponentStack ? "passthrough" : "block");
const pointerEvents =
diff --git a/packages/react-native-screen-transitions/src/shared/components/screen-container/hooks/use-content-layout.ts b/packages/react-native-screen-transitions/src/shared/components/screen-container/hooks/use-content-layout.ts
index 90903f23..06069541 100644
--- a/packages/react-native-screen-transitions/src/shared/components/screen-container/hooks/use-content-layout.ts
+++ b/packages/react-native-screen-transitions/src/shared/components/screen-container/hooks/use-content-layout.ts
@@ -1,10 +1,7 @@
import { useCallback } from "react";
import { type LayoutChangeEvent, useWindowDimensions } from "react-native";
import { runOnUI } from "react-native-reanimated";
-import {
- useDescriptorDerivations,
- useDescriptors,
-} from "../../../providers/screen/descriptors";
+import { useDescriptorsStore } from "../../../providers/screen/descriptors";
import { AnimationStore } from "../../../stores/animation.store";
import {
LifecycleTransitionRequestKind,
@@ -12,10 +9,16 @@ import {
} from "../../../stores/system.store";
export function useContentLayout() {
- const { current } = useDescriptors();
- const { isFirstKey } = useDescriptorDerivations();
+ const routeKey = useDescriptorsStore(
+ (store) => store.derivations.currentScreenKey,
+ );
+ const isFirstKey = useDescriptorsStore(
+ (store) => store.derivations.isFirstKey,
+ );
+ const experimental_animateOnInitialMount = useDescriptorsStore(
+ (store) => store.options.experimental_animateOnInitialMount,
+ );
const { height: screenHeight } = useWindowDimensions();
- const routeKey = current.route.key;
const animations = AnimationStore.getBag(routeKey);
const system = SystemStore.getBag(routeKey);
@@ -23,9 +26,6 @@ export function useContentLayout() {
system;
const { requestLifecycleTransition } = system.actions;
- const experimental_animateOnInitialMount =
- current.options.experimental_animateOnInitialMount;
-
return useCallback(
(event: LayoutChangeEvent) => {
const { width, height } = event.nativeEvent.layout;
diff --git a/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/backdrop.tsx b/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/backdrop.tsx
index cc3ff8e5..ab0bd6fc 100644
--- a/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/backdrop.tsx
+++ b/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/backdrop.tsx
@@ -3,7 +3,7 @@ import { Pressable, StyleSheet } from "react-native";
import Animated, { runOnUI } from "react-native-reanimated";
import { DefaultSnapSpec } from "../../../configs/specs";
import { useNavigationHelpers } from "../../../hooks/navigation/use-navigation-helpers";
-import { useDescriptors } from "../../../providers/screen/descriptors";
+import { useDescriptorsStore } from "../../../providers/screen/descriptors";
import { useSlotProps, useSlotStyles } from "../../../providers/screen/styles";
import { AnimationStore } from "../../../stores/animation.store";
import { GestureStore } from "../../../stores/gesture.store";
@@ -23,11 +23,23 @@ export const BackdropLayer = memo(function BackdropLayer({
backdropBehavior: BackdropBehavior;
isBackdropActive: boolean;
}) {
- const { current } = useDescriptors();
const { dismissScreen } = useNavigationHelpers();
- const BackdropComponent = current.options.backdropComponent;
- const routeKey = current.route.key;
+ const routeKey = useDescriptorsStore(
+ (store) => store.derivations.currentScreenKey,
+ );
+ const BackdropComponent = useDescriptorsStore(
+ (store) => store.options.backdropComponent,
+ );
+ const rawSnapPoints = useDescriptorsStore(
+ (store) => store.options.snapPoints,
+ );
+ const canDismiss = useDescriptorsStore(
+ (store) => store.options.gestureEnabled !== false,
+ );
+ const transitionSpec = useDescriptorsStore(
+ (store) => store.options.transitionSpec,
+ );
const animations = AnimationStore.getBag(routeKey);
const { targetProgress, animationProgress, resolvedAutoSnapPoint } =
SystemStore.getBag(routeKey);
@@ -49,9 +61,6 @@ export const BackdropLayer = memo(function BackdropLayer({
}
if (backdropBehavior === "collapse") {
- const rawSnapPoints = current.options.snapPoints;
- const canDismiss = current.options.gestureEnabled !== false;
-
// No snap points → fallback to dismiss
if (!rawSnapPoints || rawSnapPoints.length === 0) {
dismissScreen();
@@ -59,7 +68,6 @@ export const BackdropLayer = memo(function BackdropLayer({
}
const gestures = GestureStore.getBag(routeKey);
- const transitionSpec = current.options.transitionSpec;
runOnUI(() => {
"worklet";
@@ -95,6 +103,7 @@ export const BackdropLayer = memo(function BackdropLayer({
animateToProgress({
target,
+ markEntering: false,
spec,
animations,
targetProgress,
@@ -109,7 +118,9 @@ export const BackdropLayer = memo(function BackdropLayer({
animationProgress,
resolvedAutoSnapPoint,
backdropBehavior,
- current,
+ rawSnapPoints,
+ canDismiss,
+ transitionSpec,
dismissScreen,
routeKey,
]);
diff --git a/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/content.tsx b/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/content.tsx
index 4dcdcb4e..1050a64b 100644
--- a/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/content.tsx
+++ b/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/content.tsx
@@ -3,9 +3,8 @@ import { type ComponentType, memo, useMemo } from "react";
import { StyleSheet, View } from "react-native";
import { GestureDetector } from "react-native-gesture-handler";
import Animated from "react-native-reanimated";
-import { useDescriptors } from "../../../providers/screen/descriptors";
-import { useGestureContext } from "../../../providers/screen/gestures";
-import { OriginProvider } from "../../../providers/screen/origin.provider";
+import { useDescriptorsStore } from "../../../providers/screen/descriptors";
+import { useGestureStore } from "../../../providers/screen/gestures";
import { useSlotProps, useSlotStyles } from "../../../providers/screen/styles";
import type { ScreenContentComponentProps } from "../../../types";
import { ScreenFallbackHost } from "../../boundary/portal/components/boundary-portal/components/host";
@@ -22,11 +21,16 @@ type Props = {
export const ContentLayer = memo(
({ children, pointerEvents, isBackdropActive }: Props) => {
- const { current } = useDescriptors();
-
- const gestureContext = useGestureContext();
- const ContentComponent = current.options.contentComponent;
- const isNavigationMaskEnabled = !!current.options.navigationMaskEnabled;
+ const gestureContext = useGestureStore();
+ const ContentComponent = useDescriptorsStore(
+ (store) => store.options.contentComponent,
+ );
+ const isNavigationMaskEnabled = useDescriptorsStore(
+ (store) => !!store.options.navigationMaskEnabled,
+ );
+ const hasAutoSnapPoint = useDescriptorsStore(
+ (store) => store.options.snapPoints?.includes("auto") ?? false,
+ );
const contentPointerEvents = isBackdropActive ? "box-none" : pointerEvents;
const AnimatedContentComponent = useMemo(() => {
@@ -37,9 +41,6 @@ export const ContentLayer = memo(
: null;
}, [ContentComponent]);
- const hasAutoSnapPoint =
- current.options.snapPoints?.includes("auto") ?? false;
-
const handleContentLayout = useContentLayout();
const animatedContentStyle = useSlotStyles("content");
@@ -57,16 +58,14 @@ export const ContentLayer = memo(
enabled={isNavigationMaskEnabled}
>
-
- {hasAutoSnapPoint ? (
-
- {children}
-
- ) : (
- children
- )}
-
-
+ {hasAutoSnapPoint ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
);
diff --git a/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/surface-container.tsx b/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/surface-container.tsx
index c5b1d4f5..f8d30149 100644
--- a/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/surface-container.tsx
+++ b/packages/react-native-screen-transitions/src/shared/components/screen-container/layers/surface-container.tsx
@@ -1,7 +1,7 @@
import { type ComponentType, memo, useMemo } from "react";
import { StyleSheet, type ViewProps } from "react-native";
import Animated from "react-native-reanimated";
-import { useDescriptors } from "../../../providers/screen/descriptors";
+import { useDescriptorsStore } from "../../../providers/screen/descriptors";
import { useSlotProps, useSlotStyles } from "../../../providers/screen/styles";
type Props = {
@@ -10,10 +10,10 @@ type Props = {
};
export const SurfaceContainer = memo(({ children, pointerEvents }: Props) => {
- const { current } = useDescriptors();
-
/** @deprecated Use `contentComponent` instead. */
- const DeprecatedSurfaceComponent = current.options.surfaceComponent;
+ const DeprecatedSurfaceComponent = useDescriptorsStore(
+ (store) => store.options.surfaceComponent,
+ );
const AnimatedSurfaceComponent = useMemo | null>(() => {
return DeprecatedSurfaceComponent
diff --git a/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-close-transition-intent.ts b/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-close-transition-intent.ts
index c7945bd1..c1ca5962 100644
--- a/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-close-transition-intent.ts
+++ b/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-close-transition-intent.ts
@@ -3,10 +3,10 @@ import { useLayoutEffect, useMemo, useRef } from "react";
import useStableCallback from "../../../hooks/use-stable-callback";
import {
type BaseDescriptor,
- useDescriptorDerivations,
+ useDescriptorsStore,
} from "../../../providers/screen/descriptors";
-import { useBlankStackContext } from "../../../providers/stack/blank-stack.provider";
-import { useStackCoreContext } from "../../../providers/stack/core.provider";
+import { useBlankStackStore } from "../../../providers/stack/blank-stack.provider";
+import { useStackCoreStore } from "../../../providers/stack/core.provider";
import { GestureStore } from "../../../stores/gesture.store";
import {
LifecycleTransitionRequestKind,
@@ -27,10 +27,15 @@ const useBlankStackClose = ({
requestLifecycleTransition,
resetStores,
}: CloseHookParams) => {
- const { handleCloseRoute } = useBlankStackContext();
+ const handleCloseRoute = useBlankStackStore(
+ (store) => store?.handleCloseRoute,
+ );
+ const isClosing = useBlankStackStore(
+ (store) => store?.scenesByKey[current.route.key]?.activity === "closing",
+ );
useLayoutEffect(() => {
- if (current.activity !== "closing") {
+ if (!isClosing) {
return;
}
@@ -38,10 +43,13 @@ const useBlankStackClose = ({
LifecycleTransitionRequestKind.BlankStackClose,
0,
);
- }, [current.activity, requestLifecycleTransition]);
+ }, [isClosing, requestLifecycleTransition]);
const handleBlankStackCloseEnd = useStableCallback((finished: boolean) => {
if (!finished) return;
+ if (!handleCloseRoute) {
+ throw new Error("Blank stack close handler was not found.");
+ }
handleCloseRoute({ route: current.route });
requestAnimationFrame(() => {
resetStores();
@@ -59,7 +67,9 @@ const useNativeStackClose = ({
requestLifecycleTransition,
resetStores,
}: CloseHookParams) => {
- const { parentScreenKey } = useDescriptorDerivations();
+ const parentScreenKey = useDescriptorsStore(
+ (store) => store.derivations.parentScreenKey,
+ );
const pendingActionRef = useRef(null);
const nearestAncestorDismissing = useMemo(() => {
@@ -121,8 +131,8 @@ export function useCloseTransitionIntent(
handleNativeCloseEnd?: (finished: boolean) => void;
} {
const routeKey = current.route.key;
- const { flags } = useStackCoreContext();
- const isNativeStack = flags.STACK_TYPE === StackType.NATIVE;
+ const stackType = useStackCoreStore((store) => store.flags.STACK_TYPE);
+ const isNativeStack = stackType === StackType.NATIVE;
const { requestLifecycleTransition } = system.actions;
const resetStores = useStableCallback(() => {
diff --git a/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-open-transition-intent.ts b/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-open-transition-intent.ts
index 8b7d1078..3781105d 100644
--- a/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-open-transition-intent.ts
+++ b/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-open-transition-intent.ts
@@ -1,7 +1,7 @@
import { useLayoutEffect } from "react";
import {
type BaseDescriptor,
- useDescriptorDerivations,
+ useDescriptorsStore,
} from "../../../providers/screen/descriptors";
import type { AnimationStoreMap } from "../../../stores/animation.store";
import {
@@ -41,7 +41,9 @@ export function useOpenTransitionIntent(
animations: AnimationStoreMap,
system: SystemStoreMap,
) {
- const { isFirstKey } = useDescriptorDerivations();
+ const isFirstKey = useDescriptorsStore(
+ (store) => store.derivations.isFirstKey,
+ );
const { requestLifecycleTransition } = system.actions;
// biome-ignore lint/correctness/useExhaustiveDependencies: Must only run once on mount
@@ -78,6 +80,7 @@ export function useOpenTransitionIntent(
return;
}
+ animations.entering.set(1);
requestLifecycleTransition(
LifecycleTransitionRequestKind.Open,
initialProgress ?? 1,
diff --git a/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-transition-start-controller.ts b/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-transition-start-controller.ts
index 2d9b460c..3b394f7d 100644
--- a/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-transition-start-controller.ts
+++ b/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/hooks/use-transition-start-controller.ts
@@ -1,6 +1,7 @@
import { useAnimatedReaction } from "react-native-reanimated";
import type { BaseDescriptor } from "../../../providers/screen/descriptors";
import type { AnimationStoreMap } from "../../../stores/animation.store";
+import { GestureStore } from "../../../stores/gesture.store";
import {
LifecycleTransitionRequestKind,
type SystemStoreMap,
@@ -29,6 +30,7 @@ export const useTransitionStartController = ({
} = system;
const { clearLifecycleTransitionRequest } = system.actions;
const transitionSpec = current.options.transitionSpec;
+ const isDragging = GestureStore.getValue(current.route.key, "dragging");
useAnimatedReaction(
() => {
@@ -61,10 +63,14 @@ export const useTransitionStartController = ({
animateToProgress({
target,
spec: transitionSpec,
+ emitWillAnimate:
+ kind !== LifecycleTransitionRequestKind.Open ||
+ !animations.entering.get(),
animations,
targetProgress,
animationProgress,
onAnimationFinish,
+ isDragging,
});
clearLifecycleTransitionRequest();
diff --git a/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/index.tsx b/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/index.tsx
index f1ebdf88..4a7691df 100644
--- a/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/index.tsx
+++ b/packages/react-native-screen-transitions/src/shared/components/screen-lifecycle/index.tsx
@@ -1,4 +1,4 @@
-import { useDescriptors } from "../../providers/screen/descriptors";
+import { useDescriptorsStore } from "../../providers/screen/descriptors";
import { AnimationStore } from "../../stores/animation.store";
import { SystemStore } from "../../stores/system.store";
import { useScreenHistory } from "./hooks/history/use-screen-history";
@@ -15,7 +15,8 @@ interface Props {
* Reads current/previous descriptors from DescriptorsProvider context.
*/
export const ScreenLifecycle = ({ children }: Props) => {
- const { current, previous } = useDescriptors();
+ const current = useDescriptorsStore((store) => store.current);
+ const previous = useDescriptorsStore((store) => store.previous);
const animations = AnimationStore.getBag(current.route.key);
const system = SystemStore.getBag(current.route.key);
diff --git a/packages/react-native-screen-transitions/src/shared/configs/presets.ts b/packages/react-native-screen-transitions/src/shared/configs/presets.ts
index 2f449484..6e1ae32f 100644
--- a/packages/react-native-screen-transitions/src/shared/configs/presets.ts
+++ b/packages/react-native-screen-transitions/src/shared/configs/presets.ts
@@ -237,10 +237,10 @@ export const ElasticCard = (
};
export const SharedIGImage = ({
- sharedBoundTag,
+ id,
...config
}: Partial & {
- sharedBoundTag: string;
+ id: string;
}): ScreenTransitionConfig => {
return {
gestureEnabled: true,
@@ -278,11 +278,11 @@ export const SharedIGImage = ({
const navigationStyles =
bounds({
- id: sharedBoundTag,
+ id,
}).navigation.zoom() ?? {};
// Extract raw style values from bounds result (legacy format)
- const sourceStyle = navigationStyles[sharedBoundTag] as
+ const sourceStyle = navigationStyles[id] as
| Record
| undefined;
const containerStyle = navigationStyles[
@@ -347,7 +347,7 @@ export const SharedIGImage = ({
pointerEvents: current.gesture.dismissing ? "none" : "auto",
},
},
- [sharedBoundTag]: {
+ [id]: {
style: {
...(sourceStyle ?? {}),
transform: [
@@ -384,10 +384,10 @@ export const SharedIGImage = ({
};
export const SharedAppleMusic = ({
- sharedBoundTag,
+ id,
...config
}: Partial & {
- sharedBoundTag: string;
+ id: string;
}): ScreenTransitionConfig => {
return {
enableTransitions: true,
@@ -434,7 +434,7 @@ export const SharedAppleMusic = ({
const dragXScale = interpolate(normX, [0, 1], xScaleOuput);
const dragYScale = interpolate(normY, [0, 1], yScaleOuput);
- const boundValues = bounds({ id: sharedBoundTag }).values({
+ const boundValues = bounds({ id }).values({
method: focused ? "content" : "transform",
anchor: "top",
scaleMode: "uniform",
@@ -453,7 +453,7 @@ export const SharedAppleMusic = ({
* ===============================
*/
if (focused) {
- const maskedValues = bounds({ id: sharedBoundTag }).values({
+ const maskedValues = bounds({ id }).values({
space: "absolute",
method: "size",
target: "fullscreen",
@@ -545,7 +545,7 @@ export const SharedAppleMusic = ({
const contentScale = interpolate(progress, [1, 2], [1, 0.9], "clamp");
return {
- [sharedBoundTag]: {
+ [id]: {
style: {
transform: [
{ translateX: dragX || 0 },
@@ -593,10 +593,10 @@ export const SharedAppleMusic = ({
};
export const SharedXImage = ({
- sharedBoundTag,
+ id,
...config
}: Partial & {
- sharedBoundTag: string;
+ id: string;
}): ScreenTransitionConfig => {
return {
//@ts-expect-error - Should not lead to any issues
@@ -616,7 +616,7 @@ export const SharedXImage = ({
const navigationStyles =
bounds({
- id: sharedBoundTag,
+ id,
}).navigation.zoom() ?? {};
const maskStyle = navigationStyles[NAVIGATION_MASK_ELEMENT_STYLE_ID] as
| Record
diff --git a/packages/react-native-screen-transitions/src/shared/constants.ts b/packages/react-native-screen-transitions/src/shared/constants.ts
index 9a69d908..59dadb0e 100644
--- a/packages/react-native-screen-transitions/src/shared/constants.ts
+++ b/packages/react-native-screen-transitions/src/shared/constants.ts
@@ -71,6 +71,7 @@ const DEFAULT_GESTURE_VALUES = {
dragging: 0,
settling: 0,
active: null,
+ initiator: "none",
direction: null,
// Deprecated aliases
diff --git a/packages/react-native-screen-transitions/src/shared/hooks/navigation/use-navigation-helpers.ts b/packages/react-native-screen-transitions/src/shared/hooks/navigation/use-navigation-helpers.ts
index 509287b9..5a04ba64 100644
--- a/packages/react-native-screen-transitions/src/shared/hooks/navigation/use-navigation-helpers.ts
+++ b/packages/react-native-screen-transitions/src/shared/hooks/navigation/use-navigation-helpers.ts
@@ -4,28 +4,29 @@ import { useDescriptorsStore } from "../../providers/screen/descriptors";
import { useStack } from "./use-stack";
export function useNavigationHelpers() {
- const current = useDescriptorsStore((store) => store.descriptors.current);
+ const route = useDescriptorsStore((store) => store.current.route);
+ const navigation = useDescriptorsStore((store) => store.current.navigation);
const requestStackDismiss = useStack((stack) => stack.requestDismiss);
const requestDismiss = useCallback((): boolean => {
- return requestStackDismiss?.({ route: current.route }) ?? false;
- }, [current.route, requestStackDismiss]);
+ return requestStackDismiss?.({ route }) ?? false;
+ }, [route, requestStackDismiss]);
const dismissScreen = useCallback((): boolean => {
- const state = current.navigation.getState();
+ const state = navigation.getState();
const routeIndex = state.routes.findIndex(
- (route) => route.key === current.route.key,
+ (stateRoute) => stateRoute.key === route.key,
);
const routeStillPresent = routeIndex !== -1;
if (!routeStillPresent || routeIndex === 0) return false;
- current.navigation.dispatch({
+ navigation.dispatch({
...StackActions.pop(),
- source: current.route.key,
+ source: route.key,
target: state.key,
});
return true;
- }, [current]);
+ }, [navigation, route.key]);
return { dismissScreen, requestDismiss };
}
diff --git a/packages/react-native-screen-transitions/src/shared/hooks/navigation/use-screen-state.ts b/packages/react-native-screen-transitions/src/shared/hooks/navigation/use-screen-state.ts
index 58c91f9c..d3197ead 100644
--- a/packages/react-native-screen-transitions/src/shared/hooks/navigation/use-screen-state.ts
+++ b/packages/react-native-screen-transitions/src/shared/hooks/navigation/use-screen-state.ts
@@ -3,7 +3,7 @@ import { useCallback, useMemo } from "react";
import { snapDescriptorToIndex } from "../../animation/snap-to";
import {
type BaseDescriptor,
- useDescriptors,
+ useDescriptorsStore,
} from "../../providers/screen/descriptors";
import type { ScreenTransitionConfig } from "../../types/screen.types";
import type { BaseStackNavigation } from "../../types/stack.types";
@@ -65,7 +65,9 @@ export function useScreenState<
>(): ScreenState {
const { routes, scenes, routeKeys, focusedIndex } =
useStack();
- const { current } = useDescriptors();
+ const current = useDescriptorsStore(
+ (store) => store.current,
+ ) as BaseDescriptor;
const index = useMemo(
() => routeKeys.indexOf(current.route.key),
diff --git a/packages/react-native-screen-transitions/src/shared/hooks/use-stable-callback-value.ts b/packages/react-native-screen-transitions/src/shared/hooks/use-stable-callback-value.ts
deleted file mode 100644
index e2788b2f..00000000
--- a/packages/react-native-screen-transitions/src/shared/hooks/use-stable-callback-value.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * Credit:
- * https://github.com/MatiPl01/react-native-sortables/blob/main/packages/react-native-sortables/src/integrations/reanimated/hooks/useStableCallbackValue.ts
- */
-import { useCallback, useEffect, useState } from "react";
-import {
- cancelAnimation,
- isWorkletFunction,
- makeMutable,
- runOnJS,
-} from "react-native-reanimated";
-
-type AnyFunction = (...args: Array) => any;
-
-function useMutableValue(initialValue: T) {
- const [mutable] = useState(() => makeMutable(initialValue));
-
- useEffect(() => {
- return () => {
- cancelAnimation(mutable);
- };
- }, [mutable]);
-
- return mutable;
-}
-
-// We cannot store a function as a SharedValue because reanimated will treat
-// it as an animation and will try to execute the animation when assigned
-// to the SharedValue. Since we want the function to be treated as a value,
-// we wrap it in an object and store the object in the SharedValue.
-type WrappedCallback = {
- call: C;
-};
-
-const wrap = (callback: C): WrappedCallback => {
- if (isWorkletFunction(callback)) {
- return { call: callback };
- }
- return {
- call: ((...args: Parameters) => {
- "worklet";
- runOnJS(callback)(...args);
- }) as C,
- };
-};
-
-/** Creates a stable worklet callback that can be called from the UI runtime
- * @param callback The JavaScript or worklet function to be called
- * @returns A worklet function that can be safely called from the UI thread
- * @default Behavior:
- * - If passed a regular JS function, calls it on the JS thread using runOnJS
- * - If passed a worklet function, calls it directly on the UI thread
- * @important The returned function maintains a stable reference and properly handles
- * thread execution based on the input callback type
- */
-export default function useStableCallbackValue(
- callback?: C,
-) {
- const stableCallback = useMutableValue>(null);
-
- useEffect(() => {
- if (callback) {
- stableCallback.set(wrap(callback));
- } else {
- stableCallback.set(null);
- }
- }, [callback, stableCallback]);
-
- return useCallback(
- (...args: Parameters) => {
- "worklet";
- stableCallback.get()?.call(...args);
- },
- [stableCallback],
- );
-}
diff --git a/packages/react-native-screen-transitions/src/shared/providers/register-bounds.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/register-bounds.provider.tsx
deleted file mode 100644
index 69532c45..00000000
--- a/packages/react-native-screen-transitions/src/shared/providers/register-bounds.provider.tsx
+++ /dev/null
@@ -1,402 +0,0 @@
-import { useFocusEffect } from "@react-navigation/native";
-import { type ReactNode, useCallback, useMemo, useRef } from "react";
-import type { View } from "react-native";
-import {
- type AnimatedRef,
- measure,
- runOnJS,
- runOnUI,
- type StyleProps,
- useAnimatedReaction,
- useSharedValue,
-} from "react-native-reanimated";
-import type { SharedValue } from "react-native-reanimated/lib/typescript/commonTypes";
-import useStableCallback from "../hooks/use-stable-callback";
-import useStableCallbackValue from "../hooks/use-stable-callback-value";
-import { AnimationStore } from "../stores/animation.store";
-import {
- createPendingPairKey,
- createScreenPairKey,
-} from "../stores/bounds/helpers/link-pairs.helpers";
-import { getEntry } from "../stores/bounds/internals/entries";
-import { getDestination, getSource } from "../stores/bounds/internals/links";
-import { prepareStyleForBounds } from "../utils/bounds/helpers/styles/styles";
-import createProvider from "../utils/create-provider";
-import { applyMeasuredBoundsWrites } from "./helpers/measured-bounds-writes";
-import { useDescriptorDerivations, useDescriptors } from "./screen/descriptors";
-
-/**
- * @deprecated Internal legacy provider for the old shared-bound-tag registration
- * path. Do not build new boundary behavior here; use the Transition.Boundary
- * measurement pipeline instead.
- */
-interface MaybeMeasureAndStoreParams {
- onPress?: ((...args: unknown[]) => void) | undefined;
- shouldSetSource?: boolean;
- shouldSetDestination?: boolean;
-}
-
-interface RegisterBoundsRenderProps {
- handleInitialLayout: () => void;
- captureActiveOnPress: () => void;
-}
-
-interface RegisterBoundsProviderProps {
- sharedBoundTag?: string;
- animatedRef: AnimatedRef;
- style: StyleProps;
- onPress?: ((...args: unknown[]) => void) | undefined;
- remeasureOnFocus?: boolean;
- children: (props: RegisterBoundsRenderProps) => ReactNode;
-}
-
-interface RegisterBoundsContextValue {
- updateSignal: SharedValue;
-}
-
-const getRouteParamId = (
- route: { params?: object } | undefined,
-): string | null => {
- const params = route?.params as Record | undefined;
- const rawId = params?.id;
- return typeof rawId === "string" ? rawId : null;
-};
-
-const matchesSelectionTag = (
- tag: string | undefined,
- selectedId: string | null,
-): boolean => {
- "worklet";
- if (!tag || !selectedId) return false;
- if (tag === selectedId) return true;
- if (tag.endsWith(`:${selectedId}`)) return true;
- if (tag.endsWith(`-${selectedId}`)) return true;
- return false;
-};
-
-/**
- * Handles initial layout measurement for destination elements.
- * Only measures if an animation is in progress (local or parent).
- */
-const useInitialLayoutHandler = (params: {
- sharedBoundTag?: string;
- currentScreenKey: string;
- ancestorKeys: string[];
- maybeMeasureAndStore: (options: MaybeMeasureAndStoreParams) => void;
-}) => {
- const {
- sharedBoundTag,
- currentScreenKey,
- ancestorKeys,
- maybeMeasureAndStore,
- } = params;
-
- const isAnimating = AnimationStore.getValue(
- currentScreenKey,
- "progressAnimating",
- );
-
- // Check if any ancestor is animating
- const ancestorAnimations = ancestorKeys.map((key) =>
- AnimationStore.getValue(key, "progressAnimating"),
- );
-
- const hasMeasuredOnLayout = useSharedValue(false);
-
- return useCallback(() => {
- "worklet";
- if (!sharedBoundTag || hasMeasuredOnLayout.get()) return;
-
- // Check if current or any ancestor is animating
- let isAnyAnimating = isAnimating.get();
- for (let i = 0; i < ancestorAnimations.length; i++) {
- if (ancestorAnimations[i].get()) {
- isAnyAnimating = 1;
- break;
- }
- }
-
- if (!isAnyAnimating) return;
-
- maybeMeasureAndStore({
- shouldSetSource: false,
- shouldSetDestination: true,
- });
-
- hasMeasuredOnLayout.set(true);
- }, [
- sharedBoundTag,
- hasMeasuredOnLayout,
- isAnimating,
- ancestorAnimations,
- maybeMeasureAndStore,
- ]);
-};
-
-/**
- * Measures non-pressable elements when screen becomes blurred.
- * Captures bounds right before transition starts.
- */
-const useBlurMeasurement = (params: {
- enabled: boolean;
- sharedBoundTag?: string;
- selectedRouteId: string | null;
- ancestorKeys: string[];
- maybeMeasureAndStore: (options: MaybeMeasureAndStoreParams) => void;
-}) => {
- const { current } = useDescriptors();
- const {
- enabled,
- sharedBoundTag,
- selectedRouteId,
- ancestorKeys,
- maybeMeasureAndStore,
- } = params;
- const hasCapturedSource = useRef(false);
-
- const ancestorClosing = [current.route.key, ...ancestorKeys].map((key) =>
- AnimationStore.getValue(key, "closing"),
- );
-
- const maybeMeasureOnBlur = useStableCallbackValue(() => {
- "worklet";
- if (!enabled) return;
- if (!matchesSelectionTag(sharedBoundTag, selectedRouteId)) return;
-
- //.some doesnt work here apparently... :-(
- for (const closing of ancestorClosing) {
- if (closing.get()) return;
- }
-
- maybeMeasureAndStore({ shouldSetSource: true });
- });
-
- useFocusEffect(
- useCallback(() => {
- hasCapturedSource.current = false;
-
- if (!enabled) {
- return;
- }
-
- return () => {
- if (!sharedBoundTag || hasCapturedSource.current) return;
- runOnUI(maybeMeasureOnBlur)();
- };
- }, [enabled, sharedBoundTag, maybeMeasureOnBlur]),
- );
-
- return {
- markSourceCaptured: () => {
- hasCapturedSource.current = true;
- },
- };
-};
-
-/**
- * Syncs child measurements when parent updates.
- */
-const useParentSyncReaction = (params: {
- parentContext: RegisterBoundsContextValue | null;
- maybeMeasureAndStore: (options?: MaybeMeasureAndStoreParams) => void;
-}) => {
- const { parentContext, maybeMeasureAndStore } = params;
-
- useAnimatedReaction(
- () => parentContext?.updateSignal.get(),
- (value) => {
- "worklet";
- if (value === 0 || value === undefined) return;
- maybeMeasureAndStore();
- },
- );
-};
-
-let useRegisterBoundsContext: () => RegisterBoundsContextValue | null;
-
-const registerBoundsBundle = createProvider("RegisterBounds", {
- guarded: false,
-})(
- ({ style, onPress, sharedBoundTag, animatedRef, children }) => {
- const { current, next } = useDescriptors();
- const { ancestorKeys, previousScreenKey } = useDescriptorDerivations();
- const currentScreenKey = current.route.key;
- const selectedNextRouteId = getRouteParamId(next?.route);
-
- // Context & signals
- const parentContext = useRegisterBoundsContext();
-
- const ownSignal = useSharedValue(0);
- const updateSignal: SharedValue =
- parentContext?.updateSignal ?? ownSignal;
-
- const isAnimating = AnimationStore.getValue(
- currentScreenKey,
- "progressAnimating",
- );
- const preparedStyles = useMemo(() => prepareStyleForBounds(style), [style]);
-
- const emitUpdate = useStableCallbackValue(() => {
- "worklet";
- const isRoot = !parentContext;
- if (isRoot) updateSignal.set(updateSignal.get() + 1);
- });
-
- const maybeMeasureAndStore = useStableCallbackValue(
- ({
- onPress,
- shouldSetSource,
- shouldSetDestination,
- }: MaybeMeasureAndStoreParams = {}) => {
- "worklet";
- if (!sharedBoundTag) {
- if (onPress) runOnJS(onPress)();
- return;
- }
-
- const sourcePairKey = createPendingPairKey(currentScreenKey);
- const destinationPairKey = previousScreenKey
- ? createScreenPairKey(previousScreenKey, currentScreenKey)
- : undefined;
- const pendingSourcePairKey = previousScreenKey
- ? createPendingPairKey(previousScreenKey)
- : undefined;
-
- if (shouldSetSource && isAnimating.get()) {
- const existing = getEntry(sharedBoundTag, currentScreenKey);
- if (existing?.bounds) {
- applyMeasuredBoundsWrites({
- entryTag: sharedBoundTag,
- linkId: sharedBoundTag,
- currentScreenKey,
- measured: existing.bounds,
- preparedStyles,
- linkWrite: {
- type: "source",
- pairKey: sourcePairKey,
- },
- });
- }
-
- if (onPress) runOnJS(onPress)();
- return;
- }
-
- const hasPendingSource = pendingSourcePairKey
- ? getSource(pendingSourcePairKey, sharedBoundTag) !== null
- : false;
- const hasSource =
- getSource(sourcePairKey, sharedBoundTag) !== null ||
- (destinationPairKey
- ? getSource(destinationPairKey, sharedBoundTag) !== null
- : false);
- const hasDestination = destinationPairKey
- ? getDestination(destinationPairKey, sharedBoundTag) !== null
- : false;
-
- const wantsSetSource = !!shouldSetSource;
- const wantsSetDestination = !!shouldSetDestination;
- const wantsSnapshotOnly = !wantsSetSource && !wantsSetDestination;
-
- const canSetSource = wantsSetSource;
- const canSetDestination =
- wantsSetDestination && (hasPendingSource || hasSource);
- const canSnapshotOnly =
- wantsSnapshotOnly &&
- (hasPendingSource || hasSource || hasDestination);
-
- if (!canSetSource && !canSetDestination && !canSnapshotOnly) {
- if (onPress) runOnJS(onPress)();
- return;
- }
-
- const measured = measure(animatedRef);
- if (!measured) {
- if (onPress) runOnJS(onPress)();
- return;
- }
-
- emitUpdate();
-
- applyMeasuredBoundsWrites({
- entryTag: sharedBoundTag,
- linkId: sharedBoundTag,
- currentScreenKey,
- measured,
- preparedStyles,
- linkWrite: canSetSource
- ? {
- type: "source",
- pairKey: sourcePairKey,
- }
- : canSetDestination && destinationPairKey
- ? {
- type: "destination",
- pairKey: destinationPairKey,
- }
- : undefined,
- });
-
- if (canSetSource && canSetDestination && destinationPairKey) {
- applyMeasuredBoundsWrites({
- entryTag: sharedBoundTag,
- linkId: sharedBoundTag,
- currentScreenKey,
- measured,
- preparedStyles,
- linkWrite: {
- type: "destination",
- pairKey: destinationPairKey,
- },
- });
- }
-
- if (onPress) runOnJS(onPress)();
- },
- );
-
- const handleInitialLayout = useInitialLayoutHandler({
- sharedBoundTag,
- currentScreenKey,
- ancestorKeys,
- maybeMeasureAndStore,
- });
-
- // Side effects
- const { markSourceCaptured } = useBlurMeasurement({
- enabled: !onPress,
- sharedBoundTag,
- selectedRouteId: selectedNextRouteId,
- maybeMeasureAndStore,
- ancestorKeys,
- });
-
- useParentSyncReaction({ parentContext, maybeMeasureAndStore });
-
- const captureActiveOnPress = useStableCallback(() => {
- if (!sharedBoundTag) {
- onPress?.();
- return;
- }
- runOnUI(maybeMeasureAndStore)({ onPress, shouldSetSource: true });
- markSourceCaptured();
- });
-
- return {
- value: { updateSignal },
- children: <>{children({ handleInitialLayout, captureActiveOnPress })}>,
- };
- },
-);
-
-/**
- * Legacy bounds registration provider used by transition-aware components.
- *
- * @deprecated Prefer the newer bounds system (`Transition.Boundary`, `bounds()`,
- * and navigation-style bounds helpers) for new code. This provider remains only
- * for backwards compatibility with the older shared-bound-tag registration path.
- */
-const RegisterBoundsProvider = registerBoundsBundle.RegisterBoundsProvider;
-useRegisterBoundsContext = registerBoundsBundle.useRegisterBoundsContext;
-
-export { RegisterBoundsProvider };
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/animation.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/animation.provider.tsx
index 21c7e3f9..2e94d7d2 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/animation.provider.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/animation.provider.tsx
@@ -1,10 +1,4 @@
-import {
- type ReactNode,
- useCallback,
- useContext,
- useLayoutEffect,
- useMemo,
-} from "react";
+import { type ReactNode, useCallback, useLayoutEffect, useMemo } from "react";
import { useSharedValue } from "react-native-reanimated";
import { createBoundsAccessor } from "../../../utils/bounds";
import createProvider from "../../../utils/create-provider";
@@ -34,178 +28,181 @@ export type ScreenAnimationContextResult = {
value: ScreenAnimationContextValue;
};
-export const {
- ScreenAnimationProvider,
- ScreenAnimationContext,
- useScreenAnimationContext,
-} = createProvider("ScreenAnimation", {
- guarded: true,
-})((): ScreenAnimationContextResult => {
- const parentContext = useContext(ScreenAnimationContext);
- const parentScreenInterpolatorProps = parentContext?.screenInterpolatorProps;
- const parentScreenInterpolatorPropsRevision =
- parentContext?.screenInterpolatorPropsRevision;
- const parentAncestorScreenAnimationSources =
- parentContext?.ancestorScreenAnimationSources;
- const parentRegisterDescendantScreenAnimationSource =
- parentContext?.registerDescendantScreenAnimationSource;
- const parentAncestorDescendantScreenAnimationRegistrars =
- parentContext?.ancestorDescendantScreenAnimationRegistrars;
-
- const {
- screenInterpolatorProps,
- screenInterpolatorPropsRevision,
- selectedInterpolatorOptions,
- nextInterpolator,
- currentInterpolator,
- } = useScreenAnimationPipeline();
-
- const selfScreenAnimationSource = useMemo(
- () => ({
- screenInterpolatorProps,
- screenInterpolatorPropsRevision,
- }),
- [screenInterpolatorProps, screenInterpolatorPropsRevision],
- );
-
- const selfScreenAnimationTransitionSource =
- useMemo(
- () => ({
- ...selfScreenAnimationSource,
- boundsAccessor: createBoundsAccessor(() => {
- "worklet";
- return selfScreenAnimationSource.screenInterpolatorProps.get();
+export const { ScreenAnimationProvider, useScreenAnimationStore } =
+ createProvider("ScreenAnimation", {
+ guarded: true,
+ })(
+ (_props, { useParentStore }): ScreenAnimationContextResult => {
+ const parentContext = useParentStore();
+ const parentScreenInterpolatorProps =
+ parentContext?.screenInterpolatorProps;
+ const parentScreenInterpolatorPropsRevision =
+ parentContext?.screenInterpolatorPropsRevision;
+ const parentAncestorScreenAnimationSources =
+ parentContext?.ancestorScreenAnimationSources;
+ const parentRegisterDescendantScreenAnimationSource =
+ parentContext?.registerDescendantScreenAnimationSource;
+ const parentAncestorDescendantScreenAnimationRegistrars =
+ parentContext?.ancestorDescendantScreenAnimationRegistrars;
+
+ const {
+ screenInterpolatorProps,
+ screenInterpolatorPropsRevision,
+ selectedInterpolatorOptions,
+ nextInterpolator,
+ currentInterpolator,
+ } = useScreenAnimationPipeline();
+
+ const selfScreenAnimationSource = useMemo(
+ () => ({
+ screenInterpolatorProps,
+ screenInterpolatorPropsRevision,
}),
- }),
- [selfScreenAnimationSource],
- );
-
- const descendantScreenAnimationSources = useSharedValue<
- ScreenAnimationDescendantSources["value"]
- >([]);
-
- const registerDescendantScreenAnimationSource =
- useCallback(
- (source, depth) => {
- descendantScreenAnimationSources.modify(
- (
- currentSources: T,
- ): T => {
- "worklet";
- const existingIndex = currentSources.findIndex(
- (currentSource) => currentSource.source === source,
+ [screenInterpolatorProps, screenInterpolatorPropsRevision],
+ );
+
+ const selfScreenAnimationTransitionSource =
+ useMemo(
+ () => ({
+ ...selfScreenAnimationSource,
+ boundsAccessor: createBoundsAccessor(() => {
+ "worklet";
+ return selfScreenAnimationSource.screenInterpolatorProps.get();
+ }),
+ }),
+ [selfScreenAnimationSource],
+ );
+
+ const descendantScreenAnimationSources = useSharedValue<
+ ScreenAnimationDescendantSources["value"]
+ >([]);
+
+ const registerDescendantScreenAnimationSource =
+ useCallback(
+ (source, depth) => {
+ descendantScreenAnimationSources.modify(
+ (
+ currentSources: T,
+ ): T => {
+ "worklet";
+ const existingIndex = currentSources.findIndex(
+ (currentSource) => currentSource.source === source,
+ );
+
+ if (
+ existingIndex !== -1 &&
+ currentSources[existingIndex]?.depth === depth
+ ) {
+ return currentSources;
+ }
+
+ const nextSources =
+ existingIndex === -1
+ ? [...currentSources, { source, depth }]
+ : currentSources.map((currentSource, index) =>
+ index === existingIndex
+ ? { source, depth }
+ : currentSource,
+ );
+
+ return nextSources.sort((a, b) => a.depth - b.depth) as T;
+ },
);
- if (
- existingIndex !== -1 &&
- currentSources[existingIndex]?.depth === depth
- ) {
- return currentSources;
- }
-
- const nextSources =
- existingIndex === -1
- ? [...currentSources, { source, depth }]
- : currentSources.map((currentSource, index) =>
- index === existingIndex ? { source, depth } : currentSource,
- );
-
- return nextSources.sort((a, b) => a.depth - b.depth) as T;
+ return () => {
+ descendantScreenAnimationSources.modify(
+ (
+ currentSources: T,
+ ): T => {
+ "worklet";
+ return currentSources.filter(
+ (currentSource) => currentSource.source !== source,
+ ) as T;
+ },
+ );
+ };
+ },
+ [descendantScreenAnimationSources],
+ );
+
+ const ancestorScreenAnimationSources = useMemo(() => {
+ if (
+ !parentScreenInterpolatorProps ||
+ !parentScreenInterpolatorPropsRevision ||
+ !parentAncestorScreenAnimationSources
+ ) {
+ return [];
+ }
+
+ return [
+ {
+ screenInterpolatorProps: parentScreenInterpolatorProps,
+ screenInterpolatorPropsRevision:
+ parentScreenInterpolatorPropsRevision,
+ },
+ ...parentAncestorScreenAnimationSources,
+ ];
+ }, [
+ parentScreenInterpolatorProps,
+ parentScreenInterpolatorPropsRevision,
+ parentAncestorScreenAnimationSources,
+ ]);
+
+ const ancestorDescendantScreenAnimationRegistrars = useMemo(() => {
+ if (!parentRegisterDescendantScreenAnimationSource) {
+ return [];
+ }
+
+ // Each provider exposes its own descendant registrar and forwards ancestor
+ // registrars, letting a mounted child register with every ancestor scope.
+ return [
+ {
+ register: parentRegisterDescendantScreenAnimationSource,
+ depth: 1,
},
+ ...(parentAncestorDescendantScreenAnimationRegistrars ?? []).map(
+ (registrar) => ({
+ register: registrar.register,
+ depth: registrar.depth + 1,
+ }),
+ ),
+ ];
+ }, [
+ parentRegisterDescendantScreenAnimationSource,
+ parentAncestorDescendantScreenAnimationRegistrars,
+ ]);
+
+ useLayoutEffect(() => {
+ const cleanups = ancestorDescendantScreenAnimationRegistrars.map(
+ (registrar) =>
+ registrar.register(
+ selfScreenAnimationTransitionSource,
+ registrar.depth,
+ ),
);
return () => {
- descendantScreenAnimationSources.modify(
- (
- currentSources: T,
- ): T => {
- "worklet";
- return currentSources.filter(
- (currentSource) => currentSource.source !== source,
- ) as T;
- },
- );
+ for (const cleanup of cleanups) {
+ cleanup();
+ }
};
- },
- [descendantScreenAnimationSources],
- );
-
- const ancestorScreenAnimationSources = useMemo(() => {
- if (
- !parentScreenInterpolatorProps ||
- !parentScreenInterpolatorPropsRevision ||
- !parentAncestorScreenAnimationSources
- ) {
- return [];
- }
-
- return [
- {
- screenInterpolatorProps: parentScreenInterpolatorProps,
- screenInterpolatorPropsRevision: parentScreenInterpolatorPropsRevision,
- },
- ...parentAncestorScreenAnimationSources,
- ];
- }, [
- parentScreenInterpolatorProps,
- parentScreenInterpolatorPropsRevision,
- parentAncestorScreenAnimationSources,
- ]);
-
- const ancestorDescendantScreenAnimationRegistrars = useMemo(() => {
- if (!parentRegisterDescendantScreenAnimationSource) {
- return [];
- }
-
- // Each provider exposes its own descendant registrar and forwards ancestor
- // registrars, letting a mounted child register with every ancestor scope.
- return [
- {
- register: parentRegisterDescendantScreenAnimationSource,
- depth: 1,
- },
- ...(parentAncestorDescendantScreenAnimationRegistrars ?? []).map(
- (registrar) => ({
- register: registrar.register,
- depth: registrar.depth + 1,
- }),
- ),
- ];
- }, [
- parentRegisterDescendantScreenAnimationSource,
- parentAncestorDescendantScreenAnimationRegistrars,
- ]);
-
- useLayoutEffect(() => {
- const cleanups = ancestorDescendantScreenAnimationRegistrars.map(
- (registrar) =>
- registrar.register(
- selfScreenAnimationTransitionSource,
- registrar.depth,
- ),
- );
-
- return () => {
- for (const cleanup of cleanups) {
- cleanup();
- }
- };
- }, [
- ancestorDescendantScreenAnimationRegistrars,
- selfScreenAnimationTransitionSource,
- ]);
-
- return {
- value: {
- screenInterpolatorProps,
- screenInterpolatorPropsRevision,
- selectedInterpolatorOptions,
- nextInterpolator,
- currentInterpolator,
- ancestorScreenAnimationSources,
- descendantScreenAnimationSources,
- registerDescendantScreenAnimationSource,
- ancestorDescendantScreenAnimationRegistrars,
+ }, [
+ ancestorDescendantScreenAnimationRegistrars,
+ selfScreenAnimationTransitionSource,
+ ]);
+
+ return {
+ value: {
+ screenInterpolatorProps,
+ screenInterpolatorPropsRevision,
+ selectedInterpolatorOptions,
+ nextInterpolator,
+ currentInterpolator,
+ ancestorScreenAnimationSources,
+ descendantScreenAnimationSources,
+ registerDescendantScreenAnimationSource,
+ ancestorDescendantScreenAnimationRegistrars,
+ },
+ };
},
- };
-});
+ );
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/accessors/use-build-bounds-accessor.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/accessors/use-build-bounds-accessor.tsx
index 6578c573..e15d4914 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/accessors/use-build-bounds-accessor.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/accessors/use-build-bounds-accessor.tsx
@@ -1,9 +1,11 @@
import { useMemo } from "react";
import { createBoundsAccessor } from "../../../../../utils/bounds";
-import { useScreenAnimationContext } from "../../animation.provider";
+import { useScreenAnimationStore } from "../../animation.provider";
export const useBuildBoundsAccessor = () => {
- const { screenInterpolatorProps } = useScreenAnimationContext();
+ const screenInterpolatorProps = useScreenAnimationStore(
+ (store) => store.screenInterpolatorProps,
+ );
return useMemo(() => {
return createBoundsAccessor(() => {
"worklet";
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/accessors/use-build-transition-accessor.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/accessors/use-build-transition-accessor.ts
index 8ba91e32..33f5574f 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/accessors/use-build-transition-accessor.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/accessors/use-build-transition-accessor.ts
@@ -4,7 +4,7 @@ import type {
ScreenTransitionTarget,
} from "../../../../../types/animation.types";
import { createBoundsAccessor } from "../../../../../utils/bounds";
-import { useScreenAnimationContext } from "../../animation.provider";
+import { useScreenAnimationStore } from "../../animation.provider";
import type {
ScreenAnimationDescendantSources,
ScreenAnimationSource,
@@ -124,7 +124,7 @@ export const useBuildTransitionAccessor = () => {
screenInterpolatorPropsRevision,
ancestorScreenAnimationSources,
descendantScreenAnimationSources,
- } = useScreenAnimationContext();
+ } = useScreenAnimationStore();
return useMemo(() => {
const selfSource = {
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/hydrate-transition-state/index.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/hydrate-transition-state/index.ts
index adab038e..c330e5bb 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/hydrate-transition-state/index.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/hydrate-transition-state/index.ts
@@ -1,4 +1,3 @@
-import { EPSILON } from "../../../../../constants";
import type {
ScreenTransitionOptions,
TransitionInterpolatorOptions,
@@ -92,6 +91,7 @@ export const hydrateTransitionState = (
out.gesture.dragging = s.gesture.dragging.get();
out.gesture.settling = s.gesture.settling.get();
out.gesture.active = s.gesture.active.get();
+ out.gesture.initiator = out.gesture.active ?? "none";
out.gesture.direction = s.gesture.direction.get();
const handoff = out.gesture.handoff;
const useHandoffSnapshot = out.gesture.dismissing;
@@ -166,7 +166,7 @@ export const hydrateTransitionState = (
x: s.gesture.internal.progressDeltaX.get(),
y: s.gesture.internal.progressDeltaY.get(),
},
- effectiveOptions?.gestureDirection ?? s.options.gestureDirection,
+ options.gestureDirection,
getResolvedSnapBounds(
s.sortedNumericSnapPoints,
s.hasAutoSnapPoint ? s.resolvedAutoSnapPoint.get() : null,
@@ -174,35 +174,18 @@ export const hydrateTransitionState = (
),
);
- // Unsure where else to place this if im being honest.
- // I think for here is fine
if (s.visualProgress.get() !== out.progress) {
s.visualProgress.set(out.progress);
}
- const hasResidualGestureValues =
- Math.abs(out.gesture.normX) > EPSILON ||
- Math.abs(out.gesture.normY) > EPSILON ||
- Math.abs(out.gesture.normScale) > EPSILON ||
- Math.abs(out.gesture.rotation) > EPSILON;
-
- const isGestureActive =
- out.gesture.dragging ||
- out.gesture.settling ||
- (!out.gesture.dismissing && hasResidualGestureValues)
- ? 1
- : 0;
-
- const isProgressAnimating = s.progressAnimating.get();
-
- out.animating = isProgressAnimating || isGestureActive ? 1 : 0;
+ out.animating = s.progressAnimating.get();
out.gesture.normalizedX = out.gesture.normX;
out.gesture.normalizedY = out.gesture.normY;
out.gesture.isDismissing = out.gesture.dismissing;
out.gesture.isDragging = out.gesture.dragging;
- out.settled = s.progressSettled.get() && !isGestureActive ? 1 : 0;
+ out.settled = s.progressSettled.get();
out.logicallySettled = out.settled;
out.meta = s.meta;
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/pipeline.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/pipeline.ts
index a37e418d..7531ad1b 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/pipeline.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/helpers/pipeline.ts
@@ -18,7 +18,7 @@ import type {
ScreenTransitionState,
} from "../../../../types/animation.types";
-import { type BaseDescriptor, useDescriptors } from "../../descriptors";
+import { type BaseDescriptor, useDescriptorsStore } from "../../descriptors";
import { buildScreenTransitionOptions } from "./build-screen-transition-options";
import { updateDerivations } from "./derivations";
import { hasTransitionsEnabled } from "./has-transitions-enabled";
@@ -157,7 +157,7 @@ const createInitialInterpolatorProps = ({
progress: currentProgress,
entering: currentEntering,
animating: currentEntering,
- willAnimate: currentEntering,
+ willAnimate: 0,
});
const previous = prevDescriptor
? createInitialTransitionState({
@@ -173,7 +173,7 @@ const createInitialInterpolatorProps = ({
progress: 0,
entering: 1,
animating: 1,
- willAnimate: 1,
+ willAnimate: 0,
settled: 0,
})
: undefined;
@@ -280,11 +280,9 @@ export function useScreenAnimationPipeline(): ScreenAnimationPipeline {
const dimensions = useWindowDimensions();
const insets = useSafeAreaInsets();
- const {
- current: currDescriptor,
- next: nextDescriptor,
- previous: prevDescriptor,
- } = useDescriptors();
+ const currDescriptor = useDescriptorsStore((store) => store.current);
+ const nextDescriptor = useDescriptorsStore((store) => store.next);
+ const prevDescriptor = useDescriptorsStore((store) => store.previous);
const currentAnimation = useBuildTransitionState(currDescriptor);
const nextAnimation = useBuildTransitionState(nextDescriptor);
@@ -314,10 +312,8 @@ export function useScreenAnimationPipeline(): ScreenAnimationPipeline {
});
const propsRevisionState = useSharedValue({ value: 0 });
-
const screenInterpolatorPropsRevision = useDerivedValue(() => {
"worklet";
-
screenInterpolatorProps.modify((frame) => {
"worklet";
const interpolatorOptions = selectedInterpolatorOptions.get();
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/index.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/index.tsx
index b1269154..2f74dff3 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/index.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/index.tsx
@@ -1,6 +1,6 @@
export {
ScreenAnimationProvider,
- useScreenAnimationContext,
+ useScreenAnimationStore,
} from "./animation.provider";
export {
type ScreenAnimationTarget,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/use-screen-animation.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/use-screen-animation.tsx
index 146ebe68..c00feb97 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/animation/use-screen-animation.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/animation/use-screen-animation.tsx
@@ -3,7 +3,7 @@ import type {
ScreenInterpolationProps,
ScreenTransitionTarget,
} from "../../../types/animation.types";
-import { useScreenAnimationContext } from "./animation.provider";
+import { useScreenAnimationStore } from "./animation.provider";
import { useBuildTransitionAccessor } from "./helpers/accessors/use-build-transition-accessor";
import { readScreenAnimationRevisions } from "./helpers/read-screen-animation-revisions";
import type {
@@ -37,7 +37,7 @@ export function useScreenAnimation(
screenInterpolatorPropsRevision,
ancestorScreenAnimationSources,
descendantScreenAnimationSources,
- } = useScreenAnimationContext();
+ } = useScreenAnimationStore();
const transition = useBuildTransitionAccessor();
const transitionTarget = normalizeScreenAnimationTarget(
target,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/descriptors/descriptors.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/descriptors/descriptors.provider.tsx
index d1da6e1e..bc2a118f 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/descriptors/descriptors.provider.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/descriptors/descriptors.provider.tsx
@@ -1,5 +1,7 @@
import type { ReactNode } from "react";
import { useMemo } from "react";
+import { useScreenTransitionsAdapterOptionalContext } from "../../../adapters/with-screen-transitions/context";
+import { useBlankStackStore } from "../../../providers/stack/blank-stack.provider";
import type { BaseStackDescriptor } from "../../../types/stack.types";
import createProvider from "../../../utils/create-provider";
import type { DescriptorDerivations } from "./helpers/derive-descriptor-derivations";
@@ -25,87 +27,96 @@ export type DescriptorDerivationsContextValue = DescriptorDerivations;
interface DescriptorStoreValue<
TDescriptor extends BaseDescriptor = BaseDescriptor,
-> {
+> extends DescriptorsContextValue {
descriptors: DescriptorsContextValue;
derivations: DescriptorDerivationsContextValue;
+ options: TDescriptor["options"];
}
-interface DescriptorsProviderProps {
+type DescriptorsProviderProps = {
children: ReactNode;
- previous?: TDescriptor;
- current: TDescriptor;
- next?: TDescriptor;
-}
-
-const {
- DescriptorsProvider: InternalDescriptorsProvider,
- useDescriptorsStore,
-} = createProvider("Descriptors", { guarded: true })<
- DescriptorsProviderProps,
- DescriptorStoreValue
->(({ previous, current, next, children }) => {
- const descriptors = useMemo(
- () => ({ previous, current, next }),
- [previous, current, next],
- );
-
- const { ancestorKeys, ancestorDestinationPairKey } = useMemo(
- () => getAncestorKeyState(current),
- [current],
- );
-
- const derivations = useMemo(
- () =>
- deriveDescriptorDerivations({
- previous,
- current,
- next,
- ancestorKeys,
- ancestorDestinationPairKey,
- }),
- [previous, current, next, ancestorKeys, ancestorDestinationPairKey],
- );
+ previous?: BaseDescriptor;
+ current?: BaseDescriptor;
+ next?: BaseDescriptor;
+ routeKey?: string;
+};
- const value = useMemo(
- () => ({
- descriptors,
- derivations,
- }),
- [descriptors, derivations],
- );
+export const { DescriptorsProvider, useDescriptorsStore } = createProvider(
+ "Descriptors",
+ { guarded: true },
+)>(
+ ({ previous, current, next, routeKey, children }) => {
+ const blankStackCurrent = useBlankStackStore((store) =>
+ routeKey ? store?.scenesByKey[routeKey]?.descriptor : undefined,
+ );
+ const blankStackPrevious = useBlankStackStore((store) =>
+ routeKey ? store?.scenesByKey[routeKey]?.previousDescriptor : undefined,
+ );
+ const blankStackNext = useBlankStackStore((store) =>
+ routeKey ? store?.scenesByKey[routeKey]?.nextDescriptor : undefined,
+ );
+ const adapterContext = useScreenTransitionsAdapterOptionalContext();
+ const adapterScene = routeKey
+ ? (adapterContext?.scenesByKey?.[routeKey] ??
+ adapterContext?.scenes[
+ adapterContext.routeIndexByKey.get(routeKey) ?? -1
+ ])
+ : undefined;
- return {
- value,
- children,
- };
-});
+ const resolvedCurrent =
+ current ?? blankStackCurrent ?? adapterScene?.descriptor;
+ const resolvedPrevious =
+ previous ?? blankStackPrevious ?? adapterScene?.previousDescriptor;
+ const resolvedNext = next ?? blankStackNext ?? adapterScene?.nextDescriptor;
-export function DescriptorsProvider({
- children,
- previous,
- current,
- next,
-}: DescriptorsProviderProps) {
- const providerProps = {
- previous,
- current,
- next,
- children,
- };
+ if (!resolvedCurrent) {
+ throw new Error(
+ `Descriptors scene "${routeKey ?? "unknown"}" was not found.`,
+ );
+ }
- return ;
-}
+ const descriptors = useMemo(
+ () => ({
+ previous: resolvedPrevious,
+ current: resolvedCurrent,
+ next: resolvedNext,
+ }),
+ [resolvedPrevious, resolvedCurrent, resolvedNext],
+ );
-export function useDescriptors<
- TDescriptor extends BaseDescriptor = BaseDescriptor,
->() {
- return useDescriptorsStore(
- (store) => store.descriptors,
- ) as DescriptorsContextValue;
-}
+ const { ancestorKeys, ancestorDestinationPairKey } = useMemo(
+ () => getAncestorKeyState(resolvedCurrent),
+ [resolvedCurrent],
+ );
-export function useDescriptorDerivations(): DescriptorDerivationsContextValue {
- return useDescriptorsStore((store) => store.derivations);
-}
+ const derivations = useMemo(
+ () =>
+ deriveDescriptorDerivations({
+ previous: resolvedPrevious,
+ current: resolvedCurrent,
+ next: resolvedNext,
+ ancestorKeys,
+ ancestorDestinationPairKey,
+ }),
+ [
+ resolvedPrevious,
+ resolvedCurrent,
+ resolvedNext,
+ ancestorKeys,
+ ancestorDestinationPairKey,
+ ],
+ );
-export { useDescriptorsStore };
+ return {
+ value: {
+ previous: resolvedPrevious,
+ current: resolvedCurrent,
+ next: resolvedNext,
+ descriptors,
+ derivations,
+ options: resolvedCurrent.options,
+ },
+ children,
+ };
+ },
+);
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/descriptors/index.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/descriptors/index.tsx
index d3517b64..b6fa87f2 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/descriptors/index.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/descriptors/index.tsx
@@ -1,7 +1,5 @@
export {
type BaseDescriptor,
DescriptorsProvider,
- useDescriptorDerivations,
- useDescriptors,
useDescriptorsStore,
} from "./descriptors.provider";
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/gestures.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/gestures.provider.tsx
index 45382796..4f207aa2 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/gestures.provider.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/gestures.provider.tsx
@@ -3,7 +3,7 @@ import { Gesture } from "react-native-gesture-handler";
import { useSharedValue } from "react-native-reanimated";
import { ScrollStore } from "../../../stores/scroll.store";
import createProvider from "../../../utils/create-provider";
-import { useDescriptorDerivations } from "../descriptors";
+import { useDescriptorsStore } from "../descriptors";
import { useScreenGestureConfig } from "./hooks/use-screen-gesture-config";
import { useWalkUpAndRegisterShadowingClaims } from "./ownership/use-walk-up-and-register-shadowing-claims";
import { useBuildPanGesture } from "./pan/use-build-pan-gesture";
@@ -19,74 +19,83 @@ interface ScreenGestureProviderProps {
children: React.ReactNode;
}
-export const {
- ScreenGestureProvider,
- useScreenGestureContext: useGestureContext,
-} = createProvider("ScreenGesture", { guarded: false })<
- ScreenGestureProviderProps,
- GestureContextType
->(({ children }): { value: GestureContextType; children: React.ReactNode } => {
- const gestureContext = useGestureContext();
- const gestureConfig = useScreenGestureConfig();
- const { currentScreenKey } = useDescriptorDerivations();
+export const { ScreenGestureProvider, useScreenGestureStore: useGestureStore } =
+ createProvider("ScreenGesture", { guarded: false })<
+ ScreenGestureProviderProps,
+ GestureContextType
+ >(
+ (
+ { children },
+ { useParentStore },
+ ): { value: GestureContextType; children: React.ReactNode } => {
+ const gestureContext = useParentStore();
+ const gestureConfig = useScreenGestureConfig();
+ const currentScreenKey = useDescriptorsStore(
+ (store) => store.derivations.currentScreenKey,
+ );
- const scrollState = ScrollStore.getValue(currentScreenKey, "coordination");
+ const scrollState = ScrollStore.getValue(
+ currentScreenKey,
+ "coordination",
+ );
- // Ancestors read this before activating. If a nested screen claims the same
- // direction, it writes here so the ancestor can fail and let it take priority.
- const childDirectionClaims =
- useSharedValue(NO_DIRECTION_CLAIMS);
+ // Ancestors read this before activating. If a nested screen claims the same
+ // direction, it writes here so the ancestor can fail and let it take priority.
+ const childDirectionClaims =
+ useSharedValue(NO_DIRECTION_CLAIMS);
- // The first gesture to activate owns navigation release. Other gestures may
- // still join as companion trackers during the same simultaneous composition.
- const gestureCompositionOwner = useSharedValue(null);
+ // The first gesture to activate owns navigation release. Other gestures may
+ // still join as companion trackers during the same simultaneous composition.
+ const gestureCompositionOwner =
+ useSharedValue(null);
- const panGesture = useBuildPanGesture({
- scrollState,
- gestureConfig,
- childDirectionClaims,
- gestureCompositionOwner,
- });
+ const panGesture = useBuildPanGesture({
+ scrollState,
+ gestureConfig,
+ childDirectionClaims,
+ gestureCompositionOwner,
+ });
- const pinchGesture = useBuildPinchGesture({
- gestureConfig,
- gestureCompositionOwner,
- });
+ const pinchGesture = useBuildPinchGesture({
+ gestureConfig,
+ gestureCompositionOwner,
+ });
- const detectorGesture = useMemo(
- () => Gesture.Simultaneous(panGesture, pinchGesture),
- [panGesture, pinchGesture],
- );
+ const detectorGesture = useMemo(
+ () => Gesture.Simultaneous(panGesture, pinchGesture),
+ [panGesture, pinchGesture],
+ );
- const value = useMemo(
- () => ({
- routeKey: currentScreenKey,
- detectorGesture,
- panGesture,
- pinchGesture,
- scrollState,
- gestureContext,
- claimedDirections: gestureConfig.participation.claimedDirections,
- childDirectionClaims,
- }),
- [
- currentScreenKey,
- detectorGesture,
- panGesture,
- pinchGesture,
- scrollState,
- gestureContext,
- gestureConfig.participation.claimedDirections,
- childDirectionClaims,
- ],
- );
+ const value = useMemo(
+ () => ({
+ routeKey: currentScreenKey,
+ detectorGesture,
+ panGesture,
+ pinchGesture,
+ scrollState,
+ gestureContext,
+ claimedDirections: gestureConfig.participation.claimedDirections,
+ childDirectionClaims,
+ }),
+ [
+ currentScreenKey,
+ detectorGesture,
+ panGesture,
+ pinchGesture,
+ scrollState,
+ gestureContext,
+ gestureConfig.participation.claimedDirections,
+ childDirectionClaims,
+ ],
+ );
- useWalkUpAndRegisterShadowingClaims(
- gestureConfig.participation.claimedDirections,
- );
+ useWalkUpAndRegisterShadowingClaims(
+ gestureConfig.participation.claimedDirections,
+ );
- return {
- value,
- children,
- };
-});
+ return {
+ value,
+ children,
+ };
+ },
+ );
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-screen-gesture-config.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-screen-gesture-config.ts
index 0111d7f3..216d87db 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-screen-gesture-config.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-screen-gesture-config.ts
@@ -1,16 +1,15 @@
import { useMemo } from "react";
-import { useDescriptorDerivations, useDescriptors } from "../../descriptors";
-import { useGestureContext } from "../gestures.provider";
+import { useDescriptorsStore } from "../../descriptors";
+import { useGestureStore } from "../gestures.provider";
import { resolveScreenGestureConfig } from "../shared/policy";
import type { ScreenGestureConfig } from "../types";
export function useScreenGestureConfig(): ScreenGestureConfig {
- const gestureContext = useGestureContext();
- const {
- current: { options },
- } = useDescriptors();
-
- const { isFirstKey } = useDescriptorDerivations();
+ const gestureContext = useGestureStore();
+ const options = useDescriptorsStore((store) => store.options);
+ const isFirstKey = useDescriptorsStore(
+ (store) => store.derivations.isFirstKey,
+ );
return useMemo(
() =>
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-screen-gesture.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-screen-gesture.ts
index 535db459..ce959d4b 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-screen-gesture.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-screen-gesture.ts
@@ -2,7 +2,7 @@ import {
type ChainTarget,
resolveChainTarget,
} from "../../../../utils/resolve-chain-target";
-import { useGestureContext } from "../gestures.provider";
+import { useGestureStore } from "../gestures.provider";
import { walkGestureAncestors } from "../shared/ancestors";
export type ScreenGestureTarget = ChainTarget;
@@ -21,7 +21,7 @@ export type ScreenGestureTarget = ChainTarget;
* ```
*/
export const useScreenGesture = (target?: ScreenGestureTarget) => {
- const ctx = useGestureContext();
+ const ctx = useGestureStore();
return (
resolveChainTarget({
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-stable-runtime-config.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-stable-runtime-config.ts
index a63d7c0b..d64f6e47 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-stable-runtime-config.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/hooks/use-stable-runtime-config.ts
@@ -3,7 +3,7 @@ import { type SharedValue, useSharedValue } from "react-native-reanimated";
import { AnimationStore } from "../../../../stores/animation.store";
import { GestureStore } from "../../../../stores/gesture.store";
import { SystemStore } from "../../../../stores/system.store";
-import { useDescriptorDerivations } from "../../descriptors";
+import { useDescriptorsStore } from "../../descriptors";
import type { GesturePolicy, GestureRuntime } from "../types";
type RuntimeConfigInput = Omit<
@@ -14,7 +14,9 @@ type RuntimeConfigInput = Omit<
export function useStableRuntimeConfig(
runtimeConfigInput: RuntimeConfigInput,
): SharedValue> {
- const { currentScreenKey } = useDescriptorDerivations();
+ const currentScreenKey = useDescriptorsStore(
+ (store) => store.derivations.currentScreenKey,
+ );
const { participation, policy } = runtimeConfigInput;
const stores = useMemo(() => {
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/index.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/index.tsx
index 424466ab..7b388403 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/index.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/index.tsx
@@ -1,6 +1,6 @@
export {
ScreenGestureProvider,
- useGestureContext,
+ useGestureStore,
} from "./gestures.provider";
export type {
DirectionClaim,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/ownership/shadowing-claims.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/ownership/shadowing-claims.ts
index e9cbf2f9..c31d7a28 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/ownership/shadowing-claims.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/ownership/shadowing-claims.ts
@@ -1,16 +1,15 @@
import type { ClaimedDirections } from "../../../../types/ownership.types";
-import type { StackSceneActivity } from "../../../../types/stack.types";
export const resolveShadowingClaimDirections = ({
- currentActivity,
+ isCurrentScreenClosing,
currentClaimedDirections,
previousClaimedDirections,
}: {
- currentActivity: StackSceneActivity | undefined;
+ isCurrentScreenClosing: boolean;
currentClaimedDirections: ClaimedDirections;
previousClaimedDirections: ClaimedDirections;
}): ClaimedDirections => {
- if (currentActivity === "closing") {
+ if (isCurrentScreenClosing) {
return previousClaimedDirections;
}
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/ownership/use-walk-up-and-register-shadowing-claims.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/ownership/use-walk-up-and-register-shadowing-claims.ts
index 41cc1e75..7af2dab0 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/ownership/use-walk-up-and-register-shadowing-claims.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/ownership/use-walk-up-and-register-shadowing-claims.ts
@@ -8,10 +8,10 @@ import {
} from "../../../../types/ownership.types";
import {
type BaseDescriptor,
- useDescriptorDerivations,
- useDescriptors,
+ useDescriptorsStore,
} from "../../../screen/descriptors";
-import { useGestureContext } from "../gestures.provider";
+import { useBlankStackStore } from "../../../stack/blank-stack.provider";
+import { useGestureStore } from "../gestures.provider";
import { walkGestureAncestors } from "../shared/ancestors";
import { resolveScreenGestureConfig } from "../shared/policy";
import type { GestureContextType } from "../types";
@@ -135,9 +135,17 @@ const getDescriptorClaimedDirections = (
export function useWalkUpAndRegisterShadowingClaims(
claimedDirections: ClaimedDirections,
): void {
- const parentContext = useGestureContext();
- const { current, previous } = useDescriptors();
- const { isTopMostScreen, currentScreenKey } = useDescriptorDerivations();
+ const parentContext = useGestureStore();
+ const previous = useDescriptorsStore((store) => store.previous);
+ const isTopMostScreen = useDescriptorsStore(
+ (store) => store.derivations.isTopMostScreen,
+ );
+ const currentScreenKey = useDescriptorsStore(
+ (store) => store.derivations.currentScreenKey,
+ );
+ const isCurrentScreenClosing = useBlankStackStore(
+ (store) => store?.scenesByKey[currentScreenKey]?.activity === "closing",
+ );
/*
* We want to calculate effective claimed directions as claimedDirections is not enough for us.
@@ -161,14 +169,14 @@ export function useWalkUpAndRegisterShadowingClaims(
const effectiveClaimedDirections = useMemo(
() =>
resolveShadowingClaimDirections({
- currentActivity: current.activity,
+ isCurrentScreenClosing,
currentClaimedDirections: claimedDirections,
previousClaimedDirections: getDescriptorClaimedDirections(
previous,
parentContext,
),
}),
- [current.activity, claimedDirections, previous, parentContext],
+ [isCurrentScreenClosing, claimedDirections, previous, parentContext],
);
const shadowedAncestors = useMemo(
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/activation/use-pan-activation.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/activation/use-pan-activation.ts
index 4d2623a5..47e4c89d 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/activation/use-pan-activation.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/activation/use-pan-activation.ts
@@ -7,7 +7,7 @@ import { type SharedValue, useSharedValue } from "react-native-reanimated";
import { GestureStore } from "../../../../../stores/gesture.store";
import { GestureActivationState } from "../../../../../types/gesture.types";
import type { Direction } from "../../../../../types/ownership.types";
-import { useDescriptorDerivations } from "../../../descriptors";
+import { useDescriptorsStore } from "../../../descriptors";
import type { ScreenOptionsContextValue } from "../../../options";
import { resolvePanRuntime } from "../../shared/runtime";
import type {
@@ -36,7 +36,12 @@ export const usePanActivation = ({
dimensions,
gestureCompositionOwner,
}: UsePanActivationProps) => {
- const { currentScreenKey, parentScreenKey } = useDescriptorDerivations();
+ const currentScreenKey = useDescriptorsStore(
+ (store) => store.derivations.currentScreenKey,
+ );
+ const parentScreenKey = useDescriptorsStore(
+ (store) => store.derivations.parentScreenKey,
+ );
const ancestorDismissing = useMemo(() => {
if (!parentScreenKey) return null;
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/behavior/pan-lifecycle.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/behavior/pan-lifecycle.ts
index 85df871b..72756c2b 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/behavior/pan-lifecycle.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/behavior/pan-lifecycle.ts
@@ -1,7 +1,7 @@
import { clamp, runOnJS, type SharedValue } from "react-native-reanimated";
-import { EPSILON, FALSE, TRUE } from "../../../../../constants";
+import { EPSILON, TRUE } from "../../../../../constants";
+import { emitMotionStart } from "../../../../../stores/animation.store";
import { animateToProgress } from "../../../../../utils/animation/animate-to-progress";
-import { emit } from "../../../../../utils/animation/emit";
import {
normalizeGestureTranslation,
resolveGestureVelocity,
@@ -29,16 +29,7 @@ export const startPanBase = (runtime: PanGestureRuntime) => {
stores: { gestures, animations },
} = runtime;
- const wasSettling = gestures.settling.get();
- const hasResidualGesture =
- Math.abs(gestures.normX.get()) > EPSILON ||
- Math.abs(gestures.normY.get()) > EPSILON ||
- Math.abs(gestures.normScale.get()) > EPSILON ||
- Math.abs(gestures.rotation.get()) > EPSILON;
-
- if (!wasSettling || !hasResidualGesture) {
- emit(animations.willAnimate, TRUE, FALSE);
- }
+ emitMotionStart(animations);
gestures.dragging.set(TRUE);
gestures.dismissing.set(0);
@@ -133,6 +124,9 @@ export const finalizePanRelease = (
system.targetProgress.set(plan.commitProgress);
}
+ const progressAlreadyAtTarget =
+ Math.abs(animations.transitionProgress.get() - plan.target) <= EPSILON;
+
if (canDriveRelease && plan.shouldDismiss) {
snapshotGestureHandoff(gestures, {
velocity: plan.handoffVelocity,
@@ -142,6 +136,8 @@ export const finalizePanRelease = (
resetPanGestureValues({
plan,
gestures,
+ animations,
+ completeMotion: !plan.shouldDismiss && progressAlreadyAtTarget,
updateLifecycle: canDriveRelease,
});
@@ -149,12 +145,8 @@ export const finalizePanRelease = (
return;
}
- const progressAlreadyAtTarget =
- Math.abs(animations.transitionProgress.get() - plan.target) <= EPSILON;
-
if (!plan.shouldDismiss && progressAlreadyAtTarget) {
system.targetProgress.set(plan.target);
- animations.progressAnimating.set(FALSE);
return;
}
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/behavior/pan-reset.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/behavior/pan-reset.ts
index 15856743..67fb6d54 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/behavior/pan-reset.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/behavior/pan-reset.ts
@@ -1,4 +1,5 @@
import { FALSE, TRUE } from "../../../../../constants";
+import type { AnimationStoreMap } from "../../../../../stores/animation.store";
import type { GestureStoreMap } from "../../../../../stores/gesture.store";
import { animateMany } from "../../shared/reset";
import {
@@ -10,12 +11,16 @@ import type { PanReleasePlan } from "../../types";
interface ResetPanGestureValuesProps {
plan: PanReleasePlan;
gestures: GestureStoreMap;
+ animations?: AnimationStoreMap;
+ completeMotion?: boolean;
updateLifecycle?: boolean;
}
export const resetPanGestureValues = ({
plan,
gestures,
+ animations,
+ completeMotion = false,
updateLifecycle = true,
}: ResetPanGestureValuesProps) => {
"worklet";
@@ -28,8 +33,17 @@ export const resetPanGestureValues = ({
gestures.active.set(null);
gestures.direction.set(null);
gestures.settling.set(FALSE);
+ if (completeMotion) {
+ animations?.progressAnimating.set(FALSE);
+ animations?.progressSettled.set(TRUE);
+ }
};
+ if (animations && updateLifecycle && !plan.shouldDismiss) {
+ animations.progressAnimating.set(TRUE);
+ animations.progressSettled.set(FALSE);
+ }
+
clearRawPanValues(gestures);
if (updateLifecycle) {
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/use-build-pan-gesture.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/use-build-pan-gesture.ts
index fc2ded00..7bca0c75 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/use-build-pan-gesture.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pan/use-build-pan-gesture.ts
@@ -2,7 +2,7 @@ import { useMemo } from "react";
import { useWindowDimensions } from "react-native";
import { Gesture } from "react-native-gesture-handler";
import type { SharedValue } from "react-native-reanimated";
-import { useScreenOptionsContext } from "../../options";
+import { useScreenOptionsStore } from "../../options";
import { useStableRuntimeConfig } from "../hooks/use-stable-runtime-config";
import type {
DirectionClaimMap,
@@ -29,7 +29,7 @@ export const useBuildPanGesture = ({
}: UseBuildPanGestureProps): PanGesture => {
const dimensions = useWindowDimensions();
const { participation, pan: policy } = gestureConfig;
- const screenOptions = useScreenOptionsContext();
+ const screenOptions = useScreenOptionsStore();
const runtime = useStableRuntimeConfig({
participation,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/behavior/pinch-lifecycle.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/behavior/pinch-lifecycle.ts
index a10cb941..c11e8dcb 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/behavior/pinch-lifecycle.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/behavior/pinch-lifecycle.ts
@@ -1,7 +1,7 @@
import { clamp, runOnJS } from "react-native-reanimated";
-import { EPSILON, FALSE, TRUE } from "../../../../../constants";
+import { EPSILON, TRUE } from "../../../../../constants";
+import { emitMotionStart } from "../../../../../stores/animation.store";
import { animateToProgress } from "../../../../../utils/animation/animate-to-progress";
-import { emit } from "../../../../../utils/animation/emit";
import { normalizePinchScale } from "../../shared/physics";
import { snapshotGestureHandoff } from "../../shared/snapshot";
import { clearTransformTrackingValues } from "../../shared/values";
@@ -19,15 +19,7 @@ export const startPinchBase = (runtime: PinchGestureRuntime) => {
stores: { gestures, animations },
} = runtime;
- const wasSettling = gestures.settling.get();
- const hasResidualGesture =
- Math.abs(gestures.normScale.get()) > EPSILON ||
- Math.abs(gestures.rotation.get()) > EPSILON;
-
- if (!wasSettling || !hasResidualGesture) {
- emit(animations.willAnimate, TRUE, FALSE);
- }
-
+ emitMotionStart(animations);
gestures.dragging.set(TRUE);
gestures.dismissing.set(0);
gestures.settling.set(0);
@@ -82,6 +74,9 @@ export const finalizePinchRelease = (
system.targetProgress.set(release.commitProgress);
}
+ const progressAlreadyAtTarget =
+ Math.abs(animations.transitionProgress.get() - release.target) <= EPSILON;
+
if (release.shouldDismiss) {
snapshotGestureHandoff(gestures, {
velocity: release.handoffVelocity,
@@ -91,6 +86,7 @@ export const finalizePinchRelease = (
resetPinchGestureValues({
spec: release.resetSpec,
gestures,
+ animations,
shouldDismiss: release.shouldDismiss,
resetValuesImmediately: release.resetValuesImmediately,
});
@@ -99,6 +95,11 @@ export const finalizePinchRelease = (
runOnJS(requestDismiss)();
}
+ if (!release.shouldDismiss && progressAlreadyAtTarget) {
+ system.targetProgress.set(release.target);
+ return;
+ }
+
animateToProgress({
target: release.target,
onAnimationFinish: release.shouldDismiss ? dismissScreen : undefined,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/behavior/pinch-reset.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/behavior/pinch-reset.ts
index 1b2638e8..7f2663b4 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/behavior/pinch-reset.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/behavior/pinch-reset.ts
@@ -1,4 +1,5 @@
import { FALSE, TRUE } from "../../../../../constants";
+import type { AnimationStoreMap } from "../../../../../stores/animation.store";
import type { GestureStoreMap } from "../../../../../stores/gesture.store";
import type { AnimationConfig } from "../../../../../types/animation.types";
import { animateMany } from "../../shared/reset";
@@ -11,6 +12,7 @@ import {
interface ResetPinchGestureValuesProps {
spec?: AnimationConfig;
gestures: GestureStoreMap;
+ animations?: AnimationStoreMap;
shouldDismiss: boolean;
resetValuesImmediately?: boolean;
}
@@ -18,6 +20,7 @@ interface ResetPinchGestureValuesProps {
export const resetPinchGestureValues = ({
spec,
gestures,
+ animations,
shouldDismiss,
resetValuesImmediately = false,
}: ResetPinchGestureValuesProps) => {
@@ -32,8 +35,15 @@ export const resetPinchGestureValues = ({
gestures.active.set(null);
gestures.direction.set(null);
gestures.settling.set(FALSE);
+ animations?.progressAnimating.set(FALSE);
+ animations?.progressSettled.set(TRUE);
};
+ if (animations && !shouldDismiss) {
+ animations.progressAnimating.set(TRUE);
+ animations.progressSettled.set(FALSE);
+ }
+
clearRawTransformValues(gestures);
gestures.dragging.set(FALSE);
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/use-build-pinch-gesture.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/use-build-pinch-gesture.ts
index 073eb541..31a0343c 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/use-build-pinch-gesture.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/pinch/use-build-pinch-gesture.ts
@@ -1,7 +1,7 @@
import { useMemo } from "react";
import { Gesture } from "react-native-gesture-handler";
import type { SharedValue } from "react-native-reanimated";
-import { useScreenOptionsContext } from "../../options";
+import { useScreenOptionsStore } from "../../options";
import { useStableRuntimeConfig } from "../hooks/use-stable-runtime-config";
import type {
GestureCompositionOwner,
@@ -21,7 +21,7 @@ export const useBuildPinchGesture = ({
gestureCompositionOwner,
}: UseBuildPinchGestureProps): PinchGesture => {
const { participation, pinch: policy } = gestureConfig;
- const screenOptions = useScreenOptionsContext();
+ const screenOptions = useScreenOptionsStore();
const runtime = useStableRuntimeConfig({
participation,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/scroll-coordination/scroll-metadata-owner.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/scroll-coordination/scroll-metadata-owner.tsx
index b7959b0f..ced491f3 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/scroll-coordination/scroll-metadata-owner.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/scroll-coordination/scroll-metadata-owner.tsx
@@ -16,19 +16,19 @@ const DEFAULT_SCROLL_METADATA_OWNER_CONTEXT: ScrollMetadataOwnerContextValue = {
const {
ScrollMetadataOwnerProvider,
- useScrollMetadataOwnerContext: useMaybeScrollMetadataOwnerContext,
+ useScrollMetadataOwnerStore: useMaybeScrollMetadataOwnerStore,
} = createProvider("ScrollMetadataOwner", { guarded: false })<
ScrollMetadataOwnerProviderProps,
ScrollMetadataOwnerContextValue
>(({ children, value }) => ({ children, value }));
-export const useScrollMetadataOwnerContext = () =>
- useMaybeScrollMetadataOwnerContext() ?? DEFAULT_SCROLL_METADATA_OWNER_CONTEXT;
+export const useScrollMetadataOwnerStore = () =>
+ useMaybeScrollMetadataOwnerStore() ?? DEFAULT_SCROLL_METADATA_OWNER_CONTEXT;
export const useScrollMetadataOwnerProviderValue = (
axis: ScrollGestureAxis,
) => {
- const parent = useScrollMetadataOwnerContext();
+ const parent = useScrollMetadataOwnerStore();
return useMemo(() => {
if (parent[axis]) return parent;
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/scroll-coordination/use-scroll-gesture-coordination.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/scroll-coordination/use-scroll-gesture-coordination.ts
index ab7ce6b7..906cfe64 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/scroll-coordination/use-scroll-gesture-coordination.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/gestures/scroll-coordination/use-scroll-gesture-coordination.ts
@@ -10,7 +10,7 @@ import { useSharedValueState } from "../../../../hooks/reanimated/use-shared-val
import useStableCallback from "../../../../hooks/use-stable-callback";
import { AnimationStore } from "../../../../stores/animation.store";
import { ScrollStore } from "../../../../stores/scroll.store";
-import { useGestureContext } from "../gestures.provider";
+import { useGestureStore } from "../gestures.provider";
import type {
ScrollGestureAxis,
ScrollGestureAxisState,
@@ -18,8 +18,8 @@ import type {
ScrollMetadataState,
} from "../types";
import {
- useScrollMetadataOwnerContext,
useScrollMetadataOwnerProviderValue,
+ useScrollMetadataOwnerStore,
} from "./scroll-metadata-owner";
import {
clearScrollMetadataAxisState,
@@ -88,10 +88,10 @@ const clearScrollMetadataAxis = (
export const useScrollGestureCoordination = (
props: ScrollGestureCoordinationProps,
) => {
- const context = useGestureContext();
+ const context = useGestureStore();
const scrollDirection = props.direction ?? "vertical";
- const metadataOwnerContext = useScrollMetadataOwnerContext();
+ const metadataOwnerContext = useScrollMetadataOwnerStore();
const metadataOwnerProviderValue =
useScrollMetadataOwnerProviderValue(scrollDirection);
const isFirstMetadataWriterInTree = !metadataOwnerContext[scrollDirection];
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/options/index.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/options/index.ts
index 58d0b850..b4ca91c7 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/options/index.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/options/index.ts
@@ -1,6 +1,6 @@
export { syncScreenOptionsOverrides } from "./helpers";
export {
ScreenOptionsProvider,
- useScreenOptionsContext,
+ useScreenOptionsStore,
} from "./options.provider";
export type { ScreenOptionsContextValue, ScreenOptionsSnapshot } from "./types";
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/options/options.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/options/options.provider.tsx
index 76236071..6306fd20 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/options/options.provider.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/options/options.provider.tsx
@@ -1,7 +1,7 @@
import { useLayoutEffect, useMemo } from "react";
import { useSharedValue } from "react-native-reanimated";
import createProvider from "../../../utils/create-provider";
-import { useDescriptors } from "../descriptors";
+import { useDescriptorsStore } from "../descriptors";
import { resolveBaseScreenOptions, syncScreenOptionsBase } from "./helpers";
import type {
ScreenOptionsContextValue,
@@ -9,35 +9,31 @@ import type {
ScreenOptionsState,
} from "./types";
-export const { ScreenOptionsProvider, useScreenOptionsContext } =
- createProvider("ScreenOptions")<
- ScreenOptionsProviderProps,
- ScreenOptionsContextValue
- >(() => {
- const {
- current: { options },
- } = useDescriptors();
+export const { ScreenOptionsProvider, useScreenOptionsStore } = createProvider(
+ "ScreenOptions",
+)(() => {
+ const options = useDescriptorsStore((store) => store.options);
- const baseScreenOptions = useMemo(
- () => resolveBaseScreenOptions(options),
- [options],
- );
+ const baseScreenOptions = useMemo(
+ () => resolveBaseScreenOptions(options),
+ [options],
+ );
- const initialScreenOptions = useMemo(
- () => ({
- ...baseScreenOptions,
- baseOptions: baseScreenOptions,
- }),
- [baseScreenOptions],
- );
+ const initialScreenOptions = useMemo(
+ () => ({
+ ...baseScreenOptions,
+ baseOptions: baseScreenOptions,
+ }),
+ [baseScreenOptions],
+ );
- const value = useSharedValue(initialScreenOptions);
+ const value = useSharedValue(initialScreenOptions);
- useLayoutEffect(() => {
- syncScreenOptionsBase(value, baseScreenOptions);
- }, [value, baseScreenOptions]);
+ useLayoutEffect(() => {
+ syncScreenOptionsBase(value, baseScreenOptions);
+ }, [value, baseScreenOptions]);
- return {
- value,
- };
- });
+ return {
+ value,
+ };
+});
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/origin.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/origin.provider.tsx
deleted file mode 100644
index 677fbea8..00000000
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/origin.provider.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { StyleSheet, type View } from "react-native";
-import Animated, {
- type AnimatedRef,
- useAnimatedRef,
-} from "react-native-reanimated";
-import createProvider from "../../utils/create-provider";
-
-interface Props {
- children: React.ReactNode;
-}
-interface ContextValue {
- originRef: AnimatedRef;
-}
-
-export const { OriginProvider, useOriginContext } = createProvider("Origin", {
- guarded: true,
-})(({ children }) => {
- const originRef = useAnimatedRef();
-
- return {
- value: {
- originRef,
- },
- children: (
-
- {children}
-
- ),
- };
-});
-
-const styles = StyleSheet.create({
- container: { flex: 1 },
-});
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/screen-composer.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/screen-composer.tsx
index a6ac01fd..790cccd4 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/screen-composer.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/screen-composer.tsx
@@ -9,8 +9,9 @@ import { ScreenSlotProvider } from "./styles";
type Props = {
previous?: TDescriptor;
- current: TDescriptor;
+ current?: TDescriptor;
next?: TDescriptor;
+ routeKey?: string;
children: React.ReactNode;
};
@@ -18,10 +19,16 @@ export function ScreenComposer({
previous,
current,
next,
+ routeKey,
children,
}: Props) {
return (
-
+
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/constants.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/constants.ts
index ea0055d3..9b60b747 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/constants.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/constants.ts
@@ -64,7 +64,7 @@ export const PROP_RESET_VALUES: Record = {
pointerEvents: "auto",
};
-const LOCAL_ONLY_STYLE_SLOT_IDS = {
+const RESERVED_STYLE_SLOT_IDS = {
content: true,
backdrop: true,
surface: true,
@@ -72,11 +72,13 @@ const LOCAL_ONLY_STYLE_SLOT_IDS = {
[NAVIGATION_MASK_CONTAINER_STYLE_ID]: true,
} as const;
-export const shouldSlotInherit = (slotId: string) => {
+export const isReservedStyleSlot = (slotId: string) => {
"worklet";
// biome-ignore lint/suspicious/noPrototypeBuiltins:
- return !Object.prototype.hasOwnProperty.call(
- LOCAL_ONLY_STYLE_SLOT_IDS,
- slotId,
- );
+ return Object.prototype.hasOwnProperty.call(RESERVED_STYLE_SLOT_IDS, slotId);
+};
+
+export const shouldSlotInherit = (slotId: string) => {
+ "worklet";
+ return !isReservedStyleSlot(slotId);
};
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/resolve-interpolator-style-handoff.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/resolve-interpolator-style-handoff.ts
new file mode 100644
index 00000000..1715e1ca
--- /dev/null
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/resolve-interpolator-style-handoff.ts
@@ -0,0 +1,138 @@
+import type {
+ NormalizedTransitionInterpolatedStyle,
+ NormalizedTransitionSlotStyle,
+} from "../../../../types/animation.types";
+import type { LocalStyleLayers } from "./resolve-slot-styles";
+
+const mergeBuckets = (
+ frozen: Record | undefined,
+ live: Record | undefined,
+ composeTransforms: boolean,
+) => {
+ "worklet";
+
+ if (!frozen) {
+ return live;
+ }
+
+ if (!live) {
+ return frozen;
+ }
+
+ const merged: Record = { ...frozen };
+
+ for (const key in live) {
+ const liveValue = live[key];
+
+ if (liveValue === undefined || liveValue === null) {
+ continue;
+ }
+
+ const frozenValue = frozen[key];
+
+ if (
+ composeTransforms &&
+ key === "transform" &&
+ Array.isArray(frozenValue) &&
+ Array.isArray(liveValue)
+ ) {
+ merged[key] = [...frozenValue, ...liveValue];
+ continue;
+ }
+
+ merged[key] = liveValue;
+ }
+
+ return merged;
+};
+
+const mergeSlots = (
+ frozen: NormalizedTransitionSlotStyle | undefined,
+ live: NormalizedTransitionSlotStyle,
+): NormalizedTransitionSlotStyle => {
+ "worklet";
+
+ if (!frozen) {
+ return live;
+ }
+
+ const merged: NormalizedTransitionSlotStyle = {};
+ const style = mergeBuckets(
+ frozen.style as Record | undefined,
+ live.style as Record | undefined,
+ true,
+ );
+
+ if (style) {
+ merged.style = style;
+ }
+
+ if (frozen.props || live.props) {
+ merged.props = mergeBuckets(frozen.props, live.props, false);
+ }
+
+ const boundsLocalTransform =
+ live.boundsLocalTransform ?? frozen.boundsLocalTransform;
+
+ if (boundsLocalTransform) {
+ merged.boundsLocalTransform = boundsLocalTransform;
+ }
+
+ return merged;
+};
+
+const composeFrozenAndLiveStyles = (
+ frozen: NormalizedTransitionInterpolatedStyle,
+ live: NormalizedTransitionInterpolatedStyle,
+) => {
+ "worklet";
+ const composed: NormalizedTransitionInterpolatedStyle = { ...frozen };
+
+ for (const slotId in live) {
+ const liveSlot = live[slotId];
+
+ if (liveSlot) {
+ composed[slotId] = mergeSlots(frozen[slotId], liveSlot);
+ }
+ }
+
+ return composed;
+};
+
+export const resolveInterpolatorStyleHandoff = ({
+ currentOwnsInterpolator,
+ currentStylesMap,
+ nextStylesMap,
+ frozenCurrentStylesMap,
+}: {
+ currentOwnsInterpolator: boolean;
+ currentStylesMap: NormalizedTransitionInterpolatedStyle | undefined;
+ nextStylesMap: NormalizedTransitionInterpolatedStyle | undefined;
+ frozenCurrentStylesMap: NormalizedTransitionInterpolatedStyle;
+}): {
+ localStylesMaps: LocalStyleLayers;
+ nextFrozenCurrentStylesMap: NormalizedTransitionInterpolatedStyle;
+} => {
+ "worklet";
+
+ if (currentOwnsInterpolator) {
+ return {
+ localStylesMaps: currentStylesMap ? [currentStylesMap] : [],
+ nextFrozenCurrentStylesMap: currentStylesMap ?? frozenCurrentStylesMap,
+ };
+ }
+
+ if (!nextStylesMap) {
+ return {
+ localStylesMaps: frozenCurrentStylesMap ? [frozenCurrentStylesMap] : [],
+ nextFrozenCurrentStylesMap: frozenCurrentStylesMap,
+ };
+ }
+
+ return {
+ localStylesMaps: [
+ composeFrozenAndLiveStyles(frozenCurrentStylesMap, nextStylesMap),
+ ],
+ nextFrozenCurrentStylesMap: frozenCurrentStylesMap,
+ };
+};
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/resolve-slot-styles/index.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/resolve-slot-styles/index.ts
index cf775ef2..e67087a4 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/resolve-slot-styles/index.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/resolve-slot-styles/index.ts
@@ -2,7 +2,7 @@ import type {
NormalizedTransitionInterpolatedStyle,
NormalizedTransitionSlotStyle,
} from "../../../../../types/animation.types";
-import { shouldSlotInherit } from "../../constants";
+import { isReservedStyleSlot, shouldSlotInherit } from "../../constants";
import { materializeResolvedSlot } from "./materialize-slot";
import { getResolvedSlotState } from "./slot-state";
import type {
@@ -72,15 +72,79 @@ const hasResettableDisappearedKeys = (
return false;
};
+const mergeStateRecord = (
+ previous: Record | undefined,
+ current: Record | undefined,
+): Record | undefined => {
+ "worklet";
+
+ if (!previous) {
+ return current;
+ }
+
+ if (!current) {
+ return previous;
+ }
+
+ return {
+ ...previous,
+ ...current,
+ };
+};
+
+const mergeResettableStyleStates = (
+ previousState: ResettableStyleState | undefined,
+ currentState: ResettableStyleState | undefined,
+): ResettableStyleState | undefined => {
+ "worklet";
+
+ if (!previousState) {
+ return currentState;
+ }
+
+ if (!currentState) {
+ return previousState;
+ }
+
+ return {
+ styleKeys: mergeStateRecord(
+ previousState.styleKeys,
+ currentState.styleKeys,
+ ),
+ styleResetValues: mergeStateRecord(
+ previousState.styleResetValues,
+ currentState.styleResetValues,
+ ),
+ propKeys: mergeStateRecord(previousState.propKeys, currentState.propKeys),
+ propResetValues: mergeStateRecord(
+ previousState.propResetValues,
+ currentState.propResetValues,
+ ),
+ };
+};
+
const getResolvedSlotOutput = ({
slot,
previousState,
+ deferMissingKeyResets,
}: {
slot: NormalizedTransitionSlotStyle | undefined;
previousState: ResettableStyleState | undefined;
+ deferMissingKeyResets: boolean;
}) => {
"worklet";
const state = getResolvedSlotState(slot);
+
+ if (deferMissingKeyResets) {
+ // This used to force a reset when animationProgress reached zero during
+ // close. Zero is still drawable while ownership and native visibility
+ // settle, so it is not a safe cleanup boundary for reserved slots.
+ return {
+ resolvedSlot: getForwardedSlot(slot, state.hasAnyKeys),
+ nextState: mergeResettableStyleStates(previousState, state.nextState),
+ };
+ }
+
const hasStyleResetPatch = hasResettableDisappearedKeys(
previousState?.styleKeys,
previousState?.styleResetValues,
@@ -401,6 +465,7 @@ const appendResolvedSlot = (
const { resolvedSlot, nextState } = getResolvedSlotOutput({
slot: getSlotForId(context, slotId),
previousState: context.previousStyleStatesBySlot[slotId],
+ deferMissingKeyResets: isReservedStyleSlot(slotId),
});
writeResolvedSlotOutput({
@@ -470,9 +535,11 @@ const appendPreviousSlots = (context: ResolveSlotStylesContext) => {
};
/**
- * Resolves slot styles for the current screen pass and emits reset values for
- * transition-owned keys that existed in the previous resolved map but no longer
- * exist in the current one.
+ * Resolves slot styles for the current screen pass.
+ *
+ * Custom slots reset omitted keys immediately. Reserved screen slots retain
+ * omitted keys because lifecycle progress does not define a safe visual reset
+ * boundary.
*/
export const resolveSlotStyles = ({
localStylesMaps,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/select-interpolator-frame.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/select-interpolator-frame.ts
new file mode 100644
index 00000000..18198377
--- /dev/null
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/select-interpolator-frame.ts
@@ -0,0 +1,41 @@
+import type { ScreenInterpolatorFrame } from "../../animation/helpers/pipeline";
+
+export type SelectedInterpolatorFrame = Pick<
+ ScreenInterpolatorFrame,
+ | "progress"
+ | "transitionProgress"
+ | "next"
+ | "focused"
+ | "active"
+ | "inactive"
+ | "logicallySettled"
+>;
+
+export const selectInterpolatorFrame = (
+ frame: ScreenInterpolatorFrame,
+ useCurrentOnly: boolean,
+): SelectedInterpolatorFrame => {
+ "worklet";
+
+ if (!useCurrentOnly) {
+ return {
+ progress: frame.progress,
+ transitionProgress: frame.transitionProgress,
+ next: frame.next,
+ focused: frame.focused,
+ active: frame.active,
+ inactive: frame.inactive,
+ logicallySettled: frame.logicallySettled,
+ };
+ }
+
+ return {
+ progress: frame.current.progress,
+ transitionProgress: frame.current.transitionProgress,
+ next: undefined,
+ focused: true,
+ active: frame.current,
+ inactive: frame.previous,
+ logicallySettled: frame.current.settled,
+ };
+};
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/visibility-gate.ts b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/visibility-gate.ts
index e71ed0eb..15f2ee99 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/visibility-gate.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/helpers/visibility-gate.ts
@@ -18,6 +18,31 @@ type ScreenVisibilityGateDecision = {
shouldOpenGate: boolean;
};
+type InitialDestinationStyleGateState = {
+ shouldPrepareStyles: boolean;
+ isVisibilityBlocked: boolean;
+ stylesReady: boolean;
+};
+
+type InitialDestinationStyleGateDecision = {
+ shouldMarkStylesReady: boolean;
+ shouldWithholdStyles: boolean;
+};
+
+export const resolveInitialDestinationStyleGate = ({
+ shouldPrepareStyles,
+ isVisibilityBlocked,
+ stylesReady,
+}: InitialDestinationStyleGateState): InitialDestinationStyleGateDecision => {
+ "worklet";
+
+ return {
+ shouldMarkStylesReady:
+ shouldPrepareStyles && isVisibilityBlocked && !stylesReady,
+ shouldWithholdStyles: shouldPrepareStyles && !stylesReady,
+ };
+};
+
export const resolveScreenVisibilityGate = ({
isFloatingOverlay,
hasVisibilityGateOpened,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/hooks/use-interpolated-style-maps.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/hooks/use-interpolated-style-maps.tsx
index 826e0750..a76ade3a 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/hooks/use-interpolated-style-maps.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/hooks/use-interpolated-style-maps.tsx
@@ -1,5 +1,10 @@
import { useMemo } from "react";
-import { useDerivedValue, useSharedValue } from "react-native-reanimated";
+import {
+ type SharedValue,
+ useAnimatedReaction,
+ useDerivedValue,
+ useSharedValue,
+} from "react-native-reanimated";
import { NO_STYLES } from "../../../../constants";
import { AnimationStore } from "../../../../stores/animation.store";
import {
@@ -12,7 +17,7 @@ import type {
TransitionInterpolatedStyle,
} from "../../../../types/animation.types";
import { logger } from "../../../../utils/logger";
-import { useScreenAnimationContext } from "../../animation";
+import { useScreenAnimationStore } from "../../animation";
import { useBuildBoundsAccessor } from "../../animation/helpers/accessors/use-build-bounds-accessor";
import { useBuildTransitionAccessor } from "../../animation/helpers/accessors/use-build-transition-accessor";
import type { ScreenInterpolatorFrame } from "../../animation/helpers/pipeline";
@@ -21,16 +26,22 @@ import { syncSelectedInterpolatorOptions } from "../../animation/helpers/selecte
import { useDescriptorsStore } from "../../descriptors";
import {
syncScreenOptionsOverrides,
- useScreenOptionsContext,
+ useScreenOptionsStore,
} from "../../options";
import { collectInterpolatorSharedValues } from "../helpers/collect-interpolator-shared-values";
import { normalizeSlots } from "../helpers/normalize-slots";
+import { resolveInterpolatorStyleHandoff } from "../helpers/resolve-interpolator-style-handoff";
import type { LocalStyleLayers } from "../helpers/resolve-slot-styles";
+import {
+ type SelectedInterpolatorFrame,
+ selectInterpolatorFrame,
+} from "../helpers/select-interpolator-frame";
import { stripInterpolatorOptions } from "../helpers/strip-interpolator-options";
import {
hasCloseTransitionFinished,
isOpenTransitionBlocked,
} from "../helpers/transition-visual-state";
+import { resolveInitialDestinationStyleGate } from "../helpers/visibility-gate";
const NO_STYLE_LAYERS: LocalStyleLayers = [];
@@ -42,8 +53,7 @@ type InterpolatorResult = {
type RunInterpolatorParams = {
interpolator: ScreenStyleInterpolator | undefined;
props: ScreenInterpolatorFrame;
- progress: ScreenInterpolatorFrame["progress"];
- next: ScreenInterpolatorFrame["next"];
+ selectedFrame: SelectedInterpolatorFrame;
bounds: Parameters[0]["bounds"];
transition: Parameters[0]["transition"];
};
@@ -65,8 +75,7 @@ const normalizeRawStyleMap = (
const runInterpolator = ({
interpolator,
props,
- progress,
- next,
+ selectedFrame,
bounds,
transition,
}: RunInterpolatorParams): InterpolatorResult | undefined => {
@@ -79,8 +88,7 @@ const runInterpolator = ({
try {
const raw = interpolator({
...props,
- progress,
- next,
+ ...selectedFrame,
bounds,
transition,
});
@@ -101,17 +109,6 @@ const runInterpolator = ({
}
};
-const appendLayer = (
- layers: LocalStyleLayers,
- result: InterpolatorResult | undefined,
-) => {
- "worklet";
-
- if (result) {
- layers.push(result.stylesMap);
- }
-};
-
/**
* Builds the raw interpolated style layers for the current screen pass.
*
@@ -128,16 +125,26 @@ const appendLayer = (
* normal interpolator selection once the gesture-driven close is no longer in
* play.
*
- * The result is ordered from lowest to highest priority. Resolution happens
- * downstream, where slot ids determine whether slots inherit from ancestors and
- * where higher owner layers override lower owner layers per key.
+ * At an ownership handoff, the last current-owner styles are frozen. The next
+ * owner replaces matching scalar keys and composes its live transforms after
+ * the frozen transforms. The previous interpolator is not evaluated again until
+ * it regains ownership.
*/
-export const useInterpolatedStylesMap = () => {
+export const useInterpolatedStylesMap = ({
+ enabled,
+ visibilityBlocked,
+}: {
+ enabled: boolean;
+ visibilityBlocked: SharedValue;
+}) => {
const currentScreenKey = useDescriptorsStore(
(s) => s.derivations.currentScreenKey,
);
const nextScreenKey = useDescriptorsStore((s) => s.derivations.nextScreenKey);
- const screenOptions = useScreenOptionsContext();
+ const destinationPairKey = useDescriptorsStore(
+ (s) => s.derivations.destinationPairKey,
+ );
+ const screenOptions = useScreenOptionsStore();
const {
screenInterpolatorProps,
screenInterpolatorPropsRevision,
@@ -146,7 +153,7 @@ export const useInterpolatedStylesMap = () => {
currentInterpolator,
ancestorScreenAnimationSources,
descendantScreenAnimationSources,
- } = useScreenAnimationContext();
+ } = useScreenAnimationStore();
const boundsAccessor = useBuildBoundsAccessor();
const transition = useBuildTransitionAccessor();
const nextInterpolatorReady = useSharedValue(0);
@@ -169,6 +176,30 @@ export const useInterpolatedStylesMap = () => {
} = SystemStore.getBag(activeScreenKey);
const isGesturingDuringCloseAnimation = useSharedValue(false);
+ const initialDestinationStylesReady = useSharedValue(0);
+ const frozenCurrentStylesMap =
+ useSharedValue(NO_STYLES);
+ const shouldPrepareInitialDestinationStyles =
+ enabled && !!destinationPairKey && !nextScreenKey && !!currentInterpolator;
+
+ useAnimatedReaction(
+ () => {
+ "worklet";
+ return visibilityBlocked.get();
+ },
+ (isVisibilityBlocked) => {
+ "worklet";
+ const styleGate = resolveInitialDestinationStyleGate({
+ shouldPrepareStyles: shouldPrepareInitialDestinationStyles,
+ isVisibilityBlocked,
+ stylesReady: !!initialDestinationStylesReady.get(),
+ });
+
+ if (styleGate.shouldMarkStylesReady) {
+ initialDestinationStylesReady.set(1);
+ }
+ },
+ );
const localStylesMaps = useDerivedValue(() => {
"worklet";
@@ -180,7 +211,7 @@ export const useInterpolatedStylesMap = () => {
);
const props = screenInterpolatorProps.get();
- const { current, next, progress } = props;
+ const { current, next } = props;
const isDragging = current.gesture.dragging;
const isNextClosing = !!next?.closing;
@@ -230,23 +261,28 @@ export const useInterpolatedStylesMap = () => {
? "current"
: "next";
- let selectedProgress = progress;
- let selectedNext = next;
+ const selectedFrame = selectInterpolatorFrame(props, isInGestureMode);
+
+ const currentResult = currentOwnsInterpolator
+ ? runInterpolator({
+ interpolator: currentInterpolator,
+ props,
+ selectedFrame,
+ bounds: boundsAccessor,
+ transition,
+ })
+ : undefined;
+
+ const initialDestinationStyleGate = resolveInitialDestinationStyleGate({
+ shouldPrepareStyles: shouldPrepareInitialDestinationStyles,
+ isVisibilityBlocked: visibilityBlocked.get(),
+ stylesReady: !!initialDestinationStylesReady.get(),
+ });
- if (isInGestureMode) {
- selectedProgress = current.progress;
- selectedNext = undefined;
+ if (initialDestinationStyleGate.shouldWithholdStyles) {
+ return NO_STYLE_LAYERS;
}
- const currentResult = runInterpolator({
- interpolator: currentInterpolator,
- props,
- progress: selectedProgress,
- next: selectedNext,
- bounds: boundsAccessor,
- transition,
- });
-
if (interpolatorOptionsOwner === "current") {
syncSelectedInterpolatorOptions(
selectedInterpolatorOptions,
@@ -255,18 +291,26 @@ export const useInterpolatedStylesMap = () => {
);
syncScreenOptionsOverrides(currentResult?.rawStyleMap, screenOptions);
- if (!currentResult) {
- return NO_STYLE_LAYERS;
+ const handoff = resolveInterpolatorStyleHandoff({
+ currentOwnsInterpolator: true,
+ currentStylesMap: currentResult?.stylesMap,
+ nextStylesMap: undefined,
+ frozenCurrentStylesMap: frozenCurrentStylesMap.get(),
+ });
+
+ if (handoff.nextFrozenCurrentStylesMap !== frozenCurrentStylesMap.get()) {
+ frozenCurrentStylesMap.set(handoff.nextFrozenCurrentStylesMap);
}
- return [currentResult.stylesMap];
+ return handoff.localStylesMaps.length
+ ? handoff.localStylesMaps
+ : NO_STYLE_LAYERS;
}
const nextResult = runInterpolator({
interpolator: nextInterpolator,
props,
- progress: selectedProgress,
- next: selectedNext,
+ selectedFrame,
bounds: boundsAccessor,
transition,
});
@@ -278,16 +322,16 @@ export const useInterpolatedStylesMap = () => {
);
syncScreenOptionsOverrides(undefined, screenOptions);
- const layers: LocalStyleLayers = [];
-
- appendLayer(layers, currentResult);
- appendLayer(layers, nextResult);
-
- if (layers.length === 0) {
- return NO_STYLE_LAYERS;
- }
+ const handoff = resolveInterpolatorStyleHandoff({
+ currentOwnsInterpolator: false,
+ currentStylesMap: currentResult?.stylesMap,
+ nextStylesMap: nextResult?.stylesMap,
+ frozenCurrentStylesMap: frozenCurrentStylesMap.get(),
+ });
- return layers;
+ return handoff.localStylesMaps.length
+ ? handoff.localStylesMaps
+ : NO_STYLE_LAYERS;
});
return {
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/hooks/use-maybe-block-visibility.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/hooks/use-maybe-block-visibility.tsx
index 6be81fa9..c911c06f 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/hooks/use-maybe-block-visibility.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/hooks/use-maybe-block-visibility.tsx
@@ -8,13 +8,15 @@ import {
import { AnimationStore } from "../../../../stores/animation.store";
import { SystemStore } from "../../../../stores/system.store";
import { getVisibilityBlockOffset } from "../../../../utils/visibility-block-offset";
-import { useDescriptorDerivations } from "../../descriptors";
+import { useDescriptorsStore } from "../../descriptors";
import { hasCloseTransitionFinished } from "../helpers/transition-visual-state";
import { resolveScreenVisibilityGate } from "../helpers/visibility-gate";
export const useMaybeBlockVisibility = (isFloatingOverlay?: boolean) => {
const { height } = useWindowDimensions();
- const { currentScreenKey } = useDescriptorDerivations();
+ const currentScreenKey = useDescriptorsStore(
+ (store) => store.derivations.currentScreenKey,
+ );
const { closing, entering } = AnimationStore.getBag(currentScreenKey);
const {
animationProgress,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/index.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/index.tsx
index dd532bbe..728115fa 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/index.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/index.tsx
@@ -7,7 +7,6 @@ export {
} from "./hooks/slot-resolvers";
export {
- ScreenSlotContext,
type ScreenSlotName,
ScreenSlotProvider,
useScreenSlots,
diff --git a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/slot.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/slot.provider.tsx
index 4f0285c1..8d1b5f74 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/screen/styles/slot.provider.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/screen/styles/slot.provider.tsx
@@ -1,4 +1,4 @@
-import { type ReactNode, useContext, useLayoutEffect, useMemo } from "react";
+import { type ReactNode, useLayoutEffect, useMemo } from "react";
import { StyleSheet } from "react-native";
import Animated, { type SharedValue } from "react-native-reanimated";
import type {
@@ -37,58 +37,72 @@ export type ScreenSlotContextValue = {
visibilityBlocked: SharedValue;
};
-export const {
- ScreenSlotProvider,
- ScreenSlotContext,
- useScreenSlotContext: useScreenSlots,
-} = createProvider("ScreenSlot", {
- guarded: true,
-})(({ children, isFloatingOverlay }) => {
- const parentContext = useContext(ScreenSlotContext);
- const currentScreenKey = useDescriptorsStore(
- (s) => s.derivations.currentScreenKey,
- );
+export const { ScreenSlotProvider, useScreenSlotStore: useScreenSlots } =
+ createProvider("ScreenSlot", {
+ guarded: true,
+ })(
+ ({ children, isFloatingOverlay }, { useParentStore }) => {
+ const parentContext = useParentStore();
+ const currentScreenKey = useDescriptorsStore(
+ (s) => s.derivations.currentScreenKey,
+ );
- const { localStylesMaps, nextInterpolatorReady } = useInterpolatedStylesMap();
+ const { animatedStyle, animatedProps, shouldBlockVisibility } =
+ useMaybeBlockVisibility(isFloatingOverlay);
- const slotsMap = useResolvedStylesMap({
- localStylesMaps,
- ancestorStylesMap: parentContext?.slotsMap,
- });
- const { animatedStyle, animatedProps, shouldBlockVisibility } =
- useMaybeBlockVisibility(isFloatingOverlay);
- const value = useMemo(
- () => ({
- localStylesMaps,
- nextInterpolatorReady,
- slotsMap,
- visibilityBlocked: shouldBlockVisibility,
- }),
- [localStylesMaps, nextInterpolatorReady, shouldBlockVisibility, slotsMap],
- );
+ const { localStylesMaps, nextInterpolatorReady } =
+ useInterpolatedStylesMap({
+ enabled: !isFloatingOverlay,
+ visibilityBlocked: shouldBlockVisibility,
+ });
- useLayoutEffect(() => {
- registerScreenSlots(currentScreenKey, value);
+ const slotsMap = useResolvedStylesMap({
+ localStylesMaps,
+ ancestorStylesMap: parentContext?.slotsMap,
+ });
+ const value = useMemo(
+ () => ({
+ localStylesMaps,
+ nextInterpolatorReady,
+ slotsMap,
+ visibilityBlocked: shouldBlockVisibility,
+ }),
+ [
+ localStylesMaps,
+ nextInterpolatorReady,
+ shouldBlockVisibility,
+ slotsMap,
+ ],
+ );
- return () => {
- unregisterScreenSlots(currentScreenKey, value);
- };
- }, [currentScreenKey, value]);
+ useLayoutEffect(() => {
+ registerScreenSlots(currentScreenKey, value);
- return {
- value,
- children: (
-
-
- {children}
-
-
- ),
- };
-});
+ return () => {
+ unregisterScreenSlots(currentScreenKey, value);
+ };
+ }, [currentScreenKey, value]);
+
+ const content = useMemo(
+ () => (
+
+
+ {children}
+
+
+ ),
+ [children, animatedStyle, isFloatingOverlay, animatedProps],
+ );
+
+ return {
+ value,
+ children: content,
+ };
+ },
+ );
const styles = StyleSheet.create({
container: { flex: 1 },
diff --git a/packages/react-native-screen-transitions/src/shared/providers/stack/blank-stack-state/helpers/build-blank-stack-state.ts b/packages/react-native-screen-transitions/src/shared/providers/stack/blank-stack-state/helpers/build-blank-stack-state.ts
index 4cc7cdb9..090e2f10 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/stack/blank-stack-state/helpers/build-blank-stack-state.ts
+++ b/packages/react-native-screen-transitions/src/shared/providers/stack/blank-stack-state/helpers/build-blank-stack-state.ts
@@ -138,20 +138,6 @@ const getSceneActivityWindow = <
};
};
-const withDescriptorActivityState = (
- descriptor: StackDescriptorSource,
- activity: StackSceneActivity,
-): TDescriptor => {
- if (descriptor.activity === activity) {
- return descriptor as TDescriptor;
- }
-
- return {
- ...descriptor,
- activity,
- } as TDescriptor;
-};
-
const buildBaseScenes = <
TDescriptor extends BaseStackDescriptor,
TNavigation extends BaseStackNavigation,
@@ -207,10 +193,9 @@ const buildBaseScenes = <
childStateUnchanged &&
previousDescriptor.route === sourceDescriptor.route &&
previousDescriptor.navigation === sourceDescriptor.navigation &&
- previousDescriptor.options === sourceDescriptor.options &&
- previousDescriptor.activity === activity
+ previousDescriptor.options === sourceDescriptor.options
? previousDescriptor
- : withDescriptorActivityState(sourceDescriptor, activity);
+ : (sourceDescriptor as TDescriptor);
routeKeys.push(route.key);
routeChildStates[route.key] = routeChildState;
@@ -222,6 +207,7 @@ const buildBaseScenes = <
}
scenes.push({
+ activity,
route: descriptor.route,
descriptor,
});
@@ -289,6 +275,7 @@ const withSceneRelationships = ({
if (
previousScene &&
+ previousScene.activity === nextScene.activity &&
previousScene.route === nextScene.route &&
previousScene.descriptor === nextScene.descriptor &&
previousScene.previousDescriptor === nextScene.previousDescriptor &&
diff --git a/packages/react-native-screen-transitions/src/shared/providers/stack/blank-stack.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/stack/blank-stack.provider.tsx
index 65c9fa1f..df726839 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/stack/blank-stack.provider.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/stack/blank-stack.provider.tsx
@@ -1,51 +1,70 @@
import type { Route } from "@react-navigation/native";
-import * as React from "react";
-import { useMemo } from "react";
+import { type ReactNode, useMemo } from "react";
import {
type StackContextValue,
StackProvider,
} from "../../hooks/navigation/use-stack";
import type {
- BlankStackProviderContextValue,
BlankStackProviderProps,
- BlankStackProviderRenderProps,
- BlankStackProviderResult,
+ BlankStackStoreValue,
} from "../../types/providers/blank-stack-provider.types";
import type {
BaseStackDescriptor,
BaseStackNavigation,
BaseStackScene,
} from "../../types/stack.types";
+import createProvider from "../../utils/create-provider";
import { useBlankStackState } from "./blank-stack-state";
-import { useStackCoreContext } from "./core.provider";
+import { useStackCoreStore } from "./core.provider";
-const BlankStackProviderContext =
- React.createContext(null);
-BlankStackProviderContext.displayName = "BlankStackProvider";
+type InternalBlankStackProviderProps = BlankStackProviderProps<
+ BaseStackDescriptor,
+ BaseStackNavigation
+> & {
+ children: ReactNode;
+};
+
+const createScenesByKey = (
+ scenes: BaseStackScene[],
+) => {
+ const scenesByKey: Record> = {};
-function useBlankStackContext(): BlankStackProviderContextValue {
- const context = React.useContext(BlankStackProviderContext);
- if (!context) {
- throw new Error(
- "useBlankStackContext must be used within BlankStackProvider",
- );
+ for (const scene of scenes) {
+ scenesByKey[scene.route.key] = scene;
}
- return context;
-}
-function useBlankStackProviderValue<
- TDescriptor extends BaseStackDescriptor,
- TNavigation extends BaseStackNavigation,
->(
- props: BlankStackProviderProps,
-): BlankStackProviderResult {
- const { flags } = useStackCoreContext();
- const { state, handleCloseRoute, requestDismiss } = useBlankStackState(props);
- const navigatorKey = props.state.key;
+ return scenesByKey;
+};
- const focusedIndex = props.state.index;
+type BlankStackStoreProviderProps = {
+ children: ReactNode;
+ value: BlankStackStoreValue;
+};
- const stackContextValue = useMemo(
+const { BlankStackProvider: BlankStackStoreProvider, useBlankStackStore } =
+ createProvider("BlankStack", { guarded: false })<
+ BlankStackStoreProviderProps,
+ BlankStackStoreValue
+ >(({ children, value }) => ({ children, value }));
+
+function BlankStackProvider({
+ state: stackState,
+ children,
+ ...props
+}: InternalBlankStackProviderProps) {
+ const flags = useStackCoreStore((store) => store.flags);
+ const { state, handleCloseRoute, requestDismiss } = useBlankStackState({
+ ...props,
+ state: stackState,
+ });
+ const navigatorKey = stackState.key;
+ const focusedIndex = stackState.index;
+ const scenesByKey = useMemo(
+ () => createScenesByKey(state.scenes),
+ [state.scenes],
+ );
+
+ const stackValue = useMemo(
() => ({
flags,
navigatorKey,
@@ -65,54 +84,26 @@ function useBlankStackProviderValue<
requestDismiss,
],
);
+ const blankStackValue = {
+ navigatorKey,
+ routeKeys: state.routeKeys,
+ routes: state.routes,
+ scenes: state.scenes,
+ scenesByKey,
+ focusedIndex,
+ requestDismiss,
+ shouldShowFloatOverlay: state.shouldShowFloatOverlay,
+ handleCloseRoute,
+ };
- const blankStackProviderContextValue =
- useMemo(
- () => ({
- handleCloseRoute,
- }),
- [handleCloseRoute],
- );
-
- const renderProps = useMemo>(
- () => ({
- scenes: state.scenes,
- focusedIndex,
- shouldShowFloatOverlay: state.shouldShowFloatOverlay,
- }),
- [state.scenes, focusedIndex, state.shouldShowFloatOverlay],
+ return (
+
+
+ {children}
+
+
);
-
- return { stackContextValue, blankStackProviderContextValue, renderProps };
-}
-
-function withBlankStack<
- TDescriptor extends BaseStackDescriptor = BaseStackDescriptor,
- TNavigation extends BaseStackNavigation = BaseStackNavigation,
->(
- Component: React.ComponentType>,
-): React.FC> {
- return function BlankStackProvider(
- props: BlankStackProviderProps,
- ) {
- const { stackContextValue, blankStackProviderContextValue, renderProps } =
- useBlankStackProviderValue(props);
-
- return (
-
-
-
-
-
- );
- };
}
-export type {
- BlankStackProviderContextValue,
- BlankStackProviderProps,
- BlankStackProviderRenderProps,
-};
-export { useBlankStackContext, withBlankStack };
+export type { BlankStackProviderProps, BlankStackStoreValue };
+export { BlankStackProvider, useBlankStackStore };
diff --git a/packages/react-native-screen-transitions/src/shared/providers/stack/core.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/stack/core.provider.tsx
index 52081ad6..9c8e01fb 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/stack/core.provider.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/stack/core.provider.tsx
@@ -1,6 +1,5 @@
import { SafeAreaProviderCompat } from "@react-navigation/elements";
-import type * as React from "react";
-import { useMemo } from "react";
+import { memo, type ReactNode, useMemo } from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StackType } from "../../types/stack.types";
@@ -15,7 +14,7 @@ export interface StackCoreConfig {
interface StackCoreProviderProps {
config: StackCoreConfig;
- children: React.ReactNode;
+ children: ReactNode;
}
export interface StackCoreContextValue {
@@ -27,84 +26,55 @@ export interface StackCoreContextValue {
};
}
-const { StackCoreProvider: InternalStackCoreProvider, useStackCoreContext } =
- createProvider("StackCore", { guarded: true })<
- StackCoreProviderProps,
- StackCoreContextValue
- >(({ config, children }) => {
- const {
- TRANSITIONS_ALWAYS_ON = false,
- DISABLE_NATIVE_SCREENS = false,
- DISABLE_NATIVE_SCREEN_CONTAINER = false,
- STACK_TYPE = StackType.BLANK,
- } = config;
+const StackCoreRoot = memo(function StackCoreRoot({
+ children,
+ stackType,
+}: {
+ children: ReactNode;
+ stackType: StackType;
+}) {
+ return (
+
+ {children}
+
+ );
+});
- const flags = useMemo(
- () => ({
- TRANSITIONS_ALWAYS_ON,
- STACK_TYPE,
- DISABLE_NATIVE_SCREENS,
- DISABLE_NATIVE_SCREEN_CONTAINER,
- }),
- [
- TRANSITIONS_ALWAYS_ON,
- STACK_TYPE,
- DISABLE_NATIVE_SCREENS,
- DISABLE_NATIVE_SCREEN_CONTAINER,
- ],
- );
+export const { StackCoreProvider, useStackCoreStore } = createProvider(
+ "StackCore",
+ { guarded: true },
+)(({ config, children }) => {
+ const {
+ TRANSITIONS_ALWAYS_ON = false,
+ DISABLE_NATIVE_SCREENS = false,
+ DISABLE_NATIVE_SCREEN_CONTAINER = false,
+ STACK_TYPE = StackType.BLANK,
+ } = config;
- return {
- value: { flags },
- children: (
-
- {children}
-
- ),
- };
- });
+ const flags = useMemo(
+ () => ({
+ TRANSITIONS_ALWAYS_ON,
+ STACK_TYPE,
+ DISABLE_NATIVE_SCREENS,
+ DISABLE_NATIVE_SCREEN_CONTAINER,
+ }),
+ [
+ TRANSITIONS_ALWAYS_ON,
+ STACK_TYPE,
+ DISABLE_NATIVE_SCREENS,
+ DISABLE_NATIVE_SCREEN_CONTAINER,
+ ],
+ );
-/**
- * HOC that wraps a component with the StackCore provider.
- * Just a simple open gate
- */
-export function withStackCore(
- defaultConfig: StackCoreConfig,
- Component: React.ComponentType,
-): React.FC {
- return function StackCoreWrapper({
- DISABLE_NATIVE_SCREENS,
- DISABLE_NATIVE_SCREEN_CONTAINER,
- TRANSITIONS_ALWAYS_ON,
- STACK_TYPE,
- ...props
- }: TProps & StackCoreConfig) {
- // Start from defaults, then apply explicit overrides from the caller.
- const config: StackCoreConfig = {
- TRANSITIONS_ALWAYS_ON:
- TRANSITIONS_ALWAYS_ON ?? defaultConfig.TRANSITIONS_ALWAYS_ON,
- STACK_TYPE: STACK_TYPE ?? defaultConfig.STACK_TYPE,
- DISABLE_NATIVE_SCREENS:
- DISABLE_NATIVE_SCREENS ?? defaultConfig.DISABLE_NATIVE_SCREENS,
- DISABLE_NATIVE_SCREEN_CONTAINER:
- DISABLE_NATIVE_SCREEN_CONTAINER ??
- defaultConfig.DISABLE_NATIVE_SCREEN_CONTAINER,
- };
- return (
-
-
-
- );
+ return {
+ value: { flags },
+ children: {children},
};
-}
+});
const styles = StyleSheet.create({
container: { flex: 1 },
});
-
-export { useStackCoreContext };
diff --git a/packages/react-native-screen-transitions/src/shared/providers/stack/direct.provider.tsx b/packages/react-native-screen-transitions/src/shared/providers/stack/direct.provider.tsx
index 5c77b5c3..f92c4a28 100644
--- a/packages/react-native-screen-transitions/src/shared/providers/stack/direct.provider.tsx
+++ b/packages/react-native-screen-transitions/src/shared/providers/stack/direct.provider.tsx
@@ -1,5 +1,4 @@
-import type * as React from "react";
-import { useMemo } from "react";
+import { type ReactNode, useMemo } from "react";
import type { NativeStackDescriptorMap } from "../../../native-stack/types";
import {
type StackContextValue,
@@ -10,16 +9,34 @@ import type {
DirectStackProps,
DirectStackScene,
} from "../../types/providers/direct-stack.types";
+import createProvider from "../../utils/create-provider";
import { isOverlayVisible } from "../../utils/overlay/visibility";
-import { useStackCoreContext } from "./core.provider";
+import { useStackCoreStore } from "./core.provider";
-function useDirectStackValue(
- props: DirectStackProps,
-): DirectStackContextValue & { stackContextValue: StackContextValue } {
- const { state, navigation, descriptors, describe } = props;
- const { flags } = useStackCoreContext();
- const navigatorKey = state.key;
+type DirectStackProviderProps = DirectStackProps & {
+ children: ReactNode;
+};
+
+type DirectStackStoreProviderProps = {
+ children: ReactNode;
+ value: DirectStackContextValue;
+};
+const { DirectStackProvider: DirectStackStoreProvider, useDirectStackStore } =
+ createProvider("DirectStack", { guarded: true })<
+ DirectStackStoreProviderProps,
+ DirectStackContextValue
+ >(({ children, value }) => ({ children, value }));
+
+function DirectStackProvider({
+ state,
+ navigation,
+ descriptors,
+ describe,
+ children,
+}: DirectStackProviderProps) {
+ const flags = useStackCoreStore((store) => store.flags);
+ const navigatorKey = state.key;
const preloadedDescriptors = useMemo(() => {
return state.preloadedRoutes.reduce(
(acc, route) => {
@@ -41,23 +58,41 @@ function useDirectStackValue(
};
let shouldShowFloatOverlay = false;
- for (const route of allRoutes) {
+ for (let index = 0; index < allRoutes.length; index++) {
+ const route = allRoutes[index];
+ if (!route) {
+ throw new Error(`Missing native stack route at index ${index}.`);
+ }
const descriptor = allDescriptors[route.key];
const isPreloaded =
preloadedDescriptors[route.key] !== undefined &&
descriptors[route.key] === undefined;
- scenes.push({ route, descriptor, isPreloaded });
+ if (!descriptor) {
+ throw new Error(
+ `Missing native descriptor for route "${route.key}".`,
+ );
+ }
+
+ scenes.push({
+ activity:
+ index === state.index
+ ? "active"
+ : index === state.index - 1
+ ? "inert"
+ : "inactive",
+ route,
+ descriptor,
+ isPreloaded,
+ });
routeKeys.push(route.key);
- if (!shouldShowFloatOverlay && descriptor) {
- const options = descriptor.options;
- if (
- options?.enableTransitions === true &&
- isOverlayVisible(options)
- ) {
- shouldShowFloatOverlay = true;
- }
+ if (
+ !shouldShowFloatOverlay &&
+ descriptor.options?.enableTransitions === true &&
+ isOverlayVisible(descriptor.options)
+ ) {
+ shouldShowFloatOverlay = true;
}
}
@@ -70,13 +105,13 @@ function useDirectStackValue(
}, [
state.routes,
state.preloadedRoutes,
+ state.index,
preloadedDescriptors,
descriptors,
]);
const focusedIndex = state.index;
-
- const stackContextValue = useMemo(
+ const stackValue = useMemo(
() => ({
flags,
navigatorKey,
@@ -87,43 +122,28 @@ function useDirectStackValue(
}),
[flags, navigatorKey, routeKeys, allRoutes, scenes, focusedIndex],
);
+ const directStackValue = {
+ state,
+ navigation,
+ descriptors,
+ scenes,
+ focusedIndex,
+ shouldShowFloatOverlay,
+ };
- // DirectStack context value
- const lifecycleValue = useMemo(
- () => ({
- state,
- navigation,
- descriptors,
- scenes,
- focusedIndex,
- shouldShowFloatOverlay,
- }),
- [
- state,
- navigation,
- descriptors,
- scenes,
- focusedIndex,
- shouldShowFloatOverlay,
- ],
+ return (
+
+
+ {children}
+
+
);
-
- return { ...lifecycleValue, stackContextValue };
}
-function withDirectStack(
- Component: React.ComponentType,
-): React.FC {
- return function DirectStackProvider(props: TProps) {
- const { stackContextValue, ...lifecycleValue } = useDirectStackValue(props);
-
- return (
-
-
-
- );
- };
-}
-
-export type { DirectStackContextValue, DirectStackProps, DirectStackScene };
-export { withDirectStack };
+export type {
+ DirectStackContextValue,
+ DirectStackProps,
+ DirectStackProviderProps,
+ DirectStackScene,
+};
+export { DirectStackProvider, useDirectStackStore };
diff --git a/packages/react-native-screen-transitions/src/shared/stores/animation.store.ts b/packages/react-native-screen-transitions/src/shared/stores/animation.store.ts
index b207ba79..2dc17ddb 100644
--- a/packages/react-native-screen-transitions/src/shared/stores/animation.store.ts
+++ b/packages/react-native-screen-transitions/src/shared/stores/animation.store.ts
@@ -3,6 +3,8 @@ import {
makeMutable,
type SharedValue,
} from "react-native-reanimated";
+import { FALSE, TRUE } from "../constants";
+import { emit } from "../utils/animation/emit";
import { createStore } from "../utils/create-store";
export type AnimationStoreMap = {
@@ -16,6 +18,23 @@ export type AnimationStoreMap = {
entering: SharedValue;
};
+export const emitMotionStart = (animations: AnimationStoreMap) => {
+ "worklet";
+
+ animations.progressSettled.set(FALSE);
+
+ if (animations.progressAnimating.get()) {
+ return false;
+ }
+
+ emit(animations.willAnimate, TRUE, FALSE);
+ requestAnimationFrame(() => {
+ "worklet";
+ animations.progressAnimating.set(TRUE);
+ });
+ return true;
+};
+
function createAnimationBag(): AnimationStoreMap {
return {
transitionProgress: makeMutable(0),
diff --git a/packages/react-native-screen-transitions/src/shared/types/gesture.types.ts b/packages/react-native-screen-transitions/src/shared/types/gesture.types.ts
index 95b5e445..b606e836 100644
--- a/packages/react-native-screen-transitions/src/shared/types/gesture.types.ts
+++ b/packages/react-native-screen-transitions/src/shared/types/gesture.types.ts
@@ -222,8 +222,14 @@ export type GestureValues = {
raw: RawGestureValues;
/**
* The gesture that is currently active.
+ *
+ * @deprecated Use `initiator` for the accepted activation identity.
*/
active: ActiveGesture | null;
+ /**
+ * The accepted activation identity for the current gesture attempt.
+ */
+ initiator: ActiveGesture | "none";
/**
* The initial pan direction that activated the gesture.
*
@@ -240,6 +246,9 @@ export type GestureValues = {
handoff: GestureHandoffValues;
/**
* A flag indicating if the screen is in the process of dismissing (0 or 1).
+ *
+ * @deprecated Use `closing` for public screen lifecycle state. Release handoff
+ * details do not have a public replacement.
*/
dismissing: number;
/**
@@ -248,6 +257,8 @@ export type GestureValues = {
dragging: number;
/**
* A flag indicating if released gesture values are animating back to neutral.
+ *
+ * @deprecated Use `animating` or `settled` for public motion state.
*/
settling: number;
/** @deprecated Use `normX` instead. */
diff --git a/packages/react-native-screen-transitions/src/shared/types/providers/blank-stack-provider.types.ts b/packages/react-native-screen-transitions/src/shared/types/providers/blank-stack-provider.types.ts
index 394ddb94..7f250e2a 100644
--- a/packages/react-native-screen-transitions/src/shared/types/providers/blank-stack-provider.types.ts
+++ b/packages/react-native-screen-transitions/src/shared/types/providers/blank-stack-provider.types.ts
@@ -1,4 +1,3 @@
-import type { StackContextValue } from "../../hooks/navigation/use-stack";
import type {
BaseStackDescriptor,
BaseStackNavigation,
@@ -10,7 +9,7 @@ import type {
/**
* Props for blank stack - generic over descriptor and navigation types.
- * Defaults to base types for backward compatibility.
+ * Defaults to the shared base types.
*/
export interface BlankStackProviderProps<
TDescriptor extends BaseStackDescriptor = BaseStackDescriptor,
@@ -29,29 +28,16 @@ export interface BlankStackProviderProps<
* Context value for blank stack — only fields unique to blank stack lifecycle.
* Shared fields (routes, scenes, etc.) live in StackContext.
*/
-export interface BlankStackProviderContextValue {
- handleCloseRoute: (payload: { route: BaseStackRoute }) => void;
-}
-
-/**
- * Props passed to the render child of `withBlankStack`.
- * Only the fields that stack-view components actually consume.
- */
-export interface BlankStackProviderRenderProps<
+export interface BlankStackStoreValue<
TDescriptor extends BaseStackDescriptor = BaseStackDescriptor,
> {
+ navigatorKey: string;
+ routeKeys: string[];
+ routes: TDescriptor["route"][];
scenes: BaseStackScene[];
+ scenesByKey: Record>;
focusedIndex: number;
+ requestDismiss: (payload: { route: BaseStackRoute }) => boolean;
shouldShowFloatOverlay: boolean;
-}
-
-/**
- * Internal result shape returned by useBlankStackProviderValue.
- */
-export interface BlankStackProviderResult<
- TDescriptor extends BaseStackDescriptor = BaseStackDescriptor,
-> {
- stackContextValue: StackContextValue;
- blankStackProviderContextValue: BlankStackProviderContextValue;
- renderProps: BlankStackProviderRenderProps;
+ handleCloseRoute: (payload: { route: BaseStackRoute }) => void;
}
diff --git a/packages/react-native-screen-transitions/src/shared/types/providers/direct-stack.types.ts b/packages/react-native-screen-transitions/src/shared/types/providers/direct-stack.types.ts
index 5af26207..3740866c 100644
--- a/packages/react-native-screen-transitions/src/shared/types/providers/direct-stack.types.ts
+++ b/packages/react-native-screen-transitions/src/shared/types/providers/direct-stack.types.ts
@@ -8,8 +8,10 @@ import type {
NativeStackDescriptorMap,
NativeStackNavigationHelpers,
} from "../../../native-stack/types";
+import type { StackSceneActivity } from "../stack.types";
export interface DirectStackScene {
+ activity: StackSceneActivity;
route: StackNavigationState["routes"][number];
descriptor: NativeStackDescriptor;
isPreloaded: boolean;
diff --git a/packages/react-native-screen-transitions/src/shared/types/screen.types.ts b/packages/react-native-screen-transitions/src/shared/types/screen.types.ts
index 1b88f0cd..0da609b8 100644
--- a/packages/react-native-screen-transitions/src/shared/types/screen.types.ts
+++ b/packages/react-native-screen-transitions/src/shared/types/screen.types.ts
@@ -169,38 +169,6 @@ export type TransitionAwareProps = AnimatedProps & {
* }
*/
styleId?: string;
-
- /**
- * Marks this component for measurement to enable shared element transitions.
- * Components with the same tag across different screens will animate between each other.
- *
- * @example
- * // Screen A:
- *
- *
- *
- *
- * // Screen B:
- *
- *
- *
- *
- * @deprecated Use `Transition.Boundary` with `id` instead.
- */
- sharedBoundTag?: string;
-
- /**
- * Re-measures this component when the screen regains focus and updates
- * any matching shared-bound source link in place.
- *
- * Useful when layout can change while unfocused (for example, programmatic
- * ScrollView/FlatList scrolling triggered from another screen).
- *
- * @deprecated Legacy `sharedBoundTag` refresh option. The boundary
- * measurement pipeline refreshes layout through `Transition.Boundary`.
- * @default false
- */
- remeasureOnFocus?: boolean;
};
export type ScreenTransitionConfig = {
diff --git a/packages/react-native-screen-transitions/src/shared/types/stack.types.ts b/packages/react-native-screen-transitions/src/shared/types/stack.types.ts
index ac194855..b3475581 100644
--- a/packages/react-native-screen-transitions/src/shared/types/stack.types.ts
+++ b/packages/react-native-screen-transitions/src/shared/types/stack.types.ts
@@ -47,15 +47,12 @@ export interface BaseStackDescriptor<
route: TRoute;
navigation: TNavigation;
options: TOptions;
- activity?: StackSceneActivity;
render?: () => React.JSX.Element | null;
}
export type StackDescriptorSource<
TDescriptor extends BaseStackDescriptor = BaseStackDescriptor,
-> = Omit & {
- activity?: StackSceneActivity;
-};
+> = TDescriptor;
/**
* Base scene interface - route + descriptor pair.
@@ -64,6 +61,7 @@ export type StackDescriptorSource<
export interface BaseStackScene<
TDescriptor extends BaseStackDescriptor = BaseStackDescriptor,
> {
+ activity: StackSceneActivity;
route: TDescriptor["route"];
descriptor: TDescriptor;
previousDescriptor?: TDescriptor;
diff --git a/packages/react-native-screen-transitions/src/shared/utils/animation/animate-to-progress.ts b/packages/react-native-screen-transitions/src/shared/utils/animation/animate-to-progress.ts
index 3235623e..64401491 100644
--- a/packages/react-native-screen-transitions/src/shared/utils/animation/animate-to-progress.ts
+++ b/packages/react-native-screen-transitions/src/shared/utils/animation/animate-to-progress.ts
@@ -1,6 +1,9 @@
import { runOnJS, type SharedValue } from "react-native-reanimated";
import { FALSE, TRUE } from "../../constants";
-import type { AnimationStoreMap } from "../../stores/animation.store";
+import {
+ type AnimationStoreMap,
+ emitMotionStart,
+} from "../../stores/animation.store";
import type { TransitionSpec } from "../../types/animation.types";
import { animate, isSpringAnimationConfig } from "./animate";
@@ -21,6 +24,7 @@ interface AnimateToProgressProps {
markEntering?: boolean;
/** Optional initial velocity for spring-based progress (units: progress/sec). */
initialVelocity?: number;
+ isDragging?: SharedValue;
}
const setTransitionLifecycleFlags = (
@@ -64,6 +68,7 @@ export const animateToProgress = ({
emitWillAnimate = true,
markEntering = true,
initialVelocity,
+ isDragging,
}: AnimateToProgressProps) => {
"worklet";
@@ -82,15 +87,10 @@ export const animateToProgress = ({
? { ...config, velocity: initialVelocity }
: config;
- const {
- transitionProgress,
- willAnimate,
- progressAnimating,
- progressSettled,
- entering,
- } = animations;
+ const { transitionProgress, progressAnimating, progressSettled, entering } =
+ animations;
- const startAnimation = () => {
+ const startAnimation = (activateMotion: boolean) => {
"worklet";
const { from: animationProgressFrom, to: animationProgressTo } =
resolveAnimationProgressRange(transitionProgress.get(), value);
@@ -118,7 +118,9 @@ export const animateToProgress = ({
return;
}
- progressAnimating.set(TRUE); //<-- Do not move this into the callback
+ if (activateMotion) {
+ progressAnimating.set(TRUE);
+ }
progressSettled.set(FALSE);
transitionProgress.set(
animate(
@@ -126,11 +128,10 @@ export const animateToProgress = ({
effectiveConfig,
(state) => {
"worklet";
- if (state.settled) {
- progressSettled.set(TRUE);
- }
-
if (!state.finished) return;
+ if (isDragging?.get()) return;
+
+ progressSettled.set(TRUE);
animationProgress.set(animationProgressTo);
@@ -175,14 +176,13 @@ export const animateToProgress = ({
if (emitWillAnimate) {
setTransitionLifecycleFlags(animations, isClosing, markEntering);
- willAnimate.set(TRUE);
+ emitMotionStart(animations);
requestAnimationFrame(() => {
"worklet";
- willAnimate.set(FALSE);
- startAnimation();
+ startAnimation(false);
});
return;
}
- startAnimation();
+ startAnimation(true);
};
diff --git a/packages/react-native-screen-transitions/src/shared/utils/bounds/helpers/create-bounds-accessor-core.ts b/packages/react-native-screen-transitions/src/shared/utils/bounds/helpers/create-bounds-accessor-core.ts
index a41c1710..3790fcfa 100644
--- a/packages/react-native-screen-transitions/src/shared/utils/bounds/helpers/create-bounds-accessor-core.ts
+++ b/packages/react-native-screen-transitions/src/shared/utils/bounds/helpers/create-bounds-accessor-core.ts
@@ -1,3 +1,4 @@
+import { NO_STYLES } from "../../../constants";
import type {
BoundsAccessor,
BoundsInterpolationProps,
@@ -64,6 +65,12 @@ const createBoundsAccessorParts = ({
const scoped: BoundsScopedAccessor = {
styles: (options?: BoundsComputeOptions): BoundsStyleResult => {
"worklet";
+ // Keep the component at its base layout for pre-animation refresh
+ // measurement, then remove generated styles again after settlement.
+ if (!props.active.animating) {
+ return NO_STYLES;
+ }
+
return prepareBoundStyles({
props,
options: {
diff --git a/packages/react-native-screen-transitions/src/shared/utils/create-provider.tsx b/packages/react-native-screen-transitions/src/shared/utils/create-provider.tsx
index 5cdee20b..c01b68cb 100644
--- a/packages/react-native-screen-transitions/src/shared/utils/create-provider.tsx
+++ b/packages/react-native-screen-transitions/src/shared/utils/create-provider.tsx
@@ -2,6 +2,18 @@
* THANK YOU @MatiPl01
* https://github.com/MatiPl01/react-native-sortables/blob/main/packages/react-native-sortables/src/providers/utils/createProvider.tsx
* SUPER COOL AMAZING UTILITY
+ *
+ * Store-only provider: values propagate exclusively through a subscription
+ * store read with `use${Name}Store(selector)`. There is intentionally no raw
+ * context channel, so consumers subscribe only to the values they render.
+ *
+ * Factories do not memoize what they return:
+ * - `value`: the store shallow-compares snapshots and keeps the previous
+ * object when the contents are unchanged. Derived object and array fields
+ * must still have stable identities.
+ * - `children`: passed-through children keep their identity across
+ * provider-local renders. Factories that wrap children should use a
+ * module-level memoized component for the wrapper.
*/
import {
createContext,
@@ -9,13 +21,10 @@ import {
useCallback,
useContext,
useLayoutEffect,
- useMemo,
useRef,
useSyncExternalStore,
} from "react";
-type InnerProviderComponent = React.FC<{ children: ReactNode }>;
-
type ProviderSnapshot<
ContextValue,
Guarded extends boolean,
@@ -32,9 +41,8 @@ type ProviderStoreHook = {
): Selected;
};
-type ProviderOptionalStoreHook = {
- (): ContextValue | null;
- (selector: (value: ContextValue | null) => Selected): Selected;
+export type ProviderFactoryInternals = {
+ useParentStore: ProviderStoreHook;
};
export interface ProviderStoreApi {
@@ -53,6 +61,42 @@ const NullProviderStore: ProviderStoreApi = {
subscribe: () => () => {},
};
+const shallowEqual = (a: unknown, b: unknown): boolean => {
+ if (Object.is(a, b)) {
+ return true;
+ }
+
+ if (
+ typeof a !== "object" ||
+ a === null ||
+ typeof b !== "object" ||
+ b === null
+ ) {
+ return false;
+ }
+
+ const aKeys = Object.keys(a);
+ const bKeys = Object.keys(b);
+
+ if (aKeys.length !== bKeys.length) {
+ return false;
+ }
+
+ for (const key of aKeys) {
+ if (
+ !(key in b) ||
+ !Object.is(
+ (a as Record)[key],
+ (b as Record)[key],
+ )
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
const createProviderStore = (
initialSnapshot: ContextValue | null,
): MutableProviderStoreApi => {
@@ -67,7 +111,7 @@ const createProviderStore = (
}
},
setSnapshot: (nextSnapshot) => {
- if (Object.is(snapshot, nextSnapshot)) {
+ if (shallowEqual(snapshot, nextSnapshot)) {
return false;
}
@@ -88,36 +132,73 @@ export default function createProvider<
Guarded extends boolean = true,
>(name: ProviderName, options?: { guarded?: Guarded }) {
return (
- factory: (props: ProviderProps) => {
+ factory: (
+ props: ProviderProps,
+ internals: ProviderFactoryInternals,
+ ) => {
value?: ContextValue;
enabled?: boolean;
- children?:
- | ReactNode
- | ((
- innerProvider: {
- [K in ProviderName as `${K}Provider`]: InnerProviderComponent;
- },
- ) => ReactNode);
+ children?: ReactNode;
},
) => {
const { guarded = true } = options ?? {};
const providerDisplayName = `${name}Provider`;
- const innerProviderDisplayName = `${name}InnerProvider`;
-
- const Context = createContext(null);
- Context.displayName = name;
const StoreContext = createContext | null>(
null,
);
StoreContext.displayName = `${name}Store`;
+ const createStoreHook = (strict: boolean) => {
+ return (
+ selector?: (value: ContextValue | null) => Selected,
+ ): Selected | ContextValue | null => {
+ const store = useContext(StoreContext);
+ const resolvedStore =
+ store ?? (NullProviderStore as ProviderStoreApi);
+ const selectorRef = useRef(selector);
+ selectorRef.current = selector;
+
+ const getSelectedSnapshot = useCallback(() => {
+ if (strict && store === null) {
+ throw new Error(
+ `${name} store must be used within a ${name}Provider`,
+ );
+ }
+
+ const snapshot = resolvedStore.getSnapshot();
+
+ if (strict && snapshot === null) {
+ throw new Error(
+ `${name} store must be used within an enabled ${name}Provider`,
+ );
+ }
+
+ return selectorRef.current ? selectorRef.current(snapshot) : snapshot;
+ }, [resolvedStore, store]);
+
+ return useSyncExternalStore(
+ resolvedStore.subscribe,
+ getSelectedSnapshot,
+ getSelectedSnapshot,
+ );
+ };
+ };
+
+ const useStoreSelector = createStoreHook(guarded);
+ const factoryInternals: ProviderFactoryInternals = {
+ useParentStore: createStoreHook(false) as ProviderStoreHook<
+ ContextValue,
+ false
+ >,
+ };
+
const Provider: React.FC = (props) => {
const {
children = (props as { children?: ReactNode }).children,
enabled = true,
value,
- } = factory(props);
+ } = factory(props, factoryInternals);
if (!value) {
throw new Error(
@@ -125,23 +206,19 @@ export default function createProvider<
);
}
- const memoValue = useMemo(
- () => (enabled ? value : null),
- [enabled, value],
- );
-
+ const snapshotValue = enabled ? value : null;
const storeRef = useRef | null>(
null,
);
const pendingNotifyRef = useRef(false);
if (storeRef.current === null) {
- storeRef.current = createProviderStore(memoValue);
+ storeRef.current = createProviderStore(snapshotValue);
}
const store = storeRef.current;
pendingNotifyRef.current =
- store.setSnapshot(memoValue) || pendingNotifyRef.current;
+ store.setSnapshot(snapshotValue) || pendingNotifyRef.current;
useLayoutEffect(() => {
if (!pendingNotifyRef.current) {
@@ -152,130 +229,22 @@ export default function createProvider<
store.notify();
});
- // Per-instance ref ensures InnerProvider reads latest value while keeping
- // a stable component reference.
- const valueRef = useRef(memoValue);
- valueRef.current = memoValue;
-
- const InnerProvider = useMemo((): InnerProviderComponent => {
- const NamedInnerProvider: InnerProviderComponent = ({ children }) => (
-
-
- {children}
-
-
- );
-
- NamedInnerProvider.displayName = innerProviderDisplayName;
-
- return NamedInnerProvider;
- }, [store]);
-
- if (typeof children === "function") {
- return children({
- [`${name}Provider`]: InnerProvider,
- } as { [K in ProviderName as `${K}Provider`]: InnerProviderComponent });
- }
-
return (
-
- {children}
-
+ {children}
);
};
Provider.displayName = providerDisplayName;
- const useEnhancedContext = (): ContextValue | null => {
- const context = useContext(Context);
-
- if (guarded && context === null) {
- throw new Error(
- `${name} context must be used within a ${name}Provider`,
- );
- }
-
- return context;
- };
-
- const useStoreSelector = (
- selector?: (value: ContextValue | null) => Selected,
- ): Selected | ContextValue | null => {
- const store = useContext(StoreContext);
- const resolvedStore =
- store ?? (NullProviderStore as ProviderStoreApi);
- const selectorRef = useRef(selector);
- selectorRef.current = selector;
-
- const getSelectedSnapshot = useCallback(() => {
- if (guarded && store === null) {
- throw new Error(
- `${name} store must be used within a ${name}Provider`,
- );
- }
-
- const snapshot = resolvedStore.getSnapshot();
-
- if (guarded && snapshot === null) {
- throw new Error(
- `${name} store must be used within an enabled ${name}Provider`,
- );
- }
-
- return selectorRef.current ? selectorRef.current(snapshot) : snapshot;
- }, [resolvedStore, store]);
-
- return useSyncExternalStore(
- resolvedStore.subscribe,
- getSelectedSnapshot,
- getSelectedSnapshot,
- );
- };
-
- const useOptionalStoreSelector = (
- selector?: (value: ContextValue | null) => Selected,
- ): Selected | ContextValue | null => {
- const store = useContext(StoreContext);
- const resolvedStore =
- store ?? (NullProviderStore as ProviderStoreApi);
- const selectorRef = useRef(selector);
- selectorRef.current = selector;
-
- const getSelectedSnapshot = useCallback(() => {
- const snapshot = resolvedStore.getSnapshot();
- return selectorRef.current ? selectorRef.current(snapshot) : snapshot;
- }, [resolvedStore]);
-
- return useSyncExternalStore(
- resolvedStore.subscribe,
- getSelectedSnapshot,
- getSelectedSnapshot,
- );
- };
-
return {
- [`${name}Context`]: Context,
- [`${name}StoreContext`]: StoreContext,
[`${name}Provider`]: Provider,
- [`use${name}Context`]: useEnhancedContext,
[`use${name}Store`]: useStoreSelector,
- [`use${name}OptionalStore`]: useOptionalStoreSelector,
} as {
- [P in ProviderName as `${P}Context`]: React.Context;
- } & {
- [P in ProviderName as `${P}StoreContext`]: React.Context | null>;
- } & {
[P in ProviderName as `${P}Provider`]: React.FC;
- } & {
- [P in ProviderName as `use${P}Context`]: () => Guarded extends true
- ? ContextValue
- : ContextValue | null;
} & {
[P in ProviderName as `use${P}Store`]: ProviderStoreHook<
ContextValue,
Guarded
>;
- } & {
- [P in ProviderName as `use${P}OptionalStore`]: ProviderOptionalStoreHook;
};
};
}
diff --git a/test/setup.ts b/test/setup.ts
index 13603934..88dc084d 100644
--- a/test/setup.ts
+++ b/test/setup.ts
@@ -45,6 +45,9 @@ declare global {
var __reanimatedMeasureSpy:
| ((ref: { current?: { tag?: string } }) => void)
| undefined;
+ var __reanimatedDeferredTimingCallbacks:
+ | Array<() => void>
+ | undefined;
}
globalThis.resetMutableRegistry = () => {
for (const { obj, initial } of mutableObjects) {
@@ -58,6 +61,7 @@ mock.module("react-native", () => ({
select: (obj: { ios?: T; android?: T; default?: T }) =>
obj.ios ?? obj.default,
},
+ useWindowDimensions: () => ({ width: 390, height: 844 }),
StyleSheet: {
absoluteFill: {},
absoluteFillObject: {},
@@ -109,6 +113,9 @@ mock.module("react-native-reanimated", () => ({
},
runOnJS: any>(callback: T) => callback,
runOnUI: any>(callback: T) => callback,
+ useDerivedValue: (factory: () => T) => ({
+ get: factory,
+ }),
useAnimatedReaction: (
prepare: () => unknown,
react: (value: unknown, previousValue: unknown) => void,
@@ -125,9 +132,15 @@ mock.module("react-native-reanimated", () => ({
React.useRef(createTestMutable(initial)).current,
withTiming: (
toValue: number,
- config?: { __finished?: boolean },
+ config?: { __defer?: boolean; __finished?: boolean },
callback?: (finished?: boolean) => void,
) => {
+ if (config?.__defer && callback) {
+ globalThis.__reanimatedDeferredTimingCallbacks?.push(() => {
+ callback(true);
+ });
+ return toValue;
+ }
callback?.(config?.__finished ?? true);
return toValue;
},