copy-field.tsx54 lines · main
1'use client';
2
3import { useRef, useState } from 'react';
4
5import { CopyIcon } from './animated-icons';
6
7interface Props {
8 value: string;
9 label?: string;
10}
11
12/**
13 * Read-only text input paired with a copy-to-clipboard icon button. On
14 * click the icon morphs into a checkmark for ~1.5s. Selection of the
15 * input contents on focus makes the value easy to grab manually too.
16 */
17export function CopyField({ value, label }: Props) {
18 const [copied, setCopied] = useState(false);
19 const inputRef = useRef<HTMLInputElement>(null);
20
21 async function copy() {
22 try {
23 await navigator.clipboard.writeText(value);
24 } catch {
25 // Fallback: select and prompt execCommand — rarely needed in 2026 browsers.
26 inputRef.current?.select();
27 document.execCommand?.('copy');
28 }
29 setCopied(true);
30 setTimeout(() => setCopied(false), 1500);
31 }
32
33 return (
34 <div className="flex w-full items-center gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] p-1 pr-1">
35 <input
36 ref={inputRef}
37 type="text"
38 readOnly
39 value={value}
40 aria-label={label ?? 'copyable value'}
41 onFocus={(e) => e.currentTarget.select()}
42 className="flex-1 bg-transparent px-2 py-1.5 font-mono text-xs text-[var(--color-text)] outline-none"
43 />
44 <button
45 type="button"
46 onClick={copy}
47 aria-label={copied ? 'copied' : 'copy to clipboard'}
48 className="flex size-8 shrink-0 items-center justify-center rounded border border-[var(--color-border-subtle)] text-[var(--color-text-muted)] transition-colors hover:border-[var(--color-border)] hover:text-[var(--color-primary)]"
49 >
50 <CopyIcon className="size-4" copied={copied} />
51 </button>
52 </div>
53 );
54}