{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"drawer","type":"registry:ui","title":"Drawer","description":"Panel that slides in from any edge \u2014 viewport-level, or attached inside a Card or layout pane.","package":"react-fancy","dependencies":["lucide-react"],"registryDependencies":["portal"],"files":[{"path":"components\/fancy\/drawer\/Drawer.context.ts","content":"import { createContext, useContext } from \"react\";\nimport type { DrawerContextValue } from \".\/Drawer.types\";\n\nexport const DrawerContext = createContext<DrawerContextValue | null>(null);\n\nexport function useDrawer(): DrawerContextValue {\n  const ctx = useContext(DrawerContext);\n  if (!ctx) {\n    throw new Error(\"Drawer compound components must be used within <Drawer>\");\n  }\n  return ctx;\n}\n","type":"registry:ui","target":"components\/fancy\/drawer\/Drawer.context.ts"},{"path":"components\/fancy\/drawer\/Drawer.tsx","content":"import { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { Portal } from \"..\/Portal\";\nimport { DrawerContext } from \".\/Drawer.context\";\nimport { DrawerHeader } from \".\/DrawerHeader\";\nimport { DrawerBody } from \".\/DrawerBody\";\nimport { DrawerFooter } from \".\/DrawerFooter\";\nimport { DrawerContainer } from \".\/DrawerContainer\";\nimport { useFocusTrap } from \"..\/..\/hooks\/use-focus-trap\";\nimport { useEscapeKey } from \"..\/..\/hooks\/use-escape-key\";\nimport { useAnimation } from \"..\/..\/hooks\/use-animation\";\nimport type { DrawerProps, DrawerSide } from \".\/Drawer.types\";\n\n\/**\n * `size` addresses the drawer's own axis: width on the horizontal edges,\n * height on the vertical ones. Keeping one scale across both means `size=\"lg\"`\n * does not have to be re-learned when a drawer moves from the side to the\n * bottom.\n *\/\nconst WIDTH = {\n  sm: \"w-64\",\n  md: \"w-80\",\n  lg: \"w-96\",\n  xl: \"w-[32rem]\",\n  full: \"w-full\",\n} as const;\n\nconst HEIGHT = {\n  sm: \"h-32\",\n  md: \"h-64\",\n  lg: \"h-96\",\n  xl: \"h-[32rem]\",\n  full: \"h-full\",\n} as const;\n\nconst ANCHOR: Record<DrawerSide, string> = {\n  left: \"inset-y-0 left-0\",\n  right: \"inset-y-0 right-0\",\n  top: \"inset-x-0 top-0\",\n  bottom: \"inset-x-0 bottom-0\",\n};\n\nconst ENTER: Record<DrawerSide, string> = {\n  left: \"fancy-slide-in-left\",\n  right: \"fancy-slide-in-right\",\n  top: \"fancy-slide-in-top\",\n  bottom: \"fancy-slide-in-bottom\",\n};\n\nconst EXIT: Record<DrawerSide, string> = {\n  left: \"fancy-slide-out-left\",\n  right: \"fancy-slide-out-right\",\n  top: \"fancy-slide-out-top\",\n  bottom: \"fancy-slide-out-bottom\",\n};\n\n\/** The panel's edge shadow reads as depth away from the anchored edge. *\/\nconst EDGE: Record<DrawerSide, string> = {\n  left: \"border-r\",\n  right: \"border-l\",\n  top: \"border-b\",\n  bottom: \"border-t\",\n};\n\nfunction DrawerRoot({\n  children,\n  open,\n  onClose,\n  side = \"right\",\n  size = \"md\",\n  attach = \"viewport\",\n  backdrop = true,\n  dismissOnBackdrop = true,\n  dismissOnEscape = true,\n  className,\n  ...rest\n}: DrawerProps) {\n  const panelRef = useRef<HTMLDivElement>(null);\n  const horizontal = side === \"left\" || side === \"right\";\n  const isViewport = attach === \"viewport\";\n\n  const close = useCallback(() => onClose(), [onClose]);\n  useEscapeKey(close, open && dismissOnEscape);\n\n  \/\/ A container-attached drawer is a panel, not a dialog \u2014 trapping focus in it\n  \/\/ would strand keyboard users inside a Card. Only the viewport form is modal.\n  useFocusTrap(panelRef, open && isViewport);\n\n  useEffect(() => {\n    if (!open || !isViewport) return;\n    const original = document.body.style.overflow;\n    document.body.style.overflow = \"hidden\";\n    return () => {\n      document.body.style.overflow = original;\n    };\n  }, [open, isViewport]);\n\n  const { mounted, className: animClass, ref: animRef } = useAnimation({\n    open,\n    enterClass: ENTER[side],\n    exitClass: EXIT[side],\n  });\n\n  const ctx = useMemo(() => ({ open, close, side }), [open, close, side]);\n\n  if (!mounted) return null;\n\n  const frame = (\n    <div\n      data-react-fancy-drawer-frame=\"\"\n      className={cn(isViewport ? \"fixed inset-0 z-50\" : \"absolute inset-0 z-20\", !backdrop && \"pointer-events-none\")}\n    >\n      {backdrop ? (\n        <div\n          data-react-fancy-drawer-backdrop=\"\"\n          className={cn(\n            \"absolute inset-0 bg-black\/40 transition-opacity\",\n            isViewport && \"backdrop-blur-sm\",\n            open ? \"opacity-100\" : \"opacity-0\",\n          )}\n          onClick={dismissOnBackdrop ? close : undefined}\n          aria-hidden=\"true\"\n        \/>\n      ) : null}\n\n      <div\n        ref={(node) => {\n          panelRef.current = node;\n          (animRef as React.MutableRefObject<HTMLElement | null>).current = node;\n        }}\n        data-react-fancy-drawer=\"\"\n        data-side={side}\n        role=\"dialog\"\n        aria-modal={isViewport || undefined}\n        {...rest}\n        className={cn(\n          \"pointer-events-auto absolute flex flex-col border-zinc-200 bg-white shadow-xl dark:border-zinc-700 dark:bg-zinc-900\",\n          ANCHOR[side],\n          EDGE[side],\n          horizontal ? WIDTH[size] : HEIGHT[size],\n          \/\/ Cross-axis fills the anchor; the main axis is capped so a viewport\n          \/\/ drawer can never grow past the screen on a small device.\n          horizontal ? \"h-full\" : \"w-full\",\n          isViewport && (horizontal ? \"max-w-[90vw]\" : \"max-h-[90vh]\"),\n          !isViewport && (horizontal ? \"max-w-full\" : \"max-h-full\"),\n          animClass,\n          className,\n        )}\n      >\n        {children}\n      <\/div>\n    <\/div>\n  );\n\n  return (\n    <DrawerContext.Provider value={ctx}>\n      {isViewport ? <Portal>{frame}<\/Portal> : frame}\n    <\/DrawerContext.Provider>\n  );\n}\n\nexport const Drawer = Object.assign(DrawerRoot, {\n  Header: DrawerHeader,\n  Body: DrawerBody,\n  Footer: DrawerFooter,\n  Container: DrawerContainer,\n});\n","type":"registry:ui","target":"components\/fancy\/drawer\/Drawer.tsx"},{"path":"components\/fancy\/drawer\/Drawer.types.ts","content":"import type { HTMLAttributes, ReactNode } from \"react\";\n\n\/** Which edge the drawer is anchored to, and therefore slides in from. *\/\nexport type DrawerSide = \"left\" | \"right\" | \"top\" | \"bottom\";\n\n\/**\n * How large the drawer is along its own axis.\n *\n * `size` controls WIDTH for `left`\/`right` and HEIGHT for `top`\/`bottom` \u2014 the\n * cross-axis always fills its anchor. One scale, two meanings, so `size=\"md\"`\n * reads the same whichever edge you attach to.\n *\/\nexport type DrawerSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n\/**\n * Where the drawer is anchored.\n *\n * - `viewport` (default) \u2014 portalled, `fixed`, covers the screen, locks body\n *   scroll and traps focus. The classic app-level drawer.\n * - `container` \u2014 `absolute` inside the nearest positioned ancestor, no portal,\n *   no scroll lock, no focus trap. Attach it to a Card, a layout region, or the\n *   shell around a prompt input and it stays inside that box.\n *\/\nexport type DrawerAttach = \"viewport\" | \"container\";\n\nexport interface DrawerContextValue {\n  open: boolean;\n  close: () => void;\n  side: DrawerSide;\n}\n\nexport interface DrawerProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n  children: ReactNode;\n  open: boolean;\n  onClose: () => void;\n  \/** Edge to anchor to. Default `right`. *\/\n  side?: DrawerSide;\n  \/** Extent along the drawer's own axis. Default `md`. *\/\n  size?: DrawerSize;\n  \/** Viewport-level or scoped to a container. Default `viewport`. *\/\n  attach?: DrawerAttach;\n  \/** Render the scrim behind the panel. Default `true`. *\/\n  backdrop?: boolean;\n  \/** Clicking the scrim closes. Default `true`. *\/\n  dismissOnBackdrop?: boolean;\n  \/** Escape closes. Default `true`. *\/\n  dismissOnEscape?: boolean;\n  className?: string;\n}\n\nexport interface DrawerHeaderProps {\n  children: ReactNode;\n  \/** Show the close button. Default `true`. *\/\n  closable?: boolean;\n  className?: string;\n}\n\nexport interface DrawerBodyProps {\n  children: ReactNode;\n  className?: string;\n}\n\nexport interface DrawerFooterProps {\n  children: ReactNode;\n  className?: string;\n}\n\nexport interface DrawerContainerProps extends HTMLAttributes<HTMLDivElement> {\n  children: ReactNode;\n  className?: string;\n}\n","type":"registry:ui","target":"components\/fancy\/drawer\/Drawer.types.ts"},{"path":"components\/fancy\/drawer\/DrawerBody.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\nimport type { DrawerBodyProps } from \".\/Drawer.types\";\n\nexport function DrawerBody({ children, className }: DrawerBodyProps) {\n  return (\n    <div\n      data-react-fancy-drawer-body=\"\"\n      className={cn(\"min-h-0 flex-1 overflow-auto px-4 py-3 text-zinc-700 dark:text-zinc-300\", className)}\n    >\n      {children}\n    <\/div>\n  );\n}\n\nDrawerBody.displayName = \"DrawerBody\";\n","type":"registry:ui","target":"components\/fancy\/drawer\/DrawerBody.tsx"},{"path":"components\/fancy\/drawer\/DrawerContainer.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\nimport type { DrawerContainerProps } from \".\/Drawer.types\";\n\n\/**\n * The anchor for `<Drawer attach=\"container\">`.\n *\n * A container-attached drawer positions itself `absolute`, which resolves\n * against the nearest *positioned* ancestor \u2014 so without `relative` somewhere\n * above it, the drawer silently escapes to the viewport and looks like it\n * ignored `attach` entirely. This wrapper supplies `relative` + `overflow-hidden`\n * so the panel slides within the box rather than out of it.\n *\n *   <Drawer.Container className=\"h-80 rounded-xl border\">\n *     <YourContent \/>\n *     <Drawer attach=\"container\" side=\"bottom\" open={open} onClose={close}>\n *       <Drawer.Body>Filters<\/Drawer.Body>\n *     <\/Drawer>\n *   <\/Drawer.Container>\n *\n * Any element works as long as it is positioned \u2014 this is the shortcut, not a\n * requirement. Wrapping a Card, a layout pane, or a prompt-input shell in\n * `relative overflow-hidden` does the same job.\n *\/\nexport function DrawerContainer({ children, className, ...rest }: DrawerContainerProps) {\n  return (\n    <div\n      data-react-fancy-drawer-container=\"\"\n      {...rest}\n      className={cn(\"relative overflow-hidden\", className)}\n    >\n      {children}\n    <\/div>\n  );\n}\n\nDrawerContainer.displayName = \"DrawerContainer\";\n","type":"registry:ui","target":"components\/fancy\/drawer\/DrawerContainer.tsx"},{"path":"components\/fancy\/drawer\/DrawerFooter.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\nimport type { DrawerFooterProps } from \".\/Drawer.types\";\n\nexport function DrawerFooter({ children, className }: DrawerFooterProps) {\n  return (\n    <div\n      data-react-fancy-drawer-footer=\"\"\n      className={cn(\n        \"flex shrink-0 items-center justify-end gap-2 border-t border-zinc-200 px-4 py-3 dark:border-zinc-700\",\n        className,\n      )}\n    >\n      {children}\n    <\/div>\n  );\n}\n\nDrawerFooter.displayName = \"DrawerFooter\";\n","type":"registry:ui","target":"components\/fancy\/drawer\/DrawerFooter.tsx"},{"path":"components\/fancy\/drawer\/DrawerHeader.tsx","content":"import { X } from \"lucide-react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { useDrawer } from \".\/Drawer.context\";\nimport type { DrawerHeaderProps } from \".\/Drawer.types\";\n\nexport function DrawerHeader({ children, closable = true, className }: DrawerHeaderProps) {\n  const { close } = useDrawer();\n\n  return (\n    <div\n      data-react-fancy-drawer-header=\"\"\n      className={cn(\n        \"flex shrink-0 items-center justify-between border-b border-zinc-200 px-4 py-3 dark:border-zinc-700\",\n        className,\n      )}\n    >\n      <div className=\"text-sm font-semibold text-zinc-900 dark:text-zinc-100\">{children}<\/div>\n      {closable ? (\n        <button\n          type=\"button\"\n          onClick={close}\n          className=\"rounded-lg p-1 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300\"\n          aria-label=\"Close\"\n        >\n          <X size={18} \/>\n        <\/button>\n      ) : null}\n    <\/div>\n  );\n}\n\nDrawerHeader.displayName = \"DrawerHeader\";\n","type":"registry:ui","target":"components\/fancy\/drawer\/DrawerHeader.tsx"},{"path":"components\/fancy\/drawer\/index.ts","content":"export { Drawer } from \".\/Drawer\";\nexport { useDrawer } from \".\/Drawer.context\";\nexport type {\n  DrawerProps,\n  DrawerHeaderProps,\n  DrawerBodyProps,\n  DrawerFooterProps,\n  DrawerContainerProps,\n  DrawerContextValue,\n  DrawerSide,\n  DrawerSize,\n  DrawerAttach,\n} from \".\/Drawer.types\";\n","type":"registry:ui","target":"components\/fancy\/drawer\/index.ts"}]}