{"name":"drawer","title":"Drawer","description":"A vaul-style bottom sheet with drag-to-dismiss, snap points, and swipe-to-open edge areas. Built on the @zag-js/drawer machine.","installCommand":"bunx marko-ui add drawer -y","usageTags":"<Drawer>","importSnippet":"import Drawer from \"@/components/ui/drawer/drawer.marko\";","usageSnippet":"<Drawer>\n  <@trigger|props|>\n    <button ...props>Open</button>\n  </@trigger>\n  <@title>Are you absolutely sure?</@title>\n  <@description>This action cannot be undone.</@description>\n  <@footer>\n    <button>Submit</button>\n  </@footer>\n</Drawer>","tags":["Drawer"],"parts":[{"name":"description"},{"name":"footer"},{"name":"title"},{"name":"trigger","param":"Record<string, unknown>","description":"Render-prop trigger: `<@trigger|props|><Button type=\"button\" ...props/></@trigger>`."}],"props":[{"name":"closeOnEscape","type":"boolean | undefined","required":false,"default":"true","description":"Whether to close the drawer when the escape key is pressed."},{"name":"closeOnInteractOutside","type":"boolean | undefined","required":false,"default":"true","description":"Whether to close the drawer when the outside is clicked."},{"name":"closeThreshold","type":"number","required":false,"default":"0.25","description":"The threshold distance for dismissing the drawer."},{"name":"defaultOpen","type":"boolean | undefined","required":false,"description":"The initial open state of the drawer."},{"name":"defaultSnapPoint","type":"SnapPoint | null | undefined","required":false,"default":"1","description":"The default snap point of the drawer."},{"name":"defaultTriggerValue","type":"string | null | undefined","required":false,"description":"The default trigger value (uncontrolled)."},{"name":"dir","type":"'ltr' | 'rtl' | undefined","required":false,"default":"ltr","description":"The document's text/writing direction."},{"name":"draggable","type":"boolean","required":false,"description":"Whether the content body itself is draggable, or only the grabber."},{"name":"finalFocusEl","type":"(() => MaybeElement) | undefined","required":false,"description":"Element to receive focus when the sheet is closed."},{"name":"getRootNode","type":"(() => ShadowRoot | Document | Node) | undefined","required":false,"description":"A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron."},{"name":"hideCloseTrigger","type":"boolean","required":false,"description":"Show the top-right close button."},{"name":"hideGrabber","type":"boolean","required":false,"description":"Hide the vaul-style grab handle (dragging the content body still works)."},{"name":"ids","type":"ElementIds","required":false,"description":"The ids of the elements in the drawer. Useful for composition."},{"name":"initialFocusEl","type":"(() => MaybeElement) | undefined","required":false,"description":"Element to receive focus when the sheet is opened."},{"name":"modal","type":"boolean | undefined","required":false,"default":"true","description":"Whether to prevent pointer interaction outside the element and hide all content below it."},{"name":"open","type":"boolean | undefined","required":false,"description":"Whether the drawer is open."},{"name":"preventDragOnScroll","type":"boolean","required":false,"default":"true","description":"Whether to prevent dragging on scrollable elements.\nWhen enabled, the sheet will not start dragging if the user is interacting with a scrollable element."},{"name":"preventScroll","type":"boolean | undefined","required":false,"default":"true","description":"Whether to prevent scrolling behind the sheet when it's opened"},{"name":"restoreFocus","type":"boolean | undefined","required":false,"default":"true","description":"Whether to restore focus to the element that had focus before the sheet was opened."},{"name":"role","type":"'dialog' | 'alertdialog' | undefined","required":false,"default":"dialog","description":"The sheet's role"},{"name":"snapPoint","type":"SnapPoint | null | undefined","required":false,"description":"The currently active snap point."},{"name":"snapPoints","type":"SnapPoint[] | undefined","required":false,"default":"[1]","description":"The snap points of the drawer.\nArray of numbers or strings representing the snap points."},{"name":"snapToSequentialPoints","type":"boolean | undefined","required":false,"default":"false","description":"Whether the drawer should snap to sequential points when swiping."},{"name":"stack","type":"DrawerStack | undefined","required":false,"description":"Optional external store for coordinating app-level drawer stack visuals\n(e.g. indent and background layers)."},{"name":"swipeArea","type":"boolean","required":false,"description":"Render an edge swipe area that opens the drawer by swiping inward."},{"name":"swipeDirection","type":"SwipeDirection | undefined","required":false,"default":"down","description":"The direction in which the drawer can be swiped."},{"name":"swipeVelocityThreshold","type":"number","required":false,"default":"700","description":"The threshold velocity (in pixels/s) for closing the drawer."},{"name":"trapFocus","type":"boolean | undefined","required":false,"default":"true","description":"Whether to trap focus inside the sheet when it's opened."},{"name":"triggerValue","type":"string | null | undefined","required":false,"description":"The value of the trigger that currently controls the drawer."}],"events":[{"name":"onEscapeKeyDown","arg":"KeyboardEvent","description":"Function called when the escape key is pressed"},{"name":"onFocusOutside","arg":"FocusOutsideEvent","description":"Function called when the focus is moved outside the component"},{"name":"onInteractOutside","arg":"InteractOutsideEvent","description":"Function called when an interaction happens outside the component"},{"name":"onOpenChange","arg":"OpenChangeDetails","description":"Function called when the open state changes."},{"name":"onPointerDownOutside","arg":"PointerDownOutsideEvent","description":"Function called when the pointer is pressed down outside the component"},{"name":"onRequestDismiss","arg":"LayerDismissEvent","description":"Function called when this layer is closed due to a parent layer being closed"},{"name":"onSnapPointChange","arg":"SnapPointChangeDetails","description":"Callback fired when the snap point changes."},{"name":"onTriggerValueChange","arg":"TriggerValueChangeDetails","description":"Callback when the active trigger value changes."},{"name":"openChange","arg":"boolean","description":"Sugar for onOpenChange — receives the plain boolean, enables `open:=state`."},{"name":"snapPointChange","arg":"drawerMachine.SnapPoint","description":"Sugar for onSnapPointChange — receives the plain snap point."},{"name":"triggerValueChange","arg":"string","description":"Sugar for onTriggerValueChange — receives the plain trigger value."}],"keyboard":[],"accessibilityNotes":[],"examples":[{"id":"drawer-basic","title":"Basic","source":"import Drawer from \"@marko-ui/shadcn/ui/drawer/drawer.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\n\n<Drawer>\n  <@trigger|props|>\n    <Button type=\"button\" ...props variant=\"outline\">Open drawer</Button>\n  </@trigger>\n  <@title>Open drawer</@title>\n  <@description>The default drawer: a title, a body and a footer.</@description>\n  <div class=\"p-4 text-sm text-muted-foreground\">\n    Drawer content goes in the tag's body.\n  </div>\n  <@footer>\n    <Button type=\"button\" variant=\"outline\">Cancel</Button>\n    <Button type=\"button\">Save</Button>\n  </@footer>\n</Drawer>","description":"The default drawer: a trigger, a title, the tag's body and a footer with the actions."},{"id":"drawer-demo","title":"Default","source":"import Drawer from \"@marko-ui/shadcn/ui/drawer/drawer.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\nimport RadioGroup from \"@marko-ui/shadcn/ui/radio-group/radio-group.marko\";\nimport Toaster from \"@marko-ui/shadcn/ui/toast/toast.marko\";\nimport { toast } from \"@marko-ui/shadcn/ui/toast/store.ts\";\n\nstatic const deliveryTimes = [\n  {\n    value: \"asap\",\n    id: \"delivery-asap\",\n    label: \"Standard delivery\",\n    description: \"25–35 min · Driver assigned now\",\n    badge: \"Fastest\",\n  },\n  {\n    value: \"5-00\",\n    id: \"delivery-5-00\",\n    label: \"5:00 PM – 5:15 PM\",\n    description: \"Prep starts at 4:45 PM\",\n  },\n  {\n    value: \"5-30\",\n    id: \"delivery-5-30\",\n    label: \"5:30 PM – 5:45 PM\",\n    description: \"Good if you're heading home\",\n  },\n  {\n    value: \"6-00\",\n    id: \"delivery-6-00\",\n    label: \"6:00 PM – 6:15 PM\",\n    description: \"Most popular · High demand\",\n  },\n  {\n    value: \"6-30\",\n    id: \"delivery-6-30\",\n    label: \"6:30 PM – 6:45 PM\",\n    description: \"Last slot before kitchen closes\",\n  },\n];\n\n<let/open=false>\n<let/deliveryTime=(\"asap\" as string | null)>\n<let/isMobile=false>\n\n<lifecycle\n  onMount() {\n    const query = window.matchMedia(\"(max-width: 767px)\");\n    const update = () => { isMobile = query.matches; };\n    update();\n    query.addEventListener(\"change\", update);\n    return { cleanup: () => query.removeEventListener(\"change\", update) };\n  }\n  onDestroy() { this.cleanup?.(); }\n/>\n\n<const/handleConfirm=() => {\n  const selected = deliveryTimes.find((time) => time.value === deliveryTime);\n  if (!selected) return;\n  open = false;\n  toast.message({ title: \"Delivery time confirmed\", description: selected.label });\n}/>\n\n<Drawer\n  open:=open\n  hideGrabber=!isMobile\n  swipeDirection=(isMobile ? \"down\" : \"end\")\n>\n  <@trigger|props|>\n    <Button type=\"button\" ...props variant=\"secondary\">Open Drawer</Button>\n  </@trigger>\n  <@title>Pick a delivery time</@title>\n  <@description>We'll prepare your order as soon as possible.</@description>\n  <div class=\"flex-1 overflow-y-auto p-4\">\n    <RadioGroup value:=deliveryTime class=\"gap-2\">\n      <for|time| of=deliveryTimes>\n        <@item value=time.value>\n          <div role=\"group\" data-slot=\"field\" data-orientation=\"horizontal\" class=\"mu-field group/field flex w-full mu-field-orientation-horizontal flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\">\n            <div data-slot=\"field-content\" class=\"mu-field-content group/field-content flex flex-1 flex-col leading-snug\">\n              <div data-slot=\"field-label\" class=\"mu-field-title flex w-fit items-center gap-2\">\n                ${time.label}\n                <if=time.badge>\n                  <span data-slot=\"badge\" data-variant=\"secondary\" class=\"mu-badge group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none mu-badge-variant-secondary\">${time.badge}</span>\n                </if>\n              </div>\n              <p data-slot=\"field-description\" class=\"mu-field-description leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance last:mt-0 nth-last-2:-mt-1 [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary\">${time.description}</p>\n            </div>\n          </div>\n        </@item>\n      </for>\n    </RadioGroup>\n  </div>\n  <@footer>\n    <Button type=\"button\" onClick=handleConfirm class=\"h-[34px]\">Confirm Delivery Time</Button>\n    <Button type=\"button\" variant=\"outline\" onClick=() => { open = false; }>Cancel</Button>\n  </@footer>\n</Drawer>\n<Toaster/>","description":"A drawer for editing profile details."},{"id":"drawer-no-footer","title":"Without footer","source":"import Drawer from \"@marko-ui/shadcn/ui/drawer/drawer.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\n\n<Drawer>\n  <@trigger|props|>\n    <Button type=\"button\" ...props variant=\"outline\">View notifications</Button>\n  </@trigger>\n  <@title>Notifications</@title>\n  <@description>You have 3 unread messages.</@description>\n</Drawer>","description":"The footer tag is optional — omit it for a drawer with no actions row."},{"id":"drawer-sides","title":"Position","source":"import Drawer from \"@marko-ui/shadcn/ui/drawer/drawer.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\n\n<Drawer swipeDirection=\"end\">\n  <@trigger|props|>\n    <Button type=\"button\" ...props variant=\"outline\">Open from the right</Button>\n  </@trigger>\n  <@title>Side drawer</@title>\n  <@description>Logical `end` resolves to right in LTR, left in RTL.</@description>\n</Drawer>","description":"Set `swipeDirection` to `up`, `right`, `down`, or `left` to change which edge the drawer opens from."},{"id":"drawer-swipe-handle","title":"Swipe handle","source":"import Drawer from \"@marko-ui/shadcn/ui/drawer/drawer.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\n\n<Drawer swipeDirection=\"up\">\n  <@trigger|props|>\n    <Button type=\"button\" ...props variant=\"outline\">Up</Button>\n  </@trigger>\n  <@title>Up drawer</@title>\n  <@description>Drawer with a swipe handle.</@description>\n  <div class=\"h-80 w-full bg-muted\"/>\n</Drawer>","description":"Our drawer always renders a drag grabber unless `hideGrabber` is set — there's no separate `showSwipeHandle` toggle to opt into, since the grabber is the default rather than an add-on."},{"id":"drawer-nested","title":"Nested","source":"import Drawer from \"@marko-ui/shadcn/ui/drawer/drawer.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\n\n<Drawer swipeDirection=\"down\">\n  <@trigger|props|>\n    <Button type=\"button\" ...props variant=\"outline\">Down</Button>\n  </@trigger>\n  <@title>Down drawer</@title>\n  <@description>Open another drawer from the same direction.</@description>\n  <div class=\"h-80 w-full bg-muted\"/>\n  <@footer>\n    <Drawer swipeDirection=\"down\">\n      <@trigger|props|>\n        <Button type=\"button\" ...props>Open nested drawer</Button>\n      </@trigger>\n      <@title>Nested drawer</@title>\n      <@description>The parent drawer stays mounted behind this one.</@description>\n      <div class=\"h-80 w-full bg-muted\"/>\n    </Drawer>\n  </@footer>\n</Drawer>","description":"Open a drawer from inside another drawer. Our @zag-js/drawer port stacks nested drawers as independent instances — it does not implement upstream's stacked-indent visual (the parent drawer does not scale/dim behind the frontmost one).","essential":true},{"id":"drawer-non-modal","title":"Non modal","source":"import Drawer from \"@marko-ui/shadcn/ui/drawer/drawer.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\n\n<Drawer modal=false closeOnInteractOutside=false swipeDirection=\"end\">\n  <@trigger|props|>\n    <Button type=\"button\" ...props variant=\"outline\">Non Modal</Button>\n  </@trigger>\n  <@title>Non Modal Drawer</@title>\n  <div class=\"h-80 w-full bg-muted\"/>\n</Drawer>","description":"Set `modal={false}` to allow interaction with the rest of the page while the drawer is open, and `closeOnInteractOutside={false}` (our equivalent of upstream's `disablePointerDismissal`) to keep outside presses from closing it."},{"id":"drawer-snap-points","title":"Snap points","source":"import Drawer from \"@marko-ui/shadcn/ui/drawer/drawer.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\n\n<Drawer snapPoints=[0.4, 0.7, 1] defaultSnapPoint=0.4>\n  <@trigger|props|>\n    <Button type=\"button\" ...props variant=\"secondary\">Open with snap points</Button>\n  </@trigger>\n  <@title>Snap points</@title>\n  <@description>Drag the handle to snap between 40%, 70% and full height.</@description>\n\n  <div class=\"h-[85vh] space-y-3\">\n    <p class=\"text-muted-foreground text-sm\">\n      The drawer resolves each snap point against the viewport and settles\n      on the nearest one when you release the drag.\n    </p>\n    <for|index| from=1 to=12>\n      <div class=\"bg-muted/50 rounded-md px-3 py-2 text-sm\">\n        Row ${index}\n      </div>\n    </for>\n  </div>\n</Drawer>","description":"Pass `snapPoints` and `defaultSnapPoint` to let the drawer settle at fixed heights as it's dragged."},{"id":"drawer-dialog","title":"Responsive dialog","source":"import Dialog from \"@marko-ui/shadcn/ui/dialog/dialog.marko\";\nimport Drawer from \"@marko-ui/shadcn/ui/drawer/drawer.marko\";\nimport Button from \"@marko-ui/shadcn/ui/button/button.marko\";\nimport TextInput from \"@marko-ui/shadcn/ui/input/input.marko\";\nimport Label from \"@marko-ui/shadcn/ui/label/label.marko\";\n\n<let/isDesktop=false>\n\n<lifecycle\n  onMount() {\n    const query = window.matchMedia(\"(min-width: 768px)\");\n    const update = () => { isDesktop = query.matches; };\n    update();\n    query.addEventListener(\"change\", update);\n    return { cleanup: () => query.removeEventListener(\"change\", update) };\n  }\n  onDestroy() { this.cleanup?.(); }\n/>\n\n<if=isDesktop>\n  <Dialog>\n    <@trigger|props|>\n      <Button type=\"button\" ...props variant=\"outline\">Edit Profile</Button>\n    </@trigger>\n    <@title>Edit profile</@title>\n    <@description>Make changes to your profile here. Click save when you're done.</@description>\n    <form class=\"grid items-start gap-6\">\n      <div class=\"grid gap-3\">\n        <Label for=\"drawer-dialog-email\">Email</Label>\n        <TextInput type=\"email\" id=\"drawer-dialog-email\" name=\"email\" value=\"shadcn@example.com\"/>\n      </div>\n      <div class=\"grid gap-3\">\n        <Label for=\"drawer-dialog-username\">Username</Label>\n        <TextInput id=\"drawer-dialog-username\" name=\"username\" value=\"@shadcn\"/>\n      </div>\n      <Button type=\"submit\">Save changes</Button>\n    </form>\n  </Dialog>\n</if>\n<else>\n  <Drawer>\n    <@trigger|props|>\n      <Button type=\"button\" ...props variant=\"outline\">Edit Profile</Button>\n    </@trigger>\n    <@title>Edit profile</@title>\n    <@description>Make changes to your profile here. Click save when you're done.</@description>\n    <form class=\"grid items-start gap-6\">\n      <div class=\"grid gap-3\">\n        <Label for=\"drawer-dialog-email-mobile\">Email</Label>\n        <TextInput type=\"email\" id=\"drawer-dialog-email-mobile\" name=\"email\" value=\"shadcn@example.com\"/>\n      </div>\n      <div class=\"grid gap-3\">\n        <Label for=\"drawer-dialog-username-mobile\">Username</Label>\n        <TextInput id=\"drawer-dialog-username-mobile\" name=\"username\" value=\"@shadcn\"/>\n      </div>\n    </form>\n    <@footer>\n      <Button type=\"submit\">Save changes</Button>\n    </@footer>\n  </Drawer>\n</else>","description":"Combine `Dialog` and `Drawer` to render a dialog on desktop and a drawer on mobile, switching on a `matchMedia` breakpoint check."}],"importInstallCommand":"bun add @marko-ui/shadcn marko-zag","body":{},"nativeAttributes":"div","composition":"`<Drawer>` is a single component, not a composed tree — `@trigger` is a render-prop slot (spread the given props onto your own trigger element), and `@title`/`@description`/`@footer` are named body slots plus the tag's own default body that render into the drawer's header, body, and footer regions. Omit any slot you don't need; `@title`/`@description` render nothing without a wrapping header until at least one is supplied, and `@footer` is fully optional (see the “Without footer” example below)."}
