{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"message-actions","type":"registry:ui","title":"Message Actions","description":"The action row under an AI reply — copy, regenerate, share and thumbs up/down feedback, revealed on hover.","author":"Scrim UI (https://scrimui.dev)","categories":["messages"],"docs":"https://scrimui.dev/components/message-actions","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/message-actions/message-actions.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/* ------------------------------------------------------------------ */\n/* Types                                                               */\n/* ------------------------------------------------------------------ */\n\nexport type MessageActionsProps = {\n  /** Dim the actions while the message is still streaming. */\n  disabled?: boolean;\n  /** Compact mode shows icon-only buttons. */\n  compact?: boolean;\n  onCopy?: () => void;\n  onRegenerate?: () => void;\n  onShare?: () => void;\n  onFeedback?: (vote: \"up\" | \"down\") => void;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction CopyIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\">\n      <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" />\n      <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" />\n    </svg>\n  );\n}\n\nfunction RegenerateIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\">\n      <path d=\"M21 12a9 9 0 1 1-2.64-6.36L21 8\" />\n      <path d=\"M21 3v5h-5\" />\n    </svg>\n  );\n}\n\nfunction ShareIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"13\" height=\"13\">\n      <path d=\"M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8\" />\n      <path d=\"M16 6l-4-4-4 4\" />\n      <path d=\"M12 2v13\" />\n    </svg>\n  );\n}\n\nfunction ThumbsUpIcon({ filled }: { filled?: boolean }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill={filled ? \"currentColor\" : \"none\"}\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      width=\"13\"\n      height=\"13\"\n    >\n      <path d=\"M7 10v12\" />\n      <path d=\"M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z\" />\n    </svg>\n  );\n}\n\nfunction ThumbsDownIcon({ filled }: { filled?: boolean }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill={filled ? \"currentColor\" : \"none\"}\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      width=\"13\"\n      height=\"13\"\n    >\n      <path d=\"M17 14V2\" />\n      <path d=\"M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z\" />\n    </svg>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* MessageActions                                                      */\n/* ------------------------------------------------------------------ */\n\nexport function MessageActions({\n  disabled = false,\n  compact = false,\n  onCopy,\n  onRegenerate,\n  onShare,\n  onFeedback,\n  className = \"\",\n}: MessageActionsProps) {\n  const [copied, setCopied] = React.useState(false);\n  const [vote, setVote] = React.useState<\"up\" | \"down\" | null>(null);\n\n  const copy = () => {\n    onCopy?.();\n    setCopied(true);\n    window.setTimeout(() => setCopied(false), 1500);\n  };\n\n  const base =\n    \"inline-flex h-7 items-center gap-1.5 rounded-lg px-2 text-xs text-zinc-500 transition-colors dark:text-zinc-400\";\n  const idle = \"hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-100\";\n  const disabledCls = disabled ? \"cursor-not-allowed opacity-50 hover:bg-transparent hover:text-zinc-500\" : idle;\n\n  return (\n    <div className={`flex items-center gap-0.5 ${className}`}>\n      {/* aria-label on all three, not just in compact mode: `compact` hides the\n          text and leaves a button whose only content is an svg, so without it\n          a screen reader announces an unnamed button. Harmless when the text is\n          showing — an explicit label simply wins over the inner text. */}\n      {onCopy && (\n        <button\n          type=\"button\"\n          onClick={copy}\n          disabled={disabled}\n          aria-label={copied ? \"Copied\" : \"Copy\"}\n          className={`${base} ${disabledCls}`}\n        >\n          <CopyIcon />\n          {!compact && (copied ? \"Copied\" : \"Copy\")}\n        </button>\n      )}\n      {onRegenerate && (\n        <button\n          type=\"button\"\n          onClick={onRegenerate}\n          disabled={disabled}\n          aria-label=\"Regenerate\"\n          className={`${base} ${disabledCls}`}\n        >\n          <RegenerateIcon />\n          {!compact && \"Regenerate\"}\n        </button>\n      )}\n      {onFeedback && (\n        <>\n          <button\n            type=\"button\"\n            onClick={() => {\n              onFeedback(\"up\");\n              setVote((v) => (v === \"up\" ? null : \"up\"));\n            }}\n            disabled={disabled}\n            aria-label=\"Good response\"\n            className={`${base} ${disabledCls} ${vote === \"up\" ? \"text-emerald-700 dark:text-emerald-400\" : \"\"}`}\n          >\n            <ThumbsUpIcon filled={vote === \"up\"} />\n          </button>\n          <button\n            type=\"button\"\n            onClick={() => {\n              onFeedback(\"down\");\n              setVote((v) => (v === \"down\" ? null : \"down\"));\n            }}\n            disabled={disabled}\n            aria-label=\"Bad response\"\n            className={`${base} ${disabledCls} ${vote === \"down\" ? \"text-red-600 dark:text-red-400\" : \"\"}`}\n          >\n            <ThumbsDownIcon filled={vote === \"down\"} />\n          </button>\n        </>\n      )}\n      {onShare && (\n        <button\n          type=\"button\"\n          onClick={onShare}\n          disabled={disabled}\n          aria-label=\"Share\"\n          className={`${base} ${disabledCls}`}\n        >\n          <ShareIcon />\n          {!compact && \"Share\"}\n        </button>\n      )}\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/message-actions.tsx"}]}