{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "title": "Copy Button",
  "author": "nghia.ly <physicalmeans@gmail.com>",
  "description": "Copy text to clipboard with visual feedback and animation.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "src/registry/components/copy-button/copy-button.tsx",
      "content": "\"use client\"\n\nimport { CheckIcon, CircleXIcon, CopyIcon } from \"lucide-react\"\nimport type { HTMLMotionProps, Variants } from \"motion/react\"\nimport { AnimatePresence, motion } from \"motion/react\"\nimport type { ComponentProps } from \"react\"\n\nimport { Button } from \"@/components/ui/button\"\nimport type { CopyState } from \"@/hooks/use-copy-to-clipboard\"\nimport { useCopyToClipboard } from \"@/hooks/use-copy-to-clipboard\"\n\nexport const motionIconVariants: Variants = {\n  initial: { opacity: 0, scale: 0.8, filter: \"blur(2px)\" },\n  animate: { opacity: 1, scale: 1, filter: \"blur(0px)\" },\n  exit: { opacity: 0, scale: 0.8 },\n}\n\nexport const motionIconProps: HTMLMotionProps<\"span\"> = {\n  variants: motionIconVariants,\n  initial: \"initial\",\n  animate: \"animate\",\n  exit: \"exit\",\n  transition: { duration: 0.15, ease: \"easeOut\" },\n}\n\nexport type CopyStateIconProps = {\n  state: CopyState\n  /**\n   * Custom icon for idle state.\n   * @defaultValue CopyIcon\n   * */\n  idleIcon?: React.ReactNode\n  /**\n   * Custom icon for done state.\n   * @defaultValue CheckIcon\n   * */\n  doneIcon?: React.ReactNode\n  /**\n   * Custom icon for error state.\n   * @defaultValue CircleXIcon\n   * */\n  errorIcon?: React.ReactNode\n}\n\nexport function CopyStateIcon({\n  state,\n  idleIcon,\n  doneIcon,\n  errorIcon,\n}: CopyStateIconProps) {\n  return (\n    <AnimatePresence mode=\"popLayout\" initial={false}>\n      {state === \"idle\" ? (\n        <motion.span key=\"idle\" {...motionIconProps}>\n          {idleIcon ?? <CopyIcon />}\n        </motion.span>\n      ) : state === \"done\" ? (\n        <motion.span key=\"done\" {...motionIconProps}>\n          {doneIcon ?? <CheckIcon strokeWidth={3} />}\n        </motion.span>\n      ) : state === \"error\" ? (\n        <motion.span key=\"error\" {...motionIconProps}>\n          {errorIcon ?? <CircleXIcon />}\n        </motion.span>\n      ) : null}\n    </AnimatePresence>\n  )\n}\n\nexport type CopyButtonProps = ComponentProps<typeof Button> & {\n  /** The text to copy, or a function that returns the text. */\n  text: string | (() => string)\n  /** Called with the copied text on successful copy. */\n  onCopySuccess?: (text: string) => void\n  /** Called with the error if the copy operation fails. */\n  onCopyError?: (error: Error) => void\n} & Pick<CopyStateIconProps, \"idleIcon\" | \"doneIcon\" | \"errorIcon\">\n\nexport function CopyButton({\n  size = \"icon\",\n  children,\n  text,\n  idleIcon,\n  doneIcon,\n  errorIcon,\n  onClick,\n  onCopySuccess,\n  onCopyError,\n  ...props\n}: CopyButtonProps) {\n  const { state, copy } = useCopyToClipboard({\n    onCopySuccess,\n    onCopyError,\n  })\n\n  return (\n    <Button\n      size={size}\n      onClick={(e) => {\n        copy(text)\n        onClick?.(e)\n      }}\n      aria-label=\"Copy\"\n      {...props}\n    >\n      <CopyStateIcon\n        state={state}\n        idleIcon={idleIcon}\n        doneIcon={doneIcon}\n        errorIcon={errorIcon}\n      />\n      {children}\n    </Button>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "src/hooks/use-copy-to-clipboard.ts",
      "content": "\"use client\"\n\nimport { useCallback, useRef, useState } from \"react\"\n\nexport type CopyState = \"idle\" | \"done\" | \"error\"\n\nexport type UseCopyToClipboardOptions = {\n  onCopySuccess?: (text: string) => void\n  onCopyError?: (error: Error) => void\n  resetDelay?: number\n}\n\nexport function useCopyToClipboard({\n  onCopySuccess,\n  onCopyError,\n  resetDelay = 1500,\n}: UseCopyToClipboardOptions = {}) {\n  const [state, setState] = useState<CopyState>(\"idle\")\n  const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n  const copy = useCallback(\n    async (text: string | (() => string)) => {\n      // Clear any pending reset\n      if (resetTimeoutRef.current) {\n        clearTimeout(resetTimeoutRef.current)\n      }\n\n      try {\n        const finalText = typeof text === \"function\" ? text() : text\n        await navigator.clipboard.writeText(finalText)\n        setState(\"done\")\n        onCopySuccess?.(finalText)\n      } catch (error) {\n        setState(\"error\")\n        onCopyError?.(error instanceof Error ? error : new Error(\"Copy failed\"))\n      } finally {\n        // Schedule reset to idle\n        resetTimeoutRef.current = setTimeout(() => {\n          setState(\"idle\")\n        }, resetDelay)\n      }\n    },\n    [onCopyError, onCopySuccess, resetDelay]\n  )\n\n  return { state, copy } as const\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:component"
}