# Hero section

> Three full screen SaaS heroes: a live dashboard rising from the bottom edge over a drifting mesh, a workflow graph that routes sample events node by node, and editorial type over a mesh gradient.

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/hero-section).

- Page, with a live preview: https://shadcn.exchange/r/silicon-ui/hero-section
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/silicon-ui/hero-section-light-882521b0ffa80ada.png
- Registry item: https://ui.teamofsilicons.com/r/hero-section.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/hero-section.json
```

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

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `https://ui.teamofsilicons.com/r/silicon-motion-tokens.json`, `https://ui.teamofsilicons.com/r/avatar.json`, `https://ui.teamofsilicons.com/r/button.json`, `https://ui.teamofsilicons.com/r/copy-button.json`, `https://ui.teamofsilicons.com/r/line-chart.json`, `https://ui.teamofsilicons.com/r/segmented-control.json`, `https://ui.teamofsilicons.com/r/sparkline.json`

## Notes from the registry

Docs and live preview: https://ui.teamofsilicons.com/hero-section

## Files

### `registry/blocks/hero-section/hero-section.tsx`

```tsx
"use client";

import { useState } from "react";
import type { ReactNode } from "react";
import SegmentedControl from "../../segmented-control/segmented-control";
import { HeroLumen } from "./hero-lumen";
import { HeroRelay } from "./hero-relay";
import { HeroCadence } from "./hero-cadence";
import { HeroMesh } from "./hero-mesh";
import type { HeroMeshPoint } from "./hero-mesh";
import { HeroContent } from "./hero-content";
import type { HeroAction, HeroInstallCommand } from "./hero-content";
import styles from "./hero-section.module.css";

export { HeroCadence, HeroContent, HeroLumen, HeroMesh, HeroRelay };
export type { HeroAction, HeroInstallCommand, HeroMeshPoint };

export type HeroSectionVariant = "centered" | "split" | "minimal";

export interface HeroSectionProps {
  /**
   * `centered` is a headline over a drifting mesh with a live dashboard rising from the bottom edge, `split` sets the copy
   * beside a live workflow graph that runs sample events node by node, and `minimal` is large type over a mesh gradient.
   * Each design fills one full screen.
   */
  variant?: HeroSectionVariant;
  /** Plays the entrance once on mount. Defaults to true. */
  animateIn?: boolean;
  /** The main call to action. With `doneLabel` and no `href`, the button confirms in place. */
  primaryAction?: HeroAction | null;
  /** The second call to action. */
  secondaryAction?: HeroAction | null;
  /**
   * Your own headline. When set, the hero renders your content in the chosen layout (see `HeroContent`) instead of the
   * designed sample, together with the props below.
   */
  title?: string;
  description?: string;
  announcement?: HeroAction | null;
  /** Install commands for your own content; the first one is shown with a copy button. */
  install?: HeroInstallCommand[] | null;
  /** A visual beside your own content in the split layout. */
  media?: ReactNode;
  /** Small facts under your own content, such as the version and license. */
  meta?: string[];
  /** Kept for compatibility; highlighted phrases render as plain text. */
  highlight?: string;
  className?: string;
}

/**
 * A full screen landing page hero in three designs: a product screenshot in perspective over a drifting mesh, a live workflow
 * graph that routes sample events, and editorial type over a mesh gradient.
 */
export function HeroSection({ variant = "centered", animateIn = true, primaryAction, secondaryAction, title, description, announcement, install, media, meta, className }: HeroSectionProps) {
  if (title) return <HeroContent layout={variant} title={title} description={description} announcement={announcement} primaryAction={primaryAction} secondaryAction={secondaryAction} install={install} media={media} meta={meta} animateIn={animateIn} className={className} />;
  const actions = { primaryAction: primaryAction ?? undefined, secondaryAction: secondaryAction ?? undefined };
  if (variant === "split") return <HeroRelay animateIn={animateIn} className={className} {...actions} />;
  if (variant === "minimal") return <HeroCadence animateIn={animateIn} className={className} {...actions} />;
  return <HeroLumen animateIn={animateIn} className={className} {...actions} />;
}

const variantOptions = [
  { value: "centered", label: "Screenshot" },
  { value: "split", label: "Workflow" },
  { value: "minimal", label: "Mesh" },
];

/**
 * Preview: the hero, full screen, with a small glass switch floating over its top edge. The switch takes no space of its own,
 * so every design fills the screen exactly. Switching replays the entrance.
 */
export function HeroSectionBlock({ variant: initial = "centered" }: { variant?: HeroSectionVariant }) {
  const [variant, setVariant] = useState<HeroSectionVariant>(initial);
  return <div className={styles.preview}>
    <HeroSection key={variant} variant={variant} />
    <div className={styles.switcher}>
      <SegmentedControl label="Hero design" options={variantOptions} value={variant} onValueChange={value => setVariant(value as HeroSectionVariant)} className={styles.switch} />
    </div>
  </div>;
}

export default HeroSectionBlock;
```

### `registry/blocks/hero-section/hero-section.module.css`

```css
/* Shared shell for the three hero variants. Each variant adds its own module for its one idea. */
.hero { position: relative; container: hero / inline-size; overflow: clip; background: var(--background); color: var(--foreground); font-family: var(--font-body); letter-spacing: var(--tracking-body); }

.title { margin: 0; font-family: var(--font-display); font-size: clamp(var(--text-3xl), var(--text-xl) + 4.4cqi, var(--text-5xl)); font-weight: 500; letter-spacing: var(--tracking-display); line-height: var(--leading-display); text-wrap: balance; }
.description { margin: 0; color: var(--text-secondary); font-size: var(--text-lg); line-height: var(--leading-body); text-wrap: pretty; }

@container hero (max-width: 560px) {
  .description { font-size: var(--text-base); }
}

/* The designed variants are one full screen each, edge to edge, like Hero signup. The small viewport unit keeps them from
   hiding under mobile browser bars. Your own content (HeroContent) keeps its natural height inside your page. */
.screen { width: 100%; min-height: 100vh; min-height: 100svh; }

/* Preview: the hero owns the whole frame; the design switch floats over its top edge as a small glass pill. */
.preview { position: relative; display: grid; width: 100%; min-height: 100vh; min-height: 100svh; }
.preview > .hero { min-height: inherit; }
.switcher {
  position: absolute; z-index: var(--layer-raised); top: var(--space-inset); left: 50%; translate: -50% 0;
  display: flex; max-width: calc(100% - 2 * var(--space-inset));
  border: 1px solid color-mix(in oklab, var(--foreground) 10%, transparent); border-radius: var(--radius-full); corner-shape: round;
  background: color-mix(in oklab, var(--surface) 64%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(1.5); backdrop-filter: blur(18px) saturate(1.5);
  box-shadow: var(--shadow-showcase);
}
:global(:root[data-theme="dark"]) .switcher { border-color: color-mix(in oklab, var(--foreground) 14%, transparent); background: color-mix(in oklab, var(--surface) 58%, transparent); box-shadow: var(--shadow-showcase); }
/* The segmented control drops its own well; the glass is its surface. */
.switcher .switch { border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; }
.switcher .switch :global(button) { border-radius: var(--radius-full); corner-shape: round; }

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

### `registry/blocks/hero-section/hero-content.tsx`

```tsx
"use client";

import { useEffect, useState } from "react";
import type { ReactNode } from "react";
import { MotionConfig, motion } from "motion/react";
import { ArrowRight, ArrowUpRight, Check } from "lucide-react";
import { Button } from "../../button/button";
import { CopyButton } from "../../copy-button/copy-button";
import { heroGroup, heroRise } from "./hero-motion";
import shell from "./hero-section.module.css";
import styles from "./hero-content.module.css";

export interface HeroAction {
  label: string;
  href?: string;
  onClick?: () => void;
  /** Opens in a new tab and shows an outward arrow. */
  external?: boolean;
  /** Shown in place of the label for a moment after a press, such as "Trial started". Buttons only. */
  doneLabel?: string;
}

export interface HeroInstallCommand {
  /** Short label, such as npm or pnpm. The first command is shown. */
  label: string;
  command: string;
}

export interface HeroContentProps {
  layout: "centered" | "split" | "minimal";
  announcement?: HeroAction | null;
  title: string;
  description?: string;
  primaryAction?: HeroAction | null;
  secondaryAction?: HeroAction | null;
  install?: HeroInstallCommand[] | null;
  media?: ReactNode;
  meta?: string[];
  animateIn?: boolean;
  className?: string;
}

/** A link or button that looks like an Silicon UI button. Links keep native navigation; a button with `doneLabel` confirms in place. */
export function ActionLink({ action, kind, className }: { action: HeroAction; kind: "primary" | "secondary"; className?: string }) {
  const [done, setDone] = useState(false);
  useEffect(() => {
    if (!done) return;
    const timer = window.setTimeout(() => setDone(false), 2400);
    return () => window.clearTimeout(timer);
  }, [done]);
  const cls = [styles.action, styles[kind], className].filter(Boolean).join(" ");
  const icon = action.external ? <ArrowUpRight size={16} strokeWidth={1.75} aria-hidden="true" /> : kind === "primary" ? <ArrowRight className={styles.arrow} size={16} strokeWidth={1.75} aria-hidden="true" /> : null;
  if (action.href) return <a className={cls} href={action.href} onClick={action.onClick} {...(action.external ? { target: "_blank", rel: "noopener noreferrer" } : {})}>{action.label}{icon}</a>;
  return <Button variant={kind} size="lg" className={[styles.button, className].filter(Boolean).join(" ")} onClick={() => { action.onClick?.(); if (action.doneLabel) setDone(true); }}>
    {done ? <><Check size={16} strokeWidth={1.75} aria-hidden="true" />{action.doneLabel}</> : <>{action.label}{icon}</>}
  </Button>;
}

/**
 * The hero with your own content: announcement, headline, description, actions, an optional install line, and an
 * optional visual beside the copy. Use it when none of the three designed variants fits the product.
 */
export function HeroContent({ layout, announcement, title, description, primaryAction, secondaryAction, install, media, meta, animateIn = true, className }: HeroContentProps) {
  const item = heroRise;
  const command = install?.[0];
  // Reduced motion: Motion skips every transform (rise, zoom, tilt entrance); opacity still fades briefly.
  return <MotionConfig reducedMotion="user"><section className={[shell.hero, styles.hero, styles[layout], className].filter(Boolean).join(" ")}>
    <div className={styles.inner}>
      <motion.div className={styles.copy} variants={heroGroup} initial={animateIn ? "hidden" : false} animate="shown">
        {announcement && <motion.div variants={item}>
          {announcement.href
            ? <a className={styles.announcement} href={announcement.href} onClick={announcement.onClick}>{announcement.label}<ArrowRight className={styles.arrow} size={14} strokeWidth={1.75} aria-hidden="true" /></a>
            : <button type="button" className={styles.announcement} onClick={announcement.onClick}>{announcement.label}<ArrowRight className={styles.arrow} size={14} strokeWidth={1.75} aria-hidden="true" /></button>}
        </motion.div>}
        <motion.h1 variants={item} className={[shell.title, styles.title].join(" ")}>{title}</motion.h1>
        {description && <motion.p variants={item} className={[shell.description, styles.description].join(" ")}>{description}</motion.p>}
        {(primaryAction || secondaryAction) && <motion.div variants={item} className={styles.actions}>
          {primaryAction && <ActionLink action={primaryAction} kind="primary" />}
          {secondaryAction && <ActionLink action={secondaryAction} kind="secondary" />}
        </motion.div>}
        {command && <motion.div variants={item} className={styles.install}>
          <span className={styles.prompt} aria-hidden="true">$</span>
          <code className={styles.command}>{command.command}</code>
          <CopyButton value={command.command} label="Copy install command" iconOnly variant="plain" />
        </motion.div>}
        {meta && meta.length > 0 && <motion.ul variants={item} className={styles.meta}>{meta.map(entry => <li key={entry}>{entry}</li>)}</motion.ul>}
      </motion.div>
      {media && <motion.div className={styles.media} variants={item} initial={animateIn ? "hidden" : false} animate="shown">{media}</motion.div>}
    </div>
  </section></MotionConfig>;
}

export default HeroContent;
```

### `registry/blocks/hero-section/hero-content.module.css`

```css
.inner { display: grid; max-width: 1200px; margin: 0 auto; padding: var(--space-screen) var(--space-page); }
.copy { display: grid; grid-template-columns: minmax(0, 1fr); justify-items: center; gap: var(--space-comfort); max-width: 760px; margin: 0 auto; text-align: center; }
.title { max-width: 16ch; }
.description { max-width: 560px; }

.announcement { display: inline-flex; max-width: 100%; min-height: 32px; align-items: center; gap: var(--space-related); border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; padding: var(--space-tight) var(--space-group); background: var(--surface); color: var(--text-secondary); font: inherit; font-size: var(--text-sm); text-align: left; text-decoration: none; cursor: pointer; transition: color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard); }
.arrow { flex: none; transition: transform var(--duration-fast) var(--ease-standard); }

.actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-group); margin-top: var(--space-group); }
.action { display: inline-flex; 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-comfort); font: inherit; font-size: var(--text-sm); font-weight: 500; text-decoration: none; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: transform var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
.action:active { transform: scale(.97); }
.primary { background: var(--foreground); color: var(--background); }
.secondary { border-color: var(--border); background: var(--surface); color: var(--foreground); }
@media (hover: hover) and (pointer: fine) {
  .primary:hover { opacity: .9; }
  .secondary:hover { background: var(--surface-muted); }
  .announcement:hover { border-color: var(--border-strong); color: var(--foreground); }
  :is(.action, .announcement):hover .arrow { transform: translateX(2px); }
}

.install { display: flex; width: min(100%, 440px); height: var(--control-height-md); align-items: center; gap: var(--space-related); margin-top: var(--space-related); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: 0 var(--space-tight) 0 var(--space-inset); background: var(--surface); font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: var(--text-sm); text-align: left; }
.prompt { color: var(--text-muted); }
.command { flex: 1; overflow: hidden; color: var(--foreground); font: inherit; text-overflow: ellipsis; white-space: nowrap; }

.meta { display: flex; flex-wrap: wrap; gap: var(--space-related) var(--space-comfort); margin: var(--space-inset) 0 0; border-top: 1px solid var(--border-subtle); padding: var(--space-comfort) 0 0; color: var(--text-muted); font-size: var(--text-sm); list-style: none; }

.media { min-width: 0; }

.split .inner { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); align-items: center; gap: var(--space-page); }
.split .copy, .minimal .copy { justify-items: start; margin: 0; text-align: left; }
.split .actions, .minimal .actions { justify-content: flex-start; }
.minimal .copy { max-width: 860px; }

@container hero (max-width: 800px) {
  .split .inner { grid-template-columns: minmax(0, 1fr); gap: var(--space-page); }
}
@container hero (max-width: 560px) {
  .inner { padding: var(--space-stage) var(--space-inset); }
  .actions { width: 100%; }
  .action { flex: 1 1 auto; }
}

@media (prefers-reduced-motion: reduce) {
  .action, .announcement, .arrow { transition: none; }
  .action:active { transform: none; }
}

/* Buttons are Silicon UI buttons; this only keeps them the height of the link actions beside them. */
.button { height: var(--control-height-md); min-height: var(--control-height-md); border-radius: var(--radius-control); corner-shape: var(--corner-shape); font-size: var(--text-sm); }
```

### `registry/blocks/hero-section/hero-motion.ts`

```ts
import { useSyncExternalStore } from "react";
import { useReducedMotion } from "motion/react";
import type { Transition, Variants } from "motion/react";
import { motionTokens } from "../../lib/motion-tokens";

export type Bezier = [number, number, number, number];
export const ease = {
  enter: [...motionTokens.ease.enter] as Bezier,
  standard: [...motionTokens.ease.standard] as Bezier,
  inOut: [...motionTokens.ease.inOut] as Bezier,
};

/** The one entrance a hero plays: children rise in reading order, once. */
export const heroGroup: Variants = { hidden: {}, shown: { transition: { staggerChildren: motionTokens.stagger.line, delayChildren: .04 } } };
export const heroRise: Variants = {
  hidden: { opacity: 0, y: 12, filter: `blur(${motionTokens.blur.subtle}px)` },
  shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: .64, ease: ease.enter } },
};
export const heroFade: Variants = { hidden: { opacity: 0 }, shown: { opacity: 1, transition: { duration: motionTokens.duration.standard } } };

export const instant: Transition = { duration: 0 };

const noop = () => () => {};
/**
 * Reduced motion, read only after hydration. The server cannot know the preference, so the first client render matches the
 * server's full motion markup and the reduced branch takes over on the next render, before anything has moved.
 */
export function useHeroReducedMotion() {
  const hydrated = useSyncExternalStore(noop, () => true, () => false);
  return !!useReducedMotion() && hydrated;
}
```

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

```tsx
"use client";

import { useState } from "react";
import { AnimatePresence, motion, useReducedMotion, type TargetAndTransition, type Transition } from "motion/react";
import { CircleAlert, Copy } from "lucide-react";
import { motionTokens } from "../lib/motion-tokens";
import { useCopyFeedback } from "../lib/use-copy-feedback";
import styles from "./copy-button.module.css";

export interface CopyButtonProps {
  value: string;
  label?: string;
  className?: string;
  iconOnly?: boolean;
  variant?: "outline" | "plain";
  disabled?: boolean;
  onCopied?: () => void;
}

/** Copy feedback is deliberately unhurried: a slow, almost critically damped spring and long, soft crossfades read as calm, never busy.
 *  The same motion plays in reverse when the confirmation hands back to idle, so nothing ever snaps. */
const settle = { type: "spring", visualDuration: .5, bounce: .06 } as const;
const enter = { duration: .36, ease: [...motionTokens.ease.enter] } as const;
const leave = { duration: .2, ease: [...motionTokens.ease.standard] } as const;
const instant = { duration: motionTokens.duration.instant } as const;
const soft = `blur(${motionTokens.blur.soft}px)`;
const rest: TargetAndTransition = { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" };
const fadeIn: TargetAndTransition = { ...rest, opacity: 0 };
const fadeOut: TargetAndTransition = { opacity: 0, transition: instant };
/** Icons trade places in one soft breath: the old glyph shrinks into a blur while the new one grows out of it on the slow spring. */
const iconIn: TargetAndTransition = { opacity: 0, scale: .6, filter: soft };
const iconOut: TargetAndTransition = { opacity: 0, scale: .6, filter: soft, transition: { duration: .24, ease: [...motionTokens.ease.standard] } };
const iconEnter: Transition = { scale: settle, opacity: { ...enter, delay: .03 }, filter: { ...enter, delay: .03 } };
/** Letters rise about .3em out of a soft blur; the outgoing ones lift away a little faster. */
const glyphIn: TargetAndTransition = { opacity: 0, y: 4, filter: soft };
const glyphOut: TargetAndTransition = { opacity: 0, y: -3, filter: soft, transition: leave };

/** The success tick draws itself from its short stroke, the way a hand would write it: quick to start, then easing into place while the icon settles. */
function DrawnCheck({ reduced }: { reduced: boolean }) {
  return <svg width={16} height={16} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.75} strokeLinecap="round" strokeLinejoin="round">
    <motion.path d="M4 12l5 5L20 6" initial={reduced ? false : { pathLength: 0, opacity: 0 }} animate={{ pathLength: 1, opacity: 1 }} transition={{ pathLength: { duration: .5, ease: [...motionTokens.ease.standard], delay: .05 }, opacity: { duration: .01, delay: .05 } }} />
  </svg>;
}

