{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "title": "Copy Button",
  "description": "A shadcn Button-based clipboard control with animated copy and success states.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/base/ui/copy-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Check, Copy } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\n\nconst variants = {\n  hidden: { opacity: 0, scale: 0.5 },\n  visible: { opacity: 1, scale: 1 },\n};\n\ntype ButtonProps = React.ComponentPropsWithoutRef<typeof Button>;\ntype ButtonClickEvent = Parameters<NonNullable<ButtonProps[\"onClick\"]>>[0];\n\nexport type CopyButtonProps = Omit<\n  ButtonProps,\n  \"children\" | \"value\" | \"onCopy\"\n> & {\n  /** Text written to the clipboard when the button is pressed. */\n  value: string;\n  /** How long, in ms, the copied state is shown before reverting. */\n  timeout?: number;\n  /** Called with the copied value after a successful write. */\n  onCopy?: (value: string) => void;\n};\n\nexport function CopyButton({\n  value,\n  timeout = 1000,\n  onCopy,\n  onClick,\n  className,\n  variant = \"outline\",\n  size = \"icon\",\n  type = \"button\",\n  \"aria-label\": ariaLabel = \"Copy to clipboard\",\n  ...props\n}: CopyButtonProps) {\n  const [copied, setCopied] = React.useState(false);\n  const shouldReduceMotion = useReducedMotion();\n  const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  React.useEffect(() => {\n    return () => {\n      if (timer.current) clearTimeout(timer.current);\n    };\n  }, []);\n\n  const handleCopy = React.useCallback(\n    async (event: ButtonClickEvent) => {\n      onClick?.(event);\n\n      if (event.defaultPrevented) return;\n\n      try {\n        await navigator.clipboard.writeText(value);\n      } catch {\n        // Clipboard access can be denied (insecure context, no permission) —\n        // bail out without flipping into the copied state.\n        return;\n      }\n\n      onCopy?.(value);\n      setCopied(true);\n\n      if (timer.current) clearTimeout(timer.current);\n      timer.current = setTimeout(() => setCopied(false), timeout);\n    },\n    [onClick, onCopy, timeout, value],\n  );\n\n  // An icon swap is a tiny state change, so keep it snappy: ease-out, well\n  // under 150ms. Motion is removed entirely under prefers-reduced-motion.\n  const transition = shouldReduceMotion\n    ? { duration: 0 }\n    : ({ duration: 0.13, ease: [0.215, 0.61, 0.355, 1] } as const);\n\n  return (\n    <Button\n      type={type}\n      variant={variant}\n      size={size}\n      aria-label={ariaLabel}\n      data-copied={copied}\n      onClick={handleCopy}\n      className={className}\n      {...props}\n    >\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.span\n          key={copied ? \"check\" : \"copy\"}\n          variants={variants}\n          initial=\"hidden\"\n          animate=\"visible\"\n          exit=\"hidden\"\n          transition={transition}\n          className=\"inline-flex\"\n        >\n          {copied ? (\n            <Check data-icon=\"icon\" aria-hidden=\"true\" />\n          ) : (\n            <Copy data-icon=\"icon\" aria-hidden=\"true\" />\n          )}\n        </motion.span>\n      </AnimatePresence>\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {copied ? \"Copied to clipboard\" : \"\"}\n      </span>\n    </Button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/copy-button.tsx"
    }
  ],
  "meta": {
    "tags": [
      "clipboard",
      "copy",
      "success",
      "icon-swap"
    ],
    "effects": [
      "state-transition",
      "icon-swap"
    ],
    "cssOnly": true
  },
  "categories": [
    "button"
  ],
  "type": "registry:ui"
}