# Page header

> A project page header that folds into a compact bar as you scroll, with tabs whose counts roll.

A shadcn/ui block from the [@silicon-ui](https://ui.teamofsilicons.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/silicon-ui/page-header).

- Page, with a live preview: https://shadcn.exchange/r/silicon-ui/page-header
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/silicon-ui/page-header-light-610808580bca1b45.png
- Registry item: https://ui.teamofsilicons.com/r/page-header.json
- Categories: blocks
- Listed: 2026-10-17, source last changed 2026-10-17

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/silicon-ui/page-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: `@radix-ui/react-dropdown-menu`, `@radix-ui/react-tabs`, `lucide-react`, `motion`
- Registry items: `https://ui.teamofsilicons.com/r/silicon-motion-tokens.json`, `https://ui.teamofsilicons.com/r/animated-counter.json`, `https://ui.teamofsilicons.com/r/avatar.json`, `https://ui.teamofsilicons.com/r/avatar-group.json`, `https://ui.teamofsilicons.com/r/badge.json`, `https://ui.teamofsilicons.com/r/button.json`, `https://ui.teamofsilicons.com/r/progress.json`

## Notes from the registry

Docs and live preview: https://ui.teamofsilicons.com/page-header

## Files

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

```tsx
"use client";

import { Fragment, useEffect, useId, useRef, useState } from "react";
import type { FocusEvent, ReactNode, UIEvent } from "react";
import * as DropdownPrimitive from "@radix-ui/react-dropdown-menu";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { AnimatePresence, LayoutGroup, motion, useReducedMotion, type Transition, type Variants } from "motion/react";
import { Archive, ArchiveRestore, Bell, BellOff, BellRing, CalendarDays, Check, ChevronRight, Circle, CircleAlert, CircleCheck, CircleDashed, Ellipsis, FileSpreadsheet, FileText, Film, Link2, Megaphone, PenTool, Plus, Presentation, type LucideIcon } from "lucide-react";
import { AnimatedCounter } from "../../animated-counter/animated-counter";
import { Avatar } from "../../avatar/avatar";
import { AvatarGroup } from "../../avatar-group/avatar-group";
import { Badge } from "../../badge/badge";
import { Button } from "../../button/button";
import { Progress } from "../../progress/progress";
import { motionTokens } from "../../lib/motion-tokens";
import styles from "./page-header.module.css";

/** The header condenses past CONDENSE_AT and only opens again near the top, so it never flickers at the threshold. */
const CONDENSE_AT = 16;
const EXPAND_AT = 4;
/** Secondary actions fold into the overflow menu when the breadcrumbs would get less room than this, and return with a little slack. */
const LEAD_MIN = 240;
const LEAD_RETURN = 272;
const DONE_AT_START = 31;
const PROJECT_URL = "https://northline.example/projects/checkout-redesign";

type PersonId = "emma" | "marcus" | "ava" | "sofia" | "jasmine";
const people: Record<PersonId, { name: string; src: string }> = {
  emma: { name: "Emma Collins", src: "/media/people/emma-collins.jpg" },
  marcus: { name: "Marcus Johnson", src: "/media/people/marcus-johnson.jpg" },
  ava: { name: "Ava Mitchell", src: "/media/people/ava-mitchell.jpg" },
  sofia: { name: "Sofia Ramirez", src: "/media/people/sofia-ramirez.jpg" },
  jasmine: { name: "Jasmine Brooks", src: "/media/people/jasmine-brooks.jpg" },
};
const members = (["emma", "marcus", "ava", "sofia", "jasmine"] as const).map(key => people[key]);

type Section = "overview" | "issues" | "updates" | "files";
const sections: { value: Section; label: string }[] = [{ value: "overview", label: "Overview" }, { value: "issues", label: "Issues" }, { value: "updates", label: "Updates" }, { value: "files", label: "Files" }];
const order = (section: Section) => sections.findIndex(item => item.value === section);

type Issue = { id: string; title: string; owner: PersonId; label: string; fresh?: boolean };
const startingIssues: Issue[] = [
  { id: "CHK-138", title: "Saved card list clips the expiry date on small screens", owner: "sofia", label: "Bug" },
  { id: "CHK-136", title: "Add an edit link beside each section of the review step", owner: "emma", label: "Design" },
  { id: "CHK-135", title: "Wallet sheet opens twice after a failed card check", owner: "marcus", label: "Bug" },
  { id: "CHK-133", title: "Show the delivery estimate before payment", owner: "ava", label: "Research" },
  { id: "CHK-131", title: "Promo code field loses focus after an invalid code", owner: "sofia", label: "Bug" },
  { id: "CHK-129", title: "Track drop-off between review and pay", owner: "ava", label: "Analytics" },
  { id: "CHK-127", title: "Preselect the last used card for returning customers", owner: "marcus", label: "Feature" },
  { id: "CHK-124", title: "Write clearer copy for declined cards", owner: "emma", label: "Content" },
  { id: "CHK-122", title: "Address autocomplete drops apartment numbers", owner: "sofia", label: "Bug" },
];
const draftIssues = ["Keyboard focus skips the save card checkbox", "Recalculate tax when the shipping country changes", "Pay button needs a pressed and loading state", "Review totals wrap awkwardly at large text sizes"];

type Update = { id: string; author: PersonId; date: string; tone: "success" | "warning"; status: string; body: string; fresh?: boolean };
const startingUpdates: Update[] = [
  { id: "u4", author: "emma", date: "Sep 19", tone: "success", status: "On track", body: "Saved payment methods reached every iOS customer on Thursday. Returning customers now finish checkout 3.1 points more often. The review step is next." },
  { id: "u3", author: "ava", date: "Sep 12", tone: "success", status: "On track", body: "Five usability sessions are done. People trust a single review step as long as the total stays visible while they edit." },
  { id: "u2", author: "marcus", date: "Sep 5", tone: "warning", status: "At risk", body: "Card tokenization is waiting on the payments team. We moved the review step ahead so the October date can hold." },
  { id: "u1", author: "emma", date: "Aug 29", tone: "success", status: "On track", body: "Kickoff. Scope is saved cards, one review step, and a staged rollout starting October 14." },
];
const draftUpdates = ["The review step is in staging behind a flag. If error rates hold through Friday, we open it to 10% of traffic on Monday.", "Declined card copy is final and with support for review. No change to the October 14 rollout."];

const files: { name: string; kind: string; size: string; owner: PersonId; date: string; icon: LucideIcon }[] = [
  { name: "Checkout flows v3", kind: "Design file", size: "18.4 MB", owner: "emma", date: "Sep 20", icon: PenTool },
  { name: "Review step walkthrough", kind: "Video", size: "46 MB", owner: "emma", date: "Sep 18", icon: Film },
  { name: "Payments API notes", kind: "Document", size: "96 KB", owner: "marcus", date: "Sep 16", icon: FileText },
  { name: "Usability findings, round two", kind: "PDF", size: "2.1 MB", owner: "ava", date: "Sep 13", icon: FileText },
  { name: "Rollout plan", kind: "Slides", size: "4.8 MB", owner: "sofia", date: "Sep 11", icon: Presentation },
  { name: "Conversion baseline", kind: "Spreadsheet", size: "640 KB", owner: "ava", date: "Sep 9", icon: FileSpreadsheet },
  { name: "Empty and error states", kind: "Design file", size: "9.2 MB", owner: "emma", date: "Sep 6", icon: PenTool },
  { name: "Launch checklist", kind: "Document", size: "54 KB", owner: "sofia", date: "Sep 2", icon: FileText },
];

const milestoneIcons = { done: CircleCheck, active: CircleDashed, planned: Circle };
const milestones: { name: string; note: string; state: keyof typeof milestoneIcons }[] = [
  { name: "Saved payment methods", note: "Shipped Sep 18", state: "done" },
  { name: "Single review step", note: "In progress, due Oct 1", state: "active" },
  { name: "Staged rollout", note: "Planned for Oct 14", state: "planned" },
];
const activity: { who: PersonId; text: string; time: string }[] = [
  { who: "ava", text: "moved CHK-131 to review", time: "1h ago" },
  { who: "marcus", text: "merged the saved cards endpoint", time: "3h ago" },
  { who: "emma", text: "shared Checkout flows v3", time: "Yesterday" },
  { who: "sofia", text: "closed CHK-119, a double charge on retry", time: "Yesterday" },
  { who: "jasmine", text: "set the rollout date to October 14", time: "Sep 17" },
];

type Notice = { key: number; text: string; tone: "success" | "error" | "neutral" };
type MenuAction = { key: string; label: string; icon: ReactNode; onSelect: () => void; disabled?: boolean; separatorBefore?: boolean };

const still: Transition = { duration: 0 };
const quick: Transition = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
const blur = (px: number) => `blur(${px}px)`;
const icon = { size: 16, strokeWidth: 1.75, "aria-hidden": true } as const;

/** Panels slide a few pixels in the direction of the tab that was chosen. */
const panelSlide: Variants = {
  enter: (direction: number) => ({ opacity: 0, x: direction * 12 }),
  center: { opacity: 1, x: 0, transition: { x: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } } },
  exit: (direction: number) => ({ opacity: 0, x: direction * -8, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }),
};
const panelFade: Variants = { enter: { opacity: 0, x: 0 }, center: { opacity: 1, x: 0, transition: { duration: motionTokens.duration.instant } }, exit: { opacity: 0, x: 0, transition: { duration: motionTokens.duration.instant } } };

/** Rows open and close their own height, so the list closes the gap instead of jumping. */
function rowMotion(reduce: boolean) {
  return reduce
    ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: motionTokens.duration.instant } }
    : { initial: { opacity: 0, height: 0 }, animate: { opacity: 1, height: "auto" }, exit: { opacity: 0, height: 0 }, transition: { height: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.standard] } } };
}

function StatusDot() {
  return <span className={styles.dot} />;
}

/** The overflow button never scales: it anchors the menu. A shared highlight glides between items under the pointer. */
function OverflowMenu({ actions, reduce }: { actions: MenuAction[]; reduce: boolean }) {
  const [highlight, setHighlight] = useState<{ top: number; height: number; glide: boolean } | null>(null);
  const pointer = useRef(false);
  const clearTimer = useRef(0);
  useEffect(() => () => window.clearTimeout(clearTimer.current), []);
  function onFocus(event: FocusEvent<HTMLDivElement>) {
    const item = event.target instanceof HTMLElement ? event.target.closest<HTMLElement>('[role="menuitem"]') : null;
    window.clearTimeout(clearTimer.current);
    if (!item) { clearTimer.current = window.setTimeout(() => setHighlight(null), pointer.current ? 70 : 0); return; }
    const glide = pointer.current;
    setHighlight(current => ({ top: item.offsetTop, height: item.offsetHeight, glide: glide && current !== null }));
  }
  return <DropdownPrimitive.Root onOpenChange={open => { if (open) { window.clearTimeout(clearTimer.current); setHighlight(null); } }}>
    <DropdownPrimitive.Trigger asChild><Button variant="secondary" size="sm" className={styles.iconButton} aria-label="More actions"><Ellipsis size={17} strokeWidth={1.75} aria-hidden="true" /></Button></DropdownPrimitive.Trigger>
    <DropdownPrimitive.Portal>
      <DropdownPrimitive.Content className={styles.menu} align="end" sideOffset={6} collisionPadding={12} loop onFocus={onFocus} onPointerMoveCapture={() => { pointer.current = true; }} onKeyDownCapture={() => { pointer.current = false; }}>
        <motion.span className={styles.highlight} aria-hidden="true" initial={false} animate={highlight ? { y: highlight.top, height: highlight.height, opacity: 1 } : { opacity: 0 }} transition={{ default: highlight?.glide && !reduce ? motionTokens.spring.snappy : still, opacity: { duration: reduce ? 0 : .08 } }} />
        {actions.map(action => <Fragment key={action.key}>
          {action.separatorBefore && <DropdownPrimitive.Separator className={styles.separator} />}
          <DropdownPrimitive.Item className={styles.item} disabled={action.disabled} onSelect={action.onSelect}>{action.icon}{action.label}</DropdownPrimitive.Item>
        </Fragment>)}
      </DropdownPrimitive.Content>
    </DropdownPrimitive.Portal>
  </DropdownPrimitive.Root>;
}

function OverviewPanel({ done, open }: { done: number; open: number }) {
  const total = done + open;
  return <div className={styles.overview}>
    <div className={styles.progress}><Progress value={done} max={total} label={`${done} of ${total} issues done`} showValue /></div>
    <div>
      <h3 className={styles.sectionTitle}>Milestones</h3>
      <ol className={styles.milestones}>{milestones.map(item => {
        const Icon = milestoneIcons[item.state];
        return <li key={item.name} data-state={item.state}><Icon size={16} strokeWidth={1.75} aria-hidden="true" /><span className={styles.milestoneName}>{item.name}</span><span className={styles.note}>{item.note}</span></li>;
      })}</ol>
    </div>
    <div>
      <h3 className={styles.sectionTitle}>Recent activity</h3>
      <ul className={styles.activity}>{activity.map(item => <li key={`${item.who}-${item.text}`}><Avatar name={people[item.who].name} src={people[item.who].src} size="sm" /><p><strong>{people[item.who].name}</strong> {item.text}</p><span className={styles.note}>{item.time}</span></li>)}</ul>
    </div>
  </div>;
}

function IssuesPanel({ issues, closing, reduce, onComplete, onReset, register }: { issues: Issue[]; closing: string[]; reduce: boolean; onComplete: (issue: Issue) => void; onReset: () => void; register: (id: string, node: HTMLButtonElement | null) => void }) {
  const row = rowMotion(reduce);
  return <>
    <ul className={styles.issues} aria-label="Open issues">
      <AnimatePresence initial={false}>
        {issues.map(issue => {
          const isClosing = closing.includes(issue.id);
          const owner = people[issue.owner];
          return <motion.li key={issue.id} className={styles.issue} data-fresh={issue.fresh || undefined} data-closing={isClosing || undefined} {...row}>
            <div className={styles.issueInner}>
              <button ref={node => register(issue.id, node)} type="button" className={styles.check} aria-label={`Mark ${issue.id} done`} aria-disabled={isClosing || undefined} onClick={() => onComplete(issue)}>
                <AnimatePresence initial={false} mode="popLayout">
                  <motion.span key={isClosing ? "done" : "open"} className={styles.checkGlyph} initial={reduce ? { opacity: 0 } : { opacity: 0, scale: .5 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: reduce ? 1 : .5, transition: quick }} transition={reduce ? still : motionTokens.spring.snappy}>
                    {isClosing ? <CircleCheck size={18} strokeWidth={1.75} aria-hidden="true" /> : <Circle size={18} strokeWidth={1.75} aria-hidden="true" />}
                  </motion.span>
                </AnimatePresence>
              </button>
              <span className={styles.issueText}><span className={styles.issueId}>{issue.id}</span><span className={styles.issueTitle}>{issue.title}</span></span>
              <span className={styles.issueLabel}>{issue.label}</span>
              <Avatar name={owner.name} src={owner.src} size="sm" />
            </div>
          </motion.li>;
        })}
      </AnimatePresence>
    </ul>
    {issues.length === 0 && <motion.div className={styles.empty} initial={{ opacity: 0, y: reduce ? 0 : 6 }} animate={{ opacity: 1, y: 0 }} transition={reduce ? { duration: motionTokens.duration.instant } : motionTokens.spring.smooth}>
      <CircleCheck size={24} strokeWidth={1.75} aria-hidden="true" />
      <p>No open issues</p>
      <span>Everything in this project is done.</span>
      <Button variant="secondary" size="sm" onClick={onReset}>Restore sample issues</Button>
    </motion.div>}
  </>;
}

function UpdatesPanel({ updates, reduce }: { updates: Update[]; reduce: boolean }) {
  const row = rowMotion(reduce);
  return <ol className={styles.updates} aria-label="Project updates">
    <AnimatePresence initial={false}>
      {updates.map(update => {
        const author = people[update.author];
        return <motion.li key={update.id} className={styles.update} data-fresh={update.fresh || undefined} {...row}>
          <article className={styles.updateInner}>
            <Avatar name={author.name} src={author.src} size="md" />
            <div>
              <div className={styles.updateHead}><span className={styles.author}>{author.name}</span><span className={styles.note}>{update.date}</span><Badge size="sm" tone={update.tone} icon={<StatusDot />}>{update.status}</Badge></div>
              <p>{update.body}</p>
            </div>
          </article>
        </motion.li>;
      })}
    </AnimatePresence>
  </ol>;
}

function FilesPanel() {
  return <ul className={styles.files} aria-label="Project files">{files.map(file => {
    const Icon = file.icon;
    const owner = people[file.owner];
    return <li key={file.name}><Icon size={16} strokeWidth={1.75} aria-hidden="true" /><span className={styles.fileName}><span>{file.name}</span><span className={styles.note}>{file.kind}, {file.size}</span></span><Avatar name={owner.name} src={owner.src} size="sm" /><span className={styles.fileDate}>{file.date}</span></li>;
  })}</ul>;
}

export function PageHeader() {
  const id = useId();
  const reduce = useReducedMotion() ?? false;
  const bar = useRef<HTMLDivElement>(null);
  const secondary = useRef<HTMLDivElement>(null);
  const trailing = useRef<HTMLDivElement>(null);
  const scroller = useRef<HTMLDivElement>(null);
  const tabList = useRef<HTMLDivElement>(null);
  const titleRef = useRef<HTMLHeadingElement>(null);
  const toggles = useRef(new Map<string, HTMLButtonElement>());
  const timers = useRef(new Set<number>());
  const counters = useRef({ issue: 0, update: 0, notice: 0 });
  const [view, setView] = useState<{ section: Section; direction: number }>({ section: "overview", direction: 1 });
  const [condensed, setCondensed] = useState(false);
  const [layout, setLayout] = useState({ measured: false, collapsed: false, animate: false });
  const [following, setFollowing] = useState(false);
  const [archived, setArchived] = useState(false);
  const [sharing, setSharing] = useState<"idle" | "sending" | "sent">("idle");
  const [issues, setIssues] = useState(startingIssues);
  const [closing, setClosing] = useState<string[]>([]);
  const [done, setDone] = useState(DONE_AT_START);
  const [updates, setUpdates] = useState(startingUpdates);
  const [notice, setNotice] = useState<Notice | null>(null);

  // Only the header width is observed, so a label that grows (Follow to Following) never folds the button you just pressed.
  // The first measurement applies at once; later width changes fold the actions with a spring.
  useEffect(() => {
    const row = bar.current, folded = secondary.current, fixed = trailing.current;
    if (!row || !folded || !fixed || typeof ResizeObserver === "undefined") return;
    const observer = new ResizeObserver(() => {
      const room = row.clientWidth - folded.offsetWidth - fixed.offsetWidth - 16;
      setLayout(current => {
        const collapsed = current.measured && current.collapsed ? room < LEAD_RETURN : room < LEAD_MIN;
        return current.measured && current.collapsed === collapsed ? current : { measured: true, collapsed, animate: current.measured };
      });
    });
    observer.observe(row);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    const pending = timers.current;
    return () => pending.forEach(handle => window.clearTimeout(handle));
  }, []);

  useEffect(() => {
    if (!notice) return;
    const handle = window.setTimeout(() => setNotice(null), 3200);
    return () => window.clearTimeout(handle);
  }, [notice]);

  // Keep the chosen tab in view when the list scrolls sideways on narrow screens.
  useEffect(() => {
    const list = tabList.current;
    const tab = list?.querySelector<HTMLElement>('[role="tab"][data-state="active"]');
    if (!list || !tab) return;
    const start = tab.offsetLeft - 12;
    const end = tab.offsetLeft + tab.offsetWidth + 12 - list.clientWidth;
    const behavior = reduce ? "auto" : "smooth";
    if (list.scrollLeft > start) list.scrollTo({ left: start, behavior });
    else if (list.scrollLeft < end) list.scrollTo({ left: end, behavior });
  }, [view.section, reduce]);

  function later(run: () => void, delay: number) {
    const handle = window.setTimeout(() => { timers.current.delete(handle); run(); }, delay);
    timers.current.add(handle);
  }

  function notify(text: string, tone: Notice["tone"] = "success") {
    counters.current.notice += 1;
    setNotice({ key: counters.current.notice, text, tone });
  }

  /** Switching sections while condensed shows the top of the new panel and keeps the compact bar. */
  function selectSection(next: Section, reveal = false) {
    setView(current => current.section === next ? current : { section: next, direction: order(next) > order(current.section) ? 1 : -1 });
    const node = scroller.current;
    if (node && (reveal || next !== view.section) && node.scrollTop > CONDENSE_AT + 1) node.scrollTop = CONDENSE_AT + 1;
  }

  function onScroll(event: UIEvent<HTMLDivElement>) {
    const top = event.currentTarget.scrollTop;
    setCondensed(current => (current ? top > EXPAND_AT : top > CONDENSE_AT));
  }

  function backToTop() {
    scroller.current?.scrollTo({ top: 0, behavior: reduce ? "auto" : "smooth" });
    titleRef.current?.focus({ preventScroll: true });
  }

  function toggleFollow() {
    const next = !following;
    setFollowing(next);
    notify(next ? "Following Checkout redesign" : "Stopped following Checkout redesign", next ? "success" : "neutral");
  }

  function shareUpdate() {
    if (archived || sharing !== "idle") return;
    setSharing("sending");
    later(() => {
      const index = counters.current.update++;
      const key = `draft-${index}`;
      setUpdates(list => [{ id: key, author: "jasmine", date: "Just now", tone: "success", status: "On track", body: draftUpdates[index % draftUpdates.length]!, fresh: true }, ...list]);
      setSharing("sent");
      selectSection("updates", true);
      notify("Update shared with the project team");
      later(() => setSharing("idle"), 1800);
      later(() => setUpdates(list => list.map(item => item.id === key ? { ...item, fresh: false } : item)), 1800);
    }, 700);
  }

  function createIssue() {
    if (archived) return;
    const index = counters.current.issue++;
    const issue: Issue = { id: `CHK-${139 + index}`, title: draftIssues[index % draftIssues.length]!, owner: "jasmine", label: "Triage", fresh: true };
    setIssues(list => [issue, ...list]);
    selectSection("issues", true);
    notify(`${issue.id} created and assigned to you`);
    later(() => setIssues(list => list.map(item => item.id === issue.id ? { ...item, fresh: false } : item)), 1800);
  }

  function completeIssue(issue: Issue) {
    if (closing.includes(issue.id)) return;
    const index = issues.findIndex(item => item.id === issue.id);
    const remaining = issues.filter(item => item.id !== issue.id && !closing.includes(item.id));
    const neighbor = remaining[Math.min(index, remaining.length - 1)];
    setClosing(list => [...list, issue.id]);
    later(() => {
      const hadFocus = document.activeElement === toggles.current.get(issue.id);
      setIssues(list => list.filter(item => item.id !== issue.id));
      setClosing(list => list.filter(item => item !== issue.id));
      setDone(count => count + 1);
      notify(`${issue.id} marked done`);
      if (hadFocus) (neighbor ? toggles.current.get(neighbor.id) : scroller.current?.querySelector<HTMLElement>('[role="tabpanel"][data-state="active"]'))?.focus();
    }, 380);
  }

  function resetIssues() {
    setIssues(startingIssues);
    setDone(DONE_AT_START);
    notify("Sample issues restored", "neutral");
  }

  function toggleArchive() {
    const next = !archived;
    setArchived(next);
    notify(next ? "Project archived. New issues and updates are paused." : "Project restored", "neutral");
  }

  async function copyLink() {
    try {
      await navigator.clipboard.writeText(PROJECT_URL);
      notify("Link copied");
    } catch {
      notify("Couldn't copy the link. Try again from the address bar.", "error");
    }
  }

  function openCrumb(label: string) {
    notify(`${label} would open here.`, "neutral");
  }

  const status = archived ? { tone: "neutral" as const, label: "Archived" } : { tone: "success" as const, label: "On track" };
  const menuActions: MenuAction[] = [
    ...(layout.collapsed ? [
      { key: "follow", label: following ? "Unfollow" : "Follow", icon: following ? <BellOff {...icon} /> : <Bell {...icon} />, onSelect: toggleFollow },
      { key: "share", label: "Share update", icon: <Megaphone {...icon} />, onSelect: shareUpdate, disabled: archived || sharing !== "idle" },
    ] : []),
    { key: "copy", label: "Copy link", icon: <Link2 {...icon} />, onSelect: copyLink, separatorBefore: layout.collapsed },
    { key: "archive", label: archived ? "Restore project" : "Archive project", icon: archived ? <ArchiveRestore {...icon} /> : <Archive {...icon} />, onSelect: toggleArchive },
  ];

  /** Leaving copy fades fast; arriving copy settles on the smooth spring with a short blur. */
  const swap = (entering: boolean): Transition => reduce ? still : { ...motionTokens.spring.smooth, opacity: { duration: entering ? motionTokens.duration.standard : motionTokens.duration.fast, ease: [...motionTokens.ease.standard], delay: entering ? .05 : 0 }, filter: { duration: entering ? motionTokens.duration.standard : motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
  const shown = { opacity: 1, y: 0, scale: 1, filter: blur(0) };

  return (
    <TabsPrimitive.Root asChild value={view.section} onValueChange={value => selectSection(value as Section)}>
      <section className={styles.frame} aria-labelledby={`${id}-title`} data-measured={layout.measured || undefined}>
        <header className={styles.header}>
          <div ref={bar} className={styles.bar}>
            <div className={styles.lead}>
              <motion.nav className={styles.crumbs} aria-label="Breadcrumb" inert={condensed} initial={false} animate={condensed ? { opacity: 0, y: -8, filter: blur(motionTokens.blur.subtle) } : shown} transition={swap(!condensed)}>
                <ol>
                  <li className={styles.rootCrumb}><button type="button" className={styles.crumb} onClick={() => openCrumb("Northline")}>Northline</button><ChevronRight size={14} strokeWidth={1.75} aria-hidden="true" /></li>
                  <li><button type="button" className={styles.crumb} onClick={() => openCrumb("Projects")}>Projects</button><ChevronRight size={14} strokeWidth={1.75} aria-hidden="true" /></li>
                  <li><span className={styles.current} aria-current="page">Checkout redesign</span></li>
                </ol>
              </motion.nav>
              <motion.button type="button" className={styles.compact} inert={!condensed} aria-label="Checkout redesign, back to top" onClick={backToTop} initial={false} animate={condensed ? shown : { opacity: 0, y: 14, filter: blur(motionTokens.blur.soft) }} transition={swap(condensed)}>
                <span className={styles.compactTitle}>Checkout redesign</span>
                <Badge className={styles.compactBadge} size="sm" tone={status.tone} icon={<StatusDot />}>{status.label}</Badge>
              </motion.button>
            </div>

            <div className={styles.actions}>
              <motion.div className={styles.secondary} inert={layout.collapsed} initial={false} animate={layout.collapsed ? { width: 0, opacity: 0 } : { width: "auto", opacity: 1 }} transition={layout.animate && !reduce ? { width: motionTokens.spring.smooth, opacity: { duration: layout.collapsed ? motionTokens.duration.fast : motionTokens.duration.standard, ease: [...motionTokens.ease.standard], delay: layout.collapsed ? .04 : .08 } } : still}>
                <div ref={secondary} className={styles.secondaryInner}>
                  <Button variant="secondary" size="sm" onClick={toggleFollow}>{following ? <><BellRing {...icon} /> Following</> : <><Bell {...icon} /> Follow</>}</Button>
                  <Button variant="secondary" size="sm" loading={sharing === "sending"} disabled={archived} onClick={shareUpdate}>{sharing === "sent" ? <><Check {...icon} /> Shared</> : <><Megaphone {...icon} /> Share update</>}</Button>
                </div>
              </motion.div>
              <div ref={trailing} className={styles.trailing}>
                <OverflowMenu actions={menuActions} reduce={reduce} />
                <Button className={styles.create} size="sm" aria-label="New issue" disabled={archived} onClick={createIssue}><Plus {...icon} /><span className={styles.createText}>New issue</span></Button>
              </div>
            </div>
          </div>

          <motion.div className={styles.intro} initial={false} animate={{ height: condensed ? 0 : "auto" }} transition={reduce ? still : motionTokens.spring.smooth}>
            <div className={styles.introInner}>
              <motion.div className={styles.titleRow} initial={false} animate={condensed ? { opacity: 0, y: -10, scale: .62, filter: blur(motionTokens.blur.subtle) } : shown} transition={swap(!condensed)}>
                <h2 ref={titleRef} id={`${id}-title`} className={styles.title} tabIndex={-1}>Checkout redesign</h2>
                <Badge tone={status.tone} icon={<StatusDot />}>{status.label}</Badge>
              </motion.div>
              <motion.div className={styles.details} initial={false} animate={condensed ? { opacity: 0, y: -6 } : { opacity: 1, y: 0 }} transition={swap(!condensed)}>
                <p className={styles.description}>Rebuilding mobile checkout around saved payment methods and a single review step, then rolling it out in stages from October 14.</p>
                <div className={styles.meta}>
                  <AvatarGroup members={members} max={4} size="sm" label="Project members" />
                  <span>Led by <strong>Emma Collins</strong></span>
                  <span className={styles.metaIcon}><CalendarDays size={14} strokeWidth={1.75} aria-hidden="true" />Target Oct 14</span>
                </div>
              </motion.div>
            </div>
          </motion.div>

          <LayoutGroup id={id}>
            <TabsPrimitive.List asChild aria-label="Project sections">
              <motion.div ref={tabList} layoutScroll className={styles.tabs}>
                {sections.map(item => {
                  const count = item.value === "issues" ? issues.length : item.value === "updates" ? updates.length : item.value === "files" ? files.length : null;
                  return <TabsPrimitive.Trigger key={item.value} value={item.value} className={styles.tab}>
                    <span className={styles.tabLabel}>{item.label}</span>
                    {count !== null && <span className={styles.count}><AnimatedCounter value={count} /></span>}
                    {view.section === item.value && <motion.span className={styles.indicator} layoutId="indicator" layoutDependency={view.section} transition={reduce ? still : motionTokens.spring.morph} aria-hidden="true" />}
                  </TabsPrimitive.Trigger>;
                })}
              </motion.div>
            </TabsPrimitive.List>
          </LayoutGroup>
        </header>

        <div ref={scroller} className={styles.scroller} onScroll={onScroll}>
          <AnimatePresence initial={false} mode="popLayout" custom={view.direction}>
            <TabsPrimitive.Content key={view.section} value={view.section} forceMount asChild>
              <motion.div className={styles.panel} custom={view.direction} variants={reduce ? panelFade : panelSlide} initial="enter" animate="center" exit="exit">
                {view.section === "overview" && <OverviewPanel done={done} open={issues.length} />}
                {view.section === "issues" && <IssuesPanel issues={issues} closing={closing} reduce={reduce} onComplete={completeIssue} onReset={resetIssues} register={(key, node) => { if (node) toggles.current.set(key, node); else toggles.current.delete(key); }} />}
                {view.section === "updates" && <UpdatesPanel updates={updates} reduce={reduce} />}
                {view.section === "files" && <FilesPanel />}
              </motion.div>
            </TabsPrimitive.Content>
          </AnimatePresence>
        </div>

        <div className={styles.toastLayer} aria-hidden="true">
          <AnimatePresence initial={false} mode="popLayout">
            {notice && <motion.div key={notice.key} className={styles.toast} data-tone={notice.tone} initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14, filter: blur(motionTokens.blur.soft) }} animate={{ opacity: 1, y: 0, filter: blur(0) }} exit={reduce ? { opacity: 0, transition: still } : { opacity: 0, y: 8, filter: blur(motionTokens.blur.subtle), transition: quick }} transition={reduce ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] }, filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }}>
              {notice.tone === "success" ? <Check {...icon} /> : notice.tone === "error" ? <CircleAlert {...icon} /> : null}
              <span>{notice.text}</span>
            </motion.div>}
          </AnimatePresence>
        </div>
        <p className={styles.srOnly} role="status" aria-live="polite">{notice?.text}</p>
      </section>
    </TabsPrimitive.Root>
  );
}

export default PageHeader;
```

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

```css
/* The frame is the page: a fixed header over a scrolling body. Breakpoints follow the frame width, not the viewport. */
.frame { --pad: 28px; position: relative; display: flex; width: min(100%, 980px); height: 640px; min-width: 0; flex-direction: column; margin-inline: auto; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); color: var(--foreground); font-family: var(--font-body); font-size: var(--text-sm); letter-spacing: var(--tracking-body); line-height: var(--leading-body); container: page-header / inline-size; }

.header { position: relative; z-index: 1; flex: none; padding: var(--space-inset) var(--pad) 0; background: var(--surface); box-shadow: inset 0 -1px 0 var(--border); }
.bar { display: grid; min-height: var(--control-height-sm); grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-inset); }
/* Breadcrumbs and the compact title share one cell and trade places when the header condenses. */
.lead { display: grid; min-width: 0; align-items: center; }
.lead > * { grid-area: 1 / 1; min-width: 0; }

.crumbs ol { display: flex; min-width: 0; align-items: center; gap: var(--space-related); margin: 0; padding: 0; list-style: none; }
.crumbs li { display: inline-flex; flex: none; align-items: center; gap: var(--space-related); color: var(--text-muted); white-space: nowrap; }
.crumbs li:last-child { flex: 0 1 auto; min-width: 0; }
.crumbs li > svg { flex: none; color: var(--border-strong); }
.crumb { margin: 0; border: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); padding: var(--space-tight) var(--space-hair); background: none; color: var(--text-muted); font: inherit; letter-spacing: inherit; cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); }
.current { display: block; min-width: 0; overflow: hidden; color: var(--text-secondary); text-overflow: ellipsis; }

.compact { display: inline-flex; max-width: calc(100% + 8px); justify-self: start; align-items: center; gap: var(--space-group); margin-left: calc(var(--space-related) * -1); border: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); padding: var(--space-tight) var(--space-related); background: transparent; color: var(--foreground); font: inherit; letter-spacing: inherit; text-align: left; cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard); }
.compactTitle { min-width: 0; overflow: hidden; font-size: var(--text-base); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
.compactBadge { flex: none; }
.dot { width: 6px; height: 6px; border-radius: 50%; corner-shape: round; background: currentColor; }

/* Secondary actions fold to the right, under the overflow button, as their slot narrows. Only the right edge clips. */
.actions { display: flex; align-items: center; justify-content: flex-end; }
.secondary { display: flex; flex: none; clip-path: inset(-8px 0 -8px -8px); }
.secondaryInner { display: flex; flex: none; gap: var(--space-related); padding-right: var(--space-related); }
.trailing { display: flex; flex: none; align-items: center; gap: var(--space-related); }
.actions .iconButton { width: var(--control-height-sm); padding-inline: 0; }

.intro { overflow: clip; }
.introInner { display: grid; justify-items: start; gap: var(--space-group); padding: var(--space-comfort) 0 var(--space-comfort); }
.titleRow { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-related) var(--space-inset); transform-origin: 0 0; }
.title { 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); }
.title:focus { outline: none; }
.details { display: grid; justify-items: start; gap: var(--space-inset); }
.description { max-width: 60ch; margin: 0; color: var(--text-secondary); text-wrap: pretty; }
.meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-related) var(--space-comfort); color: var(--text-muted); font-size: var(--text-xs); }
.metaIcon { display: inline-flex; align-items: center; gap: var(--space-related); }
.meta strong { color: var(--text-secondary); font-weight: 500; }

/* Tabs bleed to the frame edges so the rule under them spans the page; the first label lines up with the title. */
.tabs { position: relative; display: flex; gap: var(--space-hair); margin: var(--space-hair) calc(var(--pad) * -1) 0; padding: 0 calc(var(--pad) - var(--space-group)); overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; }
.tabs::-webkit-scrollbar { display: none; }
.tab { position: relative; display: inline-flex; height: 44px; flex: none; align-items: center; gap: var(--space-related); border: 0; padding: 0 var(--space-group); background: none; color: var(--text-secondary); font: inherit; font-weight: 500; letter-spacing: inherit; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
.tab::before { position: absolute; inset: var(--space-related) 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 4.5%, transparent); content: ""; opacity: 0; transition: opacity var(--duration-fast) var(--ease-standard); }
.tab[data-state="active"] { color: var(--foreground); }
.tabLabel, .count { position: relative; }
.count { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.tab[data-state="active"] .count { color: var(--text-secondary); }
/* The shared counter renders at display size by default; here it takes the tab's type. */
.count > span { color: inherit; font-family: inherit; font-size: inherit; font-weight: inherit; letter-spacing: inherit; line-height: inherit; }
.indicator { position: absolute; right: var(--space-group); bottom: 0; left: var(--space-group); height: 2px; border-radius: var(--radius-full) var(--radius-full) 0 0; corner-shape: round; background: var(--accent); }

.scroller { position: relative; flex: 1 1 auto; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding: var(--space-block) var(--pad) var(--space-screen); scrollbar-color: var(--border-strong) transparent; scrollbar-width: thin; }
/* Every panel is taller than the condensed viewport, so condensing never clamps the scroll back to the top. */
.panel { min-height: 600px; }
.sectionTitle { margin: 0 0 var(--space-related); font-size: var(--text-sm); font-weight: 500; }
.note { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; white-space: nowrap; }

.overview { display: grid; gap: var(--space-block); }
.progress { max-width: 480px; }
.milestones, .activity, .issues, .updates, .files { margin: 0; padding: 0; list-style: none; }
.milestones li { display: grid; min-height: 48px; grid-template-columns: 16px minmax(0, 1fr) auto; align-items: center; gap: var(--space-hair) var(--space-group); border-bottom: 1px solid var(--border-subtle); }
.milestones li:last-child, .activity li:last-child, .files li:last-child { border-bottom: 0; }
.milestones li > svg { color: var(--text-muted); }
.milestones li[data-state="done"] > svg { color: var(--success); }
.milestones li[data-state="active"] > svg { color: var(--accent); }
.activity li { display: grid; min-height: 52px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--space-group); border-bottom: 1px solid var(--border-subtle); }
.activity p { margin: 0; color: var(--text-secondary); }
.activity strong { color: var(--foreground); font-weight: 500; }

.issue, .update { overflow-x: visible; overflow-y: clip; border-bottom: 1px solid var(--border-subtle); }
.issue:last-child, .update:last-child { border-bottom-color: transparent; }
.issueInner { position: relative; isolation: isolate; display: grid; min-height: 52px; grid-template-columns: 32px minmax(0, 1fr) auto auto; align-items: center; gap: var(--space-group); margin-inline: calc(var(--space-group) * -1); padding: var(--space-tight) var(--space-group); }
.check { position: relative; display: grid; width: 32px; height: 32px; place-items: center; margin-left: calc(var(--space-related) * -1); border: 0; border-radius: 50%; corner-shape: round; padding: 0; background: none; color: var(--text-muted); cursor: pointer; transition: color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
.checkGlyph { display: grid; place-items: center; }
.issue[data-closing] .check { color: var(--success); cursor: default; }
.issueText { display: flex; min-width: 0; align-items: baseline; gap: var(--space-group); }
.issueId { flex: none; min-width: 60px; color: var(--text-muted); font-size: var(--text-xs); }
.issueTitle { min-width: 0; overflow: hidden; text-decoration: line-through transparent; text-overflow: ellipsis; white-space: nowrap; transition: color var(--duration-standard) var(--ease-standard), text-decoration-color var(--duration-standard) var(--ease-standard); }
.issue[data-closing] .issueTitle { color: var(--text-muted); text-decoration-color: currentColor; }
.issueLabel { color: var(--text-muted); font-size: var(--text-xs); }
/* A new row arrives lit and settles to the page, so it is easy to find without a lasting marker. */
.issue[data-fresh] .issueInner::before, .update[data-fresh] .updateInner::before { position: absolute; z-index: -1; inset: var(--space-tight) 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6%, transparent); content: ""; animation: arrived 1.8s var(--ease-standard) both; }
@keyframes arrived { 0%, 35% { opacity: 1; } 100% { opacity: 0; } }

.updateInner { position: relative; isolation: isolate; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--space-inset); margin-inline: calc(var(--space-group) * -1); padding: var(--space-comfort) var(--space-group); }
.updateHead { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-tight) var(--space-group); min-height: 22px; }
.author { font-weight: 500; }
.updateInner p { max-width: 64ch; margin: var(--space-related) 0 0; color: var(--text-secondary); text-wrap: pretty; }

.files li { display: grid; min-height: 56px; grid-template-columns: 16px minmax(0, 1fr) auto 52px; align-items: center; gap: var(--space-inset); border-bottom: 1px solid var(--border-subtle); }
.files li > svg { color: var(--text-secondary); }
.fileName { display: grid; min-width: 0; }
.fileName > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fileDate { color: var(--text-muted); font-size: var(--text-xs); text-align: right; white-space: nowrap; }

.empty { display: grid; justify-items: center; gap: var(--space-tight); padding: var(--space-screen) var(--space-inset); text-align: center; }
.empty > svg { margin-bottom: var(--space-related); color: var(--success); }
.empty p { margin: 0; font-size: var(--text-base); font-weight: 500; }
.empty span { color: var(--text-muted); }
.empty button { margin-top: var(--space-inset); }

.toastLayer { position: absolute; z-index: 3; right: var(--space-inset); bottom: var(--space-inset); left: var(--space-inset); display: flex; justify-content: center; pointer-events: none; }
.toast { display: inline-flex; max-width: 100%; align-items: center; gap: var(--space-related); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: var(--space-related) var(--space-inset); background: var(--surface-raised); box-shadow: var(--shadow-floating); color: var(--foreground); font-size: var(--text-sm); }
.toast > svg { flex: none; color: var(--text-secondary); }
.toast[data-tone="success"] > svg { color: var(--success); }
.toast[data-tone="error"] > svg { color: var(--danger); }
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* The menu grows from the overflow button. Transitions, not keyframes, so a quick reopen reverses from where it is. */
.menu { --menu-y: -5px; position: relative; z-index: var(--layer-dropdown); min-width: 13rem; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); padding: var(--space-tight); background: var(--surface-raised); box-shadow: var(--shadow-floating); color: var(--foreground); font-family: var(--font-body); font-size: var(--text-sm); letter-spacing: var(--tracking-body); transform-origin: var(--radix-dropdown-menu-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-spring) var(--ease-spring); }
.menu[data-side="top"] { --menu-y: 5px; }
@starting-style { .menu[data-state="open"] { opacity: 0; transform: translateY(var(--menu-y)) scale(.97); } }
.menu[data-state="closed"] { opacity: 0; transform: translateY(calc(var(--menu-y) * .5)) scale(.985); transition: opacity var(--duration-instant) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); animation: menu-exit var(--duration-instant) linear both; pointer-events: none; }
@keyframes menu-exit { to { --menu-exit: 1; } }
.highlight { position: absolute; top: 0; right: var(--space-tight); left: var(--space-tight); border-radius: calc(var(--radius-container) - var(--space-related)); corner-shape: var(--corner-shape); background: var(--surface-muted); opacity: 0; pointer-events: none; }
.item { position: relative; display: flex; min-height: 36px; align-items: center; gap: var(--space-group); border-radius: calc(var(--radius-container) - var(--space-related)); corner-shape: var(--corner-shape); padding: 0 var(--space-group); outline: none; cursor: pointer; user-select: none; }
.item > svg { flex: none; color: var(--text-secondary); }
.item[data-disabled] { opacity: .45; cursor: default; }
.separator { height: 1px; margin: var(--space-tight) calc(var(--space-tight) * -1); background: var(--border-subtle); }

@media (hover: hover) and (pointer: fine) {
  .crumb:hover { color: var(--foreground); }
  .compact:hover { background: color-mix(in oklab, var(--foreground) 4.5%, transparent); }
  .tab:hover { color: var(--foreground); }
  .tab:hover::before { opacity: 1; }
  .check:hover { background: color-mix(in oklab, var(--foreground) 5%, transparent); color: var(--foreground); }
  .issue[data-closing] .check:hover { background: none; color: var(--success); }
}

/* Before the first measurement, narrow frames already hide the actions that will fold, so nothing flashes on load. */
@container page-header (max-width: 719px) { .frame:not([data-measured]) .secondary { width: 0 !important; opacity: 0 !important; } }
@container page-header (max-width: 559px) {
  .header, .scroller { --pad: 18px; }
  .crumbs .rootCrumb, .compact .compactBadge { display: none; }
  /* The tab row scrolls sideways here; its far edge fades so the next tab reads as more to come. */
  .tabs { padding-right: calc(var(--pad) + var(--space-inset)); mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent); }
  .milestones li { grid-template-columns: 16px minmax(0, 1fr); padding-block: var(--space-group); }
  .milestones li > .note { grid-column: 2; }
  .title { font-size: var(--text-2xl); }
  .introInner { padding-top: var(--space-inset); }
  .issueInner { grid-template-columns: 32px minmax(0, 1fr) auto; align-items: start; padding-block: var(--space-group); }
  .issueInner .check { margin-top: calc(var(--space-related) * -1); }
  .issueText { display: grid; gap: var(--space-hair); }
  .issueTitle { overflow: visible; white-space: normal; }
  .issueLabel, .fileDate { display: none; }
  .files li { grid-template-columns: 16px minmax(0, 1fr) auto; }
  .activity li { grid-template-columns: auto minmax(0, 1fr); padding-block: var(--space-group); }
  .activity li > .note { grid-column: 2; margin-top: calc(var(--space-related) * -1); }
}
@container page-header (max-width: 459px) {
  .createText { display: none; }
  .actions .create { width: var(--control-height-sm); padding-inline: 0; }
}
/* On a phone the frame is the screen, so it takes most of it rather than stopping short with the body cut mid list. */
@media (max-width: 560px) { .frame { height: clamp(560px, calc(100svh - 48px), 760px); border-radius: var(--radius-control); corner-shape: var(--corner-shape); } }

@media (prefers-reduced-motion: reduce) {
  .crumb, .compact, .tab, .tab::before, .check, .issueTitle { transition: none; }
  .menu, .menu[data-state="closed"] { transform: none; transition: opacity var(--duration-instant) linear; }
}
```

### `registry/components/progress/progress.tsx`

```tsx
"use client";

import { useEffect } from "react";
import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion, useTransform, type HTMLMotionProps, type TargetAndTransition, type Transition } from "motion/react";
import { Check } from "lucide-react";
import { motionTokens } from "../lib/motion-tokens";
import styles from "./progress.module.css";

export interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
  value?: number;
  max?: number;
  label?: string;
  showValue?: boolean;
}

const exitFast: Transition = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
const textIn: TargetAndTransition = { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` };
const textOut: TargetAndTransition = { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: exitFast };
const iconIn: TargetAndTransition = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
const shown: TargetAndTransition = { opacity: 1, y: "0em", scale: 1, filter: "blur(0px)" };
const fadeOut: TargetAndTransition = { opacity: 0, transition: { duration: motionTokens.duration.instant } };