type Glyph = { id: string; char: string; order: number };
const toGlyphs = (chars: string[], seq: number): Glyph[] => chars.map((char, order) => ({ id: `${seq}:${order}`, char, order }));

/** Shared leading and trailing characters keep their identity, so "Copy" to "Copied" only replaces the changed letters. */
function useGlyphs(text: string) {
  const [state, setState] = useState(() => ({ text, seq: 0, glyphs: toGlyphs([...text], 0) }));
  if (state.text === text) return state.glyphs;
  const prev = [...state.text], next = [...text];
  let start = 0, end = 0;
  while (start < prev.length && start < next.length && prev[start] === next[start]) start++;
  while (end < prev.length - start && end < next.length - start && prev[prev.length - 1 - end] === next[next.length - 1 - end]) end++;
  if (start < 2) start = 0;
  if (end < 2) end = 0;
  const seq = state.seq + 1;
  const glyphs = [...state.glyphs.slice(0, start), ...toGlyphs(next.slice(start, next.length - end), seq), ...state.glyphs.slice(state.glyphs.length - end)];
  setState({ text, seq, glyphs });
  return glyphs;
}

/**
 * The label cell already reserves its widest state, so the row stays put and only the letters move. It is a layout root:
 * the letters measure their glide inside the label only, so when the button itself moves (a page entering, a panel
 * sliding, a list reflowing) they ride along instead of flying in from where the button used to be.
 */
function MorphText({ text, reduced }: { text: string; reduced: boolean }) {
  const glyphs = useGlyphs(text);
  return <motion.span className={styles.glyphs} layoutRoot>
    <AnimatePresence mode="popLayout" initial={false}>
      {glyphs.map(glyph => <motion.span key={glyph.id} className={styles.glyph} layout={reduced ? false : "position"} layoutDependency={text} initial={reduced ? fadeIn : glyphIn} animate={rest} exit={reduced ? fadeOut : glyphOut} transition={reduced ? instant : { ...enter, delay: Math.min(glyph.order * .02, .12), layout: settle }}>{glyph.char}</motion.span>)}
    </AnimatePresence>
  </motion.span>;
}

export function CopyButton({ value, label = "Copy", className, iconOnly = false, variant = "outline", disabled, onCopied }: CopyButtonProps) {
  const { state, copy } = useCopyFeedback();
  const reduced = useReducedMotion() ?? false;
  const text = state === "copied" ? "Copied" : state === "error" ? "Failed" : label;

  async function handleCopy() {
    if (await copy(value)) onCopied?.();
  }

  return <><button
    type="button"
    className={[styles.button, iconOnly && styles.iconOnly, variant === "plain" && styles.plain, className].filter(Boolean).join(" ")}
    onClick={() => void handleCopy()}
    aria-label={label}
    data-copy-state={state}
    disabled={disabled}
  >
    <span className={styles.icon} aria-hidden="true">
      <AnimatePresence initial={false}>
        <motion.span key={state} className={styles.iconInner} data-state={state} initial={reduced ? fadeIn : iconIn} animate={rest} exit={reduced ? fadeOut : iconOut} transition={reduced ? instant : iconEnter}>
          {state === "copied" ? <DrawnCheck reduced={reduced} /> : state === "error" ? <CircleAlert size={16} strokeWidth={1.75} /> : <Copy size={16} strokeWidth={1.75} />}
        </motion.span>
      </AnimatePresence>
    </span>
    {!iconOnly && <span className={styles.label} aria-hidden="true">
      <span className={styles.measure}>{label}</span><span className={styles.measure}>Copied</span><span className={styles.measure}>Failed</span>
      <MorphText text={text} reduced={reduced} />
    </span>}
  </button><span className={styles.srOnly} role="status" aria-live="polite">{state === "idle" ? "" : state === "error" ? `${label}: Could not copy` : `${label}: Copied`}</span></>;
}
```

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

```css
/* Width never changes: the label cell reserves its widest state while icon and letters crossfade inside it. Press releases on the spring curve. */
.button { display: inline-flex; width: max-content; max-width: 100%; min-height: var(--control-height-sm); align-items: center; justify-content: center; gap: var(--space-related); border: 1px solid var(--border); border-radius: var(--radius-control); padding: 0 var(--space-group); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
.button:active:not(:disabled) { transform: scale(.97); transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-fast), var(--duration-instant); transition-timing-function: var(--ease-standard); }
.iconOnly:active:not(:disabled) { transform: scale(.96); }
@media (hover: hover) and (pointer: fine) { .button:hover:not(:disabled) { background: var(--surface-muted); border-color: var(--border-strong); } .plain:hover:not(:disabled) { border-color: transparent; background: var(--surface-muted); color: var(--foreground); } }
.button:disabled { cursor: not-allowed; opacity: .5; }
/* Each icon carries its own color, so the outgoing glyph never tints on its way out. */
.icon { position: relative; display: grid; width: 16px; height: 16px; flex: none; }
.iconInner { position: absolute; inset: 0; display: grid; place-items: center; }
.iconInner[data-state="copied"] { color: var(--success); }
.iconInner[data-state="error"] { color: var(--danger); }
/* The inline padding gives blurred edge letters room before the horizontal clip; the negative margin keeps the box the same size. */
.label { display: grid; min-width: 0; margin-inline: calc(var(--space-tight) * -1); padding-inline: var(--space-tight); overflow-x: clip; text-align: left; white-space: nowrap; }
.measure, .glyphs { grid-area: 1 / 1; }
.measure { visibility: hidden; }
.glyphs { position: relative; display: inline-flex; justify-self: start; white-space: pre; }
.glyph { display: inline-block; }
.iconOnly { width: var(--control-height-sm); padding: 0; }
.plain { border-color: transparent; background: transparent; color: var(--text-secondary); }
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
@media (prefers-reduced-motion: reduce) { .button { transition: none; } .button:active:not(:disabled) { transform: none; } }
```

### `lib/use-copy-feedback.ts`

```ts
"use client";

import { useCallback, useEffect, useRef, useState } from "react";

export type CopyFeedbackState = "idle" | "copied" | "error";

/** Shared clipboard state for actions that render their own button or menu. */
export function useCopyFeedback(duration = 1900) {
  const [state, setState] = useState<CopyFeedbackState>("idle");
  const [activeKey, setActiveKey] = useState<string | null>(null);
  const timeout = useRef<ReturnType<typeof setTimeout> | null>(null);

  const reset = useCallback(() => {
    if (timeout.current) clearTimeout(timeout.current);
    timeout.current = null;
    setState("idle");
    setActiveKey(null);
  }, []);

  useEffect(() => () => {
    if (timeout.current) clearTimeout(timeout.current);
  }, []);

  const copy = useCallback(async (value: string, key = "default") => {
    if (timeout.current) clearTimeout(timeout.current);
    setActiveKey(key);
    try {
      await navigator.clipboard.writeText(value);
      setState("copied");
      timeout.current = setTimeout(reset, duration);
      return true;
    } catch {
      setState("error");
      timeout.current = setTimeout(reset, duration);
      return false;
    }
  }, [duration, reset]);

  return { state, activeKey, copy, reset };
}
```

### `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/blocks/hero-section/hero-mesh.tsx`

```tsx
"use client";

import { useEffect, useRef } from "react";
import type { CSSProperties } from "react";
import { useReducedMotion } from "motion/react";
import styles from "./hero-mesh.module.css";

/**
 * One point of a mesh, in the same model the Gradient mesh editor saves: a position as a fraction of the box (0 to 1 from the
 * top left) and a reach. The color is not given here: it is read from the CSS variable `--mesh-{n}` on the element, so light
 * and dark themes each author their own palette in CSS and the canvas follows a theme switch without a remount.
 */
export interface HeroMeshPoint {
  x: number;
  y: number;
  /** Reach of the color as a fraction of the box, 0.15 to 1.2. */
  spread: number;
}

export interface HeroMeshProps {
  points: readonly HeroMeshPoint[];
  /** Film grain, 0 to 1. */
  grain?: number;
  /** Drift speed. 1 loops every 20 seconds; 0 holds the mesh still. */
  speed?: number;
  className?: string;
  style?: CSSProperties;
}

type Rgb = [number, number, number];

/** Same loop as the Gradient mesh editor: integer frequencies, so the drift is seamless. */
const PERIOD = 20, TRAVEL = .075, BREATH = .08, MAX = 8;
/** The drift is slow, so thirty frames a second is indistinguishable from sixty and halves the cost. */
const FRAME = 1000 / 30;

const VS = `attribute vec2 a; void main() { gl_Position = vec4(a, 0.0, 1.0); }`;
const FS = `
precision mediump float;
uniform vec2 uRes;
uniform vec3 uBase;
uniform vec4 uP[${MAX}];
uniform vec3 uC[${MAX}];
uniform int uN;
uniform float uGrain;
float hash(vec2 p) { vec3 q = fract(vec3(p.xyx) * .1031); q += dot(q, q.yzx + 33.33); return fract((q.x + q.y) * q.z); }
void main() {
  vec2 uv = vec2(gl_FragCoord.x / uRes.x, 1.0 - gl_FragCoord.y / uRes.y);
  vec3 col = uBase;
  for (int i = 0; i < ${MAX}; i++) {
    if (i >= uN) break;
    vec4 p = uP[i];
    float d = length((uv - p.xy) / max(p.z, 1e-4));
    col = mix(col, uC[i], 1.0 - smoothstep(0.0, 1.0, d));
  }
  float n = hash(floor(gl_FragCoord.xy)) + hash(floor(gl_FragCoord.xy) + 19.19) - 1.0;
  float lum = dot(col, vec3(.2126, .7152, .0722));
  col += n * (uGrain * .1 * (.55 + 1.8 * lum * (1.0 - lum)) + .6 / 255.0);
  gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
}`;

function hashOf(text: string) {
  let h = 2166136261;
  for (let i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 16777619); }
  return h >>> 0;
}

const UNIFORMS = ["uRes", "uBase", "uP", "uC", "uN", "uGrain"] as const;

/** Resolves any CSS color (hex, oklch, color-mix) to sRGB through a one pixel 2D canvas. */
function toRgb(color: string, scratch: CanvasRenderingContext2D): Rgb {
  scratch.clearRect(0, 0, 1, 1);
  scratch.fillStyle = "#000";
  scratch.fillStyle = color;
  scratch.fillRect(0, 0, 1, 1);
  const [r = 0, g = 0, b = 0] = scratch.getImageData(0, 0, 1, 1).data;
  return [r / 255, g / 255, b / 255];
}

/** The static CSS render of the same mesh: shown on the server, before the canvas paints, and when WebGL is missing. */
function cssMesh(points: readonly HeroMeshPoint[]): CSSProperties {
  const stops = [0, .2, .4, .6, .8, 1].map(t => [t, Math.round((1 - t * t * (3 - 2 * t)) * 100)] as const);
  const layers = points.map((p, i) => `radial-gradient(ellipse ${p.spread * 100}% ${p.spread * 100}% at ${p.x * 100}% ${p.y * 100}%, ${stops.map(([t, a]) => `color-mix(in srgb, var(--mesh-${i + 1}) ${a}%, transparent) ${t * 100}%`).join(", ")})`);
  return { backgroundColor: "var(--mesh-base)", backgroundImage: layers.reverse().join(", ") };
}

/**
 * A lean, non-editing render of the Gradient mesh: a single WebGL triangle composites each point over the last with the
 * editor's smoothstep falloff and grain, drifting on the editor's seamless loop. It draws at thirty frames a second only while
 * it is on screen and the tab is visible, and draws one still frame under reduced motion.
 */
export function HeroMesh({ points, grain = .35, speed = 1, className, style }: HeroMeshProps) {
  const root = useRef<HTMLDivElement>(null);
  const canvas = useRef<HTMLCanvasElement>(null);
  const reduced = !!useReducedMotion();
  const key = JSON.stringify(points);

  useEffect(() => {
    const host = root.current, node = canvas.current;
    if (!host || !node) return;
    const gl = node.getContext("webgl", { alpha: false, antialias: false, depth: false, stencil: false, powerPreference: "low-power" });
    const scratch = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
    if (!gl || !scratch) return;
    const compile = (type: number, src: string) => { const s = gl.createShader(type)!; gl.shaderSource(s, src); gl.compileShader(s); return s; };
    const prog = gl.createProgram()!;
    gl.attachShader(prog, compile(gl.VERTEX_SHADER, VS));
    gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FS));
    gl.linkProgram(prog);
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return;
    gl.useProgram(prog);
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
    const loc = gl.getAttribLocation(prog, "a");
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
    const u = Object.fromEntries(UNIFORMS.map(name => [name, gl.getUniformLocation(prog, name)])) as Record<(typeof UNIFORMS)[number], WebGLUniformLocation | null>;

    const pts = (JSON.parse(key) as HeroMeshPoint[]).slice(0, MAX);
    const drift = pts.map((_, i) => { const h = hashOf(`mesh-${i}`); return { ph1: (h % 628) / 100, ph2: ((h >>> 10) % 628) / 100, k1: 1 + ((h >>> 20) % 2), k2: 1 + ((h >>> 22) % 2) }; });
    const P = new Float32Array(MAX * 4), C = new Float32Array(MAX * 3);
    let base: Rgb = [1, 1, 1];

    const readColors = () => {
      const css = getComputedStyle(host);
      base = toRgb(css.getPropertyValue("--mesh-base").trim() || "#fff", scratch);
      pts.forEach((_, i) => { const c = toRgb(css.getPropertyValue(`--mesh-${i + 1}`).trim() || "#fff", scratch); C.set(c, i * 3); });
    };

    let width = 0, height = 0;
    const size = () => {
      // The mesh has no detail finer than its grain, so one canvas pixel per CSS pixel is enough on any screen.
      width = Math.max(1, Math.round(host.clientWidth));
      height = Math.max(1, Math.round(host.clientHeight));
      node.width = width; node.height = height;
    };

    let t = hashOf(key) % PERIOD;
    const draw = () => {
      const a = (2 * Math.PI * t) / PERIOD;
      pts.forEach((p, i) => {
        const d = drift[i]!;
        P[i * 4] = p.x + TRAVEL * Math.sin(a * d.k1 + d.ph1);
        P[i * 4 + 1] = p.y + TRAVEL * Math.cos(a * d.k2 + d.ph2);
        P[i * 4 + 2] = p.spread * (1 + BREATH * Math.sin(a + d.ph1 * .7));
      });
      gl.viewport(0, 0, width, height);
      gl.uniform2f(u.uRes, width, height);
      gl.uniform3f(u.uBase, base[0], base[1], base[2]);
      gl.uniform4fv(u.uP, P);
      gl.uniform3fv(u.uC, C);
      gl.uniform1i(u.uN, pts.length);
      gl.uniform1f(u.uGrain, grain);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
      host.dataset.painted = "";
    };

    readColors();
    size();
    draw();

    const moving = !reduced && speed > 0;
    let visible = false, frame = 0, last = 0;
    const tick = (now: number) => {
      frame = requestAnimationFrame(tick);
      if (now - last < FRAME) return;
      const dt = last ? Math.min(.1, (now - last) / 1000) : 0;
      last = now;
      t = (t + dt * speed) % PERIOD;
      draw();
    };
    const sync = () => {
      const run = moving && visible && document.visibilityState === "visible";
      if (run && !frame) { last = 0; frame = requestAnimationFrame(tick); }
      if (!run && frame) { cancelAnimationFrame(frame); frame = 0; }
    };
    const io = new IntersectionObserver(([entry]) => { if (!entry) return; visible = entry.isIntersecting; sync(); });
    io.observe(host);
    const ro = new ResizeObserver(() => { size(); draw(); });
    ro.observe(host);
    const theme = () => { readColors(); draw(); };
    const mo = new MutationObserver(theme);
    mo.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "data-accent", "class"] });
    const scheme = matchMedia("(prefers-color-scheme: dark)");
    scheme.addEventListener("change", theme);
    document.addEventListener("visibilitychange", sync);
    return () => {
      cancelAnimationFrame(frame);
      io.disconnect(); ro.disconnect(); mo.disconnect();
      scheme.removeEventListener("change", theme);
      document.removeEventListener("visibilitychange", sync);
      delete host.dataset.painted;
    };
  }, [key, grain, speed, reduced]);

  return <div ref={root} className={[styles.mesh, className].filter(Boolean).join(" ")} style={{ ...cssMesh(points), ...style }} aria-hidden="true">
    <canvas ref={canvas} className={styles.canvas} />
  </div>;
}

