{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-message-4","type":"registry:block","title":"Reply actions in the footer","description":"Copy, regenerate and rating controls under an unframed assistant reply.","dependencies":[],"registryDependencies":["bubble","button","message"],"files":[{"path":"c-message-4.tsx","type":"registry:block","content":"\"use client\"\n\nimport { useEffect, useState } from \"react\"\n\nimport { Bubble, BubbleContent } from \"@/components/ui/bubble\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Message,\n  MessageContent,\n  MessageFooter,\n} from \"@/components/ui/message\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\n/*\n * Two drafts, because Regenerate has to return a visibly different answer to\n * teach anything. One draft and a spinner reads as a button that is broken.\n */\nconst DRAFTS = [\n  [\n    \"4.2 moves invoicing onto the new tax service, so EU orders now carry a VAT line on the PDF, the hosted receipt and the CSV export.\",\n    \"Two smaller changes ride along: the billing portal keeps the filters you left it on, and a failed payment retries twice before it is marked overdue.\",\n  ],\n  [\n    \"The headline in 4.2 is EU VAT. It is calculated per order rather than per account, and it shows up everywhere an amount does.\",\n    \"We also cleared a long-standing annoyance in the billing portal: filters survive a reload, and overdue is only set after two failed retries.\",\n  ],\n]\n\n/*\n * A rating belongs to the answer that earned it, so regenerating clears it.\n * A thumb left lit reads as feedback on a draft nobody rated.\n */\nexport function Pattern() {\n  const [draftIndex, setDraftIndex] = useState(0)\n  const [vote, setVote] = useState<\"up\" | \"down\" | null>(null)\n  const [copied, setCopied] = useState(false)\n\n  const draft = DRAFTS[draftIndex]\n\n  /*\n   * What goes to the clipboard is derived from draft during render, so this\n   * timed label is the only thing the effect has to unwind.\n   */\n  useEffect(() => {\n    if (!copied) return\n    const id = window.setTimeout(() => setCopied(false), 1600)\n    return () => window.clearTimeout(id)\n  }, [copied])\n\n  return (\n    <div className=\"mx-auto flex w-full max-w-2xl flex-col gap-6\">\n      <Message align=\"end\">\n        <MessageContent>\n          <Bubble variant=\"muted\">\n            <BubbleContent>\n              Summarise what changed in 4.2 for the customer note.\n            </BubbleContent>\n          </Bubble>\n        </MessageContent>\n      </Message>\n\n      <Message>\n        <MessageContent>\n          {/*\n            A ghost variant anywhere in the row drops the padding the footer\n            would otherwise inherit, which sits the toolbar flush with the\n            prose. aria-live carries a swapped draft to a screen reader.\n          */}\n          <Bubble variant=\"ghost\" aria-live=\"polite\">\n            <BubbleContent>\n              <div className=\"space-y-3\">\n                {draft.map((paragraph) => (\n                  <p key={paragraph}>{paragraph}</p>\n                ))}\n              </div>\n            </BubbleContent>\n          </Bubble>\n          <MessageFooter className=\"gap-0.5\">\n            <span className=\"pe-1 tabular-nums\">09:41</span>\n            {/*\n              Copy is a one-shot, so it carries no aria-pressed. A pressed\n              rating swaps variant, because no style tunes a colour override.\n            */}\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon-xs\"\n              aria-label={copied ? \"Reply copied\" : \"Copy reply\"}\n              title={copied ? \"Copied\" : \"Copy\"}\n              onClick={() => {\n                /*\n                 * Optional-chained twice: a page served over plain http has no\n                 * navigator.clipboard at all. The label flips either way.\n                 */\n                void navigator.clipboard?.writeText?.(draft.join(\"\\n\\n\"))\n                setCopied(true)\n              }}\n            >\n              {copied ? (\n                <IconPlaceholder\n                  lucide=\"CheckIcon\"\n                  tabler=\"IconCheck\"\n                  hugeicons=\"Tick02Icon\"\n                  phosphor=\"CheckIcon\"\n                  remixicon=\"RiCheckLine\"\n                  aria-hidden=\"true\"\n                />\n              ) : (\n                <IconPlaceholder\n                  lucide=\"CopyIcon\"\n                  tabler=\"IconCopy\"\n                  hugeicons=\"Copy01Icon\"\n                  phosphor=\"CopyIcon\"\n                  remixicon=\"RiFileCopyLine\"\n                  aria-hidden=\"true\"\n                />\n              )}\n            </Button>\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon-xs\"\n              aria-label=\"Regenerate reply\"\n              title=\"Regenerate\"\n              onClick={() => {\n                setDraftIndex((index) => (index + 1) % DRAFTS.length)\n                setVote(null)\n              }}\n            >\n              <IconPlaceholder\n                lucide=\"RefreshCwIcon\"\n                tabler=\"IconRefreshDot\"\n                hugeicons=\"Refresh04Icon\"\n                phosphor=\"ArrowsClockwiseIcon\"\n                remixicon=\"RiRestartLine\"\n                aria-hidden=\"true\"\n              />\n            </Button>\n            <Button\n              type=\"button\"\n              variant={vote === \"up\" ? \"secondary\" : \"ghost\"}\n              size=\"icon-xs\"\n              aria-label=\"Good reply\"\n              title=\"Good reply\"\n              aria-pressed={vote === \"up\"}\n              onClick={() => setVote(vote === \"up\" ? null : \"up\")}\n            >\n              <IconPlaceholder\n                lucide=\"ThumbsUpIcon\"\n                tabler=\"IconThumbUp\"\n                hugeicons=\"ThumbsUpIcon\"\n                phosphor=\"ThumbsUpIcon\"\n                remixicon=\"RiThumbUpLine\"\n                aria-hidden=\"true\"\n              />\n            </Button>\n            <Button\n              type=\"button\"\n              variant={vote === \"down\" ? \"secondary\" : \"ghost\"}\n              size=\"icon-xs\"\n              aria-label=\"Bad reply\"\n              title=\"Bad reply\"\n              aria-pressed={vote === \"down\"}\n              onClick={() => setVote(vote === \"down\" ? null : \"down\")}\n            >\n              <IconPlaceholder\n                lucide=\"ThumbsDownIcon\"\n                tabler=\"IconThumbDown\"\n                hugeicons=\"ThumbsDownIcon\"\n                phosphor=\"ThumbsDownIcon\"\n                remixicon=\"RiThumbDownLine\"\n                aria-hidden=\"true\"\n              />\n            </Button>\n          </MessageFooter>\n        </MessageContent>\n      </Message>\n    </div>\n  )\n}","target":"components/examples/c-message-4.tsx"}],"meta":{"order":4,"gridSize":2}}