/** Outgoing copies are hidden from assistive tech while they fade. */
function Swap(props: HTMLMotionProps<"span">) {
  const present = useIsPresent();
  return <motion.span {...props} aria-hidden={present ? props["aria-hidden"] : true} />;
}

export function Progress({ value = 0, max = 100, label, showValue = false, className, ...props }: ProgressProps) {
  const reduce = useReducedMotion();
  const safeMax = max > 0 ? max : 100;
  const safeValue = Math.min(Math.max(value, 0), safeMax);
  const percentage = Math.round((safeValue / safeMax) * 100);
  const complete = percentage >= 100;
  // One spring drives both the fill and the counted label, so the number always matches the bar.
  const progress = useMotionValue(percentage);
  // The fill slides in from the left instead of scaling, so its rounded end keeps its shape at every value.
  const x = useTransform(progress, latest => `${Math.min(Math.max(latest, 0), 100) - 100}%`);
  const counted = useTransform(progress, latest => `${Math.round(Math.min(Math.max(latest, 0), 100))}%`);
  useEffect(() => {
    if (reduce) { progress.jump(percentage); return; }
    const controls = animate(progress, percentage, motionTokens.spring.smooth);
    return () => controls.stop();
  }, [percentage, progress, reduce]);
  const enter: Transition = reduce ? { duration: motionTokens.duration.instant } : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };
  const classes = [styles.progress, className].filter(Boolean).join(" ");
  return <div {...props} className={classes} data-complete={complete ? "" : undefined} role="progressbar" aria-label={label ?? "Progress"} aria-valuemin={0} aria-valuemax={safeMax} aria-valuenow={safeValue} aria-valuetext={`${percentage}%`}>
    {(label || showValue) ? <div className={styles.meta}>
      {label ? <span className={styles.label}><AnimatePresence mode="popLayout" initial={false}><Swap key={label} className={styles.line} initial={reduce ? { opacity: 0 } : textIn} animate={shown} exit={reduce ? fadeOut : textOut} transition={enter}>{label}</Swap></AnimatePresence></span> : <span />}
      {showValue ? <span className={styles.value}>
        {/* Completion lands as the fill arrives: a check settles in beside the final count. */}
        <AnimatePresence initial={false}>{complete && <motion.span key="done" className={styles.done} initial={reduce ? { opacity: 0 } : iconIn} animate={shown} exit={reduce ? fadeOut : { ...iconIn, transition: exitFast }} transition={reduce ? enter : { ...motionTokens.spring.snappy, delay: .24 }}><Check size={14} strokeWidth={1.75} aria-hidden="true" /></motion.span>}</AnimatePresence>
        <motion.span className={styles.count}>{counted}</motion.span>
      </span> : null}
    </div> : null}
    <div className={styles.track}><motion.span className={styles.fill} style={{ x }} /></div>
  </div>;
}