export default HeroMesh;
```

### `registry/blocks/hero-section/hero-mesh.module.css`

```css
/* The canvas fades in over its own CSS render once it has painted, so the first frame never flashes. */
.mesh { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.canvas { display: block; width: 100%; height: 100%; opacity: 0; transition: opacity var(--duration-considered, .48s) var(--ease-standard); }
.mesh[data-painted] .canvas { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .canvas { transition: none; } }
```

### `registry/blocks/hero-section/hero-cadence.tsx`

```tsx
"use client";

import { MotionConfig, motion } from "motion/react";
import { ActionLink } from "./hero-content";
import type { HeroAction } from "./hero-content";
import { HeroMesh } from "./hero-mesh";
import type { HeroMeshPoint } from "./hero-mesh";
import { heroGroup, heroRise } from "./hero-motion";
import shell from "./hero-section.module.css";
import styles from "./hero-cadence.module.css";

export interface HeroCadenceProps {
  primaryAction?: HeroAction;
  secondaryAction?: HeroAction;
  animateIn?: boolean;
  className?: string;
}

/** Six points, authored the way the Gradient mesh editor saves them. Colors live in CSS, one palette per theme. */
const MESH: HeroMeshPoint[] = [
  { x: .1, y: .88, spread: .72 },
  { x: .86, y: .12, spread: .66 },
  { x: .16, y: .14, spread: .56 },
  { x: .62, y: .62, spread: .5 },
  { x: .94, y: .92, spread: .48 },
  { x: .44, y: .3, spread: .34 },
];

/** Sample customers. The marks are the brands' own icons, drawn in the text color. */
const BRANDS = [
  { name: "Linear", logo: "/block-logos/linear.svg" },
  { name: "Vercel", logo: "/block-logos/vercel.svg" },
  { name: "Raycast", logo: "/block-logos/raycast.svg" },
  { name: "Notion", logo: "/block-logos/notion.svg" },
  { name: "Figma", logo: "/block-logos/figma.svg" },
  { name: "Stripe", logo: "/block-logos/stripe.svg" },
  { name: "Loom", logo: "/block-logos/loom.svg" },
];

/** Minimal hero, one full screen: large type over a slowly drifting mesh gradient with grain, and the teams that use the product along the bottom edge. */
export function HeroCadence({ primaryAction = { label: "Download for Mac", doneLabel: "Download started" }, secondaryAction = { label: "Try it on the web", doneLabel: "Opening Cadence" }, animateIn = true, className }: HeroCadenceProps) {
  const item = heroRise;
  // Reduced motion: Motion skips every transform (rise, zoom, tilt entrance); opacity still fades briefly.
  return <MotionConfig reducedMotion="user"><section className={[shell.hero, shell.screen, styles.hero, className].filter(Boolean).join(" ")}>
    <HeroMesh className={styles.mesh} points={MESH} grain={.55} speed={.45} />
    <motion.div className={styles.inner} variants={heroGroup} initial={animateIn ? "hidden" : false} animate="shown">
      <motion.h1 variants={item} className={[shell.title, styles.title].join(" ")}>Your week, planned before Monday</motion.h1>
      <div className={styles.row}>
        <motion.p variants={item} className={[shell.description, styles.description].join(" ")}>Cadence reads your tasks, meetings and focus goals, then books time for the work that matters. When plans change, it moves everything for you.</motion.p>
        <motion.div variants={item} className={styles.cta}>
          <div className={styles.actions}>
            <ActionLink action={primaryAction} kind="primary" />
            <ActionLink action={secondaryAction} kind="secondary" />
          </div>
          <p className={styles.fine}>Free for personal use. Teams from $8 a seat.</p>
        </motion.div>
      </div>
      <motion.div variants={item} className={styles.brands}>
        <p>Teams at these companies plan with Cadence</p>
        <ul aria-label="Sample customers">
          {BRANDS.map(brand => <li key={brand.name}><i style={{ maskImage: `url(${brand.logo})`, WebkitMaskImage: `url(${brand.logo})` }} aria-hidden="true" />{brand.name}</li>)}
        </ul>
      </motion.div>
    </motion.div>
  </section></MotionConfig>;
}

export default HeroCadence;
```

### `registry/blocks/hero-section/hero-cadence.module.css`

```css
/* Minimal: the mesh is the whole screen, and type does the rest. The copy sits in the optical centre; customers hold the bottom edge. */
.hero { isolation: isolate; display: grid; }
.mesh {
  --mesh-base: #f2ede7;
  --mesh-1: #bdd3ec; --mesh-2: #c3c6ff; --mesh-3: #ffd0b0; --mesh-4: #f6bfd6; --mesh-5: #cfe8dc; --mesh-6: #fff3dc;
  z-index: -1;
}
:global(:root[data-theme="dark"]) .mesh {
  --mesh-base: #09090f;
  --mesh-1: #0d3a47; --mesh-2: #2a2f86; --mesh-3: #5a2a3c; --mesh-4: #3a1f5a; --mesh-5: #7a3a22; --mesh-6: #12142c;
}
/* A theme-aware scrim keeps body text above 4.5:1 wherever the mesh drifts. */
.hero::after { content: ""; position: absolute; z-index: -1; inset: 0; background: linear-gradient(to bottom, transparent 40%, color-mix(in oklab, var(--background) 22%, transparent)); pointer-events: none; }

.inner { display: grid; box-sizing: border-box; width: 100%; max-width: 1320px; grid-template-rows: 1fr auto auto 1fr auto; gap: var(--space-page); margin: 0 auto; padding: clamp(var(--space-screen), 12svh, calc(var(--space-screen) + var(--space-page))) var(--space-page) var(--space-page); }
.title { grid-row: 2; }
.row { grid-row: 3; }
.brands { grid-row: 5; }
.title { max-width: 14ch; font-size: clamp(var(--text-4xl), var(--text-base) + 7cqi, var(--text-5xl)); }
.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--space-block) var(--space-stage); }
.description { max-width: 520px; color: color-mix(in oklab, var(--foreground) 76%, transparent); }
.cta { display: grid; justify-items: end; gap: var(--space-group); }
.actions { display: flex; flex-wrap: wrap; gap: var(--space-group); }
.fine { margin: 0; color: color-mix(in oklab, var(--foreground) 62%, transparent); font-size: var(--text-sm); }

.brands { display: grid; gap: var(--space-comfort); border-top: 1px solid color-mix(in oklab, var(--foreground) 14%, transparent); padding-top: var(--space-section); }
.brands p { margin: 0; color: color-mix(in oklab, var(--foreground) 62%, transparent); font-size: var(--text-sm); }
.brands ul { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-comfort) var(--space-block); margin: 0; padding: 0; list-style: none; }
.brands li { display: inline-flex; align-items: center; gap: var(--space-related); color: color-mix(in oklab, var(--foreground) 78%, transparent); font-size: var(--text-lg); font-weight: 500; letter-spacing: var(--tracking-body); }
.brands i { width: 22px; height: 22px; background: currentColor; -webkit-mask-position: center; mask-position: center; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; -webkit-mask-size: contain; mask-size: contain; }

