{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "copy-button",
  "title": "Copy Button",
  "description": "A copy-button component.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "@cubby-ui/button",
    "@cubby-ui/toast"
  ],
  "files": [
    {
      "path": "registry/default/copy-button/copy-button.tsx",
      "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\nimport { Button } from \"@/registry/default/button/button\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useCopyToClipboard } from \"@/registry/default/copy-button/hooks/use-copy-to-clipboard\";\r\nimport {\r\n  toast as toastApi,\r\n  type AnchoredToastOptions,\r\n} from \"@/registry/default/toast/toast\";\r\nimport { HugeiconsIcon } from \"@hugeicons/react\";\r\nimport {\r\n  Cancel01Icon,\r\n  Copy01Icon,\r\n  Tick02Icon,\r\n} from \"@hugeicons/core-free-icons\";\r\n\r\ntype CopyButtonToastConfig = Omit<AnchoredToastOptions, \"anchor\">;\r\n\r\nconst DEFAULT_COPY_ICON = (\r\n  <HugeiconsIcon icon={Copy01Icon} strokeWidth={2} className=\"size-4\" />\r\n);\r\nconst DEFAULT_CHECK_ICON = (\r\n  <HugeiconsIcon\r\n    icon={Tick02Icon}\r\n    strokeWidth={2}\r\n    className=\"size-4 text-green-500\"\r\n  />\r\n);\r\nconst DEFAULT_ERROR_ICON = (\r\n  <HugeiconsIcon\r\n    icon={Cancel01Icon}\r\n    strokeWidth={2}\r\n    className=\"size-4 text-red-500\"\r\n  />\r\n);\r\n\r\ninterface CopyButtonProps extends Omit<\r\n  React.ComponentProps<typeof Button>,\r\n  \"onClick\" | \"children\" | \"size\" | \"variant\"\r\n> {\r\n  content: string;\r\n  timeout?: number;\r\n  copyIcon?: React.ReactNode;\r\n  checkIcon?: React.ReactNode;\r\n  errorIcon?: React.ReactNode;\r\n  onCopied?: (text: string) => void;\r\n  onCopyError?: (text: string) => void;\r\n  /**\r\n   * Show an anchored toast above the button on successful copy.\r\n   * Pass `true` for defaults, or an options object to customize the toast.\r\n   */\r\n  toast?: true | CopyButtonToastConfig;\r\n}\r\n\r\nfunction CopyButton({\r\n  content,\r\n  timeout = 2000,\r\n  className,\r\n  copyIcon,\r\n  checkIcon,\r\n  errorIcon,\r\n  onCopied,\r\n  onCopyError,\r\n  toast,\r\n  ref,\r\n  ...props\r\n}: CopyButtonProps) {\r\n  const internalRef = React.useRef<HTMLButtonElement>(null);\r\n  const toastEnabled = Boolean(toast);\r\n  const toastConfig: CopyButtonToastConfig = toast === true ? {} : (toast ?? {});\r\n\r\n  const { isCopied, isError, copyToClipboard, reset } = useCopyToClipboard({\r\n    // When an anchored toast is attached, the toast's lifecycle owns the\r\n    // reset via `onClose` — so disable the hook's internal auto-reset.\r\n    timeout: toastEnabled ? null : timeout,\r\n    onCopied: (text) => {\r\n      onCopied?.(text);\r\n      if (toastEnabled) {\r\n        toastApi.anchored({\r\n          description: \"Copied to clipboard!\",\r\n          side: \"top\",\r\n          sideOffset: 8,\r\n          arrow: true,\r\n          duration: timeout,\r\n          ...toastConfig,\r\n          anchor: internalRef,\r\n          onClose: () => {\r\n            reset();\r\n            toastConfig.onClose?.();\r\n          },\r\n        });\r\n      }\r\n    },\r\n    onCopyError: (text) => {\r\n      onCopyError?.(text);\r\n      if (toastEnabled) {\r\n        toastApi.anchored({\r\n          side: \"top\",\r\n          sideOffset: 8,\r\n          arrow: true,\r\n          duration: timeout,\r\n          ...toastConfig,\r\n          // The configurable description is the success message — the error\r\n          // toast always states the failure.\r\n          description: \"Failed to copy to clipboard\",\r\n          anchor: internalRef,\r\n          onClose: () => {\r\n            reset();\r\n            toastConfig.onClose?.();\r\n          },\r\n        });\r\n      }\r\n    },\r\n  });\r\n\r\n  const mergedRef = React.useCallback(\r\n    (node: HTMLButtonElement | null) => {\r\n      internalRef.current = node;\r\n      if (typeof ref === \"function\") ref(node);\r\n      else if (ref) ref.current = node;\r\n    },\r\n    [ref],\r\n  );\r\n\r\n  return (\r\n    <Button\r\n      ref={mergedRef}\r\n      data-slot=\"copy-button\"\r\n      size=\"icon_xs\"\r\n      variant=\"ghost\"\r\n      disabled={isCopied}\r\n      onClick={() => copyToClipboard(content)}\r\n      className={cn(\r\n        \"text-muted-foreground size-auto rounded-md p-1.5 [grid-template-areas:'stack'] [&>span]:grid [&>span]:place-content-center [&>span]:p-0\",\r\n        className,\r\n      )}\r\n      aria-label={\r\n        isCopied\r\n          ? \"Copied to clipboard\"\r\n          : isError\r\n            ? \"Copy failed\"\r\n            : \"Copy to clipboard\"\r\n      }\r\n      title={isCopied ? \"Copied!\" : isError ? \"Copy failed\" : \"Copy\"}\r\n      {...props}\r\n    >\r\n      <span\r\n        aria-hidden=\"true\"\r\n        className={cn(\r\n          \"ease flex items-center justify-center blur-none transition-[scale,opacity,filter] delay-0 duration-300 [grid-area:stack]\",\r\n          (isCopied || isError) && \"scale-50 opacity-0 blur-xs delay-0\",\r\n        )}\r\n      >\r\n        {copyIcon ?? DEFAULT_COPY_ICON}\r\n      </span>\r\n\r\n      <span\r\n        aria-hidden=\"true\"\r\n        className={cn(\r\n          \"ease flex scale-50 items-center justify-center opacity-0 blur-xs transition-[scale,opacity,filter] delay-0 duration-300 [grid-area:stack]\",\r\n          isCopied && \"scale-100 opacity-100 blur-none delay-0\",\r\n        )}\r\n      >\r\n        {checkIcon ?? DEFAULT_CHECK_ICON}\r\n      </span>\r\n\r\n      <span\r\n        aria-hidden=\"true\"\r\n        className={cn(\r\n          \"ease flex scale-50 items-center justify-center opacity-0 blur-xs transition-[scale,opacity,filter] delay-0 duration-300 [grid-area:stack]\",\r\n          isError && \"scale-100 opacity-100 blur-none delay-0\",\r\n        )}\r\n      >\r\n        {errorIcon ?? DEFAULT_ERROR_ICON}\r\n      </span>\r\n    </Button>\r\n  );\r\n}\r\n\r\nexport { CopyButton };\r\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/copy-button/copy-button.tsx"
    },
    {
      "path": "registry/default/copy-button/hooks/use-copy-to-clipboard.ts",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useEffect, useState } from \"react\";\r\n\r\nasync function writeToClipboard(text: string): Promise<boolean> {\r\n  try {\r\n    await navigator.clipboard.writeText(text);\r\n    return true;\r\n  } catch {\r\n    try {\r\n      const textarea = document.createElement(\"textarea\");\r\n      textarea.value = text;\r\n      textarea.style.position = \"fixed\";\r\n      textarea.style.opacity = \"0\";\r\n      document.body.appendChild(textarea);\r\n      textarea.select();\r\n\r\n      const success = document.execCommand(\"copy\");\r\n      document.body.removeChild(textarea);\r\n\r\n      return success;\r\n    } catch {\r\n      return false;\r\n    }\r\n  }\r\n}\r\n\r\nexport interface UseCopyToClipboardOptions {\r\n  /** ms before `isCopied`/`isError` auto-resets. Pass `null` to disable (e.g. when another mechanism owns the lifecycle). */\r\n  timeout?: number | null;\r\n  onCopied?: (text: string) => void;\r\n  onCopyError?: (text: string) => void;\r\n}\r\n\r\nexport function useCopyToClipboard({\r\n  timeout = 2000,\r\n  onCopied,\r\n  onCopyError,\r\n}: UseCopyToClipboardOptions = {}) {\r\n  const [status, setStatus] = useState<\"idle\" | \"copied\" | \"error\">(\"idle\");\r\n\r\n  useEffect(() => {\r\n    if (status !== \"idle\" && timeout != null) {\r\n      const timer = setTimeout(() => setStatus(\"idle\"), timeout);\r\n      return () => clearTimeout(timer);\r\n    }\r\n  }, [status, timeout]);\r\n\r\n  const copyToClipboard = useCallback(\r\n    async (text: string): Promise<boolean> => {\r\n      const success = await writeToClipboard(text);\r\n      if (success) {\r\n        setStatus(\"copied\");\r\n        onCopied?.(text);\r\n      } else {\r\n        setStatus(\"error\");\r\n        onCopyError?.(text);\r\n      }\r\n      return success;\r\n    },\r\n    [onCopied, onCopyError],\r\n  );\r\n\r\n  const reset = useCallback(() => setStatus(\"idle\"), []);\r\n\r\n  return {\r\n    isCopied: status === \"copied\",\r\n    isError: status === \"error\",\r\n    copyToClipboard,\r\n    reset,\r\n  };\r\n}\r\n",
      "type": "registry:hook",
      "target": "components/ui/cubby-ui/copy-button/hooks/use-copy-to-clipboard.ts"
    }
  ],
  "type": "registry:ui"
}