{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "conversation",
  "title": "Conversation",
  "description": "A quiet conversation surface with authored hierarchy and contextual message actions.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "springs"
  ],
  "files": [
    {
      "path": "src/components/trovecn/ai-workbench/conversation.tsx",
      "content": "\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\nimport { CheckIcon, CopyIcon, PencilIcon, RotateCcwIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { spring } from \"@/lib/springs\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ConversationMessageRole = \"user\" | \"assistant\";\n\nexport interface ConversationMessage {\n  id: string;\n  role: ConversationMessageRole;\n  /** Consumer-rendered text, markdown, or other inline response content. */\n  content: ReactNode;\n  /** Rendered below a user message; assistant messages intentionally omit it. */\n  timestamp?: ReactNode;\n}\n\nexport interface ConversationProps {\n  messages: readonly ConversationMessage[];\n  onCopy?: (message: ConversationMessage) => void;\n  onEdit?: (message: ConversationMessage) => void;\n  onRegenerate?: (message: ConversationMessage) => void;\n  emptyState?: ReactNode;\n  className?: string;\n}\n\n/**\n * A quiet conversation reading surface. It establishes authored hierarchy and\n * contextual actions, leaving transport, markdown, and product state outside.\n */\nfunction Conversation({\n  messages,\n  onCopy,\n  onEdit,\n  onRegenerate,\n  emptyState,\n  className,\n}: ConversationProps) {\n  const reduceMotion = useReducedMotion();\n  const [copiedMessageId, setCopiedMessageId] = useState<string>();\n\n  async function handleCopy(message: ConversationMessage) {\n    let copied = false;\n\n    if (typeof message.content === \"string\" && navigator.clipboard) {\n      try {\n        await navigator.clipboard.writeText(message.content);\n        copied = true;\n      } catch {\n        // A consumer callback can provide a fallback for restricted clipboard contexts.\n      }\n    }\n\n    onCopy?.(message);\n    if (!copied && !onCopy) return;\n\n    setCopiedMessageId(message.id);\n    window.setTimeout(() => setCopiedMessageId(undefined), 1_600);\n  }\n\n  if (messages.length === 0) {\n    return emptyState ? (\n      <div data-slot=\"conversation\" className={className}>\n        {emptyState}\n      </div>\n    ) : null;\n  }\n\n  return (\n    <section data-slot=\"conversation\" aria-label=\"Conversation\" className={cn(\"w-full\", className)}>\n      <div className=\"space-y-2\">\n        <AnimatePresence initial={false}>\n          {messages.map((message) => (\n            <motion.article\n              key={message.id}\n              data-slot=\"conversation-message\"\n              data-role={message.role}\n              aria-label={message.role === \"user\" ? \"Your message\" : \"Conversation response\"}\n              initial={{ opacity: 0, y: reduceMotion ? 0 : 4 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{\n                opacity: 0,\n                y: reduceMotion ? 0 : -4,\n                transition: reduceMotion ? { duration: 0 } : spring.quick.exit,\n              }}\n              transition={reduceMotion ? { duration: 0 } : spring.quick.enter}\n              className={cn(\n                \"group/message flex min-w-0 flex-col\",\n                message.role === \"user\" ? \"items-end\" : \"items-start\",\n              )}\n            >\n              <div\n                className={cn(\n                  \"max-w-[min(88%,32rem)] min-w-0 text-lede leading-7 text-foreground\",\n                  message.role === \"user\" ? \"rounded-2xl bg-muted px-3.5 py-2.5\" : \"py-2.5\",\n                )}\n              >\n                {message.content}\n              </div>\n\n              <div\n                className={cn(\n                  \"flex min-h-6 items-center gap-1 opacity-100 transition-opacity duration-quick sm:opacity-0 sm:group-hover/message:opacity-100 sm:group-focus-within/message:opacity-100\",\n                  message.role === \"user\" && \"mt-1.5\",\n                )}\n              >\n                {message.role === \"user\" && message.timestamp ? (\n                  <span className=\"mr-1 text-caption text-muted-foreground\">\n                    {message.timestamp}\n                  </span>\n                ) : null}\n                {typeof message.content === \"string\" || onCopy ? (\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    className=\"text-muted-foreground\"\n                    onClick={() => void handleCopy(message)}\n                    aria-label={message.role === \"user\" ? \"Copy your message\" : \"Copy response\"}\n                  >\n                    <AnimatePresence mode=\"wait\" initial={false}>\n                      <motion.span\n                        key={copiedMessageId === message.id ? \"copied\" : \"copy\"}\n                        initial={{ opacity: 0, scale: reduceMotion ? 1 : 0.8 }}\n                        animate={{ opacity: 1, scale: 1 }}\n                        exit={{ opacity: 0, scale: reduceMotion ? 1 : 0.8 }}\n                        transition={reduceMotion ? { duration: 0 } : spring.quick.enter}\n                      >\n                        {copiedMessageId === message.id ? <CheckIcon /> : <CopyIcon />}\n                      </motion.span>\n                    </AnimatePresence>\n                  </Button>\n                ) : null}\n                {message.role === \"user\" && onEdit ? (\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    className=\"text-muted-foreground\"\n                    onClick={() => onEdit(message)}\n                    aria-label=\"Edit your message\"\n                  >\n                    <PencilIcon />\n                  </Button>\n                ) : null}\n                {message.role === \"assistant\" && onRegenerate ? (\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"icon-xs\"\n                    className=\"text-muted-foreground\"\n                    onClick={() => onRegenerate(message)}\n                    aria-label=\"Regenerate response\"\n                  >\n                    <RotateCcwIcon />\n                  </Button>\n                ) : null}\n              </div>\n            </motion.article>\n          ))}\n        </AnimatePresence>\n      </div>\n    </section>\n  );\n}\n\nexport { Conversation };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}