{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/image",
  "title": "Image",
  "description": "Image renderer with iTerm2/Kitty protocol and ASCII fallback",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/accessibility.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/image.tsx",
      "content": "import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\nimport { useIsScreenReaderEnabled, useStdout, Box, Text } from \"ink\";\nimport React, { useEffect, useState } from \"react\";\n\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\nimport type { VisualAccessibilityProps } from \"@/lib/accessibility\";\n\nexport type ImageProtocol = \"auto\" | \"iterm2\" | \"kitty\" | \"ascii\";\n\ninterface ImageBaseProps {\n  src: string;\n  width?: number;\n  height?: number;\n  protocol?: ImageProtocol;\n}\n\nexport type ImageProps = ImageBaseProps & VisualAccessibilityProps;\n\ninterface WritableTerminal {\n  write(data: string): unknown;\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 = (\n  stdout: WritableTerminal,\n  src: string,\n  width?: number,\n  height?: number\n): 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\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\n    stdout.write(`\\u001B]1337;File=${args}:${base64}\\u0007`);\n  } catch {\n    /* noop */\n  }\n};\n\nconst writeKitty = (\n  stdout: WritableTerminal,\n  src: string,\n  width?: number,\n  height?: number\n): void => {\n  try {\n    const data = fs.readFileSync(src);\n    const base64 = data.toString(\"base64\");\n\n    const chunkSize = 4096;\n    let offset = 0;\n    let first = true;\n\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\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\n      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  \"aria-hidden\": ariaHidden,\n}: ImageProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const { stdout } = useStdout();\n  const isScreenReaderEnabled = useIsScreenReaderEnabled();\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 (\n      resolvedProtocol === \"ascii\" ||\n      isScreenReaderEnabled ||\n      !stdout.isTTY\n    ) {\n      setRendered(true);\n      return;\n    }\n\n    try {\n      if (resolvedProtocol === \"iterm2\") {\n        writeIterm2(stdout, src, width, height);\n        setRendered(true);\n      } else if (resolvedProtocol === \"kitty\") {\n        writeKitty(stdout, src, width, height);\n        setRendered(true);\n      }\n    } catch (error) {\n      setRenderError(error instanceof Error ? error.message : String(error));\n    }\n  }, [height, isScreenReaderEnabled, resolvedProtocol, src, stdout, width]);\n\n  if (isScreenReaderEnabled) {\n    return ariaHidden ? null : <Text>{alt}</Text>;\n  }\n\n  if (resolvedProtocol === \"ascii\" || renderError) {\n    const boxWidth = width ?? 20;\n    const horizontal = unicode ? \"─\" : \"-\";\n    const vertical = unicode ? \"│\" : \"|\";\n    const topLeft = unicode ? \"┌\" : \"+\";\n    const topRight = unicode ? \"┐\" : \"+\";\n    const bottomLeft = unicode ? \"└\" : \"+\";\n    const bottomRight = unicode ? \"┘\" : \"+\";\n    const topBottom = horizontal.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      `${topLeft}${topBottom}${topRight}`,\n      ...Array.from(\n        { length: Math.floor(innerRows / 2) },\n        () => `${vertical}${empty}${vertical}`\n      ),\n      `${vertical} ${label.padEnd(boxWidth - 3)}${vertical}`,\n      ...Array.from(\n        { length: Math.ceil(innerRows / 2) },\n        () => `${vertical}${empty}${vertical}`\n      ),\n      `${bottomLeft}${topBottom}${bottomRight}`,\n    ];\n\n    return (\n      <Box\n        flexDirection=\"column\"\n        gap={0}\n        aria-hidden={ariaHidden}\n        aria-label={ariaHidden ? undefined : alt}\n      >\n        {displayLines.map((line, i) => (\n          <Text key={i} color={theme.colors.border}>\n            {line}\n          </Text>\n        ))}\n        {alt && (\n          <Text color={theme.colors.mutedForeground} dimColor>\n            {alt}\n          </Text>\n        )}\n        <Text color={theme.colors.mutedForeground} dimColor>\n          [\n          {resolvedProtocol === \"ascii\"\n            ? \"ascii fallback\"\n            : `error: ${renderError}`}\n          ] {ext}\n        </Text>\n      </Box>\n    );\n  }\n\n  return (\n    <Box\n      flexDirection=\"column\"\n      gap={0}\n      aria-hidden={ariaHidden}\n      aria-label={ariaHidden ? undefined : alt}\n    >\n      <Text color={theme.colors.mutedForeground} dimColor>\n        {alt ?? filename} [{resolvedProtocol}]\n      </Text>\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "utility"
  ],
  "type": "registry:ui"
}