{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "title": "Copy Button",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A copy-to-clipboard button with a morphing check icon, an optional label, a fallback for blocked clipboards, an error state and a polite announcement.",
  "dependencies": [
    "motion@^12",
    "lucide-react",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/copy-button.tsx",
      "content": "// Ballmac UI: Copy Button. https://ui.ballmac.com/components/copy-button\n\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { AlertCircle, Check, Copy } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\nconst copyButtonVariants = cva(\n  \"relative inline-flex shrink-0 items-center justify-center gap-1.5 rounded-md text-sm font-medium whitespace-nowrap outline-none transition-[color,background-color,border-color,box-shadow] duration-150 select-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        ghost: \"text-muted-foreground hover:bg-accent hover:text-foreground\",\n        outline: \"border bg-background text-foreground shadow-xs hover:bg-accent\",\n        solid: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n      },\n      size: {\n        sm: \"h-7 text-xs [&_svg]:size-3.5\",\n        default: \"h-8 text-[13px] [&_svg]:size-4\",\n        lg: \"h-10 [&_svg]:size-4\",\n      },\n    },\n    defaultVariants: { variant: \"ghost\", size: \"default\" },\n  }\n)\n\n/** Writes text to the clipboard, falling back to a hidden textarea where the async API is blocked (insecure origins, some iframes). */\nasync function copyText(text: string): Promise<boolean> {\n  try {\n    await navigator.clipboard.writeText(text)\n    return true\n  } catch {\n    try {\n      const area = document.createElement(\"textarea\")\n      area.value = text\n      area.setAttribute(\"readonly\", \"\")\n      area.style.cssText = \"position:fixed;top:0;left:0;opacity:0;pointer-events:none\"\n      document.body.appendChild(area)\n      area.select()\n      const ok = document.execCommand(\"copy\")\n      area.remove()\n      return ok\n    } catch {\n      return false\n    }\n  }\n}\n\ntype CopyButtonProps = Omit<React.ComponentProps<\"button\">, \"value\" | \"onCopy\" | \"children\"> &\n  VariantProps<typeof copyButtonVariants> & {\n    /** Text to copy. */\n    value?: string\n    /** Reads the text at click time instead, for content that changes (a selection, a form). May be async. */\n    getValue?: () => string | Promise<string>\n    /** Visible label next to the icon, such as \"Copy\". Leave out for an icon-only button. */\n    label?: string\n    /** Label shown after a successful copy. */\n    copiedLabel?: string\n    /** Accessible name of an icon-only button. */\n    ariaLabel?: string\n    /** How long the confirmation stays, in milliseconds. */\n    resetAfter?: number\n    /** Called with the copied text. */\n    onCopied?: (text: string) => void\n    /** Called when both clipboard routes fail. */\n    onError?: () => void\n  }\n\nfunction CopyButton({\n  value,\n  getValue,\n  label,\n  copiedLabel,\n  ariaLabel,\n  resetAfter = 1800,\n  onCopied,\n  onError,\n  variant,\n  size,\n  className,\n  onClick,\n  ...props\n}: CopyButtonProps) {\n  const msg = useMessages()\n  copiedLabel ??= msg(\"copy-button.copiedLabel\", \"Copied\")\n  ariaLabel ??= msg(\"copy-button.ariaLabel\", \"Copy to clipboard\")\n  const reduce = useReducedMotion()\n  const [state, setState] = React.useState<\"idle\" | \"copied\" | \"failed\">(\"idle\")\n  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)\n  React.useEffect(() => () => clearTimeout(timer.current), [])\n\n  async function handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n    onClick?.(event)\n    if (event.defaultPrevented) return\n    const text = getValue ? await getValue() : (value ?? \"\")\n    const ok = await copyText(text)\n    setState(ok ? \"copied\" : \"failed\")\n    if (ok) onCopied?.(text)\n    else onError?.()\n    clearTimeout(timer.current)\n    timer.current = setTimeout(() => setState(\"idle\"), resetAfter)\n  }\n\n  const word = state === \"copied\" ? copiedLabel : state === \"failed\" ? \"Copy failed\" : label\n  const Icon = state === \"copied\" ? Check : state === \"failed\" ? AlertCircle : Copy\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        data-slot=\"copy-button\"\n        data-state={state}\n        aria-label={word ?? ariaLabel}\n        title={word ?? ariaLabel}\n        onClick={handleClick}\n        className={cn(\n          copyButtonVariants({ variant, size }),\n          label ? { sm: \"px-2\", default: \"px-2.5\", lg: \"px-3.5\" }[size ?? \"default\"] : { sm: \"w-7\", default: \"w-8\", lg: \"w-10\" }[size ?? \"default\"],\n          state === \"copied\" && \"text-foreground\",\n          className\n        )}\n        {...props}\n      >\n        <span aria-hidden=\"true\" className=\"relative flex items-center justify-center\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            <motion.span\n              key={state}\n              className=\"flex\"\n              initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, rotate: -45 }}\n              animate={{ opacity: 1, scale: 1, rotate: 0 }}\n              exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5 }}\n              transition={{ type: \"spring\", stiffness: 520, damping: 30 }}\n            >\n              <Icon className={cn(state === \"copied\" && \"text-chart-2\", state === \"failed\" && \"text-destructive\")} />\n            </motion.span>\n          </AnimatePresence>\n        </span>\n        {label && (\n          <span data-label=\"\" aria-hidden=\"true\">\n            {word}\n          </span>\n        )}\n      </button>\n      <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n        {state === \"copied\" ? \"Copied to clipboard\" : state === \"failed\" ? \"Copy failed\" : \"\"}\n      </span>\n    </>\n  )\n}\n\nexport { CopyButton, copyButtonVariants, copyText, type CopyButtonProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/copy-button.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "copy",
      "clipboard",
      "button",
      "developer"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Pass value (or getValue for content read at click time). label adds visible text, variant is ghost | outline | solid, size sm | default | lg. Falls back to execCommand when navigator.clipboard is blocked and shows 'Copy failed' if both fail.",
      "whenToUse": [
        "Any value a developer will paste: keys, URLs, commands, IDs",
        "Toolbars on code, logs and config"
      ],
      "whenNotToUse": [
        "Copying a whole code block with a header (code-block has its own button)"
      ],
      "composesWith": [
        "code-block",
        "api-key-field",
        "terminal"
      ],
      "a11y": [
        {
          "keys": "Enter / Space",
          "action": "Copies and confirms"
        },
        {
          "keys": "Screen readers",
          "action": "Name changes to 'Copied' and a polite status announces it; failures say 'Copy failed'"
        },
        {
          "keys": "Reduced motion",
          "action": "The icon swaps with a fade only"
        }
      ],
      "customization": [
        "variant",
        "size",
        "label and copiedLabel",
        "getValue for async or dynamic text",
        "resetAfter",
        "onCopied / onError"
      ]
    },
    "examples": [
      "copy-button-demo",
      "copy-button-inline"
    ],
    "url": "https://ui.ballmac.com/components/copy-button"
  },
  "categories": [
    "developer"
  ],
  "type": "registry:ui"
}