{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "site-header",
  "type": "registry:block",
  "title": "Site header",
  "description": "A sticky website header that turns solid on scroll, with a gliding active link, mega menu panels, and a mobile sheet.",
  "categories": [
    "blocks"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/segmented-control.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/media.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/site-header/site-header.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport type {\n  AnchorHTMLAttributes,\n  ButtonHTMLAttributes,\n  HTMLAttributes,\n  KeyboardEvent as ReactKeyboardEvent,\n  MouseEvent as ReactMouseEvent,\n  PointerEvent as ReactPointerEvent,\n  ReactNode,\n  RefObject,\n} from \"react\";\nimport Image from \"next/image\";\nimport { AnimatePresence, LayoutGroup, motion, useIsPresent, useReducedMotion } from \"motion/react\";\nimport type { Transition, Variants } from \"motion/react\";\nimport {\n  ArrowRight,\n  BookOpen,\n  Boxes,\n  ChevronDown,\n  History,\n  LayoutTemplate,\n  Menu,\n  MessagesSquare,\n  Palette,\n  PanelsTopLeft,\n  Route,\n  X,\n} from \"lucide-react\";\nimport SegmentedControl from \"../../segmented-control/segmented-control\";\nimport { motionTokens } from \"../../lib/motion-tokens\";\nimport { photo } from \"../../lib/media\";\nimport { cn } from \"../../lib/utils\";\n\nexport type SiteHeaderVariant = \"simple\" | \"centered\" | \"mega\";\n\n/** One destination inside a mega menu panel. */\nexport interface SiteHeaderLink {\n  label: string;\n  /** One short line under the label. */\n  description?: string;\n  /** A plain decorative icon beside the label. */\n  icon?: ReactNode;\n  href?: string;\n}\n\n/** A card beside the links in a mega menu panel. */\nexport interface SiteHeaderFeature {\n  title: string;\n  description?: string;\n  href?: string;\n  image?: { src: string; alt: string };\n}\n\n/** A top level destination. With `links` and the mega variant it opens a panel; otherwise it is a plain link. */\nexport interface SiteHeaderItem {\n  /** Stable value, also used for `current`. */\n  value: string;\n  label: string;\n  href?: string;\n  links?: SiteHeaderLink[];\n  feature?: SiteHeaderFeature;\n}\n\nexport interface SiteHeaderAction {\n  label: string;\n  href?: string;\n  onClick?: () => void;\n}\n\n/** What `onNavigate` receives. */\nexport interface SiteHeaderDestination {\n  label: string;\n  href?: string;\n  /** The top level item the destination belongs to. */\n  section?: string;\n}\n\nexport interface SiteHeaderProps {\n  /** `simple` puts links beside the brand, `centered` centers them in a quiet capsule, `mega` opens panels for items with links. */\n  variant?: SiteHeaderVariant;\n  brand?: { name: string; href?: string; mark?: ReactNode };\n  items?: SiteHeaderItem[];\n  /** Value of the item that holds the current page (controlled). */\n  current?: string;\n  /** Initial current item when uncontrolled. */\n  defaultCurrent?: string;\n  /** Called when a destination inside an item is chosen, with that item's value. */\n  onCurrentChange?: (value: string) => void;\n  /** Called for every destination: items, panel links, the brand, and actions with an href. */\n  onNavigate?: (destination: SiteHeaderDestination) => void;\n  /** A quiet action before the primary one, such as Sign in. Pass null to hide it. */\n  secondaryAction?: SiteHeaderAction | null;\n  /** The one primary action at the end of the bar. Pass null to hide it. */\n  primaryAction?: SiteHeaderAction | null;\n  /** Sticks to the top of its scroll container. Defaults to true. */\n  sticky?: boolean;\n  /** The element that scrolls, when it is not the window. The header turns solid once it scrolls. */\n  scrollContainer?: RefObject<HTMLElement | null>;\n  /** Pixels of scroll before the background turns solid. Defaults to 8. */\n  scrollThreshold?: number;\n  /** Accessible name of the navigation landmark. */\n  label?: string;\n  className?: string;\n}\n\ntype Bezier = [number, number, number, number];\nconst enter = [...motionTokens.ease.enter] as Bezier;\nconst standard = [...motionTokens.ease.standard] as Bezier;\n/** Duration based springs restated as stiffness and damping, so a retarget keeps the velocity already in flight. */\nconst physical = (visualDuration: number, bounce: number): Transition => {\n  const root = (2 * Math.PI) / (visualDuration * 1.2);\n  return { type: \"spring\", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };\n};\nconst GROW = physical(0.44, 0),\n  SHRINK = physical(0.34, 0),\n  GLIDE = physical(0.3, 0),\n  SLIDE = physical(0.4, 0);\nconst HOVER_INTENT = 70,\n  LEAVE_GRACE = 180,\n  TRAVEL = 36;\n/** Matches the container query in site-header.module.css. */\nconst COLLAPSE_BELOW = 760;\n\n/** Panel content slides in from the side of the newly opened item; opening from closed drops in from the bar. */\nconst faceVariants: Variants = {\n  hidden: (direction: number) => ({\n    opacity: 0,\n    x: direction * TRAVEL,\n    y: direction ? 0 : -6,\n    filter: `blur(${motionTokens.blur.subtle}px)`,\n  }),\n  shown: {\n    opacity: 1,\n    x: 0,\n    y: 0,\n    filter: \"blur(0px)\",\n    transition: {\n      x: SLIDE,\n      y: SLIDE,\n      opacity: { duration: motionTokens.duration.fast, ease: enter, delay: 0.02 },\n      filter: { duration: motionTokens.duration.standard, ease: enter },\n    },\n  },\n  gone: (direction: number) => ({\n    opacity: 0,\n    x: direction * -TRAVEL * 0.6,\n    filter: `blur(${motionTokens.blur.subtle}px)`,\n    transition: {\n      x: SLIDE,\n      opacity: { duration: motionTokens.duration.instant, ease: standard },\n      filter: { duration: motionTokens.duration.instant, ease: standard },\n    },\n  }),\n};\nconst fadeVariants: Variants = {\n  hidden: { opacity: 0 },\n  shown: { opacity: 1, transition: { duration: motionTokens.duration.fast } },\n  gone: { opacity: 0, transition: { duration: motionTokens.duration.instant } },\n};\n\nconst sheetRow =\n  \"flex min-h-13 w-full cursor-pointer items-center justify-between gap-3 bg-transparent px-1 text-left text-base/[inherit] font-medium text-ink no-underline [-webkit-tap-highlight-color:transparent]\";\n\nconst ICON = { size: 16, strokeWidth: 1.75, \"aria-hidden\": true } as const;\nconst brandBookImage = photo(\"notdocs-spec\");\n\nexport const siteHeaderExampleItems: SiteHeaderItem[] = [\n  {\n    value: \"library\",\n    label: \"Library\",\n    links: [\n      { label: \"Components\", description: \"Inputs, overlays, charts and data display\", icon: <Boxes {...ICON} /> },\n      { label: \"Blocks\", description: \"Headers, heroes, pricing and footers\", icon: <PanelsTopLeft {...ICON} /> },\n      { label: \"AI interface\", description: \"Composers, sources and agent runs\", icon: <MessagesSquare {...ICON} /> },\n      { label: \"Tokens\", description: \"Colour, radius, type and motion\", icon: <Palette {...ICON} /> },\n    ],\n    feature: {\n      title: \"Brand book\",\n      description: \"Principles, writing rules, colour and motion.\",\n      image: { src: brandBookImage.src, alt: brandBookImage.alt },\n    },\n  },\n  {\n    value: \"guides\",\n    label: \"Guides\",\n    links: [\n      { label: \"Install\", description: \"Add any item with the shadcn CLI\", icon: <Route {...ICON} /> },\n      { label: \"Writing rules\", description: \"Sentence case, short and literal\", icon: <BookOpen {...ICON} /> },\n      {\n        label: \"Card guidelines\",\n        description: \"When to use each piece, with its props\",\n        icon: <LayoutTemplate {...ICON} />,\n      },\n      { label: \"Changelog\", description: \"Every release and what it changed\", icon: <History {...ICON} /> },\n    ],\n  },\n  { value: \"registry\", label: \"Registry\" },\n  { value: \"brand\", label: \"Brand book\" },\n];\n\nfunction useScrolled(threshold: number, container?: RefObject<HTMLElement | null>) {\n  const [scrolled, setScrolled] = useState(false);\n  useEffect(() => {\n    const node = container?.current ?? null;\n    const target: HTMLElement | Window = node ?? window;\n    const read = () => (node ? node.scrollTop : window.scrollY);\n    // Turns solid past the threshold and clear again only near the top, so it never flickers at the edge.\n    const update = () => {\n      const y = read();\n      setScrolled((previous) => (previous ? y > threshold / 2 : y > threshold));\n    };\n    update();\n    target.addEventListener(\"scroll\", update, { passive: true });\n    return () => target.removeEventListener(\"scroll\", update);\n  }, [threshold, container]);\n  return scrolled;\n}\n\n/** A panel face reports its natural height while current; a leaving face floats out of flow and turns inert. */\nfunction Face({\n  direction,\n  reduced,\n  onHeight,\n  children,\n}: {\n  direction: number;\n  reduced: boolean;\n  onHeight: (height: number) => void;\n  children: ReactNode;\n}) {\n  const ref = useRef<HTMLDivElement>(null);\n  const present = useIsPresent();\n  useLayoutEffect(() => {\n    const node = ref.current;\n    if (!node || !present) return;\n    onHeight(node.offsetHeight);\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => onHeight(node.offsetHeight));\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [present, onHeight]);\n  return (\n    <motion.div\n      ref={ref}\n      className={cn(\"relative p-3\", !present && \"pointer-events-none absolute top-0 right-0 left-0\")}\n      data-face=\"\"\n      data-leaving={present ? undefined : \"\"}\n      inert={!present}\n      custom={direction}\n      variants={reduced ? fadeVariants : faceVariants}\n      initial=\"hidden\"\n      animate=\"shown\"\n      exit=\"gone\"\n    >\n      {children}\n    </motion.div>\n  );\n}\n\ntype DestinationProps = Omit<HTMLAttributes<HTMLElement>, \"onClick\" | \"children\"> & {\n  link: { href?: string; label: string };\n  onChoose: (event: ReactMouseEvent) => void;\n  children: ReactNode;\n};\n/** Renders an anchor when the destination has an href and a button otherwise, so demos and real sites share one path. */\nfunction Destination({ link, onChoose, children, ...rest }: DestinationProps) {\n  return link.href ? (\n    <a {...(rest as AnchorHTMLAttributes<HTMLAnchorElement>)} href={link.href} onClick={onChoose}>\n      {children}\n    </a>\n  ) : (\n    <button {...(rest as ButtonHTMLAttributes<HTMLButtonElement>)} type=\"button\" onClick={onChoose}>\n      {children}\n    </button>\n  );\n}\n\n/**\n * A website header in three layouts. It sticks to the top and turns solid once the page scrolls, marks the current section\n * with an indicator that glides between links, opens springy mega menu panels whose content slides in from the side you\n * moved toward, and folds into a menu sheet on narrow containers.\n */\nexport const SiteHeader = forwardRef<HTMLElement, SiteHeaderProps>(function SiteHeader(\n  {\n    variant = \"mega\",\n    brand = { name: \"Iris UI\" },\n    items = siteHeaderExampleItems,\n    current: currentProp,\n    defaultCurrent,\n    onCurrentChange,\n    onNavigate,\n    secondaryAction = { label: \"Install\" },\n    primaryAction = { label: \"Browse components\" },\n    sticky = true,\n    scrollContainer,\n    scrollThreshold = 8,\n    label = \"Main\",\n    className,\n  },\n  ref,\n) {\n  const id = useId();\n  const reduced = !!useReducedMotion();\n  const scrolled = useScrolled(scrollThreshold, scrollContainer);\n  const [innerCurrent, setInnerCurrent] = useState(defaultCurrent);\n  const current = currentProp ?? innerCurrent;\n  const [hovered, setHovered] = useState<string | null>(null);\n  const [open, setOpen] = useState<{ value: string; direction: number } | null>(null);\n  const [panel, setPanel] = useState<{ height: number | null; grow: boolean }>({ height: null, grow: true });\n  const [menuOpen, setMenuOpen] = useState(false);\n  const [expanded, setExpanded] = useState<string | null>(null);\n  const rootRef = useRef<HTMLElement | null>(null);\n  const menuButtonRef = useRef<HTMLButtonElement>(null);\n  const triggerRefs = useRef(new Map<string, HTMLButtonElement | HTMLAnchorElement>());\n  const panelRef = useRef<HTMLElement>(null);\n  const openTimer = useRef<number | undefined>(undefined),\n    closeTimer = useRef<number | undefined>(undefined);\n  const focusFirst = useRef(false);\n  const hasPanels = variant === \"mega\";\n  const centered = variant === \"centered\";\n  const openItem = open ? items.find((item) => item.value === open.value) : undefined;\n\n  const setRefs = useCallback(\n    (node: HTMLElement | null) => {\n      rootRef.current = node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    },\n    [ref],\n  );\n\n  const clearTimers = useCallback(() => {\n    window.clearTimeout(openTimer.current);\n    window.clearTimeout(closeTimer.current);\n  }, []);\n  useEffect(() => clearTimers, [clearTimers]);\n\n  const openPanel = useCallback(\n    (value: string | null) => {\n      setOpen((previous) => {\n        if (!value) return null;\n        if (previous?.value === value) return previous;\n        const from = previous ? items.findIndex((item) => item.value === previous.value) : -1;\n        const to = items.findIndex((item) => item.value === value);\n        return { value, direction: from < 0 ? 0 : Math.sign(to - from) };\n      });\n      if (!value) setPanel({ height: null, grow: true });\n    },\n    [items],\n  );\n\n  const close = useCallback(\n    (restoreFocus = false) => {\n      clearTimers();\n      const was = open?.value;\n      openPanel(null);\n      if (restoreFocus && was) triggerRefs.current.get(was)?.focus();\n    },\n    [open, openPanel, clearTimers],\n  );\n\n  const closeMenu = useCallback((restoreFocus = false) => {\n    setMenuOpen(false);\n    setExpanded(null);\n    if (restoreFocus) menuButtonRef.current?.focus();\n  }, []);\n\n  const choose = useCallback(\n    (destination: SiteHeaderDestination, section: string | undefined) => {\n      if (section) {\n        if (currentProp === undefined) setInnerCurrent(section);\n        onCurrentChange?.(section);\n      }\n      onNavigate?.(destination);\n      close();\n      closeMenu();\n    },\n    [close, closeMenu, currentProp, onCurrentChange, onNavigate],\n  );\n\n  // Outside presses and Escape close whichever layer is open.\n  useEffect(() => {\n    if (!open && !menuOpen) return;\n    const onPointer = (event: PointerEvent) => {\n      if (!rootRef.current?.contains(event.target as Node)) {\n        close();\n        closeMenu();\n      }\n    };\n    const onKey = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        if (open) close(true);\n        else closeMenu(true);\n      }\n    };\n    document.addEventListener(\"pointerdown\", onPointer);\n    document.addEventListener(\"keydown\", onKey);\n    return () => {\n      document.removeEventListener(\"pointerdown\", onPointer);\n      document.removeEventListener(\"keydown\", onKey);\n    };\n  }, [open, menuOpen, close, closeMenu]);\n\n  // The sheet belongs to narrow layouts: widening the container closes it, and it holds the page still while open.\n  useEffect(() => {\n    const node = rootRef.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(([entry]) => {\n      if (!entry) return;\n      if (entry.contentRect.width >= COLLAPSE_BELOW) {\n        setMenuOpen(false);\n        setExpanded(null);\n      } else setOpen(null);\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n  useEffect(() => {\n    if (!menuOpen) return;\n    const scroller: HTMLElement = scrollContainer?.current ?? document.documentElement;\n    const previous = scroller.style.getPropertyValue(\"overflow\");\n    scroller.style.setProperty(\"overflow\", \"hidden\");\n    return () => {\n      if (previous) scroller.style.setProperty(\"overflow\", previous);\n      else scroller.style.removeProperty(\"overflow\");\n    };\n  }, [menuOpen, scrollContainer]);\n\n  useEffect(() => {\n    if (!open || !focusFirst.current) return;\n    focusFirst.current = false;\n    const frame = requestAnimationFrame(() =>\n      panelRef.current?.querySelector<HTMLElement>(\"[data-panel-link]\")?.focus(),\n    );\n    return () => cancelAnimationFrame(frame);\n  }, [open]);\n\n  function onTriggerPointerEnter(event: ReactPointerEvent, value: string) {\n    if (event.pointerType !== \"mouse\") return;\n    window.clearTimeout(closeTimer.current);\n    window.clearTimeout(openTimer.current);\n    if (open) openPanel(value);\n    else openTimer.current = window.setTimeout(() => openPanel(value), HOVER_INTENT);\n  }\n  function onRegionPointerLeave(event: ReactPointerEvent) {\n    if (event.pointerType !== \"mouse\") return;\n    window.clearTimeout(openTimer.current);\n    closeTimer.current = window.setTimeout(() => openPanel(null), LEAVE_GRACE);\n  }\n  function onRegionPointerEnter(event: ReactPointerEvent) {\n    if (event.pointerType === \"mouse\") window.clearTimeout(closeTimer.current);\n  }\n\n  function onNavKeyDown(event: ReactKeyboardEvent<HTMLElement>) {\n    if (event.key !== \"ArrowLeft\" && event.key !== \"ArrowRight\") return;\n    const triggers = Array.from(event.currentTarget.querySelectorAll<HTMLElement>(\"[data-nav-item]\"));\n    const index = triggers.indexOf(document.activeElement as HTMLElement);\n    if (index < 0) return;\n    event.preventDefault();\n    const nextIndex = (index + (event.key === \"ArrowRight\" ? 1 : -1) + triggers.length) % triggers.length;\n    triggers[nextIndex]!.focus();\n    if (open) {\n      const item = items[nextIndex];\n      openPanel(item && hasPanels && item.links?.length ? item.value : null);\n    }\n  }\n  function onTriggerKeyDown(event: ReactKeyboardEvent, value: string) {\n    if (event.key === \"ArrowDown\") {\n      event.preventDefault();\n      focusFirst.current = true;\n      openPanel(value);\n      if (open?.value === value) panelRef.current?.querySelector<HTMLElement>(\"[data-panel-link]\")?.focus();\n    }\n  }\n  function onPanelKeyDown(event: ReactKeyboardEvent<HTMLElement>) {\n    if (event.key !== \"ArrowDown\" && event.key !== \"ArrowUp\" && event.key !== \"Home\" && event.key !== \"End\") return;\n    const unique = Array.from(\n      event.currentTarget.querySelectorAll<HTMLElement>(\"[data-face]:not([data-leaving]) [data-panel-link]\"),\n    );\n    if (!unique.length) return;\n    event.preventDefault();\n    const index = unique.indexOf(document.activeElement as HTMLElement);\n    const next =\n      event.key === \"Home\"\n        ? 0\n        : event.key === \"End\"\n          ? unique.length - 1\n          : event.key === \"ArrowDown\"\n            ? Math.min(index + 1, unique.length - 1)\n            : index - 1;\n    if (next < 0) {\n      close(true);\n      return;\n    }\n    unique[next]?.focus();\n  }\n\n  // Growing carries a little life; shrinking settles without overshoot.\n  const onHeight = useCallback(\n    (height: number) =>\n      setPanel((previous) =>\n        previous.height === height ? previous : { height, grow: previous.height === null || height > previous.height },\n      ),\n    [],\n  );\n\n  const actionNode = (action: SiteHeaderAction, kind: \"secondary\" | \"primary\", extra = \"\") => {\n    const onClick = () => {\n      action.onClick?.();\n      if (action.href) onNavigate?.({ label: action.label, href: action.href });\n      closeMenu();\n    };\n    const cls = cn(\n      \"inline-flex h-9 cursor-pointer items-center justify-center rounded-button border border-transparent px-3.5 text-sm/[inherit] font-medium whitespace-nowrap no-underline [-webkit-tap-highlight-color:transparent] transition-[transform,background-color,opacity] duration-fast ease-standard active:scale-97 motion-reduce:transition-none motion-reduce:active:scale-none\",\n      kind === \"secondary\"\n        ? \"bg-transparent text-ink-2 hover:bg-hover hover:text-ink\"\n        : \"bg-ink text-page hover:opacity-90\",\n      extra,\n    );\n    return action.href ? (\n      <a className={cls} href={action.href} onClick={onClick}>\n        {action.label}\n      </a>\n    ) : (\n      <button type=\"button\" className={cls} onClick={onClick}>\n        {action.label}\n      </button>\n    );\n  };\n\n  const brandNode = (\n    <Destination\n      link={{ label: brand.name, href: brand.href }}\n      className=\"inline-flex cursor-pointer items-center gap-2 rounded-[12px] py-2 pr-2 text-base/[inherit] font-medium text-ink no-underline\"\n      onChoose={() => {\n        onNavigate?.({ label: brand.name, href: brand.href });\n        close();\n        closeMenu();\n      }}\n    >\n      {brand.mark}\n      <span>{brand.name}</span>\n    </Destination>\n  );\n\n  return (\n    <header\n      ref={setRefs}\n      className={cn(\n        \"relative z-20 @container/site-header border-b border-transparent bg-transparent font-body tracking-body text-ink transition-[background-color,border-color,box-shadow] duration-standard ease-standard motion-reduce:transition-none\",\n        sticky && \"sticky top-0\",\n        (scrolled || menuOpen || !!open) && \"border-line bg-page shadow-resting\",\n        className,\n      )}\n      data-variant={variant}\n      data-scrolled={scrolled || menuOpen || !!open ? \"\" : undefined}\n    >\n      <div\n        className=\"relative mx-auto max-w-[1200px] px-6 @max-[760px]/site-header:px-4\"\n        onPointerLeave={onRegionPointerLeave}\n        onPointerEnter={onRegionPointerEnter}\n      >\n        <div\n          className={cn(\n            \"flex h-16 items-center gap-2\",\n            centered && \"grid grid-cols-[1fr_auto_1fr] @max-[760px]/site-header:flex\",\n          )}\n        >\n          <div className=\"flex min-w-0\">{brandNode}</div>\n          <LayoutGroup id={id}>\n            {/* Arrow keys and the hover pill are handled here for every item at once; the events come from the focusable items inside. */}\n            {/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- delegated from the item buttons and links */}\n            <nav\n              className={cn(\"ml-4 min-w-0 @max-[760px]/site-header:hidden\", centered && \"m-0\")}\n              aria-label={label}\n              onKeyDown={onNavKeyDown}\n              onPointerLeave={() => setHovered(null)}\n            >\n              <ul\n                className={cn(\n                  \"flex items-center gap-0.5\",\n                  centered && \"rounded-button border border-line bg-hover p-[3px]\",\n                )}\n              >\n                {items.map((item) => {\n                  const isCurrent = current === item.value;\n                  const withPanel = hasPanels && !!item.links?.length;\n                  const isOpen = open?.value === item.value;\n                  const panelId = `${id}-panel`;\n                  const common = {\n                    className: cn(\n                      \"relative isolate inline-flex h-9 cursor-pointer items-center gap-1 rounded-button px-3 text-sm/[inherit] font-medium whitespace-nowrap text-ink-2 no-underline [-webkit-tap-highlight-color:transparent] transition-[color] duration-fast ease-standard hover:text-ink motion-reduce:transition-none\",\n                      centered && \"h-8 px-3.5\",\n                      (isCurrent || isOpen) && \"text-ink\",\n                    ),\n                    \"data-nav-item\": \"\",\n                    \"data-current\": isCurrent ? \"\" : undefined,\n                    \"data-open\": isOpen ? \"\" : undefined,\n                    onPointerEnter: (event: ReactPointerEvent) => {\n                      if (event.pointerType === \"mouse\") setHovered(item.value);\n                      if (withPanel) onTriggerPointerEnter(event, item.value);\n                      else if (event.pointerType === \"mouse\" && open)\n                        closeTimer.current = window.setTimeout(() => openPanel(null), LEAVE_GRACE);\n                    },\n                    onFocus: () => setHovered(null),\n                  };\n                  const decorations = (\n                    <>\n                      {hovered === item.value && variant !== \"centered\" && (\n                        <motion.span\n                          key={`hover-${variant}`}\n                          layoutId={`hover-${variant}`}\n                          className=\"absolute inset-0 -z-1 rounded-[inherit] bg-hover\"\n                          transition={reduced ? { duration: 0 } : GLIDE}\n                          aria-hidden=\"true\"\n                        />\n                      )}\n                      {isCurrent && (\n                        <motion.span\n                          key={`current-${variant}`}\n                          layoutId={`current-${variant}`}\n                          className={cn(\n                            \"absolute right-3 bottom-1 left-3 h-0.5 rounded-[2px] bg-accent\",\n                            withPanel && \"right-[calc(0.75rem+18px)]\",\n                            centered &&\n                              \"inset-0 -z-1 h-auto rounded-[inherit] border border-line bg-surface shadow-resting\",\n                          )}\n                          transition={reduced ? { duration: 0 } : motionTokens.spring.morph}\n                          aria-hidden=\"true\"\n                        />\n                      )}\n                    </>\n                  );\n                  return (\n                    <li key={item.value} className=\"flex\">\n                      {withPanel ? (\n                        <button\n                          {...common}\n                          ref={(node: HTMLButtonElement | null) => {\n                            if (node) triggerRefs.current.set(item.value, node);\n                            else triggerRefs.current.delete(item.value);\n                          }}\n                          type=\"button\"\n                          aria-expanded={isOpen}\n                          aria-controls={isOpen ? panelId : undefined}\n                          onClick={() => {\n                            clearTimers();\n                            openPanel(isOpen ? null : item.value);\n                          }}\n                          onKeyDown={(event) => onTriggerKeyDown(event, item.value)}\n                        >\n                          {decorations}\n                          <span className=\"relative\">{item.label}</span>\n                          <ChevronDown\n                            className={cn(\n                              \"text-ink-3 transition-transform duration-standard ease-standard motion-reduce:transition-none\",\n                              isOpen && \"rotate-180\",\n                            )}\n                            size={14}\n                            strokeWidth={2}\n                            aria-hidden=\"true\"\n                          />\n                        </button>\n                      ) : (\n                        <Destination\n                          link={item}\n                          {...common}\n                          aria-current={isCurrent ? \"page\" : undefined}\n                          onChoose={() =>\n                            choose({ label: item.label, href: item.href, section: item.value }, item.value)\n                          }\n                        >\n                          {decorations}\n                          <span className=\"relative\">{item.label}</span>\n                        </Destination>\n                      )}\n                    </li>\n                  );\n                })}\n              </ul>\n            </nav>\n          </LayoutGroup>\n          <div className={cn(\"ml-auto flex items-center gap-2\", centered && \"justify-self-end\")}>\n            {secondaryAction && actionNode(secondaryAction, \"secondary\", \"@max-[760px]/site-header:hidden\")}\n            {primaryAction && actionNode(primaryAction, \"primary\")}\n            <button\n              ref={menuButtonRef}\n              type=\"button\"\n              className=\"relative hidden size-10 cursor-pointer place-items-center rounded-button bg-transparent text-ink [-webkit-tap-highlight-color:transparent] transition-[background-color] duration-fast ease-standard active:bg-hover @max-[760px]/site-header:grid\"\n              aria-expanded={menuOpen}\n              aria-controls={`${id}-sheet`}\n              aria-label={menuOpen ? \"Close menu\" : \"Open menu\"}\n              onClick={() => {\n                if (menuOpen) closeMenu();\n                else setMenuOpen(true);\n              }}\n            >\n              <AnimatePresence initial={false} mode=\"popLayout\">\n                <motion.span\n                  key={menuOpen ? \"close\" : \"open\"}\n                  className=\"grid place-items-center\"\n                  initial={reduced ? { opacity: 0 } : { opacity: 0, rotate: menuOpen ? -45 : 45, scale: 0.8 }}\n                  animate={{ opacity: 1, rotate: 0, scale: 1 }}\n                  exit={reduced ? { opacity: 0 } : { opacity: 0, rotate: menuOpen ? 45 : -45, scale: 0.8 }}\n                  transition={\n                    reduced\n                      ? { duration: 0 }\n                      : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.instant } }\n                  }\n                >\n                  {menuOpen ? (\n                    <X size={20} strokeWidth={1.75} aria-hidden=\"true\" />\n                  ) : (\n                    <Menu size={20} strokeWidth={1.75} aria-hidden=\"true\" />\n                  )}\n                </motion.span>\n              </AnimatePresence>\n            </button>\n          </div>\n        </div>\n\n        {hasPanels && (\n          <AnimatePresence>\n            {openItem?.links && (\n              <motion.section\n                key=\"panel\"\n                id={`${id}-panel`}\n                ref={panelRef}\n                className=\"absolute top-[calc(100%-4px)] right-6 left-6 origin-top overflow-hidden rounded-panel border border-line bg-surface shadow-floating @max-[760px]/site-header:hidden\"\n                aria-label={openItem.label}\n                onKeyDown={onPanelKeyDown}\n                initial={reduced ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.985 }}\n                animate={{ opacity: 1, y: 0, scale: 1, height: panel.height ?? \"auto\" }}\n                exit={\n                  reduced\n                    ? { opacity: 0, transition: { duration: motionTokens.duration.instant } }\n                    : {\n                        opacity: 0,\n                        y: -4,\n                        scale: 0.99,\n                        transition: { duration: motionTokens.duration.exit, ease: standard },\n                      }\n                }\n                transition={\n                  reduced\n                    ? { duration: 0 }\n                    : {\n                        height: panel.grow ? GROW : SHRINK,\n                        y: GROW,\n                        scale: GROW,\n                        opacity: { duration: motionTokens.duration.fast, ease: enter },\n                      }\n                }\n              >\n                <AnimatePresence initial={false} custom={open?.direction ?? 0}>\n                  <Face key={openItem.value} direction={open?.direction ?? 0} reduced={reduced} onHeight={onHeight}>\n                    <div\n                      className={cn(\"grid gap-2\", openItem.feature && \"grid-cols-[minmax(0,1fr)_minmax(200px,260px)]\")}\n                      data-featured={openItem.feature ? \"\" : undefined}\n                    >\n                      <ul className=\"grid grid-cols-2 gap-0.5\">\n                        {openItem.links.map((link) => (\n                          <li key={link.label}>\n                            <Destination\n                              link={link}\n                              className=\"flex size-full cursor-pointer items-start gap-3 rounded-[16px] bg-transparent p-3 text-left text-ink no-underline transition-[background-color] duration-fast ease-standard hover:bg-hover focus-visible:bg-hover motion-reduce:transition-none\"\n                              data-panel-link=\"\"\n                              onChoose={() =>\n                                choose({ label: link.label, href: link.href, section: openItem.value }, openItem.value)\n                              }\n                            >\n                              {link.icon && <span className=\"mt-0.5 flex flex-none text-ink-2\">{link.icon}</span>}\n                              <span className=\"grid min-w-0 gap-0.5\">\n                                <span className=\"text-sm/[inherit] font-medium\">{link.label}</span>\n                                {link.description && (\n                                  <span className=\"text-sm/[inherit] text-ink-3\">{link.description}</span>\n                                )}\n                              </span>\n                            </Destination>\n                          </li>\n                        ))}\n                      </ul>\n                      {openItem.feature && (\n                        <Destination\n                          link={{ label: openItem.feature.title, href: openItem.feature.href }}\n                          className=\"group/feature grid cursor-pointer content-start gap-1.5 rounded-[18px] bg-hover px-2 pt-2 pb-3 text-left text-ink no-underline\"\n                          data-panel-link=\"\"\n                          onChoose={() =>\n                            choose(\n                              { label: openItem.feature!.title, href: openItem.feature!.href, section: openItem.value },\n                              openItem.value,\n                            )\n                          }\n                        >\n                          {openItem.feature.image && (\n                            <span className=\"relative mb-1 block aspect-[16/10] overflow-hidden rounded-[12px] bg-line\">\n                              <Image\n                                className=\"object-cover transition-transform duration-considered ease-standard group-hover/feature:scale-103 motion-reduce:transition-none\"\n                                src={openItem.feature.image.src}\n                                alt={openItem.feature.image.alt}\n                                fill\n                                sizes=\"260px\"\n                              />\n                            </span>\n                          )}\n                          <span className=\"inline-flex items-center gap-1.5 px-1 text-sm/[inherit] font-medium\">\n                            {openItem.feature.title}\n                            <ArrowRight\n                              className=\"text-ink-3 transition-transform duration-fast ease-standard group-hover/feature:translate-x-0.5 motion-reduce:transition-none\"\n                              size={14}\n                              strokeWidth={2}\n                              aria-hidden=\"true\"\n                            />\n                          </span>\n                          {openItem.feature.description && (\n                            <span className=\"px-1 text-sm/[inherit] text-ink-3\">{openItem.feature.description}</span>\n                          )}\n                        </Destination>\n                      )}\n                    </div>\n                  </Face>\n                </AnimatePresence>\n              </motion.section>\n            )}\n          </AnimatePresence>\n        )}\n      </div>\n\n      <AnimatePresence>\n        {menuOpen && (\n          <>\n            <motion.div\n              key=\"scrim\"\n              className=\"absolute top-full right-0 left-0 h-dvh bg-[oklch(0%_0_0/.16)] @min-[760px]/site-header:hidden\"\n              onClick={() => closeMenu()}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: reduced ? 0 : motionTokens.duration.standard, ease: standard }}\n              aria-hidden=\"true\"\n            />\n            <motion.div\n              key=\"sheet\"\n              id={`${id}-sheet`}\n              className=\"absolute top-full right-0 left-0 overflow-hidden border-b border-line bg-page @min-[760px]/site-header:hidden\"\n              initial={reduced ? { opacity: 0 } : { height: 0 }}\n              animate={reduced ? { opacity: 1 } : { height: \"auto\" }}\n              exit={reduced ? { opacity: 0 } : { height: 0, transition: SHRINK }}\n              transition={reduced ? { duration: 0 } : GROW}\n            >\n              <nav\n                className=\"max-h-[var(--site-header-sheet-max,calc(100dvh-65px))] overflow-y-auto overscroll-contain px-4 pt-2 pb-5\"\n                aria-label={label}\n              >\n                <ul>\n                  {items.map((item, index) => {\n                    const isCurrent = current === item.value;\n                    const group = !!item.links?.length && variant === \"mega\";\n                    const isExpanded = expanded === item.value;\n                    const rowMotion = {\n                      initial: reduced ? false : { opacity: 0, y: -6 },\n                      animate: { opacity: 1, y: 0 },\n                      transition: {\n                        duration: motionTokens.duration.standard,\n                        ease: enter,\n                        delay: reduced ? 0 : 0.04 + index * motionTokens.stagger.item,\n                      },\n                    } as const;\n                    return (\n                      <motion.li key={item.value} className=\"border-b border-line-soft\" {...rowMotion}>\n                        {group ? (\n                          <>\n                            <button\n                              type=\"button\"\n                              className={sheetRow}\n                              data-current={isCurrent ? \"\" : undefined}\n                              aria-expanded={isExpanded}\n                              aria-controls={`${id}-group-${item.value}`}\n                              onClick={() => setExpanded(isExpanded ? null : item.value)}\n                            >\n                              <span>{item.label}</span>\n                              <ChevronDown\n                                className={cn(\n                                  \"text-ink-3 transition-transform duration-standard ease-standard motion-reduce:transition-none\",\n                                  isExpanded && \"rotate-180\",\n                                )}\n                                size={18}\n                                strokeWidth={1.75}\n                                aria-hidden=\"true\"\n                              />\n                            </button>\n                            <AnimatePresence initial={false}>\n                              {isExpanded && (\n                                <motion.div\n                                  key=\"group\"\n                                  id={`${id}-group-${item.value}`}\n                                  className=\"overflow-hidden\"\n                                  initial={reduced ? { opacity: 0 } : { height: 0, opacity: 0 }}\n                                  animate={reduced ? { opacity: 1 } : { height: \"auto\", opacity: 1 }}\n                                  exit={reduced ? { opacity: 0 } : { height: 0, opacity: 0 }}\n                                  transition={\n                                    reduced\n                                      ? { duration: 0 }\n                                      : {\n                                          height: motionTokens.spring.smooth,\n                                          opacity: { duration: motionTokens.duration.fast },\n                                        }\n                                  }\n                                >\n                                  <ul className=\"grid pb-3\">\n                                    {item.links!.map((link) => (\n                                      <li key={link.label}>\n                                        <Destination\n                                          link={link}\n                                          className=\"flex min-h-11 w-full cursor-pointer items-center gap-3 rounded-[12px] bg-transparent px-2 text-left text-sm/[inherit] text-ink-2 no-underline active:bg-hover active:text-ink\"\n                                          onChoose={() =>\n                                            choose(\n                                              { label: link.label, href: link.href, section: item.value },\n                                              item.value,\n                                            )\n                                          }\n                                        >\n                                          {link.icon}\n                                          <span>{link.label}</span>\n                                        </Destination>\n                                      </li>\n                                    ))}\n                                  </ul>\n                                </motion.div>\n                              )}\n                            </AnimatePresence>\n                          </>\n                        ) : (\n                          <Destination\n                            link={item}\n                            className={sheetRow}\n                            data-current={isCurrent ? \"\" : undefined}\n                            aria-current={isCurrent ? \"page\" : undefined}\n                            onChoose={() =>\n                              choose({ label: item.label, href: item.href, section: item.value }, item.value)\n                            }\n                          >\n                            <span>{item.label}</span>\n                            {isCurrent && <span className=\"size-1.5 rounded-full bg-accent\" aria-hidden=\"true\" />}\n                          </Destination>\n                        )}\n                      </motion.li>\n                    );\n                  })}\n                </ul>\n                {(secondaryAction || primaryAction) && (\n                  <motion.div\n                    className=\"mt-5 grid grid-cols-2 gap-2\"\n                    initial={reduced ? false : { opacity: 0, y: -6 }}\n                    animate={{ opacity: 1, y: 0 }}\n                    transition={{\n                      duration: motionTokens.duration.standard,\n                      ease: enter,\n                      delay: reduced ? 0 : 0.04 + items.length * motionTokens.stagger.item,\n                    }}\n                  >\n                    {secondaryAction && actionNode(secondaryAction, \"secondary\", \"border-line\")}\n                    {primaryAction && actionNode(primaryAction, \"primary\")}\n                  </motion.div>\n                )}\n              </nav>\n            </motion.div>\n          </>\n        )}\n      </AnimatePresence>\n    </header>\n  );\n});\n\nSiteHeader.displayName = \"SiteHeader\";\n\nconst variantOptions = [\n  { value: \"mega\", label: \"Mega menu\" },\n  { value: \"simple\", label: \"Simple\" },\n  { value: \"centered\", label: \"Centered\" },\n];\nconst pageLabels: Record<string, string> = {\n  library: \"Library\",\n  guides: \"Guides\",\n  registry: \"Registry\",\n  brand: \"Brand book\",\n};\n\n/** Preview: the header inside a small scrolling page, with a switch between its three layouts. */\nexport function SiteHeaderBlock({ variant: initial = \"mega\" }: { variant?: SiteHeaderVariant }) {\n  const [variant, setVariant] = useState<SiteHeaderVariant>(initial);\n  const [current, setCurrent] = useState(\"library\");\n  const [last, setLast] = useState<string | null>(null);\n  const scrollRef = useRef<HTMLDivElement>(null);\n  return (\n    <div className=\"grid w-full justify-items-center gap-4\">\n      <SegmentedControl\n        label=\"Header layout\"\n        options={variantOptions}\n        value={variant}\n        onValueChange={(value) => setVariant(value as SiteHeaderVariant)}\n      />\n      <div\n        className=\"relative @container h-[520px] w-full overflow-y-auto overscroll-contain rounded-[20px] border border-line bg-page [--site-header-sheet-max:calc(520px-67px)]\"\n        ref={scrollRef}\n      >\n        <SiteHeader\n          variant={variant}\n          current={current}\n          onCurrentChange={setCurrent}\n          onNavigate={(destination) => setLast(destination.label)}\n          scrollContainer={scrollRef}\n          secondaryAction={{ label: \"Install\", onClick: () => setLast(\"Install\") }}\n          primaryAction={{ label: \"Browse components\", onClick: () => setLast(\"Browse components\") }}\n        />\n        <div className=\"-mt-[65px]\">\n          <div className=\"grid justify-items-center gap-3 bg-(image:--gradient-soft) px-6 pt-32 pb-18 text-center @max-[520px]:px-4 @max-[520px]:pt-28 @max-[520px]:pb-14\">\n            <h2 className=\"font-display text-3xl leading-display font-medium tracking-display\">\n              {pageLabels[current] ?? \"Iris UI\"}\n            </h2>\n            <p className=\"sr-only\" aria-live=\"polite\">\n              {last ? `Opened ${last}` : \"\"}\n            </p>\n          </div>\n          <div className=\"grid gap-3 p-6\" aria-hidden=\"true\">\n            {Array.from({ length: 6 }, (_, index) => (\n              <div key={index} className=\"grid grid-cols-3 gap-3 @max-[520px]:grid-cols-1\">\n                {[0, 1, 2].map((cell) => (\n                  <span key={cell} className={cn(\"h-30 rounded-[16px] bg-hover\", cell > 0 && \"@max-[520px]:hidden\")} />\n                ))}\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default SiteHeaderBlock;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/site-header/site-header.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/site-header",
  "meta": {
    "kind": "block",
    "group": "Blocks",
    "export": "SiteHeader",
    "docs": "https://irisui.dev/components/site-header"
  }
}
