"use client" ;
import { useState, useRef, useEffect } from "react" ;
import { Mic, Square, Play, Pause, RotateCcw, Check } from "lucide-react" ;
import { Button } from "@/components/ui/button" ;
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog" ;
import { Input } from "@/components/ui/input" ;
import { Label } from "@/components/ui/label" ;
import { cn } from "@/lib/utils" ;
type RecordingState = "idle" | "recording" | "recorded" | "playing" ;
export const title = "React Dialog Block Voice Recorder" ;
export default function DialogVoiceRecorder () {
const [ open , setOpen ] = useState ( false );
const [ state , setState ] = useState < RecordingState >( "idle" );
const [ duration , setDuration ] = useState ( 0 );
const [ fileName , setFileName ] = useState ( "" );
const [ waveformBars , setWaveformBars ] = useState < number []>(
Array ( 16 ). fill ( 10 )
);
const [ saved , setSaved ] = useState ( false );
const timerRef = useRef < NodeJS . Timeout | null >( null );
const waveformRef = useRef < NodeJS . Timeout | null >( null );
useEffect (() => {
return () => {
if (timerRef.current) clearInterval (timerRef.current);
if (waveformRef.current) clearInterval (waveformRef.current);
};
}, []);
const formatTime = ( seconds : number ) => {
const mins = Math. floor (seconds / 60 );
const secs = seconds % 60 ;
return `${ mins }:${ secs . toString (). padStart ( 2 , "0" ) }` ;
};
const generateWaveform = () => {
return Array ( 16 )
. fill ( 0 )
. map (() => Math. random () * 70 + 30 );
};
const startRecording = () => {
setState ( "recording" );
setDuration ( 0 );
setWaveformBars ( generateWaveform ());
timerRef.current = setInterval (() => {
setDuration (( prev ) => prev + 1 );
}, 1000 );
waveformRef.current = setInterval (() => {
setWaveformBars ( generateWaveform ());
}, 100 );
};
const stopRecording = () => {
if (timerRef.current) clearInterval (timerRef.current);
if (waveformRef.current) clearInterval (waveformRef.current);
setState ( "recorded" );
setWaveformBars ( Array ( 16 ). fill ( 25 ));
};
const playRecording = () => {
setState ( "playing" );
setTimeout (() => setState ( "recorded" ), duration * 1000 );
};
const pausePlayback = () => {
setState ( "recorded" );
};
const discardRecording = () => {
setState ( "idle" );
setDuration ( 0 );
setFileName ( "" );
setWaveformBars ( Array ( 16 ). fill ( 10 ));
};
const saveRecording = () => {
setSaved ( true );
};
const handleClose = () => {
setOpen ( false );
setTimeout (() => {
setState ( "idle" );
setDuration ( 0 );
setFileName ( "" );
setWaveformBars ( Array ( 16 ). fill ( 10 ));
setSaved ( false );
}, 200 );
};
if (saved) {
return (
< Dialog open = {open} onOpenChange = {setOpen}>
< div className = "flex min-h-[350px] items-center justify-center" >
< DialogTrigger asChild >
< Button variant = "outline" >
< Mic className = "mr-2 h-4 w-4" />
Record Voice
</ Button >
</ DialogTrigger >
</ div >
< DialogContent style = {{ maxWidth: "384px" }}>
< div className = "flex flex-col items-center justify-center py-6 text-center" >
< Check className = "h-8 w-8 text-primary" />
< h3 className = "mt-4 text-lg font-medium" >Recording Saved</ h3 >
< p className = "mt-1 text-sm text-muted-foreground" >
{fileName || "Voice memo" } · { formatTime (duration)}
</ p >
< Button onClick = {handleClose} className = "mt-6" >
Done
</ Button >
</ div >
</ DialogContent >
</ Dialog >
);
}
return (
< Dialog open = {open} onOpenChange = {setOpen}>
< div className = "flex min-h-[350px] items-center justify-center" >
< DialogTrigger asChild >
< Button variant = "outline" >
< Mic className = "mr-2 h-4 w-4" />
Record Voice
</ Button >
</ DialogTrigger >
</ div >
< DialogContent style = {{ maxWidth: "384px" }}>
< DialogHeader >
< DialogTitle >Voice Recorder</ DialogTitle >
< DialogDescription >Record a voice memo.</ DialogDescription >
</ DialogHeader >
< div className = "py-6" >
{ /* Timer and controls */ }
< div className = "flex flex-col items-center" >
< p className = "text-4xl font-mono font-semibold tabular-nums" >
{ formatTime (duration)}
</ p >
{ /* Waveform */ }
< div className = "flex items-center justify-center gap-0.5 h-12 my-6" >
{waveformBars. map (( height , index ) => (
< div
key = {index}
className = { cn (
"w-1.5 rounded-full transition-all duration-100" ,
state === "recording"
? "bg-primary"
: "bg-muted-foreground/30"
)}
style = {{ height: `${ height }%` }}
/>
))}
</ div >
{ /* Main control button */ }
{state === "idle" && (
< Button
size = "lg"
className = "h-14 w-14 rounded-full"
onClick = {startRecording}
>
< Mic className = "h-6 w-6" />
</ Button >
)}
{state === "recording" && (
< Button
size = "lg"
variant = "destructive"
className = "h-14 w-14 rounded-full"
onClick = {stopRecording}
>
< Square className = "h-5 w-5" />
</ Button >
)}
{(state === "recorded" || state === "playing" ) && (
< div className = "flex items-center gap-3" >
< Button
size = "icon"
variant = "outline"
className = "h-10 w-10 rounded-full"
onClick = {discardRecording}
>
< RotateCcw className = "h-4 w-4" />
</ Button >
< Button
size = "lg"
className = "h-14 w-14 rounded-full"
onClick = {state === "playing" ? pausePlayback : playRecording}
>
{state === "playing" ? (
< Pause className = "h-6 w-6" />
) : (
< Play className = "h-6 w-6 ml-0.5" />
)}
</ Button >
< div className = "w-10" />
</ div >
)}
</ div >
{ /* File name input */ }
{(state === "recorded" || state === "playing" ) && (
< div className = "mt-6 space-y-2" >
< Label htmlFor = "filename" className = "text-sm" >
Name (optional)
</ Label >
< Input
id = "filename"
placeholder = "Voice memo"
value = {fileName}
onChange = {( e ) => setFileName (e.target.value)}
/>
</ div >
)}
</ div >
< DialogFooter >
{state === "idle" && (
< Button variant = "outline" onClick = {handleClose}>
Cancel
</ Button >
)}
{state === "recording" && (
< Button variant = "outline" onClick = {discardRecording}>
Cancel
</ Button >
)}
{(state === "recorded" || state === "playing" ) && (
<>
< Button variant = "outline" onClick = {handleClose}>
Cancel
</ Button >
< Button onClick = {saveRecording}>Save</ Button >
</>
)}
</ DialogFooter >
</ DialogContent >
</ Dialog >
);
}