export default Progress;
```

### `registry/components/progress/progress.module.css`

```css
.progress { width: 100%; color: var(--text-secondary); font-size: var(--text-sm); } .meta { display: flex; justify-content: space-between; gap: var(--space-group); margin-bottom: var(--space-related); line-height: var(--leading-body); } .label { position: relative; min-width: 0; } .line { display: block; } .value { display: inline-flex; flex: none; align-items: center; gap: var(--space-tight); color: var(--text-muted); font-variant-numeric: tabular-nums; } .count { display: inline-grid; justify-items: end; } /* A hidden "100%" row reserves the widest count, so the check never shifts as the number gains a digit. */ .count::before { content: "100%"; height: 0; overflow: hidden; visibility: hidden; } .done { display: grid; place-items: center; color: var(--success); } .track { height: 7px; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-muted); } .fill { display: block; width: 100%; height: 100%; border-radius: inherit; corner-shape: inherit; background: var(--accent); transition: background-color var(--duration-standard) var(--ease-standard); } .progress[data-complete] .fill { background: var(--success); transition-delay: 200ms; } @media (prefers-reduced-motion: reduce) { .fill, .progress[data-complete] .fill { transition: none; } }
```

### `registry/components/button/button.tsx`

```tsx
"use client";

import { forwardRef, isValidElement, useCallback, useEffect, useLayoutEffect, useRef } from "react";
import type { ButtonHTMLAttributes, ReactNode, Ref, RefObject } from "react";
import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion } from "motion/react";
import type { TargetAndTransition, Variants } from "motion/react";
import { motionTokens } from "../lib/motion-tokens";
import styles from "./button.module.css";

