diff --git a/apps/www/registry/example/glyph-matrix-demo.tsx b/apps/www/registry/example/glyph-matrix-demo.tsx
new file mode 100644
index 000000000..3748bd330
--- /dev/null
+++ b/apps/www/registry/example/glyph-matrix-demo.tsx
@@ -0,0 +1,15 @@
+import { GlyphMatrix } from "@/registry/magicui/glyph-matrix"
+
+export default function GlyphMatrixDemo() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/www/registry/magicui/glyph-matrix.tsx b/apps/www/registry/magicui/glyph-matrix.tsx
new file mode 100644
index 000000000..bd5685b09
--- /dev/null
+++ b/apps/www/registry/magicui/glyph-matrix.tsx
@@ -0,0 +1,169 @@
+"use client";
+import { useEffect, useRef } from "react";
+
+interface GlyphMatrixProps {
+ /** Characters to randomly pick from */
+ glyphs?: string;
+ /** Cell size in px (also font size) */
+ cellSize?: number;
+ /** Probability (0-1) a cell mutates each tick */
+ mutationRate?: number;
+ /** Tick interval in ms */
+ interval?: number;
+ /** Optional className for the wrapping canvas */
+ className?: string;
+ /** Fade out toward bottom (0 = no fade) */
+ fadeBottom?: number;
+}
+
+/**
+ * GlyphMatrix — an animated grid of subtly shifting glyphs.
+ * Uses semantic tokens (--foreground / --background) so it adapts to
+ * both light and dark modes automatically.
+ */
+export function GlyphMatrix({
+ glyphs = "01·•+*/\\<>=",
+ cellSize = 14,
+ mutationRate = 0.04,
+ interval = 90,
+ className,
+ fadeBottom = 0.6,
+}: GlyphMatrixProps) {
+ const canvasRef = useRef(null);
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext("2d");
+ if (!ctx) return;
+
+ let cols = 0;
+ let rows = 0;
+ let cells: string[] = [];
+ let alphas: number[] = [];
+ let raf = 0;
+ let last = 0;
+ let stopped = false;
+
+ const readColor = () => {
+ const styles = getComputedStyle(canvas);
+ // Resolve --foreground via a temp element so oklch() is converted to rgb
+ const probe = document.createElement("span");
+ probe.style.color = "var(--foreground)";
+ probe.style.display = "none";
+ canvas.parentElement?.appendChild(probe);
+ const color = getComputedStyle(probe).color || styles.color;
+ probe.remove();
+ return color;
+ };
+
+ let fgColor = readColor();
+
+ const resize = () => {
+ const dpr = window.devicePixelRatio || 1;
+ const { clientWidth: w, clientHeight: h } = canvas;
+
+ canvas.width = w * dpr;
+ canvas.height = h * dpr;
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+
+ cols = Math.ceil(w / cellSize);
+ rows = Math.ceil(h / cellSize);
+
+ cells = new Array(cols * rows)
+ .fill(0)
+ .map(() => glyphs[Math.floor(Math.random() * glyphs.length)]);
+ alphas = new Array(cols * rows)
+ .fill(0)
+ .map(() => 0.05 + Math.random() * 0.35);
+
+ fgColor = readColor();
+ };
+
+ const parseRgb = (c: string) => {
+ const m = c.match(/rgba?\(([^)]+)\)/);
+ if (!m) return { r: 0, g: 0, b: 0 };
+ const [r, g, b] = m[1].split(",").map((v) => parseFloat(v));
+ return { r, g, b };
+ };
+
+ const draw = () => {
+ const { clientWidth: w, clientHeight: h } = canvas;
+ ctx.clearRect(0, 0, w, h);
+
+ const { r, g, b } = parseRgb(fgColor);
+ ctx.font = `${cellSize - 2}px ui-monospace, SFMono-Regular, Menlo, monospace`;
+ ctx.textBaseline = "top";
+
+ for (let y = 0; y < rows; y++) {
+ const fade =
+ fadeBottom > 0 ? 1 - (y / rows) * fadeBottom : 1;
+ for (let x = 0; x < cols; x++) {
+ const i = y * cols + x;
+ const a = alphas[i] * fade;
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`;
+ ctx.fillText(cells[i], x * cellSize, y * cellSize);
+ }
+ }
+ };
+
+ const tick = (t: number) => {
+ if (stopped) return;
+
+ if (t - last >= interval) {
+ last = t;
+
+ const total = cols * rows;
+ const mutations = Math.max(1, Math.floor(total * mutationRate));
+
+ for (let n = 0; n < mutations; n++) {
+ const i = Math.floor(Math.random() * total);
+ cells[i] = glyphs[Math.floor(Math.random() * glyphs.length)];
+ alphas[i] = 0.05 + Math.random() * 0.45;
+ }
+
+ draw();
+ }
+
+ raf = requestAnimationFrame(tick);
+ };
+
+ resize();
+ draw();
+ raf = requestAnimationFrame(tick);
+
+ const ro = new ResizeObserver(() => {
+ resize();
+ draw();
+ });
+ ro.observe(canvas);
+
+ // Re-read color when theme changes (class on )
+ const mo = new MutationObserver(() => {
+ fgColor = readColor();
+ });
+ mo.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["class", "data-theme"],
+ });
+
+ return () => {
+ stopped = true;
+ cancelAnimationFrame(raf);
+ ro.disconnect();
+ mo.disconnect();
+ };
+ }, [glyphs, cellSize, mutationRate, interval, fadeBottom]);
+
+ return (
+
+ );
+}
+
+export default GlyphMatrix;
diff --git a/apps/www/registry/registry-examples.ts b/apps/www/registry/registry-examples.ts
index 9b88a2740..4add7daa8 100644
--- a/apps/www/registry/registry-examples.ts
+++ b/apps/www/registry/registry-examples.ts
@@ -608,6 +608,19 @@ export const examples: Registry["items"] = [
},
],
},
+ {
+ name: "glyph-matrix-demo",
+ type: "registry:example",
+ title: "Glyph Matrix Demo",
+ description: "Example showing an animated grid of subtly shifting glyphs.",
+ registryDependencies: ["@magicui/glyph-matrix"],
+ files: [
+ {
+ path: "example/glyph-matrix-demo.tsx",
+ type: "registry:example",
+ },
+ ],
+ },
{
name: "glare-hover-demo",
type: "registry:example",
diff --git a/apps/www/registry/registry-ui.ts b/apps/www/registry/registry-ui.ts
index 0969e85f4..81f626874 100644
--- a/apps/www/registry/registry-ui.ts
+++ b/apps/www/registry/registry-ui.ts
@@ -412,6 +412,19 @@ export const ui: Registry["items"] = [
},
],
},
+ {
+ 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: "magicui/glyph-matrix.tsx",
+ type: "registry:ui",
+ },
+ ],
+ },
{
name: "glare-hover",
type: "registry:ui",
diff --git a/registry.json b/registry.json
index a9590e215..3c7fe0820 100644
--- a/registry.json
+++ b/registry.json
@@ -439,6 +439,19 @@
}
}
},
+ {
+ "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",
+ "type": "registry:ui",
+ "target": "components/magicui/glyph-matrix.tsx"
+ }
+ ]
+ },
{
"name": "globe",
"type": "registry:ui",