Componentmedia1.36 KB gzipZero dependencies

Audio Recorder

Microphone recording simulator with waveform and send action.

Preview

Live Preview

00:00

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add audio-recorder

Or install the full npm package:

npm install bigbullui
import { AudioRecorder } from "@/components/ui/audio-recorder";

<AudioRecorder />

Usage guidance

Use Audio Recorder when a ticket stub surface needs Microphone recording simulator with waveform and send action. Keep props minimal and prefer composition with sibling stubs.

Avoid Audio Recorder for long form flows or dense data grids where a dedicated layout block fits better.

Source code

audio-recorder.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface AudioRecorderProps extends React.HTMLAttributes<HTMLDivElement> {
  onSend?: (seconds: number) => void;
}

/** Audio recorder simulator: waveform + duration + send. */
export function AudioRecorder({ onSend, className, ...props }: AudioRecorderProps) {
  const [recording, setRecording] = React.useState(false);
  const [seconds, setSeconds] = React.useState(0);

  React.useEffect(() => {
    if (!recording) return;
    const t = setInterval(() => setSeconds((s) => s + 1), 1000);
    return () => clearInterval(t);
  }, [recording]);

  const stop = () => {
    setRecording(false);
    onSend?.(seconds);
    setSeconds(0);
  };

  const mm = String(Math.floor(seconds / 60)).padStart(2, "0");
  const ss = String(seconds % 60).padStart(2, "0");

  return (
    <div className={cn("flex w-full max-w-sm items-center gap-3 rounded-md border border-border bg-card p-3", className)} {...props}>
      <style>{`@keyframes arWave { 0%, 100% { transform: scaleY(0.4); } 50% { transform: scaleY(1); } }`}</style>
      <button
        onClick={() => (recording ? stop() : setRecording(true))}
        className={cn(
          "flex size-11 shrink-0 items-center justify-center rounded-full transition-all duration-200 motion-reduce:transition-none",
          "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
          recording ? "bg-destructive text-white animate-pulse motion-reduce:animate-none" : "bg-accent text-accent-foreground hover:bg-accent/90"
        )}
        aria-label={recording ? "Stop and submit recording" : "Start recording"}
      >
        {recording ? (
          <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="1" /></svg>
        ) : (
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" /><path d="M19 10v2a7 7 0 0 1-14 0v-2" /><path d="M12 19v4" /></svg>
        )}
      </button>
      <div className="min-w-0 flex-1">
        <p className="font-mono text-xs tabular-nums text-muted-foreground">{mm}:{ss}</p>
        <div className="mt-1 flex h-5 items-center gap-0.5" aria-hidden="true">
          {Array.from({ length: 24 }).map((_, i) => (
            <span
              key={i}
              className={cn("w-0.5 rounded-full", recording ? "bg-destructive" : "bg-border")}
              style={recording ? { height: `${20 + ((i * 13) % 70)}%`, animation: `arWave ${0.5 + (i % 5) * 0.1}s ease-in-out infinite`, animationDelay: `${i * 40}ms` } : { height: "20%" }}
            />
          ))}
        </div>
      </div>
      {recording && (
        <button
          onClick={stop}
          className="shrink-0 rounded-md bg-success px-3 py-1.5 font-mono text-[10px] uppercase tracking-wider text-white transition-all duration-150 hover:bg-success active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-success motion-reduce:transition-none"
        >
          Send
        </button>
      )}
    </div>
  );
}

Props

PropTypeDescription
onSend(seconds: number) => void