{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "not-found-postmark",
  "title": "Not Found Postmark",
  "description": "A 404 rendered as returned mail: the failed URL addressed on an envelope that slides in, its routing history stamped hop by hop (edge, origin, router) as sequential ink postmarks, the last hop knocked down with a squash-and-settle NO SUCH ADDRESS hand-stamp and a 1px shake, then a forwarding-address label (search, home, recent pages) peels on as the real recovery page.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/not-found-postmark/component.tsx",
      "content": "\"use client\";\n\nimport { Fragment, useEffect, useId, useMemo, useState, type CSSProperties } from \"react\";\n\n// A 404 as returned mail, not an illustration of one. The \"art\" is a single\n// envelope: a real, focusable recovery `<nav>` is the actual 404 page, and\n// everything above it — the address block, the routing-history postmarks,\n// the final NO SUCH ADDRESS hand-stamp — is aria-hidden decoration layered\n// over a plain-text summary that carries the same information for anyone\n// not watching it happen. The postmarks land one at a time (250ms apart,\n// scale(1.3->1) with a small per-hop rotation scatter, an SVG feTurbulence\n// + feDisplacementMap filter roughing their ink), the last one lands harder\n// (a two-frame squash) and knocks the whole card with a 1px shake, and the\n// forwarding-address label (search, home, recent pages) peels on with a\n// slight skew right after. Reduced motion renders the whole thing already\n// landed: no stamps dropping in, no peel, no shake.\n\nexport interface DeadLetterHop {\n  /** Routing stop label, e.g. \"Edge\". */\n  label: string;\n  /** Optional small detail under the label, e.g. a node id. */\n  detail?: string;\n}\n\nexport interface DeadLetterLink {\n  /** Visible, accessible label. */\n  label: string;\n  /** Destination. Omit to render a real, focusable link that does nothing. */\n  href?: string;\n}\n\nexport interface DeadLetterProps {\n  /** The URL that 404'd. Defaults to the current location. */\n  path?: string;\n  /** Routing stops attempted, oldest first. Default: Edge / Origin / Router. */\n  hops?: DeadLetterHop[];\n  /** Recovery destinations besides Home, in source order. */\n  recentPages?: DeadLetterLink[];\n  /** Home link destination. Default \"/\". */\n  homeHref?: string;\n  /** Called with the trimmed query on submit. Omit for a real, inert form. */\n  onSearch?: (query: string) => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst DEFAULT_HOPS: DeadLetterHop[] = [\n  { label: \"Edge\", detail: \"edge-04\" },\n  { label: \"Origin\", detail: \"origin-gw\" },\n  { label: \"Router\", detail: \"svc-router\" },\n];\n\nconst DEFAULT_RECENT: DeadLetterLink[] = [\n  { label: \"Dashboard\" },\n  { label: \"Settings\" },\n  { label: \"Docs\" },\n];\n\nfunction hash(s: string): number {\n  let h = 0;\n  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;\n  return h;\n}\n\n// Deterministic scatter in [-6, 6] degrees from a label, so re-renders don't\n// jitter and every hop still reads as hand-stamped rather than uniform.\nfunction scatter(label: string, index: number): number {\n  return (Math.abs(hash(label + index)) % 13) - 6;\n}\n\nconst CSS = `\n@keyframes ns-dl-slide{from{opacity:0;transform:translateX(-14px)}to{opacity:1;transform:translateX(0)}}\n@keyframes ns-dl-stamp{\n  0%{opacity:0;transform:scale(1.3) rotate(var(--ns-dl-rot,0deg))}\n  100%{opacity:1;transform:scale(1) rotate(var(--ns-dl-rot,0deg))}\n}\n@keyframes ns-dl-final{\n  0%{opacity:0;transform:translateY(-10px) scale(1.3) rotate(var(--ns-dl-rot,-8deg))}\n  55%{opacity:1;transform:translateY(0) scale(1.08,0.88) rotate(var(--ns-dl-rot,-8deg))}\n  78%{transform:translateY(0) scale(0.96,1.05) rotate(var(--ns-dl-rot,-8deg))}\n  100%{opacity:1;transform:translateY(0) scale(1) rotate(var(--ns-dl-rot,-8deg))}\n}\n@keyframes ns-dl-shake{\n  0%,100%{transform:translateX(0)}\n  30%{transform:translateX(-1px)}\n  60%{transform:translateX(1px)}\n  85%{transform:translateX(-0.5px)}\n}\n@keyframes ns-dl-peel{\n  from{opacity:0;transform:translateY(10px) skewY(-3.5deg) scale(0.98)}\n  to{opacity:1;transform:translateY(0) skewY(0deg) scale(1)}\n}\n`;\n\nexport function DeadLetter({\n  path: pathProp,\n  hops = DEFAULT_HOPS,\n  recentPages = DEFAULT_RECENT,\n  homeHref = \"/\",\n  onSearch,\n  className = \"\",\n}: DeadLetterProps) {\n  const uid = useId();\n  const grainId = `ns-dl-grain-${uid}`;\n  const [reduced, setReduced] = useState(false);\n  const [autoPath, setAutoPath] = useState<string | null>(null);\n  const [query, setQuery] = useState(\"\");\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const onChange = () => setReduced(mq.matches);\n    onChange();\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  useEffect(() => {\n    document.title = \"404 — page not found\";\n  }, []);\n\n  useEffect(() => {\n    if (pathProp) return;\n    setAutoPath(window.location.pathname + window.location.search);\n  }, [pathProp]);\n\n  const path = pathProp ?? autoPath ?? \"/unknown\";\n\n  // --- timing --------------------------------------------------------------\n  // Full sequence must be settled with real margin before the verify gate's\n  // 1000ms default screenshot — the last two beats (a squash, a skew-peel)\n  // read as visibly broken if caught mid-motion, unlike a plain fade. Hops\n  // stamp the required 250ms apart starting near-immediately; the final\n  // stamp lands right after the last hop; the forwarding label starts\n  // peeling the moment the stamp starts landing rather than waiting for it\n  // to finish. Everything at rest by ~870ms, ~130ms of margin.\n  const slideDur = 150;\n  const hopStagger = 250;\n  const hopDelayBase = 40;\n  const hopDur = 150;\n  const lastHopDelay = hopDelayBase + (hops.length - 1) * hopStagger;\n  const finalDelay = lastHopDelay + hopDur + 20;\n  const finalDur = 140;\n  const peelDelay = finalDelay;\n  const peelDur = 160;\n\n  // Slide entrance and the late shake live on the SAME element, so they must\n  // share one `animation` shorthand as a comma list — two style objects spread\n  // together would overwrite each other's `animation`, leaving the entrance\n  // (and its fill-mode opacity) never applied and the envelope stuck invisible.\n  const envelopeStyle: CSSProperties | undefined = reduced\n    ? undefined\n    : {\n        opacity: 0,\n        animation: `ns-dl-slide ${slideDur}ms cubic-bezier(.16,1,.3,1) both, ns-dl-shake 160ms linear ${finalDelay}ms both`,\n      };\n\n  // SVG elements transform around their border box by default in some\n  // browsers, not their own visual center — explicit fill-box/center keeps\n  // the scale+rotate pivoting in place rather than swinging from a corner.\n  const hopStyle = (i: number): CSSProperties =>\n    (reduced\n      ? { transformBox: \"fill-box\", transformOrigin: \"center\", \"--ns-dl-rot\": `${scatter(hops[i]?.label ?? \"\", i)}deg` }\n      : {\n          opacity: 0,\n          transformBox: \"fill-box\",\n          transformOrigin: \"center\",\n          \"--ns-dl-rot\": `${scatter(hops[i]?.label ?? \"\", i)}deg`,\n          animation: `ns-dl-stamp ${hopDur}ms cubic-bezier(.16,1,.3,1) ${hopDelayBase + i * hopStagger}ms both`,\n        }) as unknown as CSSProperties;\n\n  const finalStyle: CSSProperties = (reduced\n    ? { transformBox: \"fill-box\", transformOrigin: \"center\", \"--ns-dl-rot\": \"-8deg\" }\n    : {\n        opacity: 0,\n        transformBox: \"fill-box\",\n        transformOrigin: \"center\",\n        \"--ns-dl-rot\": \"-8deg\",\n        animation: `ns-dl-final ${finalDur}ms cubic-bezier(.16,1,.3,1) ${finalDelay}ms both`,\n      }) as unknown as CSSProperties;\n\n  const peelStyle: CSSProperties | undefined = reduced\n    ? undefined\n    : { opacity: 0, animation: `ns-dl-peel ${peelDur}ms cubic-bezier(.16,1,.3,1) ${peelDelay}ms both` };\n\n  const hopSummary = useMemo(() => hops.map((h) => h.label).join(\" → \"), [hops]);\n\n  return (\n    <div className={[\"flex min-h-screen flex-col items-center bg-background px-6 py-10 md:px-10\", className].join(\" \")}>\n      <style>{CSS}</style>\n\n      {/* shared grain filter — geometric-only turbulence, no baked color */}\n      <svg width={0} height={0} aria-hidden style={{ position: \"absolute\" }}>\n        <defs>\n          <filter id={grainId} x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\">\n            <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.85\" numOctaves={2} seed={3} result=\"ns-dl-noise\" />\n            <feDisplacementMap in=\"SourceGraphic\" in2=\"ns-dl-noise\" scale={1.6} xChannelSelector=\"R\" yChannelSelector=\"G\" />\n          </filter>\n        </defs>\n      </svg>\n\n      <div className=\"w-full max-w-2xl\">\n        <span className=\"font-mono text-xs tracking-[0.3em] text-ns-muted\">UNDELIVERABLE MAIL</span>\n        <h1 className=\"mt-1 text-2xl font-semibold tracking-tight text-foreground md:text-3xl\">\n          404 — page not found\n        </h1>\n        <p className=\"mt-3 max-w-[60ch] font-mono text-xs leading-relaxed text-ns-muted\">\n          Route attempted: <span className=\"break-all text-foreground\">{path}</span> — failed at every\n          hop ({hopSummary}) with no such address.\n        </p>\n\n        {/* theatrical envelope — pure decoration, zero focusable elements */}\n        <div\n          aria-hidden=\"true\"\n          className=\"mt-6 rounded-md border border-border bg-background p-5 md:p-7\"\n          style={envelopeStyle}\n        >\n          <div className=\"border-b border-border pb-4\">\n            <span className=\"font-mono text-[10px] uppercase tracking-widest text-ns-muted\">To</span>\n            <div className=\"mt-1 break-all font-mono text-sm text-foreground md:text-base\">{path}</div>\n          </div>\n\n          <div className=\"relative mt-6 flex items-end gap-2 sm:gap-4\">\n            {hops.map((hop, i) => (\n              <Fragment key={`${hop.label}-${i}`}>\n                {i > 0 && (\n                  <span\n                    className=\"mb-4 hidden h-0 flex-1 border-t border-dashed border-border sm:block\"\n                    aria-hidden\n                  />\n                )}\n                <div className=\"flex flex-col items-center gap-1\">\n                  <svg width={40} height={40} viewBox=\"0 0 40 40\" style={hopStyle(i)} className=\"overflow-visible\">\n                    <g filter={`url(#${grainId})`}>\n                      <circle cx={20} cy={20} r={16.5} fill=\"none\" stroke=\"var(--foreground)\" strokeWidth={1.3} />\n                      <circle cx={20} cy={20} r={11} fill=\"none\" stroke=\"var(--border)\" strokeWidth={1} />\n                      <path\n                        d=\"M4,15 Q11,10 18,15 T32,15\"\n                        fill=\"none\"\n                        stroke=\"var(--ns-muted)\"\n                        strokeWidth={1}\n                        strokeLinecap=\"round\"\n                      />\n                      <path\n                        d=\"M4,25 Q11,20 18,25 T32,25\"\n                        fill=\"none\"\n                        stroke=\"var(--ns-muted)\"\n                        strokeWidth={1}\n                        strokeLinecap=\"round\"\n                      />\n                    </g>\n                  </svg>\n                  <span className=\"whitespace-nowrap font-mono text-[9px] uppercase tracking-wide text-ns-muted\">\n                    {hop.label}\n                  </span>\n                  {hop.detail && (\n                    <span className=\"whitespace-nowrap font-mono text-[8px] text-ns-muted/70\">{hop.detail}</span>\n                  )}\n                </div>\n              </Fragment>\n            ))}\n\n            {/* the final hop lands here — anchored to the row's own right\n                edge (already inside the card's padding) rather than centered\n                on the last icon, so the stamp can never hang off the card */}\n            <svg\n              width={128}\n              height={44}\n              viewBox=\"0 0 128 44\"\n              className=\"pointer-events-none absolute -top-7 right-0 overflow-visible\"\n              style={finalStyle}\n            >\n              <g filter={`url(#${grainId})`}>\n                <rect x={2} y={2} width={124} height={40} rx={4} fill=\"none\" stroke=\"var(--foreground)\" strokeWidth={2} />\n                <rect x={6} y={6} width={116} height={32} rx={3} fill=\"none\" stroke=\"var(--foreground)\" strokeWidth={1} />\n                <text\n                  x={64}\n                  y={19}\n                  textAnchor=\"middle\"\n                  className=\"font-mono\"\n                  fontWeight={700}\n                  fontSize={9.5}\n                  letterSpacing={0.5}\n                  fill=\"var(--foreground)\"\n                >\n                  NO SUCH\n                </text>\n                <text\n                  x={64}\n                  y={31}\n                  textAnchor=\"middle\"\n                  className=\"font-mono\"\n                  fontWeight={700}\n                  fontSize={9.5}\n                  letterSpacing={0.5}\n                  fill=\"var(--foreground)\"\n                >\n                  ADDRESS\n                </text>\n              </g>\n            </svg>\n          </div>\n        </div>\n\n        {/* forwarding-address label — the real 404 page, first in tab order */}\n        <nav\n          aria-label=\"Recovery\"\n          className=\"mt-6 rounded-md border border-border bg-background p-5 md:p-6\"\n          style={peelStyle}\n        >\n          <span className=\"font-mono text-[10px] uppercase tracking-widest text-ns-muted\">Forwarding address</span>\n\n          <form\n            role=\"search\"\n            className=\"mt-3 flex items-center gap-2\"\n            onSubmit={(e) => {\n              e.preventDefault();\n              onSearch?.(query.trim());\n            }}\n          >\n            <label htmlFor={`${uid}-search`} className=\"sr-only\">\n              Search\n            </label>\n            <input\n              id={`${uid}-search`}\n              type=\"search\"\n              value={query}\n              onChange={(e) => setQuery(e.target.value)}\n              placeholder=\"Search\"\n              className=\"h-9 flex-1 rounded-sm border border-border bg-background px-3 font-mono text-sm text-foreground placeholder:text-ns-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n            />\n            <button\n              type=\"submit\"\n              className=\"h-9 rounded-sm border border-border px-3 font-mono text-xs text-foreground transition-colors hover:border-foreground/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n            >\n              Search\n            </button>\n          </form>\n\n          <ul className=\"mt-4 flex flex-wrap items-center gap-x-6 gap-y-2\">\n            <li>\n              <a\n                href={homeHref}\n                className=\"rounded-sm px-1 py-0.5 font-mono text-xs text-foreground underline decoration-border underline-offset-4 transition-colors hover:decoration-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n              >\n                Home\n              </a>\n            </li>\n            {recentPages.map((page) => (\n              <li key={page.label}>\n                <a\n                  href={page.href || \"#\"}\n                  onClick={page.href ? undefined : (e) => e.preventDefault()}\n                  className=\"rounded-sm px-1 py-0.5 font-mono text-xs text-ns-muted underline decoration-border underline-offset-4 transition-colors hover:text-foreground hover:decoration-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n                >\n                  {page.label}\n                </a>\n              </li>\n            ))}\n          </ul>\n        </nav>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/not-found-postmark.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": "loud",
    "tags": [
      "404",
      "error-page",
      "svg",
      "postal",
      "sequence",
      "recovery",
      "page"
    ],
    "instruction": "Build a full 404 page whose graphic is a single piece of returned mail, not an illustration of an error. An envelope card (--surface fill, --border rules) slides in from the left (~150ms, ease-out-expo) holding a Geist Mono 'To:' address block with the real bad path (prop `path`, defaulting to `window.location.pathname + search`). Below the address, a row of routing-history postmarks — one per entry in prop `hops` (default Edge / Origin / Router, each `{ label, detail? }`), joined by a dashed connector rule — stamp in sequentially 250ms apart: each is an inline SVG cancellation mark (concentric circles plus two wavy cancel bars) that animates scale(1.3→1) with a small per-hop rotation scatter (deterministic from the label, ±6deg) on an ease-out-expo curve, and every postmark plus the final stamp shares one SVG `<filter>` (feTurbulence fractalNoise + feDisplacementMap against SourceGraphic, geometry-only — no baked color) so the ink reads grained rather than vector-clean. The last hop is where the trail dies: a larger NO SUCH ADDRESS rectangular hand-stamp (double-rule border, rotated -8deg) drops onto it right after that hop lands, playing a two-frame squash (scaleY down then a slight overshoot before settling to 1) while the whole envelope card gets a 1px horizontal shake — the one moment color escalates from postmark to verdict. A forwarding-address label card peels on immediately after (slight skewY + translateY easing to flat, ease-out-expo) and IS the real 404 page: a labeled search input plus a Home link plus `recentPages` links, all real, focusable, in source order, sitting entirely outside the aria-hidden envelope so nothing in the theatrical region is ever reachable by keyboard while invisible to assistive tech. The document title and an always-present h1 read exactly '404 — page not found' on mount, and a plain-text mono line above the envelope states the same story in words ('Route attempted: <path> — failed at every hop (Edge → Origin → Router) with no such address') so the failure is legible with zero motion. Reduced motion renders every postmark, the final stamp, and the forwarding label already landed in their resting transforms — no drop-in, no squash, no shake, no peel, and document.title/h1 are unaffected either way."
  },
  "type": "registry:ui"
}