{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/markdown",
  "title": "Markdown",
  "description": "Full markdown renderer for terminal",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/use-animation.json",
    "https://termcn.dev/r/ink/use-theme.json",
    "https://termcn.dev/r/ink/use-unicode.json"
  ],
  "files": [
    {
      "path": "registry/ui/markdown.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React from \"react\";\n\nimport { useAnimation } from \"@/hooks/use-animation\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\n\nexport interface MarkdownProps {\n  children: string;\n  width?: number;\n  streaming?: boolean;\n  cursor?: string;\n  isActive?: boolean;\n  \"aria-label\"?: string;\n}\n\ninterface InlineSegment {\n  text: string;\n  bold?: boolean;\n  italic?: boolean;\n  code?: boolean;\n  link?: boolean;\n  url?: string;\n}\n\nconst parseInline = (line: string): InlineSegment[] => {\n  const segments: InlineSegment[] = [];\n  const re = /(\\*\\*(.+?)\\*\\*|\\*(.+?)\\*|`(.+?)`|\\[(.+?)\\]\\((.+?)\\))/g;\n  let last = 0;\n  let match: RegExpExecArray | null;\n\n  while ((match = re.exec(line)) !== null) {\n    if (match.index > last) {\n      segments.push({ text: line.slice(last, match.index) });\n    }\n\n    const [full] = match;\n    if (full.startsWith(\"**\")) {\n      segments.push({ bold: true, text: match[2] });\n    } else if (full.startsWith(\"*\")) {\n      segments.push({ italic: true, text: match[3] });\n    } else if (full.startsWith(\"`\")) {\n      segments.push({ code: true, text: match[4] });\n    } else if (full.startsWith(\"[\")) {\n      segments.push({ link: true, text: match[5], url: match[6] });\n    }\n\n    last = match.index + full.length;\n  }\n\n  if (last < line.length) {\n    segments.push({ text: line.slice(last) });\n  }\n\n  return segments;\n};\n\nconst InlineLine = ({ segments }: { segments: InlineSegment[] }) => {\n  const theme = useTheme();\n\n  return (\n    <Box>\n      {segments.map((seg, i) => {\n        if (seg.code) {\n          return (\n            <Text key={i} color={theme.colors.accent}>\n              {seg.text}\n            </Text>\n          );\n        }\n        if (seg.link) {\n          return (\n            <Box key={i}>\n              <Text underline color={theme.colors.info}>\n                {seg.text}\n              </Text>\n              <Text dimColor color={theme.colors.mutedForeground}>\n                {\" \"}\n                ({seg.url})\n              </Text>\n            </Box>\n          );\n        }\n        return (\n          <Text key={i} bold={seg.bold} italic={seg.italic}>\n            {seg.text}\n          </Text>\n        );\n      })}\n    </Box>\n  );\n};\n\nconst sanitizePartialFences = (text: string): string => {\n  const fenceCount = (text.match(/```/g) ?? []).length;\n  if (fenceCount % 2 !== 0) {\n    return `${text}\\n\\`\\`\\``;\n  }\n  return text;\n};\n\nexport const Markdown = ({\n  children,\n  width,\n  streaming = false,\n  cursor,\n  isActive = true,\n  \"aria-label\": ariaLabel,\n}: MarkdownProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const resolvedCursor = cursor ?? (unicode ? \"▌\" : \"|\");\n  const cursorFrame = useAnimation({\n    intervalMs: 530,\n    isActive: streaming && isActive,\n  });\n  const cursorVisible = cursorFrame % 2 === 0;\n\n  const safeChildren = streaming ? sanitizePartialFences(children) : children;\n  const lines = safeChildren.split(\"\\n\");\n\n  const elements: React.ReactNode[] = [];\n  let i = 0;\n\n  while (i < lines.length) {\n    const line = lines[i];\n    const fence = line.match(/^```(?:\\s*([^\\s`]+))?\\s*$/);\n\n    if (fence) {\n      const fenceStart = i;\n      const codeLines: string[] = [];\n      i += 1;\n      while (i < lines.length && !/^```\\s*$/.test(lines[i])) {\n        codeLines.push(lines[i]);\n        i += 1;\n      }\n      if (i < lines.length) {\n        i += 1;\n      }\n      if (codeLines.length === 0) {\n        elements.push(<Box key={`code-${fenceStart}`} />);\n      } else {\n        for (const [codeIndex, codeLine] of codeLines.entries()) {\n          elements.push(\n            <Text\n              key={`code-${fenceStart}-${codeIndex}`}\n              color={theme.colors.accent}\n            >\n              {codeLine || \" \"}\n            </Text>\n          );\n        }\n      }\n      continue;\n    }\n\n    const h4 = line.match(/^####\\s+(.*)/);\n    const h3 = line.match(/^###\\s+(.*)/);\n    const h2 = line.match(/^##\\s+(.*)/);\n    const h1 = line.match(/^#\\s+(.*)/);\n\n    if (h1) {\n      elements.push(\n        <Text key={i} bold color={theme.colors.primary}>\n          {h1[1]}\n        </Text>\n      );\n    } else if (h2) {\n      elements.push(\n        <Text key={i} bold color={theme.colors.primary}>\n          {h2[1]}\n        </Text>\n      );\n    } else if (h3) {\n      elements.push(\n        <Text key={i} bold color={theme.colors.primary}>\n          {h3[1]}\n        </Text>\n      );\n    } else if (h4) {\n      elements.push(\n        <Text key={i} color={theme.colors.primary}>\n          {h4[1]}\n        </Text>\n      );\n    } else if (/^---+$/.test(line)) {\n      elements.push(\n        <Text key={i} color={theme.colors.border}>\n          {(unicode ? \"─\" : \"-\").repeat(width ?? 40)}\n        </Text>\n      );\n    } else if (/^>\\s/.test(line)) {\n      const content = line.replace(/^>\\s/, \"\");\n      elements.push(\n        <Box key={i} gap={1}>\n          <Text color={theme.colors.primary}>{unicode ? \"│\" : \"|\"}</Text>\n          <InlineLine segments={parseInline(content)} />\n        </Box>\n      );\n    } else if (/^[-*]\\s/.test(line)) {\n      const content = line.replace(/^[-*]\\s/, \"\");\n      elements.push(\n        <Box key={i} gap={1}>\n          <Text color={theme.colors.mutedForeground}>\n            {unicode ? \"•\" : \"*\"}\n          </Text>\n          <InlineLine segments={parseInline(content)} />\n        </Box>\n      );\n    } else if (line === \"\") {\n      elements.push(<Box key={i} />);\n    } else {\n      elements.push(\n        <Box key={i} flexWrap=\"wrap\" width={width}>\n          <InlineLine segments={parseInline(line)} />\n        </Box>\n      );\n    }\n\n    i += 1;\n  }\n\n  if (streaming && cursorVisible && elements.length > 0) {\n    const last = elements.at(-1);\n    elements[elements.length - 1] = (\n      <Box key={`cursor-wrapper-${elements.length - 1}`} flexDirection=\"row\">\n        {last}\n        <Text aria-hidden color={theme.colors.primary}>\n          {resolvedCursor}\n        </Text>\n      </Box>\n    );\n  }\n\n  return (\n    <Box\n      flexDirection=\"column\"\n      aria-label={ariaLabel}\n      aria-state={{ busy: streaming }}\n    >\n      {elements}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "typography"
  ],
  "type": "registry:ui"
}