{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sheet",
  "title": "Sheet",
  "description": "Bottom sheet you can grab mid-flight: it tracks the finger 1:1, rubber-bands at the top, and lands where the release velocity was heading.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/sheet.tsx",
      "content": "import { useEffect, useId, useRef, useState } from \"react\"\nimport type * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion, type PanInfo } from \"motion/react\"\nimport { SPRING_DEFAULT, SPRING_MOMENTUM, projectMomentum } from \"@/registry/miseui/lib/motion\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\n/** Past this share of the sheet's height, the projected rest point means \"let go\". */\nconst DISMISS_FRACTION = 0.5\n/** A flick faster than this dismisses regardless of distance; it is clearly a throw. */\nconst DISMISS_VELOCITY_PX_PER_S = 1200\n/** Resistance above the open position (rubber-band) and freedom below it. */\nconst DRAG_ELASTIC = { top: 0.08, bottom: 1 }\nconst SCRIM_OPACITY = 0.32\n\ntype SheetRest = \"open\" | \"closed\"\n\ninterface SheetGesture {\n  /** Drag offset from the open position in px; positive is downward. */\n  offset: number\n  /** Release velocity in px/s; positive is downward. */\n  velocity: number\n  /** Rendered sheet height in px. */\n  height: number\n}\n\n/**\n * Where the sheet should come to rest after a release. Momentum is projected\n * first (Apple's decay), so a flick lands where the finger was going, not where it let go.\n */\nexport function resolveSheetRest({ offset, velocity, height }: SheetGesture): SheetRest {\n  if (velocity < 0) return \"open\"\n  if (velocity > DISMISS_VELOCITY_PX_PER_S) return \"closed\"\n  const projected = offset + projectMomentum(velocity)\n  return projected > height * DISMISS_FRACTION ? \"closed\" : \"open\"\n}\n\ninterface SheetProps extends Omit<React.ComponentProps<typeof motion.div>, \"title\" | \"children\"> {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  title: string\n  children: React.ReactNode\n}\n\nfunction Sheet({ open, onOpenChange, title, className, children, ...props }: SheetProps) {\n  const reduceMotion = useReducedMotion()\n  const titleId = useId()\n  const panelRef = useRef<HTMLDivElement>(null)\n  /** True from a drag release until the exit finishes, so only a thrown sheet gets the bouncier spring. */\n  const [releasedWithMomentum, setReleasedWithMomentum] = useState(false)\n\n  useEffect(() => {\n    if (!open) return\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") onOpenChange(false)\n    }\n    document.addEventListener(\"keydown\", onKeyDown)\n    return () => document.removeEventListener(\"keydown\", onKeyDown)\n  }, [open, onOpenChange])\n\n  const onDragEnd = (_event: unknown, info: PanInfo) => {\n    const height = panelRef.current?.offsetHeight ?? 0\n    const rest = resolveSheetRest({ offset: info.offset.y, velocity: info.velocity.y, height })\n    setReleasedWithMomentum(true)\n    if (rest === \"closed\") onOpenChange(false)\n  }\n\n  const hidden = reduceMotion ? { opacity: 0 } : { y: \"100%\" }\n  const visible = reduceMotion ? { opacity: 1 } : { y: 0 }\n  const transition = releasedWithMomentum ? SPRING_MOMENTUM : SPRING_DEFAULT\n\n  return (\n    <AnimatePresence onExitComplete={() => setReleasedWithMomentum(false)}>\n      {open && (\n        <>\n          <motion.div\n            data-testid=\"sheet-scrim\"\n            aria-hidden=\"true\"\n            className=\"fixed inset-0 z-40 bg-foreground\"\n            initial={{ opacity: 0 }}\n            animate={{ opacity: SCRIM_OPACITY }}\n            exit={{ opacity: 0 }}\n            transition={SPRING_DEFAULT}\n            onClick={() => onOpenChange(false)}\n          />\n          <motion.div\n            ref={panelRef}\n            role=\"dialog\"\n            aria-modal=\"true\"\n            aria-labelledby={titleId}\n            data-slot=\"sheet\"\n            className={cn(\n              \"fixed inset-x-0 bottom-0 z-50 mx-auto max-w-lg rounded-t-3xl bg-background p-6 pb-10 shadow-[0_-12px_40px_-16px_rgb(0_0_0/0.35)]\",\n              \"touch-none\",\n              className,\n            )}\n            initial={hidden}\n            animate={visible}\n            exit={hidden}\n            transition={transition}\n            drag={reduceMotion ? false : \"y\"}\n            dragConstraints={{ top: 0, bottom: 0 }}\n            dragElastic={DRAG_ELASTIC}\n            dragSnapToOrigin\n            onDragEnd={onDragEnd}\n            {...props}\n          >\n            <div aria-hidden=\"true\" className=\"mx-auto mb-4 h-1.5 w-10 rounded-full bg-muted-foreground/30\" />\n            <h2 id={titleId} className=\"text-title text-xl\">\n              {title}\n            </h2>\n            <div className=\"mt-3\">{children}</div>\n          </motion.div>\n        </>\n      )}\n    </AnimatePresence>\n  )\n}\n\nexport { Sheet, type SheetProps }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "transitions"
  ],
  "type": "registry:ui"
}