'use client'
import { useEffect, useMemo, useState } from 'react'
import { useReducedMotion } from 'motion/react'
import {
VoiceWaveformBar,
type VoiceWaveformState,
} from '@/components/ui/voice-waveform-bar'
const BAR_COUNT = 24
/** Deterministic sine-derived amplitude frames, no Math.random in render */
function buildSineFrame(phase: number, amplitude: number): number[] {
return Array.from({ length: BAR_COUNT }, (_, i) => {
const a =
amplitude *
(0.35 +
0.65 *
Math.abs(
Math.sin(phase + i * 0.42) * 0.55 +
Math.sin(phase * 1.7 + i * 0.18) * 0.45
))
return Math.max(0.06, Math.min(1, a))
})
}
// precomputed sequence stepped by index only
const LISTEN_FRAMES = Array.from({ length: 32 }, (_, i) =>
buildSineFrame(i * 0.35, 0.85)
)
const SPEAK_FRAMES = Array.from({ length: 32 }, (_, i) =>
buildSineFrame(i * 0.28 + 1.2, 0.7)
)
type Phase = {
state: VoiceWaveformState
duration: string
transcript: string
holdMs: number
levels?: number[]
}
const PHASES: Phase[] = [
{
state: 'idle',
duration: '0:00',
transcript: 'Tap the mic to start a voice turn with Meridian.',
holdMs: 1600,
},
{
state: 'listening',
duration: '0:04',
transcript: 'Reconcile May invoices for Apex Holdings…',
holdMs: 2200,
},
{
state: 'listening',
duration: '0:07',
transcript: 'Focus Acme Logistics and flag anything over fifty dollars.',
holdMs: 2000,
},
{
state: 'processing',
duration: '0:09',
transcript: 'Matching payments against the Xero trial balance…',
holdMs: 1800,
},
{
state: 'speaking',
duration: '0:14',
transcript:
'Found a duplicate $1,240 charge on invoice inv_2026_03_8841.',
holdMs: 2400,
},
{
state: 'speaking',
duration: '0:18',
transcript: 'Shall I draft the refund proposal for controller review?',
holdMs: 2200,
},
{
state: 'idle',
duration: '0:18',
transcript: 'Turn complete, awaiting your next instruction.',
holdMs: 2000,
},
]
export default function VoiceWaveformBarDemo() {
const reduced = useReducedMotion()
const [phaseIndex, setPhaseIndex] = useState(0)
const [frameTick, setFrameTick] = useState(0)
const [listening, setListening] = useState(false)
const phase = PHASES[phaseIndex] ?? PHASES[0]!
useEffect(() => {
if (reduced) {
setPhaseIndex(4) // speaking, composed mid-session frame
return
}
const id = window.setTimeout(() => {
setPhaseIndex((i) => (i + 1) % PHASES.length)
}, phase.holdMs)
return () => window.clearTimeout(id)
}, [phaseIndex, phase.holdMs, reduced])
// step amplitude frames while listening/speaking
useEffect(() => {
if (reduced) return
if (phase.state !== 'listening' && phase.state !== 'speaking') return
const id = window.setInterval(() => {
setFrameTick((t) => t + 1)
}, 80)
return () => window.clearInterval(id)
}, [phase.state, reduced])
const levels = useMemo(() => {
if (phase.state === 'listening') {
return LISTEN_FRAMES[frameTick % LISTEN_FRAMES.length]
}
if (phase.state === 'speaking') {
return SPEAK_FRAMES[frameTick % SPEAK_FRAMES.length]
}
return undefined
}, [phase.state, frameTick])
const state: VoiceWaveformState = listening
? 'listening'
: reduced
? 'speaking'
: phase.state
const duration = reduced ? '0:14' : phase.duration
const transcript = reduced
? 'Found a duplicate $1,240 charge on invoice inv_2026_03_8841.'
: listening
? 'Listening, speak naturally, Meridian is capturing…'
: phase.transcript
return (
<div className="mx-auto flex min-h-[320px] w-full max-w-md flex-col justify-center px-4 py-16 sm:px-6">
<div className="overflow-hidden rounded-2xl border border-border/60 bg-card">
<div className="border-b border-border/60 px-4 py-3 sm:px-5">
<p className="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground sm:text-xs">
meridian-ops · voice turn
</p>
</div>
<div className="px-4 py-6 sm:px-5">
<VoiceWaveformBar
state={state}
levels={
listening
? LISTEN_FRAMES[frameTick % LISTEN_FRAMES.length]
: levels
}
duration={duration}
transcript={transcript}
onToggle={() => setListening((v) => !v)}
label="voice"
/>
</div>
</div>
</div>
)
}