{ "$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, \"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 \n {files && files.length > 0 && (\n
\n {files.map((file, i) => (\n \n ))}\n
\n )}\n {children !== undefined && children !== null && children !== \"\" && (\n \n {children}\n \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 \n {showTime && {time}}\n {actions !== undefined && actions !== null && (\n {actions}\n )}\n \n )}\n \n );\n};\n\nexport { ChatMessage };\nexport type { ChatMessageProps };\n", "type": "registry:ui", "target": "" } ], "type": "registry:ui" }