"use client"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useEffect, useRef, type ReactNode } from "react"; import { EASE_OUT, SPRING_PANEL } from "@/lib/ease"; import { useFocusTrap } from "@/lib/hooks/use-focus-trap"; import { PresenceGate } from "@/lib/presence-gate"; import { cn } from "@/lib/utils"; export interface DrawerProps { open: boolean; onOpenChange: (open: boolean) => void; side?: "left" | "right"; children: ReactNode; /** Class for the panel surface. */ className?: string; /** Class for the backdrop. */ backdropClassName?: string; ariaLabel?: string; /** Close when the backdrop is clicked or Escape is pressed. Default true. */ dismissable?: boolean; } export function Drawer({ open, onOpenChange, side = "right", children, className, backdropClassName, ariaLabel, dismissable = true, }: DrawerProps) { const reduce = useReducedMotion(); const panelRef = useRef(null); // Focus moves into the panel on open, Tab stays inside it, and focus returns // to the opener on close. useFocusTrap(open, panelRef, { onEscape: dismissable ? () => onOpenChange(false) : undefined, }); useEffect(() => { if (!open) return; const prevOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prevOverflow; }; }, [open]); const offscreen = side === "right" ? "100%" : "-100%"; // Two fixed siblings, no wrapper: the backdrop spans the viewport edges but // paints the scrim, and the panel is inset off one side and paints its own // surface, so neither is a transparent edge-spanning layer. Both hang off // `PresenceGate`, so interaction releases in the same commit that starts the // exit rather than when it ends. return ( {open ? ( {({ gate }) => ( dismissable && onOpenChange(false)} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.25, ease: EASE_OUT }} {...gate} className={cn( "fixed inset-0 z-50 h-full w-full cursor-default bg-black/40 backdrop-blur-sm", backdropClassName, )} /> )} ) : null} {open ? ( {({ gate }) => ( {children} )} ) : null} ); }