{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "chat-message", "title": "Chat Message", "description": "A single chat transcript entry with baked-in entrance and layout motion. Right-aligned accent bubble for the user, left-aligned muted bubble for the assistant, with optional file attachments. Adapts to the global shape setting.", "dependencies": [ "framer-motion" ], "registryDependencies": [ "https://zeron-ui.vercel.app/r/surfaces.json", "utils", "https://zeron-ui.vercel.app/r/springs.json", "https://zeron-ui.vercel.app/r/shape-context.json", "https://zeron-ui.vercel.app/r/use-touch-primary.json", "https://zeron-ui.vercel.app/r/file-thumbnail.json" ], "files": [ { "path": "src/components/ui/chat-message.tsx", "content": "\"use client\";\n\nimport { forwardRef, type ReactNode } from \"react\";\nimport { motion, type HTMLMotionProps } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { spring } from \"@/lib/springs\";\nimport { useShape } from \"@/lib/shape-context\";\nimport { useTouchPrimary } from \"@/hooks/use-touch-primary\";\nimport { FileThumbnail } from \"@/components/ui/file-thumbnail\";\n\ninterface ChatMessageProps\n extends Omit, \"children\"> {\n /** Who sent the message. Drives alignment and bubble colour:\n * `user` → right-aligned emphasis 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, before the actions.\n * User-message only — ignored on assistant replies. Caller pre-formats it\n * (e.g. `\"Wednesday 6:08 PM\"`). */\n time?: ReactNode;\n /** Icon-only action buttons shown in the hover-revealed meta row (e.g. copy,\n * edit, regenerate). Rendered next to the timestamp. */\n actions?: ReactNode;\n /** Message body. When omitted the text bubble is dropped (attachment-only message). */\n children?: 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 = forwardRef(\n (\n { from, files, thumbnailSize = 64, time, actions, children, className, ...props },\n ref\n ) => {\n const shape = useShape();\n const isUser = from === \"user\";\n // Hover-reveal is unreachable on touch — keep the meta row visible there.\n const isTouch = useTouchPrimary();\n // Timestamps are a user-message affordance; assistant replies show actions only.\n const showTime = isUser && time != null;\n\n return (\n \n {files && files.length > 0 && (\n \n {files.map((file, i) => (\n \n ))}\n \n )}\n {children != null && children !== \"\" && (\n \n {children}\n \n )}\n {(showTime || actions != null) && (\n // Meta row: timestamp + icon-only actions. Always rendered (so it\n // reserves its height and the gap between bubbles never shifts) but\n // hidden until the message is hovered or an action is focused.\n // The timestamp is a user-message affordance only — assistant replies\n // show their actions alone. User rows read date → icons left-to-right.\n \n {showTime && {time}}\n {actions != null && (\n {actions}\n )}\n \n )}\n \n );\n }\n);\n\nChatMessage.displayName = \"ChatMessage\";\n\nexport { ChatMessage };\nexport type { ChatMessageProps };\nexport default ChatMessage;\n", "type": "registry:ui", "target": "components/ui/chat-message.tsx" } ], "type": "registry:ui" }