# Pantograph — Animated Navigation for React

> Morphing flyout nav where one shared dark-glass panel slides and resizes between triggers instead of closing and reopening — an anticipation-stretch morph with a caret that tracks the active trigger and content that crossfades in from the direction of travel. Hover-intent grace, full disclosure semantics (focus opens, Escape closes, arrow keys move triggers), and instant repositioning under reduced motion.

- Page: https://crucibleui.com/components/navigation/pantograph
- Category: [navigation](https://crucibleui.com/components/navigation)
- Tier: free (full source)
- Engine: motion
- npm dependencies: motion

## Install

```bash
npx shadcn@latest add @crucible/pantograph
```

Registry JSON: https://crucibleui.com/r/pantograph.json

Import from `@/components/crucible/pantograph`.

## Props (`PantographProps`)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items?` | `PantographItem[]` | `a small generic set` | Triggers to render, left to right. |
| `openOn?` | `"hover" \| "click"` | `"hover"` | How a flyout opens on pointer devices. |
| `caret?` | `boolean` | `true` | Show the caret that tracks the active trigger. |
| `closeDelay?` | `number` | `150` | Grace period in ms before the panel closes after the pointer leaves — the hover-intent forgiveness that stops flicker on diagonal travel. |
| `contentTransition?` | `"directional" \| "crossfade"` | `"directional"` | Incoming content slides in from the direction of travel (`"directional"`) or simply crossfades in place (`"crossfade"`). |
| `spring?` | `{ stiffness?: number; damping?: number; mass?: number }` | — | Spring physics for the panel morph. |
| `minWidth?` | `number` | `200` | Min panel width in px. |
| `maxWidth?` | `number` | `560` | Max panel width in px. |
| `value?` | `string \| null` | — | Controlled active trigger value; `null` means closed. |
| `defaultValue?` | `string \| null` | `null (closed)` | Initial active trigger when uncontrolled. |
| `onValueChange?` | `(value: string \| null) => void` | — | Fires whenever the active trigger changes (a value, or `null` on close). |

Also accepts: `Omit< React.ComponentPropsWithoutRef<"nav">, "onChange" | "value" | "defaultValue" >`.

## Usage example

```tsx
"use client";

import { Pantograph } from "@/components/crucible/pantograph";

function LinkList(
  links: { label: string; hint: string }[]
): React.ReactNode {
  return (
    <ul className="grid w-max min-w-52 gap-0.5">
      {links.map((l) => (
        <li key={l.label}>
          <a
            href="#"
            className="group/link flex flex-col rounded-lg px-3 py-2 transition-colors hover:bg-white/6 focus-visible:bg-white/6 focus-visible:outline-none"
          >
            <span className="text-sm font-medium text-white/85 transition-colors group-hover/link:text-white">
              {l.label}
            </span>
            <span className="text-xs text-white/45">{l.hint}</span>
          </a>
        </li>
      ))}
    </ul>
  );
}

const ITEMS = [
  {
    value: "products",
    label: "Products",
    content: LinkList([
      { label: "Overview", hint: "The whole platform at a glance" },
      { label: "Automation", hint: "Rules that run themselves" },
      { label: "Insights", hint: "Live dashboards and reports" },
    ]),
  },
  {
    value: "pricing",
    label: "Pricing",
    content: LinkList([
      { label: "Plans", hint: "Pick a tier that fits" },
      { label: "Enterprise", hint: "Volume, SSO, and support" },
    ]),
  },
  {
    value: "company",
    label: "Company",
    content: LinkList([
      { label: "About", hint: "Who we are" },
      { label: "Careers", hint: "Join the team" },
      { label: "Blog", hint: "News and writing" },
      { label: "Contact", hint: "Talk to us" },
    ]),
  },
];

export default function PantographDemo() {
  return (
    <div className="relative isolate flex h-full min-h-[360px] w-full flex-col items-center justify-start gap-8 overflow-hidden rounded-xl bg-neutral-950 px-6 pt-16">
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(255,255,255,0.04),transparent_60%)]"
      />
      <div className="relative z-10 flex flex-col items-center gap-8">
        <Pantograph items={ITEMS} />
        <p className="text-center text-sm text-neutral-500">
          Hover the menu — move between items and the panel morphs across.
        </p>
      </div>
    </div>
  );
}
```

## Accessibility & performance

Honors `prefers-reduced-motion` with a designed static frame or fade (never a blank box) and passes `className` through.

## Source (`pantograph.tsx`)

```tsx
"use client";

import {
  AnimatePresence,
  animate,
  motion,
  useMotionValue,
  useTransform,
  type Variants,
} from "motion/react";
import * as React from "react";

import { useReducedMotion } from "@/hooks/use-reduced-motion";
import { cn } from "@/lib/utils";

export interface PantographItem {
  /** Unique value identifying this trigger. */
  value: string;
  /** Trigger label shown in the bar. */
  label: React.ReactNode;
  /**
   * Flyout panel content for this trigger. Omit for a plain trigger that
   * navigates (via `href`) without opening a panel.
   */
  content?: React.ReactNode;
  /** Optional href — renders the trigger as a link. */
  href?: string;
}

export interface PantographProps
  extends Omit<
    React.ComponentPropsWithoutRef<"nav">,
    "onChange" | "value" | "defaultValue"
  > {
  /** Triggers to render, left to right. @default a small generic set */
  items?: PantographItem[];
  /** How a flyout opens on pointer devices. @default "hover" */
  openOn?: "hover" | "click";
  /** Show the caret that tracks the active trigger. @default true */
  caret?: boolean;
  /**
   * Grace period in ms before the panel closes after the pointer leaves —
   * the hover-intent forgiveness that stops flicker on diagonal travel.
   * @default 150
   */
  closeDelay?: number;
  /**
   * Incoming content slides in from the direction of travel (`"directional"`)
   * or simply crossfades in place (`"crossfade"`). @default "directional"
   */
  contentTransition?: "directional" | "crossfade";
  /** Spring physics for the panel morph. */
  spring?: { stiffness?: number; damping?: number; mass?: number };
  /** Min panel width in px. @default 200 */
  minWidth?: number;
  /** Max panel width in px. @default 560 */
  maxWidth?: number;
  /** Controlled active trigger value; `null` means closed. */
  value?: string | null;
  /** Initial active trigger when uncontrolled. @default null (closed) */
  defaultValue?: string | null;
  /** Fires whenever the active trigger changes (a value, or `null` on close). */
  onValueChange?: (value: string | null) => void;
}

const DEFAULT_SPRING = { stiffness: 440, damping: 40, mass: 0.9 };
const CONTENT_SHIFT = 22;
const CARET_PAD = 20;

function clamp(v: number, lo: number, hi: number): number {
  return Math.min(Math.max(v, lo), hi);
}

function LinkList({
  links,
}: {
  links: { label: string; hint?: string; href?: string }[];
}) {
  return (
    <ul className="grid w-max min-w-52 gap-0.5">
      {links.map((l) => (
        <li key={l.label}>
          <a
            href={l.href ?? "#"}
            className="group/link flex flex-col rounded-lg px-3 py-2 transition-colors hover:bg-white/6 focus-visible:bg-white/6 focus-visible:outline-none"
          >
            <span className="text-sm font-medium text-white/85 transition-colors group-hover/link:text-white">
              {l.label}
            </span>
            {l.hint ? (
              <span className="text-xs text-white/45">{l.hint}</span>
            ) : null}
          </a>
        </li>
      ))}
    </ul>
  );
}

const DEFAULT_ITEMS: PantographItem[] = [
  {
    value: "products",
    label: "Products",
    content: (
      <LinkList
        links={[
          { label: "Overview", hint: "The whole platform at a glance" },
          { label: "Automation", hint: "Rules that run themselves" },
          { label: "Insights", hint: "Live dashboards and reports" },
        ]}
      />
    ),
  },
  {
    value: "pricing",
    label: "Pricing",
    content: (
      <LinkList
        links={[
          { label: "Plans", hint: "Pick a tier that fits" },
          { label: "Enterprise", hint: "Volume and SSO" },
        ]}
      />
    ),
  },
  {
    value: "company",
    label: "Company",
    content: (
      <LinkList
        links={[
          { label: "About", hint: "Who we are" },
          { label: "Careers", hint: "Join the team" },
          { label: "Blog", hint: "News and writing" },
          { label: "Contact", hint: "Talk to us" },
        ]}
      />
    ),
  },
];

/**
 * Pantograph — a horizontal nav where hovering a trigger opens a single shared
 * flyout that *morphs* to the next trigger instead of closing and reopening:
 * the panel slides along the bar and resizes to fit each trigger's content
 * while the caret tracks the active trigger and the content crossfades in from
 * the direction of travel. The signature is an *anticipation stretch* — the
 * panel's leading edge extends toward the new trigger a beat before the body
 * follows and the trailing edge snaps in, so it reads as one window panning
 * over a continuous surface. Hover-intent grace prevents flicker on diagonal
 * travel. Full disclosure semantics: focus opens, Escape closes, arrow keys
 * move between triggers, `aria-expanded` reflects state. On coarse pointers it
 * opens on tap; under `prefers-reduced-motion` the panel repositions instantly
 * and the content simply crossfades.
 */
export function Pantograph({
  items = DEFAULT_ITEMS,
  openOn = "hover",
  caret = true,
  closeDelay = 150,
  contentTransition = "directional",
  spring,
  minWidth = 200,
  maxWidth = 560,
  value,
  defaultValue = null,
  onValueChange,
  className,
  ...props
}: PantographProps) {
  const reducedMotion = useReducedMotion();
  const baseId = React.useId();
  const panelId = `${baseId}-panel`;

  const rootRef = React.useRef<HTMLDivElement>(null);
  const barRef = React.useRef<HTMLDivElement>(null);
  const triggerRefs = React.useRef<Map<string, HTMLElement>>(new Map());
  const measureRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
  const closeTimer = React.useRef<number | null>(null);

  const springCfg = { ...DEFAULT_SPRING, ...spring };

  // ---- open / active state (controlled or not) -----------------------------
  const [internalValue, setInternalValue] = React.useState<string | null>(
    defaultValue
  );
  const activeValue = value !== undefined ? value : internalValue;
  const [direction, setDirection] = React.useState(1);
  const [focusValue, setFocusValue] = React.useState<string>(
    items[0]?.value ?? ""
  );
  const [finePointer, setFinePointer] = React.useState(true);

  const activeItem = items.find(
    (i) => i.value === activeValue && i.content != null
  );
  const open = activeItem != null;

  const setActive = React.useCallback(
    (next: string | null) => {
      if (next && activeValue && next !== activeValue) {
        const a = triggerCentersRef.current[activeValue];
        const b = triggerCentersRef.current[next];
        if (a != null && b != null) setDirection(b > a ? 1 : -1);
      }
      if (value === undefined) setInternalValue(next);
      onValueChange?.(next);
    },
    [activeValue, value, onValueChange]
  );

  // ---- layout measurement --------------------------------------------------
  const [navWidth, setNavWidth] = React.useState(0);
  const [dims, setDims] = React.useState<
    Record<string, { w: number; h: number }>
  >({});
  const triggerCentersRef = React.useRef<Record<string, number>>({});

  const measureTriggers = React.useCallback(() => {
    const bar = barRef.current;
    if (!bar) return;
    const barRect = bar.getBoundingClientRect();
    setNavWidth(barRect.width);
    const centers: Record<string, number> = {};
    for (const item of items) {
      const el = triggerRefs.current.get(item.value);
      if (el) {
        const r = el.getBoundingClientRect();
        centers[item.value] = r.left - barRect.left + r.width / 2;
      }
    }
    triggerCentersRef.current = centers;
  }, [items]);

  React.useLayoutEffect(() => {
    measureTriggers();
    if (typeof ResizeObserver === "undefined") return;
    const ro = new ResizeObserver(() => measureTriggers());
    if (barRef.current) ro.observe(barRef.current);
    return () => ro.disconnect();
  }, [measureTriggers]);

  // Measure each trigger's natural content size (padding included).
  React.useLayoutEffect(() => {
    if (typeof ResizeObserver === "undefined") return;
    const ro = new ResizeObserver((entries) => {
      setDims((prev) => {
        let changed = false;
        const next = { ...prev };
        for (const entry of entries) {
          const key = entry.target.getAttribute("data-measure-key");
          if (!key) continue;
          const el = entry.target as HTMLElement;
          const w = el.offsetWidth;
          const h = el.offsetHeight;
          if (!next[key] || next[key].w !== w || next[key].h !== h) {
            next[key] = { w, h };
            changed = true;
          }
        }
        return changed ? next : prev;
      });
    });
    for (const el of measureRefs.current.values()) ro.observe(el);
    return () => ro.disconnect();
  }, [items]);

  // ---- morph motion values -------------------------------------------------
  const leftMV = useMotionValue(0);
  const rightMV = useMotionValue(0);
  const heightMV = useMotionValue(0);
  const caretCenterMV = useMotionValue(0);
  const widthMV = useTransform([leftMV, rightMV], (latest: number[]) =>
    Math.max(0, latest[1] - latest[0])
  );
  const caretX = useTransform(
    [caretCenterMV, leftMV, rightMV],
    (latest: number[]) => {
      const [c, l, r] = latest;
      return clamp(c - l, CARET_PAD, Math.max(CARET_PAD, r - l - CARET_PAD));
    }
  );

  const wasOpenRef = React.useRef(false);

  React.useEffect(() => {
    if (!open || !activeValue) {
      wasOpenRef.current = false;
      return;
    }
    const dim = dims[activeValue];
    const center = triggerCentersRef.current[activeValue];
    if (!dim || center == null || !navWidth) return;

    const w = clamp(dim.w, minWidth, maxWidth);
    const h = dim.h;
    let targetLeft: number;
    if (w >= navWidth) targetLeft = (navWidth - w) / 2;
    else targetLeft = clamp(center - w / 2, 0, navWidth - w);
    const targetRight = targetLeft + w;

    if (!wasOpenRef.current || reducedMotion) {
      // Fresh open or reduced motion: seat the panel with no morph theatrics.
      leftMV.set(targetLeft);
      rightMV.set(targetRight);
      heightMV.set(h);
      caretCenterMV.set(center);
    } else {
      // Anticipation stretch: the leading edge (toward the new trigger) springs
      // fast and arrives first; the trailing edge follows a beat behind, so the
      // panel visibly stretches before it settles.
      const movingRight = targetLeft > leftMV.get();
      const lead = {
        type: "spring" as const,
        stiffness: springCfg.stiffness * 1.7,
        damping: springCfg.damping * 1.05,
        mass: springCfg.mass * 0.85,
      };
      const trail = {
        type: "spring" as const,
        stiffness: springCfg.stiffness * 0.85,
        damping: springCfg.damping,
        mass: springCfg.mass,
        delay: 0.05,
      };
      if (movingRight) {
        animate(rightMV, targetRight, lead);
        animate(leftMV, targetLeft, trail);
      } else {
        animate(leftMV, targetLeft, lead);
        animate(rightMV, targetRight, trail);
      }
      animate(heightMV, h, {
        type: "spring",
        stiffness: springCfg.stiffness,
        damping: springCfg.damping,
        mass: springCfg.mass,
      });
      animate(caretCenterMV, center, {
        type: "spring",
        stiffness: 700,
        damping: 44,
      });
    }
    wasOpenRef.current = true;
    // springCfg is derived each render; deps below cover its inputs via `spring`.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open, activeValue, dims, navWidth, reducedMotion, minWidth, maxWidth]);

  // ---- pointer / focus intent ---------------------------------------------
  React.useEffect(() => {
    if (typeof window === "undefined" || !window.matchMedia) return;
    const mql = window.matchMedia("(pointer: fine)");
    const sync = () => setFinePointer(mql.matches);
    sync();
    mql.addEventListener("change", sync);
    return () => mql.removeEventListener("change", sync);
  }, []);

  React.useEffect(
    () => () => {
      if (closeTimer.current != null) window.clearTimeout(closeTimer.current);
    },
    []
  );

  const cancelClose = React.useCallback(() => {
    if (closeTimer.current != null) {
      window.clearTimeout(closeTimer.current);
      closeTimer.current = null;
    }
  }, []);

  const scheduleClose = React.useCallback(() => {
    cancelClose();
    closeTimer.current = window.setTimeout(() => setActive(null), closeDelay);
  }, [cancelClose, closeDelay, setActive]);

  const hoverMode = openOn === "hover" && finePointer;

  const handleTriggerEnter = (item: PantographItem) => {
    if (!hoverMode) return;
    cancelClose();
    if (item.content != null) setActive(item.value);
    else setActive(null);
  };

  const handleTriggerClick = (item: PantographItem) => {
    if (item.content == null) return; // let the link/button do its thing
    if (openOn === "click" || !finePointer) {
      setActive(activeValue === item.value ? null : item.value);
    }
  };

  // Click-away for click / touch open.
  React.useEffect(() => {
    if (!open || hoverMode) return;
    const onDown = (e: PointerEvent) => {
      if (!rootRef.current?.contains(e.target as Node)) setActive(null);
    };
    document.addEventListener("pointerdown", onDown);
    return () => document.removeEventListener("pointerdown", onDown);
  }, [open, hoverMode, setActive]);

  // ---- keyboard: roving focus across triggers ------------------------------
  const enabledItems = items;
  const moveFocus = (fromValue: string, dir: 1 | -1 | "home" | "end") => {
    const count = enabledItems.length;
    if (count === 0) return;
    let idx = enabledItems.findIndex((i) => i.value === fromValue);
    if (idx < 0) idx = 0;
    let nextIdx: number;
    if (dir === "home") nextIdx = 0;
    else if (dir === "end") nextIdx = count - 1;
    else nextIdx = (idx + dir + count) % count;
    const next = enabledItems[nextIdx];
    if (!next) return;
    setFocusValue(next.value);
    triggerRefs.current.get(next.value)?.focus();
    if (open) {
      if (next.content != null) setActive(next.value);
      else setActive(null);
    }
  };

  const handleBarKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
    const from = focusValue || items[0]?.value || "";
    switch (e.key) {
      case "ArrowRight":
        e.preventDefault();
        moveFocus(from, 1);
        break;
      case "ArrowLeft":
        e.preventDefault();
        moveFocus(from, -1);
        break;
      case "Home":
        e.preventDefault();
        moveFocus(from, "home");
        break;
      case "End":
        e.preventDefault();
        moveFocus(from, "end");
        break;
      case "ArrowDown": {
        const item = items.find((i) => i.value === from);
        if (item?.content != null) {
          e.preventDefault();
          setActive(from);
        }
        break;
      }
      case "Escape":
        if (open) {
          e.preventDefault();
          setActive(null);
        }
        break;
      default:
        break;
    }
  };

  const handleRootBlur = (e: React.FocusEvent<HTMLDivElement>) => {
    if (!rootRef.current?.contains(e.relatedTarget as Node)) {
      cancelClose();
      setActive(null);
    }
  };

  const directionalContent = contentTransition === "directional" && !reducedMotion;
  const contentVariants: Variants = {
    enter: (dir: number) =>
      directionalContent
        ? { opacity: 0, x: dir * CONTENT_SHIFT, filter: "blur(5px)" }
        : { opacity: 0 },
    center: { opacity: 1, x: 0, filter: "blur(0px)" },
    exit: (dir: number) =>
      directionalContent
        ? { opacity: 0, x: -dir * CONTENT_SHIFT, filter: "blur(5px)" }
        : { opacity: 0 },
  };

  return (
    <nav
      {...props}
      aria-label={props["aria-label"] ?? "Main"}
      data-crucible="pantograph"
      className={cn("inline-block", className)}
    >
      <div
        ref={rootRef}
        className="relative inline-block"
        onPointerLeave={hoverMode ? scheduleClose : undefined}
        onPointerEnter={hoverMode ? cancelClose : undefined}
        onBlur={handleRootBlur}
      >
        {/* The trigger bar. */}
        <div
          ref={barRef}
          onKeyDown={handleBarKeyDown}
          className="relative flex items-center gap-1 rounded-full border border-white/10 bg-neutral-900/70 p-1.5 backdrop-blur-md"
        >
          {items.map((item) => {
            const isActive = item.value === activeValue && item.content != null;
            const hasPanel = item.content != null;
            const commonProps = {
              ref: (el: HTMLElement | null) => {
                if (el) triggerRefs.current.set(item.value, el);
                else triggerRefs.current.delete(item.value);
              },
              // Disclosure pattern (not an ARIA menu — the panel holds plain
              // links): triggers expose aria-expanded / aria-controls only.
              tabIndex: item.value === (focusValue || items[0]?.value) ? 0 : -1,
              "aria-expanded": hasPanel ? isActive : undefined,
              "aria-controls": hasPanel && isActive ? panelId : undefined,
              onFocus: () => {
                setFocusValue(item.value);
                if (hasPanel) setActive(item.value);
                else setActive(null);
              },
              onPointerEnter: () => handleTriggerEnter(item),
              className: cn(
                "relative rounded-full px-3.5 py-1.5 text-sm font-medium whitespace-nowrap transition-colors",
                "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-950",
                isActive ? "text-white" : "text-white/60 hover:text-white/90"
              ),
            };

            const inner = (
              <>
                {isActive && (
                  <motion.span
                    layoutId={`${baseId}-trigger-glow`}
                    aria-hidden
                    className="absolute inset-0 -z-10 rounded-full bg-white/[0.07]"
                    transition={
                      reducedMotion
                        ? { duration: 0 }
                        : { type: "spring", stiffness: 620, damping: 34 }
                    }
                  />
                )}
                <span className="relative">{item.label}</span>
              </>
            );

            return item.href ? (
              <a
                key={item.value}
                href={item.href}
                {...commonProps}
                onClick={(e) => {
                  if (hasPanel && (openOn === "click" || !finePointer)) {
                    e.preventDefault();
                    handleTriggerClick(item);
                  }
                }}
              >
                {inner}
              </a>
            ) : (
              <button
                key={item.value}
                type="button"
                {...commonProps}
                onClick={() => handleTriggerClick(item)}
              >
                {inner}
              </button>
            );
          })}
        </div>

        {/* The single shared flyout that morphs between triggers. */}
        <AnimatePresence>
          {open && activeItem && (
            <motion.div
              id={panelId}
              role="region"
              aria-label={
                typeof activeItem.label === "string" ? activeItem.label : undefined
              }
              key="pantograph-panel"
              className="absolute top-full left-0 z-20 mt-3 origin-top"
              style={{ x: leftMV, width: widthMV, height: heightMV }}
              initial={
                reducedMotion
                  ? { opacity: 0 }
                  : { opacity: 0, scale: 0.96, y: -6 }
              }
              animate={
                reducedMotion
                  ? { opacity: 1 }
                  : { opacity: 1, scale: 1, y: 0 }
              }
              exit={
                reducedMotion
                  ? { opacity: 0 }
                  : { opacity: 0, scale: 0.97, y: -6 }
              }
              transition={
                reducedMotion
                  ? { duration: 0.12 }
                  : { type: "spring", stiffness: 500, damping: 40, mass: 0.7 }
              }
              onPointerEnter={hoverMode ? cancelClose : undefined}
              onPointerLeave={hoverMode ? scheduleClose : undefined}
            >
              <div className="relative h-full w-full overflow-hidden rounded-2xl border border-white/10 bg-neutral-900/85 shadow-[0_1px_0_rgba(255,255,255,0.06)_inset,0_24px_60px_-20px_rgba(0,0,0,0.8)] backdrop-blur-xl">
                {/* Top hairline sheen. */}
                <div
                  aria-hidden
                  className="pointer-events-none absolute inset-x-0 top-0 h-px bg-linear-to-r from-transparent via-white/25 to-transparent"
                />
                <AnimatePresence
                  mode="popLayout"
                  initial={false}
                  custom={direction}
                >
                  <motion.div
                    key={activeItem.value}
                    custom={direction}
                    variants={contentVariants}
                    initial="enter"
                    animate="center"
                    exit="exit"
                    transition={
                      reducedMotion
                        ? { duration: 0.14 }
                        : {
                            type: "spring",
                            stiffness: 520,
                            damping: 42,
                            mass: 0.7,
                          }
                    }
                    className="absolute inset-0 p-3"
                  >
                    {activeItem.content}
                  </motion.div>
                </AnimatePresence>
              </div>

              {/* Caret — slides to track the active trigger, never teleports. */}
              {caret && (
                <motion.span
                  aria-hidden
                  className="pointer-events-none absolute -top-1.5 z-10 h-3 w-3 -translate-x-1/2 rotate-45 rounded-tl-[3px] border-t border-l border-white/10 bg-neutral-900/85 backdrop-blur-xl"
                  style={{ left: caretX }}
                />
              )}
            </motion.div>
          )}
        </AnimatePresence>
      </div>

      {/* Hidden measurers — natural content size drives the morph target. */}
      <div
        aria-hidden
        className="pointer-events-none invisible fixed top-0 left-0 -z-50"
        style={{ visibility: "hidden" }}
      >
        {items.map((item) =>
          item.content != null ? (
            <div
              key={item.value}
              data-measure-key={item.value}
              ref={(el) => {
                if (el) measureRefs.current.set(item.value, el);
                else measureRefs.current.delete(item.value);
              }}
              className="absolute w-max p-3"
            >
              {item.content}
            </div>
          ) : null
        )}
      </div>
    </nav>
  );
}
```
