{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "title": "Copy Button",
  "author": "ncdai <dai@chanhdai.com>",
  "description": "Copy text to clipboard with visual, haptic, and audio feedback.",
  "dependencies": [
    "motion",
    "@rexa-developer/tiks",
    "web-haptics"
  ],
  "registryDependencies": [
    "button",
    "https://chanhdai.com/r/icon-swap.json"
  ],
  "files": [
    {
      "path": "src/registry/components/copy-button/copy-button.tsx",
      "content": "\"use client\"\n\nimport type { ComponentProps } from \"react\"\nimport { motion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { CopyState } from \"@/hooks/use-copy-to-clipboard\"\nimport { useCopyToClipboard } from \"@/hooks/use-copy-to-clipboard\"\nimport { Button } from \"@/components/ui/button\"\nimport { IconSwap, IconSwapItem } from \"@/registry/components/icon-swap\"\nimport { IconPlaceholder } from \"@/registry/icons/icon-placeholder\"\n\nexport type CopyStateIconProps = {\n  state: CopyState\n  /** Custom icon for idle state. */\n  idleIcon?: React.ReactNode\n  /** Custom icon for done state. */\n  doneIcon?: React.ReactNode\n  /** Custom icon for error state. */\n  errorIcon?: React.ReactNode\n}\n\nexport function CopyStateIcon({\n  state,\n  idleIcon,\n  doneIcon,\n  errorIcon,\n}: CopyStateIconProps) {\n  return (\n    <IconSwap>\n      <IconSwapItem key={state} as={motion.span}>\n        {state === \"idle\" &&\n          (idleIcon ?? (\n            <IconPlaceholder\n              data-slot=\"idle-icon\"\n              lucide=\"CopyIcon\"\n              tabler=\"IconCopy\"\n              hugeicons=\"Copy01Icon\"\n              phosphor=\"CopyIcon\"\n              remixicon=\"RiFileCopyLine\"\n            />\n          ))}\n\n        {state === \"done\" &&\n          (doneIcon ?? (\n            <IconPlaceholder\n              data-slot=\"done-icon\"\n              lucide=\"CheckIcon\"\n              tabler=\"IconCheck\"\n              hugeicons=\"Tick02Icon\"\n              phosphor=\"CheckIcon\"\n              remixicon=\"RiCheckLine\"\n            />\n          ))}\n\n        {state === \"error\" &&\n          (errorIcon ?? (\n            <IconPlaceholder\n              data-slot=\"error-icon\"\n              lucide=\"CircleXIcon\"\n              tabler=\"IconX\"\n              hugeicons=\"CancelCircleIcon\"\n              phosphor=\"XCircleIcon\"\n              remixicon=\"RiCloseCircleLine\"\n            />\n          ))}\n      </IconSwapItem>\n    </IconSwap>\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} & Omit<CopyStateIconProps, \"state\">\n\nexport function CopyButton({\n  className,\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      className={cn(\"will-change-transform\", className)}\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",
      "target": "@components/copy-button.tsx"
    },
    {
      "path": "src/hooks/use-copy-to-clipboard.ts",
      "content": "\"use client\"\n\nimport { useCallback, useRef, useState } from \"react\"\nimport { useTiks } from \"@rexa-developer/tiks/react\"\nimport { useWebHaptics } from \"web-haptics/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 { trigger: haptic } = useWebHaptics()\n  const { success: tiksSuccess, error: tiksError } = useTiks()\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\n        setState(\"done\")\n\n        haptic(\"success\")\n        tiksSuccess()\n\n        onCopySuccess?.(finalText)\n      } catch (error) {\n        setState(\"error\")\n\n        haptic(\"error\")\n        tiksError()\n\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    [onCopySuccess, onCopyError, haptic, tiksSuccess, tiksError, resetDelay]\n  )\n\n  return { state, copy } as const\n}\n",
      "type": "registry:hook",
      "target": "@hooks/use-copy-to-clipboard.ts"
    }
  ],
  "docs": "https://chanhdai.com/components/copy-button",
  "categories": [
    "utilities"
  ],
  "type": "registry:component"
}