{
  "schemaVersion": 1,
  "name": "site-header",
  "useFor": "Use once at the top of every page with the page's sections as links (#features) and the main action. Set fixed={false} inside a frame or preview.",
  "constraints": [
    "A filled header bar",
    "More than one key",
    "A white key in the header",
    "A sticky header (fixed with a spacer instead)",
    "Many links in the header"
  ],
  "documentation": "`SiteHeader` props: `name` (default \"opencommerce\"), `byline` (default \"by Agentcard\"), `logo` (your own, in place\nof the wordmark), `href` (default \"#top\"), `logoLabel`, `links` ({ label, href, current }[]), `action` ({ label,\nhref }), `spy` (default true), `fixed` (default true), plus header props.\n\nMotion: once the page scrolls the trays drop 12px and slide together (250ms, in-out) and the byline folds (250ms\nease-out); the phone sheet drops 6px in as it opens (200ms ease-out) and closes at once. Reduced motion keeps only the\nfades.",
  "source": "opencommerce-agentcard-web src/components/site-header.tsx, src/components/logo.tsx, src/components/mobile-nav.tsx, src/app/globals.css (.nav-tray, .nav-cta, .mobile-sheet)",
  "exports": [
    "SiteHeader",
    "HeaderLink"
  ],
  "client": true,
  "docsUrl": "https://opencommerce-ds.vercel.app/components/site-header",
  "markdownUrl": "https://opencommerce-ds.vercel.app/r/site-header.md",
  "examples": [
    {
      "path": "examples/site-header.tsx",
      "content": "import { SiteHeader } from \"@/components/site/site-header\";\n\nconst LINKS = [\n  { label: \"Home\", href: \"#top\" },\n  { label: \"Features\", href: \"#features\" },\n  { label: \"Pricing\", href: \"#pricing\" },\n  { label: \"FAQ\", href: \"#faq\" },\n  { label: \"Docs\", href: \"https://docs.agentcard.sh\" },\n];\n\nexport function Site() {\n  return (\n    <div className=\"min-h-40 bg-surface pt-4\">\n      <SiteHeader fixed={false} links={LINKS} action={{ label: \"Get started\", href: \"https://app.agentcard.sh\" }} />\n    </div>\n  );\n}\n",
      "exports": [
        "Site"
      ],
      "requires": [
        "site-header"
      ],
      "packages": [],
      "previews": [
        {
          "name": "Site",
          "url": "https://opencommerce-ds.vercel.app/preview/site-header/Site"
        }
      ]
    }
  ],
  "type": "site",
  "title": "Site header",
  "description": "Fixed header of three glass trays: the wordmark, the links with their pill, and one blue key.",
  "surface": "site",
  "kinds": [
    "header",
    "header-action",
    "nav-link",
    "nav-dock"
  ],
  "requires": [
    "theme",
    "nav-dock",
    "utils"
  ],
  "packages": [],
  "files": [
    {
      "path": "components/site/site-header.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { NavDock, TRAY, type DockLink } from \"@/components/site/nav-dock\";\nimport { cn } from \"@/lib/utils\";\n\n// The header (the opencommerce site, site-header.tsx): no bar behind it, three trays of frosted glass, each its own\n// pane: the wordmark, the links (NavDock, with its lit pill) and the one blue key. At the top of the page the links sit\n// dead center and the wordmark and the key hold the ends of the column. Once the page scrolls, the trays drop 12px, the\n// byline folds away, and the wordmark and the key slide in to sit 6px from the links. The slide is a transform measured\n// off the layout, so nothing reflows, and it takes the in-out curve (250ms) because the trays travel and stay on screen.\n// Fixed, with a spacer holding its place: a sticky header makes the browser redo the glass's blur on every scroll frame.\n// On a phone the links leave for a menu button beside the key, which drops a sheet of links under the header.\n\ntype HeaderLink = DockLink;\n\nconst GAP = 6;\nconst SLIDE = \"transition-transform duration-(--ds-duration-moderate) ease-in-out motion-reduce:transition-none\";\n\nfunction Wordmark({ name, byline, compact }: { name: React.ReactNode; byline?: React.ReactNode; compact: boolean }) {\n  return (\n    <span className=\"flex items-baseline font-display leading-none\">\n      <span data-wordmark-name=\"\" className=\"text-h5 font-medium tracking-tight\">\n        {name}\n      </span>\n      {byline && (\n        <span\n          className={cn(\n            \"overflow-x-clip text-caption whitespace-nowrap text-fg-muted transition-[max-width,opacity,margin] duration-(--ds-duration-moderate) ease-out motion-reduce:transition-none max-sm:hidden\",\n            compact ? \"ml-0 max-w-0 opacity-0\" : \"ml-1.5 max-w-25 opacity-100\",\n          )}\n        >\n          {byline}\n        </span>\n      )}\n    </span>\n  );\n}\n\nfunction SiteHeader({\n  name = \"opencommerce\",\n  byline = \"by Agentcard\",\n  logo,\n  href = \"#top\",\n  logoLabel,\n  links = [],\n  action,\n  spy = true,\n  fixed = true,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"header\">, \"children\"> & {\n  /** The wordmark's name and the byline beside it (folds away once the page scrolls). */\n  name?: React.ReactNode;\n  byline?: React.ReactNode;\n  /** Your own logo in place of the wordmark. */\n  logo?: React.ReactNode;\n  /** Where the logo leads. */\n  href?: string;\n  /** The logo link's accessible name. */\n  logoLabel?: string;\n  /** The links in the middle tray. */\n  links?: HeaderLink[];\n  /** The one key at the right. */\n  action?: { label: React.ReactNode; href: string };\n  /** The links' pill follows the section in view. */\n  spy?: boolean;\n  /** Fixed to the top of the screen, with a spacer in the flow. Off: in the flow (previews, a page in a frame). */\n  fixed?: boolean;\n}) {\n  const [scrolled, setScrolled] = React.useState(false);\n  const [shift, setShift] = React.useState({ logo: 0, go: 0 });\n  const [open, setOpen] = React.useState(false);\n  const logoCell = React.useRef<HTMLDivElement>(null);\n  const navCell = React.useRef<HTMLDivElement>(null);\n  const goCell = React.useRef<HTMLDivElement>(null);\n  const sheetId = React.useId();\n\n  // Read the scroll once per frame, not on every event.\n  React.useEffect(() => {\n    let frame = 0;\n    const read = () => {\n      frame = 0;\n      setScrolled(window.scrollY > 8);\n    };\n    const onScroll = () => {\n      if (!frame) frame = requestAnimationFrame(read);\n    };\n    read();\n    window.addEventListener(\"scroll\", onScroll, { passive: true });\n    return () => {\n      cancelAnimationFrame(frame);\n      window.removeEventListener(\"scroll\", onScroll);\n    };\n  }, []);\n\n  // The cells are never transformed (their trays are), so the shift is read straight off them. The wordmark is aimed\n  // at the width it will settle at (its name plus the tray's padding and border), not its live width, which shrinks\n  // every frame while the byline folds.\n  React.useLayoutEffect(() => {\n    const measure = () => {\n      const nav = navCell.current?.getBoundingClientRect();\n      const logoBox = logoCell.current?.getBoundingClientRect();\n      const go = goCell.current?.getBoundingClientRect();\n      const trayEl = logoCell.current?.firstElementChild as HTMLElement | null;\n      const nameEl = trayEl?.querySelector<HTMLElement>(\"[data-wordmark-name]\") ?? trayEl?.firstElementChild?.firstElementChild as HTMLElement | null;\n      let next = { logo: 0, go: 0 };\n      if (scrolled && nav?.width && logoBox && go && trayEl && nameEl) {\n        const cs = getComputedStyle(trayEl);\n        const edges = [\"paddingLeft\", \"paddingRight\", \"borderLeftWidth\", \"borderRightWidth\"] as const;\n        const right = logoBox.left + nameEl.offsetWidth + edges.reduce((sum, k) => sum + parseFloat(cs[k]), 0);\n        next = { logo: Math.round(nav.left - GAP - right), go: Math.round(nav.right + GAP - go.left) };\n      }\n      setShift((prev) => (prev.logo === next.logo && prev.go === next.go ? prev : next));\n    };\n    measure();\n    const ro = new ResizeObserver(measure);\n    [logoCell, navCell, goCell].forEach((r) => r.current && ro.observe(r.current));\n    window.addEventListener(\"resize\", measure);\n    return () => {\n      ro.disconnect();\n      window.removeEventListener(\"resize\", measure);\n    };\n  }, [scrolled]);\n\n  // The phone sheet closes on Escape and when the screen grows past the breakpoint.\n  React.useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setOpen(false);\n    const wide = window.matchMedia(\"(min-width: 48rem)\");\n    const onWide = () => wide.matches && setOpen(false);\n    window.addEventListener(\"keydown\", onKey);\n    wide.addEventListener(\"change\", onWide);\n    return () => {\n      window.removeEventListener(\"keydown\", onKey);\n      wide.removeEventListener(\"change\", onWide);\n    };\n  }, [open]);\n\n  const bar = (\n    <header\n      data-slot=\"site-header\"\n      className={cn(\"pointer-events-none z-40\", fixed ? \"fixed inset-x-0 top-0\" : \"relative\", className)}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"relative mx-auto grid h-(--ds-layout-header) w-(--ds-frame-column) grid-cols-[1fr_auto_1fr] items-center max-md:grid-cols-[1fr_auto]\",\n          SLIDE,\n          scrolled && \"translate-y-3\",\n        )}\n      >\n        <div ref={logoCell} className=\"pointer-events-auto w-max justify-self-start\">\n          <div className={cn(TRAY, \"px-3 pt-2.25 pb-2.75\", SLIDE)} style={{ transform: `translateX(${shift.logo}px)` }}>\n            <a href={href} aria-label={logoLabel} className=\"block rounded-chip outline-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-solid focus-visible:outline-focus-ring\">\n              {logo ?? <Wordmark name={name} byline={byline} compact={scrolled} />}\n            </a>\n          </div>\n        </div>\n        <div ref={navCell} className=\"pointer-events-auto max-md:hidden\">\n          {links.length > 0 && <NavDock links={links} spy={spy} />}\n        </div>\n        <div ref={goCell} className=\"pointer-events-auto w-max justify-self-end\">\n          {/* No transform at rest: any transform makes this the box the phone sheet is laid out in. */}\n          <div className={cn(\"flex items-center gap-1.5\", SLIDE)} style={shift.go ? { transform: `translateX(${shift.go}px)` } : undefined}>\n            {action && (\n              <div className={cn(TRAY, \"p-0.75\")}>\n                <a\n                  href={action.href}\n                  className=\"inline-flex items-center rounded-sm border border-line-brand-edge bg-primary px-3.5 py-2 text-body-sm leading-none whitespace-nowrap text-on-primary outline-none transition-[background-color,scale] duration-(--ds-duration-fast) ease-out hover:bg-primary-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring active:scale-97 motion-reduce:transition-none motion-reduce:active:scale-100\"\n                >\n                  {action.label}\n                </a>\n              </div>\n            )}\n            {links.length > 0 && (\n              <button\n                type=\"button\"\n                aria-expanded={open}\n                aria-controls={sheetId}\n                aria-label={open ? \"Close menu\" : \"Open menu\"}\n                onClick={() => setOpen((o) => !o)}\n                className={cn(TRAY, \"flex size-9.5 items-center justify-center text-fg outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus-ring md:hidden\")}\n              >\n                <svg width=\"16\" height=\"16\" viewBox=\"0 0 18 18\" fill=\"none\" aria-hidden className=\"stroke-current [stroke-linecap:round] [stroke-width:1.5]\">\n                  {open ? <path d=\"M4 4l10 10M14 4L4 14\" /> : <path d=\"M3 5.5h12M3 12.5h12\" />}\n                </svg>\n              </button>\n            )}\n          </div>\n        </div>\n        {links.length > 0 && (\n          <nav\n            id={sheetId}\n            aria-label=\"Main\"\n            hidden={!open}\n            className=\"pointer-events-auto absolute inset-x-0 top-full flex-col border-b border-line bg-surface p-2 transition-[opacity,translate] duration-(--ds-duration-base) ease-out [&:not([hidden])]:flex starting:-translate-y-1.5 starting:opacity-0 motion-reduce:transition-opacity md:hidden\"\n          >\n            {links.map((link) => (\n              <a\n                key={link.href + link.label}\n                href={link.href}\n                onClick={() => setOpen(false)}\n                className=\"rounded-control px-3 py-3 text-button text-fg outline-none transition-colors duration-(--ds-duration-fast) ease-out hover:bg-tint focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-solid focus-visible:outline-focus-ring\"\n              >\n                {link.label}\n              </a>\n            ))}\n          </nav>\n        )}\n      </div>\n    </header>\n  );\n\n  return fixed ? (\n    <>\n      <div aria-hidden className=\"h-(--ds-layout-header)\" />\n      {bar}\n    </>\n  ) : (\n    bar\n  );\n}\n\nexport { SiteHeader };\nexport type { HeaderLink };\n"
    }
  ]
}
