{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "nav-overstrike-typewriter",
  "title": "Nav Overstrike Typewriter",
  "description": "A real site nav with a typed ruled line beneath its links, shaded by striking two or three characters into the same cell (real typewriter overstrike compositing, not this registry's usual single-glyph density ramp), with the current page reading as the deepest stack.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/nav-overstrike-typewriter/component.tsx",
      "content": "\"use client\";\n\nimport { useLayoutEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// NavOverstrikeTypewriter — a real site nav whose backing strip is rendered\n// as typewriter OVERSTRIKE shading: striking several characters into the\n// same cell without advancing the carriage, so their ink compounds into a\n// denser mark. This is a real technique from typewriter-art practice (e.g.\n// \"M\" over \"8\" over \":\" to fake a mid-tone the machine's fixed character set\n// has no single glyph for) — it is NOT this registry's usual glyph-density\n// ramp, where one glyph is picked off an ordered list by luminance. Every\n// cell here gets 1-3 *separate* fillText passes composited on top of each\n// other with independent jitter and alpha, so coverage genuinely compounds\n// where strokes overlap — a fake version would just look up a single denser\n// glyph, which is exactly the mechanic this component exists to be distinct\n// from.\n//\n// The strip sits as a thin ruled line beneath the real link row, one column\n// band per nav item. The item carrying aria-current=\"page\" always strikes\n// at the deepest stack (3 passes) — \"the active item is whichever cells\n// carry the deepest overstrike stack\" is not a metaphor, it is literally how\n// the current-page indicator reads. A slow ping-pong sweep — a carriage\n// returning and going back over the line — passes across the strip\n// continuously, giving every cell it crosses one extra, freshly-chosen\n// overstrike pass that fades with proximity, so the strip visibly re-types\n// itself at rest without ever using --ns-accent (interaction-only) in the\n// ambient field.\n//\n// House idiom duplicated on purpose (no shared helpers, per project\n// convention): cell metrics via an offscreen measureText after\n// document.fonts.ready, tokens via getComputedStyle + a MutationObserver on\n// documentElement's class, one rAF loop paused on document visibilitychange.\n// ---------------------------------------------------------------------------\n\nexport interface NavOverstrikeTypewriterLink {\n  href: string;\n  label: string;\n  /** marks this item as the current page — also the deepest overstrike stack */\n  current?: boolean;\n}\n\nexport interface NavOverstrikeTypewriterProps {\n  /** brand text on the left */\n  wordmark?: string;\n  /** nav links, in order */\n  links?: NavOverstrikeTypewriterLink[];\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst DEFAULT_LINKS: NavOverstrikeTypewriterLink[] = [\n  { href: \"#work\", label: \"Work\", current: true },\n  { href: \"#process\", label: \"Process\" },\n  { href: \"#pricing\", label: \"Pricing\" },\n  { href: \"#contact\", label: \"Contact\" },\n];\n\n// Characters real overstrike shading actually reaches for: tall, ink-heavy\n// letterforms and punctuation a typewriter's fixed set would stack to fake\n// tone the machine has no single glyph for.\nconst GLYPH_POOL = [\"M\", \"W\", \"N\", \"H\", \"K\", \"X\", \"#\", \"8\", \"%\", \"&\", \":\", \".\", \"-\", \"+\", \"/\"];\n\nconst BASE_STACK_MIN = 1;\nconst BASE_STACK_MAX = 2;\nconst ACTIVE_STACK = 3;\nconst MAX_STACK = 3;\nconst JITTER_PX = 1.3;\nconst SWEEP_PERIOD_MS = 7200; // one carriage return + return-stroke, full cycle\nconst SWEEP_SIGMA = 26; // px, width of the re-strike kernel around the carriage\n// Reduced motion freezes here rather than at t=0: at this sweep phase the\n// carriage sits clear of the active item's own permanent 3-stack, so a\n// single static frame shows BOTH cues at once — the always-deep active\n// column and a freshly re-struck ambient cell — rather than a blank strip\n// or the two effects coinciding and looking like one.\nconst STATIC_SWEEP_T = 0.63;\n\nfunction hash32(x: number): number {\n  x = Math.imul(x ^ (x >>> 16), 0x45d9f3b);\n  x = Math.imul(x ^ (x >>> 16), 0x45d9f3b);\n  x = x ^ (x >>> 16);\n  return x >>> 0;\n}\n\nfunction glyphFor(seed: number): string {\n  return GLYPH_POOL[hash32(seed) % GLYPH_POOL.length];\n}\n\nfunction jitterFor(seed: number): number {\n  const h = hash32(seed + 0x2545f4);\n  return ((h % 1000) / 1000 - 0.5) * JITTER_PX * 2;\n}\n\n// Triangle wave: 0 -> 1 -> 0 over one period, like a carriage sweeping right\n// then returning left rather than snapping back.\nfunction triangle(t: number): number {\n  const p = t * 2;\n  return p > 1 ? 2 - p : p;\n}\n\nexport function NavOverstrikeTypewriter({\n  wordmark = \"ns-ui\",\n  links = DEFAULT_LINKS,\n  className = \"\",\n}: NavOverstrikeTypewriterProps) {\n  const navRef = useRef<HTMLElement | null>(null);\n  const stripRef = useRef<HTMLDivElement | null>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const linkRefs = useRef<Map<string, HTMLAnchorElement>>(new Map());\n\n  // token derive happens in useLayoutEffect, before first paint, so no rAF /\n  // ResizeObserver / reduced-motion branch can draw with an empty ink string\n  useLayoutEffect(() => {\n    const strip = stripRef.current;\n    const canvas = canvasRef.current;\n    if (!strip || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const offscreen = document.createElement(\"canvas\").getContext(\"2d\");\n    if (!offscreen) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let fg = \"\";\n    let muted = \"\";\n    const readTokens = () => {\n      const cs = getComputedStyle(document.documentElement);\n      fg = cs.getPropertyValue(\"--foreground\").trim();\n      muted = cs.getPropertyValue(\"--ns-muted\").trim();\n    };\n    readTokens();\n\n    let disposed = false;\n    let raf = 0;\n    let dpr = 1;\n    let cssW = 0;\n    let cssH = 0;\n    let cellW = 0;\n    let cellH = 0;\n    let cols = 0;\n    let fontPx = 12;\n    let activeStart = -1;\n    let activeEnd = -1;\n\n    const computeActiveRange = () => {\n      const activeEl = strip\n        .closest(\"nav\")\n        ?.querySelector<HTMLAnchorElement>('a[aria-current=\"page\"]');\n      if (!activeEl || !cellW) {\n        activeStart = -1;\n        activeEnd = -1;\n        return;\n      }\n      const canvasRect = canvas.getBoundingClientRect();\n      const linkRect = activeEl.getBoundingClientRect();\n      const startPx = linkRect.left - canvasRect.left;\n      const endPx = linkRect.right - canvasRect.left;\n      activeStart = Math.max(0, Math.floor(startPx / cellW));\n      activeEnd = Math.min(cols, Math.ceil(endPx / cellW));\n    };\n\n    const computeGrid = () => {\n      const rect = canvas.getBoundingClientRect();\n      cssW = rect.width;\n      cssH = rect.height;\n      if (cssW <= 0 || cssH <= 0) return;\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(cssW * dpr));\n      canvas.height = Math.max(1, Math.round(cssH * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n      // font size derived proportionally from the strip's own measured\n      // height, never a fixed px constant — keeps the mark compact rather\n      // than turning into a coarse zoomed-in grid on a narrow nav.\n      fontPx = Math.max(9, Math.min(14, Math.round(cssH * 0.62)));\n      offscreen.font = `${fontPx}px var(--font-mono), \"GeistMono\", ui-monospace, monospace`;\n      const m = offscreen.measureText(\"M\");\n      cellW = Math.max(7, m.width * 1.55);\n      cellH = cssH;\n      cols = Math.max(1, Math.floor(cssW / cellW));\n      cellW = cssW / cols; // even division across the full strip width\n\n      ctx.font = `${fontPx}px var(--font-mono), \"GeistMono\", ui-monospace, monospace`;\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"middle\";\n      computeActiveRange();\n    };\n\n    const drawCell = (col: number, stackDepth: number, extraSeedBucket: number) => {\n      const cx = col * cellW + cellW / 2;\n      const cy = cellH / 2;\n      for (let pass = 0; pass < stackDepth; pass++) {\n        const seed = col * 977 + pass * 131 + extraSeedBucket * 31;\n        const glyph = glyphFor(seed);\n        const dx = jitterFor(seed);\n        const dy = jitterFor(seed + 1);\n        ctx.fillStyle = pass % 2 === 0 ? fg : muted;\n        ctx.globalAlpha = 0.3 - pass * 0.05;\n        ctx.fillText(glyph, cx + dx, cy + dy);\n      }\n      ctx.globalAlpha = 1;\n    };\n\n    const draw = (nowMs: number, sweepTOverride?: number) => {\n      if (cols <= 0) return;\n      ctx.clearRect(0, 0, cssW, cssH);\n      const sweepT = sweepTOverride ?? triangle((nowMs % SWEEP_PERIOD_MS) / SWEEP_PERIOD_MS);\n      const sweepX = sweepT * cssW;\n      // a coarse time bucket, not per-frame, so a re-struck cell holds its\n      // freshly-picked glyph for a beat instead of flickering every 16ms\n      const bucket = Math.floor(nowMs / 420);\n\n      for (let c = 0; c < cols; c++) {\n        const isActive = c >= activeStart && c < activeEnd;\n        let depth = isActive\n          ? ACTIVE_STACK\n          : BASE_STACK_MIN + (hash32(c) % 2 === 0 ? 0 : BASE_STACK_MAX - BASE_STACK_MIN);\n\n        const cellCx = c * cellW + cellW / 2;\n        const dist = Math.abs(cellCx - sweepX);\n        const kernel = Math.exp(-(dist * dist) / (2 * SWEEP_SIGMA * SWEEP_SIGMA));\n        const restruck = kernel > 0.12;\n        if (restruck) depth = Math.min(MAX_STACK, depth + 1);\n\n        drawCell(c, depth, restruck ? bucket : c);\n      }\n    };\n\n    const loop = (ts: number) => {\n      if (disposed || document.hidden) return;\n      draw(ts);\n      raf = requestAnimationFrame(loop);\n    };\n\n    const onVis = () => {\n      if (!document.hidden && !reduced && !disposed) raf = requestAnimationFrame(loop);\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    let resizeTimer: ReturnType<typeof setTimeout> | null = null;\n    const ro = new ResizeObserver(() => {\n      if (resizeTimer) clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(() => {\n        resizeTimer = null;\n        computeGrid();\n        if (reduced) draw(0, STATIC_SWEEP_T);\n      }, 100);\n    });\n    ro.observe(strip);\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced) draw(0, STATIC_SWEEP_T);\n    });\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n\n    document.fonts.ready.then(() => {\n      if (disposed) return;\n      computeGrid();\n      if (reduced) {\n        draw(0, STATIC_SWEEP_T);\n      } else {\n        raf = requestAnimationFrame(loop);\n      }\n    });\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      if (resizeTimer) clearTimeout(resizeTimer);\n      ro.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [links]);\n\n  return (\n    <nav\n      ref={navRef}\n      aria-label=\"Primary\"\n      className={`relative w-full border-b border-border bg-background ${className}`}\n    >\n      <div className=\"mx-auto flex w-full max-w-6xl items-center justify-between px-6 pt-4\">\n        <a\n          href=\"#top\"\n          className=\"shrink-0 rounded-sm font-mono text-[15px] font-semibold tracking-tight text-foreground transition-colors duration-150 hover:text-ns-accent focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ns-accent\"\n        >\n          {wordmark}\n        </a>\n        <ul className=\"flex items-center gap-7\">\n          {links.map((link) => (\n            <li key={link.href}>\n              <a\n                ref={(el) => {\n                  if (el) linkRefs.current.set(link.href, el);\n                  else linkRefs.current.delete(link.href);\n                }}\n                href={link.href}\n                aria-current={link.current ? \"page\" : undefined}\n                className={`rounded-sm text-sm transition-colors duration-150 hover:text-foreground focus-visible:text-foreground focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ns-accent ${\n                  link.current ? \"text-foreground\" : \"text-ns-muted\"\n                }`}\n              >\n                {link.label}\n              </a>\n            </li>\n          ))}\n        </ul>\n      </div>\n\n      {/* decorative overstrike strip — a ruled typed line beneath the real\n          links, aria-hidden. It illustrates which item is current (deepest\n          stack) and that the strip is alive at rest (the re-strike sweep);\n          it never carries navigation semantics of its own. */}\n      <div ref={stripRef} aria-hidden=\"true\" className=\"relative mx-auto mt-2 h-5 w-full max-w-6xl px-6\">\n        <canvas ref={canvasRef} className=\"block h-full w-full\" />\n      </div>\n    </nav>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/nav-overstrike-typewriter.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "nav",
      "typewriter",
      "overstrike",
      "mono",
      "canvas",
      "monochrome",
      "accessibility"
    ],
    "instruction": "<NavOverstrikeTypewriter wordmark? links? className?> renders a real <nav aria-label=\"Primary\"> — a wordmark anchor and a <ul> of real <a> links, each with a normal href, visible focus ring, hover state, and aria-current=\"page\" on whichever link's `current` prop is true. Nothing about navigation, keyboard order, or accessible naming depends on the canvas: a screen-reader or keyboard user gets a plain, working link list. Beneath that link row sits a thin aria-hidden canvas strip, decorative only, drawn as genuine typewriter OVERSTRIKE shading — the technique typewriter artists use to strike several characters into one cell without advancing the carriage so their ink compounds into a mark denser than any single glyph in the machine's character set (an 'M' over an '8' over a ':' to fake a mid-tone). This is explicitly NOT this registry's ~68-component glyph-density-ramp mechanic, where one glyph is selected off an ordered list by luminance: every cell here gets 1-3 *separate* canvas fillText passes, each with independent jitter and alpha, composited on top of each other so coverage genuinely accumulates where strokes overlap — collapsing that into a single denser-glyph lookup would silently become the ramp mechanic and erase the whole reason this component exists. The strip is column-mapped onto the link row above it (measured via getBoundingClientRect, so it tracks any wordmark/link/viewport change): the column band under the item carrying aria-current=\"page\" always strikes at the deepest stack (3 passes), which is literally how the current-page indicator reads — 'the active item is whichever cells carry the deepest overstrike stack' is not a metaphor. A slow ping-pong sweep (~7.2s per return cycle) — a carriage returning and passing back over the line — continuously crosses the strip, giving whichever cells it's over one extra, freshly re-picked overstrike pass that fades by distance from the carriage, so the strip visibly re-types itself at rest with no input; this is unconditional self-animation (not an autoplay-only effect), confirmed distinct at t=0s/2.5s/5s. prefers-reduced-motion freezes the sweep at phase 0.63 rather than t=0 — a point where the carriage sits clear of the active column's own permanent 3-stack, so a single static frame shows both the always-deep active band and a freshly re-struck ambient cell at once, rather than a blank strip or the two effects visually merging. Colors are --foreground and --ns-muted only, read via getComputedStyle(document.documentElement) inside useLayoutEffect (before first paint) and re-read on a MutationObserver watching documentElement's class; --ns-accent appears only as real interaction chrome on the links themselves (hover/focus-visible), never in the ambient overstrike field. Cell size is derived proportionally from the strip's own measured height via an offscreen canvas measureText call made after document.fonts.ready, not a fixed pixel constant, so the mark stays compact and legible rather than turning into a coarse zoomed-in grid on a narrow nav. ResizeObserver (debounced 100ms) recomputes the grid and the active column band on layout change; the render loop pauses on document visibilitychange. Zero dependencies, no external font."
  },
  "type": "registry:ui"
}