'use client'; import { cloneElement, createContext, isValidElement, useContext, useEffect, useId, useMemo, useState, type HTMLAttributes, type ReactElement, type ReactNode, } from 'react'; import { createPortal } from 'react-dom'; import { cn } from '@/lib/utils/cn'; type DrawerDirection = 'top' | 'right' | 'bottom' | 'left'; type DrawerContextValue = { open: boolean; setOpen: (open: boolean) => void; direction: DrawerDirection; }; const DrawerContext = createContext(null); function useDrawerContext() { const context = useContext(DrawerContext); if (!context) { throw new Error('Drawer components must be used within Drawer.'); } return context; } function renderWithOptionalChild( child: ReactNode, props: Record, fallback: ReactNode, ) { if (isValidElement(child)) { return cloneElement(child as ReactElement, props); } return fallback; } export function Drawer({ children, open, defaultOpen = false, onOpenChange, direction = 'bottom', title, description, footer, }: { children: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; direction?: DrawerDirection; title?: string; description?: string; footer?: ReactNode; size?: 'md' | 'lg' | 'xl'; }) { const [internalOpen, setInternalOpen] = useState(defaultOpen); const isControlled = open !== undefined; const resolvedOpen = isControlled ? open : internalOpen; const value = useMemo( () => ({ open: resolvedOpen, direction, setOpen: (nextOpen) => { if (!isControlled) { setInternalOpen(nextOpen); } onOpenChange?.(nextOpen); }, }), [direction, isControlled, onOpenChange, resolvedOpen], ); return ( {title || description || footer ? (
{title ? {title} : null} {description ? ( {description} ) : null}
{children}
{footer ? {footer} : null}
) : ( children )}
); } export function DrawerTrigger({ children, asChild = false, }: { children: ReactNode; asChild?: boolean; }) { const { setOpen } = useDrawerContext(); if (asChild) { return renderWithOptionalChild( children, { onClick: () => setOpen(true), }, children, ); } return ( ); } export function DrawerClose({ children, asChild = false, }: { children: ReactNode; asChild?: boolean; }) { const { setOpen } = useDrawerContext(); if (asChild) { return renderWithOptionalChild( children, { onClick: () => setOpen(false), }, children, ); } return ( ); } export function DrawerContent({ children, className, 'aria-label': ariaLabel, }: HTMLAttributes) { const { open, setOpen, direction } = useDrawerContext(); const titleId = useId(); const descriptionId = useId(); useEffect(() => { if (!open) { return; } const previousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { setOpen(false); } }; window.addEventListener('keydown', handleKeyDown); return () => { document.body.style.overflow = previousOverflow; window.removeEventListener('keydown', handleKeyDown); }; }, [open, setOpen]); if (!open || typeof document === 'undefined') { return null; } const positionClassName = direction === 'right' ? 'inset-y-0 right-0 h-full border-l' : direction === 'left' ? 'inset-y-0 left-0 h-full border-r' : direction === 'top' ? 'inset-x-0 top-0 border-b' : 'inset-x-0 bottom-0 border-t'; return createPortal(
, document.body, ); } type DrawerMetaContextValue = { titleId: string; descriptionId: string; }; const DrawerMetaContext = createContext(null); function useDrawerMetaContext() { const context = useContext(DrawerMetaContext); if (!context) { throw new Error( 'Drawer title and description must be used within DrawerContent.', ); } return context; } export function DrawerHeader({ className, ...props }: HTMLAttributes) { return (
); } export function DrawerFooter({ className, ...props }: HTMLAttributes) { return (
); } export function DrawerTitle({ className, ...props }: HTMLAttributes) { const { titleId } = useDrawerMetaContext(); return (

); } export function DrawerDescription({ className, ...props }: HTMLAttributes) { const { descriptionId } = useDrawerMetaContext(); return (

); }