{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "clipboard-field",
  "title": "Clipboard Field",
  "description": "A one-click install/copy field that morphs between the command and a success label using CSS grid fr tracks — JS only copies and toggles state.",
  "dependencies": [],
  "files": [
    {
      "path": "components/ui/clipboard-field.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type ButtonHTMLAttributes,\n} from 'react'\nimport { cn } from '@/lib/utils'\nimport { playHoverSound, playClickSound } from '@/lib/sound'\n\nconst KEYFRAME_STYLE_ID = '__clipboard_field_kf__'\n\nfunction ensureKeyframes() {\n  if (typeof document === 'undefined') return\n  if (document.getElementById(KEYFRAME_STYLE_ID)) return\n  const style = document.createElement('style')\n  style.id = KEYFRAME_STYLE_ID\n  style.textContent = `\n    @keyframes cf-fade-out {\n      from { opacity: 1; transform: scale(1); }\n      to   { opacity: 0; transform: scale(0.97); }\n    }\n    @keyframes cf-fade-in {\n      from { opacity: 0; transform: scale(0.97); }\n      to   { opacity: 1; transform: scale(1); }\n    }\n    @keyframes cf-icon-exit {\n      from { opacity: 1; transform: scale(1) rotate(0deg); }\n      to   { opacity: 0; transform: scale(0.7) rotate(-8deg); }\n    }\n    @keyframes cf-icon-enter {\n      from { opacity: 0; transform: scale(0.7) rotate(8deg); }\n      to   { opacity: 1; transform: scale(1) rotate(0deg); }\n    }\n  `\n  document.head.appendChild(style)\n}\n\nexport interface ClipboardFieldProps extends Omit<\n  ButtonHTMLAttributes<HTMLButtonElement>,\n  'children' | 'onCopy' | 'value'\n> {\n  value: string\n  prompt?: string\n  copiedLabel?: string\n  copyLabel?: string\n  resetDelay?: number\n  onCopy?: () => void\n  hideIcon?: boolean\n}\n\nfunction CopyIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      width={14}\n      height={14}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.75}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n      className={className}\n    >\n      <rect x=\"9\" y=\"9\" width=\"12\" height=\"12\" rx=\"2\" />\n      <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\" />\n    </svg>\n  )\n}\n\nfunction CheckIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      width={14}\n      height={14}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n      className={className}\n    >\n      <path d=\"M20 6 9 17l-5-5\" />\n    </svg>\n  )\n}\n\nexport const ClipboardField = forwardRef<HTMLButtonElement, ClipboardFieldProps>(\n  (\n    {\n      value,\n      prompt = '$',\n      copiedLabel = 'Copied to clipboard',\n      copyLabel = 'Copy to clipboard',\n      resetDelay = 2000,\n      onCopy,\n      hideIcon = false,\n      disabled,\n      className,\n      onClick,\n      type = 'button',\n      ...props\n    },\n    ref,\n  ) => {\n    const [copied, setCopied] = useState(false)\n    const resetTimer = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n    useEffect(() => {\n      ensureKeyframes()\n    }, [])\n\n    useEffect(\n      () => () => {\n        if (resetTimer.current) clearTimeout(resetTimer.current)\n      },\n      [],\n    )\n\n    const handleCopy = useCallback(async () => {\n      if (disabled) return\n      playClickSound()\n      try {\n        await navigator.clipboard.writeText(value)\n        setCopied(true)\n        onCopy?.()\n        if (resetTimer.current) clearTimeout(resetTimer.current)\n        resetTimer.current = setTimeout(() => setCopied(false), resetDelay)\n      } catch {}\n    }, [disabled, value, onCopy, resetDelay])\n\n    return (\n      <button\n        ref={ref}\n        type={type}\n        disabled={disabled}\n        data-copied={copied ? '' : undefined}\n        aria-label={copied ? copiedLabel : copyLabel}\n        onClick={(e) => {\n          onClick?.(e)\n          if (!e.defaultPrevented) void handleCopy()\n        }}\n        onMouseEnter={() => playHoverSound()}\n        onPointerDown={(e) => {\n          e.preventDefault()\n        }}\n        className={cn(\n          'clipboard-field group relative inline-grid max-w-full cursor-pointer items-center gap-x-2',\n          'rounded-xl squircle-corners border border-(--color-border) bg-(--color-surface-2)',\n          'px-3 py-2.5 font-mono text-[13px] leading-none text-(--color-fg)',\n          'outline-none select-none',\n          hideIcon ? 'grid-cols-[auto_1fr]' : 'grid-cols-[auto_1fr_auto]',\n          'transition-[border-color,background-color] duration-(--motion-dur-fast) ease-(--motion-ease-in-out) motion-reduce:transition-none',\n          'hover:border-(--color-border-strong) hover:bg-(--color-surface)',\n          'focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 focus-visible:ring-offset-(--color-bg)',\n          'active:scale-[0.98] motion-reduce:active:scale-100',\n          'disabled:pointer-events-none disabled:opacity-50',\n          className,\n        )}\n        {...props}\n      >\n        <span\n          aria-hidden=\"true\"\n          className=\"col-start-1 row-start-1 text-(--color-muted) tabular-nums\"\n        >\n          {prompt}\n        </span>\n\n        {/* Both layers share col 2 — crossfade only, button width never changes. */}\n        <span\n          data-slot=\"command\"\n          style={{\n            willChange: 'opacity, transform',\n            animation: copied\n              ? 'cf-fade-out 0.18s var(--motion-ease-in-out) forwards'\n              : 'cf-fade-in 0.14s var(--motion-ease-out) forwards',\n          }}\n          className={cn(\n            'col-start-2 row-start-1 min-w-0 overflow-hidden whitespace-nowrap text-start text-(--color-muted)',\n            'pointer-events-none',\n            'motion-reduce:[animation:none]',\n          )}\n        >\n          {value}\n        </span>\n\n        <span\n          data-slot=\"copied\"\n          aria-hidden={!copied}\n          style={{\n            willChange: 'opacity, transform',\n            animation: copied\n              ? 'cf-fade-in 0.18s 40ms var(--motion-ease-out) both'\n              : 'cf-fade-out 0.1s var(--motion-ease-in-out) both',\n          }}\n          className={cn(\n            'col-start-2 row-start-1 min-w-0 overflow-hidden whitespace-nowrap text-start text-(--color-fg)',\n            'motion-reduce:[animation:none] motion-reduce:opacity-0',\n          )}\n        >\n          {copiedLabel}\n        </span>\n\n        {!hideIcon && (\n          <span\n            aria-hidden=\"true\"\n            className=\"relative col-start-3 row-start-1 size-4 shrink-0 text-(--color-muted) transition-colors duration-(--motion-dur-fast) group-hover:text-(--color-fg) group-data-[copied]:text-(--color-fg)\"\n          >\n            <span\n              style={{\n                willChange: 'opacity, transform',\n                animation: copied\n                  ? 'cf-icon-exit 0.08s var(--motion-ease-in-out) forwards'\n                  : 'cf-icon-enter 0.12s var(--motion-ease-out) forwards',\n              }}\n              className=\"absolute inset-0 flex items-center justify-center motion-reduce:[animation:none]\"\n            >\n              <CopyIcon />\n            </span>\n            <span\n              style={{\n                willChange: 'opacity, transform',\n                animation: copied\n                  ? 'cf-icon-enter 0.12s 40ms var(--motion-ease-out) both'\n                  : 'cf-icon-exit 0.06s var(--motion-ease-in-out) both',\n              }}\n              className=\"absolute inset-0 flex items-center justify-center motion-reduce:[animation:none] motion-reduce:opacity-0\"\n            >\n              <CheckIcon />\n            </span>\n          </span>\n        )}\n      </button>\n    )\n  },\n)\n\nClipboardField.displayName = 'ClipboardField'\n\nexport function ClipboardFieldPreview() {\n  return (\n    <div className=\"flex w-full items-center justify-center p-6\">\n      <ClipboardField value=\"npx shadcn@latest add @nexvyn/badge\" className=\"w-full max-w-md\" />\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/sound.ts",
      "type": "registry:lib",
      "content": "'use client'\n\nconst MUTE_KEY = 'sound-muted'\nconst MUTE_EVENT = 'sound-mute-change'\n\nlet muted = false\nif (typeof window !== 'undefined') {\n  try {\n    muted = localStorage.getItem(MUTE_KEY) === '1'\n  } catch {}\n}\n\nexport function isSoundMuted(): boolean {\n  return muted\n}\n\nexport function setSoundMuted(value: boolean) {\n  muted = value\n  if (typeof window !== 'undefined') {\n    try {\n      localStorage.setItem(MUTE_KEY, value ? '1' : '0')\n    } catch {}\n    window.dispatchEvent(new CustomEvent(MUTE_EVENT))\n  }\n}\n\nexport function toggleSoundMuted(): boolean {\n  setSoundMuted(!muted)\n  return muted\n}\n\nexport function subscribeSoundMuted(callback: () => void) {\n  if (typeof window === 'undefined') return () => {}\n  window.addEventListener(MUTE_EVENT, callback)\n  const onStorage = (e: StorageEvent) => {\n    if (e.key === MUTE_KEY) callback()\n  }\n  window.addEventListener('storage', onStorage)\n  return () => {\n    window.removeEventListener(MUTE_EVENT, callback)\n    window.removeEventListener('storage', onStorage)\n  }\n}\n\nlet audioCtx: AudioContext | null = null\n\nfunction initAudio(): AudioContext | null {\n  if (typeof window === 'undefined') return null\n  if (!audioCtx) {\n    const AudioContextClass =\n      window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext\n    if (AudioContextClass) {\n      audioCtx = new AudioContextClass()\n    }\n  }\n  if (audioCtx && audioCtx.state === 'suspended') {\n    audioCtx.resume().catch(() => {})\n  }\n  return audioCtx\n}\n\nif (typeof window !== 'undefined') {\n  const unlock = () => {\n    initAudio()\n    window.removeEventListener('pointerdown', unlock)\n    window.removeEventListener('keydown', unlock)\n    window.removeEventListener('touchstart', unlock)\n  }\n  window.addEventListener('pointerdown', unlock, { passive: true })\n  window.addEventListener('keydown', unlock, { passive: true })\n  window.addEventListener('touchstart', unlock, { passive: true })\n}\n\nexport function getAudioContext(): AudioContext | null {\n  return initAudio()\n}\n\nexport function playHoverSound(volume = 0.12, pitch = 1.2) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1400 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(350 * pitch, now + 0.012)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.012)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.012)\n  } catch {}\n}\n\nexport function playClickSound(volume = 0.2, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'triangle'\n    osc.frequency.setValueAtTime(850 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(160 * pitch, now + 0.02)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.02)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.02)\n  } catch {}\n}\n\nexport function playTickSound(volume = 0.1, pitch = 1.5) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(1800 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(450 * pitch, now + 0.01)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.01)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.01)\n  } catch {}\n}\n\nexport function playBounceSound(volume = 0.3, pitch = 1.0) {\n  if (muted) return\n  try {\n    const ctx = getAudioContext()\n    if (!ctx) return\n    const now = ctx.currentTime\n\n    const osc = ctx.createOscillator()\n    const gain = ctx.createGain()\n\n    osc.type = 'sine'\n    osc.frequency.setValueAtTime(260 * pitch, now)\n    osc.frequency.exponentialRampToValueAtTime(650 * pitch, now + 0.04)\n    osc.frequency.exponentialRampToValueAtTime(190 * pitch, now + 0.14)\n\n    gain.gain.setValueAtTime(volume, now)\n    gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.14)\n\n    osc.connect(gain)\n    gain.connect(ctx.destination)\n\n    osc.start(now)\n    osc.stop(now + 0.14)\n\n    const subOsc = ctx.createOscillator()\n    const subGain = ctx.createGain()\n\n    subOsc.type = 'triangle'\n    subOsc.frequency.setValueAtTime(420 * pitch, now)\n    subOsc.frequency.exponentialRampToValueAtTime(120 * pitch, now + 0.05)\n\n    subGain.gain.setValueAtTime(volume * 0.7, now)\n    subGain.gain.exponentialRampToValueAtTime(0.0001, now + 0.05)\n\n    subOsc.connect(subGain)\n    subGain.connect(ctx.destination)\n\n    subOsc.start(now)\n    subOsc.stop(now + 0.05)\n  } catch {}\n}\n"
    }
  ],
  "type": "registry:ui"
}
