{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "title": "Copy button",
  "description": "One exact value taken to the clipboard in one keystroke, composed over the kit's Button because the shadcn registry carries no copy control. Idle, copied and refused are three states and three announcements: the outcome lands in a visually-hidden role=\"status\" region, so a person who cannot see the tick is told at the same moment, and a browser that refuses clipboard access says so instead of doing nothing.",
  "dependencies": [
    "lucide-react@1.33.0"
  ],
  "registryDependencies": [
    "@waimakers/utils",
    "@waimakers/theme",
    "@waimakers/button"
  ],
  "files": [
    {
      "path": "src/components/ui/copy-button.tsx",
      "content": "import { Check, Copy } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"./button\";\n\nconst RESET_AFTER_MS = 2_000;\n\ntype CopyOutcome = \"idle\" | \"copied\" | \"failed\";\n\nexport interface CopyButtonProps extends Omit<ComponentProps<typeof Button>, \"children\" | \"value\"> {\n  value: string;\n  label: string;\n\n  children?: ReactNode;\n\n  onCopied?: (value: string) => void;\n\n  onCopyFailed?: (value: string) => void;\n\n  writeText?: (value: string) => Promise<void>;\n}\n\nconst defaultWriteText = (value: string) => navigator.clipboard.writeText(value);\n\nfunction CopyButton({\n  value,\n  label,\n  children,\n  onCopied,\n  onCopyFailed,\n  writeText = defaultWriteText,\n  className,\n  variant = \"outline\",\n  size,\n  ...props\n}: CopyButtonProps) {\n  const [outcome, setOutcome] = useState<CopyOutcome>(\"idle\");\n\n  const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  useEffect(\n    () => () => {\n      if (resetTimer.current !== null) {\n        clearTimeout(resetTimer.current);\n      }\n    },\n    [],\n  );\n\n  const copy = useCallback(async () => {\n    if (resetTimer.current !== null) {\n      clearTimeout(resetTimer.current);\n    }\n    setOutcome(\"idle\");\n\n    let next: CopyOutcome;\n\n    try {\n      await writeText(value);\n      next = \"copied\";\n      onCopied?.(value);\n    } catch {\n      next = \"failed\";\n      onCopyFailed?.(value);\n    }\n    setOutcome(next);\n\n    resetTimer.current = setTimeout(() => {\n      setOutcome(\"idle\");\n    }, RESET_AFTER_MS);\n  }, [value, writeText, onCopied, onCopyFailed]);\n\n  const announcement =\n    outcome === \"copied\"\n      ? `${label} copied.`\n      : outcome === \"failed\"\n        ? `${label} could not be copied. Your browser refused clipboard access — select the text and copy it by hand.`\n        : \"\";\n  return (\n    <>\n      <Button\n        type=\"button\"\n        variant={variant}\n        size={size ?? (children === undefined ? \"icon\" : \"sm\")}\n        aria-label={`Copy the ${label}`}\n        data-slot=\"copy-button\"\n        data-state={outcome}\n        className={cn(className)}\n        onClick={() => {\n          void copy();\n        }}\n        {...props}\n      >\n        {outcome === \"copied\" ? <Check aria-hidden=\"true\" /> : <Copy aria-hidden=\"true\" />}\n        {children}\n      </Button>\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </>\n  );\n}\n\nexport { CopyButton, RESET_AFTER_MS };\n",
      "type": "registry:ui",
      "target": "components/ui/copy-button.tsx"
    }
  ],
  "type": "registry:ui"
}