{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "divider-teletext-mosaic",
  "title": "Divider Teletext Mosaic",
  "description": "A full-width section divider rendered as a real teletext / videotex ALPHAMOSAIC graphics band: each character cell is a 6-bit code across a 2-wide x 3-tall sub-cell 'sextant' grid, the actual broadcast alphamosaic addressing (distinct from this registry's 2x2 quadrant block dividers and 2x4 braille-dot components), painted row by row on a fixed cadence the way a real teletext page arrives off the broadcast signal.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/divider-teletext-mosaic/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// DividerTeletextMosaic — a full-width section divider rendered as a\n// teletext / videotex ALPHAMOSAIC graphics band.\n//\n// Real broadcast teletext (World System Teletext / CEEFAX-era) never had a\n// per-pixel framebuffer: each character cell is a 6-bit code selecting one\n// of 64 patterns across a fixed 2-wide x 3-tall sub-cell grid (\"sextant\"\n// addressing — top-left, top-right, mid-left, mid-right, bottom-left,\n// bottom-right). That is a genuinely different sub-cell grid from anything\n// else in this registry: block-element dividers quantize 2x2 quadrants,\n// braille components address a 2x4 dot grid — this is 2x3, the real\n// alphamosaic grid a teletext decoder actually drew, contiguous (no gaps\n// between sub-cells, matching the \"contiguous mosaic\" mode broadcasters used\n// for photo-like graphics, as opposed to \"separated mosaic\" which insets a\n// gap per block).\n//\n// Sub-cells are rendered as filled canvas rects, not the Unicode sextant\n// block range (U+1FB00-U+1FB3B, \"Symbols for Legacy Computing\") — this\n// repo's mono stack (GeistMono / GeistMono Fallback / ui-monospace) does not\n// carry that range, and shipping unverified glyph coverage risks silent\n// tofu. Drawing the six sub-cells directly is also what a real teletext\n// decoder chip did: it never asked a font for a glyph, it lit sub-cell\n// segments off the 6-bit code.\n//\n// ALIVE AT REST: a real teletext page doesn't arrive all at once — rows\n// paint in one at a time as they're received off the broadcast VBI signal.\n// This band reproduces that: a write cursor sweeps top-to-bottom on a fixed\n// cadence, re-sampling a slow generative field into a fresh 6-bit pattern\n// per cell each time a row's turn comes, then holds that content until the\n// next pass. A short sync pause follows a full pass before the page\n// \"refreshes\" again, mirroring the real per-page transmission cycle.\n// ---------------------------------------------------------------------------\n\nconst ROWS = 6; // character rows in the band\nconst ROW_INTERVAL_MS = 420; // time between successive row writes (\"as received\")\nconst PAUSE_MS = 600; // sync pause between a completed pass and the next\nconst FLASH_MS = 220; // dwell of the just-written row's highlight hairline\nconst DITHER_STRENGTH = 0.32; // ordered-dither spread around the 0.5 threshold\n\n// 2x3 ordered-dither bias table, one entry per sub-cell index\n// (subRow*2 + subCol), scattered rather than linear so the threshold band\n// doesn't read as a simple diagonal — the same discipline real videotex\n// picture-mosaic dithering used to fake tone out of a binary grid.\nconst DITHER_ORDER = [0, 3, 4, 1, 2, 5];\nconst DITHER_BIAS = DITHER_ORDER.map((k) => (k + 0.5) / 6 - 0.5);\n\nfunction fieldValue(sx: number, sy: number, t: number) {\n  // three non-commensurate traveling components summed and normalized —\n  // enough structure to read as a low-res broadcast picture once\n  // sextant-quantized, and slow enough that a 420ms row-write cadence\n  // samples visibly different content on every pass.\n  const f =\n    Math.sin(sx * 0.35 + t * 0.9) +\n    0.6 * Math.sin(sy * 0.6 - sx * 0.12 + t * 0.53) +\n    0.4 * Math.sin((sx + sy) * 0.22 + t * 0.31);\n  const v = (f + 2) / 4; // amplitude sum is +/-2 -> 0..1\n  return Math.pow(Math.min(1, Math.max(0, v)), 1.2);\n}\n\nfunction subCellPattern(colGlobal: number, row: number, t: number) {\n  // returns a 6-bit mask, bit i = sub-cell i (0=top-left ... 5=bottom-right)\n  let bits = 0;\n  for (let subRow = 0; subRow < 3; subRow++) {\n    for (let subCol = 0; subCol < 2; subCol++) {\n      const idx = subRow * 2 + subCol;\n      const sx = colGlobal * 2 + subCol;\n      const sy = row * 3 + subRow;\n      const v = fieldValue(sx, sy, t);\n      const threshold = 0.5 + DITHER_BIAS[idx] * DITHER_STRENGTH;\n      if (v > threshold) bits |= 1 << idx;\n    }\n  }\n  return bits;\n}\n\nexport interface DividerTeletextMosaicProps {\n  /** cell size in px; row height. Cell width is derived from monospace metrics. Default 14. */\n  cellSize?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function DividerTeletextMosaic({\n  cellSize = 14,\n  className = \"\",\n}: DividerTeletextMosaicProps) {\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(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let fg = \"currentColor\";\n    let cellW = cellSize * 0.6;\n    const cellH = cellSize;\n    let cols = 0;\n    let sized = false;\n    let disposed = false;\n\n    // per-cell state: pattern (6-bit) and the timestamp (ms, component-local\n    // clock) it was last written, for the write-flash hairline\n    let patterns: Uint8Array[] = [];\n    let writeAt: Float32Array[] = [];\n\n    const readTokens = () => {\n      fg = getComputedStyle(canvas).color;\n    };\n\n    const allocRows = () => {\n      patterns = Array.from({ length: ROWS }, () => new Uint8Array(cols));\n      writeAt = Array.from({ length: ROWS }, () => new Float32Array(cols).fill(-Infinity));\n    };\n\n    // globalT: seconds, drives the generative field. Never resets — content\n    // keeps evolving pass over pass so no two page loads look identical.\n    let globalT = 0;\n    // step machine: stepIndex 0..ROWS-1 writes that row, stepIndex===ROWS is\n    // the sync pause; stepAcc accumulates ms toward the current step's\n    // duration, same accumulator discipline as the sibling VU divider.\n    let stepIndex = 0;\n    let stepAcc = 0;\n\n    const stepDuration = (i: number) => (i < ROWS ? ROW_INTERVAL_MS : PAUSE_MS);\n\n    const writeRow = (row: number) => {\n      if (!sized) return;\n      for (let c = 0; c < cols; c++) {\n        patterns[row][c] = subCellPattern(c, row, globalT);\n        writeAt[row][c] = globalT;\n      }\n    };\n\n    // full pass at increasing globalT, used both for the initial warm start\n    // (a page never opens as a blank cell grid) and after a resize (a new\n    // column count leaves nothing painted otherwise)\n    const warmPass = () => {\n      for (let r = 0; r < ROWS; r++) {\n        globalT += ROW_INTERVAL_MS / 1000;\n        writeRow(r);\n      }\n      stepIndex = 0;\n      stepAcc = 0;\n    };\n\n    const resize = () => {\n      const { width } = canvas.getBoundingClientRect();\n      const height = ROWS * cellH;\n      if (width < 2) {\n        sized = false;\n        return;\n      }\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\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      const fontFamily = getComputedStyle(canvas).fontFamily;\n      const off = document.createElement(\"canvas\").getContext(\"2d\");\n      if (off) {\n        off.font = `${cellSize}px ${fontFamily}`;\n        cellW = Math.max(6, off.measureText(\"MMMMMMMMMM\").width / 10);\n      }\n\n      cols = Math.max(8, Math.floor(width / cellW));\n      sized = true;\n      allocRows();\n      warmPass();\n    };\n\n    const draw = () => {\n      if (!sized) return;\n      ctx.clearRect(0, 0, cols * cellW + cellW, ROWS * cellH);\n      ctx.fillStyle = fg;\n\n      const subW = cellW / 2;\n      const subH = cellH / 3;\n\n      for (let r = 0; r < ROWS; r++) {\n        const rowPatterns = patterns[r];\n        const rowWriteAt = writeAt[r];\n        for (let c = 0; c < cols; c++) {\n          const bits = rowPatterns[c];\n          if (bits === 0) continue;\n          const x = c * cellW;\n          const y = r * cellH;\n          for (let subRow = 0; subRow < 3; subRow++) {\n            for (let subCol = 0; subCol < 2; subCol++) {\n              const idx = subRow * 2 + subCol;\n              if (!(bits & (1 << idx))) continue;\n              ctx.fillRect(\n                Math.round(x + subCol * subW),\n                Math.round(y + subRow * subH),\n                Math.ceil(subW) + 1,\n                Math.ceil(subH) + 1\n              );\n            }\n          }\n          if (!reduced) {\n            const age = (globalT - rowWriteAt[c]) * 1000;\n            if (age >= 0 && age < FLASH_MS) {\n              const alpha = 1 - age / FLASH_MS;\n              ctx.globalAlpha = alpha * 0.6;\n              ctx.fillRect(Math.round(x), Math.round(y), Math.ceil(cellW) + 1, 1);\n              ctx.globalAlpha = 1;\n            }\n          }\n        }\n      }\n    };\n\n    // -- loop ----------------------------------------------------------------\n    let raf = 0;\n    let last = 0;\n\n    const loop = (now: number) => {\n      const dtMs = last ? Math.min(250, now - last) : 1000 / 60;\n      last = now;\n      globalT += dtMs / 1000;\n      stepAcc += dtMs;\n\n      let guard = 0;\n      while (stepAcc >= stepDuration(stepIndex) && guard < ROWS + 1) {\n        stepAcc -= stepDuration(stepIndex);\n        if (stepIndex < ROWS) writeRow(stepIndex);\n        stepIndex = (stepIndex + 1) % (ROWS + 1);\n        guard++;\n      }\n\n      draw();\n      if (!document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced) draw();\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    let resizeTimer: ReturnType<typeof setTimeout> | null = null;\n    const onResize = () => {\n      if (resizeTimer) clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(() => {\n        resizeTimer = null;\n        resize();\n        draw();\n      }, 150);\n    };\n    window.addEventListener(\"resize\", onResize);\n\n    const onVis = () => {\n      cancelAnimationFrame(raf);\n      if (!document.hidden && !reduced && sized) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    document.fonts.ready.then(() => {\n      if (disposed) return;\n      readTokens();\n      resize();\n\n      if (reduced) {\n        // freeze on a deliberately non-t0 frame: run the row-write sweep\n        // forward through one full extra pass past the warm start, so\n        // every row has been freshly rewritten at least twice and the\n        // mosaic is at its most legible full-page state — never the sync\n        // pause (which, while not empty, is the least eventful frame) and\n        // never t0's cold warm-fill.\n        for (let r = 0; r < ROWS; r++) {\n          globalT += ROW_INTERVAL_MS / 1000;\n          writeRow(r);\n        }\n        draw();\n        return;\n      }\n\n      draw();\n      raf = requestAnimationFrame(loop);\n    });\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      if (resizeTimer) clearTimeout(resizeTimer);\n      mo.disconnect();\n      window.removeEventListener(\"resize\", onResize);\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [cellSize]);\n\n  return (\n    <div\n      role=\"separator\"\n      aria-orientation=\"horizontal\"\n      className={`ns-dtm w-full font-mono ${className}`}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className=\"block w-full text-foreground\"\n        style={{ height: ROWS * cellSize }}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/divider-teletext-mosaic.tsx"
    }
  ],
  "meta": {
    "collection": "core",
    "tags": [
      "divider",
      "separator",
      "hr",
      "teletext",
      "videotex",
      "alphamosaic",
      "sextant",
      "ceefax",
      "canvas",
      "monochrome"
    ],
    "instruction": "Build <DividerTeletextMosaic cellSize? className?> as a full-width <canvas> band, ROWS=6 cells tall, wrapped in a <div role=\"separator\" aria-orientation=\"horizontal\">, a drop-in replacement for <hr>/border-top between page sections. SOURCE, NOT INVENTED: real broadcast teletext (World System Teletext / CEEFAX-era decoders) never had a per-pixel framebuffer — each character cell holds a 6-bit code selecting one of 64 patterns across a fixed 2-wide x 3-tall sub-cell grid, addressed by position (top-left, top-right, mid-left, mid-right, bottom-left, bottom-right), the real 'alphamosaic' graphics mode of the standard. THE DISCRIMINATOR — read this before anything else: this is a genuinely different sub-cell grid from anything else in the registry. Quadrant-block dividers quantize a 2x2 sub-grid (four Unicode block-element combinations); braille components address a 2x4 dot grid. This is 2x3 — six sub-cells, a different real broadcast protocol's addressing, not a reskin of either. GLYPH ROUTE, STATED: Unicode does define a full sextant block (U+1FB00-U+1FB3B, Symbols for Legacy Computing), but this repo's mono stack (GeistMono / GeistMono Fallback / ui-monospace) does not reliably carry that range, so sub-cells are drawn as filled canvas rects directly rather than risking tofu from unverified glyph coverage — which is also what a real teletext decoder chip did: it lit sub-cell segments off the 6-bit code, it never asked a font for a glyph. CONTIGUOUS MOSAIC: sub-cells are drawn edge-to-edge with no gap between them, matching the real 'contiguous mosaic' mode broadcasters used for photo-like graphics (as opposed to 'separated mosaic', which insets a gap per block) — cellW is derived once via an offscreen canvas's measureText('MMMMMMMMMM').width / 10 in the canvas's own font family (the same house cell-metrics convention every grid-based ascii component in this repo uses), re-measured after document.fonts.ready; cellH is fixed to cellSize. CONTENT: each sub-cell's bit comes from a slow generative field — three non-commensurate traveling sine components summed over global sub-cell coordinates and the component's own continuously-advancing clock, normalized 0..1 with a 1.2 gamma — thresholded at 0.5 with a small per-sub-cell bias drawn from a scattered 2x3 ordered-dither table (spread +/-0.42, scaled by a 0.32 dither strength), the same discipline real videotex picture-mosaic dithering used to fake tone gradients out of a strictly binary 6-bit grid. ALIVE AT REST, THE MECHANIC: a real teletext page doesn't arrive all at once — it paints row by row as it's received off the broadcast VBI signal. A write cursor advances one row every 420ms, re-sampling the field into a fresh 6-bit pattern for every cell in that row and holding it there until the row's next turn; after all 6 rows have been rewritten, a 600ms sync pause elapses (page fully painted, untouched, never blank) before the sweep restarts from row 0 — the component's own clock never resets, so no two passes sample the same field state and the mosaic is never identical from one page-load to the next. The just-written row gets a brief top-edge hairline in --foreground fading from 60% alpha to 0 over 220ms — the one-frame flicker of a row actively being received — skipped entirely under prefers-reduced-motion. On mount, and after every resize (a new column count leaves nothing painted otherwise), a full warm pass writes every row once before the first paint so the opening frame is never a blank grid. TOKENS: fg is read once from getComputedStyle(canvas).color (driven by the .text-foreground class on the canvas) and re-derived on a documentElement class MutationObserver so a theme flip is live; the write-flash hairline modulates that same color's alpha only, never a second color. --border is never used as a fill or stroke (it measures under 1.2:1 against --background in light theme and would be structurally invisible as the mosaic's own ink); --ns-accent never appears — this is a resting divider with no interactive surface. prefers-reduced-motion freezes after running one additional full row-write pass past the warm start (every row rewritten at least twice, field well clear of its cold-start state), then draws once with no flash hairlines and no further updates — chosen because it's the mosaic at its most legible full-page state, never the sync pause (the least eventful frame, though not empty) and never t0's cold warm-fill. The render loop pauses on document.hidden and resumes cleanly on visibilitychange. Direct-DOM rAF, zero React state on the hot path, zero dependencies. A11Y: role=separator carries the divider's semantics with no accessible name required; the canvas is aria-hidden decoration; there is no keyboard surface because there is nothing to operate. Props: cellSize (row height px, default 14; the band is fixed at 6 rows, cell width derives from monospace metrics), className."
  },
  "type": "registry:ui"
}