"use client"; import { Check, Copy } from "lucide-react"; import { type ButtonHTMLAttributes, forwardRef, useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; type CopyState = "idle" | "copied" | "failed"; export interface CopyButtonProps extends Omit, "children" | "value" | "onCopy"> { /** Text written to the clipboard. */ value: string; /** Visible text. Leave it out for an icon only button. */ label?: string; /** Visible text after copying. Default "Copied". */ copiedLabel?: string; /** How long the copied state stays, in ms. Default 1500. */ timeout?: number; /** Called after the text reaches the clipboard. */ onCopied?: (value: string) => void; } // Both icons, and both labels, share a grid cell and trade places with a fade and a small // scale. Sharing the cell keeps the button the same width in either state. const SWAP = "col-start-1 row-start-1 transition-[opacity,scale] duration-200 ease-out motion-reduce:transition-none"; const SHOWN = "scale-100 opacity-100"; const ICON_HIDDEN = "scale-50 opacity-0"; const TEXT_HIDDEN = "scale-[0.97] opacity-0"; export const CopyButton = forwardRef(function CopyButton( { value, label, copiedLabel = "Copied", timeout = 1500, onCopied, className, onClick, ...props }, ref, ) { const [state, setState] = useState("idle"); const timer = useRef | null>(null); useEffect( () => () => { if (timer.current) clearTimeout(timer.current); }, [], ); const copy = async () => { try { await navigator.clipboard.writeText(value); setState("copied"); onCopied?.(value); } catch { // Clipboard access can be blocked, for example in an insecure context. setState("failed"); } if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => setState("idle"), timeout); }; const copied = state === "copied"; const status = copied ? "Copied to clipboard" : state === "failed" ? "Could not copy" : ""; return ( ); });