import { type ReactNode, useCallback, useState } from 'react'; import { type LayoutChangeEvent, Modal, Pressable, type StyleProp, View, type ViewStyle } from 'react-native'; import { useModalRender } from '../../../hooks/use-modal-render'; import { useReducedMotion } from '../../../hooks/use-reduced-motion'; import { useSafeInsets } from '../../../hooks/use-safe-insets'; import { cn } from '../../../lib/cn'; import { SPRING_PANEL } from '../../../lib/ease'; import type { SurfaceElevation } from '../../../lib/elevated'; import { MotiView } from '../../../moti/components/view'; import { AnimatePresence } from '../../../moti/presence/animate-presence'; import { Surface } from '../../display/Surface/surface'; import { Text } from '../../typography/Text/text'; import { OverlayBlur } from '../Overlay/overlay-blur'; import { OverlayOutlet } from '../Overlay/overlay-portal'; import type { OverlayType } from '../Overlay/overlay-type'; export type DrawerSide = 'left' | 'right'; export type DrawerProps = { open: boolean; onOpenChange: (open: boolean) => void; side?: DrawerSide; /** * Swap the drawer panel's ladder shadow for the input field's large, diffuse * halo (`shadow-floating`). It replaces the `shadow-elevated-N` rung rather * than adding to it, so the panel keeps its `elevation` tint but trades the * layered drop for the halo. @default false */ floating?: boolean; /** * Surface elevation of the drawer panel (0–3) — drives the background tint * and the `shadow-elevated-N` recipe (drop shadow + dark-mode rim). `0` is the * flat resting surface (no shadow or border). Defaults to `6`. */ elevation?: SurfaceElevation; children: ReactNode; /** The scrim behind the panel: `"blur"`, `"opacity"`, or `"none"`. Default `"blur"`. */ overlay?: OverlayType; /** Close when the backdrop is tapped. Default true. */ closeOnOutsidePress?: boolean; accessibilityLabel?: string; style?: StyleProp; /** * Wrap content in device safe-area insets (status bar top, home indicator * bottom). Requires `react-native-safe-area-context` and `` * in the tree. Set to `false` to manage insets yourself. @default true */ safeArea?: boolean; /** * Fires after the drawer has fully presented (iOS `Modal.onShow`) — the moment * it is safe to request keyboard focus on content inside it. No-op on web. */ onShow?: () => void; testID?: string; }; export function Drawer({ open, onOpenChange, side = 'right', floating = false, elevation = 3, children, overlay = 'blur', closeOnOutsidePress = true, accessibilityLabel, style, safeArea = true, onShow, testID, }: DrawerProps) { const reduce = useReducedMotion(); const { rendered, onExitComplete: handleExitComplete } = useModalRender(open); const insets = useSafeInsets(); // Panel width drives the offscreen slide distance; measured on first layout, // falls back to a wide default so it starts fully offscreen before measure. const [width, setWidth] = useState(360); const handleRequestClose = useCallback(() => onOpenChange(false), [onOpenChange]); const handleBackdropPress = useCallback(() => { if (closeOnOutsidePress) onOpenChange(false); }, [closeOnOutsidePress, onOpenChange]); const handleLayout = useCallback((e: LayoutChangeEvent) => setWidth(e.nativeEvent.layout.width), []); if (!rendered) return null; const offscreen = side === 'right' ? width : -width; return ( {open ? ( {/* Blur sits outside the dim's opacity fade: a parent opacity fades out the CSS backdrop-filter on web (backdrop-root clipping), so OverlayBlur fades its own opacity instead. */} {overlay === 'blur' ? : null} {typeof children === 'string' || typeof children === 'number' ? ( {children} ) : ( children )} ) : null} {/* Overlay outlet: above the panel, outside the slide animation. */} ); }