{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "demo-matrix",
  "title": "Matrix Demo",
  "description": "Interactive demo showcasing real-time clock, animation presets, VU meter, and a pixel draw pad.",
  "registryDependencies": [
    "@unlumen-ui/matrix"
  ],
  "files": [
    {
      "path": "registry/demo/components/unlumen/matrix/index.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { UnlumenSlider } from \"@/components/ui/unlumen-slider\";\nimport {\n  type Frame,\n  Matrix,\n  digits,\n  emptyFrame,\n  loader,\n  pulse,\n  setPixel,\n  snake,\n  wave,\n} from \"@/components/unlumen-ui/matrix\";\n\nconst COLON: Frame = [\n  [0, 0, 0],\n  [0, 0, 0],\n  [0, 1, 0],\n  [0, 0, 0],\n  [0, 1, 0],\n  [0, 0, 0],\n  [0, 0, 0],\n];\n\nconst COLON_OFF: Frame = [\n  [0, 0, 0],\n  [0, 0, 0],\n  [0, 0, 0],\n  [0, 0, 0],\n  [0, 0, 0],\n  [0, 0, 0],\n  [0, 0, 0],\n];\n\nfunction ClockDisplay({ size = 10, gap = 2 }: { size?: number; gap?: number }) {\n  const [now, setNow] = useState<Date | null>(null);\n  const [colonOn, setColonOn] = useState(true);\n\n  useEffect(() => {\n    setNow(new Date());\n    const id = setInterval(() => {\n      setNow(new Date());\n      setColonOn((v) => !v);\n    }, 1000);\n    return () => clearInterval(id);\n  }, []);\n\n  if (!now) {\n    return (\n      <div style={{ height: 7 * (size + gap) - gap }} className=\"opacity-0\" />\n    );\n  }\n\n  const h = now.getHours();\n  const m = now.getMinutes();\n  const s = now.getSeconds();\n\n  const parts: Array<{ type: \"digit\"; value: number } | { type: \"colon\" }> = [\n    { type: \"digit\", value: Math.floor(h / 10) },\n    { type: \"digit\", value: h % 10 },\n    { type: \"colon\" },\n    { type: \"digit\", value: Math.floor(m / 10) },\n    { type: \"digit\", value: m % 10 },\n    { type: \"colon\" },\n    { type: \"digit\", value: Math.floor(s / 10) },\n    { type: \"digit\", value: s % 10 },\n  ];\n\n  return (\n    <div className=\"flex items-center gap-1\">\n      {parts.map((part, i) =>\n        part.type === \"colon\" ? (\n          <Matrix\n            key={i}\n            rows={7}\n            cols={3}\n            pattern={colonOn ? COLON : COLON_OFF}\n            size={size}\n            gap={gap}\n            ariaLabel=\":\"\n          />\n        ) : (\n          <Matrix\n            key={i}\n            rows={7}\n            cols={5}\n            pattern={digits[part.value]}\n            size={size}\n            gap={gap}\n            ariaLabel={String(part.value)}\n          />\n        ),\n      )}\n    </div>\n  );\n}\n\nconst NUM_BANDS = 16;\n\nfunction useVuLevels(numBands: number) {\n  const [levels, setLevels] = useState<number[]>(() =>\n    Array.from({ length: numBands }, (_, i) => {\n      const t = i / (numBands - 1);\n      return 0.15 + Math.sin(t * Math.PI) * 0.55;\n    }),\n  );\n\n  const frameRef = useRef(0);\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      frameRef.current++;\n      const f = frameRef.current;\n      setLevels((prev) =>\n        prev.map((v, i) => {\n          const bass =\n            Math.abs(Math.sin(f * 0.07 + i * 0.35)) *\n            (i < numBands * 0.4 ? 0.9 : 0.5);\n          const mid =\n            Math.abs(Math.sin(f * 0.13 + i * 0.6)) *\n            (i >= numBands * 0.3 && i < numBands * 0.7 ? 0.7 : 0.2);\n          const treble =\n            Math.abs(Math.sin(f * 0.21 + i * 0.9)) *\n            (i >= numBands * 0.6 ? 0.6 : 0.1);\n          const noise = (Math.random() - 0.5) * 0.08;\n          const target = Math.max(\n            0.04,\n            Math.min(1, bass + mid + treble + noise),\n          );\n          return v + (target - v) * 0.25;\n        }),\n      );\n    }, 60);\n    return () => clearInterval(id);\n  }, [numBands]);\n\n  return levels;\n}\n\n// ─── Preset showcase ──────────────────────────────────────────────────────────\n\nconst PRESETS = [\n  { name: \"Loader\", frames: loader },\n  { name: \"Wave\", frames: wave },\n  { name: \"Snake\", frames: snake },\n  { name: \"Pulse\", frames: pulse },\n] as const;\n\n// ─── Draw pad ─────────────────────────────────────────────────────────────────\n\nconst PAD_ROWS = 7;\nconst PAD_COLS = 7;\n\nfunction DrawPad({ size = 24, gap = 3 }: { size?: number; gap?: number }) {\n  const [grid, setGrid] = useState<Frame>(() => emptyFrame(PAD_ROWS, PAD_COLS));\n  const painting = useRef(false);\n  const lastCell = useRef<string | null>(null);\n\n  function toggleCell(row: number, col: number, forceOn?: boolean) {\n    const key = `${row}-${col}`;\n    if (key === lastCell.current) return;\n    lastCell.current = key;\n    setGrid((prev) => {\n      const next = prev.map((r) => [...r]);\n      next[row][col] =\n        forceOn != null ? (forceOn ? 1 : 0) : prev[row][col] > 0 ? 0 : 1;\n      return next;\n    });\n  }\n\n  const cellW = size + gap;\n  const svgW = PAD_COLS * cellW - gap;\n  const svgH = PAD_ROWS * cellW - gap;\n\n  function getCellFromEvent(\n    e: React.MouseEvent<SVGSVGElement>,\n  ): [number, number] | null {\n    const rect = e.currentTarget.getBoundingClientRect();\n    const x = e.clientX - rect.left;\n    const y = e.clientY - rect.top;\n    const col = Math.floor(x / cellW);\n    const row = Math.floor(y / cellW);\n    if (row < 0 || row >= PAD_ROWS || col < 0 || col >= PAD_COLS) return null;\n    return [row, col];\n  }\n\n  return (\n    <div className=\"flex flex-col items-center gap-3\">\n      <svg\n        width={svgW}\n        height={svgH}\n        viewBox={`0 0 ${svgW} ${svgH}`}\n        className=\"cursor-crosshair touch-none select-none\"\n        onMouseDown={(e) => {\n          painting.current = true;\n          lastCell.current = null;\n          const cell = getCellFromEvent(e);\n          if (cell) toggleCell(cell[0], cell[1]);\n        }}\n        onMouseMove={(e) => {\n          if (!painting.current) return;\n          const cell = getCellFromEvent(e);\n          if (cell) toggleCell(cell[0], cell[1], true);\n        }}\n        onMouseUp={() => {\n          painting.current = false;\n          lastCell.current = null;\n        }}\n        onMouseLeave={() => {\n          painting.current = false;\n          lastCell.current = null;\n        }}\n      >\n        {Array.from({ length: PAD_ROWS }, (_, row) =>\n          Array.from({ length: PAD_COLS }, (_, col) => {\n            const on = grid[row]?.[col] > 0;\n            const cx = col * cellW + size / 2;\n            const cy = row * cellW + size / 2;\n            return (\n              <circle\n                key={`${row}-${col}`}\n                cx={cx}\n                cy={cy}\n                r={(size / 2) * 0.85}\n                className={cn(\n                  \"transition-all duration-100\",\n                  on\n                    ? \"fill-foreground drop-shadow-[0_0_4px_currentColor]\"\n                    : \"fill-muted-foreground/20 hover:fill-muted-foreground/40\",\n                )}\n              />\n            );\n          }),\n        )}\n      </svg>\n      <button\n        onClick={() => setGrid(emptyFrame(PAD_ROWS, PAD_COLS))}\n        className=\"text-[10px] font-mono tracking-widest uppercase text-muted-foreground hover:text-foreground transition-colors\"\n      >\n        clear\n      </button>\n    </div>\n  );\n}\n\n// ─── Image to Frame ───────────────────────────────────────────────────────────\n\nconst IMG_MIN = 4;\nconst IMG_MAX = 32;\nconst IMG_DEFAULT = 16;\n\nfunction imageToFrame(\n  image: HTMLImageElement,\n  rows: number,\n  cols: number,\n  threshold: number,\n  invert: boolean,\n): Frame {\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = cols;\n  canvas.height = rows;\n  const ctx = canvas.getContext(\"2d\")!;\n  ctx.imageSmoothingEnabled = true;\n  ctx.imageSmoothingQuality = \"high\";\n  ctx.drawImage(image, 0, 0, cols, rows);\n  const { data } = ctx.getImageData(0, 0, cols, rows);\n  const frame: Frame = [];\n  for (let r = 0; r < rows; r++) {\n    const row: number[] = [];\n    for (let c = 0; c < cols; c++) {\n      const i = (r * cols + c) * 4;\n      const luma =\n        (0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]) / 255;\n      const alpha = data[i + 3] / 255;\n      const value = invert ? luma : 1 - luma;\n      row.push(value * alpha >= threshold ? 1 : 0);\n    }\n    frame.push(row);\n  }\n  return frame;\n}\n\nfunction frameToCode(frame: Frame): string {\n  const inner = frame.map((row) => `  [${row.join(\", \")}]`).join(\",\\n\");\n  return `const frame: Frame = [\\n${inner},\\n];`;\n}\n\nfunction ImageToFramePanel() {\n  const [frame, setFrame] = useState<Frame | null>(null);\n  const [rows, setRows] = useState(IMG_DEFAULT);\n  const [cols, setCols] = useState(IMG_DEFAULT);\n  const [threshold, setThreshold] = useState(0.5);\n  const [invert, setInvert] = useState(false);\n  const [isDragging, setIsDragging] = useState(false);\n  const [copied, setCopied] = useState(false);\n  const imageRef = useRef<HTMLImageElement | null>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  function recompute(\n    img: HTMLImageElement,\n    r: number,\n    c: number,\n    t: number,\n    inv: boolean,\n  ) {\n    setFrame(imageToFrame(img, r, c, t, inv));\n  }\n\n  function handleFile(file: File) {\n    if (!file.type.startsWith(\"image/\")) return;\n    const url = URL.createObjectURL(file);\n    const img = new Image();\n    img.onload = () => {\n      URL.revokeObjectURL(url);\n      imageRef.current = img;\n      recompute(img, rows, cols, threshold, invert);\n    };\n    img.onerror = () => URL.revokeObjectURL(url);\n    img.src = url;\n  }\n\n  const pixelSize = Math.max(2, Math.floor(320 / Math.max(rows, cols)) - 1);\n\n  return (\n    <div className=\"flex flex-col items-center gap-5 w-full\">\n      {!frame ? (\n        <button\n          onClick={() => inputRef.current?.click()}\n          onDragOver={(e) => {\n            e.preventDefault();\n            setIsDragging(true);\n          }}\n          onDragLeave={() => setIsDragging(false)}\n          onDrop={(e) => {\n            e.preventDefault();\n            setIsDragging(false);\n            const file = e.dataTransfer.files[0];\n            if (file) handleFile(file);\n          }}\n          className={cn(\n            \"group flex flex-col items-center justify-center gap-3\",\n            \"w-56 h-40 rounded-xl border-2 border-dashed\",\n            \"transition-all duration-200 cursor-pointer\",\n            isDragging\n              ? \"border-foreground/40 bg-foreground/5\"\n              : \"border-border hover:border-foreground/30 hover:bg-muted/30\",\n          )}\n        >\n          <svg\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            className={cn(\n              \"transition-colors duration-200\",\n              isDragging\n                ? \"text-foreground/60\"\n                : \"text-muted-foreground group-hover:text-foreground/50\",\n            )}\n          >\n            <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" />\n            <circle cx=\"9\" cy=\"9\" r=\"2\" />\n            <path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\" />\n          </svg>\n          <span className=\"text-xs text-muted-foreground font-mono tracking-wide\">\n            {isDragging ? \"drop image\" : \"upload image\"}\n          </span>\n        </button>\n      ) : (\n        <div className=\"flex flex-col items-center gap-4 w-full max-w-xs\">\n          <Matrix\n            rows={rows}\n            cols={cols}\n            pattern={frame}\n            size={pixelSize}\n            gap={1}\n            ariaLabel=\"image to frame preview\"\n          />\n          <div className=\"flex flex-col gap-2.5 w-full\">\n            <div className=\"flex gap-4\">\n              <UnlumenSlider\n                value={rows}\n                onChange={(v) => {\n                  const n = v as number;\n                  setRows(n);\n                  if (imageRef.current)\n                    recompute(imageRef.current, n, cols, threshold, invert);\n                }}\n                min={IMG_MIN}\n                max={IMG_MAX}\n                step={1}\n                label=\"rows\"\n                valuePosition=\"right\"\n                className=\"flex-1\"\n              />\n              <UnlumenSlider\n                value={cols}\n                onChange={(v) => {\n                  const n = v as number;\n                  setCols(n);\n                  if (imageRef.current)\n                    recompute(imageRef.current, rows, n, threshold, invert);\n                }}\n                min={IMG_MIN}\n                max={IMG_MAX}\n                step={1}\n                label=\"cols\"\n                valuePosition=\"right\"\n                className=\"flex-1\"\n              />\n            </div>\n            <UnlumenSlider\n              value={threshold}\n              onChange={(v) => {\n                const n = v as number;\n                setThreshold(n);\n                if (imageRef.current)\n                  recompute(imageRef.current, rows, cols, n, invert);\n              }}\n              min={0.1}\n              max={0.9}\n              step={0.01}\n              label=\"threshold\"\n              valuePosition=\"right\"\n              formatValue={(v) => v.toFixed(2)}\n            />\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center gap-3\">\n                <button\n                  onClick={() => {\n                    const next = !invert;\n                    setInvert(next);\n                    if (imageRef.current)\n                      recompute(imageRef.current, rows, cols, threshold, next);\n                  }}\n                  className={cn(\n                    \"flex items-center gap-1.5 text-[10px] font-mono tracking-widest uppercase transition-colors\",\n                    invert\n                      ? \"text-foreground\"\n                      : \"text-muted-foreground hover:text-foreground/60\",\n                  )}\n                >\n                  <span\n                    className={cn(\n                      \"inline-block w-3 h-3 rounded-full border transition-all\",\n                      invert\n                        ? \"bg-foreground border-foreground\"\n                        : \"bg-transparent border-muted-foreground\",\n                    )}\n                  />\n                  invert\n                </button>\n                <button\n                  onClick={() => inputRef.current?.click()}\n                  className=\"text-[10px] font-mono tracking-widest uppercase text-muted-foreground hover:text-foreground transition-colors\"\n                >\n                  change\n                </button>\n              </div>\n              <button\n                onClick={() => {\n                  if (!frame) return;\n                  navigator.clipboard.writeText(frameToCode(frame));\n                  setCopied(true);\n                  setTimeout(() => setCopied(false), 1800);\n                }}\n                className={cn(\n                  \"text-[10px] font-mono tracking-widest uppercase transition-colors px-2.5 py-1 rounded border\",\n                  copied\n                    ? \"border-foreground/30 text-foreground bg-foreground/5\"\n                    : \"border-border text-muted-foreground hover:text-foreground hover:border-foreground/30\",\n                )}\n              >\n                {copied ? \"copied!\" : \"copy frame\"}\n              </button>\n            </div>\n          </div>\n        </div>\n      )}\n      <input\n        ref={inputRef}\n        type=\"file\"\n        accept=\"image/*\"\n        className=\"hidden\"\n        onChange={(e) => {\n          const file = e.target.files?.[0];\n          if (file) handleFile(file);\n          e.target.value = \"\";\n        }}\n      />\n    </div>\n  );\n}\n\n// ─── Tabs ─────────────────────────────────────────────────────────────────────\n\ntype Tab = \"clock\" | \"presets\" | \"vu\" | \"draw\" | \"image\";\n\nconst TABS: { id: Tab; label: string }[] = [\n  { id: \"clock\", label: \"Clock\" },\n  { id: \"presets\", label: \"Presets\" },\n  { id: \"vu\", label: \"VU Meter\" },\n  { id: \"draw\", label: \"Draw\" },\n  { id: \"image\", label: \"Image\" },\n];\n\n// ─── Main demo ────────────────────────────────────────────────────────────────\n\nexport default function MatrixDemo() {\n  const [tab, setTab] = useState<Tab>(\"clock\");\n  const [activePreset, setActivePreset] = useState(0);\n  const vuLevels = useVuLevels(NUM_BANDS);\n\n  useEffect(() => {\n    if (tab !== \"presets\") return;\n    const id = setInterval(\n      () => setActivePreset((p) => (p + 1) % PRESETS.length),\n      2800,\n    );\n    return () => clearInterval(id);\n  }, [tab]);\n\n  return (\n    <div className=\"flex flex-col items-center justify-center gap-8 p-8 min-h-[420px]\">\n      {/* Tab bar */}\n      <div className=\"flex gap-1 rounded-lg border border-border bg-muted/40 p-1\">\n        {TABS.map(({ id, label }) => (\n          <button\n            key={id}\n            onClick={() => setTab(id)}\n            className={cn(\n              \"rounded-md px-3 py-1.5 text-xs font-medium transition-all duration-200\",\n              tab === id\n                ? \"bg-background text-foreground shadow-sm\"\n                : \"text-muted-foreground hover:text-foreground\",\n            )}\n          >\n            {label}\n          </button>\n        ))}\n      </div>\n\n      {/* Panels */}\n      {tab === \"clock\" && (\n        <div className=\"flex flex-col items-center gap-4\">\n          <ClockDisplay size={10} gap={2} />\n          <p className=\"text-[10px] font-mono tracking-widest uppercase text-muted-foreground\">\n            Live clock · digit matrices\n          </p>\n        </div>\n      )}\n\n      {tab === \"presets\" && (\n        <div className=\"flex flex-col items-center gap-6\">\n          <div className=\"flex gap-8 items-end\">\n            {PRESETS.map((preset, i) => (\n              <button\n                key={preset.name}\n                onClick={() => setActivePreset(i)}\n                className=\"flex flex-col items-center gap-2 group\"\n              >\n                <Matrix\n                  rows={7}\n                  cols={7}\n                  frames={preset.frames}\n                  fps={i === activePreset ? 10 : 6}\n                  size={11}\n                  gap={2}\n                  className={cn(\n                    \"transition-opacity duration-300\",\n                    i === activePreset ? \"opacity-100\" : \"opacity-100\",\n                  )}\n                  ariaLabel={preset.name}\n                />\n                <span\n                  className={cn(\n                    \"text-[9px] font-mono tracking-widest uppercase transition-colors duration-300\",\n                    i === activePreset\n                      ? \"text-foreground\"\n                      : \"text-muted-foreground group-hover:text-foreground/60\",\n                  )}\n                >\n                  {preset.name}\n                </span>\n              </button>\n            ))}\n          </div>\n          <p className=\"text-[10px] font-mono tracking-widest uppercase text-muted-foreground\">\n            Click a preset to focus\n          </p>\n        </div>\n      )}\n\n      {tab === \"vu\" && (\n        <div className=\"flex flex-col items-center gap-4\">\n          <Matrix\n            rows={7}\n            cols={NUM_BANDS}\n            mode=\"vu\"\n            levels={vuLevels}\n            size={10}\n            gap={2}\n            ariaLabel=\"VU meter\"\n          />\n          <p className=\"text-[10px] font-mono tracking-widest uppercase text-muted-foreground\">\n            Simulated audio levels\n          </p>\n        </div>\n      )}\n\n      {tab === \"draw\" && (\n        <div className=\"flex flex-col items-center gap-4\">\n          <DrawPad size={22} gap={3} />\n          <p className=\"text-[10px] font-mono tracking-widest uppercase text-muted-foreground\">\n            Click or drag to draw\n          </p>\n        </div>\n      )}\n\n      {tab === \"image\" && <ImageToFramePanel />}\n    </div>\n  );\n}\n",
      "type": "registry:example"
    }
  ],
  "type": "registry:example"
}