Componentmedia1.59 KB gzipZero dependencies

AudioPlayer

Full deck audio player with play/pause morph button, seek bar, time readout, volume slider, and track info

Preview

Live Preview
data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='18' fill='%236b7280'/%3E%3Ccircle cx='20' cy='20' r='12' fill='none' stroke='%23fff' stroke-width='2'/%3E%3C/svg%3E

STAGE LIVE RECORDING

ORCHESTRA ROW C

0:003:30
0:003:30
0:00

Installation

Tokens guide →

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

npx bigbullui add audio-player

Or install the full npm package:

npm install bigbullui
import { AudioPlayer } from "@/components/ui/audio-player"

<AudioPlayer title="Track Title" artist="Artist Name" duration={180} />

Usage guidance

Use AudioPlayer when a ticket stub surface needs Full deck audio player with play/pause morph button, seek bar, time readout, volume slider, and track info Keep props minimal and prefer composition with sibling stubs.

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

Source code

audio-player.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 AudioPlayerProps {
  title: string;
  artist?: string;
  duration?: number;
  cover?: React.ReactNode;
  className?: string;
}

export function AudioPlayer({
  title,
  artist = "Unknown Artist",
  duration = 180,
  cover,
  className,
  ...props
}: AudioPlayerProps) {
  const [isPlaying, setIsPlaying] = React.useState(false);
  const [progress, setProgress] = React.useState(0);
  const [volume, setVolume] = React.useState(0.7);

  React.useEffect(() => {
    let timer: NodeJS.Timeout;
    if (isPlaying) {
      timer = setInterval(() => {
        setProgress((p) => Math.min(p + 1, duration));
      }, 1000);
    }
    return () => clearInterval(timer);
  }, [isPlaying, duration]);

  const togglePlay = () => {
    setIsPlaying(!isPlaying);
  };

  const formatTime = (seconds: number) => {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins}:${secs < 10 ? "0" : ""}${secs}`;
  };

  return (
    <div
      className={cn(
        "w-full max-w-sm rounded-xl border-2 border-dashed border-border bg-card p-6 shadow-sm outline-1 outline-dashed outline-offset-[-6px]",
        className
      )}
      {...props}
    >
      {cover && (
        <div className="rounded-lg overflow-hidden bg-muted h-40">
          {typeof cover === "string" ? cover : null}
        </div>
      )}

      <div className="flex items-center justify-between mb-4">
        <h3 className="font-mono text-[10px] uppercase tracking-[0.15em] text-muted-foreground">
          {title}
        </h3>
        {artist && (
          <p className="font-mono text-sm text-muted-foreground">{artist}</p>
        )}
      </div>

      <div className="mb-4">
        <div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
          <span>0:00</span>
          <span>{formatTime(duration)}</span>
        </div>
        <div
          className={cn(
            "w-full bg-background rounded-full h-2.5 overflow-hidden",
            "motion-reduce:transition-none"
          )}
        >
          <div
            className={cn(
              "h-full bg-primary w-0 rounded-full transition-all duration-500 ease-out",
              progress > 0 && "shadow-lg",
              "motion-reduce:transition-none"
            )}
            style={{ width: `${(progress / duration) * 100}%` }}
          >
          </div>
        </div>
        <div className="flex items-center justify-between text-xs text-muted-foreground mt-1">
          <span>0:00</span>
          <span>{formatTime(duration)}</span>
        </div>
      </div>

      <div className="flex items-center justify-between mb-4">
        <button
          onClick={togglePlay}
          className={cn(
            "rounded-full p-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
            "bg-primary/10 text-primary",
            "transition-colors hover:bg-primary/20",
            "motion-reduce:animate-none"
          )}
        >
          {isPlaying ? (
            <svg
              className="w-5 h-5"
            >
              <polygon
                points="5 3 19 12 5 21 5 12"
                fill="currentColor"
              />
            </svg>
          ) : (
            <svg
              className="w-5 h-5"
            >
              <path d="M8 5v14l11-7z" fill="currentColor" />
            </svg>
          )}
        </button>

        <div className="flex items-center gap-2">
          <span className="text-[10px] uppercase tracking-[0.15em] text-muted-foreground">
            {formatTime(progress)}
          </span>
          <input
            type="range"
            min="0"
            max={duration}
            value={progress}
            onChange={(e) => setProgress(parseInt(e.target.value, 10))}
            className="w-24 appearance-none rounded-md bg-muted h-1.5 outline-none"
          />
        </div>
      </div>

      <div className="flex items-center justify-between">
        <button
          onClick={() => setVolume(0)}
          className={cn(
            "p-1 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring hover:bg-muted transition-colors",
            "motion-reduce:animate-none"
          )}
          aria-label="Mute"
        >
          <svg
            className="w-4 h-4"
          >
            <path
              d="M6 4h4v16H6V4zm10-4v16h4v-2h2v-4h2v-4h4v-2h2V4h-4v2h-2v4h-2v2h-4v-2H6zm14.5-3.5a8.38 8.38 0 0 1-.9 3.8 8.4 8.4 0 0 1-3.3 1.8 8.38 8.38 0 0 1-3.8-.5 8.36 8.36 0 0 1-2.4-2.2 8.35 8.35 0 0 1-.5-3.3 8.38 8.38 0 0 1 1.8-3.3 8.4 8.4 0 0 1 3.8-.9 8.38 8.38 0 0 1 3.8 1.8 8.36 8.36 0 0 1 1.8 3.3 8.35 8.35 0 0 1-.5 3.3z"
            />
          </svg>
        </button>
        <button
          onClick={() => setVolume(1)}
          className={cn(
            "p-1 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring hover:bg-primary-50 transition-colors",
            "motion-reduce:animate-none"
          )}
          aria-label="Unmute"
        >
          <svg
            className="w-4 h-4"
          >
            <path
              d="M5 9v6h4v5h2v-5h4v5h2v-6H5zm2-3a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"
            />
          </svg>
        </button>
      </div>
    </div>
  );
}

Props

PropTypeDescription
titlestring
artist?string
duration?number
cover?React.ReactNode
className?string