{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "password-input",
  "title": "Password Input",
  "description": "A password input with an animated eye toggle that follows your cursor and blinks periodically.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/password-input.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport { forwardRef, useEffect, useId, useRef, useState, type InputHTMLAttributes } from 'react'\nimport { motion, useMotionValue, useReducedMotion, useSpring } from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { playHoverSound, playClickSound } from '@/lib/sound'\n\nconst EYE_OPEN_TOP = 'M1 12 C1 12 5 4 12 4 C19 4 23 12 23 12'\nconst EYE_CLOSED_TOP = 'M1 12 C1 12 5 12 12 12 C19 12 23 12 23 12'\nconst EYE_OPEN_BOTTOM = 'M1 12 C1 12 5 20 12 20 C19 20 23 12 23 12'\nconst EYE_CLOSED_BOTTOM = 'M1 12 C1 12 5 12 12 12 C19 12 23 12 23 12'\nconst CLIP_OPEN = 'M1 12 C1 12 5 4 12 4 C19 4 23 12 23 12 C23 12 19 20 12 20 C5 20 1 12 1 12Z'\nconst CLIP_CLOSED = 'M1 12 C1 12 5 12 12 12 C19 12 23 12 23 12 C23 12 19 12 12 12 C5 12 1 12 1 12Z'\n\nconst PUPIL_RX = 5\nconst PUPIL_RY = 3.5\n\nexport interface PasswordInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {\n  label?: string\n  showLabel?: string\n  hideLabel?: string\n  containerClassName?: string\n  labelClassName?: string\n}\n\nexport const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(\n  (\n    {\n      label = 'Password',\n      showLabel = 'Show password',\n      hideLabel = 'Hide password',\n      containerClassName,\n      labelClassName,\n      className,\n      id: idProp,\n      ...props\n    },\n    ref,\n  ) => {\n    const [visible, setVisible] = useState(false)\n    const [blinking, setBlinking] = useState(false)\n    const eyeRef = useRef<SVGSVGElement | null>(null)\n    const reactId = useId()\n    const id = idProp ?? reactId\n    const prefersReducedMotion = useReducedMotion()\n\n    const px = useMotionValue(0)\n    const py = useMotionValue(0)\n    const sx = useSpring(px, { stiffness: 350, damping: 28, mass: 0.4 })\n    const sy = useSpring(py, { stiffness: 350, damping: 28, mass: 0.4 })\n\n    useEffect(() => {\n      if (typeof window === 'undefined' || prefersReducedMotion) return\n      const handle = (e: MouseEvent) => {\n        const el = eyeRef.current\n        if (!el) return\n        const rect = el.getBoundingClientRect()\n        const cx = rect.left + rect.width / 2\n        const cy = rect.top + rect.height / 2\n        const dx = e.clientX - cx\n        const dy = e.clientY - cy\n        const dist = Math.hypot(dx, dy) || 1\n        const nx = dx / dist\n        const ny = dy / dist\n        const reach = Math.min(1, dist / 240)\n        px.set(nx * PUPIL_RX * reach)\n        py.set(ny * PUPIL_RY * reach)\n      }\n      window.addEventListener('mousemove', handle)\n      return () => window.removeEventListener('mousemove', handle)\n    }, [px, py, prefersReducedMotion])\n\n    useEffect(() => {\n      if (prefersReducedMotion) return\n      let timer: ReturnType<typeof setTimeout>\n      let blinkTimer: ReturnType<typeof setTimeout>\n      const schedule = () => {\n        const next = 2000 + Math.random() * 4000\n        timer = setTimeout(() => {\n          setBlinking(true)\n          blinkTimer = setTimeout(() => setBlinking(false), 120)\n          schedule()\n        }, next)\n      }\n      schedule()\n      return () => {\n        clearTimeout(timer)\n        clearTimeout(blinkTimer)\n      }\n    }, [prefersReducedMotion])\n\n    const closed = blinking || visible\n    const transition = prefersReducedMotion\n      ? { duration: 0 }\n      : { duration: 0.14, ease: [0.22, 1, 0.36, 1] as const }\n\n    return (\n      <div className={cn('w-72', containerClassName)} data-state={visible ? 'visible' : 'hidden'}>\n        {label ? (\n          <label\n            htmlFor={id}\n            className={cn('pb-2 font-medium text-sm font-sans', labelClassName)}\n            style={{ color: 'var(--color-fg)' }}\n          >\n            {label}\n          </label>\n        ) : null}\n        <div className=\"relative\">\n          <input\n            ref={ref}\n            id={id}\n            type={visible ? 'text' : 'password'}\n            className={cn(\n              'h-11 w-full rounded-lg ps-3.5 pe-12 py-2.5 text-sm outline-none',\n              'border border-(--color-border)',\n              'focus-visible:ring-2 focus-visible:ring-(--color-accent) focus-visible:ring-offset-1 focus-visible:ring-offset-(--color-bg)',\n              'transition-[border-color,box-shadow] duration-150',\n              'placeholder:text-(--color-subtle)',\n              className,\n            )}\n            style={{\n              backgroundColor: 'var(--color-surface-2)',\n              color: 'var(--color-fg)',\n            }}\n            {...props}\n          />\n          <button\n            type=\"button\"\n            onMouseEnter={() => playHoverSound()}\n            onClick={() => {\n              playClickSound()\n              setVisible((v) => !v)\n            }}\n            aria-label={visible ? hideLabel : showLabel}\n            aria-pressed={visible}\n            data-state={visible ? 'visible' : 'hidden'}\n            className={cn(\n              'absolute inset-e-3 top-1/2 -translate-y-1/2 cursor-pointer text-(--color-muted)',\n              \"before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:size-11\",\n              'transition-[color,transform] duration-150',\n              'hover:text-(--color-fg) active:scale-90',\n              'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-(--color-accent) focus-visible:rounded-sm',\n            )}\n          >\n            <svg\n              ref={eyeRef}\n              className=\"h-5 w-5\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth=\"1.7\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              aria-hidden=\"true\"\n            >\n              <defs>\n                <clipPath id={`${id}-eye-clip`}>\n                  <motion.path\n                    d={CLIP_OPEN}\n                    animate={{ d: closed ? CLIP_CLOSED : CLIP_OPEN }}\n                    transition={transition}\n                  />\n                </clipPath>\n              </defs>\n              <motion.path\n                d={EYE_OPEN_TOP}\n                animate={{ d: closed ? EYE_CLOSED_TOP : EYE_OPEN_TOP }}\n                transition={transition}\n              />\n              <motion.path\n                d={EYE_OPEN_BOTTOM}\n                animate={{ d: closed ? EYE_CLOSED_BOTTOM : EYE_OPEN_BOTTOM }}\n                transition={transition}\n              />\n              <g clipPath={`url(#${id}-eye-clip)`}>\n                <motion.g style={{ x: sx, y: sy }}>\n                  <circle cx=\"12\" cy=\"12\" r=\"3\" fill=\"currentColor\" stroke=\"none\" />\n                </motion.g>\n              </g>\n            </svg>\n          </button>\n        </div>\n      </div>\n    )\n  },\n)\n\nPasswordInput.displayName = 'PasswordInput'\n\nexport function PasswordInputPreview() {\n  return <PasswordInput />\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"
}
