{
  "schemaVersion": 1,
  "name": "drawer",
  "useFor": "Use on phones for a short task or a choice that belongs to the page under it (pick a shipping address, review a cart). Set direction for top, left or right; bottom is the default. Mark a scrolling list inside it with data-drawer-no-drag so scrolling does not drag the drawer.",
  "constraints": [
    "A drawer on desktop for a long form (use a sheet or dialog)",
    "Content that needs the full height of the screen",
    "A drawer opened from another drawer"
  ],
  "documentation": "",
  "source": "Extension from opencommerce-agentcard-web src/app/globals.css (.win) and sheet.tsx",
  "exports": [
    "Drawer",
    "DrawerClose",
    "DrawerContent",
    "DrawerDescription",
    "DrawerFooter",
    "DrawerHeader",
    "DrawerOverlay",
    "DrawerPortal",
    "DrawerTitle",
    "DrawerTrigger"
  ],
  "client": true,
  "docsUrl": "https://opencommerce-ds.vercel.app/components/drawer",
  "markdownUrl": "https://opencommerce-ds.vercel.app/r/drawer.md",
  "examples": [
    {
      "path": "examples/drawer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from \"@/components/ui/drawer\";\n\nconst ADDRESSES = [\n  { id: \"home\", name: \"Home\", line: \"18 Harbor St, Portland, OR 97209\" },\n  { id: \"office\", name: \"Office\", line: \"400 Market St, Floor 6, San Francisco, CA 94105\" },\n  { id: \"locker\", name: \"Parcel locker\", line: \"Locker 22, Union Station, Portland, OR 97209\" },\n];\n\nfunction ShipTo({ defaultOpen = false, direction }: { defaultOpen?: boolean; direction?: \"bottom\" | \"top\" | \"left\" | \"right\" }) {\n  const [chosen, setChosen] = React.useState(\"home\");\n  const [open, setOpen] = React.useState(defaultOpen);\n  const current = ADDRESSES.find((a) => a.id === chosen)!;\n  return (\n    <div className=\"grid justify-items-start gap-3\">\n      <Drawer open={open} onOpenChange={setOpen} direction={direction}>\n        <DrawerTrigger asChild>\n          <Button variant=\"secondary\">Ship to: {current.name}</Button>\n        </DrawerTrigger>\n        <DrawerContent>\n          <DrawerHeader>\n            <DrawerTitle>Where should it ship?</DrawerTitle>\n            <DrawerDescription>The agent uses this address at checkout for order ord_8KQ2.</DrawerDescription>\n          </DrawerHeader>\n          <div role=\"radiogroup\" aria-label=\"Shipping address\" className=\"grid gap-2 px-6 pb-4\" data-drawer-no-drag>\n            {ADDRESSES.map((a) => (\n              <button\n                key={a.id}\n                type=\"button\"\n                role=\"radio\"\n                aria-checked={chosen === a.id}\n                onClick={() => setChosen(a.id)}\n                className=\"grid gap-0.5 rounded-control border border-hairline px-4 py-3 text-left outline-none transition-[background-color,border-color] duration-150 ease-out hover:bg-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring aria-checked:border-line-brand aria-checked:bg-blue-5 motion-reduce:transition-none\"\n              >\n                <span className=\"text-body-sm font-medium text-fg\">{a.name}</span>\n                <span className=\"text-caption text-fg-muted\">{a.line}</span>\n              </button>\n            ))}\n          </div>\n          <DrawerFooter>\n            <DrawerClose asChild>\n              <Button variant=\"secondary\">Cancel</Button>\n            </DrawerClose>\n            <DrawerClose asChild>\n              <Button>Use this address</Button>\n            </DrawerClose>\n          </DrawerFooter>\n        </DrawerContent>\n      </Drawer>\n      <p role=\"status\" className=\"text-body-sm text-fg-muted\">\n        Shipping to {current.line}.\n      </p>\n    </div>\n  );\n}\n\nexport function Site() {\n  return <ShipTo />;\n}\n\nexport function Right() {\n  return <ShipTo direction=\"right\" />;\n}\n\nexport function Open() {\n  return (\n    <div className=\"h-130\">\n      <ShipTo defaultOpen />\n    </div>\n  );\n}\n",
      "exports": [
        "Site",
        "Right",
        "Open"
      ],
      "requires": [
        "drawer",
        "button"
      ],
      "packages": [],
      "previews": [
        {
          "name": "Site",
          "url": "https://opencommerce-ds.vercel.app/preview/drawer/Site"
        },
        {
          "name": "Right",
          "url": "https://opencommerce-ds.vercel.app/preview/drawer/Right"
        },
        {
          "name": "Open",
          "url": "https://opencommerce-ds.vercel.app/preview/drawer/Open"
        }
      ]
    }
  ],
  "type": "ui",
  "title": "Drawer",
  "description": "A window pulled from an edge with a handle, that can be dragged away.",
  "surface": "site",
  "kinds": [
    "window"
  ],
  "requires": [
    "theme",
    "surface",
    "utils"
  ],
  "packages": [
    "@radix-ui/react-dialog"
  ],
  "files": [
    {
      "path": "components/ui/drawer.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DrawerPrimitive from \"@radix-ui/react-dialog\";\n\nimport { usePortalContainer } from \"@/lib/surface\";\nimport { cn } from \"@/lib/utils\";\n\n// A panel pulled from an edge, drawn as the brand's sheet (sheet.tsx): one of its windows, white with a 16 percent ink\n// edge, 12px corners and the soft long drop, floating 8px in from the screen's edge over the ink scrim, with a 40 by 4px\n// handle in the 35 percent ink where a window's bar would be. It slides in from its edge as it fades (300ms ease-out on\n// translate) and can be dragged away: a flick (faster than 0.11px per ms) or a pull past a third of its size sends it\n// off its edge (200ms ease-out) and closes it; a shorter pull springs back (300ms ease-out); pulling the other way meets\n// growing resistance instead of a wall. The drag follows the pointer off the panel (pointer capture) and ignores a\n// second finger. Reduced motion keeps only the fade on the way in.\ntype Direction = \"bottom\" | \"top\" | \"left\" | \"right\";\n\nconst DrawerContext = React.createContext<{ direction: Direction; setOpen: (open: boolean) => void }>({ direction: \"bottom\", setOpen: () => {} });\n\nfunction Drawer({ open: openProp, defaultOpen = false, onOpenChange, direction = \"bottom\", ...props }: React.ComponentProps<typeof DrawerPrimitive.Root> & { direction?: Direction }) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultOpen);\n  const open = openProp ?? uncontrolled;\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (openProp === undefined) setUncontrolled(next);\n      onOpenChange?.(next);\n    },\n    [openProp, onOpenChange],\n  );\n  return (\n    <DrawerContext.Provider value={{ direction, setOpen }}>\n      <DrawerPrimitive.Root data-slot=\"drawer\" open={open} onOpenChange={setOpen} {...props} />\n    </DrawerContext.Provider>\n  );\n}\n\nfunction DrawerTrigger(props: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {\n  return <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />;\n}\n\nfunction DrawerPortal(props: React.ComponentProps<typeof DrawerPrimitive.Portal>) {\n  const container = usePortalContainer();\n  return <DrawerPrimitive.Portal data-slot=\"drawer-portal\" container={container} {...props} />;\n}\n\nfunction DrawerClose(props: React.ComponentProps<typeof DrawerPrimitive.Close>) {\n  return <DrawerPrimitive.Close data-slot=\"drawer-close\" {...props} />;\n}\n\nfunction DrawerOverlay({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {\n  return <DrawerPrimitive.Overlay data-slot=\"drawer-overlay\" className={cn(\"fixed inset-0 z-50 bg-scrim backdrop-blur-xs transition-opacity duration-300 ease-out starting:opacity-0\", className)} {...props} />;\n}\n\nconst PLACE: Record<Direction, string> = {\n  bottom: \"inset-x-2 bottom-2 max-h-[85dvh] flex-col starting:translate-y-full\",\n  top: \"inset-x-2 top-2 max-h-[85dvh] flex-col-reverse starting:-translate-y-full\",\n  right: \"inset-y-2 right-2 w-[min(24rem,calc(100vw-1rem))] flex-row-reverse starting:translate-x-full\",\n  left: \"inset-y-2 left-2 w-[min(24rem,calc(100vw-1rem))] flex-row starting:-translate-x-full\",\n};\n\n// Where a pull closes the drawer: toward its own edge (positive for bottom and right).\nconst AXIS: Record<Direction, { axis: \"x\" | \"y\"; sign: 1 | -1 }> = { bottom: { axis: \"y\", sign: 1 }, top: { axis: \"y\", sign: -1 }, right: { axis: \"x\", sign: 1 }, left: { axis: \"x\", sign: -1 } };\nconst VELOCITY = 0.11;\nconst SKIP = \"button, a, input, textarea, select, [role=slider], [contenteditable=true], [data-drawer-no-drag]\";\n\nfunction useDrag(direction: Direction, close: () => void) {\n  const panel = React.useRef<HTMLDivElement>(null);\n  const drag = React.useRef<{ id: number; start: number; at: number; time: number } | null>(null);\n  const { axis, sign } = AXIS[direction];\n  const translate = (px: number) => (axis === \"y\" ? `translateY(${px}px)` : `translateX(${px}px)`);\n\n  const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {\n    if (drag.current || event.button !== 0) return; // one pointer only\n    if ((event.target as Element).closest(SKIP)) return;\n    if (window.getSelection()?.toString()) return;\n    const start = axis === \"y\" ? event.clientY : event.clientX;\n    drag.current = { id: event.pointerId, start, at: 0, time: performance.now() };\n    event.currentTarget.setPointerCapture(event.pointerId);\n    if (panel.current) panel.current.style.transition = \"none\";\n  };\n\n  const onPointerMove = (event: React.PointerEvent<HTMLDivElement>) => {\n    const d = drag.current;\n    if (!d || d.id !== event.pointerId || !panel.current) return;\n    const raw = (axis === \"y\" ? event.clientY : event.clientX) - d.start;\n    const toward = raw * sign;\n    // Toward the edge it follows the pointer; away from it, resistance grows with distance.\n    const moved = toward >= 0 ? toward : -Math.sqrt(-toward) * 2;\n    d.at = moved;\n    panel.current.style.transform = translate(moved * sign);\n  };\n\n  const end = (event: React.PointerEvent<HTMLDivElement>) => {\n    const d = drag.current;\n    if (!d || d.id !== event.pointerId || !panel.current) return;\n    drag.current = null;\n    const el = panel.current;\n    const size = axis === \"y\" ? el.offsetHeight : el.offsetWidth;\n    const velocity = Math.abs(d.at) / Math.max(1, performance.now() - d.time);\n    const dismiss = d.at > 0 && (d.at > size / 3 || velocity > VELOCITY);\n    const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    if (dismiss && reduce) {\n      close();\n      return;\n    }\n    // Off its edge (and then closed), or back into place.\n    el.style.transition = dismiss ? \"transform 200ms var(--ds-easing-out)\" : \"transform 300ms var(--ds-easing-out)\";\n    el.style.transform = dismiss ? translate((size + 16) * sign) : \"\";\n    const done = (e: TransitionEvent) => {\n      if (e.target !== el || e.propertyName !== \"transform\") return;\n      el.removeEventListener(\"transitionend\", done);\n      if (dismiss) close();\n      else el.style.transition = \"\";\n    };\n    el.addEventListener(\"transitionend\", done);\n  };\n\n  return { panel, handlers: { onPointerDown, onPointerMove, onPointerUp: end, onPointerCancel: end } };\n}\n\nfunction DrawerContent({ className, children, ...props }: React.ComponentProps<typeof DrawerPrimitive.Content>) {\n  const { direction, setOpen } = React.useContext(DrawerContext);\n  const { panel, handlers } = useDrag(direction, () => setOpen(false));\n  const vertical = direction === \"bottom\" || direction === \"top\";\n  return (\n    <DrawerPortal>\n      <DrawerOverlay />\n      <DrawerPrimitive.Content\n        ref={panel}\n        data-slot=\"drawer-content\"\n        data-direction={direction}\n        className={cn(\n          \"group/drawer fixed z-50 flex touch-none overflow-hidden rounded-panel border border-line-window bg-surface text-fg shadow-floating outline-none transition-[translate,opacity] duration-300 ease-out starting:opacity-0 motion-reduce:starting:translate-x-0 motion-reduce:starting:translate-y-0\",\n          PLACE[direction],\n          className,\n        )}\n        {...handlers}\n        {...props}\n      >\n        <div aria-hidden data-slot=\"drawer-handle\" className={cn(\"shrink-0 rounded-pill bg-line-strong\", vertical ? \"mx-auto my-3 h-1 w-10\" : \"mx-3 my-auto h-10 w-1\")} />\n        <div className=\"flex min-h-0 min-w-0 flex-1 touch-auto flex-col overflow-auto\">{children}</div>\n      </DrawerPrimitive.Content>\n    </DrawerPortal>\n  );\n}\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"drawer-header\"\n      className={cn(\"flex flex-col gap-2 px-6 pt-2 pb-4 group-data-[direction=bottom]/drawer:text-center group-data-[direction=top]/drawer:text-center md:group-data-[direction=bottom]/drawer:text-left md:group-data-[direction=top]/drawer:text-left\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"drawer-footer\" className={cn(\"mt-auto flex flex-col gap-3 border-t border-line px-6 py-4 sm:flex-row sm:justify-end\", className)} {...props} />;\n}\n\n// 24px display at 500, tracked tight, like the sheet's title.\nfunction DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) {\n  return <DrawerPrimitive.Title data-slot=\"drawer-title\" className={cn(\"font-display text-h3 leading-display font-medium tracking-tight text-fg\", className)} {...props} />;\n}\n\nfunction DrawerDescription({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Description>) {\n  return <DrawerPrimitive.Description data-slot=\"drawer-description\" className={cn(\"text-body-sm leading-relaxed text-fg-muted\", className)} {...props} />;\n}\n\nexport { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger };\n"
    }
  ]
}
