import { createContext, useContext, useEffect, useRef, type ReactNode } from "react"; import { useWindowDimensions } from "react-native"; import { useSharedValue, withTiming, Easing, type SharedValue, } from "react-native-reanimated"; import { type GestureType } from "react-native-gesture-handler"; import { UnistylesRuntime } from "react-native-unistyles"; import { usePanelStore } from "@/stores/panel-store"; const ANIMATION_DURATION = 220; const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1); const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__); function logExplorerAnimation( event: string, details: Record ): void { if (!IS_DEV) { return; } console.log(`[ExplorerAnimation] ${event}`, details); } interface ExplorerSidebarAnimationContextValue { translateX: SharedValue; backdropOpacity: SharedValue; windowWidth: number; animateToOpen: () => void; animateToClose: () => void; isGesturing: SharedValue; closeGestureRef: React.MutableRefObject; } const ExplorerSidebarAnimationContext = createContext(null); export function ExplorerSidebarAnimationProvider({ children }: { children: ReactNode }) { const { width: windowWidth } = useWindowDimensions(); const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; const mobileView = usePanelStore((state) => state.mobileView); const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen); // Derive isOpen from the unified panel state const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen; // Right sidebar: closed = +windowWidth (off-screen right), open = 0 const translateX = useSharedValue(isOpen ? 0 : windowWidth); const backdropOpacity = useSharedValue(isOpen ? 1 : 0); const isGesturing = useSharedValue(false); const closeGestureRef = useRef(undefined); // Track previous isOpen to detect changes const prevIsOpen = useRef(isOpen); // Sync animation with store state changes (e.g., backdrop tap, programmatic open/close) useEffect(() => { // Skip if this is initial render or if we're mid-gesture if (prevIsOpen.current === isOpen) { return; } const previousIsOpen = prevIsOpen.current; prevIsOpen.current = isOpen; // Don't animate if we're in the middle of a gesture - the gesture handler will handle it if (isGesturing.value) { logExplorerAnimation("sync-skipped-during-gesture", { previousIsOpen, nextIsOpen: isOpen, mobileView, desktopFileExplorerOpen, }); return; } logExplorerAnimation("sync-state-change", { previousIsOpen, nextIsOpen: isOpen, mobileView, desktopFileExplorerOpen, windowWidth, }); if (isOpen) { translateX.value = withTiming(0, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }); backdropOpacity.value = withTiming(1, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }); } else { translateX.value = withTiming(windowWidth, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }); backdropOpacity.value = withTiming(0, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }); } }, [ isOpen, translateX, backdropOpacity, windowWidth, isGesturing, mobileView, desktopFileExplorerOpen, ]); const animateToOpen = () => { "worklet"; translateX.value = withTiming(0, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }); backdropOpacity.value = withTiming(1, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }); }; const animateToClose = () => { "worklet"; translateX.value = withTiming(windowWidth, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }); backdropOpacity.value = withTiming(0, { duration: ANIMATION_DURATION, easing: ANIMATION_EASING, }); }; return ( {children} ); } export function useExplorerSidebarAnimation() { const context = useContext(ExplorerSidebarAnimationContext); if (!context) { throw new Error("useExplorerSidebarAnimation must be used within ExplorerSidebarAnimationProvider"); } return context; }