{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drawer",
  "title": "Drawer",
  "description": "A draggable bottom or side sheet with rubber-band physics, flick-to-dismiss, scrim, and scroll lock.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/drawer/drawer.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, type PanInfo } from \"framer-motion\";\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nexport type DrawerSide = \"bottom\" | \"right\";\n\nexport type DrawerProps = {\n  /** Controlled open state. */\n  open: boolean;\n  /** Called when the drawer requests to open or close. */\n  onOpenChange: (open: boolean) => void;\n  /** Side the drawer slides in from. */\n  side?: DrawerSide;\n  /** Optional accessible title rendered at the top. */\n  title?: React.ReactNode;\n  /** Extra classes for the panel. */\n  className?: string;\n  children?: React.ReactNode;\n};\n\nconst PANEL_BY_SIDE: Record<DrawerSide, string> = {\n  bottom:\n    \"inset-x-0 bottom-0 max-h-[90vh] rounded-t-2xl border-t border-border\",\n  right:\n    \"inset-y-0 right-0 w-full max-w-md rounded-l-2xl border-l border-border\",\n};\n\nconst CLOSE_OFFSET = 120;\nconst CLOSE_VELOCITY = 600;\n\nfunction useMounted() {\n  const [mounted, setMounted] = React.useState(false);\n  React.useEffect(() => setMounted(true), []);\n  return mounted;\n}\n\nconst Drawer = React.forwardRef<HTMLDivElement, DrawerProps>(\n  (\n    { open, onOpenChange, side = \"bottom\", title, className, children },\n    ref,\n  ) => {\n    const mounted = useMounted();\n    const isBottom = side === \"bottom\";\n\n    React.useEffect(() => {\n      if (!open) return;\n      const onKey = (e: KeyboardEvent) => {\n        if (e.key === \"Escape\") onOpenChange(false);\n      };\n      document.addEventListener(\"keydown\", onKey);\n      const prevOverflow = document.body.style.overflow;\n      document.body.style.overflow = \"hidden\";\n      return () => {\n        document.removeEventListener(\"keydown\", onKey);\n        document.body.style.overflow = prevOverflow;\n      };\n    }, [open, onOpenChange]);\n\n    const handleDragEnd = (\n      _e: MouseEvent | TouchEvent | PointerEvent,\n      info: PanInfo,\n    ) => {\n      const offset = isBottom ? info.offset.y : info.offset.x;\n      const velocity = isBottom ? info.velocity.y : info.velocity.x;\n      if (offset > CLOSE_OFFSET || velocity > CLOSE_VELOCITY) {\n        onOpenChange(false);\n      }\n    };\n\n    if (!mounted) return null;\n\n    const hidden = isBottom ? { y: \"100%\" } : { x: \"100%\" };\n    const shown = isBottom ? { y: 0 } : { x: 0 };\n\n    return createPortal(\n      <AnimatePresence>\n        {open ? (\n          <div className=\"fixed inset-0 z-modal\">\n            <motion.div\n              aria-hidden\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              onClick={() => onOpenChange(false)}\n              className=\"absolute inset-0 bg-foreground/40 backdrop-blur-sm\"\n            />\n            <motion.div\n              ref={ref}\n              role=\"dialog\"\n              aria-modal=\"true\"\n              data-slot=\"drawer\"\n              initial={hidden}\n              animate={shown}\n              exit={hidden}\n              transition={{\n                type: \"spring\",\n                damping: 32,\n                stiffness: 320,\n                mass: 0.9,\n              }}\n              drag={isBottom ? \"y\" : \"x\"}\n              dragConstraints={{ top: 0, bottom: 0, left: 0, right: 0 }}\n              dragElastic={\n                isBottom ? { top: 0, bottom: 0.6 } : { left: 0, right: 0.6 }\n              }\n              onDragEnd={handleDragEnd}\n              className={`absolute flex flex-col bg-card p-5 text-card-foreground shadow-xl ${PANEL_BY_SIDE[side]} ${className ?? \"\"}`}\n            >\n              {isBottom ? (\n                <div className=\"mx-auto mb-4 h-1.5 w-12 shrink-0 cursor-grab rounded-full bg-muted-foreground/30 active:cursor-grabbing\" />\n              ) : null}\n              {title ? (\n                <h2 className=\"mb-3 text-lg font-semibold text-foreground\">\n                  {title}\n                </h2>\n              ) : null}\n              <div className=\"overflow-y-auto\">{children}</div>\n            </motion.div>\n          </div>\n        ) : null}\n      </AnimatePresence>,\n      document.body,\n    );\n  },\n);\nDrawer.displayName = \"Drawer\";\n\nexport { Drawer };\n",
      "type": "registry:ui",
      "target": "components/godui/drawer.tsx"
    }
  ],
  "type": "registry:ui"
}
