{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "opentui/image",
  "title": "Image",
  "description": "Image renderer with iTerm2/Kitty protocol and ASCII fallback",
  "dependencies": [
    "@opentui/react"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/opentui/use-theme.json"
  ],
  "files": [
    {
      "path": "registry/opentui/ui/image.tsx",
      "content": "/* @jsxImportSource @opentui/react */\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { useTheme } from \"@/hooks/use-theme\";\n\nexport type ImageProtocol = \"auto\" | \"iterm2\" | \"kitty\" | \"ascii\";\n\nexport interface ImageProps {\n  src: string;\n  width?: number;\n  height?: number;\n  protocol?: ImageProtocol;\n  alt?: string;\n}\n\nconst detectProtocol = (): Exclude<ImageProtocol, \"auto\"> => {\n  const termProgram = process.env[\"TERM_PROGRAM\"] ?? \"\";\n  const term = process.env[\"TERM\"] ?? \"\";\n\n  if (termProgram === \"iTerm.app\") {\n    return \"iterm2\";\n  }\n  if (term === \"xterm-kitty\" || process.env[\"KITTY_WINDOW_ID\"]) {\n    return \"kitty\";\n  }\n  return \"ascii\";\n};\n\nconst writeIterm2 = (src: string, width?: number, height?: number): void => {\n  try {\n    const data = fs.readFileSync(src);\n    const base64 = data.toString(\"base64\");\n    const filename = path.basename(src);\n    const size = data.length;\n    let args = `name=${Buffer.from(filename).toString(\"base64\")};size=${size};inline=1`;\n    if (width) {\n      args += `;width=${width}`;\n    }\n    if (height) {\n      args += `;height=${height}`;\n    }\n    process.stdout.write(`\\u001B]1337;File=${args}:${base64}\\u0007`);\n  } catch {\n    /* noop */\n  }\n};\n\nconst writeKitty = (src: string, width?: number, height?: number): void => {\n  try {\n    const data = fs.readFileSync(src);\n    const base64 = data.toString(\"base64\");\n    const chunkSize = 4096;\n    let offset = 0;\n    let first = true;\n    while (offset < base64.length) {\n      const chunk = base64.slice(offset, offset + chunkSize);\n      offset += chunkSize;\n      const more = offset < base64.length ? 1 : 0;\n      let header: string;\n      if (first) {\n        const wPart = width ? `,c=${width}` : \"\";\n        const hPart = height ? `,r=${height}` : \"\";\n        header = `a=T,f=100,m=${more}${wPart}${hPart}`;\n        first = false;\n      } else {\n        header = `m=${more}`;\n      }\n      process.stdout.write(`\\u001B_G${header};${chunk}\\u001B\\\\`);\n    }\n  } catch {\n    /* noop */\n  }\n};\n\nexport const Image = ({\n  src,\n  width = 20,\n  height,\n  protocol = \"auto\",\n  alt,\n}: ImageProps) => {\n  const theme = useTheme();\n  const [, setRendered] = useState(false);\n  const [renderError, setRenderError] = useState<string | null>(null);\n\n  const resolvedProtocol = protocol === \"auto\" ? detectProtocol() : protocol;\n  const filename = path.basename(src);\n  const ext = path.extname(src).toLowerCase();\n\n  useEffect(() => {\n    if (resolvedProtocol === \"ascii\") {\n      setRendered(true);\n      return;\n    }\n    try {\n      if (resolvedProtocol === \"iterm2\") {\n        writeIterm2(src, width, height);\n        setRendered(true);\n      } else if (resolvedProtocol === \"kitty\") {\n        writeKitty(src, width, height);\n        setRendered(true);\n      }\n    } catch (error) {\n      setRenderError(error instanceof Error ? error.message : String(error));\n    }\n  }, [src, resolvedProtocol, width, height]);\n\n  if (resolvedProtocol === \"ascii\" || renderError) {\n    const boxWidth = width ?? 20;\n    const topBottom = \"─\".repeat(boxWidth - 2);\n    const empty = \" \".repeat(boxWidth - 2);\n    const label = (alt ?? filename)\n      .slice(0, boxWidth - 4)\n      .padStart(\n        Math.floor((boxWidth - 2) / 2) +\n          Math.floor((alt ?? filename).slice(0, boxWidth - 4).length / 2)\n      );\n\n    const innerRows = 3;\n    const displayLines: string[] = [\n      `┌${topBottom}┐`,\n      ...Array.from({ length: Math.floor(innerRows / 2) }, () => `│${empty}│`),\n      `│ ${label.padEnd(boxWidth - 3)}│`,\n      ...Array.from({ length: Math.ceil(innerRows / 2) }, () => `│${empty}│`),\n      `└${topBottom}┘`,\n    ];\n\n    return (\n      <box flexDirection=\"column\" gap={0}>\n        {displayLines.map((line, i) => (\n          <text key={i} fg={theme.colors.border}>\n            {line}\n          </text>\n        ))}\n        {alt && <text fg=\"#666\">{alt}</text>}\n        <text fg=\"#666\">{`[${resolvedProtocol === \"ascii\" ? \"ascii fallback\" : `error: ${renderError}`}] ${ext}`}</text>\n      </box>\n    );\n  }\n\n  return (\n    <box flexDirection=\"column\" gap={0}>\n      <text fg=\"#666\">{`${alt ?? filename} [${resolvedProtocol}]`}</text>\n    </box>\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/image.tsx"
    }
  ],
  "categories": [
    "utility"
  ],
  "type": "registry:ui"
}