{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reveal-sheet",
  "type": "registry:ui",
  "title": "Reveal Sheet",
  "description": "Directional accessible sheet with a circular reveal, adjustable bounce, and optional edge shine.",
  "dependencies": [
    "@radix-ui/react-dialog",
    "@gsap/react",
    "gsap",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "cssVars": {
    "light": {
      "line": "rgba(0, 0, 0, 0.08)"
    },
    "dark": {
      "line": "rgb(255 255 255 / 0.08)"
    }
  },
  "files": [
    {
      "path": "src/registry/ui/reveal-sheet.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as Dialog from \"@radix-ui/react-dialog\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type RevealSheetSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport interface RevealSheetProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  side?: RevealSheetSide;\n  speed?: number;\n  bounce?: number;\n  showGrid?: boolean;\n  showShine?: boolean;\n  shineDirection?: \"clockwise\" | \"counterclockwise\";\n  shineSpeed?: number;\n  shineIntensity?: number;\n  title?: string;\n  description?: string;\n  children: React.ReactNode;\n  className?: string;\n  showClose?: boolean;\n}\n\nconst sideClasses: Record<RevealSheetSide, string> = {\n  top: \"inset-x-0 top-0 max-h-[85vh] rounded-b-3xl border-b\",\n  right: \"inset-y-0 right-0 w-full max-w-xl rounded-l-3xl border-l\",\n  bottom: \"inset-x-0 bottom-0 max-h-[85vh] rounded-t-3xl border-t\",\n  left: \"inset-y-0 left-0 w-full max-w-xl rounded-r-3xl border-r\",\n};\n\nconst transformOrigins: Record<RevealSheetSide, string> = {\n  top: \"center top\",\n  right: \"right center\",\n  bottom: \"center bottom\",\n  left: \"left center\",\n};\n\nconst revealOrigins: Record<RevealSheetSide, string> = {\n  top: \"50% 0%\",\n  right: \"100% 50%\",\n  bottom: \"50% 100%\",\n  left: \"0% 50%\",\n};\n\nconst closeButton =\n  \"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\";\n\nconst RevealSheetPanel = ({\n  open,\n  onExitComplete,\n  overlayRef,\n  side = \"right\",\n  speed = 1,\n  bounce = 1,\n  showGrid = true,\n  showShine = true,\n  shineDirection = \"clockwise\",\n  shineSpeed = 1,\n  shineIntensity = 0.55,\n  title = \"Explore the collection\",\n  description,\n  children,\n  className,\n  showClose = true,\n}: Omit<RevealSheetProps, \"onOpenChange\"> & {\n  onExitComplete: () => void;\n  overlayRef: React.RefObject<HTMLDivElement | null>;\n}) => {\n  const contentRef = React.useRef<HTMLDivElement>(null);\n  const shineRef = React.useRef<HTMLDivElement>(null);\n  const timelineRef = React.useRef<gsap.core.Timeline | null>(null);\n  const returnFocusRef = React.useRef<HTMLElement | null>(null);\n  const bounceAmount = Number.isFinite(bounce)\n    ? Math.min(2, Math.max(0, bounce))\n    : 1;\n\n  useGSAP(\n    () => {\n      const media = gsap.matchMedia();\n\n      media.add(\"(prefers-reduced-motion: no-preference)\", () => {\n        const panel = contentRef.current;\n        const overlay = overlayRef.current;\n        if (!panel || !overlay) return;\n\n        const horizontal = side === \"left\" || side === \"right\";\n        const stretchAxis = horizontal ? \"scaleX\" : \"scaleY\";\n        const radius = horizontal\n          ? Math.hypot(panel.offsetWidth, panel.offsetHeight / 2) + 2\n          : Math.hypot(panel.offsetWidth / 2, panel.offsetHeight) + 2;\n        const origin = revealOrigins[side];\n\n        gsap.set(panel, {\n          clipPath: `circle(0px at ${origin})`,\n          transformOrigin: transformOrigins[side],\n          willChange: \"clip-path, transform\",\n        });\n\n        const timeline = gsap.timeline({\n          paused: true,\n          onComplete: () =>\n            gsap.set(panel, {\n              clearProps: \"clipPath,transform,transformOrigin,willChange\",\n            }),\n          onReverseComplete: onExitComplete,\n        })\n          .fromTo(\n            overlay,\n            { opacity: 0 },\n            { opacity: 1, duration: 0.35, ease: \"power2.out\" },\n            0,\n          )\n          .to(panel, {\n            clipPath: `circle(${radius}px at ${origin})`,\n            duration: 0.72,\n            ease: \"power3.inOut\",\n          }, 0)\n          .to(panel, {\n            [stretchAxis]: 1 + 0.032 * bounceAmount,\n            duration: 0.18,\n            ease: \"power3.out\",\n          }, \"-=0.18\")\n          .to(panel, {\n            [stretchAxis]: 1 - 0.008 * bounceAmount,\n            duration: 0.16,\n            ease: \"power2.inOut\",\n          })\n          .to(panel, {\n            [stretchAxis]: 1 + 0.006 * bounceAmount,\n            duration: 0.14,\n            ease: \"power2.out\",\n          })\n          .to(panel, {\n            [stretchAxis]: 1,\n            duration: 0.16,\n            ease: \"power2.out\",\n          })\n          .timeScale(Number.isFinite(speed) ? Math.max(0.1, speed) : 1);\n\n        timelineRef.current = timeline;\n        if (open) timeline.play();\n      }, contentRef);\n\n      return () => {\n        media.revert();\n        timelineRef.current = null;\n      };\n    },\n    { scope: contentRef },\n  );\n\n  useGSAP(\n    () => {\n      const shine = shineRef.current;\n      if (!shine || !showShine) return;\n      const media = gsap.matchMedia();\n      media.add(\"(prefers-reduced-motion: no-preference)\", () => {\n        gsap.fromTo(\n          shine,\n          { \"--shine-angle\": \"0deg\" },\n          {\n            \"--shine-angle\": shineDirection === \"clockwise\" ? \"360deg\" : \"-360deg\",\n            duration:\n              7 /\n              (Number.isFinite(shineSpeed)\n                ? Math.max(0.1, shineSpeed)\n                : 1),\n            ease: \"none\",\n            repeat: -1,\n          },\n        );\n      });\n      return () => media.revert();\n    },\n    {\n      scope: contentRef,\n      dependencies: [showShine, shineDirection, shineSpeed],\n      revertOnUpdate: true,\n    },\n  );\n\n  React.useEffect(() => {\n    const timeline = timelineRef.current;\n    if (open) {\n      timeline?.play();\n    } else if (timeline) {\n      gsap.set(contentRef.current, { willChange: \"clip-path, transform\" });\n      timeline.reverse();\n    } else {\n      onExitComplete();\n    }\n  }, [open, onExitComplete]);\n\n  return (\n    <Dialog.Content\n      ref={contentRef}\n      onOpenAutoFocus={() => {\n        returnFocusRef.current =\n          document.activeElement instanceof HTMLElement\n            ? document.activeElement\n            : null;\n      }}\n      onCloseAutoFocus={(event) => {\n        const target = returnFocusRef.current;\n        returnFocusRef.current = null;\n        if (!target?.isConnected) return;\n        event.preventDefault();\n        target.focus({ preventScroll: true });\n      }}\n      className={cn(\n        \"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%)]\",\n        sideClasses[side],\n        className,\n      )}\n    >\n      {showGrid && (\n        <div className=\"pointer-events-none absolute inset-0 opacity-25 [background-image:linear-gradient(to_right,var(--line)_1px,transparent_1px),linear-gradient(to_bottom,var(--line)_1px,transparent_1px)] [background-size:32px_32px] [mask-image:linear-gradient(to_bottom,black,transparent_55%)]\" />\n      )}\n      <div className=\"relative flex min-h-0 flex-1 flex-col\">\n        <header className=\"flex items-start justify-between gap-6 border-b border-border/70 px-6 py-5 sm:px-8\">\n          <div className=\"min-w-0 space-y-1\">\n            <Dialog.Title className=\"font-serif text-2xl tracking-tight text-foreground sm:text-3xl\">\n              {title}\n            </Dialog.Title>\n            {description && (\n              <Dialog.Description className=\"max-w-md text-sm leading-6 text-muted-foreground\">\n                {description}\n              </Dialog.Description>\n            )}\n          </div>\n          {showClose && (\n            <Dialog.Close asChild>\n              <button\n                type=\"button\"\n                className={closeButton}\n                aria-label=\"Close sheet\"\n              >\n                <X className=\"size-4 motion-safe:transition-transform motion-safe:duration-300 motion-safe:ease-out motion-safe:group-hover:rotate-90 motion-safe:group-hover:scale-125\" />\n              </button>\n            </Dialog.Close>\n          )}\n        </header>\n        <div className=\"relative min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-8\">\n          {children}\n        </div>\n      </div>\n      {showShine && (\n        <div\n          ref={shineRef}\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 rounded-[inherit] p-[2px] [background:conic-gradient(from_var(--shine-angle,0deg),transparent_0deg,transparent_298deg,rgba(255,255,255,0.15)_321deg,rgba(255,255,255,0.95)_338deg,rgba(255,255,255,0.15)_351deg,transparent_360deg)]\"\n          style={{\n            opacity: Number.isFinite(shineIntensity)\n              ? Math.min(1, Math.max(0, shineIntensity))\n              : 0.55,\n            mask: \"linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)\",\n            maskComposite: \"exclude\",\n            WebkitMaskComposite: \"xor\",\n          }}\n        />\n      )}\n    </Dialog.Content>\n  );\n};\n\nexport const RevealSheet = ({\n  open,\n  onOpenChange,\n  ...panelProps\n}: RevealSheetProps) => {\n  const [present, setPresent] = React.useState(open);\n  const overlayRef = React.useRef<HTMLDivElement>(null);\n  const onExitComplete = React.useCallback(() => setPresent(false), []);\n\n  if (open && !present) setPresent(true);\n\n  return (\n    <Dialog.Root open={present} onOpenChange={onOpenChange}>\n      <Dialog.Portal>\n        <Dialog.Overlay\n          ref={overlayRef}\n          className=\"fixed inset-0 z-40 bg-black/45 backdrop-blur-[2px]\"\n        />\n        <RevealSheetPanel\n          {...panelProps}\n          open={open}\n          onExitComplete={onExitComplete}\n          overlayRef={overlayRef}\n        />\n      </Dialog.Portal>\n    </Dialog.Root>\n  );\n};\n\nRevealSheet.displayName = \"RevealSheet\";\n\nexport default RevealSheet;\n",
      "type": "registry:ui",
      "target": "@ui/reveal-sheet.tsx"
    }
  ]
}
