| "use client"; |
|
|
| import { motion, AnimatePresence } from "framer-motion"; |
| import { Mic, MicOff } from "lucide-react"; |
|
|
| interface VoiceButtonProps { |
| isRecording: boolean; |
| onToggle: () => void; |
| } |
|
|
| export function VoiceButton({ isRecording, onToggle }: VoiceButtonProps) { |
| return ( |
| <div className="relative"> |
| {/* Pulsing rings when recording */} |
| <AnimatePresence> |
| {isRecording && ( |
| <> |
| <motion.div |
| className="absolute inset-0 rounded-full" |
| style={{ border: "2px solid #a1a1aa" }} |
| initial={{ scale: 1, opacity: 0.6 }} |
| animate={{ scale: 1.8, opacity: 0 }} |
| transition={{ duration: 1.5, repeat: Infinity, ease: "easeOut" }} |
| /> |
| <motion.div |
| className="absolute inset-0 rounded-full" |
| style={{ border: "2px solid #a1a1aa" }} |
| initial={{ scale: 1, opacity: 0.4 }} |
| animate={{ scale: 2.2, opacity: 0 }} |
| transition={{ |
| duration: 1.5, |
| repeat: Infinity, |
| ease: "easeOut", |
| delay: 0.3, |
| }} |
| /> |
| <motion.div |
| className="absolute inset-0 rounded-full" |
| style={{ border: "1px solid #a1a1aa" }} |
| initial={{ scale: 1, opacity: 0.3 }} |
| animate={{ scale: 2.6, opacity: 0 }} |
| transition={{ |
| duration: 1.5, |
| repeat: Infinity, |
| ease: "easeOut", |
| delay: 0.6, |
| }} |
| /> |
| </> |
| )} |
| </AnimatePresence> |
|
|
| {} |
| <AnimatePresence> |
| {isRecording && ( |
| <motion.div |
| className="absolute inset-0 rounded-full" |
| style={{ backgroundColor: "#a1a1aa30" }} |
| initial={{ scale: 1 }} |
| animate={{ scale: 1.2 }} |
| exit={{ scale: 1 }} |
| transition={{ duration: 0.3 }} |
| /> |
| )} |
| </AnimatePresence> |
|
|
| {} |
| <motion.button |
| onClick={onToggle} |
| className={`relative z-10 w-9 h-9 rounded-full flex items-center justify-center transition-colors ${ |
| isRecording |
| ? "bg-foreground text-background shadow-[0_0_20px_#a1a1aa40]" |
| : "bg-foreground/10 text-foreground hover:bg-foreground/15 border border-border" |
| }`} |
| whileTap={{ scale: 0.9 }} |
| whileHover={{ scale: 1.05 }} |
| aria-label={isRecording ? "Stop recording" : "Start voice recording"} |
| > |
| {isRecording ? ( |
| <motion.div |
| animate={{ scale: [1, 1.2, 1] }} |
| transition={{ duration: 0.8, repeat: Infinity }} |
| > |
| <MicOff className="w-3.5 h-3.5" /> |
| </motion.div> |
| ) : ( |
| <Mic className="w-3.5 h-3.5" /> |
| )} |
| </motion.button> |
| </div> |
| ); |
| } |
|
|
| |
| export function WaveformVisualizer({ isActive }: { isActive: boolean }) { |
| const bars = 24; |
|
|
| return ( |
| <div className="flex items-center justify-center gap-[2px] h-8"> |
| {Array.from({ length: bars }).map((_, i) => ( |
| <motion.div |
| key={i} |
| className="w-[3px] rounded-full" |
| style={{ backgroundColor: isActive ? "#a1a1aa" : "#3f3f4640" }} |
| animate={ |
| isActive |
| ? { |
| height: [4, Math.random() * 28 + 4, 4], |
| } |
| : { height: 4 } |
| } |
| transition={ |
| isActive |
| ? { |
| duration: 0.4 + Math.random() * 0.3, |
| repeat: Infinity, |
| repeatType: "reverse", |
| delay: i * 0.03, |
| } |
| : { duration: 0.3 } |
| } |
| /> |
| ))} |
| </div> |
| ); |
| } |
|
|