{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voice-sample-player",
  "type": "registry:ui",
  "title": "Voice Sample Player",
  "description": "An audio player pill: press play and the name slides away, a lime waveform equalizer slides in and starts oscillating, and the icon morphs to pause. Pass a src to play real audio.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/voice-sample-player.tsx",
      "type": "registry:component",
      "target": "components/tweenui/voice-sample-player.tsx",
      "content": "'use client';\n\nimport { useCallback, useEffect, useRef, useState, type ComponentPropsWithoutRef } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n// Deterministic base heights so server and client render the same bars.\nconst BAR_COUNT = 40;\nconst BARS = Array.from({ length: BAR_COUNT }, (_, i) => {\n  const t = i / (BAR_COUNT - 1);\n  const wave =\n    Math.abs(Math.sin(t * Math.PI * 5)) * 0.6 + Math.abs(Math.sin(t * Math.PI * 13)) * 0.25;\n  return Math.min(1, 0.22 + wave);\n});\n\nexport interface VoiceSamplePlayerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange'> {\n  /** Speaker / voice name. */\n  name: string;\n  /** Short line under the name (accent, tone, language…). */\n  description?: string;\n  /** Avatar image URL. */\n  avatar?: string;\n  /** Alt text for the avatar (defaults to the name). */\n  avatarAlt?: string;\n  /** Audio file to play. Omit to animate without sound (previews). */\n  src?: string;\n  /** Controlled playing state. */\n  playing?: boolean;\n  /** Uncontrolled initial state. */\n  defaultPlaying?: boolean;\n  /** Fires when playback starts or stops. */\n  onPlayingChange?: (playing: boolean) => void;\n}\n\nconst VoiceSamplePlayer = ({\n  name,\n  description,\n  avatar,\n  avatarAlt,\n  src,\n  playing,\n  defaultPlaying = false,\n  onPlayingChange,\n  className,\n  ...props\n}: VoiceSamplePlayerProps) => {\n  const isControlled = playing !== undefined;\n  const [internal, setInternal] = useState(defaultPlaying);\n  const active = isControlled ? playing : internal;\n\n  const contentRef = useRef<HTMLDivElement>(null);\n  const waveRef = useRef<HTMLDivElement>(null);\n  const svgRef = useRef<SVGSVGElement>(null);\n  const playIconRef = useRef<SVGSVGElement>(null);\n  const pauseIconRef = useRef<SVGSVGElement>(null);\n  const audioRef = useRef<HTMLAudioElement | null>(null);\n  const firstRun = useRef(true);\n\n  const setActive = useCallback(\n    (next: boolean) => {\n      if (!isControlled) setInternal(next);\n      onPlayingChange?.(next);\n    },\n    [isControlled, onPlayingChange]\n  );\n\n  // Keep the latest setter for the audio \"ended\" listener without re-creating audio.\n  const endedRef = useRef<() => void>(() => {});\n  useEffect(() => {\n    endedRef.current = () => setActive(false);\n  }, [setActive]);\n\n  // Create the audio element once per source.\n  useEffect(() => {\n    if (!src) return;\n    const audio = new Audio(src);\n    audio.preload = 'metadata';\n    audioRef.current = audio;\n    const onEnded = () => endedRef.current();\n    audio.addEventListener('ended', onEnded);\n    return () => {\n      audio.pause();\n      audio.removeEventListener('ended', onEnded);\n      audio.src = '';\n      audioRef.current = null;\n    };\n  }, [src]);\n\n  // Sync playback with state.\n  useEffect(() => {\n    const audio = audioRef.current;\n    if (!audio) return;\n    if (active) audio.play().catch(() => endedRef.current());\n    else {\n      audio.pause();\n      audio.currentTime = 0;\n    }\n  }, [active]);\n\n  // Slide the label out / waveform in, and morph the play ↔ pause icon.\n  useGSAP(\n    () => {\n      const immediate = firstRun.current || prefersReducedMotion();\n      firstRun.current = false;\n      const opts = immediate ? { duration: 0 } : { duration: 0.5, ease: 'power2.inOut' as const };\n\n      gsap.to(contentRef.current, {\n        yPercent: active ? -130 : 0,\n        autoAlpha: active ? 0 : 1,\n        ...opts,\n      });\n      gsap.to(waveRef.current, { yPercent: active ? 0 : 130, autoAlpha: active ? 1 : 0, ...opts });\n      gsap.to(playIconRef.current, {\n        yPercent: active ? -130 : 0,\n        autoAlpha: active ? 0 : 1,\n        ...opts,\n      });\n      gsap.to(pauseIconRef.current, {\n        yPercent: active ? 0 : 130,\n        autoAlpha: active ? 1 : 0,\n        ...opts,\n      });\n    },\n    { dependencies: [active] }\n  );\n\n  // Equalizer: oscillate the bars while playing.\n  useGSAP(\n    () => {\n      const bars = gsap.utils.toArray<SVGRectElement>('rect', svgRef.current);\n      if (!bars.length) return;\n      gsap.set(bars, {\n        transformBox: 'fill-box',\n        transformOrigin: 'center center',\n        scaleY: (i: number) => BARS[i % BARS.length],\n      });\n      if (!active || prefersReducedMotion()) return;\n\n      bars.forEach((bar, i) => {\n        gsap.to(bar, {\n          scaleY: 0.25 + Math.random() * 0.75,\n          duration: 0.45 + Math.random() * 0.4,\n          ease: 'sine.inOut',\n          repeat: -1,\n          yoyo: true,\n          delay: i * 0.03,\n        });\n      });\n    },\n    { dependencies: [active] }\n  );\n\n  return (\n    <div\n      {...props}\n      data-voice-sample-player\n      data-playing={active ? 'true' : 'false'}\n      className={cn(\n        'flex w-full max-w-[420px] items-center gap-3 rounded-full p-2 transition-colors duration-500 ease-in-out motion-reduce:transition-none',\n        active ? 'bg-[#045f64]' : 'bg-[#045f64]/10 dark:bg-[#045f64]/20',\n        className\n      )}\n    >\n      <figure className=\"size-14 shrink-0 overflow-hidden rounded-full bg-[#045f64]/15\">\n        {avatar ? (\n          <img src={avatar} alt={avatarAlt ?? name} className=\"size-full object-cover\" />\n        ) : (\n          <span className=\"flex size-full items-center justify-center text-sm font-medium text-[#045f64] dark:text-[#9fd4d6]\">\n            {name.slice(0, 2).toUpperCase()}\n          </span>\n        )}\n      </figure>\n\n      <div className=\"relative h-14 flex-1 overflow-hidden\">\n        <div ref={contentRef} className=\"absolute inset-0 flex flex-col justify-center\">\n          <p className=\"truncate text-sm font-medium text-[#045f64] dark:text-[#9fd4d6]\">{name}</p>\n          {description ? (\n            <p className=\"truncate text-xs text-[#045f64]/60 dark:text-[#9fd4d6]/80\">\n              {description}\n            </p>\n          ) : null}\n        </div>\n\n        <div\n          ref={waveRef}\n          className=\"absolute inset-0 flex items-center opacity-0 motion-reduce:opacity-100\"\n          aria-hidden=\"true\"\n        >\n          <svg\n            ref={svgRef}\n            viewBox={`0 0 ${BAR_COUNT * 6} 32`}\n            className=\"h-8 w-full\"\n            fill=\"#c6f56f\"\n            preserveAspectRatio=\"none\"\n          >\n            {BARS.map((_, i) => (\n              <rect key={i} x={i * 6 + 1.5} y={4} width={3} height={24} rx={1.5} />\n            ))}\n          </svg>\n        </div>\n      </div>\n\n      <button\n        type=\"button\"\n        onClick={() => setActive(!active)}\n        aria-pressed={active}\n        aria-label={active ? `Pause ${name}` : `Play ${name}`}\n        className=\"relative flex size-14 shrink-0 cursor-pointer items-center justify-center rounded-full bg-white text-[#045f64] shadow-[0_1px_1px_rgba(16,24,40,0.16)] transition-transform hover:scale-105 motion-reduce:transition-none motion-reduce:hover:scale-100 dark:bg-[#12161F] dark:text-[#9fd4d6]\"\n      >\n        <span className=\"relative size-6 overflow-hidden\">\n          <svg\n            ref={playIconRef}\n            viewBox=\"0 0 24 24\"\n            className=\"absolute inset-0 size-6 fill-current\"\n            aria-hidden=\"true\"\n          >\n            <path d=\"M8 5.14v13.72a1 1 0 0 0 1.5.86l11-6.86a1 1 0 0 0 0-1.72l-11-6.86A1 1 0 0 0 8 5.14Z\" />\n          </svg>\n          <svg\n            ref={pauseIconRef}\n            viewBox=\"0 0 24 24\"\n            className=\"absolute inset-0 size-6 fill-current opacity-0 motion-reduce:opacity-100\"\n            aria-hidden=\"true\"\n          >\n            <rect x=\"6\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n            <rect x=\"14\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\" />\n          </svg>\n        </span>\n      </button>\n    </div>\n  );\n};\n\nexport default VoiceSamplePlayer;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/voice-sample-player"
}