import * as React from 'react' import { Button } from '@/components/ui/button' import { DropdownMenuItem } from '@/components/ui/dropdown-menu' import { triggerHaptic } from '@/lib/haptics' import { Check, Copy, X } from '@/lib/icons' import { cn } from '@/lib/utils' type CopyPayload = string | (() => Promise | string) type CopyButtonAppearance = 'button' | 'icon' | 'inline' | 'menu-item' | 'tool-row' type CopyStatus = 'copied' | 'error' | 'idle' const COPIED_RESET_MS = 1_500 export async function writeClipboardText(text: string) { if (!text) { return } if (window.hermesDesktop?.writeClipboard) { await window.hermesDesktop.writeClipboard(text) return } if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text) return } throw new Error('Clipboard API is unavailable') } export interface CopyButtonProps { appearance?: CopyButtonAppearance buttonSize?: React.ComponentProps['size'] buttonVariant?: React.ComponentProps['variant'] children?: React.ReactNode className?: string disabled?: boolean errorMessage?: string haptic?: boolean iconClassName?: string label?: string onCopied?: () => void onCopyError?: (error: unknown) => void preventDefault?: boolean showLabel?: boolean stopPropagation?: boolean text: CopyPayload title?: string } export function CopyButton({ appearance = 'button', buttonSize, buttonVariant = 'ghost', children, className, disabled = false, errorMessage = 'Copy failed', haptic = true, iconClassName, label = 'Copy', onCopied, onCopyError, preventDefault = false, showLabel, stopPropagation = false, text, title }: CopyButtonProps) { const [status, setStatus] = React.useState('idle') const resetRef = React.useRef(null) React.useEffect(() => { return () => { if (resetRef.current !== null) { window.clearTimeout(resetRef.current) } } }, []) const copy = React.useCallback( async (event?: Event | React.MouseEvent) => { if (preventDefault) { event?.preventDefault() } if (stopPropagation) { event?.stopPropagation() } try { const value = typeof text === 'function' ? await text() : text if (!value) { return } await writeClipboardText(value) if (haptic) { triggerHaptic('selection') } if (resetRef.current !== null) { window.clearTimeout(resetRef.current) } setStatus('copied') resetRef.current = window.setTimeout(() => { setStatus('idle') resetRef.current = null }, COPIED_RESET_MS) onCopied?.() } catch (error) { onCopyError?.(error) if (resetRef.current !== null) { window.clearTimeout(resetRef.current) } setStatus('error') resetRef.current = window.setTimeout(() => { setStatus('idle') resetRef.current = null }, COPIED_RESET_MS) } }, [haptic, onCopied, onCopyError, preventDefault, stopPropagation, text] ) const Icon = status === 'copied' ? Check : status === 'error' ? X : Copy const icon = const visibleChildren = (showLabel ?? (appearance !== 'icon' && appearance !== 'tool-row')) ? status === 'copied' ? 'Copied' : status === 'error' ? 'Failed' : (children ?? label) : null const content = ( <> {icon} {visibleChildren} ) const feedbackLabel = status === 'copied' ? 'Copied' : status === 'error' ? errorMessage : (title ?? label) const ariaLabel = status === 'idle' ? label : feedbackLabel if (appearance === 'menu-item') { return ( { event.preventDefault() void copy(event) }} > {content} ) } if (appearance === 'inline') { return ( ) } if (appearance === 'tool-row') { return ( ) } return ( ) }