Voice Input

PreviousNext

An interactive voice recording interface with animated visual feedback, timer display, and customizable start/stop callbacks.

Docs
moleculeuiui

Preview

Loading preview…
registry/molecule-ui/voice-input.tsx
"use client"

import React from "react"
import { Mic } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"

import { cn } from "@/lib/utils"

export interface VoiceInputProps {
  /**
   * Callback function called when voice recording starts.
   */
  onStart?: () => void
  /**
   * Callback function called when voice recording stops.
   */
  onStop?: () => void
}

export function VoiceInput({
  className,
  onStart,
  onStop,
  ...props
}: React.ComponentProps<"div"> & VoiceInputProps) {
  const [_listening, _setListening] = React.useState<boolean>(false)
  const [_time, _setTime] = React.useState<number>(0)

  React.useEffect(() => {
    let intervalId: NodeJS.Timeout

    if (_listening) {
      onStart?.()
      intervalId = setInterval(() => {
        _setTime((t) => t + 1)
      }, 1000)
    } else {
      onStop?.()
      _setTime(0)
    }

    return () => clearInterval(intervalId)
  }, [_listening, onStart, onStop])

  const formatTime = (seconds: number) => {
    const mins = Math.floor(seconds / 60)
    const secs = seconds % 60
    return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
  }

  const onClickHandler = () => {
    _setListening(!_listening)
  }

  return (
    <div
      className={cn("flex flex-col items-center justify-center", className)}
      {...props}
    >
      <motion.div
        className="flex cursor-pointer items-center justify-center rounded-full border p-2"
        layout
        transition={{
          layout: {
            duration: 0.4,
          },
        }}
        onClick={onClickHandler}
      >
        <div className="flex h-6 w-6 items-center justify-center">
          {_listening ? (
            <motion.div
              className="bg-primary h-4 w-4 rounded-sm"
              animate={{
                rotate: [0, 180, 360],
              }}
              transition={{
                duration: 2,
                repeat: Number.POSITIVE_INFINITY,
                ease: "easeInOut",
              }}
            />
          ) : (
            <Mic />
          )}
        </div>
        <AnimatePresence mode="wait">
          {_listening && (
            <motion.div
              initial={{ opacity: 0, width: 0, marginLeft: 0 }}
              animate={{ opacity: 1, width: "auto", marginLeft: 8 }}
              exit={{ opacity: 0, width: 0, marginLeft: 0 }}
              transition={{
                duration: 0.4,
              }}
              className="flex items-center justify-center gap-2 overflow-hidden"
            >
              {/* Frequency Animation */}
              <div className="flex items-center justify-center gap-0.5">
                {[...Array(12)].map((_, i) => (
                  <motion.div
                    key={i}
                    className="bg-primary w-0.5 rounded-full"
                    initial={{ height: 2 }}
                    animate={{
                      height: _listening
                        ? [2, 3 + Math.random() * 10, 3 + Math.random() * 5, 2]
                        : 2,
                    }}
                    transition={{
                      duration: _listening ? 1 : 0.3,
                      repeat: _listening ? Infinity : 0,
                      delay: _listening ? i * 0.05 : 0,
                      ease: "easeInOut",
                    }}
                  />
                ))}
              </div>
              {/* Timer */}
              <div className="text-muted-foreground w-10 text-center text-xs">
                {formatTime(_time)}
              </div>
            </motion.div>
          )}
        </AnimatePresence>
      </motion.div>
    </div>
  )
}

Installation

npx shadcn@latest add @moleculeui/voice-input

Usage

import { VoiceInput } from "@/components/ui/voice-input"
<VoiceInput />