import * as React from "react"; import * as Schema from "effect/Schema"; export class ClipboardApiUnavailableError extends Schema.TaggedErrorClass()( "ClipboardApiUnavailableError", { target: Schema.String, }, ) { override get message(): string { return `Clipboard API is unavailable while copying ${this.target}.`; } } export class ClipboardWriteError extends Schema.TaggedErrorClass()( "ClipboardWriteError", { target: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to copy ${this.target} to the clipboard.`; } } export async function writeTextToClipboard(value: string, target = "text") { if ( typeof window === "undefined" || typeof navigator === "undefined" || !navigator.clipboard?.writeText ) { throw new ClipboardApiUnavailableError({ target, }); } if (!value) return false; try { await navigator.clipboard.writeText(value); return true; } catch (cause) { throw new ClipboardWriteError({ target, cause, }); } } export function useCopyToClipboard({ timeout = 2000, target = "text", onCopy, onError, }: { timeout?: number; target?: string; onCopy?: (ctx: TContext) => void; onError?: (error: Error, ctx: TContext) => void; } = {}): { copyToClipboard: (value: string, ctx: TContext) => void; isCopied: boolean } { const [isCopied, setIsCopied] = React.useState(false); const timeoutIdRef = React.useRef(null); const onCopyRef = React.useRef(onCopy); const onErrorRef = React.useRef(onError); const targetRef = React.useRef(target); const timeoutRef = React.useRef(timeout); onCopyRef.current = onCopy; onErrorRef.current = onError; targetRef.current = target; timeoutRef.current = timeout; const copyToClipboard = React.useCallback((value: string, ctx: TContext): void => { void writeTextToClipboard(value, targetRef.current).then( (didCopy) => { if (!didCopy) return; if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); } setIsCopied(true); onCopyRef.current?.(ctx); if (timeoutRef.current !== 0) { timeoutIdRef.current = setTimeout(() => { setIsCopied(false); timeoutIdRef.current = null; }, timeoutRef.current); } }, (error) => { console.error(error); onErrorRef.current?.(error, ctx); }, ); }, []); // Cleanup timeout on unmount React.useEffect(() => { return (): void => { if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); } }; }, []); return { copyToClipboard, isCopied }; }