{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"sheet","type":"registry:component","title":"Sheet","description":"Drag it down or flick it away; pull it up and it pushes back.","author":"Yash Bavadiya <https://xevrion.dev>","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"components/sheet.tsx","type":"registry:component","content":"\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// The iOS drawer curve: fast out of the gate, long gentle settle. Closing is\n// quicker than opening because exits should get out of the way.\nconst EASE = \"cubic-bezier(0.32, 0.72, 0, 1)\";\nconst OPEN = `transform 500ms ${EASE}`;\nconst CLOSE = `transform 300ms ${EASE}, visibility 0s 300ms`;\nconst FADE_OPEN = `opacity 500ms ${EASE}`;\nconst FADE_CLOSE = `opacity 300ms ${EASE}`;\n\n// A release faster than this (px per ms) dismisses however short the drag,\n// so a quick flick is enough.\nconst FLICK_VELOCITY = 0.11;\n// Otherwise the sheet must be dragged past this share of its height.\nconst DISMISS_DISTANCE = 0.25;\n\nexport function Sheet({\n  open,\n  onOpenChange,\n  label,\n  children,\n}: {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  label: string;\n  children: React.ReactNode;\n}) {\n  const sheetRef = useRef<HTMLDivElement>(null);\n  const backdropRef = useRef<HTMLDivElement>(null);\n  const drag = useRef<{ startY: number; startTime: number; dy: number }>(\n    null,\n  );\n\n  useEffect(() => {\n    if (!open) return;\n    const previous = document.activeElement as HTMLElement | null;\n    sheetRef.current?.focus({ preventScroll: true });\n    const onKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") onOpenChange(false);\n    };\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => {\n      document.removeEventListener(\"keydown\", onKeyDown);\n      previous?.focus({ preventScroll: true });\n    };\n  }, [open, onOpenChange]);\n\n  // During a drag the styles are written straight to the elements, so moving\n  // the pointer never re-renders React. On release they're set to exactly\n  // what React renders for the resulting state, and the transition picks up\n  // from wherever the finger let go.\n  const settle = (close: boolean) => {\n    const sheet = sheetRef.current;\n    const backdrop = backdropRef.current;\n    if (!sheet || !backdrop) return;\n    sheet.style.transition = close ? CLOSE : OPEN;\n    sheet.style.transform = close ? \"translateY(100%)\" : \"translateY(0)\";\n    backdrop.style.transition = close ? FADE_CLOSE : FADE_OPEN;\n    backdrop.style.opacity = close ? \"0\" : \"1\";\n    if (close) onOpenChange(false);\n  };\n\n  return (\n    <>\n      <div\n        ref={backdropRef}\n        aria-hidden\n        className={cn(\n          \"fixed inset-0 z-50 bg-black/40\",\n          !open && \"pointer-events-none\",\n        )}\n        style={{ opacity: open ? 1 : 0, transition: open ? FADE_OPEN : FADE_CLOSE }}\n        onClick={() => onOpenChange(false)}\n      />\n      <div\n        ref={sheetRef}\n        role=\"dialog\"\n        aria-modal\n        aria-label={label}\n        tabIndex={-1}\n        inert={!open}\n        className={cn(\n          \"fixed inset-x-0 bottom-0 z-50 mx-auto w-full max-w-md cursor-grab touch-none rounded-t-[28px] bg-background px-6 pt-3 pb-8 shadow-[0_0_0_1px_var(--border),0_-8px_40px_oklch(0_0_0/0.15)] outline-hidden select-none active:cursor-grabbing motion-reduce:transition-none!\",\n          // The surface continues below the screen edge, so pulling the sheet\n          // up reveals more sheet rather than a gap.\n          \"after:absolute after:inset-x-0 after:top-full after:h-[50vh] after:bg-background after:shadow-[1px_0_0_var(--border),-1px_0_0_var(--border)]\",\n          open ? \"visible\" : \"invisible\",\n        )}\n        style={{\n          transform: open ? \"translateY(0)\" : \"translateY(100%)\",\n          transition: open ? OPEN : CLOSE,\n        }}\n        onPointerDown={(e) => {\n          if (e.button !== 0) return;\n          if ((e.target as HTMLElement).closest(\"button, a, input, textarea\")) {\n            return;\n          }\n          drag.current = {\n            startY: e.clientY,\n            startTime: performance.now(),\n            dy: 0,\n          };\n          e.currentTarget.setPointerCapture(e.pointerId);\n          e.currentTarget.style.transition = \"none\";\n          if (backdropRef.current) backdropRef.current.style.transition = \"none\";\n        }}\n        onPointerMove={(e) => {\n          if (!drag.current) return;\n          let dy = e.clientY - drag.current.startY;\n          // Past the top it still gives, but less the further you pull, like\n          // stretching something that wants to snap back.\n          if (dy < 0) dy = -Math.pow(-dy, 0.7);\n          drag.current.dy = dy;\n          e.currentTarget.style.transform = `translateY(${dy}px)`;\n          const progress = Math.max(dy, 0) / e.currentTarget.offsetHeight;\n          if (backdropRef.current) {\n            backdropRef.current.style.opacity = String(1 - progress);\n          }\n        }}\n        onPointerUp={(e) => {\n          if (!drag.current) return;\n          const { dy, startTime } = drag.current;\n          drag.current = null;\n          const velocity = dy / (performance.now() - startTime);\n          const height = e.currentTarget.offsetHeight;\n          settle(dy > height * DISMISS_DISTANCE || velocity > FLICK_VELOCITY);\n        }}\n        onPointerCancel={() => {\n          if (!drag.current) return;\n          drag.current = null;\n          settle(false);\n        }}\n      >\n        <div className=\"mx-auto mb-5 h-1.5 w-10 rounded-full bg-border\" />\n        {children}\n      </div>\n    </>\n  );\n}\n\nexport default function SheetDemo() {\n  const [open, setOpen] = useState(false);\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        onClick={() => setOpen(true)}\n        className=\"h-10 rounded-full bg-muted px-4 text-sm font-medium shadow-raised transition-[scale] duration-150 ease-out active:scale-[0.96] motion-reduce:transition-none\"\n      >\n        Open sheet\n      </button>\n      <Sheet open={open} onOpenChange={setOpen} label=\"Lab notes\">\n        <h2 className=\"text-base font-medium\">Lab notes</h2>\n        <p className=\"mt-1.5 text-sm text-pretty text-muted-foreground\">\n          Drag this sheet down to close it. A quick flick works too, and\n          dragging it up pushes back.\n        </p>\n        <button\n          type=\"button\"\n          onClick={() => setOpen(false)}\n          className=\"mt-6 h-11 w-full rounded-full bg-foreground text-sm font-medium text-background transition-[scale] duration-150 ease-out active:scale-[0.96] motion-reduce:transition-none\"\n        >\n          Done\n        </button>\n      </Sheet>\n    </>\n  );\n}\n"}],"cssVars":{"light":{"shadow-raised":"0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.06), 0 6px 16px -6px oklch(0 0 0 / 0.12)"},"dark":{"shadow-raised":"0 0 0 1px oklch(1 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px -6px oklch(0 0 0 / 0.6)"},"theme":{"shadow-raised":"var(--shadow-raised)"}},"categories":["feedback"],"docs":"From ui lab: https://lab.xevrion.dev/lab/sheet"}