{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message",
  "title": "Message",
  "description": "User, assistant, system, and tool messages with avatar, header, content, actions, and streaming state.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/message.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Check, Copy, Info, type LucideIcon, Sparkles, User, Wrench } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const MESSAGE_VARIANTS = [\"bubble\", \"plain\", \"compact\"] as const;\nexport const MESSAGE_ROLES = [\"user\", \"assistant\", \"system\", \"tool\"] as const;\nexport type MessageVariant = (typeof MESSAGE_VARIANTS)[number];\nexport type MessageRole = (typeof MESSAGE_ROLES)[number];\nexport type MessageProps = ComponentProps<\"article\"> & {\n  variant?: MessageVariant;\n  /** Who authored the message. */\n  from?: MessageRole;\n  /** Marks the message as still receiving content. */\n  streaming?: boolean;\n};\n\ntype MessageContextValue = {\n  id: string;\n  role: MessageRole;\n  variant: MessageVariant;\n  streaming: boolean;\n  contentRef: React.RefObject<HTMLDivElement | null>;\n};\n\nconst MessageContext = createContext<MessageContextValue | null>(null);\n\nfunction useMessage(part: string) {\n  const context = useContext(MessageContext);\n  if (!context) throw new Error(`${part} must be used within Message`);\n  return context;\n}\n\nconst roleDetails: Record<MessageRole, { label: string; icon: LucideIcon }> = {\n  user: { label: \"You\", icon: User },\n  assistant: { label: \"Assistant\", icon: Sparkles },\n  system: { label: \"System\", icon: Info },\n  tool: { label: \"Tool\", icon: Wrench },\n};\n\nfunction prefersReducedMotion() {\n  return (\n    typeof window !== \"undefined\" &&\n    typeof window.matchMedia === \"function\" &&\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  );\n}\n\nconst messageVariants = cva(\n  \"flex min-w-0 animate-in items-start fade-in-0 slide-in-from-bottom-1 text-[13px]/[18px] text-foreground duration-240 ease-out-quint motion-reduce:animate-none\",\n  {\n    variants: {\n      variant: { bubble: \"gap-3\", plain: \"gap-3\", compact: \"gap-2\" },\n      alignEnd: { true: \"flex-row-reverse\", false: \"flex-row\" },\n    },\n  },\n);\n\nexport function Message({\n  variant = \"bubble\",\n  from: role = \"assistant\",\n  streaming = false,\n  className,\n  children,\n  \"aria-label\": ariaLabel,\n  ...props\n}: MessageProps) {\n  const id = useId();\n  const contentRef = useRef<HTMLDivElement>(null);\n  const alignEnd = variant === \"bubble\" && role === \"user\";\n  return (\n    <MessageContext.Provider value={{ id, role, variant, streaming, contentRef }}>\n      <article\n        data-slot=\"message\"\n        aria-label={ariaLabel ?? `${roleDetails[role].label} message`}\n        className={cn(messageVariants({ variant, alignEnd }), className)}\n        {...props}\n        aria-busy={streaming || undefined}\n        data-variant={variant}\n        data-role={role}\n        data-streaming={streaming || undefined}\n      >\n        {children}\n      </article>\n    </MessageContext.Provider>\n  );\n}\n\nexport function MessageAvatar({ children, className, ...props }: ComponentProps<\"span\">) {\n  const context = useMessage(\"MessageAvatar\");\n  const Icon = roleDetails[context.role].icon;\n  const compact = context.variant === \"compact\";\n  return (\n    <span\n      data-slot=\"message-avatar\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"grid flex-none place-items-center overflow-hidden rounded-full font-medium tracking-[0.01em] ring-1 ring-foreground/8\",\n        compact ? \"size-6 text-[10px]\" : \"size-7 text-[10.5px]\",\n        context.role === \"assistant\"\n          ? \"bg-[color-mix(in_oklab,var(--primary)_18%,var(--card))] text-primary-ink\"\n          : \"bg-muted text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? <Icon size={compact ? 12 : 14} strokeWidth={1.8} />}\n    </span>\n  );\n}\n\nexport function MessageBody({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useMessage(\"MessageBody\");\n  const alignEnd = context.variant === \"bubble\" && context.role === \"user\";\n  return (\n    <div\n      data-slot=\"message-body\"\n      className={cn(\n        \"grid min-w-0 flex-auto\",\n        alignEnd ? \"justify-items-end\" : \"justify-items-start\",\n        context.variant === \"compact\" ? \"gap-1\" : \"gap-1.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function MessageHeader({ className, ...props }: ComponentProps<\"div\">) {\n  useMessage(\"MessageHeader\");\n  return (\n    <div\n      data-slot=\"message-header\"\n      className={cn(\"flex min-w-0 flex-wrap items-baseline gap-1.5 leading-[18px]\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function MessageAuthor({ children, className, ...props }: ComponentProps<\"span\">) {\n  const context = useMessage(\"MessageAuthor\");\n  return (\n    <span\n      data-slot=\"message-author\"\n      className={cn(\"text-[12.5px] font-medium\", className)}\n      {...props}\n    >\n      {children ?? roleDetails[context.role].label}\n    </span>\n  );\n}\n\nexport function MessageTime({ className, ...props }: ComponentProps<\"time\">) {\n  return (\n    <time\n      data-slot=\"message-time\"\n      className={cn(\"text-[11.5px] text-subtle-foreground tabular-nums\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction StreamingCaret() {\n  const ref = useRef<HTMLSpanElement>(null);\n  useEffect(() => {\n    if (prefersReducedMotion()) return;\n    const animation = ref.current?.animate?.([{ opacity: 1 }, { opacity: 0.2 }, { opacity: 1 }], {\n      duration: 1000,\n      iterations: Number.POSITIVE_INFINITY,\n      easing: \"ease-in-out\",\n    });\n    return () => animation?.cancel();\n  }, []);\n  return (\n    <span\n      ref={ref}\n      aria-hidden=\"true\"\n      data-message-caret=\"\"\n      className=\"ml-0.5 inline-block h-[15px] w-0.5 rounded-[1px] bg-foreground align-[-3px]\"\n    />\n  );\n}\n\nexport function MessageContent({ children, className, ...props }: ComponentProps<\"div\">) {\n  const context = useMessage(\"MessageContent\");\n  const { role, variant } = context;\n  const compact = variant === \"compact\";\n  const bubble = variant !== \"plain\" && role === \"user\";\n  const tool = role === \"tool\";\n  const system = role === \"system\";\n  return (\n    <div\n      data-slot=\"message-content\"\n      className={cn(\n        \"max-w-full min-w-0 border-0 text-pretty wrap-anywhere\",\n        bubble || tool ? (compact ? \"px-2.5 py-1.5\" : \"px-3 py-2\") : \"p-0\",\n        tool ? (compact ? \"rounded-lg\" : \"rounded-[10px]\") : compact ? \"rounded-xl\" : \"rounded-2xl\",\n        bubble ? \"bg-muted\" : tool ? \"bg-muted/55\" : \"bg-transparent\",\n        system ? \"text-subtle-foreground\" : tool ? \"text-muted-foreground\" : \"text-foreground\",\n        tool && \"font-mono\",\n        tool || system ? \"text-[12px]/[17px]\" : compact ? \"text-[12.5px]/[18px]\" : \"text-[13px]/5\",\n        className,\n      )}\n      {...props}\n      ref={context.contentRef}\n      id={`${context.id}-content`}\n    >\n      {children}\n      {context.streaming ? <StreamingCaret /> : null}\n    </div>\n  );\n}\n\nexport function MessageActions({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useMessage(\"MessageActions\");\n  if (context.streaming) return null;\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: a fieldset is for form controls, not message actions.\n    <div\n      data-slot=\"message-actions\"\n      role=\"group\"\n      aria-label=\"Message actions\"\n      className={cn(\n        \"-mt-0.5 flex animate-in flex-wrap items-center gap-0 duration-200 ease-out fade-in-0 motion-reduce:animate-none\",\n        context.variant === \"bubble\" && context.role === \"user\" ? \"ml-0\" : \"-ml-1.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst messageActionVariants = cva(\n  \"cursor-pointer rounded-lg p-0 text-subtle-foreground [transition:background-color_120ms_ease-out,color_120ms_ease-out,scale_140ms_cubic-bezier(0.23,1,0.32,1)] hover:bg-accent hover:text-accent-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-ring active:scale-[0.94] motion-reduce:transition-none motion-reduce:active:scale-100 dark:hover:bg-accent [&_svg:not([class*='size-'])]:size-3.5\",\n  {\n    variants: {\n      variant: { bubble: \"size-7\", plain: \"size-7\", compact: \"size-6\" },\n    },\n  },\n);\n\nexport type MessageActionProps = ComponentProps<\"button\"> & {\n  /** Accessible name for the icon-only action. */\n  label: string;\n};\n\nexport function MessageAction({ label, className, ...props }: MessageActionProps) {\n  const context = useMessage(\"MessageAction\");\n  return (\n    <Button\n      data-slot=\"message-action\"\n      type=\"button\"\n      variant=\"ghost\"\n      size=\"icon\"\n      aria-label={label}\n      title={label}\n      className={cn(messageActionVariants({ variant: context.variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport type MessageCopyProps = Omit<ComponentProps<\"button\">, \"children\"> & {\n  /** Text to copy. Defaults to the text of MessageContent. */\n  text?: string;\n  onCopied?: (text: string) => void;\n  children?: ReactNode;\n};\n\nexport function MessageCopy({\n  text,\n  onCopied,\n  onClick,\n  children,\n  className,\n  ...props\n}: MessageCopyProps) {\n  const context = useMessage(\"MessageCopy\");\n  const [copied, setCopied] = useState(false);\n  useEffect(() => {\n    if (!copied) return;\n    const timeout = window.setTimeout(() => setCopied(false), 1600);\n    return () => window.clearTimeout(timeout);\n  }, [copied]);\n  return (\n    <>\n      <Button\n        data-slot=\"message-copy\"\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"icon\"\n        aria-label=\"Copy message\"\n        title=\"Copy message\"\n        className={cn(messageActionVariants({ variant: context.variant }), className)}\n        {...props}\n        onClick={async (event) => {\n          onClick?.(event);\n          if (event.defaultPrevented) return;\n          const value = text ?? context.contentRef.current?.textContent ?? \"\";\n          try {\n            await navigator.clipboard.writeText(value);\n            setCopied(true);\n            onCopied?.(value);\n          } catch {\n            setCopied(false);\n          }\n        }}\n      >\n        <span className=\"grid place-items-center [&>*]:[grid-area:1/1]\">\n          <span\n            className={cn(\n              \"grid place-items-center transition-[opacity,scale,filter] duration-200 ease-out-quint motion-reduce:transition-none\",\n              copied ? \"scale-100 opacity-100 blur-none\" : \"scale-50 opacity-0 blur-[2px]\",\n            )}\n          >\n            <Check size={14} aria-hidden=\"true\" className=\"text-success-ink size-3.5\" />\n          </span>\n          <span\n            className={cn(\n              \"grid place-items-center transition-[opacity,scale,filter] duration-200 ease-out-quint motion-reduce:transition-none\",\n              copied ? \"scale-50 opacity-0 blur-[2px]\" : \"scale-100 opacity-100 blur-none\",\n            )}\n          >\n            {children ?? <Copy size={14} className=\"size-3.5\" aria-hidden=\"true\" />}\n          </span>\n        </span>\n      </Button>\n      <span role=\"status\" className=\"sr-only\">\n        {copied ? \"Copied to clipboard\" : \"\"}\n      </span>\n    </>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/message.tsx"
    }
  ],
  "categories": [
    "ai",
    "chat"
  ],
  "type": "registry:ui"
}