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 && (
)}
{/* 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';