{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "matrix", "files": [ { "path": "components/ui/matrix.tsx", "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useEffect, useMemo, useRef, useState } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type Frame = number[][]\ntype MatrixMode = \"default\" | \"vu\"\n\ninterface CellPosition {\n x: number\n y: number\n}\n\ninterface MatrixProps extends React.HTMLAttributes {\n rows: number\n cols: number\n pattern?: Frame\n frames?: Frame[]\n fps?: number\n autoplay?: boolean\n loop?: boolean\n size?: number\n gap?: number\n palette?: {\n on: string\n off: string\n }\n brightness?: number\n ariaLabel?: string\n onFrame?: (index: number) => void\n mode?: MatrixMode\n levels?: number[]\n}\n\nfunction clamp(value: number): number {\n return Math.max(0, Math.min(1, value))\n}\n\nfunction ensureFrameSize(frame: Frame, rows: number, cols: number): Frame {\n const result: Frame = []\n for (let r = 0; r < rows; r++) {\n const row = frame[r] || []\n result.push([])\n for (let c = 0; c < cols; c++) {\n result[r][c] = row[c] ?? 0\n }\n }\n return result\n}\n\nfunction useAnimation(\n frames: Frame[] | undefined,\n options: {\n fps: number\n autoplay: boolean\n loop: boolean\n onFrame?: (index: number) => void\n }\n): { frameIndex: number; isPlaying: boolean } {\n const [frameIndex, setFrameIndex] = useState(0)\n const [isPlaying, setIsPlaying] = useState(options.autoplay)\n const frameIdRef = useRef(undefined)\n const lastTimeRef = useRef(0)\n const accumulatorRef = useRef(0)\n\n useEffect(() => {\n if (!frames || frames.length === 0 || !isPlaying) {\n return\n }\n\n const frameInterval = 1000 / options.fps\n\n const animate = (currentTime: number) => {\n if (lastTimeRef.current === 0) {\n lastTimeRef.current = currentTime\n }\n\n const deltaTime = currentTime - lastTimeRef.current\n lastTimeRef.current = currentTime\n accumulatorRef.current += deltaTime\n\n if (accumulatorRef.current >= frameInterval) {\n accumulatorRef.current -= frameInterval\n\n setFrameIndex((prev) => {\n const next = prev + 1\n if (next >= frames.length) {\n if (options.loop) {\n options.onFrame?.(0)\n return 0\n } else {\n setIsPlaying(false)\n return prev\n }\n }\n options.onFrame?.(next)\n return next\n })\n }\n\n frameIdRef.current = requestAnimationFrame(animate)\n }\n\n frameIdRef.current = requestAnimationFrame(animate)\n\n return () => {\n if (frameIdRef.current) {\n cancelAnimationFrame(frameIdRef.current)\n }\n }\n }, [frames, isPlaying, options.fps, options.loop, options.onFrame])\n\n useEffect(() => {\n setFrameIndex(0)\n setIsPlaying(options.autoplay)\n lastTimeRef.current = 0\n accumulatorRef.current = 0\n }, [frames, options.autoplay])\n\n return { frameIndex, isPlaying }\n}\n\nfunction emptyFrame(rows: number, cols: number): Frame {\n return Array.from({ length: rows }, () => Array(cols).fill(0))\n}\n\nfunction setPixel(frame: Frame, row: number, col: number, value: number): void {\n if (row >= 0 && row < frame.length && col >= 0 && col < frame[0].length) {\n frame[row][col] = value\n }\n}\n\nexport const digits: Frame[] = [\n [\n [0, 1, 1, 1, 0],\n [1, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [0, 1, 1, 1, 0],\n ],\n [\n [0, 0, 1, 0, 0],\n [0, 1, 1, 0, 0],\n [0, 0, 1, 0, 0],\n [0, 0, 1, 0, 0],\n [0, 0, 1, 0, 0],\n [0, 0, 1, 0, 0],\n [0, 1, 1, 1, 0],\n ],\n [\n [0, 1, 1, 1, 0],\n [1, 0, 0, 0, 1],\n [0, 0, 0, 0, 1],\n [0, 0, 0, 1, 0],\n [0, 0, 1, 0, 0],\n [0, 1, 0, 0, 0],\n [1, 1, 1, 1, 1],\n ],\n [\n [0, 1, 1, 1, 0],\n [1, 0, 0, 0, 1],\n [0, 0, 0, 0, 1],\n [0, 0, 1, 1, 0],\n [0, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [0, 1, 1, 1, 0],\n ],\n [\n [0, 0, 0, 1, 0],\n [0, 0, 1, 1, 0],\n [0, 1, 0, 1, 0],\n [1, 0, 0, 1, 0],\n [1, 1, 1, 1, 1],\n [0, 0, 0, 1, 0],\n [0, 0, 0, 1, 0],\n ],\n [\n [1, 1, 1, 1, 1],\n [1, 0, 0, 0, 0],\n [1, 1, 1, 1, 0],\n [0, 0, 0, 0, 1],\n [0, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [0, 1, 1, 1, 0],\n ],\n [\n [0, 1, 1, 1, 0],\n [1, 0, 0, 0, 0],\n [1, 0, 0, 0, 0],\n [1, 1, 1, 1, 0],\n [1, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [0, 1, 1, 1, 0],\n ],\n [\n [1, 1, 1, 1, 1],\n [0, 0, 0, 0, 1],\n [0, 0, 0, 1, 0],\n [0, 0, 1, 0, 0],\n [0, 1, 0, 0, 0],\n [0, 1, 0, 0, 0],\n [0, 1, 0, 0, 0],\n ],\n [\n [0, 1, 1, 1, 0],\n [1, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [0, 1, 1, 1, 0],\n [1, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [0, 1, 1, 1, 0],\n ],\n [\n [0, 1, 1, 1, 0],\n [1, 0, 0, 0, 1],\n [1, 0, 0, 0, 1],\n [0, 1, 1, 1, 1],\n [0, 0, 0, 0, 1],\n [0, 0, 0, 0, 1],\n [0, 1, 1, 1, 0],\n ],\n]\n\nexport const chevronLeft: Frame = [\n [0, 0, 0, 1, 0],\n [0, 0, 1, 0, 0],\n [0, 1, 0, 0, 0],\n [0, 0, 1, 0, 0],\n [0, 0, 0, 1, 0],\n]\n\nexport const chevronRight: Frame = [\n [0, 1, 0, 0, 0],\n [0, 0, 1, 0, 0],\n [0, 0, 0, 1, 0],\n [0, 0, 1, 0, 0],\n [0, 1, 0, 0, 0],\n]\n\nexport const loader: Frame[] = (() => {\n const frames: Frame[] = []\n const size = 7\n const center = 3\n const radius = 2.5\n\n for (let frame = 0; frame < 12; frame++) {\n const f = emptyFrame(size, size)\n for (let i = 0; i < 8; i++) {\n const angle = (frame / 12) * Math.PI * 2 + (i / 8) * Math.PI * 2\n const x = Math.round(center + Math.cos(angle) * radius)\n const y = Math.round(center + Math.sin(angle) * radius)\n const brightness = 1 - i / 10\n setPixel(f, y, x, Math.max(0.2, brightness))\n }\n frames.push(f)\n }\n\n return frames\n})()\n\nexport const pulse: Frame[] = (() => {\n const frames: Frame[] = []\n const size = 7\n const center = 3\n\n for (let frame = 0; frame < 16; frame++) {\n const f = emptyFrame(size, size)\n const phase = (frame / 16) * Math.PI * 2\n const intensity = (Math.sin(phase) + 1) / 2\n\n setPixel(f, center, center, 1)\n\n const radius = Math.floor((1 - intensity) * 3) + 1\n for (let dy = -radius; dy <= radius; dy++) {\n for (let dx = -radius; dx <= radius; dx++) {\n const dist = Math.sqrt(dx * dx + dy * dy)\n if (Math.abs(dist - radius) < 0.7) {\n setPixel(f, center + dy, center + dx, intensity * 0.6)\n }\n }\n }\n\n frames.push(f)\n }\n\n return frames\n})()\n\nexport function vu(columns: number, levels: number[]): Frame {\n const rows = 7\n const frame = emptyFrame(rows, columns)\n\n for (let col = 0; col < Math.min(columns, levels.length); col++) {\n const level = Math.max(0, Math.min(1, levels[col]))\n const height = Math.floor(level * rows)\n\n for (let row = 0; row < rows; row++) {\n const rowFromBottom = rows - 1 - row\n if (rowFromBottom < height) {\n let brightness = 1\n if (row < rows * 0.3) {\n brightness = 1\n } else if (row < rows * 0.6) {\n brightness = 0.8\n } else {\n brightness = 0.6\n }\n frame[row][col] = brightness\n }\n }\n }\n\n return frame\n}\n\nexport const wave: Frame[] = (() => {\n const frames: Frame[] = []\n const rows = 7\n const cols = 7\n\n for (let frame = 0; frame < 24; frame++) {\n const f = emptyFrame(rows, cols)\n const phase = (frame / 24) * Math.PI * 2\n\n for (let col = 0; col < cols; col++) {\n const colPhase = (col / cols) * Math.PI * 2\n const height = Math.sin(phase + colPhase) * 2.5 + 3.5\n const row = Math.floor(height)\n\n if (row >= 0 && row < rows) {\n setPixel(f, row, col, 1)\n const frac = height - row\n if (row > 0) setPixel(f, row - 1, col, 1 - frac)\n if (row < rows - 1) setPixel(f, row + 1, col, frac)\n }\n }\n\n frames.push(f)\n }\n\n return frames\n})()\n\nexport const snake: Frame[] = (() => {\n const frames: Frame[] = []\n const rows = 7\n const cols = 7\n const path: Array<[number, number]> = []\n\n let x = 0\n let y = 0\n let dx = 1\n let dy = 0\n\n const visited = new Set()\n while (path.length < rows * cols) {\n path.push([y, x])\n visited.add(`${y},${x}`)\n\n const nextX = x + dx\n const nextY = y + dy\n\n if (\n nextX >= 0 &&\n nextX < cols &&\n nextY >= 0 &&\n nextY < rows &&\n !visited.has(`${nextY},${nextX}`)\n ) {\n x = nextX\n y = nextY\n } else {\n const newDx = -dy\n const newDy = dx\n dx = newDx\n dy = newDy\n\n const nextX = x + dx\n const nextY = y + dy\n\n if (\n nextX >= 0 &&\n nextX < cols &&\n nextY >= 0 &&\n nextY < rows &&\n !visited.has(`${nextY},${nextX}`)\n ) {\n x = nextX\n y = nextY\n } else {\n break\n }\n }\n }\n\n const snakeLength = 5\n for (let frame = 0; frame < path.length; frame++) {\n const f = emptyFrame(rows, cols)\n\n for (let i = 0; i < snakeLength; i++) {\n const idx = frame - i\n if (idx >= 0 && idx < path.length) {\n const [y, x] = path[idx]\n const brightness = 1 - i / snakeLength\n setPixel(f, y, x, brightness)\n }\n }\n\n frames.push(f)\n }\n\n return frames\n})()\n\nexport const Matrix = React.forwardRef(\n (\n {\n rows,\n cols,\n pattern,\n frames,\n fps = 12,\n autoplay = true,\n loop = true,\n size = 10,\n gap = 2,\n palette = {\n on: \"currentColor\",\n off: \"var(--muted-foreground)\",\n },\n brightness = 1,\n ariaLabel,\n onFrame,\n mode = \"default\",\n levels,\n className,\n ...props\n },\n ref\n ) => {\n const { frameIndex } = useAnimation(frames, {\n fps,\n autoplay: autoplay && !pattern,\n loop,\n onFrame,\n })\n\n const currentFrame = useMemo(() => {\n if (mode === \"vu\" && levels && levels.length > 0) {\n return ensureFrameSize(vu(cols, levels), rows, cols)\n }\n\n if (pattern) {\n return ensureFrameSize(pattern, rows, cols)\n }\n\n if (frames && frames.length > 0) {\n return ensureFrameSize(frames[frameIndex] || frames[0], rows, cols)\n }\n\n return ensureFrameSize([], rows, cols)\n }, [pattern, frames, frameIndex, rows, cols, mode, levels])\n\n const cellPositions = useMemo(() => {\n const positions: CellPosition[][] = []\n\n for (let row = 0; row < rows; row++) {\n positions[row] = []\n for (let col = 0; col < cols; col++) {\n positions[row][col] = {\n x: col * (size + gap),\n y: row * (size + gap),\n }\n }\n }\n\n return positions\n }, [rows, cols, size, gap])\n\n const svgDimensions = useMemo(() => {\n return {\n width: cols * (size + gap) - gap,\n height: rows * (size + gap) - gap,\n }\n }, [rows, cols, size, gap])\n\n const isAnimating = !pattern && frames && frames.length > 0\n\n return (\n \n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n \n \n\n \n\n {currentFrame.map((row, rowIndex) =>\n row.map((value, colIndex) => {\n const pos = cellPositions[rowIndex]?.[colIndex]\n if (!pos) return null\n\n const opacity = clamp(brightness * value)\n const isActive = opacity > 0.5\n const isOn = opacity > 0.05\n const fill = isOn\n ? \"url(#matrix-pixel-on)\"\n : \"url(#matrix-pixel-off)\"\n\n const scale = isActive ? 1.1 : 1\n const radius = (size / 2) * 0.9\n\n return (\n \n )\n })\n )}\n \n \n )\n }\n)\n\nMatrix.displayName = \"Matrix\"\n", "type": "registry:ui" } ], "type": "registry:ui" }