/** `soft` is filled and borderless, for controls in a bar (experimental). */
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "soft";
/** `xs` (24px) is for dense UI such as tree rows and toolbars, usually icon-only with an aria-label. */
export type ButtonSize = "xs" | "sm" | "md" | "lg";

export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onDrag" | "onDragEnd" | "onDragStart" | "onAnimationStart"> {
  /** The look: `primary` (default) for the main action, `secondary` bordered, `ghost` quiet, `danger` destructive, `soft` filled for bars. */
  variant?: ButtonVariant;
  /** Height: `xs` 24px, `sm` 36px, `md` 44px (default), `lg` 50px. */
  size?: ButtonSize;
  /** Shows a spinner in place of the label and sets aria-busy, keeping the button's width. */
  loading?: boolean;
}

/** Icon buttons press a little deeper, wide buttons a little less, so every size reads as the same push. */
const pressVariants: Variants = {
  pressed: (button: RefObject<HTMLButtonElement | null>) => {
    const width = button.current?.offsetWidth ?? 0;
    return { scale: width > 220 ? .985 : width && width <= 48 ? .96 : .97, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } };
  },
};

const rest: TargetAndTransition = { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" };
/** Text rises about .3em out of a soft blur; the outgoing label lifts away a little faster. */
const textIn: TargetAndTransition = { opacity: 0, y: 4, filter: `blur(${motionTokens.blur.soft}px)` };
const textOut: TargetAndTransition = { opacity: 0, y: -3, filter: `blur(${motionTokens.blur.soft}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
const iconIn: TargetAndTransition = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
const iconOut: TargetAndTransition = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
const fadeIn: TargetAndTransition = { ...rest, opacity: 0 };
const fadeOut: TargetAndTransition = { opacity: 0, transition: { duration: motionTokens.duration.instant } };
/** Scale rides the spring; opacity and blur tween so blur never overshoots below zero. */
const iconEnter = { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] }, filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } } as const;

/** A key for the label content: text plus element names, so a new label or icon crossfades while prop-only updates stay in place. */
function labelKey(node: ReactNode): string {
  if (node == null || typeof node === "boolean") return "";
  if (typeof node === "string" || typeof node === "number" || typeof node === "bigint") return String(node);
  if (Array.isArray(node)) return node.map(labelKey).join("");
  if (!isValidElement(node)) return "";
  const type = node.type as string | { displayName?: string; name?: string };
  return `<${typeof type === "string" ? type : type?.displayName ?? type?.name ?? ""}>${labelKey((node.props as { children?: ReactNode }).children)}`;
}

/** Springs the slot to the natural width of the incoming label when it changes, so a new label never snaps the button's size.
 *  The outgoing label is popped out of flow at once, so it never holds the old width. Other resizes (a late web font, a parent reflow) jump straight to the new width, so nothing wobbles on first paint. */
function useMorphWidth(content: RefObject<HTMLElement | null>, key: string, reduced: boolean) {
  const width = useMotionValue<number | "auto">("auto");
  const lastKey = useRef(key), armedUntil = useRef(0);
  useLayoutEffect(() => {
    if (lastKey.current === key) return;
    lastKey.current = key;
    armedUntil.current = performance.now() + 700;
  }, [key]);
  useEffect(() => {
    const node = content.current, slot = node?.parentElement;
    if (!node || !slot || typeof ResizeObserver === "undefined") return;
    let measured = false;
    const observer = new ResizeObserver(([entry]) => {
      if (!entry) return;
      const next = entry.contentRect.width;
      if (!next || !measured || reduced || performance.now() > armedUntil.current) { measured = next > 0; width.jump(next || "auto"); delete slot.dataset.morphing; return; }
      slot.dataset.morphing = "";
      animate(width, next, { ...motionTokens.spring.morph, onComplete: () => { delete slot.dataset.morphing; } });
    });
    observer.observe(node);
    return () => observer.disconnect();
  }, [content, reduced, width]);
  return width;
}

function LabelPhase({ children, icon, reduced, ref }: { children: ReactNode; icon: boolean; reduced: boolean; ref?: Ref<HTMLSpanElement> }) {
  const present = useIsPresent();
  return <motion.span ref={ref} className={styles.labelPhase} aria-hidden={present ? undefined : true} initial={reduced ? fadeIn : icon ? iconIn : textIn} animate={rest} exit={reduced ? fadeOut : icon ? iconOut : textOut} transition={reduced ? { duration: motionTokens.duration.instant } : icon ? iconEnter : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }}>{children}</motion.span>;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
  { className, variant = "primary", size = "md", loading = false, disabled, children, onClick, ...props },
  ref,
) {
  const reduceMotion = useReducedMotion() ?? false;
  const buttonRef = useRef<HTMLButtonElement | null>(null);
  const contentRef = useRef<HTMLSpanElement>(null);
  const key = labelKey(children);
  const width = useMorphWidth(contentRef, key, reduceMotion);
  const setRefs = useCallback((node: HTMLButtonElement | null) => {
    buttonRef.current = node;
    if (typeof ref === "function") ref(node); else if (ref) ref.current = node;
  }, [ref]);
  // A trigger that anchors a menu, popover, or dialog keeps its rect still while pressed, so the layer never measures a scaled anchor.
  // Radix triggers (asChild) pass aria-haspopup or data-state through; the stylesheet repeats the guard.
  const popup = props["aria-haspopup"];
  const anchorsLayer = (popup !== undefined && popup !== false && popup !== "false") || props.role === "combobox" || (props as Record<string, unknown>)["data-state"] !== undefined;
  const inert = disabled || loading || props["aria-disabled"] === true || props["aria-disabled"] === "true";
  const classes = [styles.button, styles[variant], styles[size], className].filter(Boolean).join(" ");
  // An icon with no text: square at the compact size.
  const iconOnly = !/\S/.test(key.replace(/<[^>]*>/g, ""));

  return (
    <motion.button
      ref={setRefs}
      tabIndex={props.tabIndex ?? 0}
      className={classes}
      data-variant={variant}
      data-icon-only={iconOnly || undefined}
      disabled={disabled}
      aria-busy={loading || undefined}
      custom={buttonRef}
      variants={pressVariants}
      whileTap={reduceMotion || anchorsLayer || inert ? undefined : "pressed"}
      transition={motionTokens.spring.snappy}
      {...props}
      // Loading keeps the button focusable (a disabled button would drop keyboard focus mid-action) and swallows presses instead.
      aria-disabled={loading || props["aria-disabled"] || undefined}
      onClick={loading ? event => event.preventDefault() : onClick}
    >
      <AnimatePresence initial={false}>
        {loading ? <motion.span key="loader" className={styles.loader} aria-hidden="true" initial={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: .6 }} animate={{ opacity: 1, scale: 1 }} exit={reduceMotion ? fadeOut : { ...iconOut, scale: .8 }} transition={reduceMotion ? { duration: motionTokens.duration.instant } : iconEnter}><span className={styles.spinner} /></motion.span> : null}
      </AnimatePresence>
      <motion.span className={[styles.labelSlot, loading ? styles.loadingLabel : ""].filter(Boolean).join(" ")} style={{ width }}>
        <span ref={contentRef} className={styles.labelContent}>
          <AnimatePresence mode="popLayout" initial={false}>
            <LabelPhase key={key} icon={!/\S/.test(key.replace(/<[^>]*>/g, ""))} reduced={reduceMotion}>{children}</LabelPhase>
          </AnimatePresence>
        </span>
      </motion.span>
    </motion.button>
  );
});

Button.displayName = "Button";

export default Button;
```

### `registry/components/button/button.module.css`

```css
/* Press scale is driven by Motion (whileTap) alone, so transform never gets a CSS transition here. */
.button { position: relative; display: inline-flex; min-height: var(--control-height-md); align-items: center; justify-content: center; gap: var(--space-related); border: 1px solid transparent; border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: 0 var(--space-inset); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
.button:disabled { cursor: not-allowed; opacity: .52; }
.button[aria-busy="true"] { cursor: progress; }
/* Anchors of floating layers never scale: Radix opens on pointerdown and would measure a shrunken trigger. */
.button:is([aria-haspopup]:not([aria-haspopup="false"]), [data-state], [role="combobox"]) { transform: none !important; }
/* The slot's width follows the incoming label on a spring. The label stays centered and may overflow evenly while the slot catches up;
   the outgoing label pops out of flow against the slot itself, so it keeps its place instead of following the new content box. */
.labelSlot { position: relative; display: inline-flex; min-width: 0; align-items: center; justify-content: center; transition: opacity var(--duration-fast) var(--ease-standard); }
.labelSlot[data-morphing] { clip-path: inset(-50% calc(var(--space-group) * -1)); }
.labelContent { display: inline-flex; flex: none; align-items: center; }
.labelPhase { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-related); white-space: nowrap; }
.labelPhase svg { flex: none; }
.primary { color: var(--primary-foreground); background: var(--primary); border-color: var(--primary); }
.secondary { color: var(--foreground); background: var(--surface); border-color: var(--border); }
.ghost { color: var(--text-secondary); background: transparent; border-color: transparent; }
.danger { color: var(--danger); background: var(--surface); border-color: var(--border); }
/* Filled and borderless, for controls that live in a bar (a search trigger, a toolbar, a switcher). Place it on --background or
   --surface; on a --surface-muted panel it has no fill to show. */
.soft { color: var(--foreground); background: var(--surface-muted); border-color: transparent; }
/* A disabled outlined button keeps its shape and fades only its label, so it still reads as a button that is off, not as plain text. */
.secondary:disabled, .danger:disabled { opacity: 1; color: var(--text-muted); background: var(--surface-muted); border-color: var(--border); }
.danger:disabled { color: color-mix(in oklab, var(--danger) 55%, var(--surface-muted)); }
.ghost:disabled, .soft:disabled { opacity: 1; color: var(--text-muted); }
@media (hover: hover) and (pointer: fine) {
  .primary:hover:not(:disabled) { opacity: .91; box-shadow: var(--shadow-resting); }
  .secondary:hover:not(:disabled) { background: var(--surface-muted); box-shadow: var(--shadow-resting); }
  .ghost:hover:not(:disabled) { color: var(--foreground); background: var(--surface-muted); }
  .danger:hover:not(:disabled) { border-color: var(--danger); background: var(--surface-muted); }
  .soft:hover:not(:disabled) { background: var(--surface-pressed); }
}
/* Color feedback on press, so touch and menu triggers (which never scale) still answer the finger. */
.primary:active:not(:disabled) { opacity: .84; }
.secondary:active:not(:disabled), .ghost:active:not(:disabled) { color: var(--foreground); background: var(--surface-muted); }
.danger:active:not(:disabled) { border-color: var(--danger); background: var(--surface-muted); }
.soft:active:not(:disabled) { background: var(--surface-pressed); }
.xs { min-height: var(--control-height-xs); gap: var(--space-tight); padding-inline: var(--space-related); font-size: var(--text-xs); }
/* A compact icon-only button is a square, with the icon at its usual 16px. */
.xs[data-icon-only] { min-width: var(--control-height-xs); padding-inline: 0; }
.xs svg { width: 16px; height: 16px; }
.sm { min-height: var(--control-height-sm); padding-inline: var(--space-group); font-size: var(--text-sm); }
.md { min-height: var(--control-height-md); }
.lg { min-height: var(--control-height-lg); padding-inline: var(--space-comfort); font-size: var(--text-sm); }
.loader { position: absolute; left: 50%; top: 50%; display: grid; width: var(--space-inset); height: var(--space-inset); margin: calc(var(--space-inset) / -2); }
.spinner { border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; corner-shape: round; animation: spin .7s linear infinite; }
.loadingLabel { opacity: 0; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .button { transition: none; } .labelSlot { transition: opacity var(--duration-instant) linear; } .spinner { animation-duration: .01ms; animation-iteration-count: 1; } }
```

### `registry/components/badge/badge.tsx`

```tsx
"use client";

import { isValidElement, useEffect, useLayoutEffect, useRef } from "react";
import type { HTMLAttributes, ReactNode } from "react";
import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion, type AnimationPlaybackControls, type HTMLMotionProps, type TargetAndTransition, type Transition } from "motion/react";
import { motionTokens } from "../lib/motion-tokens";
import styles from "./badge.module.css";

export type BadgeTone = "neutral" | "success" | "info" | "warning" | "danger";
export type BadgeSize = "sm" | "md";

export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
  tone?: BadgeTone;
  size?: BadgeSize;
  icon?: ReactNode;
}

const exitFast: Transition = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] };
const textIn: TargetAndTransition = { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` };
const textOut: TargetAndTransition = { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: exitFast };
const iconIn: TargetAndTransition = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
const shown: TargetAndTransition = { opacity: 1, y: "0em", scale: 1, filter: "blur(0px)" };
const fadeOnly: TargetAndTransition = { opacity: 0, transition: { duration: motionTokens.duration.instant } };

/** Outgoing copies are hidden from assistive tech while they fade, so only the current text is read. */
function Swap(props: HTMLMotionProps<"span">) {
  const present = useIsPresent();
  return <motion.span {...props} aria-hidden={present ? props["aria-hidden"] : true} />;
}

/** A new icon component crossfades in; re-rendering the same icon stays still. */
function iconKey(icon: ReactNode) {
  if (!isValidElement(icon)) return "icon";
  const type = icon.type as string | { displayName?: string; name?: string };
  return typeof type === "string" ? type : type.displayName ?? type.name ?? "icon";
}

export function Badge({ tone = "neutral", size = "md", icon, className, children, ...props }: BadgeProps) {
  const reduce = useReducedMotion();
  const text = typeof children === "string" || typeof children === "number" ? String(children) : null;
  const glyphKey = icon ? iconKey(icon) : "";
  const body = useRef<HTMLSpanElement>(null);
  const content = useRef<HTMLSpanElement>(null);
  // Width stays auto at rest. Only a new label or icon springs it from the old size to the new one; passive reflows (a font swap, a hidden parent) follow instantly.
  const width = useMotionValue<number | "auto">("auto");
  const changedAt = useRef(0);
  useLayoutEffect(() => { changedAt.current = performance.now(); }, [text, glyphKey]);
  useEffect(() => {
    const node = content.current;
    if (!node || typeof ResizeObserver === "undefined") return;
    let last: number | undefined;
    let controls: AnimationPlaybackControls | undefined;
    const settle = () => { width.jump("auto"); if (body.current) body.current.style.width = "auto"; };
    const observer = new ResizeObserver(([entry]) => {
      if (!entry) return;
      const next = entry.borderBoxSize?.[0]?.inlineSize ?? node.offsetWidth;
      const current = width.get();
      const from = typeof current === "number" ? current : last;
      last = next;
      controls?.stop();
      if (reduce || from === undefined || from === next || performance.now() - changedAt.current > 120) return settle();
      // Pin the old width before this frame paints, then spring to the new one.
      if (body.current) body.current.style.width = `${from}px`;
      controls = animate(width, [from, next], { ...motionTokens.spring.morph, onComplete: settle });
    });
    observer.observe(node);
    return () => { observer.disconnect(); controls?.stop(); };
  }, [width, reduce]);
  const enter: Transition = reduce ? { duration: motionTokens.duration.instant } : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };
  const classes = [styles.badge, styles[tone], styles[size], className].filter(Boolean).join(" ");
  // The pill follows its content: text and icon swap in place while the width springs to the new size.
  return <span {...props} className={classes}>
    <motion.span ref={body} className={styles.body} style={{ width }}>
      <span ref={content} className={styles.content}>
        {icon ? <span className={styles.icon} aria-hidden="true"><AnimatePresence mode="popLayout" initial={false}><Swap key={glyphKey} className={styles.glyph} initial={reduce ? { opacity: 0 } : iconIn} animate={shown} exit={reduce ? fadeOnly : { ...iconIn, transition: exitFast }} transition={reduce ? enter : motionTokens.spring.snappy}>{icon}</Swap></AnimatePresence></span> : null}
        {text === null ? children : <span className={styles.label}><AnimatePresence mode="popLayout" initial={false}><Swap key={text} className={styles.text} initial={reduce ? { opacity: 0 } : textIn} animate={shown} exit={reduce ? fadeOnly : textOut} transition={enter}>{text}</Swap></AnimatePresence></span>}
      </span>
    </motion.span>
  </span>;
}

