import * as DialogPrimitive from '@radix-ui/react-dialog'; import { motion, AnimatePresence, useDragControls, PanInfo } from 'framer-motion'; import { forwardRef, type ComponentPropsWithoutRef, createContext, useContext, useState, useCallback, } from 'react'; import { cn } from '@/lib/utils'; import { usePlatform } from '@/platform'; import { useBackButton } from '@/platform'; import { backdrop, backdropTransition, sheetSlideUp, sheetTransition, } from '../../motion/transitions'; // Close icon const CloseIcon = () => ( ); // Context for AnimatePresence and control interface SheetContextValue { open: boolean; onClose: () => void; } const SheetContext = createContext({ open: false, onClose: () => {}, }); // Root export interface SheetProps extends ComponentPropsWithoutRef { onClose?: () => void; } export const Sheet = ({ children, open, onOpenChange, onClose, ...props }: SheetProps) => { const [internalOpen, setInternalOpen] = useState(false); const isOpen = open !== undefined ? open : internalOpen; const handleOpenChange = useCallback( (newOpen: boolean) => { if (onOpenChange) { onOpenChange(newOpen); } else { setInternalOpen(newOpen); } if (!newOpen && onClose) { onClose(); } }, [onOpenChange, onClose], ); const handleClose = useCallback(() => { handleOpenChange(false); }, [handleOpenChange]); return ( {children} ); }; // Trigger export const SheetTrigger = DialogPrimitive.Trigger; // Portal export const SheetPortal = DialogPrimitive.Portal; // Close export const SheetClose = DialogPrimitive.Close; // Overlay export type SheetOverlayProps = ComponentPropsWithoutRef; export const SheetOverlay = forwardRef( ({ className, ...props }, ref) => ( ), ); SheetOverlay.displayName = 'SheetOverlay'; // Content export interface SheetContentProps extends ComponentPropsWithoutRef< typeof DialogPrimitive.Content > { showDragHandle?: boolean; showCloseButton?: boolean; enableDragToClose?: boolean; closeThreshold?: number; } export const SheetContent = forwardRef( ( { className, children, showDragHandle = true, showCloseButton = false, enableDragToClose = true, closeThreshold = 0.3, ...props }, ref, ) => { const { open, onClose } = useContext(SheetContext); const { haptic } = usePlatform(); const dragControls = useDragControls(); // Back button integration useBackButton(open ? onClose : null); const handleDragEnd = useCallback( (_: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => { const velocity = info.velocity.y; const offset = info.offset.y; const height = window.innerHeight; // Close if dragged down fast enough or past threshold if (velocity > 500 || offset > height * closeThreshold) { haptic.impact('light'); onClose(); } }, [closeThreshold, haptic, onClose], ); return ( {open && ( <> {/* Drag handle */} {showDragHandle && (
dragControls.start(e)} >
)} {/* Close button */} {showCloseButton && ( Close )} {/* Content */}
{children}
)} ); }, ); SheetContent.displayName = 'SheetContent'; // Header export type SheetHeaderProps = React.HTMLAttributes; export const SheetHeader = ({ className, ...props }: SheetHeaderProps) => (
); SheetHeader.displayName = 'SheetHeader'; // Footer export type SheetFooterProps = React.HTMLAttributes; export const SheetFooter = ({ className, ...props }: SheetFooterProps) => (
); SheetFooter.displayName = 'SheetFooter'; // Title export type SheetTitleProps = ComponentPropsWithoutRef; export const SheetTitle = forwardRef( ({ className, ...props }, ref) => ( ), ); SheetTitle.displayName = 'SheetTitle'; // Description export type SheetDescriptionProps = ComponentPropsWithoutRef; export const SheetDescription = forwardRef( ({ className, ...props }, ref) => ( ), ); SheetDescription.displayName = 'SheetDescription';