ComponentsSong Player Notch

Interactive Component

Song Player Notch

A sleek, iPhone-inspired music player that floats at the top of your screen like a dynamic island notch. Features an expandable mini-player with animated waveform visualization, full playback controls, progress seeking, and volume adjustment.

Installation

npx shadcn@latest add https://atomixui.mihircodes.in/registry/song-player

How to use

'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import { SongPlayer } from '@/components/atomixui/song-player';

const DEFAULT_THUMBNAIL = '/images/starboy.jpg';

function extractVideoId(url: string): string | null {
    const patterns = [
        /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/,
        /youtube\.com\/shorts\/([^&\n?#]+)/,
    ];
    for (const pattern of patterns) {
        const match = url.match(pattern);
        if (match) return match[1];
    }
    return null;
}

export default function SongPlayerDemo() {
    const [inputUrl, setInputUrl] = useState('');
    const [error, setError] = useState('');

    const [videoId, setVideoId] = useState<string | null>(null);
    const [title, setTitle] = useState('No song playing');
    const [artist, setArtist] = useState('Insert a YouTube link below');
    const [thumbnail, setThumbnail] = useState(DEFAULT_THUMBNAIL);
    const [isPlaying, setIsPlaying] = useState(false);
    const [currentTime, setCurrentTime] = useState(0);
    const [duration, setDuration] = useState(0);
    const [volume, setVolume] = useState(100);

    const iframeRef = useRef<HTMLIFrameElement>(null);

    const currentTimeRef = useRef(0);
    const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);

    useEffect(() => {
        currentTimeRef.current = currentTime;
    }, [currentTime]);

    useEffect(() => {
        const handleMessage = (e: MessageEvent) => {
            if (typeof e.data !== 'string') return;
            try {
                const data = JSON.parse(e.data);

                if (data.event === 'infoDelivery' && data.info) {
                    if (typeof data.info.currentTime === 'number') {
                        setCurrentTime(data.info.currentTime);
                    }
                    if (typeof data.info.duration === 'number' && data.info.duration > 0) {
                        setDuration(data.info.duration);
                    }
                    if (typeof data.info.playerState === 'number') {
                        setIsPlaying(data.info.playerState === 1);
                    }
                }

                if (data.event === 'onStateChange') {
                    setIsPlaying(data.info === 1);
                    if (data.info === 0) {
                        setCurrentTime(0);
                        setIsPlaying(false);
                    }
                }
            } catch {}
        };

        window.addEventListener('message', handleMessage);
        return () => window.removeEventListener('message', handleMessage);
    }, []);

    useEffect(() => {
        if (pollingRef.current) clearInterval(pollingRef.current);

        if (videoId && isPlaying) {
            pollingRef.current = setInterval(() => {
                iframeRef.current?.contentWindow?.postMessage(JSON.stringify({ event: 'listening' }), '*');
            }, 500);
        }

        return () => {
            if (pollingRef.current) clearInterval(pollingRef.current);
        };
    }, [videoId, isPlaying]);

    const sendCommand = useCallback((func: string, args: unknown[] = []) => {
        iframeRef.current?.contentWindow?.postMessage(JSON.stringify({ event: 'command', func, args }), '*');
    }, []);

    const togglePlay = useCallback(() => {
        if (!videoId) return;
        if (isPlaying) {
            sendCommand('pauseVideo');
            setIsPlaying(false);
        } else {
            sendCommand('playVideo');
            setIsPlaying(true);
        }
    }, [isPlaying, videoId, sendCommand]);

    const seekForward = useCallback(() => {
        const newTime = currentTimeRef.current + 10;
        sendCommand('seekTo', [newTime, true]);
        setCurrentTime(newTime);
    }, [sendCommand]);

    const seekBackward = useCallback(() => {
        const newTime = Math.max(0, currentTimeRef.current - 10);
        sendCommand('seekTo', [newTime, true]);
        setCurrentTime(newTime);
    }, [sendCommand]);

    const seekTo = useCallback(
        (time: number) => {
            const newTime = Math.max(0, Math.min(time, duration));
            sendCommand('seekTo', [newTime, true]);
            setCurrentTime(newTime);
        },
        [sendCommand, duration],
    );

    useEffect(() => {
        if (videoId) {
            sendCommand('setVolume', [volume]);
        }
    }, [videoId, volume, sendCommand]);

    const handleLoad = async () => {
        setError('');
        const id = extractVideoId(inputUrl.trim());

        if (!id) {
            setError('Invalid YouTube URL. Try youtube.com/watch?v=... or youtu.be/...');
            return;
        }
        try {
            const res = await fetch(
                `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${id}&format=json`,
            );
            if (!res.ok) throw new Error('Could not fetch video info');
            const data = await res.json();
            setTitle(data.title || 'Unknown Title');
            setArtist(data.author_name || 'Unknown Artist');
            setThumbnail(`https://img.youtube.com/vi/${id}/mqdefault.jpg`);
        } catch {
            setTitle('Unknown Title');
            setArtist('Unknown Artist');
            setThumbnail(`https://img.youtube.com/vi/${id}/mqdefault.jpg`);
        }

        setVideoId(id);
        setCurrentTime(0);
        setDuration(0);
        setIsPlaying(true);
        setInputUrl('');
    };

    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === 'Enter') handleLoad();
    };

    const iframeUrl = videoId
        ? `https://www.youtube.com/embed/${videoId}?enablejsapi=1&autoplay=1&origin=${
              typeof window !== 'undefined' ? encodeURIComponent(window.location.origin) : ''
          }`
        : null;

    return (
        <div className="relative w-full min-h-screen flex flex-col items-center justify-center gap-6">
            <SongPlayer
                title={title}
                artist={artist}
                thumbnail={thumbnail}
                isPlaying={isPlaying}
                videoId={videoId}
                currentTime={currentTime}
                duration={duration}
                volume={volume}
                togglePlay={togglePlay}
                seekForward={seekForward}
                seekBackward={seekBackward}
                seekTo={seekTo}
                setVolume={setVolume}
            />

            <div className="flex flex-col gap-3 w-full max-w-md px-4">
                <div className="w-full">
                    <div className="bg-linear-to-b from-[#282727] to-[#1B1B1B] px-4 py-2 border border-black rounded-lg shadow-[0px_0.75px_0px_0px_rgba(255,252,252,0.3)_inset,0px_1px_5px_0px_rgba(0,0,0,0.75)]">
                        <input
                            type="text"
                            value={inputUrl}
                            onChange={(e) => {
                                setInputUrl(e.target.value);
                                setError('');
                            }}
                            onKeyDown={handleKeyDown}
                            placeholder="Paste a YouTube URL..."
                            className="w-full text-white placeholder:text-white/30 text-xs outline-none transition-all duration-200"
                        />
                    </div>
                </div>

                {error && (
                    <p className="text-red-400 text-xs pl-1">
                        <span className="font-medium">Error</span>: {error}
                    </p>
                )}

                {!videoId && (
                    <p className="text-white/20 text-xs text-center">
                        Supports youtube.com/watch · youtu.be · youtube.com/shorts
                    </p>
                )}
            </div>

            {iframeUrl && (
                <iframe
                    ref={iframeRef}
                    src={iframeUrl}
                    className="sr-only pointer-events-none"
                    allow="autoplay; encrypted-media"
                    title="YouTube player"
                />
            )}
        </div>
    );
}

Supports youtube.com/watch · youtu.be · youtube.com/shorts