{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sheet-header",
  "title": "Header sheet",
  "description": "Styled sheet: the Header set. 5 decorative sheet variations (TitleSheet, StickySheet, FooterSheet, DividerSheet, AccentSheet) 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/sheet-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 sheet family: 5 right-edge panels, all carrying the same slide\n   mechanic from the right edge; the difference is ONLY in the layout of the\n   HEADER/FOOTER band. The panel is always clear/opaque (bg-card). Self-contained -\n   no radix, no portal. A fixed inset-0 backdrop + a fixed panel pinned to the edge\n   (z-50). Internal state (defaultOpen) or controlled (open + onOpenChange). A\n   backdrop click / Escape / the close button closes it. The panel takes focus on\n   open. The slide runs through AnimatePresence; under reduced-motion there is no\n   slide, only a fade. Color comes ONLY from tokens, with transparency via\n   color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\ntype Edge = \"right\" | \"left\" | \"top\" | \"bottom\"\n\nconst widthSize: Record<StyledSize, string> = {\n  sm: \"w-72\",\n  md: \"w-80\",\n  lg: \"w-96\",\n  xl: \"w-[32rem]\",\n}\n\nconst heightSize: Record<StyledSize, string> = {\n  sm: \"h-40\",\n  md: \"h-64\",\n  lg: \"h-80\",\n  xl: \"h-[32rem]\",\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 SheetProps {\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  \"Sheets slide in from the 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]\n\n/* Sample sections for the divider-separated body (DividerSheet). */\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/* 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: SheetProps\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}\n\nfunction SheetShell({\n  props,\n  renderHeader,\n  stickyHeader,\n  renderFooter,\n  renderBody = defaultBody,\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 right edge; the slideOffset mechanism is preserved. */\n  const edge: Edge = \"right\"\n  const sizeClass = cn(\"h-full\", widthSize[size])\n  const enter = slideOffset(edge)\n\n  const headerNode = title ? renderHeader(title) : null\n  const bodyInner = renderBody(children)\n  const footerNode = renderFooter?.()\n\n  return (\n    <div data-slot=\"styled-sheet\" 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-sheet-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-sheet-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 border-l 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                <div className=\"min-h-0 flex-1 overflow-y-auto\">\n                  {headerNode}\n                  {bodyInner}\n                </div>\n              ) : (\n                <>\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 TitleSheet(props: SheetProps) {\n  return (\n    <SheetShell\n      props={props}\n      renderHeader={(title) => (\n        <div\n          data-slot=\"styled-sheet-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 StickySheet(props: SheetProps) {\n  return (\n    <SheetShell\n      props={props}\n      stickyHeader\n      renderHeader={(title) => (\n        <div\n          data-slot=\"styled-sheet-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 FooterSheet(props: SheetProps) {\n  return (\n    <SheetShell\n      props={props}\n      renderHeader={(title) => (\n        <div\n          data-slot=\"styled-sheet-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-sheet-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/* Divider: baslik bandi + govdede bolum ayraclari (border-t). */\nexport function DividerSheet(props: SheetProps) {\n  return (\n    <SheetShell\n      props={props}\n      renderHeader={(title) => (\n        <div\n          data-slot=\"styled-sheet-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\n/* Accent: baslik bandinin ustunde token accent seridi (bg-primary). */\nexport function AccentSheet(props: SheetProps) {\n  return (\n    <SheetShell\n      props={props}\n      renderHeader={(title) => (\n        <div data-slot=\"styled-sheet-header\" className=\"border-b border-border\">\n          <div className=\"h-1 w-full bg-primary\" aria-hidden />\n          <div className=\"px-6 py-4 pr-12 text-base font-semibold text-foreground\">\n            {title}\n          </div>\n        </div>\n      )}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/sheet-header.tsx"
    }
  ],
  "categories": [
    "styled",
    "sheet"
  ],
  "type": "registry:component"
}