{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "glyph-matrix",
  "type": "registry:ui",
  "title": "Glyph Matrix",
  "description": "An animated grid of subtly shifting glyphs with fade effect and theme support.",
  "files": [
    {
      "path": "registry/magicui/glyph-matrix.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface GlyphMatrixProps extends React.HTMLAttributes<HTMLCanvasElement> {\n  /** Characters to randomly pick from */\n  glyphs?: string\n  /** Cell size in px (also font size) */\n  cellSize?: number\n  /** Probability (0-1) a cell mutates each tick */\n  mutationRate?: number\n  /** Tick interval in ms */\n  interval?: number\n  /** Fade out toward bottom (0 = no fade) */\n  fadeBottom?: number\n  /** Glyph color (any CSS color). Pass a theme-aware value from the consumer. */\n  color?: string\n}\n\n/**\n * GlyphMatrix — an animated grid of subtly shifting glyphs.\n * Pass a `color` prop (e.g. driven by next-themes) to adapt it to\n * light and dark modes.\n */\nexport function GlyphMatrix({\n  glyphs = \"01·•+*/\\\\<>=\",\n  cellSize = 14,\n  mutationRate = 0.04,\n  interval = 90,\n  className,\n  fadeBottom = 0.6,\n  color = \"#6B7280\",\n  style,\n  ...props\n}: GlyphMatrixProps) {\n  const canvasRef = useRef<HTMLCanvasElement | null>(null)\n  // Current glyph color as RGBA (a in 0-1). Kept in a ref so a color change\n  // (e.g. theme toggle) recolors the next frame without restarting the\n  // animation. Defaults to #6B7280.\n  const rgbaRef = useRef({ r: 107, g: 114, b: 128, a: 1 })\n\n  // Resolve the CSS color string to RGBA (handles hex, rgb, hsl, oklch, ...).\n  useEffect(() => {\n    const probe = document.createElement(\"canvas\")\n    probe.width = 1\n    probe.height = 1\n    const probeCtx = probe.getContext(\"2d\")\n    if (!probeCtx) return\n    // Seed with the default so an invalid color falls back to it: the 2d\n    // context keeps the previous fillStyle when assigned an invalid value\n    // instead of silently turning black.\n    probeCtx.fillStyle = \"#6B7280\"\n    probeCtx.fillStyle = color\n    probeCtx.fillRect(0, 0, 1, 1)\n    const [r, g, b, a] = probeCtx.getImageData(0, 0, 1, 1).data\n    rgbaRef.current = { r, g, b, a: a / 255 }\n  }, [color])\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n\n    const ctx = canvas.getContext(\"2d\")\n    if (!ctx) return\n\n    let cols = 0\n    let rows = 0\n    let cells: string[] = []\n    let alphas: number[] = []\n    let raf = 0\n    let last = 0\n    let stopped = false\n\n    const resize = () => {\n      const dpr = window.devicePixelRatio || 1\n      const { clientWidth: w, clientHeight: h } = canvas\n\n      canvas.width = w * dpr\n      canvas.height = h * dpr\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n\n      cols = Math.ceil(w / cellSize)\n      rows = Math.ceil(h / cellSize)\n\n      cells = new Array(cols * rows)\n        .fill(0)\n        .map(() => glyphs[Math.floor(Math.random() * glyphs.length)])\n      alphas = new Array(cols * rows)\n        .fill(0)\n        .map(() => 0.05 + Math.random() * 0.35)\n    }\n\n    const draw = () => {\n      const { clientWidth: w, clientHeight: h } = canvas\n      ctx.clearRect(0, 0, w, h)\n\n      ctx.font = `${cellSize - 2}px ui-monospace, SFMono-Regular, Menlo, monospace`\n      ctx.textBaseline = \"top\"\n\n      const { r, g, b, a: colorAlpha } = rgbaRef.current\n      for (let y = 0; y < rows; y++) {\n        const fade = fadeBottom > 0 ? 1 - (y / rows) * fadeBottom : 1\n        for (let x = 0; x < cols; x++) {\n          const i = y * cols + x\n          const a = alphas[i] * fade * colorAlpha\n          ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`\n          ctx.fillText(cells[i], x * cellSize, y * cellSize)\n        }\n      }\n    }\n\n    const tick = (t: number) => {\n      if (stopped) return\n\n      if (t - last >= interval) {\n        last = t\n\n        const total = cols * rows\n        const mutations = Math.max(1, Math.floor(total * mutationRate))\n\n        for (let n = 0; n < mutations; n++) {\n          const i = Math.floor(Math.random() * total)\n          cells[i] = glyphs[Math.floor(Math.random() * glyphs.length)]\n          alphas[i] = 0.05 + Math.random() * 0.45\n        }\n\n        draw()\n      }\n\n      raf = requestAnimationFrame(tick)\n    }\n\n    resize()\n    draw()\n    raf = requestAnimationFrame(tick)\n\n    const ro = new ResizeObserver(() => {\n      resize()\n      draw()\n    })\n    ro.observe(canvas)\n\n    return () => {\n      stopped = true\n      cancelAnimationFrame(raf)\n      ro.disconnect()\n    }\n  }, [glyphs, cellSize, mutationRate, interval, fadeBottom])\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={cn(\"pointer-events-none\", className)}\n      style={{ width: \"100%\", height: \"100%\", display: \"block\", ...style }}\n      aria-hidden=\"true\"\n      {...props}\n    />\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}