{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "not-found-attribute-clash",
  "title": "Not Found Attribute Clash",
  "description": "A 404 built from the ZX Spectrum's real colour-cell attribute clash: each 8x8 hardware cell could carry only one ink/paper colour pair, so image detail finer than the cell budget visibly bled and blocked. This translates that as a monochrome glyph+weight budget (one glyph and one font-weight per cell), so the numerals' own curved edges snap and bleed at the seams, with a rolling scanline that re-decides a handful of rows per frame like attribute RAM racing the beam.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/not-found-attribute-clash/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// AttributeClash404 — the ZX Spectrum's colour-cell attribute clash, ported\n// to a monochrome glyph grid. Real Spectrum hardware split the screen into\n// 8x8-pixel cells that could each carry only ONE ink/paper colour pair; any\n// image content with finer detail than that budget forced the whole cell to\n// snap to a single winner, and the loser's colour bled/blocked wherever it\n// lost. There is no hue here, so the translated budget is ONE glyph+weight\n// pair per cell instead of one colour pair — the artifact is the same shape:\n// wherever the underlying \"404\" glyph shape needs sub-cell resolution (every\n// curved edge of the numerals), the cell is forced to snap fully toward\n// \"ink\" or fully toward \"paper\", and the digits visibly block and bleed at\n// their own edges. This is the joke made mechanical, not a decorative\n// filter — a normal glyph-density field (see background-ascii-plasma) always\n// renders as much detail as its grid allows; this one is built to render\n// LESS than the field wants and show the seam.\n//\n// The \"attribute RAM racing the beam\" line in the brief is translated as a\n// literal rolling scanline: each frame only the handful of rows the beam\n// has newly crossed get their ink/paper decision recomputed from the\n// current field time, so the rows just above the beam are fresh and the\n// rows still waiting below hold a slightly older decision — a rolling seam\n// instead of a single full-field flash (prefers-reduced-motion cares about\n// exactly that distinction). Cells that sit near the 50/50 ink/paper split\n// (the true clash zone, i.e. numeral edges) additionally get a small\n// per-sweep jitter added to their decision threshold, so it is precisely\n// the contested cells — never the whole field — that flicker between\n// states pass to pass.\n//\n// Static once per resize: an offscreen \"404\" raster (read only for alpha,\n// any fill colour) is supersampled into a per-cell ink-fraction buffer.\n// Dynamic every frame: only the field noise driving glyph choice inside\n// each already-decided cell, and the rolling beam that gates when a cell's\n// ink/paper decision itself gets re-evaluated. Canvas 2D, direct-DOM rAF,\n// no React state on the hot path.\n// ---------------------------------------------------------------------------\n\nconst SUB = 3; // ink-mask supersample factor per cell, each axis (9 samples/cell)\nconst MIN_CELL_PX = 8;\nconst MAX_CELL_PX = 64;\nconst CLASH_ZONE = 0.62; // clash = 1 - |2*inkFrac-1|; band around the 50/50 edge\nconst JITTER_AMOUNT = 0.9; // decision-threshold jitter applied only inside CLASH_ZONE\nconst INK_RAMP = \"#%@\";\nconst PAPER_RAMP = \" .:-\";\nconst CLASH_RAMP = \"*+?\";\nconst STATIC_T = 2.35; // reduced-motion freeze time — see comment at its use below\n\n// small, cheap 2-octave field over grid-index coordinates (not pixels) —\n// only needed for glyph *choice* within an already-decided cell and for the\n// clash-zone jitter, never for the ink/paper decision itself, which comes\n// solely from the static \"404\" raster.\nfunction fieldValue(gx: number, gy: number, t: number): number {\n  const a =\n    Math.sin(gx * 0.5 + t * 0.6) +\n    Math.sin(gy * 0.5 - t * 0.5) +\n    Math.sin((gx + gy) * 0.35 + t * 0.33);\n  const b = Math.sin(gx * 1.3 - t * 1.1) + Math.sin(gy * 1.1 + t * 0.9);\n  const v = a * 0.6 + b * 0.4;\n  return v / 6 + 0.5; // rough-normalize to ~0..1\n}\n\nexport interface AttributeClash404Props {\n  /** target glyph-cell count across the container's smaller dimension */\n  cellsAcrossMinDim?: number;\n  /** ms for one full top-to-bottom beam pass */\n  sweepMs?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function AttributeClash404({\n  cellsAcrossMinDim = 26,\n  sweepMs = 1100,\n  className = \"\",\n}: AttributeClash404Props) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    let fg = \"\";\n    let muted = \"\";\n    let cellW = 0;\n    let cellH = 0;\n    let cols = 0;\n    let rows = 0;\n    let dpr = 1;\n    let fontFamily = \"monospace\";\n    let fontBold = \"\";\n    let fontReg = \"\";\n\n    // static per resize/fonts-ready: the \"404\" glyph's own required detail\n    let inkFraction = new Float32Array(0);\n    // dynamic, gated by the beam sweep below\n    let charBuf = new Uint8Array(0); // char code per cell\n    let alphaBuf = new Float32Array(0);\n    let boldBuf = new Uint8Array(0);\n\n    let sized = false;\n    let ready = false;\n    let disposed = false;\n    let raf = 0;\n    let last = 0;\n    let t = 0;\n    let beamAcc = 0; // unwrapped cumulative row position of the beam\n    let visible = true;\n\n    const readTokens = () => {\n      const cs = getComputedStyle(document.documentElement);\n      fg = cs.getPropertyValue(\"--foreground\").trim();\n      muted = cs.getPropertyValue(\"--ns-muted\").trim();\n    };\n\n    const buildInkMask = () => {\n      if (cols < 1 || rows < 1) return;\n      const off = document.createElement(\"canvas\");\n      off.width = cols * SUB;\n      off.height = rows * SUB;\n      const octx = off.getContext(\"2d\");\n      if (!octx) return;\n      octx.clearRect(0, 0, off.width, off.height);\n      octx.fillStyle = \"#000\"; // alpha channel only is read below, fill colour is irrelevant\n      octx.textAlign = \"center\";\n      octx.textBaseline = \"middle\";\n\n      // fit \"404\" to the offscreen raster with a real margin on each side —\n      // never let the numeral shape run edge-to-edge, and never shrink it\n      // below legibility either\n      let fontPx = off.height * 0.72;\n      octx.font = `900 ${fontPx}px ${fontFamily}`;\n      const maxW = off.width * 0.86;\n      const w0 = octx.measureText(\"404\").width;\n      if (w0 > maxW) fontPx *= maxW / w0;\n      octx.font = `900 ${fontPx}px ${fontFamily}`;\n      octx.fillText(\"404\", off.width / 2, off.height / 2);\n\n      const data = octx.getImageData(0, 0, off.width, off.height).data;\n      const frac = new Float32Array(cols * rows);\n      for (let gy = 0; gy < rows; gy++) {\n        for (let gx = 0; gx < cols; gx++) {\n          let sum = 0;\n          for (let sy = 0; sy < SUB; sy++) {\n            const py = gy * SUB + sy;\n            for (let sx = 0; sx < SUB; sx++) {\n              const px = gx * SUB + sx;\n              sum += data[(py * off.width + px) * 4 + 3]; // alpha channel\n            }\n          }\n          frac[gy * cols + gx] = sum / (SUB * SUB * 255);\n        }\n      }\n      inkFraction = frac;\n    };\n\n    const measureCell = () => {\n      const off = document.createElement(\"canvas\");\n      const octx = off.getContext(\"2d\");\n      if (!octx) return 1;\n      octx.font = `700 ${cellH}px ${fontFamily}`;\n      return Math.max(4, octx.measureText(\"MM\").width / 2);\n    };\n\n    const resize = () => {\n      const { width, height } = canvas.getBoundingClientRect();\n      if (width < 2 || height < 2) {\n        sized = false;\n        return;\n      }\n      const isCard = !!canvas.closest(\"[data-autoplay-root]\");\n      dpr = isCard\n        ? Math.min(0.6, window.devicePixelRatio || 1)\n        : Math.min(window.devicePixelRatio || 1, 1.5);\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(height * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n      fontFamily = getComputedStyle(canvas).fontFamily;\n      const minDim = Math.min(width, height);\n      const target = minDim / cellsAcrossMinDim;\n      cellH = Math.max(MIN_CELL_PX, Math.min(MAX_CELL_PX, target));\n      cellW = measureCell();\n      fontBold = `700 ${cellH}px ${fontFamily}`;\n      fontReg = `400 ${cellH}px ${fontFamily}`;\n\n      cols = Math.max(1, Math.ceil(width / cellW));\n      rows = Math.max(1, Math.ceil(height / cellH));\n      const n = cols * rows;\n      charBuf = new Uint8Array(n);\n      alphaBuf = new Float32Array(n);\n      boldBuf = new Uint8Array(n);\n      buildInkMask();\n      beamAcc = 0;\n      sized = true;\n    };\n\n    // recompute one row's ink/paper decision + glyph choice at time `tt` —\n    // this is the ONLY place a cell's attribute pair is re-decided; the\n    // beam sweep below gates how often that happens per row\n    const updateRow = (gy: number, tt: number) => {\n      const base = gy * cols;\n      for (let gx = 0; gx < cols; gx++) {\n        const i = base + gx;\n        const inkFrac = inkFraction[i] ?? 0;\n        const clash = 1 - Math.abs(inkFrac * 2 - 1);\n        const field = fieldValue(gx, gy, tt);\n        const jitter = clash > CLASH_ZONE ? (field - 0.5) * JITTER_AMOUNT : 0;\n        const bold = inkFrac + jitter >= 0.5;\n        let ramp: string;\n        if (clash > CLASH_ZONE) ramp = CLASH_RAMP;\n        else if (bold) ramp = INK_RAMP;\n        else ramp = PAPER_RAMP;\n        const ci = Math.min(\n          ramp.length - 1,\n          Math.floor(field * ramp.length)\n        );\n        charBuf[i] = ramp.charCodeAt(ci);\n        boldBuf[i] = bold ? 1 : 0;\n        alphaBuf[i] = bold ? 0.55 + 0.45 * field : 0.12 + 0.28 * field;\n      }\n    };\n\n    const drawAll = () => {\n      if (!sized) return;\n      const w = cols * cellW;\n      const h = rows * cellH;\n      ctx.clearRect(0, 0, w, h);\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"middle\";\n      ctx.fillStyle = fg;\n      let i = 0;\n      for (let gy = 0; gy < rows; gy++) {\n        for (let gx = 0; gx < cols; gx++, i++) {\n          const a = alphaBuf[i];\n          if (a <= 0.01) continue;\n          ctx.globalAlpha = a;\n          ctx.font = boldBuf[i] ? fontBold : fontReg;\n          ctx.fillText(\n            String.fromCharCode(charBuf[i]),\n            gx * cellW + cellW / 2,\n            gy * cellH + cellH / 2\n          );\n        }\n      }\n      ctx.globalAlpha = 1;\n\n      if (!reduced) {\n        // faint racing-beam line — structural readout of the mechanism,\n        // never accent (this is ambient, not interaction chrome)\n        const beamRow = beamAcc % rows;\n        ctx.strokeStyle = muted;\n        ctx.globalAlpha = 0.3;\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        const y = beamRow * cellH;\n        ctx.moveTo(0, y);\n        ctx.lineTo(w, y);\n        ctx.stroke();\n        ctx.globalAlpha = 1;\n      }\n    };\n\n    const loop = (now: number) => {\n      const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;\n      last = now;\n      t += dt;\n\n      const rowsPerSec = rows / (sweepMs / 1000);\n      const prevFloor = Math.floor(beamAcc);\n      beamAcc += rowsPerSec * dt;\n      const newFloor = Math.floor(beamAcc);\n      const steps = Math.min(newFloor - prevFloor, rows);\n      for (let k = 1; k <= steps; k++) {\n        updateRow((prevFloor + k) % rows, t);\n      }\n      if (beamAcc >= rows * 2) beamAcc -= rows; // keep the accumulator bounded\n\n      drawAll();\n      if (visible && !document.hidden) raf = requestAnimationFrame(loop);\n    };\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        resize();\n        if (ready) {\n          if (reduced) {\n            for (let gy = 0; gy < rows; gy++) updateRow(gy, STATIC_T);\n            drawAll();\n          } else {\n            for (let gy = 0; gy < rows; gy++) updateRow(gy, t);\n          }\n        }\n      }, 150);\n    });\n    ro.observe(canvas);\n\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        visible = entry.isIntersecting;\n        if (visible && !reduced && ready && !document.hidden) {\n          last = 0;\n          raf = requestAnimationFrame(loop);\n        }\n      },\n      { threshold: 0 }\n    );\n    io.observe(canvas);\n\n    const onVis = () => {\n      if (!document.hidden && !reduced && ready && visible) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced && ready) drawAll();\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    // ink mask depends on the resolved mono/sans font metrics, so wait for\n    // the webfont before the first raster + measurement pass\n    document.fonts.ready.then(() => {\n      if (disposed) return;\n      readTokens();\n      resize();\n      ready = true;\n      if (reduced) {\n        // Freeze on a full completed sweep (every row decided once) at a\n        // fixed, non-zero field time rather than t=0: the ink/paper split\n        // itself is static (it comes only from the \"404\" raster), but the\n        // glyph choice inside each already-decided cell and the jitter\n        // that marks the clash-zone cells both depend on field time, and\n        // t=0 is a degenerate phase where several of the field's summed\n        // sine terms start at exactly zero. STATIC_T=2.35 lands mid-cycle\n        // so clash-zone cells show a spread of ink/paper snap decisions\n        // instead of all agreeing — the single frame with the most visible\n        // block/bleed structure. No rAF, no beam sweep, no pointer.\n        for (let gy = 0; gy < rows; gy++) updateRow(gy, STATIC_T);\n        drawAll();\n      } else {\n        for (let gy = 0; gy < rows; gy++) updateRow(gy, 0);\n        raf = requestAnimationFrame(loop);\n      }\n    });\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      if (resizeTimer) clearTimeout(resizeTimer);\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [cellsAcrossMinDim, sweepMs]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      aria-hidden\n      className={`block h-full w-full font-mono ${className}`}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/not-found-attribute-clash.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)"
    },
    "light": {
      "ns-muted": "#4d4d4d"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "loud",
    "tags": [
      "404",
      "ascii",
      "canvas",
      "hardware",
      "retro",
      "page"
    ],
    "instruction": "Build a full 404 background whose mechanic is sourced from real ZX Spectrum hardware, not invented: the machine's screen was split into 8x8-pixel cells that could each carry only ONE ink/paper colour pair, so any image content needing finer detail than that per-cell budget forced the whole cell to snap toward one colour, and the loser's detail bled/blocked wherever it lost — the well-documented 'colour clash' artifact. THIS IS A MONOCHROME ADAPTATION, NOT A FIDELITY CLAIM: state plainly that the original clash is a COLOUR artifact (one ink/paper pair per cell) and this translation is a glyph/weight-budget artifact (one glyph character + one font-weight per cell) — there is no hue anywhere in this component. Build it as: (1) once per resize, after document.fonts.ready, rasterize '404' at 900-weight onto a small offscreen canvas supersampled 3x per output cell, fit to the offscreen raster with real margin on each side (never edge-to-edge, never shrunk below legibility), and read its alpha channel into a per-cell ink-fraction float (0..1) — this fraction is the ONLY thing that decides ink vs paper per cell, and it never changes with time, only with resize/refit; (2) every frame, a rolling beam sweeps top to bottom over roughly sweepMs (default 1100ms) and only the handful of rows it newly crosses that frame get their ink/paper DECISION re-evaluated — most of the grid is deliberately left showing its previous decision, which is the literal translation of 'attribute RAM racing the beam' as a rolling seam rather than a full-field flash; (3) for cells whose ink-fraction sits within a clash band around 50/50 (the numerals' own edges), a small per-sweep jitter is added to the decision threshold from a cheap two-octave grid-index field, so specifically those contested cells flicker between full-ink and full-paper pass to pass — cells far from any edge never flicker, because they were never in a real budget dispute on the source hardware either. Each cell renders exactly one character (dense glyphs '#%@' for ink cells, sparse ' .:-' for paper cells, a third distinct ramp '*+?' for cells currently inside the clash band) at exactly one font-weight (700 for ink, 400 for paper) and one alpha derived from the same field value — the glyph+weight pair literalizes the one-attribute-per-cell budget. Colour is --foreground only, alpha-only, read via getComputedStyle(document.documentElement) at mount with a MutationObserver on documentElement's class for theme flips; a faint --ns-muted (never --ns-accent — this is ambient structure, not interaction chrome) horizontal line traces the beam's current row as a structural readout of the mechanism. Cell size derives from Math.min(width,height)/cellsAcrossMinDim (default 26), clamped 8-64px, so the component reads as a recognizable block grid at both small registry-card scale and full-bleed rather than either pixel-soup or one giant cell. prefers-reduced-motion freezes on one completed sweep (every row decided once) at a fixed non-zero field time chosen specifically because t=0 is a degenerate phase for the summed field terms — the frozen frame must show the clash-zone cells already disagreeing with each other, not a coincidentally-uniform snap; no rAF, no beam sweep runs after that. The canvas is aria-hidden decoration only: the demo composes a real h1 reading '404 — PAGE NOT FOUND', a short paragraph, and a real focusable 'Take me home' link in normal DOM with correct focus order, entirely independent of the glyph art. Props: cellsAcrossMinDim (default 26), sweepMs (default 1100), className."
  },
  "type": "registry:ui"
}