"use client"; import * as React from "react"; import * as Dialog from "@radix-ui/react-dialog"; import { useGSAP } from "@gsap/react"; import gsap from "gsap"; import { X } from "lucide-react"; import { cn } from "@/lib/utils"; gsap.registerPlugin(useGSAP); export type RevealSheetSide = "top" | "right" | "bottom" | "left"; export interface RevealSheetProps { open: boolean; onOpenChange: (open: boolean) => void; side?: RevealSheetSide; speed?: number; bounce?: number; showGrid?: boolean; showShine?: boolean; shineDirection?: "clockwise" | "counterclockwise"; shineSpeed?: number; shineIntensity?: number; title?: string; description?: string; children: React.ReactNode; className?: string; showClose?: boolean; } const sideClasses: Record = { top: "inset-x-0 top-0 max-h-[85vh] rounded-b-3xl border-b", right: "inset-y-0 right-0 w-full max-w-xl rounded-l-3xl border-l", bottom: "inset-x-0 bottom-0 max-h-[85vh] rounded-t-3xl border-t", left: "inset-y-0 left-0 w-full max-w-xl rounded-r-3xl border-r", }; const transformOrigins: Record = { top: "center top", right: "right center", bottom: "center bottom", left: "left center", }; const revealOrigins: Record = { top: "50% 0%", right: "100% 50%", bottom: "50% 100%", left: "0% 50%", }; const closeButton = "group inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-transparent text-muted-foreground transition-colors duration-200 hover:text-red-500 dark:hover:text-red-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/50"; const RevealSheetPanel = ({ open, onExitComplete, overlayRef, side = "right", speed = 1, bounce = 1, showGrid = true, showShine = true, shineDirection = "clockwise", shineSpeed = 1, shineIntensity = 0.55, title = "Explore the collection", description, children, className, showClose = true, }: Omit & { onExitComplete: () => void; overlayRef: React.RefObject; }) => { const contentRef = React.useRef(null); const shineRef = React.useRef(null); const timelineRef = React.useRef(null); const returnFocusRef = React.useRef(null); const bounceAmount = Number.isFinite(bounce) ? Math.min(2, Math.max(0, bounce)) : 1; useGSAP( () => { const media = gsap.matchMedia(); media.add("(prefers-reduced-motion: no-preference)", () => { const panel = contentRef.current; const overlay = overlayRef.current; if (!panel || !overlay) return; const horizontal = side === "left" || side === "right"; const stretchAxis = horizontal ? "scaleX" : "scaleY"; const radius = horizontal ? Math.hypot(panel.offsetWidth, panel.offsetHeight / 2) + 2 : Math.hypot(panel.offsetWidth / 2, panel.offsetHeight) + 2; const origin = revealOrigins[side]; gsap.set(panel, { clipPath: `circle(0px at ${origin})`, transformOrigin: transformOrigins[side], willChange: "clip-path, transform", }); const timeline = gsap.timeline({ paused: true, onComplete: () => gsap.set(panel, { clearProps: "clipPath,transform,transformOrigin,willChange", }), onReverseComplete: onExitComplete, }) .fromTo( overlay, { opacity: 0 }, { opacity: 1, duration: 0.35, ease: "power2.out" }, 0, ) .to(panel, { clipPath: `circle(${radius}px at ${origin})`, duration: 0.72, ease: "power3.inOut", }, 0) .to(panel, { [stretchAxis]: 1 + 0.032 * bounceAmount, duration: 0.18, ease: "power3.out", }, "-=0.18") .to(panel, { [stretchAxis]: 1 - 0.008 * bounceAmount, duration: 0.16, ease: "power2.inOut", }) .to(panel, { [stretchAxis]: 1 + 0.006 * bounceAmount, duration: 0.14, ease: "power2.out", }) .to(panel, { [stretchAxis]: 1, duration: 0.16, ease: "power2.out", }) .timeScale(Number.isFinite(speed) ? Math.max(0.1, speed) : 1); timelineRef.current = timeline; if (open) timeline.play(); }, contentRef); return () => { media.revert(); timelineRef.current = null; }; }, { scope: contentRef }, ); useGSAP( () => { const shine = shineRef.current; if (!shine || !showShine) return; const media = gsap.matchMedia(); media.add("(prefers-reduced-motion: no-preference)", () => { gsap.fromTo( shine, { "--shine-angle": "0deg" }, { "--shine-angle": shineDirection === "clockwise" ? "360deg" : "-360deg", duration: 7 / (Number.isFinite(shineSpeed) ? Math.max(0.1, shineSpeed) : 1), ease: "none", repeat: -1, }, ); }); return () => media.revert(); }, { scope: contentRef, dependencies: [showShine, shineDirection, shineSpeed], revertOnUpdate: true, }, ); React.useEffect(() => { const timeline = timelineRef.current; if (open) { timeline?.play(); } else if (timeline) { gsap.set(contentRef.current, { willChange: "clip-path, transform" }); timeline.reverse(); } else { onExitComplete(); } }, [open, onExitComplete]); return ( { returnFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null; }} onCloseAutoFocus={(event) => { const target = returnFocusRef.current; returnFocusRef.current = null; if (!target?.isConnected) return; event.preventDefault(); target.focus({ preventScroll: true }); }} className={cn( "fixed z-50 flex flex-col overflow-hidden border-border/80 bg-[linear-gradient(155deg,#fff_0%,#f5f5f7_100%)] text-foreground shadow-[0_24px_80px_-28px_rgba(0,0,0,0.75)] outline-none dark:bg-[linear-gradient(155deg,#1c1c20_0%,#101013_100%)]", sideClasses[side], className, )} > {showGrid && (
)}
{title} {description && ( {description} )}
{showClose && ( )}
{children}
{showShine && (