{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drawer",
  "title": "Drawer",
  "description": "A panel that slides in from an edge for a side task, so the page you came from stays where you left it.",
  "dependencies": [
    "radix-ui",
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/sheet.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/drawer.tsx",
      "content": "import type * as React from \"react\"\nimport { createContext, useCallback, useContext, useMemo, useRef, useState } from \"react\"\nimport { Dialog as DialogPrimitive } from \"radix-ui\"\nimport { motion, useDragControls, useReducedMotion, type PanInfo } from \"motion/react\"\nimport { X } from \"lucide-react\"\nimport { resolveSheetRest } from \"@/registry/miseui/ui/sheet\"\nimport {\n  SHEET_SURFACE_CLASS,\n  drawerContentClass,\n  drawerScrimClass,\n  type DrawerEffect,\n  type DrawerSide,\n  type DrawerVariant,\n} from \"@/registry/miseui/ui/drawer-classes\"\nimport {\n  DrawerBody,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n} from \"@/registry/miseui/ui/drawer-parts\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ninterface DrawerContextValue {\n  variant: DrawerVariant\n  side: DrawerSide\n  effect: DrawerEffect | undefined\n  setOpen: (open: boolean) => void\n}\n\nconst DrawerContext = createContext<DrawerContextValue>({\n  variant: \"plain\",\n  side: \"right\",\n  effect: undefined,\n  setOpen: () => {},\n})\n\n/** Same resistance as Sheet: firm above the open position, free below it. */\nconst SHEET_DRAG_ELASTIC = { top: 0.08, bottom: 1 }\n\ntype DrawerProps = Omit<React.ComponentProps<typeof DialogPrimitive.Root>, \"modal\"> & {\n  /** `plain` full height at the edge, `floating` inset with radius, `split` no scrim so the page stays usable, `sheet` bottom with a grab handle. */\n  variant?: DrawerVariant\n  /** `sheet` always uses the bottom edge. */\n  side?: DrawerSide\n  /** `push` brings the panel into focus while the page recedes behind a deep blur. */\n  effect?: DrawerEffect\n}\n\nfunction Drawer({\n  variant = \"plain\",\n  side = \"right\",\n  effect,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  ...props\n}: DrawerProps) {\n  const [uncontrolled, setUncontrolled] = useState(defaultOpen)\n  const current = open ?? uncontrolled\n  const setOpen = useCallback(\n    (next: boolean) => {\n      setUncontrolled(next)\n      onOpenChange?.(next)\n    },\n    [onOpenChange],\n  )\n  const resolvedSide = variant === \"sheet\" ? \"bottom\" : side\n  const context = useMemo(\n    () => ({ variant, side: resolvedSide, effect, setOpen }),\n    [variant, resolvedSide, effect, setOpen],\n  )\n  return (\n    <DrawerContext.Provider value={context}>\n      {/* A modal dialog would trap focus and hide the page from assistive tech, the opposite of what split is for. */}\n      <DialogPrimitive.Root open={current} onOpenChange={setOpen} modal={variant !== \"split\"} {...props} />\n    </DrawerContext.Provider>\n  )\n}\n\nfunction DrawerTrigger(props: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n  return <DialogPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />\n}\n\nfunction DrawerClose(props: React.ComponentProps<typeof DialogPrimitive.Close>) {\n  return <DialogPrimitive.Close data-slot=\"drawer-close\" {...props} />\n}\n\nfunction DrawerCornerClose() {\n  return (\n    <DialogPrimitive.Close\n      data-slot=\"drawer-close\"\n      className={cn(\n        \"absolute top-2 right-2 inline-flex size-10 items-center justify-center rounded-md text-muted-foreground outline-none\",\n        \"transition-colors duration-(--duration-fast) ease-(--ease-mise) hover:bg-accent hover:text-accent-foreground\",\n        \"focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n      )}\n    >\n      <X aria-hidden=\"true\" className=\"size-4\" />\n      <span className=\"sr-only\">Close</span>\n    </DialogPrimitive.Close>\n  )\n}\n\nfunction SheetSurface({ className, children }: { className?: string; children: React.ReactNode }) {\n  const { setOpen } = useContext(DrawerContext)\n  const reduceMotion = useReducedMotion()\n  const dragControls = useDragControls()\n  const surfaceRef = useRef<HTMLDivElement>(null)\n  const onDragEnd = (_event: unknown, info: PanInfo) => {\n    const height = surfaceRef.current?.offsetHeight ?? 0\n    if (resolveSheetRest({ offset: info.offset.y, velocity: info.velocity.y, height }) === \"closed\") setOpen(false)\n  }\n  return (\n    <motion.div\n      ref={surfaceRef}\n      className={cn(\"relative\", SHEET_SURFACE_CLASS, className)}\n      drag={reduceMotion ? false : \"y\"}\n      dragControls={dragControls}\n      // Only the handle starts a drag, so text in the sheet can still be selected and scrolled.\n      dragListener={false}\n      dragConstraints={{ top: 0, bottom: 0 }}\n      dragElastic={SHEET_DRAG_ELASTIC}\n      dragSnapToOrigin\n      onDragEnd={onDragEnd}\n    >\n      <div\n        aria-hidden=\"true\"\n        data-slot=\"drawer-handle\"\n        className=\"flex h-10 shrink-0 cursor-grab touch-none items-center justify-center active:cursor-grabbing\"\n        onPointerDown={(event) => dragControls.start(event)}\n      >\n        <span className=\"h-1 w-10 rounded-full bg-muted-foreground/30\" />\n      </div>\n      {children}\n    </motion.div>\n  )\n}\n\ntype DrawerContentProps = React.ComponentProps<typeof DialogPrimitive.Content> & {\n  /** Keep this on `split`, where a click outside does not close the panel. */\n  showCloseButton?: boolean\n}\n\nfunction DrawerContent({ className, children, showCloseButton = true, onInteractOutside, ...props }: DrawerContentProps) {\n  const { variant, side, effect } = useContext(DrawerContext)\n  const isSplit = variant === \"split\"\n  const isSheet = variant === \"sheet\"\n  const inner = (\n    <>\n      {children}\n      {showCloseButton && <DrawerCornerClose />}\n    </>\n  )\n  return (\n    <DialogPrimitive.Portal>\n      {!isSplit && <DialogPrimitive.Overlay data-slot=\"drawer-overlay\" className={drawerScrimClass(effect)} />}\n      <DialogPrimitive.Content\n        data-slot=\"drawer\"\n        data-variant={variant}\n        data-side={side}\n        data-effect={effect}\n        className={cn(drawerContentClass(variant, side, effect), !isSheet && className)}\n        // In split the page beside the panel is the point, so working there must not dismiss it.\n        onInteractOutside={isSplit ? (event) => event.preventDefault() : onInteractOutside}\n        {...props}\n      >\n        {isSheet ? <SheetSurface className={className}>{inner}</SheetSurface> : inner}\n      </DialogPrimitive.Content>\n    </DialogPrimitive.Portal>\n  )\n}\n\nexport {\n  Drawer,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerContent,\n  DrawerHeader,\n  DrawerBody,\n  DrawerFooter,\n  DrawerTitle,\n  DrawerDescription,\n  type DrawerProps,\n  type DrawerContentProps,\n  type DrawerEffect,\n  type DrawerSide,\n  type DrawerVariant,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/drawer-classes.ts",
      "content": "export type DrawerSide = \"left\" | \"right\" | \"top\" | \"bottom\"\nexport type DrawerVariant = \"plain\" | \"floating\" | \"split\" | \"sheet\"\nexport type DrawerEffect = \"push\"\n\nconst EDGE_PLACEMENT: Record<DrawerSide, string> = {\n  left: \"inset-y-0 left-0 w-[min(24rem,calc(100vw-2rem))] border-r\",\n  right: \"inset-y-0 right-0 w-[min(24rem,calc(100vw-2rem))] border-l\",\n  top: \"inset-x-0 top-0 max-h-[85dvh] border-b\",\n  bottom: \"inset-x-0 bottom-0 max-h-[85dvh] border-t\",\n}\n\nconst FLOATING_PLACEMENT: Record<DrawerSide, string> = {\n  left: \"inset-y-2 left-2 w-[min(24rem,calc(100vw-1rem))]\",\n  right: \"inset-y-2 right-2 w-[min(24rem,calc(100vw-1rem))]\",\n  top: \"inset-x-2 top-2 max-h-[85dvh]\",\n  bottom: \"inset-x-2 bottom-2 max-h-[85dvh]\",\n}\n\n/**\n * The enter and exit values stay unqualified so the motion-reduce overrides win on source order;\n * only the running animation reads them, and data-state decides which one runs.\n */\nconst SLIDE_CLASS: Record<DrawerSide, string> = {\n  left: \"slide-in-from-left slide-out-to-left motion-reduce:slide-in-from-left-0 motion-reduce:slide-out-to-left-0\",\n  right: \"slide-in-from-right slide-out-to-right motion-reduce:slide-in-from-right-0 motion-reduce:slide-out-to-right-0\",\n  top: \"slide-in-from-top slide-out-to-top motion-reduce:slide-in-from-top-0 motion-reduce:slide-out-to-top-0\",\n  bottom:\n    \"slide-in-from-bottom slide-out-to-bottom motion-reduce:slide-in-from-bottom-0 motion-reduce:slide-out-to-bottom-0\",\n}\n\nconst PLAIN_MOTION_CLASS =\n  \"fade-in-0 fade-out-0 duration-(--duration-base) ease-(--ease-mise) data-[state=closed]:animate-out data-[state=open]:animate-in\"\n\n/** The materialize approach on a moving panel: it comes into focus as it arrives, and CSS lets Radix Presence hold the exit. */\nconst PUSH_MOTION_CLASS = [\n  \"fade-in-0 fade-out-0 blur-in-sm blur-out-sm will-change-[transform,opacity,filter]\",\n  \"duration-(--duration-slow) ease-(--ease-mise-out) data-[state=closed]:duration-(--duration-base)\",\n  \"data-[state=closed]:animate-out data-[state=open]:animate-in\",\n  \"motion-reduce:blur-in-0 motion-reduce:blur-out-0\",\n  \"[@media(prefers-reduced-transparency:reduce)]:blur-in-0 [@media(prefers-reduced-transparency:reduce)]:blur-out-0\",\n].join(\" \")\n\n/** A registry component cannot own the app root, so the page cannot really recede; the scrim fakes that depth. */\nconst PUSH_SCRIM_CLASS = [\n  \"bg-background/40 backdrop-blur-md backdrop-brightness-75 backdrop-saturate-50\",\n  \"[@media(prefers-reduced-transparency:reduce)]:bg-background/90 [@media(prefers-reduced-transparency:reduce)]:backdrop-filter-none\",\n].join(\" \")\n\nconst PLAIN_SCRIM_CLASS = \"bg-background/80 backdrop-blur-sm\"\n\nconst SHEET_SURFACE_CLASS =\n  \"flex max-h-[85dvh] w-full flex-col rounded-t-2xl border-t border-border bg-background pb-[env(safe-area-inset-bottom)] shadow-[0_-12px_40px_-16px_rgb(0_0_0/0.35)]\"\n\nexport function drawerContentClass(variant: DrawerVariant, side: DrawerSide, effect: DrawerEffect | undefined): string {\n  const motionClass = `${effect === \"push\" ? PUSH_MOTION_CLASS : PLAIN_MOTION_CLASS} ${SLIDE_CLASS[side]}`\n  if (variant === \"sheet\") return `fixed inset-x-0 bottom-0 z-50 mx-auto w-full max-w-lg outline-none ${motionClass}`\n  const surface = \"fixed z-50 flex flex-col bg-background outline-none\"\n  if (variant === \"floating\") {\n    return `${surface} ${FLOATING_PLACEMENT[side]} rounded-xl border border-border shadow-2xl ${motionClass}`\n  }\n  if (variant === \"split\") return `${surface} ${EDGE_PLACEMENT[side]} border-border shadow-sm ${motionClass}`\n  return `${surface} ${EDGE_PLACEMENT[side]} border-border shadow-xl ${motionClass}`\n}\n\nexport function drawerScrimClass(effect: DrawerEffect | undefined): string {\n  return [\n    \"fixed inset-0 z-50 fade-in-0 fade-out-0 duration-(--duration-base) ease-(--ease-mise)\",\n    \"data-[state=closed]:animate-out data-[state=open]:animate-in\",\n    effect === \"push\" ? PUSH_SCRIM_CLASS : PLAIN_SCRIM_CLASS,\n  ].join(\" \")\n}\n\nexport { SHEET_SURFACE_CLASS }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/drawer-parts.tsx",
      "content": "import type * as React from \"react\"\nimport { Dialog as DialogPrimitive } from \"radix-ui\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"drawer-header\" className={cn(\"grid shrink-0 gap-1 p-4 pe-12\", className)} {...props} />\n}\n\nfunction DrawerBody({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-body\"\n      className={cn(\"min-h-0 flex-1 overflow-y-auto px-4 pb-4 text-sm/6\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-footer\"\n      className={cn(\"flex shrink-0 flex-col-reverse gap-2 border-t border-border p-4 sm:flex-row sm:justify-end\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction DrawerTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n  return <DialogPrimitive.Title data-slot=\"drawer-title\" className={cn(\"text-[15px] font-semibold\", className)} {...props} />\n}\n\nfunction DrawerDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {\n  return (\n    <DialogPrimitive.Description\n      data-slot=\"drawer-description\"\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  )\n}\n\nexport { DrawerHeader, DrawerBody, DrawerFooter, DrawerTitle, DrawerDescription }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "overlays"
  ],
  "type": "registry:ui"
}