@container hero (max-width: 800px) {
  .row { grid-template-columns: minmax(0, 1fr); align-items: start; }
  .cta { justify-items: start; }
  .brands ul { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@container hero (max-width: 560px) {
  .inner { gap: var(--space-section); padding: var(--space-screen) var(--space-inset) var(--space-section); }
  .actions { width: 100%; }
  .actions > * { flex: 1 1 auto; }
  .cta { width: 100%; }
  .brands ul { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-inset) var(--space-group); }
  .brands li:nth-child(n + 7) { display: none; }
  .brands li { font-size: var(--text-base); }
  .brands i { width: 18px; height: 18px; }
}
```

### `registry/blocks/hero-section/hero-relay.tsx`

```tsx
"use client";

import { useEffect, useLayoutEffect, useRef, useState } from "react";
import type { ReactNode } from "react";
import { AnimatePresence, MotionConfig, motion } from "motion/react";
import { Check, Database, Minus, Split } from "lucide-react";
import { Button } from "../../button/button";
import { motionTokens } from "../../lib/motion-tokens";
import { ActionLink } from "./hero-content";
import type { HeroAction } from "./hero-content";
import { ease, heroGroup, heroRise, useHeroReducedMotion } from "./hero-motion";
import shell from "./hero-section.module.css";
import styles from "./hero-relay.module.css";

type NodeId = "trigger" | "lookup" | "branch" | "slack" | "linear" | "warehouse";
type Status = "idle" | "running" | "done" | "skipped";

/** Sample events. Each one runs the same workflow; the branch decides which actions it reaches. */
const EVENTS = [
  { customer: "Northwind Labs", amount: "$4,800", plan: "Enterprise, 212 seats", routed: true, issue: "ONB-482", total: 412, ms: { lookup: 118, branch: 2, slack: 164, linear: 231, warehouse: 58 } },
  { customer: "Tidepool", amount: "$240", plan: "Team, 9 seats", routed: false, issue: "", total: 171, ms: { lookup: 96, branch: 1, slack: 0, linear: 0, warehouse: 61 } },
  { customer: "Halcyon Health", amount: "$12,600", plan: "Enterprise, 540 seats", routed: true, issue: "ONB-483", total: 436, ms: { lookup: 131, branch: 2, slack: 149, linear: 244, warehouse: 66 } },
] as const;
type RelayEvent = (typeof EVENTS)[number];

/**
 * One run in phases. Odd phases run a step, even phases carry the data along the edges out of it; the last phase holds the
 * finished run on screen before the next event arrives. Durations in milliseconds.
 */
const PHASES = [900, 420, 380, 640, 380, 360, 440, 760, 2800];
const DONE = PHASES.length - 1;
const STEPS: Record<NodeId, [start: number, end: number]> = { trigger: [1, 2], lookup: [3, 4], branch: [5, 6], slack: [7, 8], linear: [7, 8], warehouse: [7, 8] };
const ACTIONS = ["slack", "linear", "warehouse"] as const;

function statusOf(id: NodeId, phase: number, event: RelayEvent): Status {
  const [start, end] = STEPS[id];
  if ((id === "slack" || id === "linear") && !event.routed && phase >= 6) return "skipped";
  return phase < start ? "idle" : phase < end ? "running" : "done";
}

/** The graph is drawn at a fixed size and scaled to its column, like a screenshot, so it never reflows mid run. */
type Box = { x: number; y: number; w: number; h: number };
const LAYOUTS = {
  wide: {
    width: 640, height: 524, summary: 480,
    nodes: { trigger: { x: 170, y: 0, w: 300, h: 60 }, lookup: { x: 170, y: 112, w: 300, h: 60 }, branch: { x: 170, y: 224, w: 300, h: 60 }, slack: { x: 0, y: 356, w: 200, h: 84 }, linear: { x: 220, y: 356, w: 200, h: 84 }, warehouse: { x: 440, y: 356, w: 200, h: 84 } } as Record<NodeId, Box>,
  },
  narrow: {
    width: 344, height: 316, summary: 0,
    nodes: { trigger: { x: 0, y: 0, w: 344, h: 52 }, lookup: { x: 0, y: 78, w: 344, h: 52 }, branch: { x: 0, y: 156, w: 344, h: 52 }, slack: { x: 0, y: 244, w: 108, h: 72 }, linear: { x: 118, y: 244, w: 108, h: 72 }, warehouse: { x: 236, y: 244, w: 108, h: 72 } } as Record<NodeId, Box>,
  },
};
type Layout = (typeof LAYOUTS)[keyof typeof LAYOUTS];

const EDGES: { from: NodeId; to: NodeId; phase: number }[] = [
  { from: "trigger", to: "lookup", phase: 2 },
  { from: "lookup", to: "branch", phase: 4 },
  { from: "branch", to: "slack", phase: 6 },
  { from: "branch", to: "linear", phase: 6 },
  { from: "branch", to: "warehouse", phase: 6 },
];

function edgePath(layout: Layout, from: NodeId, to: NodeId) {
  const a = layout.nodes[from], b = layout.nodes[to];
  const x1 = a.x + a.w / 2, y1 = a.y + a.h, x2 = b.x + b.w / 2, y2 = b.y;
  const mid = (y2 - y1) / 2;
  return `M ${x1} ${y1} C ${x1} ${y1 + mid} ${x2} ${y2 - mid} ${x2} ${y2}`;
}

const RESULTS: Record<(typeof ACTIONS)[number], (event: RelayEvent) => string> = {
  slack: event => `Sent in ${event.ms.slack} ms`,
  linear: event => `${event.issue} in ${event.ms.linear} ms`,
  warehouse: event => `Saved in ${event.ms.warehouse} ms`,
};

const logo = (name: string) => <span className={styles.logo} style={{ backgroundImage: `url(/block-logos/${name}.svg)` }} aria-hidden="true" />;

export interface HeroRelayProps {
  primaryAction?: HeroAction;
  secondaryAction?: HeroAction;
  animateIn?: boolean;
  className?: string;
}

/**
 * Split hero, one full screen: the copy beside a live workflow graph on a dotted canvas. Sample Stripe events arrive on their
 * own and run the workflow node by node; the data draws along each edge, the branch decides which actions run, and every step
 * reports its time. "Send test event" runs the next sample at once. The run pauses off screen, and under reduced motion each
 * event shows its finished run without playing.
 */
export function HeroRelay({ primaryAction = { label: "Start building", doneLabel: "Workspace created" }, secondaryAction = { label: "Read the docs", doneLabel: "Opening docs" }, animateIn = true, className }: HeroRelayProps) {
  const item = heroRise;
  const reduced = useHeroReducedMotion();
  const root = useRef<HTMLElement>(null);
  const stage = useRef<HTMLDivElement>(null);
  const [mode, setMode] = useState<keyof typeof LAYOUTS>("wide");
  const [scale, setScale] = useState(1);
  const [run, setRun] = useState({ index: 0, count: 2418, phase: 0 });
  const [visible, setVisible] = useState(false);

  useLayoutEffect(() => {
    const node = stage.current;
    if (!node) return;
    const measure = () => {
      const width = node.clientWidth;
      const next = width < 520 ? "narrow" : "wide";
      setMode(next);
      setScale(Math.min(1, width / LAYOUTS[next].width));
    };
    measure();
    const observer = new ResizeObserver(measure);
    observer.observe(node);
    return () => observer.disconnect();
  }, []);

  // The run only plays while the hero is on screen and the tab is visible.
  useEffect(() => {
    const node = root.current;
    if (!node) return;
    let onScreen = false;
    const sync = () => setVisible(onScreen && document.visibilityState === "visible");
    const io = new IntersectionObserver(([entry]) => { if (!entry) return; onScreen = entry.isIntersecting; sync(); });
    io.observe(node);
    document.addEventListener("visibilitychange", sync);
    return () => { io.disconnect(); document.removeEventListener("visibilitychange", sync); };
  }, []);

  useEffect(() => {
    if (reduced || !visible) return;
    const timer = window.setTimeout(() => setRun(current => current.phase < DONE
      ? { ...current, phase: current.phase + 1 }
      : { index: (current.index + 1) % EVENTS.length, count: current.count + 1, phase: 0 }), PHASES[run.phase]);
    return () => window.clearTimeout(timer);
  }, [run.phase, run.index, reduced, visible]);

  const sendTest = () => setRun(current => ({ index: (current.index + 1) % EVENTS.length, count: current.count + 1, phase: 1 }));

  const layout = LAYOUTS[mode];
  const event = EVENTS[run.index]!;
  // Reduced motion: every event is shown as its finished run; the button moves to the next one at once.
  const phase = reduced ? DONE : run.phase;
  const status = (id: NodeId) => statusOf(id, phase, event);
  const narrow = mode === "narrow";

  // Reduced motion: Motion skips every transform (rise, tilt entrance); opacity still fades briefly.
  return <MotionConfig reducedMotion="user"><section ref={root} className={[shell.hero, shell.screen, styles.hero, className].filter(Boolean).join(" ")}>
    <div className={styles.inner}>
      <motion.div className={styles.copy} variants={heroGroup} initial={animateIn ? "hidden" : false} animate="shown">
        <motion.h1 variants={item} className={[shell.title, styles.title].join(" ")}>Every event, handled in milliseconds</motion.h1>
        <motion.p variants={item} className={[shell.description, styles.description].join(" ")}>Relay turns webhooks from Stripe, GitHub and Postgres into typed workflows with retries, branches and a trace of every run. Write steps in TypeScript, or wire them on the canvas.</motion.p>
        <motion.div variants={item} className={styles.actions}>
          <ActionLink action={primaryAction} kind="primary" />
          <ActionLink action={secondaryAction} kind="secondary" />
        </motion.div>
        <motion.p variants={item} className={styles.fine}>Free for 10,000 runs a month. No card needed.</motion.p>
      </motion.div>

      <motion.div
        className={styles.canvas}
        initial={animateIn ? { opacity: 0, y: 24 } : false}
        animate={{ opacity: 1, y: 0 }}
        transition={{ duration: 1, ease: ease.enter, delay: .28 }}
      >
        <div className={styles.toolbar}>
          <span className={styles.file}><i className={styles.live} data-on={visible || reduced || undefined} aria-hidden="true" />on-invoice-paid.ts</span>
          <Button variant="secondary" size="sm" onClick={sendTest}>Send test event</Button>
        </div>
        <div ref={stage} className={styles.stage} style={{ height: layout.height * scale }}>
          <div className={styles.graph} role="group" aria-label={`Relay workflow, sample run for ${event.customer}`} style={{ width: layout.width, height: layout.height, transform: `scale(${scale})` }}>
            <svg className={styles.edges} width={layout.width} height={layout.height} viewBox={`0 0 ${layout.width} ${layout.height}`} aria-hidden="true">
              {EDGES.map(edge => {
                const d = edgePath(layout, edge.from, edge.to);
                const skipped = status(edge.to) === "skipped";
                const lit = phase >= edge.phase && !skipped;
                return <g key={`${edge.from}-${edge.to}`}>
                  <path d={d} className={styles.wire} data-skipped={skipped || undefined} />
                  <motion.path
                    d={d}
                    className={styles.flow}
                    initial={false}
                    animate={{ pathLength: lit ? 1 : 0, opacity: lit ? 1 : 0 }}
                    transition={lit && !reduced ? { pathLength: { duration: PHASES[edge.phase]! / 1000, ease: ease.inOut }, opacity: { duration: .08 } } : { duration: 0 }}
                  />
                </g>;
              })}
            </svg>

            <RelayNode box={layout.nodes.trigger} status={status("trigger")} icon={logo("stripe-color")} title="Invoice paid" sub={`${event.customer}, ${event.amount}`} />
            <RelayNode box={layout.nodes.lookup} status={status("lookup")} icon={logo("hubspot-color")} title="Find account in HubSpot" sub={status("lookup") === "done" ? event.plan : "Plan, seats and owner"} meta={`${event.ms.lookup} ms`} />
            <RelayNode box={layout.nodes.branch} status={status("branch")} icon={<Split size={17} strokeWidth={1.75} className={styles.glyph} aria-hidden="true" />} title="Amount over $1,000" sub={status("branch") === "done" ? event.routed ? "Yes, alert the team" : "No, record only" : "Condition"} meta={`${event.ms.branch} ms`} />
            {ACTIONS.map(id => {
              const s = status(id);
              const copy = {
                slack: { icon: logo("slack-color"), title: narrow ? "Slack" : "Post to #revenue", waiting: "Slack" },
                linear: { icon: logo("linear-color"), title: narrow ? "Linear" : "Create Linear issue", waiting: "Onboarding team" },
                warehouse: { icon: <Database size={16} strokeWidth={1.75} className={styles.glyph} aria-hidden="true" />, title: narrow ? "Warehouse" : "Save to warehouse", waiting: "Postgres" },
              }[id];
              const sub = s === "skipped" ? "Skipped" : s === "done" ? narrow ? `${event.ms[id]} ms` : RESULTS[id](event) : s === "running" ? "Running" : narrow ? "Waiting" : copy.waiting;
              return <RelayNode key={id} box={layout.nodes[id]} compact status={s} icon={copy.icon} title={copy.title} sub={sub} />;
            })}

            {!narrow && <div className={styles.summary} style={{ top: layout.summary, width: layout.width }}>
              <span className={styles.event}><code>invoice.paid</code><span>Run <span className={styles.num}>{run.count.toLocaleString("en-US")}</span></span></span>
              <AnimatePresence mode="popLayout" initial={false}>
                <motion.span
                  key={phase === DONE ? `done-${run.count}` : phase === 0 ? "waiting" : "running"}
                  className={styles.result}
                  data-done={phase === DONE || undefined}
                  initial={{ opacity: 0, y: 6 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={{ opacity: 0, y: -6 }}
                  transition={{ duration: motionTokens.duration.fast, ease: ease.standard }}
                >
                  {phase === DONE ? <><Check size={14} strokeWidth={2} aria-hidden="true" /><span>Completed in <span className={styles.num}>{event.total} ms</span></span></> : phase === 0 ? "Waiting for the next event" : <><i className={styles.spinner} aria-hidden="true" />Running</>}
                </motion.span>
              </AnimatePresence>
            </div>}
          </div>
        </div>
      </motion.div>
    </div>
  </section></MotionConfig>;
}

function RelayNode({ box, status, icon, title, sub, meta, compact = false }: { box: Box; status: Status; icon: ReactNode; title: string; sub: string; meta?: string; compact?: boolean }) {
  return <div className={styles.node} data-status={status} data-compact={compact || undefined} style={{ left: box.x, top: box.y, width: box.w, height: box.h }}>
    <span className={styles.icon}>{icon}</span>
    <span className={styles.text}>
      <strong>{title}</strong>
      <small>{sub}</small>
    </span>
    <span className={styles.state}>
      <AnimatePresence mode="popLayout" initial={false}>
        <motion.span
          key={status}
          className={styles.stateInner}
          initial={{ opacity: 0, scale: .6 }}
          animate={{ opacity: 1, scale: 1 }}
          exit={{ opacity: 0, scale: .6 }}
          transition={motionTokens.spring.snappy}
        >
          {status === "running" ? <i className={styles.spinner} role="img" aria-label="Running" />
            : status === "done" ? <>{meta && <span className={styles.meta}>{meta}</span>}<Check size={14} strokeWidth={2} className={styles.check} role="img" aria-label="Done" /></>
            : status === "skipped" ? <Minus size={14} strokeWidth={2} className={styles.skip} role="img" aria-label="Skipped" />
            : <i className={styles.pending} role="img" aria-label="Waiting" />}
        </motion.span>
      </AnimatePresence>
    </span>
  </div>;
}

export default HeroRelay;
```

### `registry/blocks/hero-section/hero-relay.module.css`

```css
/* Split, one full screen: the whole hero is a workflow canvas. A dot grid fills the screen, strongest behind the graph, and the
   copy sits on the quiet side of it. */
.hero {
  --dot: color-mix(in oklab, var(--foreground) 13%, transparent);
  --wire: color-mix(in oklab, var(--foreground) 17%, transparent);
  --node-shadow: 0 1px 1px rgb(20 24 40 / .04), 0 8px 24px -12px rgb(20 24 40 / .14);
  isolation: isolate;
  display: grid;
  align-items: center;
  background: color-mix(in oklab, var(--accent) 2%, var(--background));
}
:global(:root[data-theme="dark"]) .hero {
  --dot: color-mix(in oklab, var(--foreground) 17%, transparent);
  --wire: color-mix(in oklab, var(--foreground) 20%, transparent);
  --node-shadow: inset 0 1px 0 color-mix(in oklab, var(--foreground) 7%, transparent), 0 12px 32px -14px rgb(0 0 0 / .7);
}
.hero::before {
  content: ""; position: absolute; z-index: -1; inset: 0; pointer-events: none;
  background-image: radial-gradient(circle at 1px 1px, var(--dot) 1px, transparent 1.5px);
  background-size: 22px 22px;
  -webkit-mask-image: radial-gradient(70% 80% at 72% 52%, #000 30%, transparent 85%);
  mask-image: radial-gradient(70% 80% at 72% 52%, #000 30%, transparent 85%);
}

.inner { display: grid; box-sizing: border-box; width: 100%; max-width: 1320px; grid-template-columns: minmax(0, .92fr) minmax(0, 1fr); align-items: center; gap: var(--space-stage); margin: 0 auto; padding: clamp(var(--space-screen), 12svh, calc(var(--space-screen) + var(--space-page))) var(--space-page) var(--space-page); }

.copy { display: grid; justify-items: start; gap: var(--space-comfort); text-align: left; }
.title { max-width: 12ch; font-size: clamp(var(--text-3xl), var(--text-lg) + 4.6cqi, var(--text-5xl)); }
.description { max-width: 500px; }
.actions { display: flex; flex-wrap: wrap; gap: var(--space-group); margin-top: var(--space-group); }
.fine { margin: 0; color: var(--text-muted); font-size: var(--text-sm); }

/* The graph column: a small toolbar and the scaled graph under it. */
.canvas { display: grid; justify-items: center; gap: var(--space-inset); min-width: 0; }
.toolbar { display: flex; width: min(100%, 640px); align-items: center; justify-content: space-between; gap: var(--space-group); }
.file { display: inline-flex; min-width: 0; align-items: center; gap: var(--space-related); color: var(--text-secondary); font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, monospace); font-size: var(--text-sm); }
.live { width: 7px; height: 7px; flex: none; border-radius: 50%; corner-shape: round; background: var(--text-muted); transition: background-color var(--duration-standard) var(--ease-standard); }
.live[data-on] { background: var(--success); }

.stage { position: relative; width: min(100%, 640px); }
.graph { position: absolute; top: 0; left: 50%; translate: -50% 0; transform-origin: 50% 0; }
.edges { position: absolute; inset: 0; overflow: visible; }
.wire { fill: none; stroke: var(--wire); stroke-width: 1.25; }
.wire[data-skipped] { stroke-dasharray: 3 4; opacity: .7; }
.flow { fill: none; stroke: var(--accent); stroke-width: 1.75; stroke-linecap: round; }

/* A node: what the step is, what it found, and its state. The running step takes the accent. */
.node {
  position: absolute; display: flex; align-items: center; gap: var(--space-group);
  border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: 0 var(--space-inset);
  background: var(--surface); box-shadow: var(--node-shadow);
  font-size: var(--text-sm); line-height: 1.3;
  transition: border-color var(--duration-standard) var(--ease-standard), opacity var(--duration-standard) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard);
}
.node[data-status="running"] { border-color: color-mix(in oklab, var(--accent) 70%, var(--border)); background: color-mix(in oklab, var(--accent) 4%, var(--surface)); }
.node[data-status="skipped"] { opacity: .5; box-shadow: none; }
.icon { display: grid; width: 20px; height: 20px; flex: none; place-items: center; }
.logo { width: 18px; height: 18px; background: center / contain no-repeat; }
.glyph { color: var(--text-secondary); }
.text { display: grid; flex: 1; min-width: 0; }
.text strong { overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
.text small { overflow: hidden; color: var(--text-muted); font-size: var(--text-xs); text-overflow: ellipsis; white-space: nowrap; }
.state { display: grid; flex: none; justify-items: end; }
.stateInner { display: inline-flex; align-items: center; gap: var(--space-related); }
.meta { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
.check { color: var(--success); }
.skip { color: var(--text-muted); }
.pending { display: block; width: 10px; height: 10px; border: 1.5px solid var(--wire); border-radius: 50%; corner-shape: round; }
.spinner { display: block; width: 13px; height: 13px; flex: none; border: 1.5px solid color-mix(in oklab, var(--accent) 22%, transparent); border-top-color: var(--accent); border-radius: 50%; corner-shape: round; animation: spin .7s linear infinite; }
@keyframes spin { to { rotate: 360deg; } }

/* Compact nodes on a phone: icon and state on top, the name and its result under them. */
.node[data-compact] { display: grid; grid-template-columns: 1fr auto; grid-template-rows: auto auto; align-content: center; gap: var(--space-related) var(--space-tight); padding: 0 var(--space-inset); }
@container hero (max-width: 560px) { .node[data-compact] { gap: var(--space-related) var(--space-tight); padding: 0 var(--space-group); } }
.node[data-compact] .text { grid-column: 1 / -1; }
.node[data-compact] .state { grid-column: 2; grid-row: 1; }

/* The run summary under the graph. */
.summary { position: absolute; left: 0; display: flex; height: 44px; align-items: center; justify-content: space-between; gap: var(--space-group); border-top: 1px solid var(--border-subtle); padding: 0 var(--space-hair); color: var(--text-muted); font-size: var(--text-sm); }
.event { display: inline-flex; align-items: center; gap: var(--space-group); }
.event code { border: 1px solid var(--border-subtle); border-radius: var(--radius-detail); corner-shape: var(--corner-shape); padding: 1px var(--space-related); background: var(--surface); color: var(--text-secondary); font-family: var(--font-mono, ui-monospace, "SF Mono", Menlo, monospace); font-size: var(--text-xs); }
.num { font-variant-numeric: tabular-nums; }
.result { display: inline-flex; align-items: center; gap: var(--space-related); }
.result[data-done] { color: var(--foreground); }
.result[data-done] svg { color: var(--success); }

@container hero (max-width: 900px) {
  .inner { grid-template-columns: minmax(0, 1fr); gap: var(--space-page); }
  .hero::before { -webkit-mask-image: radial-gradient(90% 60% at 50% 78%, #000 30%, transparent 85%); mask-image: radial-gradient(90% 60% at 50% 78%, #000 30%, transparent 85%); }
}
@container hero (max-width: 560px) {
  .inner { gap: var(--space-block); padding: var(--space-screen) var(--space-inset) var(--space-section); }
  .copy { gap: var(--space-inset); }
  .actions { width: 100%; margin-top: var(--space-tight); }
  .actions > * { flex: 1 1 auto; }
  .canvas { gap: var(--space-group); }
}

@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
  .node, .live { transition: none; }
}
```

### `registry/blocks/hero-section/hero-lumen.tsx`

```tsx
"use client";

import { useLayoutEffect, useRef, useState } from "react";
import { MotionConfig, motion, useReducedMotion } from "motion/react";
import { BarChart3, BookOpen, ChevronsUpDown, Gauge, LineChart as LineIcon, Search, Settings, Sparkles, Users } from "lucide-react";
import { LineChart } from "../../line-chart/line-chart";
import { Sparkline } from "../../sparkline/sparkline";
import SegmentedControl from "../../segmented-control/segmented-control";
import { Avatar } from "../../avatar/avatar";
import { avatar } from "../../lib/media";
import { motionTokens } from "../../lib/motion-tokens";
import { ActionLink } from "./hero-content";
import type { HeroAction } from "./hero-content";
import { HeroMesh } from "./hero-mesh";
import type { HeroMeshPoint } from "./hero-mesh";
import { heroGroup, heroRise } from "./hero-motion";
import { lumenInsight, lumenKpis, lumenMovers, lumenRanges, lumenSeries } from "./hero-lumen-data";
import type { LumenRange } from "./hero-lumen-data";
import shell from "./hero-section.module.css";
import styles from "./hero-lumen.module.css";

/** A soft mesh across the whole screen: cool tints at the top, a warm one low behind the window, all a few steps off the page. */
const MESH: HeroMeshPoint[] = [
  { x: .1, y: .92, spread: .7 },
  { x: .92, y: .86, spread: .66 },
  { x: .14, y: .08, spread: .64 },
  { x: .86, y: .04, spread: .6 },
  { x: .5, y: .38, spread: .46 },
];

const NAV = [
  { label: "Overview", icon: Gauge, active: true },
  { label: "Revenue", icon: LineIcon },
  { label: "Customers", icon: Users },
  { label: "Cohorts", icon: BarChart3 },
  { label: "Forecasts", icon: Sparkles },
  { label: "Reports", icon: BookOpen },
];
const VIEWS = [{ label: "Enterprise expansion", tone: "accent" }, { label: "Churn risk", tone: "danger" }, { label: "EU customers", tone: "neutral" }];

const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });
const compact = (value: number) => value >= 1000 ? `$${+(value / 1000).toFixed(1)}K` : `$${value}`;

/** Width the dashboard is designed at, wide and narrow. It is drawn at that size and scaled to fit, like a screenshot. */
const WIDE = 1200, NARROW = 600;

export interface HeroLumenProps {
  primaryAction?: HeroAction;
  secondaryAction?: HeroAction;
  animateIn?: boolean;
  className?: string;
}

/**
 * Centered hero, one full screen: a headline over a drifting mesh, and a live revenue dashboard rising from the bottom edge. The
 * window settles from a tilt as it enters and runs off the bottom of the screen into a fade. It is real markup built from Silicon UI
 * charts; its range control works.
 */
export function HeroLumen({ primaryAction = { label: "Start free trial", doneLabel: "Trial started" }, secondaryAction = { label: "Book a demo", doneLabel: "Demo requested" }, animateIn = true, className }: HeroLumenProps) {
  const calm = !!useReducedMotion();
  const item = heroRise;
  const stage = useRef<HTMLDivElement>(null);
  const [box, setBox] = useState({ width: WIDE, scale: 1 });

  useLayoutEffect(() => {
    const node = stage.current;
    if (!node) return;
    const measure = () => {
      const css = getComputedStyle(node);
      const width = node.clientWidth - parseFloat(css.paddingLeft) - parseFloat(css.paddingRight);
      const design = width < 720 ? NARROW : WIDE;
      // On a phone the window keeps a legible size and crops evenly at both edges instead of shrinking to fit.
      setBox({ width: design, scale: design === NARROW ? Math.max(.6, Math.min(1, width / design)) : Math.min(1, width / design) });
    };
    measure();
    const observer = new ResizeObserver(measure);
    observer.observe(node);
    return () => observer.disconnect();
  }, []);

  const narrow = box.width === NARROW;
  const height = narrow ? 700 : 760;

  // Reduced motion: Motion skips every transform (rise, zoom, tilt entrance); opacity still fades briefly.
  return <MotionConfig reducedMotion="user"><section className={[shell.hero, shell.screen, styles.hero, className].filter(Boolean).join(" ")}>
    <HeroMesh className={styles.mesh} points={MESH} grain={.18} speed={.55} />
    <motion.div className={styles.copy} variants={heroGroup} initial={animateIn ? "hidden" : false} animate="shown">
      <motion.h1 variants={item} className={[shell.title, styles.title].join(" ")}>See every dollar of revenue move</motion.h1>
      <motion.p variants={item} className={[shell.description, styles.description].join(" ")}>Lumen connects Stripe, HubSpot and your warehouse, then explains each change in MRR the moment it happens. No SQL, no stale spreadsheets.</motion.p>
      <motion.div variants={item} className={styles.actions}>
        <ActionLink action={primaryAction} kind="primary" />
        <ActionLink action={secondaryAction} kind="secondary" />
      </motion.div>
      <motion.p variants={item} className={styles.fine}>Free for 14 days. Connects in about four minutes.</motion.p>
    </motion.div>

    <motion.div
      ref={stage}
      className={styles.stage}
      initial={animateIn ? { opacity: 0, y: 56 } : false}
      animate={{ opacity: 1, y: 0 }}
      transition={calm ? { duration: motionTokens.duration.standard } : { duration: 1.1, ease: [...motionTokens.ease.enter], delay: .32 }}
    >
      <div className={styles.glow} aria-hidden="true" />
      <div className={styles.perspective}>
        <motion.div
          className={styles.tilt}
          style={{ width: box.width * box.scale, height: height * box.scale }}
          initial={animateIn ? { rotateX: 22, scale: .94 } : false}
          animate={{ rotateX: 0, scale: 1 }}
          transition={{ duration: 1.5, ease: [...motionTokens.ease.enter], delay: .36 }}
        >
          {/* The scale is a Motion value rather than a CSS transform, so layout animations inside (the date range highlight)
              are projected through it and slide exactly between options at any screen width. */}
          <motion.div className={styles.scaler} style={{ width: box.width, height, scale: box.scale, originX: 0, originY: 0 }}>
            <LumenDashboard narrow={narrow} />
          </motion.div>
        </motion.div>
      </div>
    </motion.div>
  </section></MotionConfig>;
}

/** The Lumen app window. Everything is content except the range control, which changes the data the whole screen shows. */
function LumenDashboard({ narrow }: { narrow: boolean }) {
  const [range, setRange] = useState<LumenRange>("30d");
  const insight = lumenInsight[range];
  return <div className={styles.window} data-narrow={narrow || undefined} role="group" aria-label="Lumen revenue dashboard, sample data">
    <div className={styles.chrome} aria-hidden="true">
      <span className={styles.lights}><i /><i /><i /></span>
      <span className={styles.url}>app.lumen.so/acme/overview</span>
    </div>
    <div className={styles.app}>
      {!narrow && <aside className={styles.sidebar}>
        <div className={styles.workspace}>
          <span className={styles.wsMark}>A</span>
          <span className={styles.wsName}>Acme Cloud</span>
          <ChevronsUpDown size={14} strokeWidth={1.75} className={styles.dim} />
        </div>
        <div className={styles.search}><Search size={14} strokeWidth={1.75} />Search<kbd>⌘K</kbd></div>
        <div className={styles.nav}>
          {NAV.map(({ label, icon: Icon, active }) => <div key={label} className={styles.navItem} data-active={active || undefined}><Icon size={15} strokeWidth={1.75} />{label}</div>)}
        </div>
        <div className={styles.groupLabel}>Saved views</div>
        <div className={styles.nav}>
          {VIEWS.map(view => <div key={view.label} className={styles.navItem}><i className={styles.dot} data-tone={view.tone} />{view.label}</div>)}
        </div>
        <div className={styles.me}>
          <Avatar name="Chloe Nguyen" src={avatar("chloe-nguyen")} size="sm" />
          <span><strong>Chloe Nguyen</strong><small>Data analyst</small></span>
          <Settings size={15} strokeWidth={1.75} className={styles.dim} />
        </div>
      </aside>}
      <div className={styles.main}>
        <header className={styles.head}>
          <div>
            <h2 className={styles.h2}>Revenue overview</h2>
            <p className={styles.sub}>Synced with Stripe 2 minutes ago</p>
          </div>
          <SegmentedControl label="Date range" options={lumenRanges} value={range} onValueChange={value => setRange(value as LumenRange)} className={styles.range} />
        </header>

        <div className={styles.kpis}>
          {lumenKpis.map(kpi => {
            const now = kpi.byRange[range];
            return <div key={kpi.id} className={styles.kpi}>
              <Sparkline label={kpi.label} value={now.value} change={now.change} data={now.data} tone={kpi.tone} width={220} height={34} interactive={false} />
            </div>;
          })}
        </div>

        <div className={styles.grid}>
          <div className={styles.card}>
            <div className={styles.cardHead}>
              <p className={styles.insight}><Sparkles size={15} strokeWidth={1.75} className={styles.insightIcon} /><span><strong>{insight.lead}</strong> {insight.rest}</span></p>
              <div className={styles.legend} aria-hidden="true"><span><i className={styles.swatch} />This period</span><span><i className={styles.swatch} data-last="" />Last period</span></div>
            </div>
            <LineChart
              label="Net new MRR"
              data={lumenSeries[range]}
              series={[{ key: "mrr", label: "This period", area: true }, { key: "last", label: "Last period", dashed: true }]}
              height={narrow ? 210 : 232}
              legend={false}
              formatValue={value => money.format(value)}
              formatTick={compact}
            />
          </div>
          {!narrow && <div className={styles.card}>
            <div className={styles.cardTitle}>Biggest movers<span>MRR change</span></div>
            <ul className={styles.movers}>
              {lumenMovers.map(mover => <li key={mover.name}>
                <span className={styles.logo} data-mono={mover.mono || undefined} style={mover.mono ? { maskImage: `url(${mover.logo})`, WebkitMaskImage: `url(${mover.logo})` } : { backgroundImage: `url(${mover.logo})` }} />
                <span className={styles.who}><strong>{mover.name}</strong><small>{mover.change}</small></span>
                <span className={styles.amount} data-down={mover.amount < 0 || undefined}>{mover.amount < 0 ? "−" : "+"}{money.format(Math.abs(mover.amount))}</span>
              </li>)}
            </ul>
          </div>}
        </div>
      </div>
    </div>
  </div>;
}

export default HeroLumen;
```

### `registry/blocks/hero-section/hero-lumen.module.css`

```css
/* Centered, one full screen: copy over a drifting mesh, and a product window that rises from the bottom edge and runs off it.
   The hero is exactly one screen tall (never shorter than 600px); the window takes whatever height the copy leaves. */
/* The section is the container, so its children carry the side gutters and the queries below can change them. */
.hero { isolation: isolate; display: flex; height: 100vh; height: 100svh; min-height: 600px; flex-direction: column; padding-top: clamp(var(--space-screen), 14svh, calc(var(--space-screen) + var(--space-stage))); text-align: center; }

/* The mesh fills the screen. It is authored per theme: tints a few steps off the page, so it reads as light rather than color. */
.mesh {
  --mesh-base: var(--background);
  --mesh-1: #ffe6d8; --mesh-2: #e3dcff; --mesh-3: #dbe4ff; --mesh-4: #f0e0ff; --mesh-5: #e0eefc;
  z-index: -1;
}
:global(:root[data-theme="dark"]) .mesh { --mesh-1: #2a1822; --mesh-2: #1c1a44; --mesh-3: #141c40; --mesh-4: #231842; --mesh-5: #0e2133; }

.copy { position: relative; display: grid; justify-items: center; gap: var(--space-comfort); max-width: 900px; margin: 0 auto; padding-inline: var(--space-page); }
.title { max-width: 14ch; font-size: clamp(var(--text-3xl), var(--text-lg) + 5cqi, var(--text-5xl)); }
.description { max-width: 590px; }
.actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-group); margin-top: var(--space-group); }
.fine { margin: 0; color: var(--text-muted); font-size: var(--text-sm); }

/* Stage: fills the rest of the screen and is the visible crop. The window runs past its bottom edge; the mask fades it into the
   mesh, so there is no hard edge. The halo above leaves room for the light behind the window. */
.stage { --halo: 96px; position: relative; flex: 1 1 0; box-sizing: border-box; width: 100%; min-height: 0; max-width: calc(1200px + 2 * var(--space-page)); margin: calc(var(--space-page) - var(--halo)) auto 0; overflow: hidden; padding: var(--halo) var(--space-page) 0; -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%); mask-image: linear-gradient(to bottom, #000 62%, transparent 100%); }
.glow {
  position: absolute; z-index: -1; top: calc(var(--halo) - calc(var(--space-screen) + var(--space-block))); left: 10%; right: 10%; height: 240px;
  background: radial-gradient(50% 50% at 50% 50%, var(--hero-glow), transparent 72%);
  filter: blur(36px);
  --hero-glow: color-mix(in oklab, color-mix(in oklab, var(--accent) 35%, #7d8cff) 36%, transparent);
}
:global(:root[data-theme="dark"]) .glow { --hero-glow: color-mix(in oklab, color-mix(in oklab, var(--accent) 30%, #6e7cff) 58%, transparent); }
.window { position: relative; }
/* A thin light catches the top edge, the way a real display does in a dark room. */
:global(:root[data-theme="dark"]) .window::after { content: ""; position: absolute; top: 0; left: 12%; right: 12%; height: 1px; background: linear-gradient(to right, transparent, color-mix(in oklab, var(--glow) 70%, transparent), transparent); pointer-events: none; }
.perspective { display: flex; justify-content: center; perspective: 2200px; perspective-origin: 50% 0%; }
.tilt { position: relative; flex: none; transform-origin: 50% 0%; will-change: transform; }
.scaler { position: absolute; top: 0; left: 0; transform-origin: 0 0; }
/* Reduced motion: the window is already in place; drop the GPU layer kept for the entrance. */
@media (prefers-reduced-motion: reduce) { .tilt { will-change: auto; } }

/* The window. */
.window {
  display: grid; grid-template-rows: auto minmax(0, 1fr); height: 100%; overflow: hidden;
  border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); color: var(--foreground);
  text-align: left; font-size: var(--text-sm);
  box-shadow: var(--shadow-showcase-lg);
}
:global(:root[data-theme="dark"]) .window {
  border-color: color-mix(in oklab, var(--foreground) 14%, transparent);
  box-shadow: var(--shadow-showcase-lg);
}
.chrome { position: relative; display: flex; height: 40px; align-items: center; justify-content: center; border-bottom: 1px solid var(--border-subtle); background: color-mix(in oklab, var(--surface-muted) 55%, var(--surface)); }
.lights { position: absolute; left: var(--space-inset); display: flex; gap: var(--space-related); }
.lights i { width: 11px; height: 11px; border-radius: 50%; corner-shape: round; background: color-mix(in oklab, var(--foreground) 14%, transparent); }
.url { border-radius: var(--radius-detail); corner-shape: var(--corner-shape); padding: var(--space-tight) var(--space-inset); background: var(--surface); color: var(--text-muted); font-size: var(--text-xs); box-shadow: inset 0 0 0 1px var(--border-subtle); }

.app { display: grid; grid-template-columns: 224px minmax(0, 1fr); min-height: 0; }
.window[data-narrow] .app { grid-template-columns: minmax(0, 1fr); }

.sidebar { display: flex; flex-direction: column; gap: var(--space-hair); border-right: 1px solid var(--border-subtle); padding: var(--space-inset) var(--space-group); background: color-mix(in oklab, var(--surface-muted) 40%, var(--surface)); }
.workspace { display: flex; align-items: center; gap: var(--space-group); padding: var(--space-related) var(--space-related) var(--space-group); }
.wsMark { display: grid; width: 22px; height: 22px; place-items: center; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--foreground); color: var(--background); font-size: var(--text-xs); font-weight: 500; }
.wsName { flex: 1; font-weight: 500; }
.dim { color: var(--text-muted); }
.search { display: flex; align-items: center; gap: var(--space-related); margin: 0 0 var(--space-group); border: 1px solid var(--border-subtle); border-radius: var(--radius-detail); corner-shape: var(--corner-shape); padding: var(--space-related) var(--space-related); background: var(--surface); color: var(--text-muted); }
.search kbd { margin-left: auto; font: inherit; font-size: var(--text-xs); }
.nav { display: grid; gap: 1px; }
.navItem { display: flex; align-items: center; gap: var(--space-group); border-radius: var(--radius-detail); corner-shape: var(--corner-shape); padding: var(--space-related) var(--space-related); color: var(--text-secondary); }
.navItem svg { color: var(--text-muted); }
.navItem[data-active] { background: var(--accent-subtle); color: var(--foreground); font-weight: 500; }
.navItem[data-active] svg { color: var(--accent); }
.groupLabel { margin: var(--space-comfort) var(--space-related) var(--space-related); color: var(--text-muted); font-size: var(--text-xs); }
.dot { width: 8px; height: 8px; margin: 0 var(--space-tight); border-radius: 50%; corner-shape: round; background: var(--text-muted); }
.dot[data-tone="accent"] { background: var(--accent); }
.dot[data-tone="danger"] { background: var(--danger); }
.me { display: flex; align-items: center; gap: var(--space-group); margin-top: auto; border-top: 1px solid var(--border-subtle); padding: var(--space-group) var(--space-related) var(--space-hair); }
.me span:not([role]) { display: grid; flex: 1; line-height: 1.3; }
.me strong { font-weight: 500; }
.me small { color: var(--text-muted); font-size: var(--text-xs); }

.main { display: grid; align-content: start; gap: var(--space-inset); min-width: 0; padding: var(--space-section) var(--space-section); }
.head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-inset); }
.h2 { margin: 0; font-size: var(--text-lg); font-weight: 500; letter-spacing: var(--tracking-body); }
.sub { margin: var(--space-hair) 0 0; color: var(--text-muted); font-size: var(--text-xs); }
.range { flex: none; }

.kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-group); }
.window[data-narrow] .kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.kpi { min-width: 0; border: 1px solid var(--border-subtle); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: var(--space-inset) var(--space-inset) var(--space-group); background: var(--surface); }

.grid { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); gap: var(--space-group); }
.window[data-narrow] .grid { grid-template-columns: minmax(0, 1fr); }
.card { display: grid; align-content: start; gap: var(--space-inset); min-width: 0; border: 1px solid var(--border-subtle); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: var(--space-inset) var(--space-comfort); background: var(--surface); }
.cardHead { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-inset); }
.insight { display: flex; gap: var(--space-related); max-width: 46ch; margin: 0; color: var(--text-secondary); line-height: 1.45; }
.insight strong { color: var(--foreground); font-weight: 500; }
.insightIcon { flex: none; margin-top: var(--space-hair); color: var(--accent); }
.legend { display: flex; flex: none; gap: var(--space-inset); color: var(--text-muted); font-size: var(--text-xs); }
.legend span { display: inline-flex; align-items: center; gap: var(--space-related); }
.swatch { width: 14px; height: 2px; border-radius: var(--radius-full); corner-shape: round; background: var(--accent); }
.swatch[data-last] { background: repeating-linear-gradient(to right, color-mix(in oklab, var(--foreground) 46%, var(--surface)) 0 4px, transparent 4px 7px); }
.window[data-narrow] .legend { display: none; }

.cardTitle { display: flex; justify-content: space-between; font-weight: 500; }
.cardTitle span { color: var(--text-muted); font-size: var(--text-xs); font-weight: 400; }
.movers { display: grid; margin: 0; padding: 0; list-style: none; }
.movers li { display: flex; align-items: center; gap: var(--space-group); border-top: 1px solid var(--border-subtle); padding: var(--space-group) 0; }
.movers li:first-child { border-top: 0; padding-top: var(--space-hair); }
.logo { flex: none; width: 20px; height: 20px; background: center / contain no-repeat; }
.logo[data-mono] { background: var(--foreground); -webkit-mask: center / contain no-repeat; mask: center / contain no-repeat; }
.who { display: grid; flex: 1; min-width: 0; line-height: 1.3; }
.who strong { font-weight: 500; }
.who small { overflow: hidden; color: var(--text-muted); font-size: var(--text-xs); text-overflow: ellipsis; white-space: nowrap; }
.amount { color: var(--success); font-variant-numeric: tabular-nums; font-weight: 500; }
.amount[data-down] { color: var(--danger); }

@container hero (max-width: 560px) {
  .hero { padding-top: var(--space-screen); }
  .copy { gap: var(--space-inset); padding-inline: var(--space-inset); }
  .actions { width: 100%; margin-top: var(--space-related); }
  .actions > * { flex: 1 1 auto; }
  .stage { --halo: 56px; margin-top: calc(var(--space-page) - var(--halo)); padding-inline: 0; }
}

/* KPI tiles reuse Silicon UI Sparkline; its caption wraps so the label sits above a larger value. */
.kpi :global(figcaption) { flex-wrap: wrap; row-gap: var(--space-tight); }
.kpi :global(figcaption) > :first-child { flex-basis: 100%; font-size: var(--text-xs); color: var(--text-muted); }
.kpi :global(figcaption) strong { margin-left: 0; font-size: var(--text-2xl); letter-spacing: var(--tracking-body); }
.kpi :global(figcaption) small { align-self: center; }
```

### `registry/blocks/hero-section/hero-lumen-data.ts`

```ts
import type { LineChartDatum } from "../../line-chart/line-chart";

/** Sample data for Lumen, a revenue analytics product. Replace it with your own product's numbers. */

export type LumenRange = "30d" | "90d" | "12m";

export const lumenRanges: { value: LumenRange; label: string }[] = [
  { value: "30d", label: "30D" },
  { value: "90d", label: "90D" },
  { value: "12m", label: "12M" },
];

/** A smooth, deterministic walk: a trend, one slow wave, and a little noise that repeats for the same seed. */
function walk(count: number, start: number, end: number, wave: number, seed: number) {
  let s = seed;
  const rand = () => { s = (s * 16807) % 2147483647; return s / 2147483647 - .5; };
  return Array.from({ length: count }, (_, i) => {
    const t = i / (count - 1);
    return Math.round(start + (end - start) * (t * t * .35 + t * .65) + Math.sin(t * Math.PI * 2.3) * wave + rand() * wave * .6);
  });
}

const months = ["Oct", "Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep"];

function series(range: LumenRange): LineChartDatum[] {
  if (range === "12m") {
    const now = walk(12, 9_800, 36_400, 2_600, 7), before = walk(12, 6_200, 21_900, 2_200, 19);
    return months.map((month, i) => ({ key: `m${i}`, label: `${month} ${i < 3 ? 2025 : 2026}`, axisLabel: i % 2 === 0 ? month : undefined, values: { mrr: now[i], last: before[i] } }));
  }
  const days = range === "30d" ? 30 : 13;
  const step = range === "30d" ? 1 : 7;
  const now = walk(days, range === "30d" ? 640 : 4_300, range === "30d" ? 1_960 : 9_800, range === "30d" ? 260 : 900, range === "30d" ? 3 : 11);
  const before = walk(days, range === "30d" ? 520 : 3_600, range === "30d" ? 1_180 : 6_100, range === "30d" ? 220 : 700, 29);
  const end = new Date(Date.UTC(2026, 8, 24));
  return now.map((value, i) => {
    const date = new Date(end.getTime() - (days - 1 - i) * step * 86_400_000);
    const label = date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
    const every = range === "30d" ? 7 : 3;
    return { key: date.toISOString().slice(0, 10), label, axisLabel: (days - 1 - i) % every === 0 ? label : undefined, values: { mrr: value, last: before[i] } };
  });
}

export const lumenSeries: Record<LumenRange, LineChartDatum[]> = { "30d": series("30d"), "90d": series("90d"), "12m": series("12m") };

export interface LumenKpi {
  id: string;
  label: string;
  tone: "accent" | "success" | "warning" | "danger";
  byRange: Record<LumenRange, { value: string; change: string; data: number[] }>;
}

export const lumenKpis: LumenKpi[] = [
  { id: "mrr", label: "MRR", tone: "accent", byRange: {
    "30d": { value: "$482.9K", change: "+8.2%", data: walk(12, 446, 483, 4, 5) },
    "90d": { value: "$482.9K", change: "+19.9%", data: walk(12, 402, 483, 6, 9) },
    "12m": { value: "$482.9K", change: "+51.7%", data: walk(12, 318, 483, 8, 13) },
  } },
  { id: "nrr", label: "Net retention", tone: "success", byRange: {
    "30d": { value: "118%", change: "+3 pts", data: walk(12, 112, 118, 1.5, 17) },
    "90d": { value: "116%", change: "+5 pts", data: walk(12, 109, 116, 1.8, 21) },
    "12m": { value: "114%", change: "+9 pts", data: walk(12, 103, 114, 2.4, 23) },
  } },
  { id: "new", label: "New customers", tone: "success", byRange: {
    "30d": { value: "214", change: "+12%", data: walk(12, 5, 9, 2, 31) },
    "90d": { value: "602", change: "+18%", data: walk(12, 38, 56, 6, 37) },
    "12m": { value: "2,140", change: "+34%", data: walk(12, 120, 214, 16, 41) },
  } },
  { id: "churn", label: "Churned MRR", tone: "success", byRange: {
    "30d": { value: "$6.1K", change: "−14%", data: walk(12, 9, 6, 1.2, 43) },
    "90d": { value: "$21.4K", change: "−9%", data: walk(12, 26, 21, 2, 47) },
    "12m": { value: "$96.8K", change: "−22%", data: walk(12, 12, 7, 1.4, 53) },
  } },
];

export interface LumenMover {
  name: string;
  logo: string;
  /** Mono marks draw in the text color through a mask; color marks keep their own colors. */
  mono?: boolean;
  change: string;
  amount: number;
}

export const lumenMovers: LumenMover[] = [
  { name: "Linear", logo: "/block-logos/linear-color.svg", change: "Moved to Enterprise", amount: 4_200 },
  { name: "Raycast", logo: "/block-logos/raycast-color.svg", change: "Added 120 seats", amount: 2_850 },
  { name: "Vercel", logo: "/block-logos/vercel.svg", mono: true, change: "Annual prepay", amount: 1_900 },
  { name: "Loom", logo: "/block-logos/loom-color.svg", change: "Removed 40 seats", amount: -1_240 },
  { name: "Supabase", logo: "/block-logos/supabase-color.svg", change: "Added forecasting", amount: 980 },
  { name: "Framer", logo: "/block-logos/framer.svg", mono: true, change: "Upgraded to Scale", amount: 760 },
];

export const lumenInsight: Record<LumenRange, { lead: string; rest: string }> = {
  "30d": { lead: "MRR grew $36.4K this month.", rest: "62% came from 14 expansions on the Scale plan, led by Linear." },
  "90d": { lead: "MRR grew $80.6K this quarter.", rest: "Expansion outpaced new business for the first time since March." },
  "12m": { lead: "MRR grew $164.5K in twelve months.", rest: "Net retention above 110% did more than new logos did." },
};
```

### `registry/components/line-chart/line-chart.tsx`

```tsx
"use client";

import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type CSSProperties, type KeyboardEvent, type PointerEvent, type Ref } from "react";
import { AnimatePresence, animate, motion, useInView, useMotionValue, useMotionValueEvent, useReducedMotion, useSpring, useTransform, type MotionValue } from "motion/react";
import { motionTokens } from "../lib/motion-tokens";
import styles from "./line-chart.module.css";

