{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-markdown",
  "title": "Chat Markdown",
  "description": "Model output as safe markdown: no raw HTML, external links without referrer, code blocks highlighted.",
  "dependencies": [
    "marked",
    "react-markdown",
    "remark-breaks",
    "remark-gfm"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/utils.json",
    "https://ui.ndekocode.dev/r/chat-code-block.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/ui/chat-markdown.tsx",
      "content": "// Adapted from prompt-kit (MIT, Copyright (c) 2025 Julien Thibeaut).\n'use client';\n\nimport { marked } from 'marked';\nimport { isValidElement, memo, useId, useMemo } from 'react';\nimport ReactMarkdown, {\n  type Components,\n  type ExtraProps,\n} from 'react-markdown';\nimport remarkBreaks from 'remark-breaks';\nimport remarkGfm from 'remark-gfm';\n\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport {\n  CodeBlock,\n  CodeBlockCode,\n  CodeBlockCopyButton,\n  CodeBlockGroup,\n  CodeBlockLanguage,\n} from '@/registry/ndeko/ui/chat-code-block';\n\nconst REMARK_PLUGINS = [remarkGfm, remarkBreaks];\n\nconst EXTERNAL_HREF = /^(https?:)?\\/\\//i;\n\nconst LANGUAGE_CLASS = /language-([\\w+#-]+)/;\n\nconst TRAILING_NEWLINE = /\\n$/;\n\nconst PLAIN_LANGUAGE = 'text';\n\nconst LINK =\n  'text-primary-ink underline decoration-primary-ink/30 underline-offset-2 transition-colors hover:decoration-primary-ink focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/25';\n\nfunction parseMarkdownIntoBlocks(markdown: string) {\n  const tokens = marked.lexer(markdown);\n  return tokens.map((token) => token.raw);\n}\n\nfunction isExternalHref(href: string | undefined) {\n  if (!href) {\n    return false;\n  }\n  return EXTERNAL_HREF.test(href);\n}\n\nfunction textOf(node: React.ReactNode): string {\n  if (typeof node === 'string') {\n    return node;\n  }\n  if (typeof node === 'number') {\n    return String(node);\n  }\n  if (Array.isArray(node)) {\n    return node.map(textOf).join('');\n  }\n  return '';\n}\n\nfunction languageOf(className: string | undefined) {\n  if (!className) {\n    return PLAIN_LANGUAGE;\n  }\n  const match = className.match(LANGUAGE_CLASS);\n  if (!match) {\n    return PLAIN_LANGUAGE;\n  }\n  return match[1];\n}\n\ntype CodeElementProps = {\n  className?: string;\n  children?: React.ReactNode;\n};\n\nfunction fencedCodeOf(children: React.ReactNode) {\n  if (!isValidElement<CodeElementProps>(children)) {\n    return { code: textOf(children), language: PLAIN_LANGUAGE };\n  }\n  const rawCode = textOf(children.props.children);\n  const code = rawCode.replace(TRAILING_NEWLINE, '');\n  const language = languageOf(children.props.className);\n  return { code, language };\n}\n\ntype MarkdownPartProps<Tag extends keyof React.JSX.IntrinsicElements> =\n  React.ComponentProps<Tag> & ExtraProps;\n\nfunction MarkdownPre({ children }: MarkdownPartProps<'pre'>) {\n  const { code, language } = fencedCodeOf(children);\n\n  return (\n    <CodeBlock className='my-1'>\n      <CodeBlockGroup>\n        <CodeBlockLanguage>{language}</CodeBlockLanguage>\n        <CodeBlockCopyButton code={code} />\n      </CodeBlockGroup>\n      <CodeBlockCode code={code} language={language} />\n    </CodeBlock>\n  );\n}\n\nfunction MarkdownInlineCode({ children }: MarkdownPartProps<'code'>) {\n  return (\n    <code className='bg-foreground/[0.06] rounded-sm px-1 py-px font-mono text-[0.9em]'>\n      {children}\n    </code>\n  );\n}\n\nfunction MarkdownLink({ href, title, children }: MarkdownPartProps<'a'>) {\n  const isExternal = isExternalHref(href);\n\n  if (isExternal) {\n    return (\n      <a\n        href={href}\n        title={title}\n        target='_blank'\n        rel='noreferrer'\n        className={LINK}\n      >\n        {children}\n      </a>\n    );\n  }\n\n  return (\n    <a href={href} title={title} className={LINK}>\n      {children}\n    </a>\n  );\n}\n\nfunction MarkdownTable({ children }: MarkdownPartProps<'table'>) {\n  return (\n    <div className='overflow-x-auto'>\n      <table className='w-full text-[13px] tabular-nums'>{children}</table>\n    </div>\n  );\n}\n\nfunction MarkdownTableHead({ children, style }: MarkdownPartProps<'th'>) {\n  return (\n    <th\n      style={style}\n      className='hairline-b border-border text-muted-foreground px-2 py-1.5 text-start font-medium'\n    >\n      {children}\n    </th>\n  );\n}\n\nfunction MarkdownTableCell({ children, style }: MarkdownPartProps<'td'>) {\n  return (\n    <td\n      style={style}\n      className='hairline-b border-border px-2 py-1.5 align-top'\n    >\n      {children}\n    </td>\n  );\n}\n\nconst DEFAULT_COMPONENTS: Partial<Components> = {\n  pre: MarkdownPre,\n  code: MarkdownInlineCode,\n  a: MarkdownLink,\n  table: MarkdownTable,\n  th: MarkdownTableHead,\n  td: MarkdownTableCell,\n  h1: ({ children }) => (\n    <h3 className='text-base font-semibold text-balance'>{children}</h3>\n  ),\n  h2: ({ children }) => (\n    <h4 className='text-[15px] font-semibold text-balance'>{children}</h4>\n  ),\n  h3: ({ children }) => (\n    <h5 className='text-sm font-semibold text-balance'>{children}</h5>\n  ),\n  h4: ({ children }) => <h6 className='text-sm font-medium'>{children}</h6>,\n  p: ({ children }) => <p>{children}</p>,\n  ul: ({ children }) => (\n    <ul className='marker:text-muted-foreground list-disc space-y-1 ps-5'>\n      {children}\n    </ul>\n  ),\n  ol: ({ children, start }) => (\n    <ol\n      start={start}\n      className='marker:text-muted-foreground list-decimal space-y-1 ps-5 marker:tabular-nums'\n    >\n      {children}\n    </ol>\n  ),\n  li: ({ children }) => <li className='ps-0.5'>{children}</li>,\n  blockquote: ({ children }) => (\n    <blockquote className='border-foreground/10 text-muted-foreground border-s-2 ps-3'>\n      {children}\n    </blockquote>\n  ),\n  hr: () => <hr className='bg-border h-(--hairline) border-0' />,\n  strong: ({ children }) => (\n    <strong className='font-semibold'>{children}</strong>\n  ),\n};\n\ntype MarkdownBlockProps = {\n  content: string;\n  components: Partial<Components>;\n};\n\nfunction hasSameContent(\n  previous: MarkdownBlockProps,\n  next: MarkdownBlockProps\n) {\n  return previous.content === next.content;\n}\n\n/** Each block re-renders only when its own text changes, so a streaming reply only parses its last block. */\nconst MemoizedMarkdownBlock = memo(function MarkdownBlock({\n  content,\n  components,\n}: MarkdownBlockProps) {\n  return (\n    <ReactMarkdown remarkPlugins={REMARK_PLUGINS} components={components}>\n      {content}\n    </ReactMarkdown>\n  );\n}, hasSameContent);\n\ntype MarkdownProps = {\n  children: string;\n  id?: string;\n  className?: string;\n  components?: Partial<Components>;\n};\n\nfunction MarkdownComponent({\n  children,\n  id,\n  className,\n  components,\n}: MarkdownProps) {\n  const generatedId = useId();\n  const blockId = id ?? generatedId;\n  const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]);\n  const mergedComponents = useMemo(\n    () => ({ ...DEFAULT_COMPONENTS, ...components }),\n    [components]\n  );\n\n  return (\n    <div\n      data-slot='markdown'\n      className={cn(\n        'text-foreground space-y-3 text-sm leading-relaxed break-words',\n        className\n      )}\n    >\n      {blocks.map((block, index) => (\n        <MemoizedMarkdownBlock\n          key={`${blockId}-block-${index}`}\n          content={block}\n          components={mergedComponents}\n        />\n      ))}\n    </div>\n  );\n}\n\nconst Markdown = memo(MarkdownComponent);\n\nexport { Markdown, type MarkdownProps };\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "primitives",
    "chat"
  ],
  "type": "registry:ui"
}