{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"sheet","type":"registry:ui","title":"Sheet","description":"Side panel on the native <dialog> that slides in from any edge with a springy ease, staggers its content in, blurs the backdrop and returns focus on close.","dependencies":["lucide-react"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/sheet.tsx","type":"registry:ui","content":"\"use client\";\n\nimport {\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ButtonHTMLAttributes,\n  type HTMLAttributes,\n  type KeyboardEvent,\n  type MouseEvent,\n  type ReactNode,\n} from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n// Native <dialog> slides in from its side. [open] + @starting-style drive the entrance,\n// allow-discrete on display/overlay keeps it painted (and in the top layer) while it\n// slides back out after close(). Children fade up slightly after the panel lands.\nconst css = `\n.ui-sheet{--ui-sheet-off:translate(100%,0);transform:var(--ui-sheet-off);transition:transform .25s cubic-bezier(.4,0,1,1),display .25s allow-discrete,overlay .25s allow-discrete}\n.ui-sheet[data-side=left]{--ui-sheet-off:translate(-100%,0)}\n.ui-sheet[data-side=top]{--ui-sheet-off:translate(0,-100%)}\n.ui-sheet[data-side=bottom]{--ui-sheet-off:translate(0,100%)}\n.ui-sheet[open]{transform:none;transition-duration:.45s;transition-timing-function:cubic-bezier(.16,1,.3,1)}\n@starting-style{.ui-sheet[open]{transform:var(--ui-sheet-off)}}\n@keyframes ui-sheet-body{from{opacity:0;translate:0 8px}}\n.ui-sheet[open]>.ui-sheet-body>*{animation:ui-sheet-body .45s cubic-bezier(.16,1,.3,1) both}\n.ui-sheet[open]>.ui-sheet-body>:nth-child(2){animation-delay:.06s}\n.ui-sheet[open]>.ui-sheet-body>:nth-child(3){animation-delay:.12s}\n.ui-sheet[open]>.ui-sheet-body>:nth-child(n+4){animation-delay:.18s}\n.ui-sheet::backdrop{background:rgb(0 0 0/0);backdrop-filter:blur(0);transition:background .3s,backdrop-filter .3s,display .3s allow-discrete,overlay .3s allow-discrete}\n.ui-sheet[open]::backdrop{background:rgb(0 0 0/.5);backdrop-filter:blur(4px)}\n@starting-style{.ui-sheet[open]::backdrop{background:rgb(0 0 0/0);backdrop-filter:blur(0)}}\nhtml:has(.ui-sheet:modal){overflow:hidden}\n@media (prefers-reduced-motion:reduce){.ui-sheet,.ui-sheet::backdrop{transition-duration:0s!important}.ui-sheet-body>*{animation:none!important}}\n`;\n\nconst FOCUSABLE =\n  'a[href],button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),[tabindex]:not([tabindex=\"-1\"])';\n\nexport type SheetSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\ntype Ctx = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  position: \"fixed\" | \"absolute\";\n  ids: { trigger: string; content: string; title: string; description: string };\n};\nconst SheetContext = createContext<Ctx | null>(null);\n\nfunction useSheet() {\n  const ctx = useContext(SheetContext);\n  if (!ctx) throw new Error(\"Sheet parts must be rendered inside <Sheet>.\");\n  return ctx;\n}\n\nexport interface SheetProps {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /**\n   * `fixed` opens a true modal (showModal: top layer, inert page, native ::backdrop).\n   * `absolute` slides within the nearest positioned ancestor, for previews and embeds.\n   */\n  position?: \"fixed\" | \"absolute\";\n  children?: ReactNode;\n}\n\nexport function Sheet({ open: openProp, defaultOpen = false, onOpenChange, position = \"fixed\", children }: SheetProps) {\n  const [inner, setInner] = useState(defaultOpen);\n  const open = openProp ?? inner;\n  const id = useId();\n  const setOpen = (next: boolean) => {\n    if (openProp === undefined) setInner(next);\n    onOpenChange?.(next);\n  };\n  const ids = { trigger: `${id}-trigger`, content: `${id}-content`, title: `${id}-title`, description: `${id}-desc` };\n  return <SheetContext value={{ open, setOpen, position, ids }}>{children}</SheetContext>;\n}\n\nexport function SheetTrigger({ className, onClick, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {\n  const { open, setOpen, ids } = useSheet();\n  return (\n    <button\n      type=\"button\"\n      id={ids.trigger}\n      aria-haspopup=\"dialog\"\n      aria-expanded={open}\n      aria-controls={ids.content}\n      {...props}\n      onClick={(e) => {\n        onClick?.(e);\n        if (!e.defaultPrevented) setOpen(true);\n      }}\n      className={cn(\n        \"inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-border bg-card px-4 text-sm font-medium text-foreground shadow-sm outline-none transition-[background-color,scale] hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/60 active:scale-[0.97]\",\n        className,\n      )}\n    />\n  );\n}\n\nconst sideClass: Record<SheetSide, string> = {\n  right: \"inset-y-0 right-0 left-auto h-full w-3/4 max-w-sm border-l\",\n  left: \"inset-y-0 left-0 right-auto h-full w-3/4 max-w-sm border-r\",\n  top: \"inset-x-0 top-0 bottom-auto h-auto max-h-[85%] w-full border-b\",\n  bottom: \"inset-x-0 bottom-0 top-auto h-auto max-h-[85%] w-full border-t\",\n};\n\nexport interface SheetContentProps extends HTMLAttributes<HTMLDialogElement> {\n  side?: SheetSide;\n  /** Render the corner close button. */\n  showClose?: boolean;\n  /** Close when the backdrop is clicked. */\n  dismissible?: boolean;\n}\n\nexport function SheetContent({\n  side = \"right\",\n  showClose = true,\n  dismissible = true,\n  className,\n  children,\n  onKeyDown,\n  ...props\n}: SheetContentProps) {\n  const { open, setOpen, position, ids } = useSheet();\n  const ref = useRef<HTMLDialogElement>(null);\n  const returnTo = useRef<HTMLElement | null>(null);\n  const mounted = useRef(false);\n  const downOutside = useRef(false);\n  const modal = position === \"fixed\";\n\n  useEffect(() => {\n    const d = ref.current;\n    const firstRun = !mounted.current;\n    mounted.current = true;\n    if (!d) return;\n    if (open && !d.open) {\n      returnTo.current = document.activeElement as HTMLElement | null;\n      if (modal) d.showModal();\n      else {\n        d.open = true; // attribute only: no focus steal when rendered open on page load\n        if (!firstRun) (d.querySelector<HTMLElement>(\"[autofocus]\") ?? d.querySelector<HTMLElement>(FOCUSABLE))?.focus();\n      }\n    } else if (!open && d.open) {\n      d.close();\n      const back = returnTo.current?.isConnected && returnTo.current !== document.body ? returnTo.current : document.getElementById(ids.trigger);\n      back?.focus({ preventScroll: true });\n    }\n  }, [open, modal, ids.trigger]);\n\n  const outside = (e: MouseEvent<HTMLDialogElement>) => {\n    const r = e.currentTarget.getBoundingClientRect();\n    return e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom;\n  };\n\n  const onKey = (e: KeyboardEvent<HTMLDialogElement>) => {\n    onKeyDown?.(e);\n    if (modal || e.defaultPrevented) return;\n    // Contained mode is not a real modal: emulate Escape and a Tab focus trap.\n    if (e.key === \"Escape\") {\n      e.stopPropagation();\n      setOpen(false);\n    } else if (e.key === \"Tab\") {\n      const items = [...e.currentTarget.querySelectorAll<HTMLElement>(FOCUSABLE)];\n      const first = items[0];\n      const last = items[items.length - 1];\n      if (document.activeElement === (e.shiftKey ? first : last)) {\n        e.preventDefault();\n        (e.shiftKey ? last : first)?.focus();\n      }\n    }\n  };\n\n  return (\n    <>\n      <style href=\"ui-sheet\" precedence=\"default\">\n        {css}\n      </style>\n      {!modal && (\n        <div\n          aria-hidden\n          onClick={() => dismissible && setOpen(false)}\n          className={cn(\n            \"absolute inset-0 z-40 bg-black/50 backdrop-blur-[4px] transition-opacity duration-300 motion-reduce:transition-none\",\n            open ? \"opacity-100\" : \"pointer-events-none opacity-0\",\n          )}\n        />\n      )}\n      <dialog\n        {...props}\n        ref={ref}\n        id={ids.content}\n        data-side={side}\n        aria-labelledby={ids.title}\n        aria-describedby={ids.description}\n        aria-modal={modal ? undefined : true}\n        onCancel={(e) => {\n          e.preventDefault(); // let state drive the close so the exit animation runs\n          setOpen(false);\n        }}\n        onClose={() => open && setOpen(false)}\n        onKeyDown={onKey}\n        onPointerDown={(e) => (downOutside.current = outside(e))}\n        onClick={(e) => {\n          if (dismissible && modal && downOutside.current && outside(e)) setOpen(false);\n          downOutside.current = false;\n        }}\n        className={cn(\n          \"ui-sheet z-50 m-0 max-h-none max-w-none overflow-hidden border-border bg-card p-0 text-card-foreground shadow-2xl outline-none\",\n          modal ? \"fixed\" : \"absolute\",\n          sideClass[side],\n          className,\n          // A closed <dialog> must stay hidden even if a caller passes a display class like `flex`.\n          \"[&:not([open])]:hidden!\",\n        )}\n      >\n        <div className=\"ui-sheet-body flex h-full max-h-[inherit] flex-col gap-4 overflow-y-auto p-6\">{children}</div>\n        {showClose && (\n          <button\n            type=\"button\"\n            aria-label=\"Close\"\n            onClick={() => setOpen(false)}\n            className=\"absolute top-4 right-4 grid size-8 place-items-center rounded-full text-muted-foreground outline-none transition-[background-color,color,rotate] duration-200 hover:rotate-90 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60 motion-reduce:transition-none\"\n          >\n            <X className=\"size-4\" />\n          </button>\n        )}\n      </dialog>\n    </>\n  );\n}\n\nexport function SheetHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n  return <div {...props} className={cn(\"flex flex-col gap-1.5 pr-8\", className)} />;\n}\n\nexport function SheetTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {\n  const { ids } = useSheet();\n  return <h2 id={ids.title} {...props} className={cn(\"text-lg font-semibold leading-tight tracking-tight\", className)} />;\n}\n\nexport function SheetDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {\n  const { ids } = useSheet();\n  return <p id={ids.description} {...props} className={cn(\"text-sm text-muted-foreground\", className)} />;\n}\n\nexport function SheetFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n  return <div {...props} className={cn(\"mt-auto flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end\", className)} />;\n}\n\nexport function SheetClose({ className, onClick, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {\n  const { setOpen } = useSheet();\n  return (\n    <button\n      type=\"button\"\n      {...props}\n      onClick={(e) => {\n        onClick?.(e);\n        if (!e.defaultPrevented) setOpen(false);\n      }}\n      className={cn(\n        \"inline-flex h-10 items-center justify-center rounded-lg border border-border bg-transparent px-4 text-sm font-medium text-foreground outline-none transition-[background-color,scale] hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/60 active:scale-[0.97]\",\n        className,\n      )}\n    />\n  );\n}\n"}]}