export default Badge;
```

### `registry/components/badge/badge.module.css`

```css
.badge {
  display: inline-flex;
  min-height: 26px;
  align-items: center;
  border: 1px solid var(--badge-border, var(--border));
  border-radius: var(--radius-full); corner-shape: round;
  padding: 0 var(--space-group);
  color: var(--badge-foreground, var(--foreground));
  background: var(--badge-background, var(--surface));
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: -.01em;
  line-height: 1;
  white-space: nowrap;
  /* Clip keeps the inline baseline while the pill morphs around swapped text. */
  overflow: clip;
  transition: border-color var(--duration-standard) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard);
}

@media (hover: hover) and (pointer: fine) {
  .badge:hover { border-color: color-mix(in oklab, var(--badge-color, var(--foreground)) 32%, var(--border)); background: color-mix(in oklab, var(--badge-color, var(--foreground)) 10%, var(--surface)); }
}
.sm { min-height: 22px; padding: 0 var(--space-related); font-size: var(--text-xs); }
.md { font-size: var(--text-xs); }

.body { display: block; }
.content { position: relative; display: inline-flex; width: max-content; align-items: center; gap: var(--space-tight); }
.sm .content { gap: var(--space-tight); }
.label, .glyph { position: relative; display: inline-flex; align-items: center; }
.text { display: block; }

