{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"copy-button","type":"registry:ui","title":"CopyButton","description":"Button preset wired to the clipboard, morphing its icon and label to a success or failure state for a moment after each copy attempt","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["actions"],"registryDependencies":["https://fancy-ui.rama.app/r/button.json","https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/status-morph.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/copy-button/CopyButton.tsx","content":"\"use client\";\n\nimport {\n\tforwardRef,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n\ttype ButtonHTMLAttributes,\n\ttype CSSProperties,\n\ttype MouseEvent,\n\ttype ReactNode,\n} from \"react\";\nimport { cn } from \"../../utils\";\nimport { useConstant } from \"../../internals/dom/ssr\";\nimport { useCopy } from \"../../internals/use-copy\";\nimport { sound as soundFx } from \"../../sound/sound\";\nimport { prefersReducedMotion } from \"../../internals/motion/anchored\";\nimport { Button, type ButtonProps } from \"../button/Button\";\nimport type { ButtonVariant, ButtonSize } from \"../button/types\";\nimport { StatusMorph, type StatusMorphState } from \"../status-morph/StatusMorph\";\nimport \"./copy-button.css\";\n\nexport interface CopyButtonProps extends Omit<\n\tButtonHTMLAttributes<HTMLButtonElement>,\n\t\"onClick\" | \"onCopy\" | \"className\" | \"type\" | \"children\" | \"value\" | \"disabled\"\n> {\n\t/** The text written to the clipboard on activation */\n\tvalue: string;\n\t/** Idle label */\n\tlabel?: string;\n\t/** Label shown for `resetMs` after a successful copy */\n\tcopiedLabel?: string;\n\t/** Label and announcement shown for `resetMs` after a failed copy */\n\terrorLabel?: string;\n\t/** How long the copied state holds before reverting, in milliseconds */\n\tresetMs?: number;\n\t/** Passed straight through to the underlying Button */\n\tvariant?: ButtonVariant;\n\t/** Passed straight through to the underlying Button */\n\tsize?: ButtonSize;\n\t/** Disables the button and blocks the copy */\n\tdisabled?: boolean;\n\t/** Drops the visible label, moving it to `aria-label` instead */\n\ticonOnly?: boolean;\n\t/** Called with the value and whether the write actually succeeded */\n\tonCopy?: (value: string, ok: boolean) => void;\n\t/**\n\t * Overrides the default icon + label content. The success/failure skins\n\t * (border, background, text colour) and the disabled/copy wiring still\n\t * apply — set `iconOnly` too if the custom content has no readable text,\n\t * so the button keeps an accessible name.\n\t */\n\tchildren?: ReactNode;\n\t/** Fires on activation, right after the copy starts. Never called while `disabled`. */\n\tonClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Plays the matching interface cue through the sound controller. Off by\n\t * default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\n/** How long the outgoing label stays on screen, in milliseconds. */\nconst LEAVE_MS = 80;\n\n/**\n * The copy glyph handed to StatusMorph's `idle` slot. A module-scope constant\n * rather than a per-render element: it never varies, and a stable identity\n * keeps that subtree from re-reconciling on every render of the parent.\n */\nconst COPY_ICON: ReactNode = (\n\t<svg\n\t\tclassName=\"size-full\"\n\t\tviewBox=\"0 0 24 24\"\n\t\tfill=\"none\"\n\t\tstroke=\"currentColor\"\n\t\tstrokeWidth=\"2\"\n\t\tstrokeLinecap=\"round\"\n\t\tstrokeLinejoin=\"round\"\n\t\taria-hidden=\"true\"\n\t>\n\t\t<rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\" />\n\t\t<path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" />\n\t</svg>\n);\n\n/*\n * The inline `style` rides StatusMorph's rest props and lands as an attribute,\n * which beats its own `.ft-statusmorph { width: calc(1em + 1px) }` — a\n * `className=\"size-4\"` would lose here, because Tailwind utilities are layered\n * and StatusMorph's own rule is not. `font-size` is set alongside it so\n * StatusMorph's INNER `calc(1em + 1px)` (the SVG, which the outer width does\n * not reach) resolves to the same `1rem`: the glyph then fills the box exactly,\n * holding the 16px footprint the copy icon has today instead of sitting 2px\n * short of it in the top-left corner.\n *\n * `--ft-statusmorph-error` is set to the SAME chain the failure skin in\n * `copy-button.css` resolves, because the two fallbacks in the family disagree:\n * StatusMorph's own last-resort red is `oklch(0.5 0.19 25)` /\n * `oklch(0.7 0.18 25)` and the skin's is `oklch(0.577 0.245 27.325)` /\n * `oklch(0.704 0.191 22.216)`. The package ships no stylesheet, so \"neither\n * token declared\" is the out-of-the-box case — and there a 16px cross would sit\n * inside a label and a border painted a visibly different red. Reading\n * `--ft-status-error` first keeps a theme that sets it winning on both\n * surfaces; setting neither now yields one red instead of two. The success pair\n * needs no equivalent: both `--ft-status-done` fallbacks are already\n * character-identical.\n */\nconst MORPH_STYLE = {\n\twidth: \"1rem\",\n\theight: \"1rem\",\n\tfontSize: \"calc(1rem - 1px)\",\n\t\"--ft-statusmorph-error\":\n\t\t\"var(--ft-status-error, light-dark(oklch(0.577 0.245 27.325), oklch(0.704 0.191 22.216)))\",\n} as CSSProperties;\n\nexport const CopyButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, CopyButtonProps>(\n\tfunction CopyButton(\n\t\t{\n\t\t\tvalue,\n\t\t\tlabel = \"Copy\",\n\t\t\tcopiedLabel = \"Copied\",\n\t\t\terrorLabel = \"Copy failed\",\n\t\t\tresetMs = 2000,\n\t\t\tvariant = \"outline\",\n\t\t\tsize = \"md\",\n\t\t\tdisabled = false,\n\t\t\ticonOnly = false,\n\t\t\tonCopy,\n\t\t\tchildren,\n\t\t\tclassName,\n\t\t\tsound = false,\n\t\t\tonClick,\n\t\t\t...restProps\n\t\t},\n\t\tref\n\t) {\n\t\t// Read once, on purpose: `createCopy` takes its reset delay as a\n\t\t// constructor argument, not a reactive input, so `resetMs` is not meant\n\t\t// to be retuned after the button has mounted. The same frozen value is\n\t\t// handed to StatusMorph below — a reactive `resetAfter` there would let a\n\t\t// post-mount `resetMs` retime the glyph without retiming the skin, and\n\t\t// the two would then disagree for exactly the difference between them.\n\t\tconst resetWindow = useConstant(() => resetMs);\n\t\tconst copyState = useCopy(resetWindow);\n\n\t\t// The same latest-attempt guard `useCopy` keeps for `copied`, kept here\n\t\t// for the state this component owns. A permission prompt can hold one\n\t\t// write open across a second click, and both promises then resolve in\n\t\t// whatever order the user agent settled them: without the ticket, a first\n\t\t// click's late failure would repaint the error skin over a second click's\n\t\t// success, and `copyState.copied` — which IS ticket-guarded — would\n\t\t// disagree with it.\n\t\tconst attempt = useRef(0);\n\n\t\t// The glyph's own state. React has no two-way binding, so StatusMorph's\n\t\t// `onStateChange` — the port's stand-in for `bind:state` — is what walks\n\t\t// this back to \"idle\" when the morph's own `resetAfter` timer fires.\n\t\t// Deliberately not a second authority for the success window: `useCopy`\n\t\t// still owns `copyState.copied` (the skin and the visible label), and\n\t\t// both windows are armed in the same tick with the same duration, so no\n\t\t// synchronisation code is needed — `handleClick` only has to make sure a\n\t\t// repeat click re-arms both rather than one.\n\t\tconst [morphState, setMorphState] = useState<StatusMorphState>(\"idle\");\n\n\t\t// Svelte's `{#if children}` and `children ? … : …` are truthiness tests;\n\t\t// this keeps the same reading, and gives the reset effect below a stable\n\t\t// dependency instead of a ReactNode whose identity changes every render.\n\t\tconst hasChildren = Boolean(children);\n\n\t\tconst currentLabel =\n\t\t\tmorphState === \"error\" ? errorLabel : copyState.copied ? copiedLabel : label;\n\n\t\t// One attempt, one skin. The success class is gated on the ABSENCE of an\n\t\t// error, not merely on `copyState.copied`: `copy()` returns from its\n\t\t// `catch` before it touches that flag, so a failure landing inside a\n\t\t// standing success window leaves `copied` true while the glyph is already\n\t\t// on \"error\". Without the guard the button would carry both skins at once\n\t\t// and which red-or-green actually painted would come down to the order\n\t\t// the two rules happen to sit in the stylesheet — a coin toss, and a\n\t\t// public class list claiming two contradictory outcomes.\n\t\tconst classes = cn(\n\t\t\t\"ft-copybtn\",\n\t\t\tcopyState.copied && morphState !== \"error\" && \"ft-copybtn--copied border\",\n\t\t\tmorphState === \"error\" && \"ft-copybtn--failed border\",\n\t\t\tclassName\n\t\t);\n\n\t\tasync function handleClick(event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) {\n\t\t\t// This click is the only user gesture in the interaction, and the\n\t\t\t// outcome cue below plays after an await: on a reload with sound\n\t\t\t// already enabled no AudioContext exists yet, and by then the\n\t\t\t// transient activation may be gone. Creating/resuming it here,\n\t\t\t// synchronously, keeps that cue audible.\n\t\t\tif (sound && soundFx.enabled) void soundFx.unlock();\n\t\t\t// Clears any standing outcome before the new attempt, for two reasons:\n\t\t\t// the previous confirmation is stale the moment a fresh copy is in\n\t\t\t// flight, and StatusMorph re-arms its reset timer on a state CHANGE\n\t\t\t// only — writing \"success\" over \"success\" would leave the glyph on the\n\t\t\t// first click's deadline while `useCopy` restarts the skin's. The\n\t\t\t// await between the two writes is what makes this a real change rather\n\t\t\t// than a no-op collapsed inside a single render.\n\t\t\tsetMorphState(\"idle\");\n\t\t\t// `copy()` resolves false instead of throwing on a denied permission or\n\t\t\t// a missing clipboard API — that outcome is reported to the caller\n\t\t\t// honestly, not swallowed into a silent no-op, and is now shown and\n\t\t\t// announced too.\n\t\t\tconst mine = ++attempt.current;\n\t\t\t// `value` is snapshotted: it is a live prop, and a parent that swaps it\n\t\t\t// while a permission prompt holds the write open would otherwise make\n\t\t\t// `onCopy` report text that was never written.\n\t\t\tconst attempted = value;\n\t\t\tconst pending = copyState.copy(attempted);\n\t\t\t// The consumer click runs now, while the event is still live.\n\t\t\tonClick?.(event);\n\t\t\tconst ok = await pending;\n\t\t\t// `onCopy` still fires for every attempt, stale or not — it reports\n\t\t\t// what that call did, matching `copy()`'s own honest return. Only the\n\t\t\t// visible and audible cues, which describe the button's CURRENT state,\n\t\t\t// are dropped when a newer attempt has already spoken for them.\n\t\t\tif (mine === attempt.current) {\n\t\t\t\tsetMorphState(ok ? \"success\" : \"error\");\n\t\t\t\tif (sound) soundFx.play(ok ? \"copy\" : \"error\");\n\t\t\t}\n\t\t\tonCopy?.(attempted, ok);\n\t\t}\n\n\t\t// Out 80 ms, in 160 ms: a label change keeps the OUTGOING text alive for the\n\t\t// out beat as a second, `aria-hidden` layer in the same grid cell, while\n\t\t// the keyed live span fades in after that beat. Derived during render (the\n\t\t// documented \"adjust state on prop change\" pattern) so the outgoing text\n\t\t// is in the very commit that shows the new one. `prefersReducedMotion()`\n\t\t// is read once per swap: under reduced motion there is no outgoing layer\n\t\t// and the text snaps.\n\t\tconst [swap, setSwap] = useState<{\n\t\t\tshown: string;\n\t\t\tleaving: string | null;\n\t\t\tswapped: boolean;\n\t\t}>({ shown: currentLabel, leaving: null, swapped: false });\n\t\tif (swap.shown !== currentLabel) {\n\t\t\tsetSwap({\n\t\t\t\tshown: currentLabel,\n\t\t\t\t// A second swap inside the out beat: `swap.shown` was still in its\n\t\t\t\t// delayed fade-in and never painted, so keep the layer that is on\n\t\t\t\t// screen (and its timer) rather than replacing it with unseen text.\n\t\t\t\tleaving: swap.leaving !== null ? swap.leaving : prefersReducedMotion() ? null : swap.shown,\n\t\t\t\tswapped: true,\n\t\t\t});\n\t\t}\n\t\tuseEffect(() => {\n\t\t\tif (swap.leaving === null) return;\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tsetSwap((current) => ({ ...current, leaving: null }));\n\t\t\t}, LEAVE_MS);\n\t\t\treturn () => clearTimeout(timer);\n\t\t}, [swap.leaving]);\n\n\t\t// StatusMorph's `resetAfter` is what normally walks `morphState` back to\n\t\t// \"idle\", and custom `children` replace the whole icon slot — so in that\n\t\t// composition nothing owns the timer and a failure would keep the error\n\t\t// skin, the error label and the assertive live region forever. The parent\n\t\t// takes the timer over for exactly that case, on the same window and with\n\t\t// the same 0-means-1ms clamp the morph is handed below.\n\t\tuseEffect(() => {\n\t\t\tif (!hasChildren || morphState === \"idle\") return;\n\t\t\tconst timer = setTimeout(\n\t\t\t\t() => {\n\t\t\t\t\tsetMorphState(\"idle\");\n\t\t\t\t},\n\t\t\t\tresetWindow > 0 ? resetWindow : 1\n\t\t\t);\n\t\t\treturn () => clearTimeout(timer);\n\t\t}, [hasChildren, morphState, resetWindow]);\n\n\t\t/*\n\t\t * `resetAfter` is clamped away from 0 for a vocabulary clash, not a whim:\n\t\t * to `useCopy` a 0 window means \"revert on the next tick\", to StatusMorph\n\t\t * it means \"no timer at all, manual reset only\". 1ms makes both read the\n\t\t * prop the same way, so `resetMs={0}` reverts glyph and label together\n\t\t * the way it did before the glyph existed, instead of stranding the cross\n\t\t * on screen forever.\n\t\t */\n\t\tconst statusIcon = (\n\t\t\t<StatusMorph\n\t\t\t\tstate={morphState}\n\t\t\t\tonStateChange={setMorphState}\n\t\t\t\ttone=\"semantic\"\n\t\t\t\tresetAfter={resetWindow > 0 ? resetWindow : 1}\n\t\t\t\tlabels={{ success: copiedLabel, error: errorLabel }}\n\t\t\t\tidle={COPY_ICON}\n\t\t\t\tstyle={MORPH_STYLE}\n\t\t\t/>\n\t\t);\n\n\t\treturn (\n\t\t\t<Button\n\t\t\t\t{...(restProps as Partial<ButtonProps>)}\n\t\t\t\ttype=\"button\"\n\t\t\t\tref={ref}\n\t\t\t\tvariant={variant}\n\t\t\t\tsize={size}\n\t\t\t\tdisabled={disabled}\n\t\t\t\tlabel={iconOnly ? currentLabel : undefined}\n\t\t\t\tclassName={classes}\n\t\t\t\ticonStart={hasChildren ? undefined : statusIcon}\n\t\t\t\tonClick={handleClick}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t\t{/*\n\t\t\t\t\tThe announcement is StatusMorph's job, through the `role=\"status\"`\n\t\t\t\t\tregion it portals to `document.body` (which is also why it never\n\t\t\t\t\tjoins this button's accessible name), and two live regions would\n\t\t\t\t\tdouble-announce every copy. The one case with no StatusMorph at all\n\t\t\t\t\tis custom `children`, which replace `iconStart` — there the span\n\t\t\t\t\tkeeps the announcement, because custom content has no way of its\n\t\t\t\t\town to say the copy landed and colour alone is a sighted-only\n\t\t\t\t\tsignal. When something else already owns the visible label\n\t\t\t\t\t(icon-only, or custom `children`) the span is `sr-only` and there\n\t\t\t\t\tis nothing to reserve width for.\n\n\t\t\t\t\tOtherwise the label sits in a one-cell grid stacking three\n\t\t\t\t\t`aria-hidden` ghosts (idle, copied, failed) under the live span: the\n\t\t\t\t\tcell is as wide as the widest of them, in pure CSS, so the button\n\t\t\t\t\tnever changes width when the text swaps and nothing is measured\n\t\t\t\t\t(SSR-safe).\n\t\t\t\t*/}\n\t\t\t\t{iconOnly || hasChildren ? (\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"ft-copybtn__live sr-only\"\n\t\t\t\t\t\taria-live={hasChildren ? (morphState === \"error\" ? \"assertive\" : \"polite\") : undefined}\n\t\t\t\t\t>\n\t\t\t\t\t\t{currentLabel}\n\t\t\t\t\t</span>\n\t\t\t\t) : (\n\t\t\t\t\t<span className=\"ft-copybtn__label\">\n\t\t\t\t\t\t<span className=\"ft-copybtn__ghost\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t{label}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span className=\"ft-copybtn__ghost\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t{copiedLabel}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span className=\"ft-copybtn__ghost\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t{errorLabel}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t{swap.leaving !== null && (\n\t\t\t\t\t\t\t<span className=\"ft-copybtn__leave\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t{swap.leaving}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tkey={currentLabel}\n\t\t\t\t\t\t\tclassName=\"ft-copybtn__live\"\n\t\t\t\t\t\t\tdata-swapped={swap.swapped ? \"\" : undefined}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{currentLabel}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</span>\n\t\t\t\t)}\n\t\t\t</Button>\n\t\t);\n\t}\n);\n\nCopyButton.displayName = \"CopyButton\";\n","type":"registry:ui","target":"@ui/fancy/components/copy-button/CopyButton.tsx"},{"path":"react/src/components/copy-button/copy-button.css","content":"/*\n * `--ft-status-done` / `--ft-status-error` are the family's actual \"operation\n * landed\" and \"operation failed\" vocabulary — the same tokens ToolCall,\n * ToolTimeline, AgentPlan, SubagentList, CodeDiff, ApprovalCard, AiDataTable,\n * TerminalBlock, ContextRing, RecommendationCard and Toast all read. Reusing\n * them (fallback hues included, not a mockup's) means a copy outcome sitting\n * next to a tool-call or a toast on the same page reads as one palette, and\n * retinting a token once moves every success — or every failure — surface in\n * the library together, this one included. The error fallback pair is the same\n * one the toast surface uses, character for character, so the two failure\n * surfaces stay one colour even in a theme that declares neither token.\n *\n * The Svelte source wraps both rules in `:global()`, which is that compiler's\n * way of saying what plain CSS says by default: the classes land on the\n * `<button>`/`<a>` that Button renders inside its own template, which is\n * outside CopyButton's own markup. Both selectors are already anchored on the\n * component's root class (`ft-copybtn`), so nothing here leaks a generic name.\n *\n * Unlayered author CSS beats Tailwind's `@layer utilities` regardless of\n * selector order or the `:hover` state Button's own variant classes add, so\n * neither skin needs a separate hover rule to keep it from flickering back to\n * the idle variant's colours on pointer-over.\n */\n.ft-copybtn--copied {\n\tborder-color: color-mix(\n\t\tin oklab,\n\t\tvar(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145))) 35%,\n\t\ttransparent\n\t);\n\tbackground-color: color-mix(\n\t\tin oklab,\n\t\tvar(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145))) 10%,\n\t\ttransparent\n\t);\n\tcolor: var(--ft-status-done, light-dark(oklch(0.5 0.14 145), oklch(0.72 0.15 145)));\n}\n\n.ft-copybtn--failed {\n\tborder-color: color-mix(\n\t\tin oklab,\n\t\tvar(--ft-status-error, light-dark(oklch(0.577 0.245 27.325), oklch(0.704 0.191 22.216))) 35%,\n\t\ttransparent\n\t);\n\tbackground-color: color-mix(\n\t\tin oklab,\n\t\tvar(--ft-status-error, light-dark(oklch(0.577 0.245 27.325), oklch(0.704 0.191 22.216))) 10%,\n\t\ttransparent\n\t);\n\tcolor: var(--ft-status-error, light-dark(oklch(0.577 0.245 27.325), oklch(0.704 0.191 22.216)));\n}\n\n/*\n * Label cell. Three `aria-hidden` ghosts (idle, copied, failed) share one grid\n * cell with the live span, so the cell is as wide as the widest label and the\n * button never changes width when the text swaps. Pure CSS, nothing measured.\n */\n.ft-copybtn__label {\n\tdisplay: inline-grid;\n\ttext-align: start;\n}\n\n.ft-copybtn__label > * {\n\tgrid-area: 1 / 1;\n\twhite-space: nowrap;\n}\n\n.ft-copybtn__ghost {\n\tvisibility: hidden;\n\tpointer-events: none;\n\tuser-select: none;\n}\n\n.ft-copybtn__leave {\n\tpointer-events: none;\n}\n\n/* transform / opacity only ever run under `no-preference`; under reduce the\n   text snaps and the component never renders the outgoing layer. */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-copybtn__leave {\n\t\tanimation: ft-copybtn-out 80ms var(--ft-ease-in, cubic-bezier(0.7, 0, 0.84, 0)) forwards;\n\t}\n\n\t.ft-copybtn__live[data-swapped] {\n\t\tanimation: ft-copybtn-in 160ms var(--ft-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) 80ms backwards;\n\t}\n}\n\n@keyframes ft-copybtn-out {\n\tto {\n\t\topacity: 0;\n\t\ttransform: translateY(-2px);\n\t}\n}\n\n@keyframes ft-copybtn-in {\n\tfrom {\n\t\topacity: 0;\n\t\ttransform: translateY(2px);\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/copy-button/copy-button.css"},{"path":"react/src/components/copy-button/index.ts","content":"\"use client\";\n\nexport { CopyButton } from \"./CopyButton\";\nexport type { CopyButtonProps } from \"./CopyButton\";\n","type":"registry:ui","target":"@ui/fancy/components/copy-button/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/copy-button?framework=react"}}