Skip to main content

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>