{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "demo-matrix-image-to-frame",
  "title": "Matrix Image to Frame",
  "description": "Upload an image and export it as a binary Frame (0s and 1s) for use with the Matrix component.",
  "registryDependencies": [
    "@unlumen-ui/matrix"
  ],
  "files": [
    {
      "path": "registry/demo/components/unlumen/matrix-image-to-frame/index.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { UnlumenSlider } from \"@/components/ui/unlumen-slider\";\nimport { type Frame, Matrix } from \"@/components/unlumen-ui/matrix\";\n\nconst DEFAULT_ROWS = 16;\nconst DEFAULT_COLS = 16;\nconst MIN_SIZE = 4;\nconst MAX_SIZE = 32;\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 loadImageElement(file: File): Promise<HTMLImageElement> {\n  return new Promise((resolve, reject) => {\n    const url = URL.createObjectURL(file);\n    const img = new Image();\n    img.onload = () => {\n      URL.revokeObjectURL(url);\n      resolve(img);\n    };\n    img.onerror = () => {\n      URL.revokeObjectURL(url);\n      reject(new Error(\"Failed to load image\"));\n    };\n    img.src = url;\n  });\n}\n\nexport default function MatrixImageToFrameDemo() {\n  const [frame, setFrame] = useState<Frame | null>(null);\n  const [rows, setRows] = useState(DEFAULT_ROWS);\n  const [cols, setCols] = useState(DEFAULT_COLS);\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\n  const imageRef = useRef<HTMLImageElement | null>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const recompute = useCallback(\n    (img: HTMLImageElement, r: number, c: number, t: number, inv: boolean) => {\n      setFrame(imageToFrame(img, r, c, t, inv));\n    },\n    [],\n  );\n\n  const handleFile = useCallback(\n    async (file: File) => {\n      if (!file.type.startsWith(\"image/\")) return;\n      const img = await loadImageElement(file);\n      imageRef.current = img;\n      recompute(img, rows, cols, threshold, invert);\n    },\n    [rows, cols, threshold, invert, recompute],\n  );\n\n  const handleRows = useCallback(\n    (v: number) => {\n      setRows(v);\n      if (imageRef.current)\n        recompute(imageRef.current, v, cols, threshold, invert);\n    },\n    [cols, threshold, invert, recompute],\n  );\n\n  const handleCols = useCallback(\n    (v: number) => {\n      setCols(v);\n      if (imageRef.current)\n        recompute(imageRef.current, rows, v, threshold, invert);\n    },\n    [rows, threshold, invert, recompute],\n  );\n\n  const handleThreshold = useCallback(\n    (v: number) => {\n      setThreshold(v);\n      if (imageRef.current) recompute(imageRef.current, rows, cols, v, invert);\n    },\n    [rows, cols, invert, recompute],\n  );\n\n  const handleInvert = useCallback(() => {\n    const next = !invert;\n    setInvert(next);\n    if (imageRef.current)\n      recompute(imageRef.current, rows, cols, threshold, next);\n  }, [invert, rows, cols, threshold, recompute]);\n\n  const handleCopy = useCallback(() => {\n    if (!frame) return;\n    navigator.clipboard.writeText(frameToCode(frame));\n    setCopied(true);\n    setTimeout(() => setCopied(false), 1800);\n  }, [frame]);\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-6 p-8 min-h-[420px]\">\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-64 h-48 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=\"28\"\n            height=\"28\"\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-5 w-full max-w-sm\">\n          <Matrix\n            rows={rows}\n            cols={cols}\n            pattern={frame}\n            size={pixelSize}\n            gap={1}\n            ariaLabel=\"image to matrix preview\"\n          />\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) => handleRows(v as number)}\n                min={MIN_SIZE}\n                max={MAX_SIZE}\n                step={1}\n                label=\"rows\"\n                valuePosition=\"right\"\n                className=\"flex-1\"\n              />\n              <UnlumenSlider\n                value={cols}\n                onChange={(v) => handleCols(v as number)}\n                min={MIN_SIZE}\n                max={MAX_SIZE}\n                step={1}\n                label=\"cols\"\n                valuePosition=\"right\"\n                className=\"flex-1\"\n              />\n            </div>\n\n            <UnlumenSlider\n              value={threshold}\n              onChange={(v) => handleThreshold(v as number)}\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\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center gap-3\">\n                <button\n                  onClick={handleInvert}\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={handleCopy}\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\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",
      "type": "registry:example"
    }
  ],
  "type": "registry:example"
}