diff --git a/apps/www/config/docs.ts b/apps/www/config/docs.ts
index 92df27973..19e1ea203 100644
--- a/apps/www/config/docs.ts
+++ b/apps/www/config/docs.ts
@@ -651,6 +651,12 @@ export const docsConfig: DocsConfig = {
items: [],
label: "New",
},
+ {
+ title: "Glyph Matrix",
+ href: `/docs/components/glyph-matrix`,
+ items: [],
+ label: "New",
+ },
],
},
],
diff --git a/apps/www/content/docs/components/glyph-matrix.mdx b/apps/www/content/docs/components/glyph-matrix.mdx
new file mode 100644
index 000000000..761833a63
--- /dev/null
+++ b/apps/www/content/docs/components/glyph-matrix.mdx
@@ -0,0 +1,69 @@
+---
+title: Glyph Matrix
+date: 2026-06-12
+description: An animated grid of subtly shifting glyphs on a canvas, with a theme-aware color driven by the consumer.
+author: magicui
+published: true
+---
+
+
+
+## Installation
+
+
+
+
+ CLI
+ Manual
+
+
+
+```bash
+npx shadcn@latest add @magicui/glyph-matrix
+```
+
+
+
+
+
+
+
+Copy and paste the following code into your project.
+
+
+
+Update the import paths to match your project setup.
+
+
+
+
+
+
+
+## Usage
+
+```tsx showLineNumbers
+import { GlyphMatrix } from "@/registry/magicui/glyph-matrix"
+```
+
+```tsx showLineNumbers
+
+
+
+```
+
+## Props
+
+| Prop | Type | Default | Description |
+| -------------- | -------- | ---------------- | -------------------------------------------- |
+| `glyphs` | `string` | `"01·•+*/\\<>="` | Characters to randomly pick from. |
+| `cellSize` | `number` | `14` | Cell size in pixels and font size. |
+| `mutationRate` | `number` | `0.04` | Probability a cell mutates each tick. |
+| `interval` | `number` | `90` | Tick interval in milliseconds. |
+| `className` | `string` | `-` | Classes applied to the canvas element. |
+| `fadeBottom` | `number` | `0.6` | Fade strength toward the bottom of the grid. |
+| `color` | `string` | `"#6B7280"` | Glyph color (any CSS color). |
+
+## Notes
+
+The component draws with the `color` prop, which accepts any CSS color (hex, `rgb()`, `hsl()`, `oklch()`, ...) and is normalized through canvas before drawing. For light/dark support, drive `color` from the consumer — e.g. with `next-themes` as shown in the demo above.
diff --git a/apps/www/public/llms-full.txt b/apps/www/public/llms-full.txt
index ec3bbf83f..c84bf080e 100644
--- a/apps/www/public/llms-full.txt
+++ b/apps/www/public/llms-full.txt
@@ -8829,6 +8829,212 @@ export default function GlobeDemo() {
+===== COMPONENT: glyph-matrix =====
+Title: Glyph Matrix
+Description: An animated grid of subtly shifting glyphs with fade effect and theme support.
+
+--- file: magicui/glyph-matrix.tsx ---
+"use client"
+
+import { useEffect, useRef } from "react"
+
+import { cn } from "@/lib/utils"
+
+interface GlyphMatrixProps extends React.HTMLAttributes {
+ /** 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
+ /** Fade out toward bottom (0 = no fade) */
+ fadeBottom?: number
+ /** Glyph color (any CSS color). Pass a theme-aware value from the consumer. */
+ color?: string
+}
+
+/**
+ * GlyphMatrix — an animated grid of subtly shifting glyphs.
+ * Pass a `color` prop (e.g. driven by next-themes) to adapt it to
+ * light and dark modes.
+ */
+export function GlyphMatrix({
+ glyphs = "01·•+*/\\<>=",
+ cellSize = 14,
+ mutationRate = 0.04,
+ interval = 90,
+ className,
+ fadeBottom = 0.6,
+ color = "#6B7280",
+ style,
+ ...props
+}: GlyphMatrixProps) {
+ const canvasRef = useRef(null)
+ // Current glyph color as RGBA (a in 0-1). Kept in a ref so a color change
+ // (e.g. theme toggle) recolors the next frame without restarting the
+ // animation. Defaults to #6B7280.
+ const rgbaRef = useRef({ r: 107, g: 114, b: 128, a: 1 })
+
+ // Resolve the CSS color string to RGBA (handles hex, rgb, hsl, oklch, ...).
+ useEffect(() => {
+ const probe = document.createElement("canvas")
+ probe.width = 1
+ probe.height = 1
+ const probeCtx = probe.getContext("2d")
+ if (!probeCtx) return
+ // Seed with the default so an invalid color falls back to it: the 2d
+ // context keeps the previous fillStyle when assigned an invalid value
+ // instead of silently turning black.
+ probeCtx.fillStyle = "#6B7280"
+ probeCtx.fillStyle = color
+ probeCtx.fillRect(0, 0, 1, 1)
+ const [r, g, b, a] = probeCtx.getImageData(0, 0, 1, 1).data
+ rgbaRef.current = { r, g, b, a: a / 255 }
+ }, [color])
+
+ 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 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)
+ }
+
+ const draw = () => {
+ const { clientWidth: w, clientHeight: h } = canvas
+ ctx.clearRect(0, 0, w, h)
+
+ ctx.font = `${cellSize - 2}px ui-monospace, SFMono-Regular, Menlo, monospace`
+ ctx.textBaseline = "top"
+
+ const { r, g, b, a: colorAlpha } = rgbaRef.current
+ 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 * colorAlpha
+ 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)
+
+ return () => {
+ stopped = true
+ cancelAnimationFrame(raf)
+ ro.disconnect()
+ }
+ }, [glyphs, cellSize, mutationRate, interval, fadeBottom])
+
+ return (
+
+ )
+}
+
+
+===== EXAMPLE: glyph-matrix-demo =====
+Title: Glyph Matrix Demo
+
+--- file: example/glyph-matrix-demo.tsx ---
+"use client"
+
+import { useEffect, useState } from "react"
+import { useTheme } from "next-themes"
+
+import { GlyphMatrix } from "@/registry/magicui/glyph-matrix"
+
+export default function GlyphMatrixDemo() {
+ const { resolvedTheme } = useTheme()
+ // Start neutral so the first frame is visible in both themes, then adopt
+ // the resolved theme color once it's known.
+ const [color, setColor] = useState("#6B7280")
+
+ useEffect(() => {
+ if (!resolvedTheme) return
+ setColor(resolvedTheme === "dark" ? "#ffffff" : "#000000")
+ }, [resolvedTheme])
+
+ return (
+
+
+
+ )
+}
+
+
+
===== COMPONENT: grid-pattern =====
Title: Grid Pattern
Description: A background grid pattern made with SVGs, fully customizable using Tailwind CSS.
diff --git a/apps/www/public/llms.txt b/apps/www/public/llms.txt
index dd7e1290c..a375dac2d 100644
--- a/apps/www/public/llms.txt
+++ b/apps/www/public/llms.txt
@@ -33,6 +33,7 @@ This file provides LLM-friendly entry points to documentation and examples.
- [Flickering Grid](https://magicui.design/docs/components/flickering-grid): A flickering grid background made with SVGs, fully customizable using Tailwind CSS.
- [Glare Hover](https://magicui.design/docs/components/glare-hover): A diagonal glare on hover using a ::before gradient and CSS variables (angle, size, duration, color).
- [Globe](https://magicui.design/docs/components/globe): An autorotating, interactive, and highly performant globe made using WebGL.
+- [Glyph Matrix](https://magicui.design/docs/components/glyph-matrix): An animated grid of subtly shifting glyphs with fade effect and theme support.
- [Grid Pattern](https://magicui.design/docs/components/grid-pattern): A background grid pattern made with SVGs, fully customizable using Tailwind CSS.
- [Hero Video Dialog](https://magicui.design/docs/components/hero-video-dialog): A hero video dialog component.
- [Hexagon Pattern](https://magicui.design/docs/components/hexagon-pattern): A background hexagon pattern made with SVGs, fully customizable using Tailwind CSS.
@@ -130,6 +131,7 @@ This file provides LLM-friendly entry points to documentation and examples.
- [Marquee Logos](https://github.com/magicuidesign/magicui/blob/main/example/marquee-logos.tsx): Example usage
- [Marquee 3D](https://github.com/magicuidesign/magicui/blob/main/example/marquee-3d.tsx): Example usage
- [Globe Demo](https://github.com/magicuidesign/magicui/blob/main/example/globe-demo.tsx): Example usage
+- [Glyph Matrix Demo](https://github.com/magicuidesign/magicui/blob/main/example/glyph-matrix-demo.tsx): Example usage
- [Glare Hover Demo](https://github.com/magicuidesign/magicui/blob/main/example/glare-hover-demo.tsx): Example usage
- [Glare Hover Demo — CTA](https://github.com/magicuidesign/magicui/blob/main/example/glare-hover-demo-cta.tsx): Example usage
- [Glare Hover Demo — Alerts](https://github.com/magicuidesign/magicui/blob/main/example/glare-hover-demo-alert.tsx): Example usage
diff --git a/apps/www/public/r/glyph-matrix-demo.json b/apps/www/public/r/glyph-matrix-demo.json
new file mode 100644
index 000000000..8773a89ea
--- /dev/null
+++ b/apps/www/public/r/glyph-matrix-demo.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "glyph-matrix-demo",
+ "type": "registry:example",
+ "title": "Glyph Matrix Demo",
+ "description": "Example showing an animated grid of subtly shifting glyphs.",
+ "dependencies": [
+ "next-themes"
+ ],
+ "registryDependencies": [
+ "@magicui/glyph-matrix"
+ ],
+ "files": [
+ {
+ "path": "registry/example/glyph-matrix-demo.tsx",
+ "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport { useTheme } from \"next-themes\"\n\nimport { GlyphMatrix } from \"@/registry/magicui/glyph-matrix\"\n\nexport default function GlyphMatrixDemo() {\n const { resolvedTheme } = useTheme()\n // Start neutral so the first frame is visible in both themes, then adopt\n // the resolved theme color once it's known.\n const [color, setColor] = useState(\"#6B7280\")\n\n useEffect(() => {\n if (!resolvedTheme) return\n setColor(resolvedTheme === \"dark\" ? \"#ffffff\" : \"#000000\")\n }, [resolvedTheme])\n\n return (\n \n =\"\n cellSize={14}\n mutationRate={0.04}\n interval={90}\n fadeBottom={0.6}\n color={color}\n />\n
\n )\n}\n",
+ "type": "registry:example"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/www/public/r/glyph-matrix.json b/apps/www/public/r/glyph-matrix.json
new file mode 100644
index 000000000..5fb532f34
--- /dev/null
+++ b/apps/www/public/r/glyph-matrix.json
@@ -0,0 +1,14 @@
+{
+ "$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 {\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(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 \n )\n}\n",
+ "type": "registry:ui"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/www/public/r/registry.json b/apps/www/public/r/registry.json
index 499944e16..b624cc4c0 100644
--- a/apps/www/public/r/registry.json
+++ b/apps/www/public/r/registry.json
@@ -442,6 +442,18 @@
}
]
},
+ {
+ "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"
+ }
+ ]
+ },
{
"name": "glare-hover",
"type": "registry:ui",
@@ -2039,6 +2051,24 @@
}
]
},
+ {
+ "name": "glyph-matrix-demo",
+ "type": "registry:example",
+ "title": "Glyph Matrix Demo",
+ "description": "Example showing an animated grid of subtly shifting glyphs.",
+ "dependencies": [
+ "next-themes"
+ ],
+ "registryDependencies": [
+ "@magicui/glyph-matrix"
+ ],
+ "files": [
+ {
+ "path": "registry/example/glyph-matrix-demo.tsx",
+ "type": "registry:example"
+ }
+ ]
+ },
{
"name": "glare-hover-demo",
"type": "registry:example",
diff --git a/apps/www/public/registry.json b/apps/www/public/registry.json
index 499944e16..b624cc4c0 100644
--- a/apps/www/public/registry.json
+++ b/apps/www/public/registry.json
@@ -442,6 +442,18 @@
}
]
},
+ {
+ "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"
+ }
+ ]
+ },
{
"name": "glare-hover",
"type": "registry:ui",
@@ -2039,6 +2051,24 @@
}
]
},
+ {
+ "name": "glyph-matrix-demo",
+ "type": "registry:example",
+ "title": "Glyph Matrix Demo",
+ "description": "Example showing an animated grid of subtly shifting glyphs.",
+ "dependencies": [
+ "next-themes"
+ ],
+ "registryDependencies": [
+ "@magicui/glyph-matrix"
+ ],
+ "files": [
+ {
+ "path": "registry/example/glyph-matrix-demo.tsx",
+ "type": "registry:example"
+ }
+ ]
+ },
{
"name": "glare-hover-demo",
"type": "registry:example",
diff --git a/apps/www/registry.json b/apps/www/registry.json
index 499944e16..b624cc4c0 100644
--- a/apps/www/registry.json
+++ b/apps/www/registry.json
@@ -442,6 +442,18 @@
}
]
},
+ {
+ "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"
+ }
+ ]
+ },
{
"name": "glare-hover",
"type": "registry:ui",
@@ -2039,6 +2051,24 @@
}
]
},
+ {
+ "name": "glyph-matrix-demo",
+ "type": "registry:example",
+ "title": "Glyph Matrix Demo",
+ "description": "Example showing an animated grid of subtly shifting glyphs.",
+ "dependencies": [
+ "next-themes"
+ ],
+ "registryDependencies": [
+ "@magicui/glyph-matrix"
+ ],
+ "files": [
+ {
+ "path": "registry/example/glyph-matrix-demo.tsx",
+ "type": "registry:example"
+ }
+ ]
+ },
{
"name": "glare-hover-demo",
"type": "registry:example",
diff --git a/apps/www/registry/__index__.tsx b/apps/www/registry/__index__.tsx
index 8c1517ae8..35aa0bc8a 100644
--- a/apps/www/registry/__index__.tsx
+++ b/apps/www/registry/__index__.tsx
@@ -423,6 +423,23 @@ export const Index: Record = {
}),
meta: undefined,
},
+ "glyph-matrix": {
+ name: "glyph-matrix",
+ description: "An animated grid of subtly shifting glyphs with fade effect and theme support.",
+ type: "registry:ui",
+ registryDependencies: undefined,
+ files: [{
+ path: "registry/magicui/glyph-matrix.tsx",
+ type: "registry:ui",
+ target: ""
+ }],
+ component: React.lazy(async () => {
+ const mod = await import("@/registry/magicui/glyph-matrix.tsx")
+ const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') ?? item.name
+ return { default: mod.default ?? mod[exportName] }
+ }),
+ meta: undefined,
+ },
"glare-hover": {
name: "glare-hover",
description: "A diagonal glare on hover using a ::before gradient and CSS variables (angle, size, duration, color).",
@@ -2072,6 +2089,23 @@ export const Index: Record = {
}),
meta: undefined,
},
+ "glyph-matrix-demo": {
+ name: "glyph-matrix-demo",
+ description: "Example showing an animated grid of subtly shifting glyphs.",
+ type: "registry:example",
+ registryDependencies: ["@magicui/glyph-matrix"],
+ files: [{
+ path: "registry/example/glyph-matrix-demo.tsx",
+ type: "registry:example",
+ target: ""
+ }],
+ component: React.lazy(async () => {
+ const mod = await import("@/registry/example/glyph-matrix-demo.tsx")
+ const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') ?? item.name
+ return { default: mod.default ?? mod[exportName] }
+ }),
+ meta: undefined,
+ },
"glare-hover-demo": {
name: "glare-hover-demo",
description: "Pricing card with diagonal hover glare (duration 600ms).",
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..146c0da3c
--- /dev/null
+++ b/apps/www/registry/example/glyph-matrix-demo.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { useTheme } from "next-themes"
+
+import { GlyphMatrix } from "@/registry/magicui/glyph-matrix"
+
+export default function GlyphMatrixDemo() {
+ const { resolvedTheme } = useTheme()
+ // Start neutral so the first frame is visible in both themes, then adopt
+ // the resolved theme color once it's known.
+ const [color, setColor] = useState("#6B7280")
+
+ useEffect(() => {
+ if (!resolvedTheme) return
+ setColor(resolvedTheme === "dark" ? "#ffffff" : "#000000")
+ }, [resolvedTheme])
+
+ 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..cfbdada56
--- /dev/null
+++ b/apps/www/registry/magicui/glyph-matrix.tsx
@@ -0,0 +1,161 @@
+"use client"
+
+import { useEffect, useRef } from "react"
+
+import { cn } from "@/lib/utils"
+
+interface GlyphMatrixProps extends React.HTMLAttributes {
+ /** 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
+ /** Fade out toward bottom (0 = no fade) */
+ fadeBottom?: number
+ /** Glyph color (any CSS color). Pass a theme-aware value from the consumer. */
+ color?: string
+}
+
+/**
+ * GlyphMatrix — an animated grid of subtly shifting glyphs.
+ * Pass a `color` prop (e.g. driven by next-themes) to adapt it to
+ * light and dark modes.
+ */
+export function GlyphMatrix({
+ glyphs = "01·•+*/\\<>=",
+ cellSize = 14,
+ mutationRate = 0.04,
+ interval = 90,
+ className,
+ fadeBottom = 0.6,
+ color = "#6B7280",
+ style,
+ ...props
+}: GlyphMatrixProps) {
+ const canvasRef = useRef(null)
+ // Current glyph color as RGBA (a in 0-1). Kept in a ref so a color change
+ // (e.g. theme toggle) recolors the next frame without restarting the
+ // animation. Defaults to #6B7280.
+ const rgbaRef = useRef({ r: 107, g: 114, b: 128, a: 1 })
+
+ // Resolve the CSS color string to RGBA (handles hex, rgb, hsl, oklch, ...).
+ useEffect(() => {
+ const probe = document.createElement("canvas")
+ probe.width = 1
+ probe.height = 1
+ const probeCtx = probe.getContext("2d")
+ if (!probeCtx) return
+ // Seed with the default so an invalid color falls back to it: the 2d
+ // context keeps the previous fillStyle when assigned an invalid value
+ // instead of silently turning black.
+ probeCtx.fillStyle = "#6B7280"
+ probeCtx.fillStyle = color
+ probeCtx.fillRect(0, 0, 1, 1)
+ const [r, g, b, a] = probeCtx.getImageData(0, 0, 1, 1).data
+ rgbaRef.current = { r, g, b, a: a / 255 }
+ }, [color])
+
+ 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 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)
+ }
+
+ const draw = () => {
+ const { clientWidth: w, clientHeight: h } = canvas
+ ctx.clearRect(0, 0, w, h)
+
+ ctx.font = `${cellSize - 2}px ui-monospace, SFMono-Regular, Menlo, monospace`
+ ctx.textBaseline = "top"
+
+ const { r, g, b, a: colorAlpha } = rgbaRef.current
+ 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 * colorAlpha
+ 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)
+
+ return () => {
+ stopped = true
+ cancelAnimationFrame(raf)
+ ro.disconnect()
+ }
+ }, [glyphs, cellSize, mutationRate, interval, fadeBottom])
+
+ return (
+
+ )
+}
diff --git a/apps/www/registry/registry-examples.ts b/apps/www/registry/registry-examples.ts
index a112db472..eb8c8ff10 100644
--- a/apps/www/registry/registry-examples.ts
+++ b/apps/www/registry/registry-examples.ts
@@ -608,6 +608,20 @@ 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"],
+ dependencies: ["next-themes"],
+ 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",