import { useState, useCallback, useRef, useEffect } from "react"; export async function copyTextToClipboard(text: string): Promise { if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); return; } catch { // Fall through to the selection-based copy path. } } if (fallbackCopyText(text)) return; throw new Error("Clipboard unavailable"); } function fallbackCopyText(text: string): boolean { if (typeof document === "undefined" || !document.body) return false; const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", ""); textarea.style.position = "fixed"; textarea.style.top = "0"; textarea.style.left = "-9999px"; const selection = document.getSelection(); const ranges: Range[] = []; if (selection) { for (let index = 0; index < selection.rangeCount; index += 1) { ranges.push(selection.getRangeAt(index).cloneRange()); } } document.body.appendChild(textarea); textarea.select(); try { return document.execCommand("copy"); } catch { return false; } finally { textarea.remove(); if (selection) { selection.removeAllRanges(); for (const range of ranges) { selection.addRange(range); } } } } export function useCopyFeedback(timeout = 2000) { const [copiedId, setCopiedId] = useState(null); const timerRef = useRef>(undefined); useEffect(() => { return () => clearTimeout(timerRef.current); }, []); const copy = useCallback( (id: T, text: string) => { void copyTextToClipboard(text).catch(() => { // Keep the static surface usable when clipboard is unavailable. }); clearTimeout(timerRef.current); setCopiedId(id); timerRef.current = setTimeout(() => setCopiedId(null), timeout); }, [timeout], ); return { copiedId, copy }; }