export interface LineChartSeries {
  /** Stable identity. A series keeps its line across data changes, so switching ranges morphs the path instead of redrawing it. */
  key: string;
  /** Name in the legend, the tooltip, and the data table. */
  label: string;
  /** Any CSS color. Defaults to the accent for the first series, then quieter neutral steps. */
  color?: string;
  /** Draws a dashed stroke, useful for a previous period or a target. */
  dashed?: boolean;
  /** Fills the area under the line with a faint tint of its color. Defaults to true for the first series. */
  area?: boolean;
}

export interface LineChartDatum {
  /** Stable identity, such as an ISO date. */
  key: string;
  /** Full label for the tooltip and screen readers, such as "Tue, Sep 15". */
  label: string;
  /** Short label on the x axis, such as "Sep 15". Leave it out to keep the axis quiet at that point. */
  axisLabel?: string;
  /** One value per series key. A missing value counts as zero. */
  values: Record<string, number | undefined>;
}

/** Use a line chart for one or more measures over time, when the shape of the trend matters more than any single value. */
export interface LineChartProps {
  data: LineChartDatum[];
  series: LineChartSeries[];
  /** What is measured, such as "Signups". Names the chart for assistive technology. */
  label: string;
  /** Unit after each value in the tooltip, such as "ms". */
  unit?: string;
  /** Plot height in pixels. The width follows the container. */
  height?: number;
  /** Formats values in the tooltip and data table. */
  formatValue?: (value: number, series: LineChartSeries) => string;
  /** Formats the value axis. Defaults to a compact number, such as 1.2K. */
  formatTick?: (value: number) => string;
  /** Controlled hidden series keys. */
  hiddenSeries?: string[];
  defaultHiddenSeries?: string[];
  onHiddenSeriesChange?: (hidden: string[]) => void;
  /** Called as the crosshair moves, and with null when it leaves. Use it to drive a headline readout. */
  onActiveChange?: (index: number | null, datum: LineChartDatum | null) => void;
  /** Keeps the current lines on screen, dimmed, while the next range loads. */
  loading?: boolean;
  /** Shown when there are no points. */
  emptyLabel?: string;
  /** Series toggles above the plot. Shown by default when there is more than one series. */
  legend?: boolean;
  /** Header of the first column in the data table read by screen readers. */
  categoryLabel?: string;
  /** Smooth monotone curves that never swing past the data, or straight segments. */
  curve?: "smooth" | "linear";
  ref?: Ref<HTMLElement>;
  className?: string;
}

type Point = [x: number, value: number];
type Track = { shape: Point[]; presence: number; fade: { stop: () => void } | null };

