{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink/chat-message",
  "title": "Chat Message",
  "description": "Chat message bubble with role label, streaming indicator, and collapsible content",
  "dependencies": [
    "ink"
  ],
  "registryDependencies": [
    "https://termcn.dev/r/ink/interaction.json",
    "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/chat-message.tsx",
      "content": "import { Box, Text } from \"ink\";\nimport React, { useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useAnimation } from \"@/hooks/use-animation\";\nimport {\n  isActivationKey,\n  useInteraction,\n} from \"@/hooks/use-interaction\";\nimport type { InteractionProps } from \"@/hooks/use-interaction\";\nimport { useTheme } from \"@/hooks/use-theme\";\nimport { useUnicode } from \"@/hooks/use-unicode\";\n\nexport type ChatRole = \"user\" | \"assistant\" | \"system\" | \"error\";\n\nexport interface ChatMessageProps extends InteractionProps {\n  sender: ChatRole;\n  name?: string;\n  timestamp?: Date;\n  streaming?: boolean;\n  collapsed?: boolean;\n  children?: ReactNode;\n  \"aria-label\"?: string;\n}\n\nconst formatTime = (date: Date): string =>\n  date.toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" });\n\nconst wrapPlainChildren = (node: ReactNode): ReactNode =>\n  typeof node === \"string\" || typeof node === \"number\" ? (\n    <Text>{node}</Text>\n  ) : (\n    node\n  );\n\nexport const ChatMessage = ({\n  sender,\n  name,\n  timestamp,\n  streaming = false,\n  collapsed: initialCollapsed = false,\n  children,\n  id,\n  autoFocus,\n  isActive = true,\n  disabled,\n  \"aria-label\": ariaLabel,\n}: ChatMessageProps) => {\n  const theme = useTheme();\n  const unicode = useUnicode();\n  const [isCollapsed, setIsCollapsed] = useState(initialCollapsed);\n  const dotFrame = useAnimation({\n    intervalMs: 400,\n    isActive: isActive && streaming,\n  });\n\n  const { isFocused } = useInteraction(\n    (input, key) => {\n      if (initialCollapsed && isActivationKey(input, key)) {\n        setIsCollapsed((c) => !c);\n      }\n    },\n    { autoFocus, disabled, id, isActive: isActive && initialCollapsed }\n  );\n\n  const roleColor: Record<ChatRole, string> = {\n    assistant: theme.colors.success ?? \"green\",\n    error: theme.colors.error ?? \"red\",\n    system: theme.colors.mutedForeground,\n    user: theme.colors.primary,\n  };\n\n  const roleLabel: Record<ChatRole, string> = {\n    assistant: \"assistant\",\n    error: \"error\",\n    system: \"system\",\n    user: \"user\",\n  };\n\n  const color = roleColor[sender];\n\n  const dot = unicode ? \"●\" : \".\";\n  const dots = dot.repeat(dotFrame);\n\n  const childrenText = typeof children === \"string\" ? children : \"\";\n  const firstLine = childrenText.split(\"\\n\")[0] ?? \"\";\n\n  const renderContent = () => {\n    if (streaming) {\n      return (\n        <Box>\n          {children ? (\n            wrapPlainChildren(children)\n          ) : (\n            <Text aria-hidden color={color} dimColor>\n              {dots}\n            </Text>\n          )}\n        </Box>\n      );\n    }\n    if (isCollapsed) {\n      return (\n        <Box>\n          <Text dimColor>\n            {firstLine.slice(0, 60)}\n            {firstLine.length > 60 || childrenText.includes(\"\\n\") ? \"...\" : \"\"}\n          </Text>\n        </Box>\n      );\n    }\n    return <Box>{wrapPlainChildren(children)}</Box>;\n  };\n\n  return (\n    <Box\n      flexDirection=\"column\"\n      marginBottom={1}\n      aria-role=\"listitem\"\n      aria-state={{\n        busy: streaming,\n        disabled: disabled || undefined,\n        expanded: initialCollapsed ? !isCollapsed : undefined,\n      }}\n    >\n      <Text\n        aria-label={\n          ariaLabel ??\n          `${sender === \"error\" ? \"Error\" : \"Message\"} from ${name ?? roleLabel[sender]}${streaming ? \", streaming\" : \"\"}`\n        }\n      >\n        {\"\"}\n      </Text>\n      <Box gap={1}>\n        {isFocused && initialCollapsed && <Text aria-hidden>[</Text>}\n        <Text color={color} bold>\n          {name ?? roleLabel[sender]}\n        </Text>\n        {timestamp && (\n          <Text dimColor color={theme.colors.mutedForeground}>\n            {formatTime(timestamp)}\n          </Text>\n        )}\n        {isCollapsed && !streaming && (\n          <Text aria-hidden dimColor color={theme.colors.mutedForeground}>\n            [expand]\n          </Text>\n        )}\n        {isFocused && initialCollapsed && <Text aria-hidden>]</Text>}\n      </Box>\n\n      {renderContent()}\n    </Box>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}