"Copy to clipboard" buttons are everywhere — copy a share link, an API key, a code snippet — and they all lean on the same browser API: navigator.clipboard.writeText, which is async and can reject (denied permission, insecure context, no clipboard at all). useCopyToClipboard wraps that into a clean pair: a copy(text) function you call on click, and the last-copied text you can use to flip a button to "Copied!".
Implement useCopyToClipboard(). It returns [copiedText, copy]. copiedText is the last successfully copied string (or null before any copy, and null again after a failure). copy(text) is async: it writes via the Clipboard API, resolves to true and stores the text on success, or false on failure or when the API is unavailable.
function useCopyToClipboard() {
// returns [copiedText, copy]
// copiedText: string | null
// copy: (text: string) => Promise<boolean>
}
const [copied, copy] = useCopyToClipboard();
<button onClick={() => copy('https://uiready.dev')}>
{copied ? 'Copied!' : 'Copy link'}
</button>
const [, copy] = useCopyToClipboard();
const ok = await copy(apiKey);
if (!ok) toast('Copy failed — select and press ⌘C');
writeText returns a promise that rejects on denied permission or an insecure (non-HTTPS) context. Wrap it in try/catch.navigator.clipboard is undefined in old or insecure contexts; return false instead of throwing.copiedText when the write resolves; clear it to null on failure so the UI never lies about what's on the clipboard.copy — wrap it in useCallback so it can be a dependency or a memoized prop.