{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drawer-header",
  "title": "Header drawer",
  "description": "Styled drawer: the Header set. 5 decorative drawer variations (TitleDrawer, StickyDrawer, FooterDrawer, HandleDrawer, DividerDrawer) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/drawer-header.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Header-themed drawer family: 5 drawers opening from the bottom, all carrying the\n   same slide-up mechanic from the bottom edge; the difference is ONLY in the layout\n   of the HEADER/FOOTER/HANDLE band. The panel is always clear/opaque (bg-card).\n   Self-contained - no radix, no vaul, no portal. A fixed inset-0 backdrop + a fixed\n   panel pinned to the bottom edge (z-50). Internal state (defaultOpen) or\n   controlled (open + onOpenChange). A backdrop click / Escape / the close button\n   closes it. The panel takes focus on open. The slide runs through\n   AnimatePresence; under reduced-motion there is no slide, only a fade. Color comes\n   ONLY from tokens, with transparency via color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype Edge = \"right\" | \"left\" | \"top\" | \"bottom\"\n\n/* Panel height = size. Being a bottom drawer, height scales rather than width. */\nconst heightSize: Record<StyledSize, string> = {\n  sm: \"h-[35vh]\",\n  md: \"h-[50vh]\",\n  lg: \"h-[68vh]\",\n  xl: \"h-[82vh]\",\n}\n\nconst edgeAnchor: Record<Edge, string> = {\n  right: \"inset-y-0 right-0\",\n  left: \"inset-y-0 left-0\",\n  top: \"inset-x-0 top-0\",\n  bottom: \"inset-x-0 bottom-0\",\n}\n\n/* The slideOffset mechanism from essentials is preserved: the panel slides in from\n   100 percent outside its own edge. */\nfunction slideOffset(edge: Edge) {\n  switch (edge) {\n    case \"right\":\n      return { x: \"100%\" }\n    case \"left\":\n      return { x: \"-100%\" }\n    case \"top\":\n      return { y: \"-100%\" }\n    case \"bottom\":\n      return { y: \"100%\" }\n  }\n}\n\ninterface DrawerProps {\n  size?: StyledSize\n  trigger?: React.ReactNode\n  title?: React.ReactNode\n  children?: React.ReactNode\n  className?: string\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (o: boolean) => void\n}\n\nfunction useControllableOpen(\n  open: boolean | undefined,\n  defaultOpen: boolean | undefined,\n  onOpenChange: ((o: boolean) => void) | undefined\n) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultOpen ?? false)\n  const isControlled = open !== undefined\n  const value = isControlled ? open : uncontrolled\n  const setValue = React.useCallback(\n    (next: boolean) => {\n      if (!isControlled) setUncontrolled(next)\n      onOpenChange?.(next)\n    },\n    [isControlled, onOpenChange]\n  )\n  return [value, setValue] as const\n}\n\n/* Enough dummy text to scroll. Even when the content is short the panel is\n   always scrollable. */\nconst filler = [\n  \"Drawers rise from the bottom edge and hold secondary tasks without leaving the current page.\",\n  \"Everything inside scrolls on its own, so a long form or a dense list never pushes the banded header or footer out of reach.\",\n  \"Use the banded layout to separate a persistent title from the body, and keep the primary actions anchored where the eye expects them.\",\n  \"Group related fields, review a summary, or read through a policy without losing the surrounding context.\",\n  \"Press Escape or click outside the panel at any time to dismiss it and return to the page underneath.\",\n  \"The panel stays opaque on the ai2 card surface, so text keeps its contrast in both the light and dark themes.\",\n  \"A grab handle at the top hints that the panel came up from below and can be sent back down again.\",\n]\n\n/* Sample sections for the divider-separated body (DividerDrawer). */\nconst dividerSections = [\n  {\n    heading: \"Overview\",\n    body: \"A short summary sits at the top so the reader knows what the panel is for before scrolling.\",\n  },\n  {\n    heading: \"Details\",\n    body: \"Each block is separated by a token divider, which keeps long content readable without extra chrome.\",\n  },\n  {\n    heading: \"Notes\",\n    body: \"Dividers span the full width of the panel, so the sections line up with the banded header above.\",\n  },\n  {\n    heading: \"Next steps\",\n    body: \"Close the panel when you are done, or keep scrolling to review everything one more time.\",\n  },\n]\n\nconst triggerBtn =\n  \"inline-flex h-9 shrink-0 select-none items-center justify-center gap-2 rounded-lg border border-border bg-secondary px-4 text-sm font-medium text-secondary-foreground outline-none whitespace-nowrap transition-[color,background-color] duration-(--motion-fast) ease-(--motion-ease) hover:bg-surface-3 focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_i]:text-base [&_i]:leading-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\"\n\n/* The close button: pinned to the top right of the panel. A focus ring + icon\n   alignment (svg + i). */\nconst closeBtn =\n  \"absolute right-4 top-4 z-20 inline-flex size-7 items-center justify-center rounded-md text-muted-foreground outline-none transition-[color,background-color] duration-(--motion-fast) ease-(--motion-ease) hover:bg-surface-3 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_i]:text-base [&_i]:leading-none [&_svg]:size-4 [&_svg]:shrink-0\"\n\n/* Footer aksiyon butonlari. */\nconst footerGhostBtn =\n  \"inline-flex h-9 select-none items-center justify-center rounded-lg border border-border bg-transparent px-4 text-sm font-medium text-foreground outline-none whitespace-nowrap transition-[color,background-color] duration-(--motion-fast) ease-(--motion-ease) hover:bg-surface-3 focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n\nconst footerPrimaryBtn =\n  \"inline-flex h-9 select-none items-center justify-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground outline-none whitespace-nowrap transition-[color,background-color] duration-(--motion-fast) ease-(--motion-ease) hover:bg-[color-mix(in_oklab,var(--color-primary)_88%,transparent)] focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n\nconst solidBackdrop = \"bg-[color-mix(in_oklab,var(--color-foreground)_50%,transparent)]\"\n\n/* The top handle bar: a token grab handle pointing at the panel coming from below. */\nconst grabHandle =\n  \"mx-auto mt-3 h-1.5 w-12 shrink-0 rounded-full bg-[color-mix(in_oklab,var(--color-foreground)_25%,transparent)]\"\n\n/* Varsayilan govde: children + dolgu paragraflari. */\nfunction defaultBody(children: React.ReactNode) {\n  return (\n    <div className=\"space-y-4 px-6 py-5 text-sm text-muted-foreground\">\n      {children ? <div className=\"text-foreground\">{children}</div> : null}\n      {filler.map((line, i) => (\n        <p key={i}>{line}</p>\n      ))}\n    </div>\n  )\n}\n\ninterface ShellProps {\n  props: DrawerProps\n  /** Baslik bandini varyanta gore cizer. */\n  renderHeader: (title: React.ReactNode) => React.ReactNode\n  /** When true the header stays sticky top-0 inside the scroll container. */\n  stickyHeader?: boolean\n  /** A fixed action footer at the bottom. */\n  renderFooter?: () => React.ReactNode\n  /** Govde icerigi (varsayilan defaultBody). */\n  renderBody?: (children: React.ReactNode) => React.ReactNode\n  /** Baslik bandinin ustunde gorunur grab handle cubugu. */\n  showHandle?: boolean\n}\n\nfunction DrawerShell({\n  props,\n  renderHeader,\n  stickyHeader,\n  renderFooter,\n  renderBody = defaultBody,\n  showHandle,\n}: ShellProps) {\n  const { size = \"md\", trigger, title, children, className } = props\n  const reduce = useReducedMotion()\n  const [isOpen, setOpen] = useControllableOpen(\n    props.open,\n    props.defaultOpen,\n    props.onOpenChange\n  )\n  const panelRef = React.useRef<HTMLDivElement>(null)\n\n  React.useEffect(() => {\n    if (!isOpen) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") setOpen(false)\n    }\n    window.addEventListener(\"keydown\", onKey)\n    return () => window.removeEventListener(\"keydown\", onKey)\n  }, [isOpen, setOpen])\n\n  /* This family uses only the bottom edge; the slideOffset mechanism is preserved. */\n  const edge: Edge = \"bottom\"\n  const sizeClass = cn(\"w-full\", heightSize[size])\n  const enter = slideOffset(edge)\n\n  const handleNode = showHandle ? (\n    <span data-slot=\"styled-drawer-handle\" className={grabHandle} aria-hidden />\n  ) : null\n  const headerNode = title ? renderHeader(title) : null\n  const bodyInner = renderBody(children)\n  const footerNode = renderFooter?.()\n\n  return (\n    <div data-slot=\"styled-drawer\" className=\"inline-flex\">\n      {/* The ARIA state lives on the REAL trigger. There used to be a\n          span role=\"button\" tabIndex={-1} wrapper, which produced TWO buttons\n          in the AX tree: one carrying the state (not focusable) and one that\n          took focus (stateless). A screen-reader user never heard \"has dialog\"\n          or \"expanded\" - measured with the CDP AX tree. */}\n      {React.isValidElement<React.ButtonHTMLAttributes<HTMLButtonElement>>(trigger) ? (\n        React.cloneElement(trigger, {\n          \"aria-haspopup\": \"dialog\",\n          \"aria-expanded\": isOpen,\n          onClick: (event: React.MouseEvent<HTMLButtonElement>) => {\n            trigger.props.onClick?.(event)\n            setOpen(true)\n          },\n        })\n      ) : (\n        <button\n          type=\"button\"\n          aria-haspopup=\"dialog\"\n          aria-expanded={isOpen}\n          onClick={() => setOpen(true)}\n          className={triggerBtn}\n        >\n          {trigger ?? \"Open\"}\n        </button>\n      )}\n\n      <AnimatePresence>\n        {isOpen ? (\n          <div className=\"fixed inset-0 z-50\">\n            <motion.div\n              data-slot=\"styled-drawer-backdrop\"\n              onClick={() => setOpen(false)}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: reduce ? 0 : 0.2, ease: [0.4, 0, 0.2, 1] }}\n              className={cn(\"absolute inset-0\", solidBackdrop)}\n            />\n            <motion.div\n              ref={panelRef}\n              data-slot=\"styled-drawer-panel\"\n              role=\"dialog\"\n              aria-modal=\"true\"\n              tabIndex={-1}\n              onAnimationComplete={() => panelRef.current?.focus()}\n              initial={reduce ? { opacity: 0 } : enter}\n              animate={reduce ? { opacity: 1 } : { x: 0, y: 0 }}\n              exit={reduce ? { opacity: 0 } : enter}\n              transition={\n                reduce\n                  ? { duration: 0 }\n                  : { type: \"spring\", stiffness: 320, damping: 34 }\n              }\n              className={cn(\n                \"fixed flex flex-col overflow-hidden rounded-t-2xl border-t border-border bg-card text-card-foreground shadow-lg outline-none\",\n                edgeAnchor[edge],\n                sizeClass,\n                className\n              )}\n            >\n              <button\n                type=\"button\"\n                onClick={() => setOpen(false)}\n                aria-label=\"Close\"\n                className={closeBtn}\n              >\n                <X />\n              </button>\n\n              {stickyHeader ? (\n                <>\n                  {handleNode}\n                  <div className=\"min-h-0 flex-1 overflow-y-auto\">\n                    {headerNode}\n                    {bodyInner}\n                  </div>\n                </>\n              ) : (\n                <>\n                  {handleNode}\n                  {headerNode}\n                  <div className=\"min-h-0 flex-1 overflow-y-auto\">{bodyInner}</div>\n                  {footerNode}\n                </>\n              )}\n            </motion.div>\n          </div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* Title: ustte sade baslik bandi + border-b, govde altta kayar. */\nexport function TitleDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      props={props}\n      renderHeader={(title) => (\n        <div\n          data-slot=\"styled-drawer-header\"\n          className=\"border-b border-border px-6 py-4 pr-12 text-base font-semibold text-foreground\"\n        >\n          {title}\n        </div>\n      )}\n    />\n  )\n}\n\n/* Sticky: long scrollable content, the header stays sticky top-0 and flush\n   with the token (card) background. */\nexport function StickyDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      props={props}\n      stickyHeader\n      renderHeader={(title) => (\n        <div\n          data-slot=\"styled-drawer-header\"\n          className=\"sticky top-0 z-10 border-b border-border bg-card px-6 py-4 pr-12 text-base font-semibold text-foreground\"\n        >\n          {title}\n        </div>\n      )}\n    />\n  )\n}\n\n/* Footer: a fixed action footer at the bottom plus border-t; the body scrolls\n   in the middle. */\nexport function FooterDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      props={props}\n      renderHeader={(title) => (\n        <div\n          data-slot=\"styled-drawer-header\"\n          className=\"border-b border-border px-6 py-4 pr-12 text-base font-semibold text-foreground\"\n        >\n          {title}\n        </div>\n      )}\n      renderFooter={() => (\n        <div\n          data-slot=\"styled-drawer-footer\"\n          className=\"flex items-center justify-end gap-2 border-t border-border px-6 py-4\"\n        >\n          <button type=\"button\" className={footerGhostBtn}>\n            Cancel\n          </button>\n          <button type=\"button\" className={footerPrimaryBtn}>\n            Save changes\n          </button>\n        </div>\n      )}\n    />\n  )\n}\n\n/* Handle: ustte gorunur grab handle cubugu + baslik bandi. */\nexport function HandleDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      props={props}\n      showHandle\n      renderHeader={(title) => (\n        <div\n          data-slot=\"styled-drawer-header\"\n          className=\"border-b border-border px-6 pb-4 pt-3 pr-12 text-base font-semibold text-foreground\"\n        >\n          {title}\n        </div>\n      )}\n    />\n  )\n}\n\n/* Divider: baslik bandi + govdede bolum ayraclari (border-t). */\nexport function DividerDrawer(props: DrawerProps) {\n  return (\n    <DrawerShell\n      props={props}\n      renderHeader={(title) => (\n        <div\n          data-slot=\"styled-drawer-header\"\n          className=\"border-b border-border px-6 py-4 pr-12 text-base font-semibold text-foreground\"\n        >\n          {title}\n        </div>\n      )}\n      renderBody={(children) => (\n        <div className=\"text-sm text-muted-foreground\">\n          {children ? (\n            <section className=\"px-6 py-5 text-foreground\">{children}</section>\n          ) : null}\n          {dividerSections.map((section, i) => (\n            <section\n              key={section.heading}\n              className={cn(\n                \"px-6 py-5\",\n                (children || i > 0) && \"border-t border-border\"\n              )}\n            >\n              <h3 className=\"mb-1 text-sm font-semibold text-foreground\">\n                {section.heading}\n              </h3>\n              <p>{section.body}</p>\n            </section>\n          ))}\n        </div>\n      )}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/drawer-header.tsx"
    }
  ],
  "categories": [
    "styled",
    "drawer"
  ],
  "type": "registry:component"
}