{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input-copy",
  "title": "Input Copy",
  "description": "A copy-to-clipboard field that shows a monospace value with an icon or button trigger, animating a checkmark on copy.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/input-copy.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport { forwardRef, useCallback, useEffect, useRef, useState, type HTMLAttributes } from 'react'\nimport { AnimatePresence, motion, useReducedMotion } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { springs } from '@/lib/motion-tokens'\nimport { playHoverSound, playClickSound } from '@/lib/sound'\n\ntype InputCopyVariant = 'icon' | 'button'\ntype InputCopyAlign = 'right' | 'left'\n\nexport interface InputCopyProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n  value: string\n  label?: string\n  onCopy?: () => void\n  disabled?: boolean\n  variant?: InputCopyVariant\n  align?: InputCopyAlign\n  copyLabel?: string\n  copiedLabel?: string\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.5}\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=\"2 4 20 16\"\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=\"M6 12L10 16L18 8\" />\n    </svg>\n  )\n}\n\nexport const InputCopy = forwardRef<HTMLDivElement, InputCopyProps>(\n  (\n    {\n      value,\n      label,\n      onCopy,\n      disabled,\n      variant = 'icon',\n      align = 'right',\n      copyLabel = 'Copy to clipboard',\n      copiedLabel = 'Copied',\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const reduceMotion = useReducedMotion()\n    const [copied, setCopied] = useState(false)\n    const [copyCount, setCopyCount] = useState(0)\n    const [tooltipOpen, setTooltipOpen] = useState(false)\n    const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n    const handleCopy = useCallback(async () => {\n      if (disabled) return\n      playClickSound()\n      try {\n        await navigator.clipboard.writeText(value)\n        setCopied(true)\n        setCopyCount((c) => c + 1)\n        onCopy?.()\n        if (timeoutRef.current) clearTimeout(timeoutRef.current)\n        timeoutRef.current = setTimeout(() => setCopied(false), 2000)\n      } catch {}\n    }, [value, disabled, onCopy])\n\n    useEffect(() => {\n      return () => {\n        if (timeoutRef.current) clearTimeout(timeoutRef.current)\n      }\n    }, [])\n\n    const springTransition = reduceMotion ? { duration: 0 } : springs.press\n\n    const iconSwitch = (\n      <AnimatePresence mode=\"wait\" initial={false}>\n        {copied ? (\n          <motion.span\n            key={`check-${copyCount}`}\n            initial={{ opacity: 0, scale: 0.6 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.8 }}\n            transition={springTransition}\n            className=\"flex items-center justify-center\"\n          >\n            <CheckIcon />\n          </motion.span>\n        ) : (\n          <motion.span\n            key=\"copy\"\n            initial={{ opacity: 0, scale: 0.8 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.8 }}\n            transition={springTransition}\n            className=\"flex items-center justify-center\"\n          >\n            <CopyIcon />\n          </motion.span>\n        )}\n      </AnimatePresence>\n    )\n\n    const actionElement =\n      variant === 'button' ? (\n        <span className=\"shrink-0 flex items-center gap-1.5 px-1.5 py-2 text-[13px] font-normal text-muted-foreground transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none group-hover:text-foreground\">\n          <AnimatePresence mode=\"wait\" initial={false}>\n            {copied ? (\n              <motion.span\n                key={`check-label-${copyCount}`}\n                className=\"flex items-center gap-1.5\"\n                initial={{ opacity: 0, scale: 0.6 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                transition={springTransition}\n              >\n                <CheckIcon />\n                <span>{copiedLabel}</span>\n              </motion.span>\n            ) : (\n              <motion.span\n                key=\"copy-label\"\n                className=\"flex items-center gap-1.5\"\n                initial={{ opacity: 0, scale: 0.8 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                transition={springTransition}\n              >\n                <CopyIcon />\n                <span>Copy</span>\n              </motion.span>\n            )}\n          </AnimatePresence>\n        </span>\n      ) : (\n        <span className=\"shrink-0 px-1.5 py-2 text-muted-foreground transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none group-hover:text-foreground\">\n          {iconSwitch}\n        </span>\n      )\n\n    const valueElement = (\n      <span\n        className={cn(\n          'flex-1 min-w-0 text-start text-[13px] text-foreground font-mono py-2 select-none truncate',\n          align === 'left' ? 'ps-1' : 'ps-0',\n        )}\n      >\n        {value}\n      </span>\n    )\n\n    const buttonContent =\n      align === 'left' ? (\n        <>\n          {actionElement}\n          {valueElement}\n        </>\n      ) : (\n        <>\n          {valueElement}\n          {actionElement}\n        </>\n      )\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          'flex flex-col gap-0.5',\n          disabled && 'opacity-50 pointer-events-none',\n          className,\n        )}\n        {...props}\n      >\n        {label && (\n          <span\n            className={cn('text-[13px] text-muted-foreground', align === 'left' ? 'ps-1' : 'ps-0')}\n          >\n            {label}\n          </span>\n        )}\n        <div className=\"relative\">\n          <button\n            type=\"button\"\n            onClick={handleCopy}\n            onMouseEnter={() => {\n              if (!disabled) playHoverSound()\n              setTooltipOpen(true)\n            }}\n            onMouseLeave={() => setTooltipOpen(false)}\n            onFocus={() => setTooltipOpen(true)}\n            onBlur={() => setTooltipOpen(false)}\n            onPointerDown={(e) => e.preventDefault()}\n            disabled={disabled}\n            aria-label={copied ? copiedLabel : copyLabel}\n            className={cn(\n              'group flex items-center w-full cursor-pointer outline-none rounded-lg squircle-corners border border-border bg-card',\n              'transition-colors duration-(--motion-dur-fast) motion-reduce:transition-none',\n              'focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 focus-visible:ring-offset-background',\n              variant === 'icon' && 'px-2',\n              variant === 'button' && 'px-2',\n            )}\n          >\n            {buttonContent}\n          </button>\n          {variant === 'icon' && (\n            <AnimatePresence>\n              {tooltipOpen && !disabled && (\n                <motion.span\n                  role=\"tooltip\"\n                  initial={{ opacity: 0, y: 2 }}\n                  animate={{ opacity: 1, y: 0 }}\n                  exit={{ opacity: 0, y: 2 }}\n                  transition={{ duration: 0.1 }}\n                  className=\"pointer-events-none absolute top-full mt-1.5 inset-e-0 z-20 whitespace-nowrap rounded-md squircle-corners bg-foreground px-2 py-1 text-[11px] font-medium text-background motion-reduce:transition-none\"\n                >\n                  {copied ? copiedLabel : copyLabel}\n                </motion.span>\n              )}\n            </AnimatePresence>\n          )}\n        </div>\n      </div>\n    )\n  },\n)\n\nInputCopy.displayName = 'InputCopy'\n\nexport function InputCopyPreview() {\n  return (\n    <div className=\"flex h-full w-full items-center justify-center p-6\">\n      <div className=\"w-full max-w-sm\">\n        <InputCopy label=\"API Key\" value=\"sk-proj-a1b2c3d4e5f6\" variant=\"button\" />\n      </div>\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/motion-tokens.ts",
      "type": "registry:lib",
      "content": "export const durations = {\n  instant: '50ms',\n  fast: '120ms',\n  base: '200ms',\n  slow: '320ms',\n} as const\n\nexport const easings = {\n  out: 'cubic-bezier(0.22, 1, 0.36, 1)',\n  inOut: 'cubic-bezier(0.65, 0, 0.35, 1)',\n} as const\n\nexport const springs = {\n  fast: { type: 'spring', stiffness: 400, damping: 30, mass: 0.8 },\n  press: { type: 'spring', stiffness: 700, damping: 32, mass: 1 },\n  moderate: { type: 'spring', stiffness: 300, damping: 24, mass: 1 },\n  settle: { type: 'spring', stiffness: 260, damping: 26, mass: 1 },\n} as const\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"
}