const { spring, duration, ease } = motionTokens;
/** Motion drops inherited velocity on time-defined springs, so values that retarget mid-flight run the same springs written as stiffness and damping. */
const physical = ({ visualDuration, bounce }: { visualDuration: number; bounce: number }, restDelta = .001) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring" as const, stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };
const settle = physical(spring.smooth);
const glide = physical(spring.snappy, .0001);
const follow = { stiffness: glide.stiffness, damping: glide.damping, restDelta: .01 };
const draw = { duration: duration.considered * 1.75, ease: [...ease.inOut] } as const;
const fadeFast = { duration: duration.instant, ease: [...ease.standard] } as const;

/** Room above the top gridline, and the gap between the crosshair and its tooltip. */
const TOP = 12, GAP = 14;
const PALETTE = ["var(--accent)", "color-mix(in oklab, var(--foreground) 46%, var(--surface))", "color-mix(in oklab, var(--foreground) 26%, var(--surface))"];
const grouped = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
const clamp = (value: number, low: number, high: number) => Math.min(high, Math.max(low, value));
const colorOf = (series: LineChartSeries, index: number) => series.color ?? PALETTE[Math.min(index, PALETTE.length - 1)];
const clean = (value: number) => Number(value.toPrecision(12));

/** Clean gridlines: the smallest step of 1, 2, 2.5, or 5 that covers the data in four rows or fewer, from zero unless the data dips below it. */
function niceScale(low: number, high: number) {
  const lo = Math.min(0, low), hi = Math.max(high, lo + 1);
  const span = hi - lo, magnitude = 10 ** Math.floor(Math.log10(span / 4));
  for (const factor of [1, 2, 2.5, 5, 10]) {
    const step = factor * magnitude, bottom = Math.floor(lo / step) * step, top = Math.ceil(hi / step) * step;
    if ((top - bottom) / step <= 4 + 1e-9) {
      const ticks: number[] = [];
      for (let value = bottom; value <= top + step / 2; value += step) ticks.push(clean(value));
      return { min: clean(bottom), max: clean(top), ticks };
    }
  }
  return { min: lo, max: hi, ticks: [lo, hi] };
}

/** The line through every value, sampled densely on x from 0 to 1 in data units. Monotone cubic tangents keep a smooth curve from swinging past the data. */
function curveFor(values: number[], smooth: boolean): Point[] {
  if (values.length === 0) return [];
  if (values.length === 1) return [[0, values[0]!], [1, values[0]!]];
  const last = values.length - 1, step = 1 / last;
  if (!smooth) return values.map((value, index) => [index * step, value]);
  const slopes = values.slice(1).map((value, index) => (value - values[index]!) / step);
  const tangents = values.map((_, index) => index === 0 || index === last ? 0 : (Math.sign(slopes[index - 1]!) + Math.sign(slopes[index]!)) * Math.min(Math.abs(slopes[index - 1]!), Math.abs(slopes[index]!), Math.abs(slopes[index - 1]! + slopes[index]!) / 4) || 0);
  if (last > 1) { tangents[0] = (3 * slopes[0]! - tangents[1]!) / 2; tangents[last] = (3 * slopes[last - 1]! - tangents[last - 1]!) / 2; }
  else { tangents[0] = slopes[0]!; tangents[1] = slopes[0]!; }
  const samples = Math.max(1, Math.min(16, Math.round(192 / last)));
  const points: Point[] = [];
  for (let index = 0; index < last; index++) {
    const low = Math.min(values[index]!, values[index + 1]!), high = Math.max(values[index]!, values[index + 1]!);
    for (let sample = 0; sample < samples; sample++) {
      const t = sample / samples, t2 = t * t, t3 = t2 * t;
      const value = (2 * t3 - 3 * t2 + 1) * values[index]! + (t3 - 2 * t2 + t) * step * tangents[index]! + (3 * t2 - 2 * t3) * values[index + 1]! + (t3 - t2) * step * tangents[index + 1]!;
      points.push([(index + t) * step, clamp(value, low, high)]);
    }
  }
  points.push([1, values[last]!]);
  return points;
}

/** Value of a sampled line at x, straight between samples. */
function valueAt(shape: Point[], x: number) {
  if (!shape.length) return 0;
  let low = 0, high = shape.length - 1;
  if (x <= shape[low]![0]) return shape[low]![1];
  if (x >= shape[high]![0]) return shape[high]![1];
  while (high - low > 1) { const middle = (low + high) >> 1; if (shape[middle]![0] < x) low = middle; else high = middle; }
  const [x0, y0] = shape[low]!, [x1, y1] = shape[high]!;
  return x1 === x0 ? y1 : y0 + ((y1 - y0) * (x - x0)) / (x1 - x0);
}

/** Tracks are mutable animation state read by the painter, never by render. */
const update = (track: Track, patch: Partial<Track>) => Object.assign(track, patch);

const grid = Array.from({ length: 241 }, (_, index) => index / 240);

const subscribeNothing = () => () => {};
/** Reduced motion is only known in the browser, so the first client render matches the server before it takes effect. */
function useReducedMotionSafe() {
  const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);
  return !!useReducedMotion() && hydrated;
}

type Scale = { min: MotionValue<number>; max: MotionValue<number> };
function useRowY(value: number, scale: Scale, height: number) {
  return useTransform(() => { const span = scale.max.get() - scale.min.get() || 1; return Math.round(TOP + (1 - (value - scale.min.get()) / span) * (height - TOP)) + .5; });
}

/** A gridline rides the scale, so a taller range slides it down while new rows fade in above. */
function Gridline({ value, scale, height }: { value: number; scale: Scale; height: number }) {
  const y = useRowY(value, scale, height);
  return <motion.line className={styles.grid} x1={0} x2="100%" y1={y} y2={y} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0, transition: fadeFast }} transition={{ duration: duration.standard, ease: [...ease.standard] }} />;
}

function TickLabel({ value, scale, height, format }: { value: number; scale: Scale; height: number; format: (value: number) => string }) {
  const y = useRowY(value, scale, height);
  return <motion.span className={styles.tick} style={{ y }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0, transition: fadeFast }} transition={{ duration: duration.standard, ease: [...ease.standard] }}>
    <span className={styles.tickText}>{format(value)}</span>
  </motion.span>;
}

/** Axis labels that fit: at most one per 76 pixels, counted back from the latest so the newest date always shows. */
function axisPicks(data: LineChartDatum[], width: number) {
  const labeled = data.flatMap((item, index) => item.axisLabel ? [index] : []);
  if (!width || !labeled.length) return [];
  const room = Math.max(2, Math.floor(width / 76));
  const stride = Math.ceil(labeled.length / room);
  return labeled.reverse().filter((_, rank) => rank % stride === 0).reverse();
}

export function LineChart({ data, series, label, unit = "", height = 220, formatValue, formatTick = value => compact.format(value), hiddenSeries, defaultHiddenSeries, onHiddenSeriesChange, onActiveChange, loading = false, emptyLabel = "No data for this range", legend, categoryLabel = "Date", curve = "smooth", ref, className }: LineChartProps) {
  const reduced = useReducedMotionSafe();
  const figure = useRef<HTMLElement>(null);
  const plot = useRef<HTMLDivElement>(null);
  const tip = useRef<HTMLDivElement>(null);
  const clip = useRef<SVGRectElement>(null);
  const crosshair = useRef<SVGLineElement>(null);
  const lines = useRef(new Map<string, SVGPathElement>());
  const areas = useRef(new Map<string, SVGPathElement>());
  const dots = useRef(new Map<string, SVGCircleElement>());
  const groups = useRef(new Map<string, SVGGElement>());
  useImperativeHandle(ref, () => figure.current as HTMLElement);
  const inView = useInView(figure, { once: true, amount: .3 });
  const uid = `line${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
  const last = data.length - 1;
  const empty = data.length === 0;

  // Hidden series, controlled or not.
  const [ownHidden, setOwnHidden] = useState<string[]>(defaultHiddenSeries ?? []);
  const hidden = hiddenSeries ?? ownHidden;
  const toggle = (key: string) => {
    const next = hidden.includes(key) ? hidden.filter(item => item !== key) : [...hidden, key];
    if (hiddenSeries === undefined) setOwnHidden(next);
    onHiddenSeriesChange?.(next);
  };
  const visible = series.filter(item => !hidden.includes(item.key));
  const showLegend = legend ?? series.length > 1;

  // Each series becomes a dense curve in data units; the scale fits only the series on show.
  const signature = `${curve}|${series.map(item => item.key).join(",")}|${data.map(item => `${item.key}:${series.map(line => item.values[line.key] ?? 0).join(",")}`).join(";")}`;
  const targets = useMemo(() => new Map(series.map(line => [line.key, curveFor(data.map(item => item.values[line.key] ?? 0), curve === "smooth")])), [signature]); // eslint-disable-line react-hooks/exhaustive-deps
  let low = Infinity, high = -Infinity;
  for (const line of visible) for (const item of data) { const value = item.values[line.key] ?? 0; low = Math.min(low, value); high = Math.max(high, value); }
  const range = Number.isFinite(low) ? niceScale(low, high) : null;
  const [steady, setSteady] = useState(() => range ?? niceScale(0, 100));
  if (range && (range.min !== steady.min || range.max !== steady.max)) setSteady(range);
  const scale = { min: useMotionValue(steady.min), max: useMotionValue(steady.max) };

  // Scrubbing: a new range lets go of the crosshair.
  const [active, setActive] = useState<number | null>(null);
  const [seenSignature, setSeenSignature] = useState(signature);
  if (seenSignature !== signature) { setSeenSignature(signature); setActive(null); }
  const index = active === null || empty ? null : Math.min(active, last);
  const scrubbing = index !== null;

  const width = useRef(0);
  const [plotWidth, setPlotWidth] = useState(0);
  const tipSize = useRef({ w: 0, h: 0 });
  const tracks = useRef(new Map<string, Track>());
  const drawn = useMotionValue(0);
  const cursor = useMotionValue(1);
  const tipTargetX = useMotionValue(0), tipTargetY = useMotionValue(0);
  const tipSpringX = useSpring(tipTargetX, follow), tipSpringY = useSpring(tipTargetY, follow);
  const live = useRef({ series, hidden });
  useLayoutEffect(() => { live.current = { series, hidden }; });

  // One writer for every drawn attribute: morphs, toggles, the scale, the crosshair, and the measured size all repaint through here.
  const paint = useCallback(() => {
    const w = width.current;
    if (!w) return;
    const lo = scale.min.get(), span = scale.max.get() - lo || 1;
    const toY = (value: number) => TOP + (1 - (value - lo) / span) * (height - TOP);
    const at = cursor.get();
    const dotYs: number[] = [];
    for (const line of live.current.series) {
      const track = tracks.current.get(line.key);
      if (!track) continue;
      const p = track.presence;
      const y = (value: number) => toY(lo + (value - lo) * p);
      const path = track.shape.length ? `M${track.shape.map(([x, value]) => `${(x * w).toFixed(1)},${y(value).toFixed(1)}`).join("L")}` : "";
      lines.current.get(line.key)?.setAttribute("d", path);
      areas.current.get(line.key)?.setAttribute("d", path ? `${path}L${w},${height}L0,${height}Z` : "");
      const group = groups.current.get(line.key);
      if (group) group.style.opacity = String(p);
      const dot = dots.current.get(line.key);
      if (dot && track.shape.length) {
        const cy = y(valueAt(track.shape, at));
        dot.setAttribute("cx", (at * w).toFixed(1));
        dot.setAttribute("cy", cy.toFixed(1));
        if (p > .5) dotYs.push(cy);
      }
    }
    clip.current?.setAttribute("width", String(drawn.get() * (w + 16)));
    const cx = Math.round(at * w) + .5;
    crosshair.current?.setAttribute("x1", String(cx));
    crosshair.current?.setAttribute("x2", String(cx));
    // The tooltip sits beside the crosshair, flips sides before it would leave the plot, and centres on the points it describes.
    const { w: tw, h: th } = tipSize.current;
    let x = cx + GAP;
    if (x + tw > w) x = cx - GAP - tw;
    tipTargetX.set(clamp(x, 0, Math.max(0, w - tw)));
    const middle = dotYs.length ? dotYs.reduce((sum, value) => sum + value, 0) / dotYs.length : height / 2;
    tipTargetY.set(clamp(middle - th / 2, 0, Math.max(0, height - th)));
  }, [cursor, drawn, height, scale.max, scale.min, tipTargetX, tipTargetY]);
  useMotionValueEvent(cursor, "change", paint);
  useMotionValueEvent(drawn, "change", paint);
  useMotionValueEvent(scale.min, "change", paint);
  useMotionValueEvent(scale.max, "change", paint);
  useLayoutEffect(paint);

  // The plot draws in its own pixels: measure the width, follow resizes, and size the tooltip for its flip.
  useLayoutEffect(() => {
    const node = plot.current, bubble = tip.current;
    if (!node) return;
    const read = () => { width.current = node.clientWidth; setPlotWidth(node.clientWidth); if (bubble) tipSize.current = { w: bubble.offsetWidth, h: bubble.offsetHeight }; paint(); };
    read();
    if (typeof ResizeObserver === "undefined") return;
    const observer = new ResizeObserver(read);
    observer.observe(node);
    if (bubble) observer.observe(bubble);
    return () => observer.disconnect();
  }, [paint]);

  // New data morphs every line from the shape on screen, sampled on shared x positions so a range that gains or loses points still flows.
  const morph = useRef<{ stop: () => void } | null>(null);
  useEffect(() => {
    const moves: { track: Track; xs: number[]; from: number[]; to: number[]; target: Point[] }[] = [];
    for (const [key, target] of targets) {
      const track = tracks.current.get(key);
      if (!track) { tracks.current.set(key, { shape: target, presence: live.current.hidden.includes(key) ? 0 : 1, fade: null }); continue; }
      if (!track.shape.length || !target.length || reduced) { update(track, { shape: target }); continue; }
      const xs = [...new Set([...grid, ...target.map(([x]) => x)])].sort((a, b) => a - b);
      moves.push({ track, xs, from: xs.map(x => valueAt(track.shape, x)), to: xs.map(x => valueAt(target, x)), target });
    }
    morph.current?.stop();
    if (!moves.length) { paint(); return; }
    morph.current = animate(0, 1, { ...settle, restDelta: .0005, onUpdate: progress => { for (const move of moves) update(move.track, { shape: move.xs.map((x, at): Point => [x, move.from[at]! + (move.to[at]! - move.from[at]!) * progress]) }); paint(); }, onComplete: () => { for (const move of moves) update(move.track, { shape: move.target }); paint(); } });
    return () => morph.current?.stop();
  }, [paint, reduced, targets]);

  // A hidden series flattens into the baseline as it fades; the ones that stay rescale around it.
  const hiddenKey = hidden.join("|");
  useEffect(() => {
    for (const [key, track] of tracks.current) {
      const to = hidden.includes(key) ? 0 : 1;
      if (track.presence === to) continue;
      track.fade?.stop();
      if (reduced) { update(track, { presence: to }); continue; }
      update(track, { fade: animate(track.presence, to, { ...settle, onUpdate: value => { update(track, { presence: value }); paint(); } }) });
    }
    paint();
  }, [hiddenKey, paint, reduced]); // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    if (reduced) { scale.min.jump(steady.min); scale.max.jump(steady.max); return; }
    const controls = [animate(scale.min, steady.min, settle), animate(scale.max, steady.max, settle)];
    return () => controls.forEach(control => control.stop());
  }, [reduced, scale.max, scale.min, steady]);

  // The lines draw left to right the first time the chart is seen with data.
  useEffect(() => {
    if (empty) return;
    if (reduced) { drawn.jump(1); return; }
    if (!inView || drawn.get() >= 1) return;
    const controls = animate(drawn, 1, draw);
    return () => controls.stop();
  }, [drawn, empty, inView, reduced]);

  // The crosshair appears on the first scrubbed point and glides between the ones after it; the tooltip follows on its own spring.
  const wasScrubbing = useRef(false);
  useEffect(() => {
    if (index === null) { wasScrubbing.current = false; return; }
    const to = last > 0 ? index / last : .5;
    if (reduced || !wasScrubbing.current) { cursor.jump(to); tipSpringX.jump(tipTargetX.get()); tipSpringY.jump(tipTargetY.get()); }
    wasScrubbing.current = true;
    if (reduced) return;
    const controls = animate(cursor, to, glide);
    return () => controls.stop();
  }, [cursor, index, last, reduced, tipSpringX, tipSpringY, tipTargetX, tipTargetY]);

  const onActive = useRef(onActiveChange);
  useLayoutEffect(() => { onActive.current = onActiveChange; });
  useEffect(() => { onActive.current?.(index, index === null ? null : data[index] ?? null); }, [index]); // eslint-disable-line react-hooks/exhaustive-deps

  const pointAt = (clientX: number) => {
    const rect = plot.current?.getBoundingClientRect();
    if (!rect?.width || last < 1) return Math.max(0, last);
    return clamp(Math.round(((clientX - rect.left) / rect.width) * last), 0, last);
  };
  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
    if (event.key === "Escape" && scrubbing) { event.preventDefault(); setActive(null); return; }
    const from = index ?? last + 1, page = Math.max(1, Math.round(data.length / 6));
    const next = ({ ArrowLeft: from - 1, ArrowDown: from - 1, ArrowRight: index === null ? last : from + 1, ArrowUp: index === null ? last : from + 1, PageDown: from - page, PageUp: from + page, Home: 0, End: last } as Record<string, number>)[event.key];
    if (next === undefined || empty) return;
    event.preventDefault();
    setActive(clamp(next, 0, last));
  };
  const onPointerDown = (event: PointerEvent<HTMLDivElement>) => {
    if (empty) return;
    if (event.pointerType !== "mouse") event.currentTarget.setPointerCapture?.(event.pointerId);
    setActive(pointAt(event.clientX));
  };
  const onPointerMove = (event: PointerEvent<HTMLDivElement>) => { if (!empty && (event.pointerType === "mouse" || event.buttons)) setActive(pointAt(event.clientX)); };

  const format = (value: number, line: LineChartSeries) => `${formatValue ? formatValue(value, line) : grouped.format(value)}${unit ? ` ${unit}` : ""}`;
  const reading = index === null ? null : data[index];
  const valueText = reading ? `${reading.label}: ${visible.map(line => `${line.label} ${format(reading.values[line.key] ?? 0, line)}`).join(", ")}` : empty ? emptyLabel : `${visible.length} of ${series.length} series shown`;
  const summary = empty ? `${label}. ${emptyLabel}.` : `${label}, ${data[0]!.label} to ${data[last]!.label}. ${visible.map(line => `${line.label}, latest ${format(data[last]!.values[line.key] ?? 0, line)}`).join(". ")}.`;
  const picks = axisPicks(data, plotWidth);
  const allHidden = !empty && visible.length === 0;
  const tipX = reduced ? tipTargetX : tipSpringX, tipY = reduced ? tipTargetY : tipSpringY;

  return <figure ref={figure} className={[styles.figure, className].filter(Boolean).join(" ")} aria-label={label} aria-busy={loading || undefined}>
    {showLegend && <div className={styles.legend} role="group" aria-label={`${label} series`}>
      {series.map((line, at) => <button key={line.key} type="button" className={styles.toggle} aria-pressed={!hidden.includes(line.key)} onClick={() => toggle(line.key)} style={{ "--series": colorOf(line, at) } as CSSProperties}>
        <span className={styles.swatch} data-dashed={line.dashed || undefined} aria-hidden="true" />
        <span className={styles.toggleLabel}>{line.label}</span>
      </button>)}
    </div>}
    <div className={styles.chart} data-scrubbing={scrubbing || undefined} data-loading={loading || undefined}>
      <div ref={plot} className={styles.plot} style={{ height }} role="slider" tabIndex={empty ? -1 : 0} aria-label={`${label}, explore by ${categoryLabel.toLowerCase()}`} aria-orientation="horizontal" aria-valuemin={1} aria-valuemax={Math.max(1, data.length)} aria-valuenow={(index ?? last) + 1} aria-valuetext={valueText}
        onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={event => { if (event.pointerType !== "mouse") setActive(null); }} onPointerCancel={() => setActive(null)} onPointerLeave={event => { if (event.pointerType === "mouse") setActive(null); }} onKeyDown={onKeyDown} onBlur={() => setActive(null)}
        onFocus={event => { if (event.currentTarget.matches(":focus-visible") && !empty) setActive(current => current ?? last); }}>
        <svg className={styles.svg} width="100%" height={height} aria-hidden="true" focusable="false">
          <defs><clipPath id={`${uid}-draw`} clipPathUnits="userSpaceOnUse"><rect ref={clip} x={-8} y={-16} width={0} height={height + 32} /></clipPath></defs>
          <AnimatePresence initial={false}>{steady.ticks.slice(1).map(value => <Gridline key={value} value={value} scale={scale} height={height} />)}</AnimatePresence>
          <line className={styles.baseline} x1={0} x2="100%" y1={height - .5} y2={height - .5} />
          <line ref={crosshair} className={styles.crosshair} y1={TOP - 4} y2={height} />
          <g className={styles.series} clipPath={`url(#${uid}-draw)`}>
            {series.map((line, at) => <g key={line.key} ref={node => { if (node) groups.current.set(line.key, node); else groups.current.delete(line.key); }} style={{ "--series": colorOf(line, at) } as CSSProperties}>
              {(line.area ?? at === 0) && <path ref={node => { if (node) areas.current.set(line.key, node); else areas.current.delete(line.key); }} className={styles.area} />}
              <path ref={node => { if (node) lines.current.set(line.key, node); else lines.current.delete(line.key); }} className={styles.line} data-dashed={line.dashed || undefined} />
            </g>)}
          </g>
          {series.map((line, at) => <circle key={line.key} ref={node => { if (node) dots.current.set(line.key, node); else dots.current.delete(line.key); }} className={styles.dot} r={4} data-hidden={hidden.includes(line.key) || undefined} style={{ "--series": colorOf(line, at) } as CSSProperties} />)}
        </svg>
        <motion.div ref={tip} className={styles.tooltip} style={{ x: tipX, y: tipY }} aria-hidden="true">
          <p className={styles.tipTitle}>{(reading ?? data[last])?.label ?? ""}</p>
          {visible.map(line => <p key={line.key} className={styles.tipRow} style={{ "--series": colorOf(line, series.indexOf(line)) } as CSSProperties}>
            <span className={styles.tipSwatch} data-dashed={line.dashed || undefined} />
            <span className={styles.tipName}>{line.label}</span>
            <span className={styles.tipValue}>{format((reading ?? data[last])?.values[line.key] ?? 0, line)}</span>
          </p>)}
        </motion.div>
        <AnimatePresence initial={false}>
          {(empty || allHidden) && !loading && <motion.p key={empty ? "empty" : "hidden"} className={styles.message} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0, transition: fadeFast }}>{empty ? emptyLabel : "Choose a series to show"}</motion.p>}
          {loading && empty && <motion.span key="skeleton" className={styles.skeleton} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0, transition: fadeFast }} aria-hidden="true" />}
        </AnimatePresence>
      </div>
      <div className={styles.gutter} aria-hidden="true">
        <AnimatePresence initial={false}>{steady.ticks.map(value => <TickLabel key={value} value={value} scale={scale} height={height} format={formatTick} />)}</AnimatePresence>
      </div>
      <div className={styles.axis} aria-hidden="true">
        <AnimatePresence initial={false}>
          {picks.map(at => { const share = last > 0 ? at / last : .5; return <motion.span key={`${data[at]!.key}:${data[at]!.axisLabel}`} className={styles.axisLabel} style={{ left: `${share * 100}%`, translateX: `${-share * 100}%` }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0, transition: fadeFast }} transition={{ duration: duration.standard, ease: [...ease.standard] }}>{data[at]!.axisLabel}</motion.span>; })}
        </AnimatePresence>
      </div>
    </div>
    <p className={styles.srOnly}>{summary}</p>
    {!empty && <table className={styles.srOnly}>
      <caption>{label}</caption>
      <thead><tr><th scope="col">{categoryLabel}</th>{series.map(line => <th key={line.key} scope="col">{line.label}</th>)}</tr></thead>
      <tbody>{data.map(item => <tr key={item.key}><th scope="row">{item.label}</th>{series.map(line => <td key={line.key}>{format(item.values[line.key] ?? 0, line)}</td>)}</tr>)}</tbody>
    </table>}
    <p className={styles.srOnly} aria-live="polite" aria-atomic="true">{loading ? "Loading" : ""}</p>
  </figure>;
}