.icon {
  position: relative;
  display: inline-flex;
  width: auto;
  height: auto;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  color: var(--badge-color, var(--foreground));
  line-height: 1;
}

.neutral { --badge-color: var(--text-secondary); --badge-foreground: var(--text-secondary); --badge-background: var(--surface-muted); --badge-border: var(--border); }
.success { --badge-color: var(--success); --badge-foreground: var(--success); --badge-background: color-mix(in oklab, var(--success) 10%, var(--surface)); --badge-border: color-mix(in oklab, var(--success) 25%, var(--border)); }
.info { --badge-color: var(--accent); --badge-foreground: var(--accent-strong); --badge-background: var(--accent-subtle); --badge-border: color-mix(in oklab, var(--accent) 24%, var(--border)); }
.warning { --badge-color: var(--warning); --badge-foreground: var(--warning); --badge-background: color-mix(in oklab, var(--warning) 11%, var(--surface)); --badge-border: color-mix(in oklab, var(--warning) 27%, var(--border)); }
.danger { --badge-color: var(--danger); --badge-foreground: var(--danger); --badge-background: color-mix(in oklab, var(--danger) 10%, var(--surface)); --badge-border: color-mix(in oklab, var(--danger) 26%, var(--border)); }

@media (prefers-reduced-motion: reduce) {
  .badge { transition-duration: var(--duration-instant); }
}
```

### `registry/components/avatar-group/avatar-group.tsx`

```tsx
"use client";

