{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "matrix",
  "title": "Matrix",
  "description": "An LED dot matrix display component with SVG pixels, radial glow, built-in digit/animation presets, and a VU meter mode.",
  "files": [
    {
      "path": "registry/components/unlumen/matrix/index.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<HTMLDivElement> {\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<number | undefined>(undefined);\n  const lastTimeRef = useRef<number>(0);\n  const accumulatorRef = useRef<number>(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\nexport function emptyFrame(rows: number, cols: number): Frame {\n  return Array.from({ length: rows }, () => Array(cols).fill(0));\n}\n\nexport function setPixel(\n  frame: Frame,\n  row: number,\n  col: number,\n  value: number,\n): 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<string>();\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<HTMLDivElement, MatrixProps>(\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      <div\n        ref={ref}\n        role=\"img\"\n        aria-label={ariaLabel ?? \"matrix display\"}\n        aria-live={isAnimating ? \"polite\" : undefined}\n        className={cn(\"relative inline-block\", className)}\n        style={\n          {\n            \"--matrix-on\": palette.on,\n            \"--matrix-off\": palette.off,\n            \"--matrix-gap\": `${gap}px`,\n            \"--matrix-size\": `${size}px`,\n          } as React.CSSProperties\n        }\n        {...props}\n      >\n        <svg\n          width={svgDimensions.width}\n          height={svgDimensions.height}\n          viewBox={`0 0 ${svgDimensions.width} ${svgDimensions.height}`}\n          xmlns=\"http://www.w3.org/2000/svg\"\n          className=\"block\"\n          style={{ overflow: \"visible\" }}\n        >\n          <defs>\n            <radialGradient id=\"matrix-pixel-on\" cx=\"50%\" cy=\"50%\" r=\"50%\">\n              <stop offset=\"0%\" stopColor=\"var(--matrix-on)\" stopOpacity=\"1\" />\n              <stop\n                offset=\"70%\"\n                stopColor=\"var(--matrix-on)\"\n                stopOpacity=\"0.85\"\n              />\n              <stop\n                offset=\"100%\"\n                stopColor=\"var(--matrix-on)\"\n                stopOpacity=\"0.6\"\n              />\n            </radialGradient>\n\n            <radialGradient id=\"matrix-pixel-off\" cx=\"50%\" cy=\"50%\" r=\"50%\">\n              <stop\n                offset=\"0%\"\n                stopColor=\"var(--muted-foreground)\"\n                stopOpacity=\"1\"\n              />\n              <stop\n                offset=\"100%\"\n                stopColor=\"var(--muted-foreground)\"\n                stopOpacity=\"0.7\"\n              />\n            </radialGradient>\n\n            <filter\n              id=\"matrix-glow\"\n              x=\"-50%\"\n              y=\"-50%\"\n              width=\"200%\"\n              height=\"200%\"\n            >\n              <feGaussianBlur stdDeviation=\"2\" result=\"blur\" />\n              <feComposite in=\"SourceGraphic\" in2=\"blur\" operator=\"over\" />\n            </filter>\n          </defs>\n\n          <style>\n            {`\n              .matrix-pixel {\n                transition: opacity 300ms ease-out, transform 150ms ease-out;\n                transform-origin: center;\n                transform-box: fill-box;\n              }\n              .matrix-pixel-active {\n                filter: url(#matrix-glow);\n              }\n            `}\n          </style>\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                <circle\n                  key={`${rowIndex}-${colIndex}`}\n                  className={cn(\n                    \"matrix-pixel\",\n                    isActive && \"matrix-pixel-active\",\n                    !isOn && \"opacity-20 dark:opacity-[0.1]\",\n                  )}\n                  cx={pos.x + size / 2}\n                  cy={pos.y + size / 2}\n                  r={radius}\n                  fill={fill}\n                  opacity={isOn ? opacity : 0.1}\n                  style={{\n                    transform: `scale(${scale})`,\n                  }}\n                />\n              );\n            }),\n          )}\n        </svg>\n      </div>\n    );\n  },\n);\n\nMatrix.displayName = \"Matrix\";\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/matrix.tsx"
    }
  ],
  "type": "registry:ui"
}