export default LineChart;
```

### `registry/components/line-chart/line-chart.module.css`

```css
.figure { display: grid; min-width: 0; gap: var(--space-inset); margin: 0; color: var(--foreground); font-family: var(--font-body); letter-spacing: var(--tracking-body); }

/* Series toggles: a plain line swatch beside each name. A hidden series keeps its place, its swatch empties and its name steps back. */
.legend { display: flex; flex-wrap: wrap; gap: var(--space-hair) var(--space-tight); min-width: 0; margin-inline: calc(var(--space-related) * -1); }
.toggle { display: inline-flex; min-height: 32px; align-items: center; gap: var(--space-related); padding: 0 var(--space-group) 0 var(--space-related); border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer; font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
.toggle:active { transform: scale(.97); }
.toggle[aria-pressed="false"] { color: var(--text-muted); }
.toggleLabel { white-space: nowrap; transition: opacity var(--duration-fast) var(--ease-standard); }
.toggle[aria-pressed="false"] .toggleLabel { opacity: .72; text-decoration: line-through; text-decoration-color: color-mix(in oklab, currentColor 55%, transparent); }
.swatch { position: relative; width: 14px; height: 10px; flex: none; }
.swatch::before { position: absolute; top: 50%; left: 0; width: 100%; height: 2.5px; border-radius: var(--radius-full); corner-shape: round; background: var(--series); content: ""; transform: translateY(-50%) scaleX(1); transform-origin: left; transition: transform var(--duration-standard) var(--ease-spring), opacity var(--duration-fast) var(--ease-standard); }
.swatch[data-dashed]::before { background: repeating-linear-gradient(to right, var(--series) 0 4px, transparent 4px 7px); }
.toggle[aria-pressed="false"] .swatch::before { opacity: .45; transform: translateY(-50%) scaleX(.35); }
@media (hover: hover) and (pointer: fine) { .toggle:hover { background: color-mix(in oklab, var(--foreground) 5%, transparent); color: var(--foreground); } }

/* Plot, right-hand value axis, and the date axis beneath. */
.chart { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 48px; grid-template-rows: auto 26px; min-width: 0; }
.plot { position: relative; grid-area: 1 / 1; min-width: 0; outline: none; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }
.svg { display: block; overflow: visible; }
.grid { stroke: var(--border); stroke-width: 1; shape-rendering: crispEdges; }
.baseline { stroke: var(--border-strong); stroke-width: 1; shape-rendering: crispEdges; }
.crosshair { stroke: var(--border-strong); stroke-width: 1; shape-rendering: crispEdges; opacity: 0; transition: opacity var(--duration-instant) var(--ease-standard); }
.chart[data-scrubbing] .crosshair { opacity: 1; }

.series { transition: opacity var(--duration-standard) var(--ease-standard); }
.chart[data-loading] .series { opacity: .32; animation: breathe 1.4s var(--ease-in-out) infinite alternate; }
.line { fill: none; stroke: var(--series); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: stroke var(--duration-standard) var(--ease-standard); }
.line[data-dashed] { stroke-dasharray: 4 5; stroke-width: 1.75; }
.area { fill: color-mix(in oklab, var(--series) 9%, transparent); stroke: none; }
.dot { fill: var(--series); stroke: var(--surface); stroke-width: 2; opacity: 0; transition: opacity var(--duration-instant) var(--ease-standard); }
.chart[data-scrubbing] .dot:not([data-hidden]) { opacity: 1; }

/* The tooltip floats over the plot, so it is the one layer here with a shadow. Springs carry its position; opacity and a small scale bring it in. */
.tooltip { position: absolute; top: 0; left: 0; z-index: 1; display: grid; min-width: 140px; max-width: min(220px, 100%); gap: var(--space-tight); padding: var(--space-group) var(--space-group); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); opacity: 0; pointer-events: none; scale: .96; transition: opacity var(--duration-fast) var(--ease-standard), scale var(--duration-fast) var(--ease-standard); }
.chart[data-scrubbing] .tooltip { opacity: 1; scale: 1; }
.tipTitle { margin: 0 0 var(--space-hair); color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); white-space: nowrap; font-variant-numeric: tabular-nums; }
.tipRow { display: flex; min-width: 0; align-items: center; gap: var(--space-related); margin: 0; font-size: var(--text-sm); line-height: var(--leading-body); }
.tipSwatch { width: 10px; height: 2.5px; flex: none; border-radius: var(--radius-full); corner-shape: round; background: var(--series); }
.tipSwatch[data-dashed] { background: repeating-linear-gradient(to right, var(--series) 0 3px, transparent 3px 5px); }
.tipName { min-width: 0; flex: 1; overflow: hidden; color: var(--text-secondary); text-overflow: ellipsis; white-space: nowrap; }
.tipValue { flex: none; margin-left: var(--space-related); color: var(--foreground); font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; }

.message { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; color: var(--text-muted); font-size: var(--text-sm); pointer-events: none; }
/* First load with nothing to keep on screen: a calm band that breathes where the lines will be. */
.skeleton { position: absolute; inset: 30% 0 25%; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: linear-gradient(to bottom, transparent, color-mix(in oklab, var(--foreground) 5%, transparent)); animation: breathe 1.4s var(--ease-in-out) infinite alternate; }

.gutter { position: relative; grid-area: 1 / 2; min-width: 0; }
.tick { position: absolute; top: 0; left: var(--space-group); display: block; height: 0; white-space: nowrap; pointer-events: none; }
.tickText { display: block; transform: translateY(-50%); color: var(--text-muted); font-size: var(--text-xs); line-height: 1; font-variant-numeric: tabular-nums; }

.axis { position: relative; grid-area: 2 / 1; min-width: 0; }
.axisLabel { position: absolute; top: var(--space-related); color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; }

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

@keyframes breathe { from { opacity: .22; } to { opacity: .45; } }
@media (prefers-reduced-motion: reduce) {
  .toggle, .swatch::before, .line, .tooltip, .crosshair, .dot, .series { transition: none; }
  .chart[data-loading] .series, .skeleton { animation: none; }
}
```

### `lib/media.ts`

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

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

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

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

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

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

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

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

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

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

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

### `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/segmented-control/segmented-control.tsx`

```tsx
"use client";

import { useEffect, useId, useLayoutEffect, useRef, type KeyboardEvent, type ReactNode } from "react";
import { LayoutGroup, motion, useReducedMotion } from "motion/react";
import { motionTokens } from "../lib/motion-tokens";
import styles from "./segmented-control.module.css";

export interface Segment { value: string; label: string; /** Optional content after the label, such as a badge. */ accessory?: ReactNode; }
export interface SegmentedControlProps {
  options: Segment[];
  value: string;
  onValueChange: (value: string) => void;
  label?: string;
  /** Called when the pointer or focus reaches an option, before it is chosen. Use it to start loading what that option shows. */
  onOptionIntent?: (value: string) => void;
  /** `sm` (default) or `xs`, a compact 24px control for dense UI such as an Edit/Read toggle in a toolbar. */
  size?: "sm" | "xs";
  className?: string;
}

export default function SegmentedControl({ options, value, onValueChange, label, onOptionIntent, size = "sm", className }: SegmentedControlProps) {
  const id = useId();
  const reduced = useReducedMotion();
  const track = useRef<HTMLDivElement>(null);

  // When the options are wider than the container, the track scrolls inside itself. Edges fade only on the side with more to see.
  useLayoutEffect(() => {
    const node = track.current;
    if (!node) return;
    const edges = () => {
      const rest = node.scrollWidth - node.clientWidth - node.scrollLeft;
      node.toggleAttribute("data-fade-start", node.scrollLeft > 1);
      node.toggleAttribute("data-fade-end", rest > 1);
    };
    edges();
    node.addEventListener("scroll", edges, { passive: true });
    const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(edges);
    observer?.observe(node);
    return () => { node.removeEventListener("scroll", edges); observer?.disconnect(); };
  }, [options.length]);

  // The selected option is always scrolled fully into view, with a little room so it clears the fade.
  const first = useRef(true);
  useEffect(() => {
    const node = track.current;
    const button = node?.querySelector<HTMLElement>('[aria-pressed="true"]');
    if (!node || !button || node.scrollWidth <= node.clientWidth) { first.current = false; return; }
    const room = 20, start = button.offsetLeft - room, end = button.offsetLeft + button.offsetWidth + room - node.clientWidth;
    const left = node.scrollLeft > start ? start : node.scrollLeft < end ? end : node.scrollLeft;
    if (left !== node.scrollLeft) node.scrollTo({ left: Math.max(0, left), behavior: first.current || reduced ? "auto" : "smooth" });
    first.current = false;
  }, [value, reduced]);

  // Arrow keys, Home and End move the selection like a tab list; only the selected option is a tab stop.
  const selectedIndex = Math.max(0, options.findIndex(option => option.value === value));
  const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
    const last = options.length - 1;
    const target = event.key === "ArrowRight" || event.key === "ArrowDown" ? (selectedIndex === last ? 0 : selectedIndex + 1)
      : event.key === "ArrowLeft" || event.key === "ArrowUp" ? (selectedIndex === 0 ? last : selectedIndex - 1)
        : event.key === "Home" ? 0 : event.key === "End" ? last : -1;
    if (target < 0 || !options[target]) return;
    event.preventDefault();
    onValueChange(options[target].value);
    track.current?.querySelector<HTMLElement>(`[data-value="${CSS.escape(options[target].value)}"]`)?.focus({ preventScroll: true });
  };

  return <div className={`${styles.root} ${className ?? ""}`} role="group" aria-label={label} data-size={size}>
    <LayoutGroup id={id}><motion.div ref={track} layoutScroll className={styles.track}>
      {options.map((option, index) => <button key={option.value} id={`${id}-${option.value}`} className={styles.button} type="button" data-value={option.value} aria-pressed={value === option.value} tabIndex={index === selectedIndex ? 0 : -1} onClick={() => onValueChange(option.value)} onKeyDown={onKeyDown} onPointerEnter={onOptionIntent ? () => onOptionIntent(option.value) : undefined} onFocus={onOptionIntent ? () => onOptionIntent(option.value) : undefined}>
        {value === option.value && <motion.span className={styles.selection} layoutId="selection" layoutDependency={value} transition={reduced ? { duration: 0 } : motionTokens.spring.morph} aria-hidden="true" />}
        <span className={styles.label}>{option.label}{option.accessory}</span>
      </button>)}
    </motion.div></LayoutGroup>
  </div>;
}
```

### `registry/components/segmented-control/segmented-control.module.css`

```css
/* The frame can shrink below its content inside flex and grid parents; the track then scrolls inside it. */
.root { display: inline-flex; min-width: 0; max-width: 100%; flex-shrink: 1; border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface-muted); }
/* The track owns the stacking context so the gliding highlight passes under every label, not over earlier ones. */
.track { --fade-start: 0px; --fade-end: 0px; isolation: isolate; display: flex; min-width: 0; gap: var(--space-hair); overflow-x: auto; overscroll-behavior-x: contain; padding: var(--space-tight); border-radius: inherit; corner-shape: inherit; scrollbar-width: none; -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent); mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent); }
.track::-webkit-scrollbar { display: none; }
.track[data-fade-start] { --fade-start: 20px; }
.track[data-fade-end] { --fade-end: 20px; }
/* Items never scale or change weight; only the highlight travels and the label color follows it. */
.button { position: relative; flex: 0 0 auto; min-height: var(--control-height-sm); border: 0; border-radius: calc(var(--radius-control) - var(--space-tight)); padding: 0 var(--space-group); background: transparent; color: var(--text-muted); font: inherit; font-size: var(--text-sm); font-weight: 500; white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
@media (hover: hover) and (pointer: fine) { .button:hover { color: var(--foreground); } }
.button:active, .button[aria-pressed="true"] { color: var(--foreground); }
.selection { position: absolute; z-index: -1; inset: 0; border: 1px solid var(--border); border-radius: inherit; corner-shape: inherit; background: var(--surface); box-shadow: var(--shadow-resting); }
.label { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: var(--space-related); }
/* Compact: 24px segments in a thinner track, at the small type step. */
.root[data-size="xs"] .track { padding: var(--space-hair); }
.root[data-size="xs"] .button { min-height: var(--control-height-xs); padding: 0 var(--space-related); font-size: var(--text-xs); }
@media (max-width: 380px) { .button { padding: 0 var(--space-group); } }
@media (prefers-reduced-motion: reduce) { .button { transition: none; } }
```

### `registry/components/sparkline/sparkline.tsx`

```tsx
"use client";

import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent, type PointerEvent } from "react";
import { AnimatePresence, animate, motion, useInView, useMotionValue, useMotionValueEvent, useReducedMotion, useTransform, type Variants } from "motion/react";
import { motionTokens } from "../lib/motion-tokens";
import styles from "./sparkline.module.css";

export interface SparklineProps {
  data: number[];
  label: string;
  value?: string;
  change?: string;
  tone?: "accent" | "success" | "warning" | "danger";
  width?: number;
  height?: number;
  /** One label per point, such as a date. It replaces the change while that point is scrubbed. */
  labels?: string[];
  /** Formats a scrubbed point for the headline. Defaults to a grouped number. */
  formatValue?: (value: number, index: number) => string;
  /** A quiet neutral fill under the line. */
  area?: boolean;
  /** Pointer and keyboard scrubbing through the points. */
  interactive?: boolean;
}