import { useState, type CSSProperties } from "react";
import { AnimatePresence, motion, useReducedMotion, type Variants } from "motion/react";
import { motionTokens } from "../lib/motion-tokens";
import { Avatar } from "../avatar/avatar";
import styles from "./avatar-group.module.css";

export interface AvatarGroupMember { name: string; src?: string; status?: "online" | "offline"; }
export interface AvatarGroupProps { members: AvatarGroupMember[]; max?: number; size?: "sm" | "md" | "lg"; label?: string; }

/** The overflow count rolls the way it moved: more people rise in from below, fewer drop in from above. */
const rise: Variants = { hidden: (direction: number) => ({ opacity: 0, y: `${.4 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)` }), shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } }, gone: (direction: number) => ({ opacity: 0, y: `${-.4 * direction}em`, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }) };
// Same keys as `rise` so the settled style is identical whichever branch renders on the server.
const fade: Variants = { hidden: { opacity: 0, y: 0, filter: "blur(0px)" }, shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } }, gone: { opacity: 0, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } } };
// A person joining or leaving opens or closes their slot, so the rest of the stack slides instead of jumping.
const slot = { initial: { width: 0, opacity: 0, scale: .9 }, animate: { width: "auto", opacity: 1, scale: 1 }, exit: { width: 0, opacity: 0, scale: .9 } };

export function AvatarGroup({ members, max = 4, size = "md", label = "Team members" }: AvatarGroupProps) {
  const reduceMotion = !!useReducedMotion();
  const visible = members.slice(0, Math.max(0, max));
  const overflow = Math.max(0, members.length - visible.length);
  const transition = reduceMotion ? { duration: 0 } : motionTokens.spring.morph;
  const [count, setCount] = useState({ overflow, direction: 1 });
  if (count.overflow !== overflow) setCount({ overflow, direction: overflow < count.overflow ? -1 : 1 });
  return <div className={[styles.group, styles[size]].join(" ")} role="group" aria-label={label} style={{ "--count": visible.length + (overflow > 0 ? 1 : 0) } as CSSProperties}>
    <AnimatePresence initial={false}>
      {visible.map((member, index) => <motion.span key={member.name} className={styles.slot} style={{ "--index": index } as CSSProperties} {...slot} transition={transition}><span className={styles.lift}><Avatar className={styles.avatar} name={member.name} src={member.src} status={member.status} size={size} /><span className={styles.tip} aria-hidden="true">{member.name}</span></span></motion.span>)}
      {overflow > 0 ? <motion.span key="overflow" className={styles.slot} style={{ "--index": visible.length } as CSSProperties} {...slot} transition={transition}><span className={[styles.lift, styles.overflow, styles[size]].join(" ")} role="img" aria-label={`${overflow} more ${label.toLowerCase()}`}><AnimatePresence mode="popLayout" initial={false} custom={count.direction}><motion.span key={overflow} className={styles.count} custom={count.direction} variants={reduceMotion ? fade : rise} initial="hidden" animate="shown" exit="gone" aria-hidden="true">+{overflow}</motion.span></AnimatePresence></span></motion.span> : null}
    </AnimatePresence>
  </div>;
}

export default AvatarGroup;
```

### `registry/components/avatar-group/avatar-group.module.css`

```css
.group { --avatar-size: 36px; display: inline-flex; height: var(--avatar-size); align-items: center; padding-left: var(--space-related); }
.group.sm { --avatar-size: 28px; }
.group.lg { --avatar-size: 48px; }
/* Each slot is the avatar's visible width, so a slot that opens or closes moves the rest of the stack smoothly. */
.slot { position: relative; display: inline-flex; flex: 0 0 auto; height: var(--avatar-size); align-items: center; }
/* One wrapper per person carries every hover move as a single translate, so the stack never changes size. */
.lift { --fan: 0px; --aside: 0px; --rise: 0px; position: relative; display: inline-flex; margin-left: calc(var(--space-related) * -1); border-radius: var(--avatar-radius); corner-shape: var(--avatar-corner-shape); transform: translate(calc(var(--fan) + var(--aside)), var(--rise)); transition: transform var(--duration-standard) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
.avatar, .overflow { border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--border); transition: box-shadow var(--duration-fast) var(--ease-standard); }
.overflow { position: relative; display: inline-grid; place-items: center; overflow: hidden; border-radius: var(--avatar-radius); corner-shape: var(--avatar-corner-shape); background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; }
.count { display: inline-block; }
.overflow.sm { width: 28px; height: 28px; font-size: var(--text-xs); }
.overflow.md { width: 36px; height: 36px; }
.overflow.lg { width: 48px; height: 48px; font-size: var(--text-sm); }
/* The name floats above the stack, outside the layout, so showing it never moves anything. */
.tip { position: absolute; z-index: 2; bottom: calc(100% + var(--space-related)); left: 50%; padding: var(--space-tight) var(--space-related); border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); white-space: nowrap; pointer-events: none; opacity: 0; transform: translate(-50%, 3px); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard); }
/* Pointing at the stack loosens its overlap from the center; the person under the pointer lifts with a crisp ring and names themselves, and their neighbors ease aside. */
@media (hover: hover) and (pointer: fine) {
  .group:hover .lift { --fan: calc((var(--index) - (var(--count) - 1) / 2) * 4px); }
  .slot:hover { z-index: 1; }
  .slot:hover .lift { --rise: -2px; }
  .slot:hover ~ .slot .lift { --aside: 3px; }
  .slot:has(~ .slot:hover) .lift { --aside: -3px; }
  .slot:hover .avatar, .slot:hover .overflow { box-shadow: 0 0 0 1px var(--border-strong); }
  .slot:hover .tip { opacity: 1; transform: translate(-50%, 0); transition-delay: 60ms; }
}
/* Reduced motion keeps the stack still; the ring and name still answer the pointer. */
@media (prefers-reduced-motion: reduce) { .lift { transform: none !important; transition: none; } .tip { transform: translate(-50%, 0); transition: none; } }
```

### `registry/components/avatar/avatar.tsx`

```tsx
"use client";

import { useLayoutEffect, useRef, useState, type HTMLAttributes } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { SiliconImage } from "../lib/silicon-provider";
import { motionTokens } from "../lib/motion-tokens";
import styles from "./avatar.module.css";
export interface AvatarProps extends HTMLAttributes<HTMLSpanElement> { name: string; src?: string; /** `xs` (20px) is for dense lists such as space icons in a tree; `sm` 28, `md` 36, `lg` 48, `xl` 88. */ size?: "xs" | "sm" | "md" | "lg" | "xl"; status?: "online" | "offline" }
export function Avatar({ name, src, size = "md", status, className, ...props }: AvatarProps) {
  const initials = name.trim().split(/\s+/).slice(0, 2).map(part => part[0]?.toUpperCase()).join("");
  const reduceMotion = !!useReducedMotion();
  const image = useRef<HTMLImageElement>(null);
  const [failedSrc, setFailedSrc] = useState<string>();
  // A photo that is already decoded shows at once. One that is still loading waits, then fades in from a soft blur.
  useLayoutEffect(() => { const node = image.current; if (node && !node.complete) node.dataset.loading = ""; }, [src]);
  const showImage = src && failedSrc !== src;
  return <span {...props} className={[styles.avatar, styles[size], className].filter(Boolean).join(" ")} role="img" aria-label={`${name}${status ? `, ${status}` : ""}`}>
    {showImage ? <SiliconImage key={src} ref={image} src={src} alt="" fill sizes={size === "xl" ? "88px" : size === "lg" ? "48px" : size === "md" ? "36px" : size === "sm" ? "28px" : "20px"} onLoad={event => { delete event.currentTarget.dataset.loading; }} onError={() => setFailedSrc(src)} /> : <span className={src ? styles.fallback : undefined} aria-hidden="true">{initials}</span>}
    <AnimatePresence initial={false}>{status && <motion.i key={status} className={[styles.status, styles[status]].join(" ")} aria-hidden="true" initial={{ opacity: 0, scale: .6 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: .6, transition: { duration: reduceMotion ? 0 : motionTokens.duration.fast } }} transition={reduceMotion ? { duration: 0 } : motionTokens.spring.snappy} />}</AnimatePresence>
  </span>;
}
```

### `registry/components/avatar/avatar.module.css`

```css
.avatar {
  position: relative;
  display: inline-grid;
  flex: 0 0 auto;
  place-items: center;
  overflow: visible;
  border: 1px solid var(--border);
  border-radius: var(--avatar-radius); corner-shape: var(--avatar-corner-shape);
  background: var(--surface-muted);
  color: var(--foreground);
  font-size: var(--text-xs);
  font-weight: 500;
}

.avatar img {
  pointer-events: none;
  border-radius: inherit; corner-shape: inherit;
  object-fit: cover;
  transition: opacity var(--duration-standard) var(--ease-enter), filter var(--duration-standard) var(--ease-enter);
}

/* Set only while a photo is still downloading after mount, so cached photos never fade. */
.avatar img[data-loading] {
  opacity: 0;
  filter: blur(4px);
  transition: none;
}

.fallback { animation: avatar-fallback-in var(--duration-standard) var(--ease-enter) both; }

/* Initials below the smallest type step, so two letters fit 20px. */
.xs { width: 20px; height: 20px; font-size: calc(var(--text-xs) * .8); }
.sm { width: 28px; height: 28px; font-size: var(--text-xs); }
.md { width: 36px; height: 36px; }
.lg { width: 48px; height: 48px; font-size: var(--text-sm); }
.xl { width: 88px; height: 88px; font-size: var(--text-xl); }

.status {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 2;
  width: 10px;
  height: 10px;
  border: 2px solid var(--surface);
  border-radius: var(--radius-full); corner-shape: round;
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--border) 70%, transparent);
}

.xs .status { width: 6px; height: 6px; border-width: 1px; }
.sm .status { width: 8px; height: 8px; border-width: 1.5px; }
.lg .status { width: 12px; height: 12px; }
.xl .status { width: 14px; height: 14px; right: var(--space-tight); bottom: var(--space-tight); }
.online { background: var(--success); }
.offline { background: var(--text-muted); }

