Skip to content

Audio Player

A controlled playback surface for speech, generated audio, and recorded tool output.

Usage

import { AudioPlayer } from "@/components/ai/audio-player"


export function Example() {
  return <AudioPlayer title="Briefing" currentTime={42} duration={186} />
}

Install

pnpm dlx shadcn@latest add @pastaui/audio-player

Source

import { SoundwaveIcon as AudioLinesIcon } from '@solar-icons/react/linear/soundwave'
import { PauseIcon } from '@solar-icons/react/bold/pause'
import { PlayIcon } from '@solar-icons/react/bold/play'
import { RewindBackIcon as PreviousIcon } from '@solar-icons/react/bold/rewind-back'
import { RewindForwardIcon as NextIcon } from '@solar-icons/react/bold/rewind-forward'
import { AirbudsIcon } from '@solar-icons/react/bold/airbuds'
import * as React from 'react'


import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { cn } from 'cn'


interface AudioPlayerProps extends Omit<React.ComponentProps<'section'>, 'onChange'> {
  currentTime: number
  artworkUrl?: string
  duration: number
  isPlaying?: boolean
  onChange?: (time: number) => void
  onToggle?: () => void
  onPrevious?: () => void
  onNext?: () => void
  onOutput?: () => void
  subtitle?: string
  title: string
}


function formatTime(seconds: number) {
  const minutes = Math.floor(seconds / 60)
  const remainder = Math.floor(seconds % 60)
  return `${minutes}:${remainder.toString().padStart(2, '0')}`
}


function AudioPlayer({
  className,
  artworkUrl,
  currentTime,
  duration,
  isPlaying = false,
  onChange,
  onToggle,
  onPrevious,
  onNext,
  onOutput,
  subtitle,
  title,
  ...props
}: AudioPlayerProps) {
  return (
    <section
      data-slot="audio-player"
      className={cn(
        'rounded-[16px] bg-card p-6 font-sans text-sm font-[450] tracking-[-0.05px] text-muted-foreground shadow-card sm:p-7',
        className,
      )}
      {...props}
    >
      <div className="flex items-center gap-5">
        {artworkUrl ? (
          <img src={artworkUrl} alt="" className="size-20 shrink-0 rounded-[10px] object-cover" />
        ) : (
          <span className="flex size-20 shrink-0 items-center justify-center rounded-[10px] bg-muted">
            <AudioLinesIcon aria-hidden="true" className="size-8 text-muted-foreground" />
          </span>
        )}
        <div className="min-w-0 flex-1">
          <h3 className="truncate card-heading">{title}</h3>
          {subtitle && (
            <p className="mt-1 truncate text-xs leading-5 text-muted-foreground">{subtitle}</p>
          )}
        </div>
        <AudioLinesIcon aria-hidden="true" className="size-7 shrink-0 text-muted-foreground" />
      </div>
      <div className="mt-5 flex items-center gap-2.5">
        <span className="shrink-0 text-xs leading-5 text-muted-foreground tabular-nums">
          {formatTime(currentTime)}
        </span>
        <Slider
          aria-label="Audio position"
          className="min-w-0 flex-1 [--muted:var(--audio-track)] [--primary:var(--audio-progress)] [&_[data-slot=slider-thumb]]:size-4 [&_[data-slot=slider-thumb]]:opacity-0 [&_[data-slot=slider-thumb]]:transition-opacity [&_[data-slot=slider-thumb]]:duration-150 [&_[data-slot=slider-thumb]:has(:focus-visible)]:opacity-100 [&_[data-slot=slider-track]]:h-1.5 [&:hover_[data-slot=slider-thumb]]:opacity-100"
          max={duration}
          min={0}
          onValueChange={(value) => onChange?.(Array.isArray(value) ? value[0] : value)}
          value={[currentTime]}
        />
        <span className="shrink-0 text-right text-xs leading-5 text-muted-foreground tabular-nums">
          −{formatTime(Math.max(0, duration - currentTime))}
        </span>
      </div>
      <div className="relative mt-4 flex items-center justify-center gap-6">
        <Button
          aria-label={onPrevious ? 'Previous track' : 'Rewind 10 seconds'}
          disabled={!onPrevious && !onChange}
          onClick={() => (onPrevious ? onPrevious() : onChange?.(Math.max(0, currentTime - 10)))}
          className="size-11 rounded-md text-muted-foreground hover:bg-foreground/10 hover:text-muted-foreground"
          size="icon"
          variant="ghost"
        >
          <PreviousIcon className="size-8" />
        </Button>
        <Button
          aria-label={isPlaying ? 'Pause audio' : 'Play audio'}
          disabled={!onToggle}
          onClick={onToggle}
          className="size-14 rounded-md text-muted-foreground hover:bg-foreground/10 hover:text-muted-foreground"
          size="icon"
          variant="ghost"
        >
          <span className="grid *:col-start-1 *:row-start-1 *:transition-[opacity,scale] *:duration-150 *:ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:*:transition-opacity">
            <PlayIcon className={cn('size-10', isPlaying && 'scale-75 opacity-0')} />
            <PauseIcon className={cn('size-10', !isPlaying && 'scale-75 opacity-0')} />
          </span>
        </Button>
        <Button
          aria-label={onNext ? 'Next track' : 'Forward 10 seconds'}
          disabled={!onNext && !onChange}
          onClick={() => (onNext ? onNext() : onChange?.(Math.min(duration, currentTime + 10)))}
          className="size-11 rounded-md text-muted-foreground hover:bg-foreground/10 hover:text-muted-foreground"
          size="icon"
          variant="ghost"
        >
          <NextIcon className="size-8" />
        </Button>
        {onOutput ? (
          <Button
            aria-label="Audio output"
            onClick={onOutput}
            className="absolute right-0 size-8 rounded-md text-muted-foreground hover:bg-foreground/10 hover:text-foreground"
            size="icon"
            variant="ghost"
          >
            <AirbudsIcon className="size-6" />
          </Button>
        ) : (
          <AirbudsIcon
            aria-hidden="true"
            className="absolute right-0 size-6 text-muted-foreground"
          />
        )}
      </div>
    </section>
  )
}


export { AudioPlayer }
export type { AudioPlayerProps }


Props

Audio Player props
PropTypeDefaultDescription
currentTimenumber= —Controls the playback position in seconds.
durationnumber= —Defines the full audio duration.
onToggle() => void= —Toggles playback.
artworkUrlstring= —Displays cover art beside the track details.
onPrevious() => void= —Selects the previous track; otherwise rewinds 10 seconds using onChange.
onNext() => void= —Selects the next track; otherwise advances 10 seconds using onChange.
onOutput() => void= —Opens your audio-output picker when provided.
onChange(time: number) => void= —Seeks to a new playback position.