{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message-actions",
  "title": "Message Actions",
  "description": "Per-message controls — copy, regenerate, and thumb feedback.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "components/spectrumui/blocks/ai-assistants/message-actions.tsx",
      "content": "'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport { Check, Copy, GitBranch, RefreshCw, ThumbsDown, ThumbsUp } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport type { MessageFeedback } from './types';\n\nexport type MessageActionsVariant = 'Ghost' | 'Pill';\n\nexport interface MessageActionsProps {\n  content?: string;\n  feedback?: MessageFeedback | null;\n  onCopy?: (content: string) => void;\n  onRegenerate?: () => void;\n  onFeedback?: (feedback: MessageFeedback | null) => void;\n  onBranch?: () => void;\n  variant?: MessageActionsVariant;\n  className?: string;\n}\n\nexport function MessageActions({\n  content = '',\n  feedback: feedbackProp,\n  onCopy,\n  onRegenerate,\n  onFeedback,\n  onBranch,\n  variant = 'Ghost',\n  className,\n}: MessageActionsProps) {\n  const [copied, setCopied] = useState(false);\n  const [spinning, setSpinning] = useState(false);\n  const [feedbackState, setFeedbackState] = useState<MessageFeedback | null>(null);\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => () => clearTimeout(timer.current), []);\n\n  const feedback = feedbackProp !== undefined ? feedbackProp : feedbackState;\n\n  async function copy() {\n    try {\n      await navigator.clipboard.writeText(content);\n    } catch {}\n    onCopy?.(content);\n    setCopied(true);\n    clearTimeout(timer.current);\n    timer.current = setTimeout(() => setCopied(false), 1600);\n  }\n\n  function regenerate() {\n    setSpinning(true);\n    setTimeout(() => setSpinning(false), 500);\n    onRegenerate?.();\n  }\n\n  function rate(next: MessageFeedback) {\n    const value = feedback === next ? null : next;\n    setFeedbackState(value);\n    onFeedback?.(value);\n  }\n\n  return (\n    <div\n      role=\"toolbar\"\n      aria-label=\"Message actions\"\n      className={cn(\n        'flex w-fit items-center gap-0.5 text-neutral-400 dark:text-neutral-500',\n        'opacity-100 transition-opacity duration-150 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/message:opacity-100 [@media(hover:hover)]:focus-within:opacity-100',\n        variant === 'Pill' &&\n          'rounded-full border border-black/[0.07] bg-white p-0.5 shadow-xs dark:border-white/[0.08] dark:bg-neutral-900',\n        className,\n      )}\n    >\n      <ActionButton label={copied ? 'Copied' : 'Copy message'} onClick={copy}>\n        <span className=\"relative grid size-3.5 place-items-center\">\n          <Copy\n            className={cn(\n              'absolute size-3.5 transition-[opacity,filter] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]',\n              copied ? 'opacity-0 blur-[2px]' : 'opacity-100 blur-0',\n            )}\n          />\n          <Check\n            className={cn(\n              'absolute size-3.5 text-emerald-600 transition-[opacity,filter] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] dark:text-emerald-400',\n              copied ? 'opacity-100 blur-0' : 'opacity-0 blur-[2px]',\n            )}\n          />\n        </span>\n      </ActionButton>\n\n      <ActionButton label=\"Regenerate\" onClick={regenerate}>\n        <RefreshCw\n          className={cn(\n            'size-3.5 transition-transform duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]',\n            spinning && 'rotate-180',\n          )}\n        />\n      </ActionButton>\n\n      <ActionButton\n        label=\"Good response\"\n        pressed={feedback === 'positive'}\n        onClick={() => rate('positive')}\n      >\n        <ThumbsUp\n          className={cn(\n            'size-3.5 transition-[transform,color] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]',\n            feedback === 'positive' && 'scale-110 fill-current text-neutral-800 dark:text-neutral-100',\n          )}\n        />\n      </ActionButton>\n\n      <ActionButton\n        label=\"Bad response\"\n        pressed={feedback === 'negative'}\n        onClick={() => rate('negative')}\n      >\n        <ThumbsDown\n          className={cn(\n            'size-3.5 transition-[transform,color] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]',\n            feedback === 'negative' && 'scale-110 fill-current text-neutral-800 dark:text-neutral-100',\n          )}\n        />\n      </ActionButton>\n\n      {onBranch && (\n        <ActionButton label=\"Branch from here\" onClick={onBranch}>\n          <GitBranch className=\"size-3.5\" />\n        </ActionButton>\n      )}\n    </div>\n  );\n}\n\nfunction ActionButton({\n  label,\n  pressed,\n  onClick,\n  children,\n}: {\n  label: string;\n  pressed?: boolean;\n  onClick?: () => void;\n  children: React.ReactNode;\n}) {\n  return (\n    <button\n      type=\"button\"\n      title={label}\n      aria-label={label}\n      aria-pressed={pressed}\n      onClick={onClick}\n      className=\"grid size-7 place-items-center rounded-md transition-[color,background-color,transform] duration-150 hover:bg-black/[0.04] hover:text-neutral-700 active:scale-[0.92] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-neutral-400 dark:hover:bg-white/[0.06] dark:hover:text-neutral-200\"\n    >\n      {children}\n    </button>\n  );\n}\n\nexport default MessageActions;\n",
      "type": "registry:block",
      "target": "components/spectrumui/blocks/ai-assistants/message-actions.tsx"
    },
    {
      "path": "components/spectrumui/blocks/ai-assistants/types.ts",
      "content": "export type MessageRole = 'user' | 'assistant' | 'system';\n\nexport type MessageState = 'streaming' | 'complete' | 'error';\n\nexport interface Attachment {\n  id: string;\n  name: string;\n  size: number;\n  type: string;\n  url?: string;\n  previewUrl?: string;\n}\n\nexport interface Citation {\n  id: string;\n  index: number;\n  url: string;\n  title: string;\n  snippet?: string;\n  favicon?: string;\n}\n\nexport type ToolCallStatus = 'pending' | 'running' | 'success' | 'error' | 'cancelled';\n\nexport interface ToolCall {\n  id: string;\n  name: string;\n  args?: Record<string, unknown>;\n  result?: string;\n  status: ToolCallStatus;\n  startedAt?: number;\n  completedAt?: number;\n  children?: ToolCall[];\n  parallel?: boolean;\n}\n\nexport interface ReasoningStep {\n  id: string;\n  content: string;\n}\n\nexport interface Reasoning {\n  steps: ReasoningStep[];\n  status: 'thinking' | 'complete';\n  durationMs?: number;\n}\n\nexport interface Message {\n  id: string;\n  role: MessageRole;\n  content: string;\n  createdAt?: number;\n  state?: MessageState;\n  attachments?: Attachment[];\n  citations?: Citation[];\n  toolCalls?: ToolCall[];\n  reasoning?: Reasoning;\n}\n\nexport interface Conversation {\n  id: string;\n  label: string;\n}\n\nexport interface ModelOption {\n  id: string;\n  name: string;\n  description?: string;\n  badge?: string;\n  disabled?: boolean;\n}\n\nexport interface SuggestedPrompt {\n  id: string;\n  label: string;\n  prompt?: string;\n}\n\nexport type MessageFeedback = 'positive' | 'negative';\n\nexport interface UsageState {\n  promptTokens: number;\n  completionTokens: number;\n  contextWindow: number;\n  estimatedCostUsd?: number;\n}\n",
      "type": "registry:file",
      "target": "components/spectrumui/blocks/ai-assistants/types.ts"
    }
  ],
  "type": "registry:block"
}