@keyframes avatar-fallback-in { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  .avatar img { transition: none; }
  .avatar img[data-loading] { filter: none; }
  .fallback { animation: none; }
}
```

### `lib/silicon-provider.tsx`

```tsx
"use client";

/**
 * Lets an app swap the link and image elements Silicon UI components render.
 *
 * Without a provider, Silicon UI renders a plain `<a>` and `<img>`, so components work in any React app (Vite, Remix, Astro,
 * plain React). Wrap the app once to use your framework's or your own components instead:
 *
 *   import Link from "next/link";
 *   import Image from "next/image";
 *   import { SiliconProvider } from "@/components/silicon-ui/lib/silicon-provider";
 *
 *   <SiliconProvider link={Link} image={Image}>{children}</SiliconProvider>
 *
 * Any component that accepts these props works, such as a locale-aware Link from next-intl or a React Router Link
 * wrapped to map `href` to `to`.
 */
import { createContext, createElement, useContext, useMemo, type AnchorHTMLAttributes, type ComponentType, type ImgHTMLAttributes, type ReactNode, type Ref } from "react";

/** The props Silicon UI passes to a link: an href plus ordinary anchor attributes. */
export interface SiliconLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
  href: string;
  ref?: Ref<HTMLAnchorElement>;
}

/**
 * The props Silicon UI passes to an image. A subset of next/image's props, so next/image can be passed as is: either `fill`
 * (cover the positioned parent) or `width` and `height`, plus `sizes` and `priority` as hints.
 */
export interface SiliconImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "width" | "height" | "loading" | "srcSet"> {
  src: string;
  alt: string;
  width?: number;
  height?: number;
  /** Fills the nearest positioned parent, like next/image's `fill`. */
  fill?: boolean;
  /** Loads the image eagerly with high priority, like next/image's `priority`. */
  priority?: boolean;
  loading?: "eager" | "lazy";
  ref?: Ref<HTMLImageElement>;
}

export type SiliconLinkComponent = ComponentType<SiliconLinkProps>;
export type SiliconImageComponent = ComponentType<SiliconImageProps>;

/** The default link: a plain anchor. */
export function SiliconAnchor(props: SiliconLinkProps) {
  return <a {...props} />;
}

const fillStyle = { position: "absolute", inset: 0, width: "100%", height: "100%" } as const;

/** The default image: a plain, lazily decoded `<img>` that understands `fill` and `priority`. */
export function SiliconImg({ alt, fill, priority, loading, style, ...props }: SiliconImageProps) {
  // eslint-disable-next-line @next/next/no-img-element -- the framework-free default; pass next/image through SiliconProvider instead.
  return <img alt={alt} decoding="async" {...props} loading={loading ?? (priority ? "eager" : "lazy")} fetchPriority={priority ? "high" : props.fetchPriority} style={fill ? { ...fillStyle, ...style } : style} />;
}

interface SiliconComponents {
  link: SiliconLinkComponent;
  image: SiliconImageComponent;
}

const SiliconContext = createContext<SiliconComponents>({ link: SiliconAnchor, image: SiliconImg });

export interface SiliconProviderProps {
  /** The link component Silicon UI renders for navigation, such as next/link or your own wrapped Link. Defaults to `<a>`. */
  link?: SiliconLinkComponent;
  /** The image component Silicon UI renders for photos, such as next/image. Defaults to `<img>`. */
  image?: SiliconImageComponent;
  children: ReactNode;
}

/** Sets the link and image components for every Silicon UI component inside it. Nested providers inherit what they do not set. */
export function SiliconProvider({ link, image, children }: SiliconProviderProps) {
  const parent = useContext(SiliconContext);
  const value = useMemo(() => ({ link: link ?? parent.link, image: image ?? parent.image }), [link, image, parent]);
  return <SiliconContext.Provider value={value}>{children}</SiliconContext.Provider>;
}

/** The link component from the nearest SiliconProvider, or a plain anchor. */
export const useArcLink = () => useContext(SiliconContext).link;
/** The image component from the nearest SiliconProvider, or a plain img. */
export const useArcImage = () => useContext(SiliconContext).image;

/** What Silicon UI components render for a link: the provider's link component, or a plain anchor. */
export function SiliconLink(props: SiliconLinkProps) {
  return createElement(useArcLink(), props);
}

/** What Silicon UI components render for an image: the provider's image component, or a plain img. */
export function SiliconImage(props: SiliconImageProps) {
  return createElement(useArcImage(), props);
}
```

### `registry/components/animated-counter/animated-counter.tsx`

```tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { AnimatePresence, animate, motion, useInView, useMotionValue, useReducedMotion, useTransform, type MotionValue, type Variants } from "motion/react";
import { motionTokens } from "../lib/motion-tokens";
import styles from "./animated-counter.module.css";

export interface AnimatedCounterProps {
  value: number; label?: string; prefix?: string; suffix?: string; decimals?: number;
  /** Roll every digit up from zero the first time the counter scrolls into view. */
  animateOnView?: boolean;
  /** Formatting locale. Fixed by default so server and client render the same digits. */
  locale?: string;
}

type Part = { key: string; digit: number; order: number } | { key: string; text: string };

const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const rise: Variants = { hidden: { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` }, shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } }, gone: { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } } };
const fade: Variants = { hidden: { opacity: 0, y: 0, filter: "blur(0px)" }, shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } }, gone: { opacity: 0, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.instant } } };
const reveal = { ...motionTokens.spring.smooth, visualDuration: motionTokens.duration.considered };

/** Split a formatted number into columns keyed by place value, so 999 → 1,000 keeps the ones column the ones column. */
function partsFor(value: number, decimals: number, locale: string): Part[] {
  const parts = new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals, numberingSystem: "latn" }).formatToParts(value);
  let place = parts.reduce((count, part) => count + (part.type === "integer" ? part.value.length : 0), 0);
  let fraction = 0;
  let order = 0;
  return parts.flatMap((part, index): Part[] => {
    if (part.type === "integer") return [...part.value].map(char => ({ key: `i${--place}`, digit: Number(char), order: order++ }));
    if (part.type === "fraction") return [...part.value].map(char => ({ key: `f${fraction++}`, digit: Number(char), order: order++ }));
    return [{ key: part.type === "group" ? `g${place}` : part.type === "decimal" ? "d" : `${part.type}${index}`, text: part.value }];
  });
}

/** One digit on the wheel. Its offset from the wheel position decides where it sits and how visible it is. */
function Glyph({ position, digit }: { position: MotionValue<number>; digit: number }) {
  const offset = useTransform(position, current => ((((digit - current) % 10) + 15) % 10) - 5);
  const y = useTransform(offset, current => `${current}em`);
  const opacity = useTransform(offset, current => Math.max(0, 1 - Math.abs(current)));
  const visibility = useTransform(offset, current => Math.abs(current) >= 1 ? "hidden" : "visible");
  const filter = useTransform(offset, current => Math.abs(current) < .02 || Math.abs(current) >= 1 ? "none" : `blur(${(Math.abs(current) * motionTokens.blur.subtle).toFixed(2)}px)`);
  return <motion.span className={styles.glyph} style={{ y, opacity, filter, visibility }}>{digit}</motion.span>;
}

const presence = { initial: { width: 0, opacity: 0 }, animate: { width: "auto", opacity: 1 }, exit: { width: 0, opacity: 0 } };

/** A digit wheel. It always turns in the direction the whole number moved, wrapping 9 → 0 like an odometer. */
function Column({ digit, direction, armed, delay, reduceMotion }: { digit: number; direction: number; armed: boolean; delay: number; reduceMotion: boolean }) {
  const position = useMotionValue(armed ? 0 : digit);
  const wheel = useRef({ digit: armed ? 0 : digit, target: armed ? 0 : digit, revealed: !armed });
  useEffect(() => {
    const state = wheel.current;
    if (armed || state.digit === digit) { if (!armed) state.revealed = true; return; }
    state.target += direction < 0 && state.revealed ? -((state.digit - digit + 10) % 10) : (digit - state.digit + 10) % 10;
    state.digit = digit;
    if (reduceMotion) position.jump(state.target);
    else animate(position, state.target, state.revealed ? motionTokens.spring.smooth : { ...reveal, delay });
    state.revealed = true;
  }, [armed, delay, digit, direction, position, reduceMotion]);
  return <motion.span className={styles.column} {...presence} transition={reduceMotion ? { duration: 0 } : motionTokens.spring.morph}>
    <span className={styles.sizer}>0</span>
    {DIGITS.map(item => <Glyph key={item} position={position} digit={item} />)}
  </motion.span>;
}

export function AnimatedCounter({ value, label, prefix = "", suffix = "", decimals = 0, animateOnView = false, locale = "en-US" }: AnimatedCounterProps) {
  const ref = useRef<HTMLSpanElement>(null);
  const inView = useInView(ref, { once: true, amount: .6 });
  const reduceMotion = !!useReducedMotion();
  const [previous, setPrevious] = useState(value);
  const [direction, setDirection] = useState(1);
  if (value !== previous) { setPrevious(value); setDirection(value > previous ? 1 : -1); }
  const parts = partsFor(value, decimals, locale);
  const text = `${prefix}${parts.map(part => "text" in part ? part.text : part.digit).join("")}${suffix}`;
  const armed = animateOnView && !inView;
  return <span ref={ref} className={styles.counter}>
    {label && <span className={styles.label}><span className={styles.labelSwap}><AnimatePresence mode="popLayout" initial={false}><motion.span key={label} className={styles.labelText} variants={reduceMotion ? fade : rise} initial="hidden" animate="shown" exit="gone">{label}</motion.span></AnimatePresence></span></span>}
    <span className={styles.srOnly}>{text}</span>
    <span className={styles.value} aria-hidden="true">
      {prefix && <span className={styles.symbol}>{prefix}</span>}
      <AnimatePresence initial={false}>
        {parts.map(part => "digit" in part
          ? <Column key={part.key} digit={part.digit} direction={direction} armed={armed} delay={Math.min(part.order * motionTokens.stagger.item, .25)} reduceMotion={reduceMotion} />
          : <motion.span key={part.key} className={styles.symbol} {...presence} transition={reduceMotion ? { duration: 0 } : motionTokens.spring.morph}>{part.text}</motion.span>)}
      </AnimatePresence>
      {suffix && <span className={styles.symbol}>{suffix}</span>}
    </span>
  </span>;
}

export default AnimatedCounter;
```

### `registry/components/animated-counter/animated-counter.module.css`

```css
.counter { position: relative; display: inline-flex; flex-direction: column; color: var(--foreground); font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 500; letter-spacing: var(--tracking-display); line-height: 1; font-variant-numeric: tabular-nums; }
.label { margin-bottom: var(--space-related); color: var(--text-muted); font-family: var(--font-body); font-size: var(--text-xs); font-weight: 400; letter-spacing: var(--tracking-body); line-height: var(--leading-body); }
/* Each digit is a clipped wheel; separators clip sideways only so a comma keeps its tail. */
.value { display: inline-flex; align-items: flex-start; white-space: nowrap; user-select: none; }
/* The wheel's window reaches a little past the line box and feathers out, so a turning digit fades at the edge instead of being cut. */
.column { --feather: .16em; position: relative; display: inline-block; overflow: hidden; margin-block: calc(var(--feather) * -1); padding-block: var(--feather); mask-image: linear-gradient(to bottom, transparent, #000 calc(var(--feather) * 1.5), #000 calc(100% - var(--feather) * 1.5), transparent); }
.sizer { visibility: hidden; }
.glyph { position: absolute; inset: var(--feather) 0; text-align: center; }
.labelText { display: inline-block; }
.labelSwap { position: relative; display: block; }
.symbol { display: inline-block; overflow-x: clip; }
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
```
