AI Voice Waveform Visualizer
A Siri-inspired audio visualizer with organic blobs, multi-layer motion, and token-aware glass controls. Light/dark ready.
Installation
Add this component to your project using the CLI:
npx vui-registry-cli-v1 add ai-voice-waveformSource Code
"use client"
import { useEffect, useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { Mic, Square, GripHorizontal, Sparkles, Volume2, MicOff, Settings, X, Activity, Play, Pause, Download, VolumeX } from "lucide-react"
import { cn } from "@/lib/utils"
export default function AiVoiceWaveform() {
const [isListening, setIsListening] = useState(false)
const [mode, setMode] = useState<"siri" | "blob">("siri")
const [isMuted, setIsMuted] = useState(false)
const [volume, setVolume] = useState(50)
const [showSettings, setShowSettings] = useState(false)
const [voiceType, setVoiceType] = useState<"friendly" | "professional" | "robot">("friendly")
const [playbackState, setPlaybackState] = useState<"playing" | "paused">("paused")
// Toggle listening state
const toggleListening = () => {
setIsListening(!isListening)
if (!isListening) setPlaybackState("playing")
else setPlaybackState("paused")
}
const togglePlayback = () => {
setPlaybackState(playbackState === "playing" ? "paused" : "playing")
if (playbackState === "paused" && !isListening) setIsListening(true)
if (playbackState === "playing") setIsListening(false)
}
return (
<div className="relative flex min-h-[700px] w-full flex-col items-center justify-center overflow-hidden rounded-3xl bg-background shadow-2xl transition-colors duration-500 font-sans">
{/* Background Elements */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-foreground/10 via-foreground/5 to-transparent opacity-60" />
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 mix-blend-overlay" />
{/* Main Container */}
<div className="relative z-10 flex flex-col items-center justify-between h-full w-full max-w-4xl px-4 py-8 md:py-12">
{/* Top Status Bar */}
<div className="w-full flex justify-between items-center mb-8 md:mb-12">
<div className="flex items-center gap-3 bg-foreground/5 px-4 py-2 rounded-full border border-border/20 backdrop-blur-sm">
<div className={cn("h-2 w-2 rounded-full animate-pulse shadow-[0_0_10px_currentColor]", isListening ? "bg-green-500 text-green-500" : "bg-red-500 text-red-500")} />
<span className="text-xs font-medium text-neutral-300 uppercase tracking-widest">{isListening ? "Live" : "Offline"}</span>
</div>
<button
onClick={() => setShowSettings(!showSettings)}
className="p-3 rounded-full bg-foreground/5 hover:bg-foreground/10 transition-colors border border-border/20 backdrop-blur-sm group"
>
<Settings className="h-5 w-5 text-neutral-400 group-hover:text-white transition-colors" />
</button>
</div>
{/* Visualizer Area - Flexible Height */}
<div className="relative flex-1 w-full flex items-center justify-center min-h-[300px]">
<AnimatePresence mode="wait">
{isListening ? (
<motion.div
key="active"
initial={{ scale: 0.8, opacity: 0, filter: "blur(10px)" }}
animate={{ scale: 1, opacity: 1, filter: "blur(0px)" }}
exit={{ scale: 0.8, opacity: 0, filter: "blur(10px)" }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="relative w-full h-full flex items-center justify-center"
>
{mode === "siri" ? <SiriWaveform voiceType={voiceType} /> : <LiquidBlob voiceType={voiceType} />}
</motion.div>
) : (
<motion.button
key="idle"
onClick={toggleListening}
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.3 }}
className="relative h-40 w-40 rounded-full bg-card backdrop-blur-xl flex items-center justify-center border border-border shadow-[0_0_50px_hsl(var(--foreground)/0.05)] group cursor-pointer"
>
<div className="absolute inset-0 rounded-full border border-white/5 animate-ping opacity-20" />
<Mic className="h-12 w-12 text-neutral-500 group-hover:text-white transition-colors" />
</motion.button>
)}
</AnimatePresence>
</div>
{/* Dynamic Status Text */}
<div className="h-12 mt-8 mb-8 flex items-center justify-center overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key={isListening ? "listening" : "idle"}
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: -20, opacity: 0 }}
className="flex flex-col items-center gap-1"
>
{isListening ? (
<>
<p className="text-lg font-medium text-white tracking-tight flex items-center gap-2">
<Activity className="h-4 w-4 text-green-500 animate-bounce" />
Listening...
</p>
<p className="text-xs text-neutral-500 font-mono">Process ID: 8X-291</p>
</>
) : (
<p className="text-lg font-medium text-neutral-500">Tap microphone to start</p>
)}
</motion.div>
</AnimatePresence>
</div>
{/* Premium Control Dock - Capsule Shaped & Aligned */}
<div className="relative z-20 w-full max-w-lg mx-auto">
<motion.div
className="relative flex items-center justify-between gap-6 rounded-full border border-border bg-card p-3 px-6 backdrop-blur-2xl shadow-2xl ring-1 ring-foreground/5"
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ type: "spring", damping: 20, stiffness: 100 }}
>
{/* Play/Pause Controls */}
<div className="flex items-center gap-4">
<button
onClick={togglePlayback}
className="flex h-12 w-12 items-center justify-center rounded-full bg-foreground text-background hover:scale-105 active:scale-95 transition-all shadow-lg shadow-foreground/10"
>
{playbackState === "playing" ? (
<Pause className="h-5 w-5 fill-current" />
) : (
<Play className="h-5 w-5 fill-current ml-1" />
)}
</button>
<div className="flex flex-col gap-1">
<span className="text-xs font-medium text-foreground">Voice Assistant</span>
<div className="h-1 w-24 bg-foreground/10 rounded-full overflow-hidden">
<motion.div
className="h-full bg-foreground"
initial={{ width: "0%" }}
animate={{ width: isListening ? "100%" : "0%" }}
transition={{ duration: 20, ease: "linear", repeat: isListening ? Infinity : 0 }}
/>
</div>
</div>
</div>
{/* Middle Separator */}
<div className="h-8 w-[1px] bg-foreground/10" />
{/* Volume & Settings */}
<div className="flex items-center gap-4">
<button
onClick={() => setIsMuted(!isMuted)}
className="p-2 rounded-full hover:bg-white/10 text-neutral-400 hover:text-white transition-colors"
>
{isMuted ? <VolumeX className="h-5 w-5" /> : <Volume2 className="h-5 w-5" />}
</button>
{/* Mode Toggles */}
<div className="flex bg-foreground/5 rounded-full p-1 border border-border/20">
<button
onClick={() => setMode("siri")}
className={cn(
"p-2 rounded-full transition-all duration-300 relative",
mode === "siri" ? "text-white bg-white/10" : "text-neutral-500 hover:text-white"
)}
title="Siri Mode"
>
<Sparkles className="h-4 w-4" />
</button>
<button
onClick={() => setMode("blob")}
className={cn(
"p-2 rounded-full transition-all duration-300 relative",
mode === "blob" ? "text-white bg-white/10" : "text-neutral-500 hover:text-white"
)}
title="Blob Mode"
>
<GripHorizontal className="h-4 w-4" />
</button>
</div>
<button className="p-2 rounded-full hover:bg-white/10 text-neutral-400 hover:text-white transition-colors">
<Download className="h-5 w-5" />
</button>
</div>
</motion.div>
</div>
{/* Settings Panel Overlay */}
<AnimatePresence>
{showSettings && (
<motion.div
initial={{ opacity: 0, y: 50, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 50, scale: 0.95 }}
transition={{ type: "spring", damping: 20, stiffness: 300 }}
className="absolute bottom-32 w-[90%] max-w-sm bg-neutral-900/90 border border-white/10 rounded-3xl p-6 shadow-2xl z-50 backdrop-blur-xl ring-1 ring-white/5"
>
<div className="flex justify-between items-center mb-6">
<h3 className="text-base font-semibold text-white">Voice Settings</h3>
<button onClick={() => setShowSettings(false)} className="p-1 hover:bg-white/10 rounded-full transition-colors"><X className="h-4 w-4 text-neutral-400" /></button>
</div>
<div className="space-y-6">
<div>
<label className="text-xs font-medium text-neutral-500 mb-3 block uppercase tracking-wider">Personality</label>
<div className="grid grid-cols-3 gap-2">
{['friendly', 'professional', 'robot'].map((type) => (
<button
key={type}
onClick={() => setVoiceType(type as any)}
className={cn(
"px-3 py-2 rounded-xl text-xs font-medium capitalize transition-all duration-200 border",
voiceType === type
? "bg-white text-black border-white shadow-lg scale-105"
: "bg-neutral-800 text-neutral-400 border-transparent hover:bg-neutral-700 hover:text-white"
)}
>
{type}
</button>
))}
</div>
</div>
<div>
<div className="flex justify-between mb-3">
<label className="text-xs font-medium text-neutral-500 uppercase tracking-wider">Sensitivity</label>
<span className="text-xs font-mono text-white">{volume}%</span>
</div>
<div className="relative h-2 bg-neutral-800 rounded-full overflow-hidden group">
<motion.div
className="absolute top-0 left-0 h-full bg-white rounded-full"
style={{ width: `${volume}%` }}
/>
<input
type="range"
min="0" max="100"
value={volume}
onChange={(e) => setVolume(Number(e.target.value))}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
)
}
function SiriWaveform({ voiceType }: { voiceType: string }) {
const colors = {
friendly: ["bg-blue-500", "bg-purple-500", "bg-cyan-400", "bg-pink-500"],
professional: ["bg-emerald-500", "bg-teal-500", "bg-cyan-500", "bg-blue-600"],
robot: ["bg-orange-500", "bg-red-500", "bg-amber-500", "bg-yellow-500"]
}[voiceType] || ["bg-blue-500", "bg-purple-500", "bg-cyan-400", "bg-pink-500"]
return (
<div className="relative h-80 w-80 md:h-96 md:w-96 flex items-center justify-center">
<div className="absolute inset-0 rounded-full bg-white/5 blur-3xl animate-pulse" />
<div className="relative h-full w-full flex items-center justify-center mix-blend-screen">
{[0, 1, 2, 3].map((i) => (
<motion.div
key={i}
animate={{
scale: [1, 1.1 + (i * 0.1), 1],
rotate: [0, i % 2 === 0 ? 360 : -360],
x: [0, (i % 2 === 0 ? 20 : -20), 0],
y: [0, (i % 2 === 0 ? -20 : 20), 0],
}}
transition={{
duration: 3 + i,
repeat: Infinity,
ease: "easeInOut",
times: [0, 0.5, 1]
}}
className={cn(
"absolute rounded-full blur-2xl opacity-60",
colors[i],
i === 0 ? "h-48 w-48" : i === 1 ? "h-40 w-40" : i === 2 ? "h-32 w-32" : "h-24 w-24"
)}
/>
))}
{/* Core Glow */}
<div className="absolute h-20 w-20 bg-white blur-xl opacity-50 animate-pulse" />
</div>
</div>
)
}
function LiquidBlob({ voiceType }: { voiceType: string }) {
const gradient = {
friendly: "from-indigo-500 via-purple-500 to-pink-500",
professional: "from-emerald-500 via-teal-500 to-cyan-500",
robot: "from-orange-500 via-red-500 to-amber-500"
}[voiceType]
return (
<div className="relative h-80 w-80 md:h-96 md:w-96 flex items-center justify-center filter url(#goo)">
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<filter id="goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" result="goo" />
<feComposite in="SourceGraphic" in2="goo" operator="atop"/>
</filter>
</defs>
</svg>
{/* Main Blob */}
<motion.div
animate={{
borderRadius: ["50%", "40% 60% 70% 30% / 40% 50% 60% 50%", "60% 40% 30% 70% / 60% 30% 70% 40%", "50%"],
rotate: [0, 360],
scale: [1, 1.05, 0.95, 1]
}}
transition={{ duration: 10, repeat: Infinity, ease: "linear" }}
className={`absolute h-56 w-56 bg-gradient-to-br ${gradient} opacity-90 blur-sm`}
/>
{/* Satellite Blobs */}
{[...Array(3)].map((_, i) => (
<motion.div
key={i}
animate={{
x: [0, (Math.random() - 0.5) * 100, 0],
y: [0, (Math.random() - 0.5) * 100, 0],
scale: [0.8, 1.2, 0.8]
}}
transition={{
duration: 5 + Math.random() * 3,
repeat: Infinity,
ease: "easeInOut",
delay: i * 0.5
}}
className={`absolute h-16 w-16 bg-gradient-to-br ${gradient} opacity-80 blur-sm rounded-full`}
/>
))}
</div>
)
}
Dependencies
framer-motion: latestlucide-react: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| className | string | undefined | Additional CSS classes for the container. |
| initialMode | 'siri' | 'blob' | 'siri' | Initial visualization mode. |
| onListeningChange | (listening: boolean) => void | undefined | Callback when listening state toggles. |
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.

