{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "glyph-matrix",
	"title": "Glyph Matrix",
	"type": "registry:block",
	"description": "An animated grid of subtly shifting glyphs on a canvas, with a theme-aware color driven by the consumer.",
	"dependencies": [
		"runed"
	],
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport { cn } from \"$UTILS$\";\n\timport { watch } from \"runed\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tinterface GlyphMatrixProps extends HTMLAttributes<HTMLCanvasElement> {\n\t\t/** Characters to randomly pick from */\n\t\tglyphs?: string;\n\t\t/** Cell size in px (also font size) */\n\t\tcellSize?: number;\n\t\t/** Probability (0-1) a cell mutates each tick */\n\t\tmutationRate?: number;\n\t\t/** Tick interval in ms */\n\t\tinterval?: number;\n\t\t/** Fade out toward bottom (0 = no fade) */\n\t\tfadeBottom?: number;\n\t\t/** Glyph color (any CSS color). Use a lighter color for a brighter matrix look. */\n\t\tcolor?: string;\n\t\t/** Brightness multiplier. 1 = original, 1.15-1.25 = subtle, 1.4-1.6 = strong, 1.8+ = very bright. */\n\t\tboost?: number;\n\t\tclass?: string;\n\t}\n\n\ttype RGBA = {\n\t\tr: number;\n\t\tg: number;\n\t\tb: number;\n\t\ta: number;\n\t};\n\n\tconst DEFAULT_GLYPHS = \"01\\u00B7\\u2022+*/\\\\<>=\";\n\tconst DEFAULT_COLOR = \"#6B7280\";\n\tconst DEFAULT_RGBA: RGBA = { r: 107, g: 114, b: 128, a: 1 };\n\n\tlet {\n\t\tglyphs = DEFAULT_GLYPHS,\n\t\tcellSize = 14,\n\t\tmutationRate = 0.04,\n\t\tinterval = 90,\n\t\tclass: className = \"\",\n\t\tfadeBottom = 0.6,\n\t\tcolor = DEFAULT_COLOR,\n\t\tboost = 1.2,\n\t\tstyle,\n\t\t...props\n\t}: GlyphMatrixProps = $props();\n\n\tlet canvas: HTMLCanvasElement | null = $state(null);\n\tlet rgba = DEFAULT_RGBA;\n\tlet alphaBoost = 1;\n\n\tfunction randomGlyph(pool: string) {\n\t\treturn pool[Math.floor(Math.random() * pool.length)] ?? \" \";\n\t}\n\n\tfunction randomAlpha(range: number) {\n\t\treturn 0.05 + Math.random() * range;\n\t}\n\n\tfunction normalizeBoost(amount: number) {\n\t\treturn Math.max(0, amount);\n\t}\n\n\tfunction boostChannel(value: number, amount: number) {\n\t\tconst normalized = normalizeBoost(amount);\n\t\tconst mix = Math.max(0, normalized - 1);\n\t\treturn Math.min(255, Math.round(value + (255 - value) * mix));\n\t}\n\n\tfunction getAlphaBoost(amount: number) {\n\t\tconst normalized = normalizeBoost(amount);\n\t\treturn normalized <= 1 ? normalized : 1 + (normalized - 1) * 1.75;\n\t}\n\n\t// Resolve CSS colors once so the draw loop can stay cheap.\n\t// `boost` brightens the glyph color and also makes the glyphs read more clearly.\n\tfunction resolveColor(value: string, amount: number): RGBA {\n\t\tif (typeof document === \"undefined\") return DEFAULT_RGBA;\n\n\t\tconst probe = document.createElement(\"canvas\");\n\t\tconst context = probe.getContext(\"2d\");\n\n\t\tif (!context) return DEFAULT_RGBA;\n\n\t\tcontext.fillStyle = DEFAULT_COLOR;\n\t\tcontext.fillStyle = value;\n\t\tcontext.fillRect(0, 0, 1, 1);\n\n\t\tconst [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;\n\t\treturn {\n\t\t\tr: boostChannel(r, amount),\n\t\t\tg: boostChannel(g, amount),\n\t\t\tb: boostChannel(b, amount),\n\t\t\ta: a / 255,\n\t\t};\n\t}\n\n\twatch(\n\t\t[() => color, () => boost],\n\t\t([nextColor, nextBoost]) => {\n\t\t\talphaBoost = getAlphaBoost(nextBoost);\n\t\t\trgba = resolveColor(nextColor, nextBoost);\n\t\t}\n\t);\n\n\twatch(\n\t\t[\n\t\t\t() => canvas,\n\t\t\t() => glyphs,\n\t\t\t() => cellSize,\n\t\t\t() => mutationRate,\n\t\t\t() => interval,\n\t\t\t() => fadeBottom,\n\t\t],\n\t\t([\n\t\t\tcurrentCanvas,\n\t\t\tcurrentGlyphs,\n\t\t\tcurrentCellSize,\n\t\t\tcurrentMutationRate,\n\t\t\tcurrentInterval,\n\t\t\tcurrentFadeBottom,\n\t\t]) => {\n\t\t\tif (!currentCanvas) return;\n\n\t\t\tconst context = currentCanvas.getContext(\"2d\");\n\t\t\tif (!context) return;\n\n\t\t\tconst glyphPool = currentGlyphs.length > 0 ? currentGlyphs : DEFAULT_GLYPHS;\n\n\t\t\tlet cols = 0;\n\t\t\tlet rows = 0;\n\t\t\tlet cells: string[] = [];\n\t\t\tlet alphas: number[] = [];\n\t\t\tlet frame = 0;\n\t\t\tlet lastTick = 0;\n\n\t\t\t// Rebuild the matrix when the canvas size changes.\n\t\t\tconst resize = () => {\n\t\t\t\tconst dpr = window.devicePixelRatio || 1;\n\t\t\t\tconst { clientWidth: width, clientHeight: height } = currentCanvas;\n\n\t\t\t\tcurrentCanvas.width = width * dpr;\n\t\t\t\tcurrentCanvas.height = height * dpr;\n\t\t\t\tcontext.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n\t\t\t\tcols = Math.ceil(width / currentCellSize);\n\t\t\t\trows = Math.ceil(height / currentCellSize);\n\n\t\t\t\tconst total = cols * rows;\n\t\t\t\tcells = Array.from({ length: total }, () => randomGlyph(glyphPool));\n\t\t\t\talphas = Array.from({ length: total }, () => randomAlpha(0.35));\n\t\t\t};\n\n\t\t\t// Draw the current glyph state using the latest resolved color.\n\t\t\tconst draw = () => {\n\t\t\t\tconst { clientWidth: width, clientHeight: height } = currentCanvas;\n\t\t\t\tcontext.clearRect(0, 0, width, height);\n\t\t\t\tcontext.font = `${Math.max(1, currentCellSize - 2)}px ui-monospace, SFMono-Regular, Menlo, monospace`;\n\t\t\t\tcontext.textBaseline = \"top\";\n\n\t\t\t\tconst { r, g, b, a: colorAlpha } = rgba;\n\t\t\t\tfor (let y = 0; y < rows; y++) {\n\t\t\t\t\tconst fade = currentFadeBottom > 0 ? 1 - (y / rows) * currentFadeBottom : 1;\n\n\t\t\t\t\tfor (let x = 0; x < cols; x++) {\n\t\t\t\t\t\tconst index = y * cols + x;\n\t\t\t\t\t\tconst alpha = Math.min(1, alphas[index] * fade * colorAlpha * alphaBoost);\n\n\t\t\t\t\t\tcontext.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;\n\t\t\t\t\t\tcontext.fillText(cells[index] ?? \" \", x * currentCellSize, y * currentCellSize);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Mutate a few cells on each animation tick.\n\t\t\tconst tick = (time: number) => {\n\t\t\t\tif (time - lastTick >= currentInterval) {\n\t\t\t\t\tlastTick = time;\n\n\t\t\t\t\tconst total = cols * rows;\n\t\t\t\t\tif (total > 0) {\n\t\t\t\t\t\tconst mutations = Math.max(1, Math.floor(total * currentMutationRate));\n\n\t\t\t\t\t\tfor (let count = 0; count < mutations; count++) {\n\t\t\t\t\t\t\tconst index = Math.floor(Math.random() * total);\n\t\t\t\t\t\t\tcells[index] = randomGlyph(glyphPool);\n\t\t\t\t\t\t\talphas[index] = randomAlpha(0.45);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tdraw();\n\t\t\t\t}\n\n\t\t\t\tframe = requestAnimationFrame(tick);\n\t\t\t};\n\n\t\t\tresize();\n\t\t\tdraw();\n\t\t\tframe = requestAnimationFrame(tick);\n\n\t\t\tconst observer = new ResizeObserver(() => {\n\t\t\t\tresize();\n\t\t\t\tdraw();\n\t\t\t});\n\n\t\t\tobserver.observe(currentCanvas);\n\n\t\t\treturn () => {\n\t\t\t\tcancelAnimationFrame(frame);\n\t\t\t\tobserver.disconnect();\n\t\t\t};\n\t\t}\n\t);\n</script>\n\n<canvas\n\tbind:this={canvas}\n\tclass={cn(\"pointer-events-none\", className)}\n\tstyle={`width: 100%; height: 100%; display: block; ${style ?? \"\"}`}\n\taria-hidden=\"true\"\n\t{...props}\n></canvas>\n",
			"type": "registry:component",
			"target": "magic/glyph-matrix/glyph-matrix.svelte"
		},
		{
			"content": "import GlyphMatrix from './glyph-matrix.svelte';\r\n\r\nexport { GlyphMatrix }",
			"type": "registry:file",
			"target": "magic/glyph-matrix/index.ts"
		}
	]
}