Media
Recording Controls code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/recording-controls.blade.php
@props([
'state' => 'idle',
'elapsed' => 0,
'title' => 'Recording',
'description' => null,
'player' => 'default',
])
@php
/*
| Camera/mic recording transport: idle → recording → paused → stopped.
| Dispatches recording-start / recording-pause / recording-resume /
| recording-stop / recording-toggle-mute.
|
| Usage:
| <x-ui.recording-controls />
*/
$initialState = in_array($state, ['idle', 'recording', 'paused', 'stopped'], true) ? $state : 'idle';
@endphp
<div
x-data="{
player: @js((string) $player),
state: @js($initialState),
elapsed: @js(max(0, (float) $elapsed)),
muted: false,
timer: null,
get isLive() { return this.state === 'recording'; },
get canPause() { return this.state === 'recording'; },
get canResume() { return this.state === 'paused'; },
formatTime(seconds) {
const total = Math.max(0, Math.floor(Number(seconds) || 0));
const m = String(Math.floor(total / 60)).padStart(2, '0');
const s = String(total % 60).padStart(2, '0');
return `${m}:${s}`;
},
clearTimer() {
if (this.timer) { clearInterval(this.timer); this.timer = null; }
},
startTicker() {
this.clearTimer();
this.timer = setInterval(() => { this.elapsed += 1; }, 1000);
},
start() {
this.state = 'recording';
this.startTicker();
this.$dispatch('recording-start', { player: this.player });
},
pause() {
this.state = 'paused';
this.clearTimer();
this.$dispatch('recording-pause', { player: this.player });
},
resume() {
this.state = 'recording';
this.startTicker();
this.$dispatch('recording-resume', { player: this.player });
},
stop() {
this.state = 'stopped';
this.clearTimer();
this.$dispatch('recording-stop', { player: this.player, elapsed: this.elapsed });
},
reset() {
this.state = 'idle';
this.elapsed = 0;
this.clearTimer();
},
toggleMute() {
this.muted = ! this.muted;
this.$dispatch('recording-toggle-mute', { player: this.player, muted: this.muted });
},
destroy() { this.clearTimer(); },
}"
{{ $attributes->merge(['class' => 'min-w-0 max-w-full rounded-2xl border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-800 dark:bg-gray-900 sm:p-4']) }}
>
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{{ $title }}</h3>
<span
class="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide"
:class="{
'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300': state === 'idle' || state === 'stopped',
'bg-red-50 text-red-700 dark:bg-red-950/40 dark:text-red-300': state === 'recording',
'bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300': state === 'paused',
}"
>
<span
class="h-1.5 w-1.5 rounded-full"
:class="state === 'recording' ? 'animate-pulse bg-red-500' : 'bg-current'"
></span>
<span x-text="state"></span>
</span>
</div>
<p class="mt-1 text-sm leading-5 text-gray-500 dark:text-gray-400">
{{ $description ?? 'Capture camera or microphone input for your media library.' }}
</p>
</div>
<p class="font-mono text-lg font-semibold tabular-nums text-gray-900 dark:text-white sm:text-xl" x-text="formatTime(elapsed)">00:00</p>
</div>
<div class="mt-4 flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<template x-if="state === 'idle' || state === 'stopped'">
<button
type="button"
@click="state === 'stopped' ? (reset(), start()) : start()"
class="inline-flex h-11 w-full items-center justify-center gap-2 rounded-xl bg-red-600 px-4 text-sm font-semibold text-white shadow-sm transition hover:bg-red-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 sm:h-10 sm:w-auto"
>
<x-ui.icon name="record" weight="fill" size="sm" />
<span x-text="state === 'stopped' ? 'Record again' : 'Start recording'"></span>
</button>
</template>
<template x-if="canPause">
<button
type="button"
@click="pause()"
class="inline-flex h-11 w-full items-center justify-center gap-2 rounded-xl border border-gray-200 bg-white px-4 text-sm font-semibold text-gray-700 shadow-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-200 dark:hover:bg-gray-800 sm:h-10 sm:w-auto"
>
<x-ui.icon name="pause" weight="fill" size="sm" />
Pause
</button>
</template>
<template x-if="canResume">
<button
type="button"
@click="resume()"
class="inline-flex h-11 w-full items-center justify-center gap-2 rounded-xl bg-brand-600 px-4 text-sm font-semibold text-white shadow-sm transition hover:bg-brand-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 sm:h-10 sm:w-auto"
>
<x-ui.icon name="play" weight="fill" size="sm" />
Resume
</button>
</template>
<template x-if="state === 'recording' || state === 'paused'">
<button
type="button"
@click="stop()"
class="inline-flex h-11 w-full items-center justify-center gap-2 rounded-xl border border-gray-200 bg-white px-4 text-sm font-semibold text-gray-700 shadow-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-200 dark:hover:bg-gray-800 sm:h-10 sm:w-auto"
>
<x-ui.icon name="stop" weight="fill" size="sm" />
Stop
</button>
</template>
<button
type="button"
@click="toggleMute()"
class="inline-flex h-11 w-full items-center justify-center gap-2 rounded-xl border border-gray-200 bg-white px-4 text-sm font-semibold text-gray-700 shadow-sm transition hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-200 dark:hover:bg-gray-800 sm:h-10 sm:w-auto"
:aria-pressed="muted.toString()"
>
<x-ui.icon x-show="! muted" name="microphone" weight="fill" size="sm" />
<x-ui.icon x-show="muted" x-cloak name="microphone-slash" weight="fill" size="sm" />
<span x-text="muted ? 'Unmute mic' : 'Mute mic'"></span>
</button>
</div>
</div>