# Site header

> A sticky website header that turns solid on scroll, with a gliding active link, mega menu panels, and a mobile sheet.

A shadcn/ui block from the [@uiarc](https://uiarc.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uiarc/site-header).

- Page, with a live preview: https://shadcn.exchange/r/uiarc/site-header
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uiarc/site-header-light-cde8b5a3423321d9.png
- Registry item: https://uiarc.dev/r/site-header.json
- Categories: blocks
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uiarc.dev/r/site-header.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `https://uiarc.dev/r/arc-motion-tokens.json`, `https://uiarc.dev/r/segmented-control.json`

## Notes from the registry

Use this as a starting point and replace the sample data with your own.

## Files

### `registry/blocks/site-header/site-header.tsx`

```tsx
"use client";

import { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, HTMLAttributes, KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent, ReactNode, RefObject } from "react";
import Image from "next/image";
import { AnimatePresence, LayoutGroup, motion, useIsPresent, useReducedMotion } from "motion/react";
import type { Transition, Variants } from "motion/react";
import { ArrowRight, BookOpen, Boxes, ChevronDown, History, LayoutTemplate, Menu, MessagesSquare, Palette, PanelsTopLeft, Route, X } from "lucide-react";
import SegmentedControl from "../../segmented-control/segmented-control";
import { motionTokens } from "../../lib/motion-tokens";
import { photo } from "../../lib/media";
import styles from "./site-header.module.css";

export type SiteHeaderVariant = "simple" | "centered" | "mega";

/** One destination inside a mega menu panel. */
export interface SiteHeaderLink {
  label: string;
  /** One short line under the label. */
  description?: string;
  /** A plain decorative icon beside the label. */
  icon?: ReactNode;
  href?: string;
}

/** A card beside the links in a mega menu panel. */
export interface SiteHeaderFeature {
  title: string;
  description?: string;
  href?: string;
  image?: { src: string; alt: string };
}

/** A top level destination. With `links` and the mega variant it opens a panel; otherwise it is a plain link. */
export interface SiteHeaderItem {
  /** Stable value, also used for `current`. */
  value: string;
  label: string;
  href?: string;
  links?: SiteHeaderLink[];
  feature?: SiteHeaderFeature;
}

export interface SiteHeaderAction {
  label: string;
  href?: string;
  onClick?: () => void;
}

/** What `onNavigate` receives. */
export interface SiteHeaderDestination {
  label: string;
  href?: string;
  /** The top level item the destination belongs to. */
  section?: string;
}

export interface SiteHeaderProps {
  /** `simple` puts links beside the brand, `centered` centers them in a quiet capsule, `mega` opens panels for items with links. */
  variant?: SiteHeaderVariant;
  brand?: { name: string; href?: string; mark?: ReactNode };
  items?: SiteHeaderItem[];
  /** Value of the item that holds the current page (controlled). */
  current?: string;
  /** Initial current item when uncontrolled. */
  defaultCurrent?: string;
  /** Called when a destination inside an item is chosen, with that item's value. */
  onCurrentChange?: (value: string) => void;
  /** Called for every destination: items, panel links, the brand, and actions with an href. */
  onNavigate?: (destination: SiteHeaderDestination) => void;
  /** A quiet action before the primary one, such as Sign in. Pass null to hide it. */
  secondaryAction?: SiteHeaderAction | null;
  /** The one primary action at the end of the bar. Pass null to hide it. */
  primaryAction?: SiteHeaderAction | null;
  /** Sticks to the top of its scroll container. Defaults to true. */
  sticky?: boolean;
  /** The element that scrolls, when it is not the window. The header turns solid once it scrolls. */
  scrollContainer?: RefObject<HTMLElement | null>;
  /** Pixels of scroll before the background turns solid. Defaults to 8. */
  scrollThreshold?: number;
  /** Accessible name of the navigation landmark. */
  label?: string;
  className?: string;
}

type Bezier = [number, number, number, number];
const enter = [...motionTokens.ease.enter] as Bezier;
const standard = [...motionTokens.ease.standard] as Bezier;
/** Duration based springs restated as stiffness and damping, so a retarget keeps the velocity already in flight. */
const physical = (visualDuration: number, bounce: number): Transition => {
  const root = 2 * Math.PI / (visualDuration * 1.2);
  return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
};
const GROW = physical(.44, .12), SHRINK = physical(.34, 0), GLIDE = physical(.3, .1), SLIDE = physical(.4, .06);
const HOVER_INTENT = 70, LEAVE_GRACE = 180, TRAVEL = 36;
/** Matches the container query in site-header.module.css. */
const COLLAPSE_BELOW = 760;

/** Panel content slides in from the side of the newly opened item; opening from closed drops in from the bar. */
const faceVariants: Variants = {
  hidden: (direction: number) => ({ opacity: 0, x: direction * TRAVEL, y: direction ? 0 : -6, filter: `blur(${motionTokens.blur.subtle}px)` }),
  shown: { opacity: 1, x: 0, y: 0, filter: "blur(0px)", transition: { x: SLIDE, y: SLIDE, opacity: { duration: motionTokens.duration.fast, ease: enter, delay: .02 }, filter: { duration: motionTokens.duration.standard, ease: enter } } },
  gone: (direction: number) => ({ opacity: 0, x: direction * -TRAVEL * .6, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { x: SLIDE, opacity: { duration: motionTokens.duration.instant, ease: standard }, filter: { duration: motionTokens.duration.instant, ease: standard } } }),
};
const fadeVariants: Variants = { hidden: { opacity: 0 }, shown: { opacity: 1, transition: { duration: motionTokens.duration.fast } }, gone: { opacity: 0, transition: { duration: motionTokens.duration.instant } } };

export function ArcMark(props: { className?: string }) {
  return <svg className={props.className} viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="5.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d="M9 48V29C9 15 19 7 32 7s23 8 23 22v19" />
    <path d="M20 48V31c0-8 5-13 12-13s12 5 12 13v17" />
    <path d="M32 38v10" />
  </svg>;
}

const ICON = { size: 16, strokeWidth: 1.75, "aria-hidden": true } as const;
const curvedFacade = photo("curved-facade");

export const siteHeaderExampleItems: SiteHeaderItem[] = [
  {
    value: "product", label: "Product",
    links: [
      { label: "Components", description: "140 interactive React components", icon: <Boxes {...ICON} /> },
      { label: "Blocks", description: "Complete sections, ready to ship", icon: <PanelsTopLeft {...ICON} /> },
      { label: "Templates", description: "Starter sites with every page", icon: <LayoutTemplate {...ICON} /> },
      { label: "Themes", description: "Tune color, radius, and motion", icon: <Palette {...ICON} /> },
    ],
    feature: { title: "What's new in 2.4", description: "Site headers, footers, and hero sections.", image: { src: curvedFacade.src, alt: curvedFacade.alt } },
  },
  {
    value: "resources", label: "Resources",
    links: [
      { label: "Documentation", description: "Install, theme, and compose", icon: <BookOpen {...ICON} /> },
      { label: "Guides", description: "Patterns for real product work", icon: <Route {...ICON} /> },
      { label: "Changelog", description: "Every release, week by week", icon: <History {...ICON} /> },
      { label: "Community", description: "Questions, answers, and showcases", icon: <MessagesSquare {...ICON} /> },
    ],
  },
  { value: "pricing", label: "Pricing" },
  { value: "customers", label: "Customers" },
];

function useScrolled(threshold: number, container?: RefObject<HTMLElement | null>) {
  const [scrolled, setScrolled] = useState(false);
  useEffect(() => {
    const node = container?.current ?? null;
    const target: HTMLElement | Window = node ?? window;
    const read = () => node ? node.scrollTop : window.scrollY;
    // Turns solid past the threshold and clear again only near the top, so it never flickers at the edge.
    const update = () => { const y = read(); setScrolled(previous => previous ? y > threshold / 2 : y > threshold); };
    update();
    target.addEventListener("scroll", update, { passive: true });
    return () => target.removeEventListener("scroll", update);
  }, [threshold, container]);
  return scrolled;
}

/** A panel face reports its natural height while current; a leaving face floats out of flow and turns inert. */
function Face({ direction, reduced, onHeight, children }: { direction: number; reduced: boolean; onHeight: (height: number) => void; children: ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  const present = useIsPresent();
  useLayoutEffect(() => {
    const node = ref.current;
    if (!node || !present) return;
    onHeight(node.offsetHeight);
    if (typeof ResizeObserver === "undefined") return;
    const observer = new ResizeObserver(() => onHeight(node.offsetHeight));
    observer.observe(node);
    return () => observer.disconnect();
  }, [present, onHeight]);
  return <motion.div ref={ref} className={styles.face} data-face="" data-leaving={present ? undefined : ""} inert={!present} custom={direction} variants={reduced ? fadeVariants : faceVariants} initial="hidden" animate="shown" exit="gone">
    {children}
  </motion.div>;
}

type DestinationProps = Omit<HTMLAttributes<HTMLElement>, "onClick" | "children"> & { link: { href?: string; label: string }; onChoose: (event: ReactMouseEvent) => void; children: ReactNode };
/** Renders an anchor when the destination has an href and a button otherwise, so demos and real sites share one path. */
function Destination({ link, onChoose, children, ...rest }: DestinationProps) {
  return link.href
    ? <a {...(rest as AnchorHTMLAttributes<HTMLAnchorElement>)} href={link.href} onClick={onChoose}>{children}</a>
    : <button {...(rest as ButtonHTMLAttributes<HTMLButtonElement>)} type="button" onClick={onChoose}>{children}</button>;
}

/**
 * A website header in three layouts. It sticks to the top and turns solid once the page scrolls, marks the current section
 * with an indicator that glides between links, opens springy mega menu panels whose content slides in from the side you
 * moved toward, and folds into a menu sheet on narrow containers.
 */
export const SiteHeader = forwardRef<HTMLElement, SiteHeaderProps>(function SiteHeader({
  variant = "mega",
  brand = { name: "Arc" },
  items = siteHeaderExampleItems,
  current: currentProp,
  defaultCurrent,
  onCurrentChange,
  onNavigate,
  secondaryAction = { label: "Sign in" },
  primaryAction = { label: "Get Arc" },
  sticky = true,
  scrollContainer,
  scrollThreshold = 8,
  label = "Main",
  className,
}, ref) {
  const id = useId();
  const reduced = !!useReducedMotion();
  const scrolled = useScrolled(scrollThreshold, scrollContainer);
  const [innerCurrent, setInnerCurrent] = useState(defaultCurrent);
  const current = currentProp ?? innerCurrent;
  const [hovered, setHovered] = useState<string | null>(null);
  const [open, setOpen] = useState<{ value: string; direction: number } | null>(null);
  const [panel, setPanel] = useState<{ height: number | null; grow: boolean }>({ height: null, grow: true });
  const [menuOpen, setMenuOpen] = useState(false);
  const [expanded, setExpanded] = useState<string | null>(null);
  const rootRef = useRef<HTMLElement | null>(null);
  const menuButtonRef = useRef<HTMLButtonElement>(null);
  const triggerRefs = useRef(new Map<string, HTMLButtonElement | HTMLAnchorElement>());
  const panelRef = useRef<HTMLDivElement>(null);
  const openTimer = useRef<number | undefined>(undefined), closeTimer = useRef<number | undefined>(undefined);
  const focusFirst = useRef(false);
  const hasPanels = variant === "mega";
  const openItem = open ? items.find(item => item.value === open.value) : undefined;

  const setRefs = useCallback((node: HTMLElement | null) => {
    rootRef.current = node;
    if (typeof ref === "function") ref(node); else if (ref) ref.current = node;
  }, [ref]);

  const clearTimers = useCallback(() => { window.clearTimeout(openTimer.current); window.clearTimeout(closeTimer.current); }, []);
  useEffect(() => clearTimers, [clearTimers]);

  const openPanel = useCallback((value: string | null) => {
    setOpen(previous => {
      if (!value) return null;
      if (previous?.value === value) return previous;
      const from = previous ? items.findIndex(item => item.value === previous.value) : -1;
      const to = items.findIndex(item => item.value === value);
      return { value, direction: from < 0 ? 0 : Math.sign(to - from) };
    });
    if (!value) setPanel({ height: null, grow: true });
  }, [items]);

  const close = useCallback((restoreFocus = false) => {
    clearTimers();
    const was = open?.value;
    openPanel(null);
    if (restoreFocus && was) triggerRefs.current.get(was)?.focus();
  }, [open, openPanel, clearTimers]);

  const closeMenu = useCallback((restoreFocus = false) => {
    setMenuOpen(false);
    setExpanded(null);
    if (restoreFocus) menuButtonRef.current?.focus();
  }, []);

  const choose = useCallback((destination: SiteHeaderDestination, section: string | undefined) => {
    if (section) {
      if (currentProp === undefined) setInnerCurrent(section);
      onCurrentChange?.(section);
    }
    onNavigate?.(destination);
    close();
    closeMenu();
  }, [close, closeMenu, currentProp, onCurrentChange, onNavigate]);

  // Outside presses and Escape close whichever layer is open.
  useEffect(() => {
    if (!open && !menuOpen) return;
    const onPointer = (event: PointerEvent) => { if (!rootRef.current?.contains(event.target as Node)) { close(); closeMenu(); } };
    const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") { if (open) close(true); else closeMenu(true); } };
    document.addEventListener("pointerdown", onPointer);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("pointerdown", onPointer); document.removeEventListener("keydown", onKey); };
  }, [open, menuOpen, close, closeMenu]);

  // The sheet belongs to narrow layouts: widening the container closes it, and it holds the page still while open.
  useEffect(() => {
    const node = rootRef.current;
    if (!node || typeof ResizeObserver === "undefined") return;
    const observer = new ResizeObserver(([entry]) => {
      if (!entry) return;
      if (entry.contentRect.width >= COLLAPSE_BELOW) { setMenuOpen(false); setExpanded(null); } else setOpen(null);
    });
    observer.observe(node);
    return () => observer.disconnect();
  }, []);
  useEffect(() => {
    if (!menuOpen) return;
    const scroller: HTMLElement = scrollContainer?.current ?? document.documentElement;
    const previous = scroller.style.getPropertyValue("overflow");
    scroller.style.setProperty("overflow", "hidden");
    return () => { if (previous) scroller.style.setProperty("overflow", previous); else scroller.style.removeProperty("overflow"); };
  }, [menuOpen, scrollContainer]);

  useEffect(() => {
    if (!open || !focusFirst.current) return;
    focusFirst.current = false;
    const frame = requestAnimationFrame(() => panelRef.current?.querySelector<HTMLElement>("[data-panel-link]")?.focus());
    return () => cancelAnimationFrame(frame);
  }, [open]);

  function onTriggerPointerEnter(event: ReactPointerEvent, value: string) {
    if (event.pointerType !== "mouse") return;
    window.clearTimeout(closeTimer.current);
    window.clearTimeout(openTimer.current);
    if (open) openPanel(value);
    else openTimer.current = window.setTimeout(() => openPanel(value), HOVER_INTENT);
  }
  function onRegionPointerLeave(event: ReactPointerEvent) {
    if (event.pointerType !== "mouse") return;
    window.clearTimeout(openTimer.current);
    closeTimer.current = window.setTimeout(() => openPanel(null), LEAVE_GRACE);
  }
  function onRegionPointerEnter(event: ReactPointerEvent) {
    if (event.pointerType === "mouse") window.clearTimeout(closeTimer.current);
  }

  function onNavKeyDown(event: ReactKeyboardEvent<HTMLElement>) {
    if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
    const triggers = Array.from(event.currentTarget.querySelectorAll<HTMLElement>("[data-nav-item]"));
    const index = triggers.indexOf(document.activeElement as HTMLElement);
    if (index < 0) return;
    event.preventDefault();
    const nextIndex = (index + (event.key === "ArrowRight" ? 1 : -1) + triggers.length) % triggers.length;
    triggers[nextIndex]!.focus();
    if (open) { const item = items[nextIndex]; openPanel(item && hasPanels && item.links?.length ? item.value : null); }
  }
  function onTriggerKeyDown(event: ReactKeyboardEvent, value: string) {
    if (event.key === "ArrowDown") { event.preventDefault(); focusFirst.current = true; openPanel(value); if (open?.value === value) panelRef.current?.querySelector<HTMLElement>("[data-panel-link]")?.focus(); }
  }
  function onPanelKeyDown(event: ReactKeyboardEvent<HTMLDivElement>) {
    if (event.key !== "ArrowDown" && event.key !== "ArrowUp" && event.key !== "Home" && event.key !== "End") return;
    const unique = Array.from(event.currentTarget.querySelectorAll<HTMLElement>("[data-face]:not([data-leaving]) [data-panel-link]"));
    if (!unique.length) return;
    event.preventDefault();
    const index = unique.indexOf(document.activeElement as HTMLElement);
    const next = event.key === "Home" ? 0 : event.key === "End" ? unique.length - 1 : event.key === "ArrowDown" ? Math.min(index + 1, unique.length - 1) : index - 1;
    if (next < 0) { close(true); return; }
    unique[next]?.focus();
  }

  // Growing carries a little life; shrinking settles without overshoot.
  const onHeight = useCallback((height: number) => setPanel(previous => previous.height === height ? previous : { height, grow: previous.height === null || height > previous.height }), []);

  const actionNode = (action: SiteHeaderAction, kind: "secondary" | "primary", extra = "") => {
    const onClick = () => { action.onClick?.(); if (action.href) onNavigate?.({ label: action.label, href: action.href }); closeMenu(); };
    const cls = `${styles.action} ${styles[kind]} ${extra}`;
    return action.href ? <a className={cls} href={action.href} onClick={onClick}>{action.label}</a> : <button type="button" className={cls} onClick={onClick}>{action.label}</button>;
  };

  const brandNode = <Destination link={{ label: brand.name, href: brand.href }} className={styles.brand} onChoose={() => { onNavigate?.({ label: brand.name, href: brand.href }); close(); closeMenu(); }}>
    {brand.mark ?? <ArcMark className={styles.brandMark} />}<span>{brand.name}</span>
  </Destination>;

  return <header
    ref={setRefs}
    className={[styles.header, sticky ? styles.sticky : "", className].filter(Boolean).join(" ")}
    data-variant={variant}
    data-scrolled={scrolled || menuOpen || !!open ? "" : undefined}
  >
    <div className={styles.inner} onPointerLeave={onRegionPointerLeave} onPointerEnter={onRegionPointerEnter}>
      <div className={styles.bar}>
        <div className={styles.brandSlot}>{brandNode}</div>
        <LayoutGroup id={id}>
          <nav className={styles.nav} aria-label={label} onKeyDown={onNavKeyDown} onPointerLeave={() => setHovered(null)}>
            <ul className={styles.navList}>
              {items.map(item => {
                const isCurrent = current === item.value;
                const withPanel = hasPanels && !!item.links?.length;
                const isOpen = open?.value === item.value;
                const panelId = `${id}-panel`;
                const common = {
                  className: styles.navItem,
                  "data-nav-item": "",
                  "data-current": isCurrent ? "" : undefined,
                  "data-open": isOpen ? "" : undefined,
                  onPointerEnter: (event: ReactPointerEvent) => { if (event.pointerType === "mouse") setHovered(item.value); if (withPanel) onTriggerPointerEnter(event, item.value); else if (event.pointerType === "mouse" && open) closeTimer.current = window.setTimeout(() => openPanel(null), LEAVE_GRACE); },
                  onFocus: () => setHovered(null),
                };
                const decorations = <>
                  {hovered === item.value && variant !== "centered" && <motion.span key={`hover-${variant}`} layoutId={`hover-${variant}`} className={styles.hover} transition={reduced ? { duration: 0 } : GLIDE} aria-hidden="true" />}
                  {isCurrent && <motion.span key={`current-${variant}`} layoutId={`current-${variant}`} className={styles.indicator} transition={reduced ? { duration: 0 } : motionTokens.spring.morph} aria-hidden="true" />}
                </>;
                return <li key={item.value} className={styles.navCell}>
                  {withPanel
                    ? <button
                        {...common}
                        ref={(node: HTMLButtonElement | null) => { if (node) triggerRefs.current.set(item.value, node); else triggerRefs.current.delete(item.value); }}
                        type="button"
                        aria-expanded={isOpen}
                        aria-controls={isOpen ? panelId : undefined}
                        onClick={() => { clearTimers(); openPanel(isOpen ? null : item.value); }}
                        onKeyDown={event => onTriggerKeyDown(event, item.value)}
                      >
                        {decorations}
                        <span className={styles.navLabel}>{item.label}</span>
                        <ChevronDown className={styles.chevron} size={14} strokeWidth={2} aria-hidden="true" />
                      </button>
                    : <Destination
                        link={item}
                        {...common}
                        aria-current={isCurrent ? "page" : undefined}
                        onChoose={() => choose({ label: item.label, href: item.href, section: item.value }, item.value)}
                      >
                        {decorations}
                        <span className={styles.navLabel}>{item.label}</span>
                      </Destination>}
                </li>;
              })}
            </ul>
          </nav>
        </LayoutGroup>
        <div className={styles.actions}>
          {secondaryAction && actionNode(secondaryAction, "secondary", styles.wideOnly)}
          {primaryAction && actionNode(primaryAction, "primary")}
          <button ref={menuButtonRef} type="button" className={styles.menuButton} aria-expanded={menuOpen} aria-controls={`${id}-sheet`} aria-label={menuOpen ? "Close menu" : "Open menu"} onClick={() => { if (menuOpen) closeMenu(); else setMenuOpen(true); }}>
            <AnimatePresence initial={false} mode="popLayout">
              <motion.span key={menuOpen ? "close" : "open"} className={styles.menuIcon} initial={reduced ? { opacity: 0 } : { opacity: 0, rotate: menuOpen ? -45 : 45, scale: .8 }} animate={{ opacity: 1, rotate: 0, scale: 1 }} exit={reduced ? { opacity: 0 } : { opacity: 0, rotate: menuOpen ? 45 : -45, scale: .8 }} transition={reduced ? { duration: 0 } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.instant } }}>
                {menuOpen ? <X size={20} strokeWidth={1.75} aria-hidden="true" /> : <Menu size={20} strokeWidth={1.75} aria-hidden="true" />}
              </motion.span>
            </AnimatePresence>
          </button>
        </div>
      </div>

      {hasPanels && <AnimatePresence>
        {openItem?.links && <motion.div
          key="panel"
          id={`${id}-panel`}
          ref={panelRef}
          className={styles.panel}
          role="region"
          aria-label={openItem.label}
          onKeyDown={onPanelKeyDown}
          initial={reduced ? { opacity: 0 } : { opacity: 0, y: -6, scale: .985 }}
          animate={{ opacity: 1, y: 0, scale: 1, height: panel.height ?? "auto" }}
          exit={reduced ? { opacity: 0, transition: { duration: motionTokens.duration.instant } } : { opacity: 0, y: -4, scale: .99, transition: { duration: motionTokens.duration.exit, ease: standard } }}
          transition={reduced ? { duration: 0 } : { height: panel.grow ? GROW : SHRINK, y: GROW, scale: GROW, opacity: { duration: motionTokens.duration.fast, ease: enter } }}
        >
          <AnimatePresence initial={false} custom={open?.direction ?? 0}>
            <Face key={openItem.value} direction={open?.direction ?? 0} reduced={reduced} onHeight={onHeight}>
              <div className={styles.faceGrid} data-featured={openItem.feature ? "" : undefined}>
                <ul className={styles.panelLinks}>
                  {openItem.links.map(link => <li key={link.label}>
                    <Destination link={link} className={styles.panelLink} data-panel-link="" onChoose={() => choose({ label: link.label, href: link.href, section: openItem.value }, openItem.value)}>
                      {link.icon && <span className={styles.panelIcon}>{link.icon}</span>}
                      <span className={styles.panelText}><span>{link.label}</span>{link.description && <span>{link.description}</span>}</span>
                    </Destination>
                  </li>)}
                </ul>
                {openItem.feature && <Destination link={{ label: openItem.feature.title, href: openItem.feature.href }} className={styles.feature} data-panel-link="" onChoose={() => choose({ label: openItem.feature!.title, href: openItem.feature!.href, section: openItem.value }, openItem.value)}>
                  {openItem.feature.image && <span className={styles.featureImage}><Image src={openItem.feature.image.src} alt={openItem.feature.image.alt} fill sizes="260px" /></span>}
                  <span className={styles.featureTitle}>{openItem.feature.title}<ArrowRight size={14} strokeWidth={2} aria-hidden="true" /></span>
                  {openItem.feature.description && <span className={styles.featureText}>{openItem.feature.description}</span>}
                </Destination>}
              </div>
            </Face>
          </AnimatePresence>
        </motion.div>}
      </AnimatePresence>}
    </div>

    <AnimatePresence>
      {menuOpen && <>
        <motion.div key="scrim" className={styles.scrim} onClick={() => closeMenu()} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: reduced ? 0 : motionTokens.duration.standard, ease: standard }} aria-hidden="true" />
        <motion.div
          key="sheet"
          id={`${id}-sheet`}
          className={styles.sheet}
          initial={reduced ? { opacity: 0 } : { height: 0 }}
          animate={reduced ? { opacity: 1 } : { height: "auto" }}
          exit={reduced ? { opacity: 0 } : { height: 0, transition: SHRINK }}
          transition={reduced ? { duration: 0 } : GROW}
        >
          <nav className={styles.sheetInner} aria-label={label}>
            <ul className={styles.sheetList}>
              {items.map((item, index) => {
                const isCurrent = current === item.value;
                const group = !!item.links?.length && variant === "mega";
                const isExpanded = expanded === item.value;
                const rowMotion = { initial: reduced ? false : { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: { duration: motionTokens.duration.standard, ease: enter, delay: reduced ? 0 : .04 + index * motionTokens.stagger.item } } as const;
                return <motion.li key={item.value} className={styles.sheetItem} {...rowMotion}>
                  {group ? <>
                    <button type="button" className={styles.sheetRow} data-current={isCurrent ? "" : undefined} aria-expanded={isExpanded} aria-controls={`${id}-group-${item.value}`} onClick={() => setExpanded(isExpanded ? null : item.value)}>
                      <span>{item.label}</span><ChevronDown className={styles.sheetChevron} size={18} strokeWidth={1.75} aria-hidden="true" />
                    </button>
                    <AnimatePresence initial={false}>
                      {isExpanded && <motion.div key="group" id={`${id}-group-${item.value}`} className={styles.sheetGroup} initial={reduced ? { opacity: 0 } : { height: 0, opacity: 0 }} animate={reduced ? { opacity: 1 } : { height: "auto", opacity: 1 }} exit={reduced ? { opacity: 0 } : { height: 0, opacity: 0 }} transition={reduced ? { duration: 0 } : { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.fast } }}>
                        <ul>{item.links!.map(link => <li key={link.label}>
                          <Destination link={link} className={styles.sheetLink} onChoose={() => choose({ label: link.label, href: link.href, section: item.value }, item.value)}>
                            {link.icon}<span>{link.label}</span>
                          </Destination>
                        </li>)}</ul>
                      </motion.div>}
                    </AnimatePresence>
                  </> : <Destination link={item} className={styles.sheetRow} data-current={isCurrent ? "" : undefined} aria-current={isCurrent ? "page" : undefined} onChoose={() => choose({ label: item.label, href: item.href, section: item.value }, item.value)}>
                    <span>{item.label}</span>{isCurrent && <span className={styles.sheetDot} aria-hidden="true" />}
                  </Destination>}
                </motion.li>;
              })}
            </ul>
            {(secondaryAction || primaryAction) && <motion.div className={styles.sheetActions} initial={reduced ? false : { opacity: 0, y: -6 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: motionTokens.duration.standard, ease: enter, delay: reduced ? 0 : .04 + items.length * motionTokens.stagger.item }}>
              {secondaryAction && actionNode(secondaryAction, "secondary")}
              {primaryAction && actionNode(primaryAction, "primary")}
            </motion.div>}
          </nav>
        </motion.div>
      </>}
    </AnimatePresence>
  </header>;
});

SiteHeader.displayName = "SiteHeader";

const variantOptions = [{ value: "mega", label: "Mega menu" }, { value: "simple", label: "Simple" }, { value: "centered", label: "Centered" }];
const pageLabels: Record<string, string> = { product: "Product", resources: "Resources", pricing: "Pricing", customers: "Customers" };

/** Preview: the header inside a small scrolling page, with a switch between its three layouts. */
export function SiteHeaderBlock({ variant: initial = "mega" }: { variant?: SiteHeaderVariant }) {
  const [variant, setVariant] = useState<SiteHeaderVariant>(initial);
  const [current, setCurrent] = useState("product");
  const [last, setLast] = useState<string | null>(null);
  const scrollRef = useRef<HTMLDivElement>(null);
  return <div className={styles.preview}>
    <SegmentedControl label="Header layout" options={variantOptions} value={variant} onValueChange={value => setVariant(value as SiteHeaderVariant)} />
    <div className={styles.frame} ref={scrollRef}>
      <SiteHeader
        variant={variant}
        current={current}
        onCurrentChange={setCurrent}
        onNavigate={destination => setLast(destination.label)}
        scrollContainer={scrollRef}
        secondaryAction={{ label: "Sign in", onClick: () => setLast("Sign in") }}
        primaryAction={{ label: "Get Arc", onClick: () => setLast("Get Arc") }}
      />
      <div className={styles.page}>
        <div className={styles.pageHero}>
          <h2>{pageLabels[current] ?? "Arc"}</h2>
          <p className={styles.srOnly} aria-live="polite">{last ? `Opened ${last}` : ""}</p>
        </div>
        <div className={styles.pageRows} aria-hidden="true">
          {Array.from({ length: 6 }, (_, index) => <div key={index} className={styles.pageRow}><span /><span /><span /></div>)}
        </div>
      </div>
    </div>
  </div>;
}

export default SiteHeaderBlock;
```

### `registry/blocks/site-header/site-header.module.css`

```css
/* The header is its own size container, so the layout folds by the space it has rather than the viewport. */
.header { position: relative; z-index: 20; container: site-header / inline-size; border-bottom: 1px solid transparent; background: transparent; color: var(--foreground); font-family: var(--font-body); letter-spacing: var(--tracking-body); transition: background-color var(--duration-standard) var(--ease-standard), border-color var(--duration-standard) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard); }
.sticky { position: sticky; top: 0; }
.header[data-scrolled] { border-bottom-color: var(--border); background: var(--background); box-shadow: var(--shadow-resting); }

.inner { position: relative; max-width: 1200px; margin: 0 auto; padding: 0 var(--space-6); }
.bar { display: flex; height: 64px; align-items: center; gap: var(--space-2); }
.header[data-variant="centered"] .bar { display: grid; grid-template-columns: 1fr auto 1fr; }
.brandSlot { display: flex; min-width: 0; }
.brand { display: inline-flex; align-items: center; gap: var(--space-2); border: 0; border-radius: 12px; padding: var(--space-2) var(--space-2) var(--space-2) 0; background: none; color: var(--foreground); font: inherit; font-size: var(--text-base); font-weight: 500; text-decoration: none; cursor: pointer; }
.brandMark { width: 22px; height: 22px; flex: none; }

.nav { min-width: 0; margin-left: var(--space-4); }
.header[data-variant="centered"] .nav { margin: 0; }
.navList { display: flex; align-items: center; gap: 2px; margin: 0; padding: 0; list-style: none; }
.header[data-variant="centered"] .navList { padding: 3px; border: 1px solid var(--border); border-radius: var(--radius-pill); background: var(--surface-muted); }
.navCell { display: flex; }
.navItem { position: relative; isolation: isolate; display: inline-flex; height: 36px; align-items: center; gap: 4px; border: 0; border-radius: 12px; padding: 0 var(--space-3); background: none; color: var(--text-secondary); font: inherit; font-size: var(--text-sm); font-weight: 500; text-decoration: none; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
.header[data-variant="centered"] .navItem { height: 32px; border-radius: var(--radius-pill); padding: 0 14px; }
.navItem:is([data-current], [data-open]) { color: var(--foreground); }
@media (hover: hover) and (pointer: fine) { .navItem:hover { color: var(--foreground); } }
.navLabel { position: relative; }
.chevron { color: var(--text-muted); transition: transform var(--duration-standard) var(--ease-standard); }
.navItem[data-open] .chevron { transform: rotate(180deg); }
.hover { position: absolute; z-index: -1; inset: 0; border-radius: inherit; background: var(--surface-muted); }
.indicator { position: absolute; right: var(--space-3); bottom: 4px; left: var(--space-3); height: 2px; border-radius: 2px; background: var(--accent); }
.navItem:has(.chevron) .indicator { right: calc(var(--space-3) + 18px); }
.header[data-variant="centered"] .indicator { z-index: -1; inset: 0; height: auto; border: 1px solid var(--border); border-radius: inherit; background: var(--surface); box-shadow: var(--shadow-resting); }

.actions { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; }
.header[data-variant="centered"] .actions { justify-self: end; }
.action { display: inline-flex; height: 36px; align-items: center; justify-content: center; border: 1px solid transparent; border-radius: var(--radius-control); padding: 0 14px; font: inherit; font-size: var(--text-sm); font-weight: 500; text-decoration: none; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: transform var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
.action:active { transform: scale(.97); }
.secondary { background: none; color: var(--text-secondary); }
.primary { background: var(--foreground); color: var(--background); }
@media (hover: hover) and (pointer: fine) {
  .secondary:hover { background: var(--surface-muted); color: var(--foreground); }
  .primary:hover { opacity: .9; }
}
.menuButton { position: relative; display: none; width: 40px; height: 40px; place-items: center; border: 0; border-radius: 12px; background: none; color: var(--foreground); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard); }
.menuButton:active { background: var(--surface-muted); }
.menuIcon { display: grid; place-items: center; }

/* Mega menu panel: one surface whose height springs to each section's content. */
.panel { position: absolute; top: calc(100% - 4px); right: var(--space-6); left: var(--space-6); overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-panel); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: 50% 0; }
.face { position: relative; padding: var(--space-3); }
.face[data-leaving] { position: absolute; top: 0; right: 0; left: 0; pointer-events: none; }
.faceGrid { display: grid; gap: var(--space-2); }
.faceGrid[data-featured] { grid-template-columns: minmax(0, 1fr) minmax(200px, 260px); }
.panelLinks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px; margin: 0; padding: 0; list-style: none; }
.panelLink { display: flex; width: 100%; height: 100%; align-items: flex-start; gap: var(--space-3); border: 0; border-radius: 16px; padding: var(--space-3); background: none; color: var(--foreground); font: inherit; text-align: left; text-decoration: none; cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard); }
.panelLink:focus-visible, .feature:focus-visible { background: var(--surface-muted); }
.panelIcon { display: flex; flex: none; margin-top: 2px; color: var(--text-secondary); }
.panelText { display: grid; gap: 2px; min-width: 0; }
.panelText > span:first-child { font-size: var(--text-sm); font-weight: 500; }
.panelText > span:last-child:not(:first-child) { color: var(--text-muted); font-size: var(--text-sm); }
.feature { display: grid; align-content: start; gap: 6px; border: 0; border-radius: 18px; padding: var(--space-2) var(--space-2) var(--space-3); background: var(--surface-muted); color: var(--foreground); font: inherit; text-align: left; text-decoration: none; cursor: pointer; }
.featureImage { position: relative; display: block; overflow: hidden; aspect-ratio: 16 / 10; margin-bottom: 4px; border-radius: 12px; background: var(--border); }
.featureImage img { object-fit: cover; transition: transform var(--duration-considered) var(--ease-standard); }
.featureTitle { display: inline-flex; align-items: center; gap: 6px; padding: 0 var(--space-1); font-size: var(--text-sm); font-weight: 500; }
.featureTitle svg { color: var(--text-muted); transition: transform var(--duration-fast) var(--ease-standard); }
.featureText { padding: 0 var(--space-1); color: var(--text-muted); font-size: var(--text-sm); }
@media (hover: hover) and (pointer: fine) {
  .panelLink:hover { background: var(--surface-muted); }
  .feature:hover .featureImage img { transform: scale(1.03); }
  .feature:hover .featureTitle svg { transform: translateX(2px); }
}

/* Mobile sheet: drops from the bar and pushes nothing; the scrim closes it. */
.scrim { position: absolute; top: 100%; right: 0; left: 0; height: 100dvh; background: oklch(0% 0 0 / .16); }
.sheet { position: absolute; top: 100%; right: 0; left: 0; overflow: hidden; border-bottom: 1px solid var(--border); background: var(--background); }
/* --site-header-sheet-max lets a header inside a fixed height scroller keep its actions in view. */
.sheetInner { max-height: var(--site-header-sheet-max, calc(100dvh - 65px)); overflow-y: auto; overscroll-behavior: contain; padding: var(--space-2) var(--space-4) var(--space-5); }
.sheetList { margin: 0; padding: 0; list-style: none; }
.sheetItem { border-bottom: 1px solid var(--border-subtle); }
.sheetRow { display: flex; width: 100%; min-height: 52px; align-items: center; justify-content: space-between; gap: var(--space-3); border: 0; padding: 0 var(--space-1); background: none; color: var(--foreground); font: inherit; font-size: var(--text-base); font-weight: 500; text-align: left; text-decoration: none; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.sheetChevron { color: var(--text-muted); transition: transform var(--duration-standard) var(--ease-standard); }
.sheetRow[aria-expanded="true"] .sheetChevron { transform: rotate(180deg); }
.sheetDot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
.sheetGroup { overflow: hidden; }
.sheetGroup ul { display: grid; margin: 0; padding: 0 0 var(--space-3); list-style: none; }
.sheetLink { display: flex; width: 100%; min-height: 44px; align-items: center; gap: var(--space-3); border: 0; border-radius: 12px; padding: 0 var(--space-2); background: none; color: var(--text-secondary); font: inherit; font-size: var(--text-sm); text-align: left; text-decoration: none; cursor: pointer; }
.sheetLink:active { background: var(--surface-muted); color: var(--foreground); }
.sheetActions { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2); margin-top: var(--space-5); }
.sheetActions .action { height: var(--control-height-md); }
.sheetActions .secondary { border-color: var(--border); }

@container site-header (max-width: 759px) {
  .inner { padding: 0 var(--space-4); }
  .nav, .wideOnly, .panel { display: none; }
  .menuButton { display: grid; }
  .header[data-variant="centered"] .bar { display: flex; }
}
@container site-header (min-width: 760px) {
  .sheet, .scrim { display: none; }
}

/* Preview frame: a small page that scrolls under the header. */
.preview { display: grid; width: 100%; justify-items: center; gap: var(--space-4); }
.frame { position: relative; width: 100%; height: 520px; overflow-y: auto; overscroll-behavior: contain; border: 1px solid var(--border); border-radius: 20px; background: var(--background); container-type: inline-size; --site-header-sheet-max: calc(520px - 67px); }
.page { margin-top: -65px; }
.pageHero { display: grid; justify-items: center; gap: var(--space-3); padding: 128px var(--space-6) 72px; background: var(--arc-gradient-soft, var(--surface-muted)); text-align: center; }
.pageHero h2 { margin: 0; font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 500; letter-spacing: var(--tracking-display); line-height: var(--leading-display); }
.pageHero p { max-width: 420px; margin: 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
.pageRows { display: grid; gap: var(--space-3); padding: var(--space-6); }
.pageRow { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); }
.pageRow span { height: 120px; border-radius: 16px; background: var(--surface-muted); }
@container (max-width: 520px) { .pageRow { grid-template-columns: 1fr; } .pageRow span:not(:first-child) { display: none; } .pageHero { padding: 112px var(--space-4) 56px; } }

@media (prefers-reduced-motion: reduce) {
  .header, .navItem, .chevron, .action, .panelLink, .featureImage img, .featureTitle svg, .sheetChevron { transition: none; }
  .action:active { transform: none; }
}

.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
```

### `lib/media.ts`

```ts
/**
 * Shared demo media. Files live in `public/media/`, credits in `public/media/CREDITS.md`.
 * Portraits are 400 px squares; photos are 1600 px on the long side.
 */

export interface MediaPerson {
  id: string;
  name: string;
  role?: string;
  src: string;
}

export interface MediaPhoto {
  id: string;
  alt: string;
  src: string;
  width: number;
  height: number;
}

export const people = [
  { id: "emma-collins", name: "Emma Collins", role: "Product designer", src: "/media/people/emma-collins.jpg" },
  { id: "marcus-johnson", name: "Marcus Johnson", role: "Frontend engineer", src: "/media/people/marcus-johnson.jpg" },
  { id: "jasmine-brooks", name: "Jasmine Brooks", role: "Design lead", src: "/media/people/jasmine-brooks.jpg" },
  { id: "olivia-bennett", name: "Olivia Bennett", role: "Platform engineer", src: "/media/people/olivia-bennett.jpg" },
  { id: "sofia-ramirez", name: "Sofia Ramirez", role: "Operations lead", src: "/media/people/sofia-ramirez.jpg" },
  { id: "ryan-sullivan", name: "Ryan Sullivan", role: "Account executive", src: "/media/people/ryan-sullivan.jpg" },
  { id: "hannah-walsh", name: "Hannah Walsh", role: "Customer success", src: "/media/people/hannah-walsh.jpg" },
  { id: "chloe-nguyen", name: "Chloe Nguyen", role: "Data analyst", src: "/media/people/chloe-nguyen.jpg" },
  { id: "ava-mitchell", name: "Ava Mitchell", role: "Marketing manager", src: "/media/people/ava-mitchell.jpg" },
  { id: "daniel-kim", name: "Daniel Kim", role: "Backend engineer", src: "/media/people/daniel-kim.jpg" },
  { id: "jordan-reyes", name: "Jordan Reyes", role: "Support specialist", src: "/media/people/jordan-reyes.jpg" },
  { id: "mateo-alvarez", name: "Mateo Alvarez", role: "Mobile engineer", src: "/media/people/mateo-alvarez.jpg" },
  { id: "tyler-hayes", name: "Tyler Hayes", role: "Sales lead", src: "/media/people/tyler-hayes.jpg" },
  { id: "andre-williams", name: "Andre Williams", role: "Finance partner", src: "/media/people/andre-williams.jpg" },
  { id: "nathan-cole", name: "Nathan Cole", role: "Engineering manager", src: "/media/people/nathan-cole.jpg" },
  { id: "diane-foster", name: "Diane Foster", role: "Chief operating officer", src: "/media/people/diane-foster.jpg" },
] as const satisfies readonly MediaPerson[];

export const photos = [
  { id: "lounge-chair", alt: "A woven oak lounge chair with a sheepskin and linen cushion on a concrete floor", src: "/media/photos/lounge-chair.jpg", width: 1280, height: 1600 },
  { id: "table-lamp", alt: "A white mushroom table lamp glowing beside books and a small vase", src: "/media/photos/table-lamp.jpg", width: 1600, height: 900 },
  { id: "linen-throw", alt: "Folded natural linen throws with fringed edges in soft window light", src: "/media/photos/linen-throw.jpg", width: 1067, height: 1600 },
  { id: "glass-carafe", alt: "A hand pouring water from a ribbed glass carafe into tumblers", src: "/media/photos/glass-carafe.jpg", width: 1280, height: 1600 },
  { id: "stoneware-cups", alt: "Two speckled stoneware cups with a lid on a pale table", src: "/media/photos/stoneware-cups.jpg", width: 1067, height: 1600 },
  { id: "stacked-bowls", alt: "Two stacked speckled ceramic bowls against a dark wall", src: "/media/photos/stacked-bowls.jpg", width: 1600, height: 1067 },
  { id: "ceramic-lamp", alt: "A sculptural ceramic lamp with a linen shade on a walnut sideboard", src: "/media/photos/ceramic-lamp.jpg", width: 1067, height: 1600 },
  { id: "living-room", alt: "A bright living room with timber beams, arched windows, and cream sofas", src: "/media/photos/living-room.jpg", width: 1200, height: 1600 },
  { id: "sunroom", alt: "A sunroom with a round dining table, plants, and windows on three sides", src: "/media/photos/sunroom.jpg", width: 1600, height: 1067 },
  { id: "home-office", alt: "A home office with a wooden desk and deep green walls", src: "/media/photos/home-office.jpg", width: 1600, height: 1200 },
  { id: "reading-chair", alt: "A grey armchair and ottoman with a knit throw in a dark green room", src: "/media/photos/reading-chair.jpg", width: 1600, height: 900 },
  { id: "bedroom", alt: "A made bed with striped linen pillows against an oak headboard", src: "/media/photos/bedroom.jpg", width: 1067, height: 1600 },
  { id: "restaurant", alt: "A warm restaurant dining room with woven pendant lights and a tree", src: "/media/photos/restaurant.jpg", width: 1067, height: 1600 },
  { id: "wine-bar", alt: "A glass carafe of red wine on a bar table in low evening light", src: "/media/photos/wine-bar.jpg", width: 1600, height: 1067 },
  { id: "concert-hall", alt: "Curved stainless steel panels of the Walt Disney Concert Hall against a blue sky", src: "/media/photos/concert-hall.jpg", width: 1600, height: 1143 },
  { id: "curved-facade", alt: "A white tiled building facade with curved balconies", src: "/media/photos/curved-facade.jpg", width: 1600, height: 1067 },
  { id: "pool-house", alt: "A modern glass house beside a long pool under a clear sky", src: "/media/photos/pool-house.jpg", width: 1600, height: 900 },
  { id: "terracotta-waves", alt: "Wavy terracotta walls rising toward a blue sky", src: "/media/photos/terracotta-waves.jpg", width: 1067, height: 1600 },
  { id: "mountain-ridges", alt: "Layered mountain ridges under a warm evening sky", src: "/media/photos/mountain-ridges.jpg", width: 1600, height: 1068 },
  { id: "alpine-lake", alt: "A calm alpine lake reflecting a rocky peak at golden hour", src: "/media/photos/alpine-lake.jpg", width: 1067, height: 1600 },
  { id: "coastline", alt: "A long coastline with waves rolling onto a beach below green cliffs", src: "/media/photos/coastline.jpg", width: 1200, height: 1600 },
  { id: "sea-at-dusk", alt: "A calm sea at dusk with a low island on the horizon", src: "/media/photos/sea-at-dusk.jpg", width: 1067, height: 1600 },
  { id: "lisbon-tram", alt: "A yellow tram on a street lined with historic buildings in Lisbon", src: "/media/photos/lisbon-tram.jpg", width: 1600, height: 1064 },
  { id: "lisbon-bridge", alt: "The 25 de Abril Bridge crossing the Tagus in Lisbon", src: "/media/photos/lisbon-bridge.jpg", width: 1600, height: 1166 },
  { id: "lisbon-rooftops", alt: "Terracotta rooftops of Lisbon running down to the river", src: "/media/photos/lisbon-rooftops.jpg", width: 1280, height: 1600 },
  { id: "salmon-dinner", alt: "Seared salmon with a bright herb salsa and a glass of red wine", src: "/media/photos/salmon-dinner.jpg", width: 1067, height: 1600 },
  { id: "chef-plating", alt: "A chef spooning sauce onto a plated dish in a dark kitchen", src: "/media/photos/chef-plating.jpg", width: 1600, height: 1600 },
] as const satisfies readonly MediaPhoto[];

export type PersonId = (typeof people)[number]["id"];
export type PhotoId = (typeof photos)[number]["id"];

/** Looks up a person by id. */
export function person(id: PersonId): MediaPerson {
  return people.find(entry => entry.id === id)!;
}

/** Looks up a photo by id. */
export function photo(id: PhotoId): MediaPhoto {
  return photos.find(entry => entry.id === id)!;
}

/** Square 400 px avatar path for a person id, for `src` props. */
export const avatar = (id: PersonId) => person(id).src;

/** 800 × 1000 portrait crop of the same photo, for image-led layouts. */
export const portrait = (id: PersonId) => `/media/people/portrait/${id}.jpg`;

/** The first `count` people, for avatar stacks and lists. */
export const peopleSample = (count: number) => people.slice(0, count);
```