/** Copy that holds a number enters from the side it moved toward: a larger value rises from below, a smaller one drops from above. */
const rise: Variants = { hidden: (direction: number) => ({ opacity: 0, y: `${.3 * direction}em`, filter: `blur(${motionTokens.blur.soft}px)` }), shown: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] } }, gone: (direction: number) => ({ opacity: 0, y: `${-.3 * direction}em`, 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 amountIn = (text: string) => Number(text.replace(/,/g, "").replace(/−/g, "-").match(/-?\d+(?:\.\d+)?/)?.[0] ?? NaN);
const grouped = new Intl.NumberFormat("en-US");
/** The first draw: one smooth stroke, slow at both ends like a pen. */
const draw = { duration: motionTokens.duration.considered * 1.75, ease: [...motionTokens.ease.inOut] } as const;
const quick = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } as const;

type Chunk = { slot: string; key: string; text: string };
/** A number keeps its unchanged digits still, keyed by place from the right. Words key on everything up to them, so only the first changed word and what follows it roll. */
function chunksOf(text: string, whole: boolean): Chunk[] {
  if (!text) return [];
  if (whole) return [{ slot: "all", key: text, text }];
  if (!/\p{L}/u.test(text)) return [...text].map((char, index, all) => ({ slot: `n${all.length - index}`, key: char, text: char }));
  const words = text.split(/(?<=\s)/);
  return words.map((word, index) => ({ slot: `w${index}`, key: words.slice(0, index + 1).join(""), text: word }));
}

/** Text that changes rolls in place, like a numeric content transition; `morph` springs the wrapper to the new width so nothing beside it jumps. */
function Roll({ text, direction, whole = false, morph = false, tone }: { text: string; direction?: number; whole?: boolean; morph?: boolean; tone?: string }) {
  const reduceMotion = !!useReducedMotion();
  const sizer = useRef<HTMLSpanElement>(null);
  const width = useMotionValue<number | "auto">("auto");
  const [shown, setShown] = useState({ text, direction: 1 });
  if (shown.text !== text) setShown({ text, direction: direction ?? (amountIn(text) < amountIn(shown.text) ? -1 : 1) });
  useEffect(() => {
    const node = sizer.current;
    if (!node || typeof ResizeObserver === "undefined") return;
    let measured: string | null = null;
    // Layout size, not the transformed rect, so a scaling parent never leaves the text clipped. Only a new text springs; font loads jump.
    const observer = new ResizeObserver(([entry]) => {
      if (!entry) return;
      const next = entry.borderBoxSize?.[0]?.inlineSize ?? node.offsetWidth;
      if (measured !== null && measured !== node.textContent && !reduceMotion) animate(width, next, motionTokens.spring.morph);
      else width.jump(next);
      measured = node.textContent;
    });
    observer.observe(node);
    return () => observer.disconnect();
  }, [morph, reduceMotion, width]);
  const numeric = !whole && !/\p{L}/u.test(text);
  const variants = reduceMotion ? fade : rise;
  const anchor = numeric ? "right" : "left";
  return <motion.span className={styles.roll} data-end={numeric || undefined} data-whole={whole || undefined} style={morph ? { width } : undefined}>
    {morph && <span ref={sizer} className={styles.sizer} aria-hidden="true">{text}</span>}
    {/* One flex item holds the pieces, so they stay inline and the text reads and copies as one string. */}
    <span className={styles.track}><AnimatePresence mode="popLayout" initial={false} custom={shown.direction} anchorX={anchor}>
      {chunksOf(text, whole).map(chunk => <motion.span key={chunk.slot} className={styles.slot} custom={shown.direction} variants={variants} initial="hidden" animate="shown" exit="gone">
        <AnimatePresence mode="popLayout" initial={false} custom={shown.direction} anchorX={anchor}>
          <motion.span key={`${tone ?? ""}${chunk.key}`} className={tone ? `${styles.glyph} ${tone}` : styles.glyph} custom={shown.direction} variants={variants} initial="hidden" animate="shown" exit="gone">{chunk.text}</motion.span>
        </AnimatePresence>
      </motion.span>)}
    </AnimatePresence></span>
  </motion.span>;
}

type Point = [number, number];
/** Space kept above the highest and below the lowest point, and half the line width. */
const inset = 5, cap = 1.25;
const grid = Array.from({ length: 161 }, (_, index) => index / 160);

/** A smooth line through every point, sampled densely in unit space (x and y both 0 to 1, y up). Monotone cubic tangents keep it from swinging past the data. */
function curveFor(data: number[]): Point[] {
  const min = Math.min(...data), span = Math.max(...data) - min;
  const ys = data.map(value => span ? (value - min) / span : .5);
  const last = ys.length - 1, step = 1 / last;
  const slopes = ys.slice(1).map((y, index) => (y - ys[index]!) / step);
  const tangents = ys.map((_, index) => index === 0 || index === last ? slopes[0]! : (Math.sign(slopes[index - 1]!) + Math.sign(slopes[index]!)) * Math.min(Math.abs(slopes[index - 1]!), Math.abs(slopes[index]!), Math.abs(slopes[index - 1]! + slopes[index]!) / 4) || 0);
  if (last > 1) { tangents[0] = (3 * slopes[0]! - tangents[1]!) / 2; tangents[last] = (3 * slopes[last - 1]! - tangents[last - 1]!) / 2; }
  const samples = Math.max(1, Math.min(16, Math.round(128 / last)));
  const points: Point[] = [];
  for (let index = 0; index < last; index++) for (let sample = 0; sample < samples; sample++) {
    const t = sample / samples, t2 = t * t, t3 = t2 * t;
    const y = (2 * t3 - 3 * t2 + 1) * ys[index]! + (t3 - 2 * t2 + t) * step * tangents[index]! + (3 * t2 - 2 * t3) * ys[index + 1]! + (t3 - t2) * step * tangents[index + 1]!;
    points.push([(index + t) * step, Math.min(1, Math.max(0, y))]);
  }
  points.push([1, ys[last]!]);
  return points;
}

/** Height of the sampled line at x, straight between samples. */
function heightAt(shape: Point[], x: number) {
  let low = 0, high = shape.length - 1;
  if (x <= shape[low]![0]) return shape[low]![1];
  if (x >= shape[high]![0]) return shape[high]![1];
  while (high - low > 1) { const middle = (low + high) >> 1; if (shape[middle]![0] < x) low = middle; else high = middle; }
  const [x0, y0] = shape[low]!, [x1, y1] = shape[high]!;
  return x1 === x0 ? y1 : y0 + ((y1 - y0) * (x - x0)) / (x1 - x0);
}

/** Every drawn string for one frame, in the chart's own pixels so strokes and dots stay round at any width. */
function geometryOf(shape: Point[], w: number, h: number, at: number) {
  const toY = (y: number) => inset + (1 - y) * (h - inset * 2);
  const points = shape.map(([x, y]): Point => [x * w, toY(y)]);
  const pairs = points.map(([x, y]) => `${x.toFixed(2)},${y.toFixed(2)}`);
  const cx = at * w, cy = toY(heightAt(shape, at));
  return { points, line: pairs.join(" "), trail: `M${pairs.join("L")}`, fill: `M${pairs.join("L")}L${w},${h}L0,${h}Z`, dot: `${cx},${cy} ${cx},${cy}`, cx };
}

/** Where the pen is along the line once `progress` of its length is drawn, so the fill follows the tip. */
function tipAt(points: Point[], progress: number) {
  const steps = points.slice(1).map(([x, y], index) => Math.hypot(x - points[index]![0], y - points[index]![1]));
  let left = progress * steps.reduce((sum, step) => sum + step, 0);
  for (let index = 0; index < steps.length; index++) {
    const step = steps[index]!, from = points[index]![0];
    if (left <= step) return from + (points[index + 1]![0] - from) * (step ? left / step : 1);
    left -= step;
  }
  return points[points.length - 1]![0];
}

export function Sparkline({ data, label, value, change, tone = "accent", width = 160, height = 52, labels, formatValue, area = true, interactive = true }: SparklineProps) {
  const safeData = data.length > 1 ? data : [data[0] ?? 0, data[0] ?? 0];
  const last = safeData.length - 1;
  const key = safeData.join(",");
  const target = useMemo(() => curveFor(key.split(",").map(Number)), [key]);
  const description = `${label}: ${value ?? safeData[last]}. ${change ?? ""}`.trim();
  const uid = `sparkline${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
  const ref = useRef<HTMLElement>(null);
  const plot = useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, amount: .5 });
  const reduceMotion = !!useReducedMotion();
  const [box, setBox] = useState({ w: width, h: height });
  const [active, setActive] = useState<number | null>(null);
  const [landed, setLanded] = useState(false);
  const index = active === null ? null : Math.min(active, last);
  const [moved, setMoved] = useState({ index, direction: 1 });
  if (moved.index !== index) setMoved({ index, direction: index === null || moved.index === null ? 1 : Math.sign(index - moved.index) || 1 });

  const [first] = useState(() => geometryOf(target, width, height, 1));
  const shape = useRef(target);
  const size = useRef(box);
  const drawn = useMotionValue(0), cursor = useMotionValue(1);
  const line = useMotionValue(first.line), trail = useMotionValue(first.trail), fill = useMotionValue(first.fill), dot = useMotionValue(first.dot);
  const cursorX = useMotionValue(first.cx), lit = useMotionValue(first.cx + cap + 16), dim = useMotionValue(first.cx + cap), reveal = useMotionValue(0);
  const lineOpacity = useTransform(drawn, progress => progress > .001 ? 1 : 0);

  // One writer for every drawn attribute: the morphing shape, the cursor, the pen and the measured size all repaint through here.
  const paint = useCallback(() => {
    const { w, h } = size.current, progress = drawn.get();
    const next = geometryOf(shape.current, w, h, cursor.get());
    line.set(next.line); trail.set(next.trail); fill.set(next.fill); dot.set(next.dot);
    cursorX.set(next.cx); lit.set(next.cx + cap + 16); dim.set(next.cx + cap);
    reveal.set(progress >= 1 ? w + 32 : tipAt(next.points, progress) + 16);
  }, [cursor, cursorX, dim, dot, drawn, fill, line, lit, reveal, trail]);
  useMotionValueEvent(cursor, "change", paint);
  useMotionValueEvent(drawn, "change", paint);

  // The chart draws in its own pixels: measure the width, keep the height in proportion.
  useLayoutEffect(() => {
    const node = plot.current;
    if (!node || typeof ResizeObserver === "undefined") return;
    const observer = new ResizeObserver(() => { const w = node.clientWidth; if (w) setBox(current => Math.abs(current.w - w) < .5 ? current : { w, h: (w * height) / width }); });
    observer.observe(node);
    return () => observer.disconnect();
  }, [height, width]);
  useLayoutEffect(() => { size.current = box; paint(); }, [box, paint]);

  // The line draws left to right once it is seen, the fill follows the pen, then the end dot lands.
  useEffect(() => {
    // Reduced motion shows the finished line at once; the dot lands a frame later, after hydration has matched the server.
    if (reduceMotion) { drawn.jump(1); const frame = requestAnimationFrame(() => setLanded(true)); return () => cancelAnimationFrame(frame); }
    if (!inView) return;
    const controls = animate(drawn, 1, { ...draw, onComplete: () => setLanded(true) });
    return () => controls.stop();
  }, [drawn, inView, reduceMotion]);

  // New data morphs the line from the shape on screen, sampled on shared x positions so a series that gains or loses points still flows.
  useEffect(() => {
    const from = shape.current;
    if (from === target) return;
    if (reduceMotion) { shape.current = target; paint(); return; }
    const xs = [...new Set([...grid, ...target.map(([x]) => x)])].sort((a, b) => a - b);
    const start = xs.map(x => heightAt(from, x)), goal = xs.map(x => heightAt(target, x));
    const controls = animate(0, 1, { ...motionTokens.spring.smooth, onUpdate: progress => { shape.current = xs.map((x, at): Point => [x, start[at]! + (goal[at]! - start[at]!) * progress]); paint(); }, onComplete: () => { shape.current = target; paint(); } });
    return () => controls.stop();
  }, [paint, reduceMotion, target]);

  // The cursor springs to the scrubbed point and rides the line between points; leaving sends it home to the latest value.
  useEffect(() => {
    const to = index === null ? 1 : index / last;
    if (reduceMotion) { cursor.jump(to); return; }
    const controls = animate(cursor, to, motionTokens.spring.snappy);
    return () => controls.stop();
  }, [cursor, index, last, reduceMotion]);

  const scrubbing = index !== null;
  const format = (at: number) => formatValue ? formatValue(safeData[at]!, at) : grouped.format(safeData[at]!);
  const point = format(index ?? last), when = labels?.[index ?? last];
  const headline = value === undefined ? undefined : scrubbing ? point : value;
  const aside = index === null ? change ?? "" : value === undefined ? [point, when].filter(Boolean).join(" · ") : when ?? `${index + 1} of ${last + 1}`;
  const pointAt = (clientX: number) => { const rect = plot.current?.getBoundingClientRect(); return rect?.width ? Math.max(0, Math.min(last, Math.round(((clientX - rect.left) / rect.width) * last))) : last; };
  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
    if (event.key === "Escape" && scrubbing) { event.preventDefault(); setActive(null); return; }
    const from = index ?? last, page = Math.max(1, Math.round((last + 1) / 6));
    const next = ({ ArrowLeft: from - 1, ArrowDown: from - 1, ArrowRight: from + 1, ArrowUp: from + 1, PageDown: from - page, PageUp: from + page, Home: 0, End: last } as Record<string, number>)[event.key];
    if (next === undefined) return;
    event.preventDefault();
    setActive(Math.max(0, Math.min(last, next)));
  };
  const scrub = interactive ? {
    role: "slider", tabIndex: 0, "aria-label": `${label}, explore values`, "aria-valuemin": 1, "aria-valuemax": last + 1, "aria-valuenow": (index ?? last) + 1, "aria-valuetext": `${when ?? (index === null ? "Latest" : `Point ${index + 1} of ${last + 1}`)}: ${point}`,
    onPointerDown: (event: PointerEvent<HTMLDivElement>) => setActive(pointAt(event.clientX)),
    onPointerMove: (event: PointerEvent<HTMLDivElement>) => { if (event.pointerType === "mouse" || event.buttons) setActive(pointAt(event.clientX)); },
    onPointerUp: (event: PointerEvent<HTMLDivElement>) => { if (event.pointerType !== "mouse") setActive(null); },
    onPointerLeave: () => setActive(null), onPointerCancel: () => setActive(null), onBlur: () => setActive(null), onKeyDown,
  } : {};
  return <figure ref={ref} className={styles.figure} aria-label={description}>
    <figcaption className={styles.caption}>
      <span className={styles.label}><Roll text={label} whole /></span>
      {headline !== undefined && <strong><Roll text={headline} morph /></strong>}
      <small className={styles[tone]}><Roll text={aside} direction={scrubbing ? moved.direction : undefined} tone={scrubbing ? styles.point : undefined} morph /></small>
    </figcaption>
    <div ref={plot} className={styles.plot} data-interactive={interactive || undefined} {...scrub}>
      <svg className={styles.chart} viewBox={`0 0 ${box.w} ${box.h}`} aria-hidden="true" focusable="false">
        <defs>
          <clipPath id={`${uid}-lit`} clipPathUnits="userSpaceOnUse"><motion.rect x={-16} y={-16} width={lit} height={box.h + 32} /></clipPath>
          <clipPath id={`${uid}-dim`} clipPathUnits="userSpaceOnUse"><motion.rect x={dim} y={-16} width={box.w + 32} height={box.h + 32} /></clipPath>
          <clipPath id={`${uid}-fill`} clipPathUnits="userSpaceOnUse"><motion.rect x={-16} y={-16} width={reveal} height={box.h + 32} /></clipPath>
        </defs>
        {area && <motion.path className={`${styles.area} ${styles[tone]}`} d={fill} clipPath={`url(#${uid}-fill)`} />}
        <line className={styles.baseline} x1="0" y1={box.h - .5} x2={box.w} y2={box.h - .5} />
        {/* While scrubbing, the stretch after the cursor dims, as if the later points have not happened yet. */}
        <motion.path className={styles.trail} d={trail} clipPath={`url(#${uid}-dim)`} initial={false} animate={{ opacity: scrubbing && landed ? 1 : 0 }} transition={reduceMotion ? { duration: 0 } : quick} />
        <motion.polyline className={[styles.line, styles[tone]].join(" ")} points={line} clipPath={`url(#${uid}-lit)`} style={{ pathLength: drawn, opacity: lineOpacity }} />
        <motion.line className={styles.cursor} x1={cursorX} x2={cursorX} y1={0} y2={box.h} initial={false} animate={{ opacity: scrubbing ? 1 : 0 }} transition={reduceMotion ? { duration: 0 } : quick} />
        {/* A zero-length round-capped stroke is a true circle, and it takes the line's colour. It rests on the latest point and becomes the scrub handle. */}
        <motion.polyline className={[styles.end, styles[tone]].join(" ")} points={dot} initial={false} animate={{ opacity: landed ? 1 : 0, strokeWidth: landed ? scrubbing ? 10 : 7 : 0 }} transition={reduceMotion ? { duration: 0 } : { opacity: quick, strokeWidth: motionTokens.spring.snappy }} fill="none" strokeLinecap="round" />
      </svg>
    </div>
  </figure>;
}

export default Sparkline;
```

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

```css
.figure { display: grid; min-width: 0; gap: var(--space-group); margin: 0; color: var(--foreground); }.caption { display: flex; align-items: baseline; gap: var(--space-related); min-width: 0; color: var(--text-secondary); font-size: var(--text-sm); }.label { min-width: 0; overflow: hidden; white-space: nowrap; }.caption strong { margin-left: auto; color: var(--foreground); font-size: var(--text-lg); font-weight: 500; letter-spacing: var(--tracking-body); font-variant-numeric: tabular-nums; }.caption small { flex: none; font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; transition: color var(--duration-standard) var(--ease-standard); }
.accent { color: var(--accent-strong); stroke: var(--accent); }.success { color: var(--success); stroke: var(--success); }.warning { color: var(--warning); stroke: var(--warning); }.danger { color: var(--danger); stroke: var(--danger); }
/* The scrubbed date is context, not a change, so it stays neutral whatever the tone. */
.point { color: var(--text-secondary); }

/* Changing text rolls in place. Outgoing pieces are popped out of flow, so the wrapper never holds both widths; numbers stay anchored to their right edge. */
.roll { position: relative; display: inline-flex; max-width: 100%; overflow-x: clip; white-space: pre; vertical-align: top; }
.roll[data-end] { justify-content: flex-end; }
.track, .slot, .glyph { position: relative; display: inline-block; white-space: pre; }
.roll[data-whole] .track, .roll[data-whole] .slot, .roll[data-whole] .glyph { max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: top; }
.sizer { position: absolute; top: 0; left: 0; visibility: hidden; white-space: pre; pointer-events: none; }

/* The plot is the scrubber: hover or drag across it, or focus it and use the arrow keys. Vertical swipes still scroll the page. */
.plot { position: relative; min-width: 0; border-radius: calc(var(--radius-control) / 2); outline: none; -webkit-tap-highlight-color: transparent; }
.plot[data-interactive] { touch-action: pan-y; cursor: default; user-select: none; }
.plot:focus-visible { --focus-outline-offset: 8px; }
.chart { display: block; width: 100%; height: auto; overflow: visible; }
.baseline { stroke: var(--border); stroke-width: 1; }
/* The area under the line carries a faint wash of the line tone. */
.area.area { fill: color-mix(in oklab, currentColor 9%, transparent); stroke: none; }
.line, .trail { fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
.line { transition: stroke var(--duration-standard) var(--ease-standard); }
.trail { stroke: var(--border-strong); }
.cursor { stroke: var(--border-strong); stroke-width: 1; }
.end { transition: stroke var(--duration-standard) var(--ease-standard); }
@media (prefers-reduced-motion: reduce) { .line, .end, .caption small { transition: none; } }
```
