{
  "$schema": "https://blode.co/ui/schema/registry-item.json",
  "name": "chat-message",
  "title": "Chat Message",
  "author": "Matthew Blode",
  "description": "A chat transcript entry for user and assistant messages, with attachments and a hover meta row.",
  "dependencies": ["motion"],
  "registryDependencies": ["file-thumbnail"],
  "files": [
    {
      "path": "ui/chat-message.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport type { HTMLMotionProps } from \"motion/react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\ninterface ChatMessageProps extends Omit<HTMLMotionProps<\"div\">, \"children\"> {\n  /** Who sent the message. Drives alignment and styling: `user` →\n   *  right-aligned tonal bubble, `assistant` → left-aligned plain text. */\n  from: \"user\" | \"assistant\";\n  /** Optional attachments rendered as square thumbnails above the bubble. */\n  files?: File[];\n  /** Side length of each attachment thumbnail in pixels. Defaults to 64. */\n  thumbnailSize?: number;\n  /** Timestamp shown in the hover-revealed meta row (user messages only). */\n  time?: React.ReactNode;\n  /** Icon-only action buttons (copy, edit, regenerate) in the meta row. */\n  actions?: React.ReactNode;\n  /** Message body. Omit for an attachment-only message. */\n  children?: React.ReactNode;\n}\n\n// ─── ChatMessage ──────────────────────────────────────────────────────────\n// A single transcript entry with baked-in entrance + layout motion. Pairs with\n// InputMessage's onSend: render one per sent/received message. `layout=\"position\"`\n// lets earlier messages slide up smoothly when a new one is appended.\nconst ChatMessage = ({\n  from,\n  files,\n  thumbnailSize = 64,\n  time,\n  actions,\n  children,\n  className,\n  ref,\n  ...props\n}: ChatMessageProps) => {\n  const isUser = from === \"user\";\n  // Timestamps are a user-message affordance; assistant replies show actions only.\n  const showTime = isUser && time !== undefined && time !== null;\n\n  return (\n    <motion.div\n      animate={{ opacity: 1, scale: 1, y: 0 }}\n      className={cn(\n        \"group flex max-w-[80%] flex-col gap-1.5\",\n        isUser ? \"items-end self-end\" : \"items-start self-start\",\n        className,\n      )}\n      data-slot=\"chat-message\"\n      initial={{ opacity: 0, scale: 0.96, y: 8 }}\n      layout=\"position\"\n      ref={ref}\n      style={{ transformOrigin: isUser ? \"bottom right\" : \"bottom left\" }}\n      transition={{ bounce: 0.15, duration: 0.16, type: \"spring\" }}\n      {...props}\n    >\n      {files && files.length > 0 && (\n        <div className={cn(\"flex flex-wrap gap-1.5\", isUser ? \"justify-end\" : \"justify-start\")}>\n          {files.map((file, i) => (\n            <FileThumbnail\n              file={file}\n              key={`${file.name}-${file.size}-${file.lastModified}-${i}`}\n              size={thumbnailSize}\n            />\n          ))}\n        </div>\n      )}\n      {children !== undefined && children !== null && children !== \"\" && (\n        <div\n          className={cn(\n            \"whitespace-pre-wrap text-pretty break-words py-2 text-sm\",\n            // User keeps the bubble chrome (rounded tonal fill + padding); the\n            // assistant reply is flush-left plain text with no background.\n            isUser ? \"rounded-2xl bg-accent px-3.5 text-accent-foreground\" : \"text-foreground\",\n          )}\n        >\n          {children}\n        </div>\n      )}\n      {(showTime || (actions !== undefined && actions !== null)) && (\n        // Meta row: always rendered (so it reserves height and the gap between\n        // bubbles never shifts) but hidden until the message is hovered or an\n        // action is focused.\n        <div\n          className={cn(\n            \"flex items-center gap-2 px-1 text-muted-foreground text-xs leading-none select-none\",\n            \"pointer-events-none opacity-0 transition-opacity duration-150\",\n            \"group-hover:pointer-events-auto group-hover:opacity-100\",\n            \"group-focus-within:pointer-events-auto group-focus-within:opacity-100\",\n          )}\n        >\n          {showTime && <span className=\"tabular-figures\">{time}</span>}\n          {actions !== undefined && actions !== null && (\n            <span className=\"flex items-center gap-0.5\">{actions}</span>\n          )}\n        </div>\n      )}\n    </motion.div>\n  );\n};\n\nexport { ChatMessage };\nexport type { ChatMessageProps };\n",
      "type": "registry:ui",
      "target": ""
    }
  ],
  "type": "registry:ui"
}
