Spaces:
Sleeping
Sleeping
| import React, { useEffect, useState } from "react"; | |
| import { createPortal } from "react-dom"; | |
| import { X } from "lucide-react"; | |
| import { cn } from "@/lib/utils"; | |
| import { Button } from "@/components/ui/button"; | |
| interface CustomSheetProps { | |
| open: boolean; | |
| onOpenChange: (open: boolean) => void; | |
| children: React.ReactNode; | |
| className?: string; | |
| side?: "right" | "left"; | |
| title?: string; | |
| description?: string; | |
| } | |
| export const CustomSheet: React.FC<CustomSheetProps> = ({ | |
| open, | |
| onOpenChange, | |
| children, | |
| className, | |
| side = "right", | |
| title, | |
| description | |
| }) => { | |
| const [isRendered, setIsRendered] = useState(false); | |
| const [isVisible, setIsVisible] = useState(false); | |
| useEffect(() => { | |
| if (open) { | |
| setIsRendered(true); | |
| // Small delay to allow render before transition | |
| setTimeout(() => setIsVisible(true), 10); | |
| // Prevent body scroll | |
| document.body.style.overflow = 'hidden'; | |
| } else { | |
| setIsVisible(false); | |
| // Wait for transition to finish before unmounting | |
| setTimeout(() => setIsRendered(false), 300); | |
| // Restore body scroll | |
| document.body.style.overflow = ''; | |
| } | |
| return () => { | |
| document.body.style.overflow = ''; | |
| }; | |
| }, [open]); | |
| if (!isRendered) return null; | |
| return createPortal( | |
| <div className="fixed inset-0 z-[9999] flex justify-end"> | |
| {/* Backdrop */} | |
| <div | |
| className={cn( | |
| "fixed inset-0 bg-black/50 transition-opacity duration-300", | |
| isVisible ? "opacity-100" : "opacity-0" | |
| )} | |
| onClick={() => onOpenChange(false)} | |
| /> | |
| {/* Sheet Content */} | |
| <div | |
| className={cn( | |
| "relative h-full w-full max-w-md bg-background shadow-xl transition-transform duration-300 ease-in-out flex flex-col", | |
| side === "right" ? (isVisible ? "translate-x-0" : "translate-x-full") : (isVisible ? "translate-x-0" : "-translate-x-full"), | |
| className | |
| )} | |
| > | |
| {/* Header */} | |
| <div className="flex items-center justify-between p-4 border-b"> | |
| <div> | |
| {title && <h2 className="text-lg font-semibold">{title}</h2>} | |
| {description && <p className="text-sm text-muted-foreground">{description}</p>} | |
| </div> | |
| <Button variant="ghost" size="icon" onClick={() => onOpenChange(false)}> | |
| <X className="h-4 w-4" /> | |
| <span className="sr-only">Close</span> | |
| </Button> | |
| </div> | |
| {/* Body */} | |
| <div className="flex-1 overflow-y-auto p-4"> | |
| {children} | |
| </div> | |
| </div> | |
| </div>, | |
| document.body | |
| ); | |
| }; | |