{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"copy-button","type":"registry:component","title":"Copy Button","description":"Copies text to the clipboard and trades its icon for a check for a moment, with a label or as a single icon.","author":"Ryan","dependencies":["clsx","lucide-react","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/copy-button.tsx","type":"registry:component","target":"@components/motion/copy-button.tsx","content":"\"use client\";\n// easeui.dev/components/motion/copy-button\n\nimport { Check, Copy } from \"lucide-react\";\nimport { type ButtonHTMLAttributes, forwardRef, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype CopyState = \"idle\" | \"copied\" | \"failed\";\n\nexport interface CopyButtonProps\n  extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"children\" | \"value\" | \"onCopy\"> {\n  /** Text written to the clipboard. */\n  value: string;\n  /** Visible text. Leave it out for an icon only button. */\n  label?: string;\n  /** Visible text after copying. Default \"Copied\". */\n  copiedLabel?: string;\n  /** How long the copied state stays, in ms. Default 1500. */\n  timeout?: number;\n  /** Called after the text reaches the clipboard. */\n  onCopied?: (value: string) => void;\n}\n\n// Both icons, and both labels, share a grid cell and trade places with a fade and a small\n// scale. Sharing the cell keeps the button the same width in either state.\nconst SWAP =\n  \"col-start-1 row-start-1 transition-[opacity,scale] duration-200 ease-out motion-reduce:transition-none\";\nconst SHOWN = \"scale-100 opacity-100\";\nconst ICON_HIDDEN = \"scale-50 opacity-0\";\nconst TEXT_HIDDEN = \"scale-[0.97] opacity-0\";\n\nexport const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(function CopyButton(\n  { value, label, copiedLabel = \"Copied\", timeout = 1500, onCopied, className, onClick, ...props },\n  ref,\n) {\n  const [state, setState] = useState<CopyState>(\"idle\");\n  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(\n    () => () => {\n      if (timer.current) clearTimeout(timer.current);\n    },\n    [],\n  );\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(value);\n      setState(\"copied\");\n      onCopied?.(value);\n    } catch {\n      // Clipboard access can be blocked, for example in an insecure context.\n      setState(\"failed\");\n    }\n    if (timer.current) clearTimeout(timer.current);\n    timer.current = setTimeout(() => setState(\"idle\"), timeout);\n  };\n\n  const copied = state === \"copied\";\n  const status = copied ? \"Copied to clipboard\" : state === \"failed\" ? \"Could not copy\" : \"\";\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      aria-label={label ?? \"Copy\"}\n      data-state={state}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) void copy();\n      }}\n      className={cn(\n        \"relative inline-flex h-9 shrink-0 touch-manipulation select-none items-center justify-center gap-2 rounded-full bg-card text-sm font-medium text-foreground outline-none\",\n        \"shadow-[0_0_0_1px_var(--border)] transition-[background-color,scale] duration-150 ease-out hover:bg-muted active:scale-[0.97] motion-reduce:active:scale-100\",\n        \"focus-visible:ring-2 focus-visible:ring-foreground/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        label ? \"px-3.5\" : \"w-9 after:absolute after:-inset-1\",\n        className,\n      )}\n      {...props}\n    >\n      <span aria-hidden=\"true\" className=\"grid place-items-center\">\n        <Copy className={cn(SWAP, \"h-4 w-4\", copied ? ICON_HIDDEN : SHOWN)} />\n        <Check className={cn(SWAP, \"h-4 w-4\", copied ? SHOWN : ICON_HIDDEN)} />\n      </span>\n      {label ? (\n        <span aria-hidden=\"true\" className=\"grid whitespace-nowrap\">\n          <span className={cn(SWAP, copied ? TEXT_HIDDEN : SHOWN)}>{label}</span>\n          <span className={cn(SWAP, copied ? SHOWN : TEXT_HIDDEN)}>{copiedLabel}</span>\n        </span>\n      ) : null}\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {status}\n      </span>\n    </button>\n  );\n});\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}