mira, tengo esto:

Folder Structure:

src/
├─ theme/
│   theme/
│   └─ variables.css
├─ App.test.tsx
├─ lib/
│   lib/
│   └─ utils.ts
├─ pages/
│   pages/
│   ├─ Home.css
│   └─ Home.tsx
├─ setupTests.ts
├─ ui/
│   ui/
│   ├─ button.tsx
│   ├─ icon/
│   │   icon/
│   │   ├─ PlayingIcon.css
│   │   └─ PlayingIcon.tsx
│   ├─ slider.tsx
│   └─ input.tsx
├─ components/
│   components/
│   ├─ SongList.tsx
│   ├─ PlaylistInfo.tsx
│   ├─ SongRow.tsx
│   ├─ spotify/
│   │   spotify/
│   │   └─ SpotifyLink.tsx
│   ├─ ExploreContainer.css
│   ├─ Navbar.tsx
│   ├─ PlaybackBar.tsx
│   ├─ ExploreContainer.tsx
│   ├─ PlaybackControls.tsx
│   ├─ FullScreenPlayer.tsx
│   └─ NowPlayingBar.tsx
├─ main.tsx
├─ App.tsx
├─ types/
│   types/
│   └─ youtube.d.ts
├─ data/
│   data/
│   └─ songs.ts
└─ vite-env.d.ts


File Contents:

File: src/theme/variables.css
=============================
/* For information on how to create your own theme, please see:
http://ionicframework.com/docs/theming/ */



File: src/App.test.tsx
======================
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

test('renders without crashing', () => {
  const { baseElement } = render(<App />);
  expect(baseElement).toBeDefined();
});



File: src/lib/utils.ts
======================

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}



File: src/pages/Home.css
========================
body {
    font-family: sans-serif;
    -webkit-font-smoothing: auto;
    -moz-font-smoothing: auto;
    -moz-osx-font-smoothing: grayscale;
    font-smoothing: auto;
    text-rendering: optimizeLegibility;
    font-smooth: always;
    -webkit-tap-highlight-color: transparent;
    -webkit-touch-callout: none;
  }
  
  h1 {
    font-size: 1.5rem;
  }
  


File: src/pages/Home.tsx
========================
import React, {
  useState,
  useEffect,
  useRef,
  useMemo,
  useCallback,
} from "react";
import YouTube, { YouTubeProps, YouTubePlayer } from "react-youtube";
import Navbar from "../components/Navbar";
import PlaylistInfo from "../components/PlaylistInfo";
import PlaybackControls from "../components/PlaybackControls";
import SongList from "../components/SongList";
import NowPlayingBar from "../components/NowPlayingBar";
import FullScreenPlayer from "../components/FullScreenPlayer";
import PlaybackBar from "../components/PlaybackBar";
import SpotifyLink from "../components/spotify/SpotifyLink";
import { songs as initialSongs, Song } from "../data/songs";
import { IonPage, IonContent, IonHeader, IonToolbar, IonTitle } from '@ionic/react';
import './Home.css';

const formatTotalDuration = (totalSeconds: number): string => {
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  return `${hours} h ${minutes} min`;
};

const Home: React.FC = () => {
  const [currentSong, setCurrentSong] = useState<Song | null>(null);
  const [isPlaying, setIsPlaying] = useState<boolean>(false);
  const [isMobile, setIsMobile] = useState<boolean>(window.innerWidth < 1024);
  const [showNavbar, setShowNavbar] = useState<boolean>(true);
  const [isFullScreenPlayer, setIsFullScreenPlayer] = useState<boolean>(false);

  const playerRef = useRef<YouTubePlayer | null>(null);

  const [currentTime, setCurrentTime] = useState<number>(0);
  const [duration, setDuration] = useState<number>(0);

  const [volume, setVolume] = useState<number>(100);

  const lastScrollY = useRef<number>(0);

  const [searchValue, setSearchValue] = useState<string>("");

  const [currentPage, setCurrentPage] = useState<number>(1);
  const songsPerPage = 100;

  const handleSearchChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setSearchValue(e.target.value);
      setCurrentPage(1);
    },
    []
  );

  const handleClearSearch = useCallback(() => {
    setSearchValue("");
    setCurrentPage(1);
  }, []);

  const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");

  const sortAscending = useCallback(() => {
    setSortOrder("asc");
  }, []);

  const sortDescending = useCallback(() => {
    setSortOrder("desc");
  }, []);

  const [isShuffle, setIsShuffle] = useState<boolean>(false);

  const filteredSongs = useMemo(() => {
    return searchValue
      ? initialSongs.filter(
          (song) =>
            song.title.toLowerCase().includes(searchValue.toLowerCase()) ||
            song.artist.toLowerCase().includes(searchValue.toLowerCase())
        )
      : initialSongs;
  }, [searchValue]);

  const sortedSongs = useMemo(() => {
    const sorted = [...filteredSongs];
    if (sortOrder === "asc") {
      sorted.reverse();
    }
    return sorted;
  }, [filteredSongs, sortOrder]);

  const totalDurationInSeconds = useMemo(
    () => sortedSongs.reduce((sum, song) => sum + (song.duration || 0), 0),
    [sortedSongs]
  );
  const totalDuration = useMemo(
    () => formatTotalDuration(totalDurationInSeconds),
    [totalDurationInSeconds]
  );

  const playlist = useMemo(
    () => ({
      name: "Time Free",
      description: "Relajao",
      owner: "Johan Morales",
      songsCount: sortedSongs.length,
      totalDuration: totalDuration,
      coverImage: "https://i.postimg.cc/BQ8t88fC/imagen-blessd-azul.jpg",
      spotifyUrl: "https://open.spotify.com/playlist/39fLwIyFpofxhYSCyyYKpC",
    }),
    [sortedSongs.length, totalDuration]
  );

  useEffect(() => {
    const handleResize = () => {
      setIsMobile(window.innerWidth < 1024);
    };

    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  useEffect(() => {
    const handleScroll = () => {
      if (!isFullScreenPlayer) {
        if (window.scrollY > lastScrollY.current && window.scrollY > 100) {
          setShowNavbar(false);
        } else {
          setShowNavbar(true);
        }
        lastScrollY.current = window.scrollY;
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [isFullScreenPlayer]);

  const getCurrentIndex = useCallback((): number => {
    if (!currentSong?.youtubeId) return -1;
    return sortedSongs.findIndex(
      (song) => song.youtubeId === currentSong.youtubeId
    );
  }, [currentSong, sortedSongs]);

  useEffect(() => {
    if (playerRef.current && currentSong?.youtubeId) {
      try {
        playerRef.current.loadVideoById(currentSong.youtubeId);
        setCurrentTime(0);
        setDuration(0);
      } catch (error) {
        console.error("Error al cargar el video:", error);
      }
    }
  }, [currentSong]);

  // Controlar la reproducción/pausa del video
  useEffect(() => {
    if (playerRef.current) {
      if (isPlaying) {
        try {
          playerRef.current.playVideo();
        } catch (error) {
          console.error("Error intentando reproducir el video:", error);
        }
      } else {
        playerRef.current.pauseVideo();
      }
    }
  }, [isPlaying]);

  useEffect(() => {
    let intervalId: number | null = null;

    if (playerRef.current && isPlaying) {
      intervalId = window.setInterval(() => {
        if (playerRef.current) {
          const current = playerRef.current.getCurrentTime();
          const dura = playerRef.current.getDuration();
          setCurrentTime(current);
          setDuration(dura);
        }
      }, 1000);
    }

    return () => {
      if (intervalId !== null) {
        window.clearInterval(intervalId);
      }
    };
  }, [isPlaying]);

  const handlePlayPause = useCallback(() => {
    if (isPlaying) {
      playerRef.current?.pauseVideo();
      setIsPlaying(false);
    } else {
      if (currentSong?.youtubeId) {
        try {
          playerRef.current?.playVideo();
          setIsPlaying(true);
        } catch (error) {
          console.error("Error intentando reproducir el video:", error);
        }
      }
    }
  }, [isPlaying, currentSong]);

  const handleNextSong = useCallback(() => {
    if (!currentSong) return;
    let nextIndex: number;

    if (isShuffle) {
      do {
        nextIndex = Math.floor(Math.random() * sortedSongs.length);
      } while (nextIndex === getCurrentIndex());
    } else {
      const currentIndex = getCurrentIndex();
      nextIndex = (currentIndex + 1) % sortedSongs.length;
    }

    const nextSong = sortedSongs[nextIndex];
    setCurrentSong(nextSong);
    setIsPlaying(true);
  }, [currentSong, isShuffle, sortedSongs, getCurrentIndex]);

  const handlePreviousSong = useCallback(() => {
    if (!currentSong) return;
    let previousIndex: number;

    if (isShuffle) {
      do {
        previousIndex = Math.floor(Math.random() * sortedSongs.length);
      } while (previousIndex === getCurrentIndex());
    } else {
      const currentIndex = getCurrentIndex();
      previousIndex =
        (currentIndex - 1 + sortedSongs.length) % sortedSongs.length;
    }

    const previousSong = sortedSongs[previousIndex];
    setCurrentSong(previousSong);
    setIsPlaying(true);
  }, [currentSong, isShuffle, sortedSongs, getCurrentIndex]);

  const onPlayerReady: YouTubeProps["onReady"] = useCallback(
    (event: YT.PlayerEvent) => {
      playerRef.current = event.target;
      setDuration(event.target.getDuration());

      event.target.setVolume(volume);

      if (isPlaying) {
        try {
          event.target.playVideo();
        } catch (error) {
          console.error("Error intentando reproducir el video:", error);
        }
      }
    },
    [isPlaying, volume]
  );

  const onPlayerEnd: YouTubeProps["onEnd"] = useCallback(() => {
    handleNextSong();
  }, [handleNextSong]);

  const onPlayerError: YouTubeProps["onError"] = useCallback(
    (event: YT.OnErrorEvent) => {
      console.error("Error del reproductor de YouTube:", event.data);
    },
    []
  );

  const onPlayerStateChange: YouTubeProps["onStateChange"] = useCallback(
    (event: YT.OnStateChangeEvent) => {
      if (window.YT && event.data === YT.PlayerState.ENDED) {
        handleNextSong();
      } else if (
        window.YT &&
        (event.data === YT.PlayerState.CUED ||
          event.data === YT.PlayerState.BUFFERING) &&
        isPlaying
      ) {
        try {
          playerRef.current?.playVideo();
        } catch (error) {
          console.error("Error intentando reproducir el video:", error);
        }
      }
    },
    [handleNextSong, isPlaying]
  );

  const opts: YouTubeProps["opts"] = useMemo(
    () => ({
      height: "0",
      width: "0",
      playerVars: {
        autoplay: 0,
        controls: 0,
        disablekb: 1,
        fs: 0,
        modestbranding: 1,
        rel: 0,
      },
    }),
    []
  );

  const handleProgressChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const newTime = Number(e.target.value);
      if (playerRef.current) {
        try {
          playerRef.current.seekTo(newTime, true);
          setCurrentTime(newTime);
        } catch (error) {
          console.error("Error al cambiar el progreso del video:", error);
        }
      }
    },
    []
  );

  const handleVolumeChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const newVolume = Number(e.target.value);
      setVolume(newVolume);
      if (playerRef.current) {
        try {
          playerRef.current.setVolume(newVolume);
        } catch (error) {
          console.error("Error al cambiar el volumen:", error);
        }
      }
    },
    []
  );

  const toggleMute = useCallback(() => {
    if (playerRef.current) {
      try {
        if (playerRef.current.isMuted()) {
          playerRef.current.unMute();
          setVolume(playerRef.current.getVolume());
        } else {
          playerRef.current.mute();
          setVolume(0);
        }
      } catch (error) {
        console.error("Error al alternar mute/unmute:", error);
      }
    }
  }, []);

  const toggleFullScreenPlayer = useCallback(() => {
    setIsFullScreenPlayer((prev) => !prev);
    if (!isFullScreenPlayer) {
      setShowNavbar(false);
    } else {
      setShowNavbar(true);
    }
  }, [isFullScreenPlayer]);

  const ensurePlayback = useCallback(() => {
    if (isPlaying && playerRef.current) {
      try {
        playerRef.current.playVideo();
        console.log("Reproducción iniciada automáticamente.");
      } catch (error) {
        console.error("Error al intentar reproducir automáticamente:", error);
      }
    }
  }, [isPlaying]);

  useEffect(() => {
    if (currentSong && isPlaying) {
      const timeoutId = setTimeout(() => {
        ensurePlayback();
      }, 1000);

      return () => clearTimeout(timeoutId);
    }
  }, [currentSong, isPlaying, ensurePlayback]);

  const handleSongSelect = useCallback((song: Song) => {
    setCurrentSong(song);
    setIsPlaying(true);
  }, []);

  const handleShuffle = useCallback(() => {
    setIsShuffle((prev) => !prev);
  }, []);

  const handleRepeat = useCallback(() => {
    if (currentSong) {
      setIsPlaying(false);
      setCurrentSong(currentSong);
      setIsPlaying(true);
    }
  }, [currentSong]);

  const indexOfLastSong = currentPage * songsPerPage;
  const indexOfFirstSong = indexOfLastSong - songsPerPage;
  const currentSongs = useMemo(
    () => sortedSongs.slice(indexOfFirstSong, indexOfLastSong),
    [sortedSongs, indexOfFirstSong, indexOfLastSong]
  );

  const totalPages = useMemo(
    () => Math.ceil(sortedSongs.length / songsPerPage),
    [sortedSongs.length, songsPerPage]
  );

  const handleNextPage = useCallback(() => {
    setCurrentPage((prev) => Math.min(prev + 1, totalPages));
  }, [totalPages]);

  const handlePrevPage = useCallback(() => {
    setCurrentPage((prev) => Math.max(prev - 1, 1));
  }, []);

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Mi Reproductor de Música</IonTitle>
        </IonToolbar>
        {showNavbar && (
          <Navbar
            searchValue={searchValue}
            onSearchChange={handleSearchChange}
            onClear={handleClearSearch}
            onSortAscending={sortAscending}
            onSortDescending={sortDescending}
          />
        )}
      </IonHeader>
      <IonContent>
        {currentSong?.youtubeId && (
          <YouTube
            videoId={currentSong.youtubeId}
            opts={opts}
            onReady={onPlayerReady}
            onEnd={onPlayerEnd}
            onStateChange={onPlayerStateChange}
            onError={onPlayerError}
          />
        )}

        <div
          className={`pt-4 flex-1 overflow-y-auto bg-gradient-to-b from-blue-900 to-gray-900 ${
            isFullScreenPlayer ? "hidden" : ""
          } pb-24`}
        >
          <PlaylistInfo playlist={playlist} />

          <SpotifyLink
            url={playlist.spotifyUrl}
            text="Esta es mi playlist original"
            size={24}
          />

          {currentSong && (
            <div className="mt-6 px-4 sm:px-6 lg:px-8">
              <PlaybackControls
                isPlaying={isPlaying}
                onPlayPause={handlePlayPause}
                onAddToQueue={() => {
                  console.log("Agregar a la cola");
                }}
                onMoreOptions={() => {
                  console.log("Más opciones");
                }}
              />
            </div>
          )}

          <div className="mt-4 px-4 sm:px-6 lg:px-8">
            <SongList
              songs={currentSongs}
              currentSong={currentSong}
              onSongSelect={handleSongSelect}
            />
          </div>

          <div className="flex justify-between items-center mt-6 px-4 sm:px-6 lg:px-8">
            <button
              onClick={handlePrevPage}
              disabled={currentPage === 1}
              className={`px-4 py-2 bg-gray-700 text-white rounded ${
                currentPage === 1
                  ? "opacity-50 cursor-not-allowed"
                  : "hover:bg-gray-600"
              }`}
            >
              Anterior
            </button>
            <span className="text-sm text-gray-400">
              Página {currentPage} de {totalPages}
            </span>
            <button
              onClick={handleNextPage}
              disabled={currentPage === totalPages}
              className={`px-4 py-2 bg-blue-700 text-white rounded ${
                currentPage === totalPages
                  ? "opacity-50 cursor-not-allowed"
                  : "hover:bg-gray-600"
              }`}
            >
              Siguiente
            </button>
          </div>
        </div>

        {isMobile && currentSong && !isFullScreenPlayer && (
          <NowPlayingBar
            currentSong={currentSong}
            isPlaying={isPlaying}
            onPlayPause={(e) => {
              e.preventDefault();
              handlePlayPause();
            }}
            onToggleFullScreen={toggleFullScreenPlayer}
          />
        )}

        {isMobile && isFullScreenPlayer && currentSong && (
          <FullScreenPlayer
            currentSong={currentSong}
            isPlaying={isPlaying}
            onPlayPause={handlePlayPause}
            onNextSong={handleNextSong}
            onPreviousSong={handlePreviousSong}
            onShuffle={handleShuffle}
            onRepeat={handleRepeat}
            onVolumeChange={handleVolumeChange}
            volume={volume}
            onProgressChange={(values) => {
              const newTime = (values[0] / 100) * duration;
              if (playerRef.current) {
                try {
                  playerRef.current.seekTo(newTime, true);
                  setCurrentTime(newTime);
                } catch (error) {
                  console.error("Error al cambiar el progreso del video:", error);
                }
              }
            }}
            currentTime={currentTime}
            duration={duration}
            onClose={toggleFullScreenPlayer}
            isShuffle={isShuffle}
          />
        )}

        {!isMobile && currentSong && (
          <PlaybackBar
            currentSong={currentSong}
            isPlaying={isPlaying}
            onPlayPause={handlePlayPause}
            onNextSong={handleNextSong}
            onPreviousSong={handlePreviousSong}
            onShuffle={handleShuffle}
            onRepeat={handleRepeat}
            onVolumeChange={handleVolumeChange}
            volume={volume}
            onFullScreen={toggleFullScreenPlayer}
            currentTime={currentTime}
            duration={duration}
            onProgressChange={handleProgressChange}
            isShuffle={isShuffle}
          />
        )}
      </IonContent>
    </IonPage>
  );
};

export default Home;


File: src/setupTests.ts
=======================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

// Mock matchmedia
window.matchMedia = window.matchMedia || function() {
  return {
      matches: false,
      addListener: function() {},
      removeListener: function() {}
  };
};



File: src/ui/button.tsx
=======================
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from '../lib/utils';

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300",
  {
    variants: {
      variant: {
        default: "bg-gray-900 text-gray-50 hover:bg-gray-900/90 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/90",
        destructive:
          "bg-red-500 text-gray-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90",
        outline:
          "border border-gray-200 bg-white hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:hover:bg-gray-800 dark:hover:text-gray-50",
        secondary:
          "bg-gray-100 text-gray-900 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80",
        ghost: "hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50",
        link: "text-gray-900 underline-offset-4 hover:underline dark:text-gray-50",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button";
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  }
);

Button.displayName = "Button";

export { Button, buttonVariants };


File: src/ui/icon/PlayingIcon.css
=================================
@keyframes pulse {
    0%,
    100% {
      transform: scaleY(1);
    }
    50% {
      transform: scaleY(1.5);
    }
  }
  
  .playing-icon {
    display: flex;
    align-items: flex-end;
    margin-right: 0.5rem;
  }
  
  .playing-icon div {
    width: 4px;
    height: 12px;
    background-color: #34d399;
    margin: 0 1px;
    animation: pulse 1s infinite;
  }
  
  .playing-icon div:nth-child(1) {
    animation-delay: 0s;
  }
  
  .playing-icon div:nth-child(2) {
    animation-delay: 0.2s;
  }
  
  .playing-icon div:nth-child(3) {
    animation-delay: 0.4s;
  }
  


File: src/ui/icon/PlayingIcon.tsx
=================================
import React from "react";
import "./PlayingIcon.css";

const PlayingIcon: React.FC = () => {
  return (
    <div className="playing-icon">
      <div></div>
      <div></div>
      <div></div>
    </div>
  );
};

export default PlayingIcon;



File: src/ui/slider.tsx
=======================
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";

import { cn } from "../lib/utils";

const Slider = React.forwardRef<
  React.ElementRef<typeof SliderPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
  <SliderPrimitive.Root
    ref={ref}
    className={cn(
      "relative flex w-full touch-none select-none items-center",
      className
    )}
    {...props}
  >
    <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
      <SliderPrimitive.Range className="absolute h-full bg-gray-900 dark:bg-gray-50" />
    </SliderPrimitive.Track>
    <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-gray-900 bg-white ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-gray-50 dark:bg-gray-950 dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300" />
  </SliderPrimitive.Root>
));

Slider.displayName = SliderPrimitive.Root.displayName;

export { Slider };


File: src/ui/input.tsx
======================
import * as React from "react"
import { cn } from "../lib/utils"

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-10 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-800 dark:bg-gray-950 dark:ring-offset-gray-950 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Input.displayName = "Input"

export { Input }



File: src/components/SongList.tsx
=================================
import React, { useCallback } from "react";
import SongRow from "./SongRow";

interface Song {
  title: string;
  artist: string;
  album: string;
  added: string;
  duration?: number;
  youtubeId?: string;
}

interface SongListProps {
  songs: Song[];
  currentSong: Song | null;
  onSongSelect: (song: Song) => void;
}

const SongList: React.FC<SongListProps> = ({
  songs,
  currentSong,
  onSongSelect,
}) => {
  const handleSelect = useCallback(
    (song: Song) => {
      onSongSelect(song);
    },
    [onSongSelect]
  );

  return (
    <div className="px-6">
      <table className="w-full">
        <thead>
          <tr className="text-left text-sm text-gray-400 border-b border-gray-500">
            <th className="hidden lg:table-cell pb-2">#</th>
            <th className="pb-2">Título</th>
            <th className="hidden lg:table-cell pb-2">Álbum</th>
            <th className="hidden lg:table-cell pb-2">
              Fecha en que se agregó
            </th>
            <th className="hidden lg:table-cell pb-2">Duración</th>
          </tr>
        </thead>
        <tbody>
          {songs.map((track, index) => (
            <SongRow
              key={
                track.youtubeId
                  ? `${track.youtubeId}-${index}`
                  : `index-${index}`
              }
              track={track}
              index={index}
              isCurrent={track.youtubeId === currentSong?.youtubeId}
              onSelect={handleSelect}
            />
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default React.memo(SongList);



File: src/components/PlaylistInfo.tsx
=====================================
import React from "react";

interface Playlist {
  name: string;
  description: string;
  owner: string;
  songsCount: number;
  totalDuration: string;
  coverImage: string;
}

interface PlaylistInfoProps {
  playlist: Playlist;
}

const PlaylistInfo: React.FC<PlaylistInfoProps> = ({ playlist }) => {
  const bullet = "\u2022";
  const isMobile = window.innerWidth < 768;

  return (
    <div className="px-6 mb-6 lg:flex lg:items-center">
      <div className={`${isMobile ? "mb-4" : ""}`}>
        <img
          src={playlist.coverImage || "/placeholder.svg"}
          alt="Playlist cover"
          className={`object-cover rounded-md ${
            isMobile
              ? "w-48 h-48 mx-auto" // Tamaño aumentado para móviles
              : "w-40 h-40 mr-0 lg:mr-6 mb-4 lg:mb-0" // Tamaño original para otros dispositivos
          }`}
        />
      </div>
      <div className="lg:flex-grow">
        <div>
          {!isMobile && (
            <p className="text-xs uppercase mb-1">Playlist pública</p>
          )}
          <h1 className="text-3xl lg:text-6xl font-bold mb-1">
            {playlist.name}
          </h1>
          <p className="text-sm mb-2">{playlist.description}</p>
          <p className="text-sm flex items-center">
            <img
              src="https://i.postimg.cc/PJPVDPZW/boyaca-yo.webp"
              alt="Propietario"
              className="w-6 h-6 rounded-full mr-2"
            />
            {playlist.owner} {bullet} 2M veces guardada {bullet}{" "}
            {playlist.songsCount} canciones {bullet} {playlist.totalDuration}
          </p>
        </div>
      </div>
    </div>
  );
};

export default PlaylistInfo;



File: src/components/SongRow.tsx
================================
import React, { memo } from "react";
import PlayingIcon from "../ui/icon/PlayingIcon";

interface Song {
  title: string;
  artist: string;
  album: string;
  added: string;
  duration?: number;
  youtubeId?: string;
}

interface SongRowProps {
  track: Song;
  index: number;
  isCurrent: boolean;
  onSelect: (song: Song) => void;
}

const getYouTubeThumbnail = (youtubeId: string): string =>
  `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;

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

const SongRow: React.FC<SongRowProps> = ({
  track,
  index,
  isCurrent,
  onSelect,
}) => (
  <tr
    key={track.youtubeId ? `${track.youtubeId}-${index}` : `index-${index}`}
    className={`hover:bg-gray-800 cursor-pointer ${
      isCurrent ? "bg-gray-700" : ""
    }`}
    onClick={() => {
      if (track.youtubeId) {
        onSelect(track);
      }
    }}
  >
    <td className="hidden lg:table-cell py-2">{index + 1}</td>
    <td className="py-2">
      <div className="flex items-center">
        {isCurrent && <PlayingIcon />}
        <img
          src={
            track.youtubeId
              ? getYouTubeThumbnail(track.youtubeId)
              : "/placeholder.svg"
          }
          alt="Video thumbnail"
          className="w-10 h-10 mr-4 object-cover rounded-md"
          loading="lazy"
        />
        <div>
          <p
            className={`font-medium ${
              isCurrent ? "text-green-500" : "text-white"
            }`}
          >
            {track.title}
          </p>
          <p className="text-sm text-gray-400">{track.artist}</p>
        </div>
      </div>
    </td>
    <td className="hidden lg:table-cell py-2 text-sm text-gray-400">
      {track.album}
    </td>
    <td className="hidden lg:table-cell py-2 text-sm text-gray-400">
      {track.added}
    </td>
    <td className="hidden lg:table-cell py-2 text-sm text-gray-400">
      {track.duration ? formatTime(track.duration) : "--:--"}
    </td>
  </tr>
);

export default memo(SongRow);



File: src/components/spotify/SpotifyLink.tsx
============================================
import React from "react";
import { FaSpotify } from "react-icons/fa";

interface SpotifyLinkProps {
  url: string;
  text: string;
  size?: number;
}

const SpotifyLink: React.FC<SpotifyLinkProps> = ({ url, text, size = 24 }) => {
  return (
    <div className="mt-6 px-4 sm:px-6 lg:px-8">
      <p className="text-gray-400 flex items-center justify-center sm:justify-start">
        <FaSpotify
          className="text-green-500 mr-2 hover:text-green-400 transition-colors duration-300"
          size={size}
          aria-hidden="true"
        />
        {text}:{" "}
        <a
          href={url}
          target="_blank"
          rel="noopener noreferrer"
          className="text-blue-500 underline ml-1 flex items-center hover:text-blue-400 transition-colors duration-300"
        >
          Abrir en Spotify
        </a>
      </p>
    </div>
  );
};

export default SpotifyLink;



File: src/components/ExploreContainer.css
=========================================
#container {
  text-align: center;
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
}

#container strong {
  font-size: 20px;
  line-height: 26px;
}

#container p {
  font-size: 16px;
  line-height: 22px;
  color: #8c8c8c;
  margin: 0;
}

#container a {
  text-decoration: none;
}


File: src/components/Navbar.tsx
===============================
import React, { useState } from "react";
import { Search, X } from "lucide-react";
import { BiSortAZ, BiSortZA } from "react-icons/bi";
import { Input } from "../ui/input";


interface NavbarProps {
  searchValue: string;
  onSearchChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  onClear: () => void;
  onSortAscending: () => void;
  onSortDescending: () => void;
}

const Navbar: React.FC<NavbarProps> = ({
  searchValue,
  onSearchChange,
  onClear,
  onSortAscending,
  onSortDescending,
}) => {
  const [isAscending, setIsAscending] = useState<boolean>(true);
  const [isLoading, setIsLoading] = useState<boolean>(false);

  const handleSort = async () => {
    setIsLoading(true);
    // Simular una operación de ordenación con un retraso de 500 ms
    await new Promise((resolve) => setTimeout(resolve, 500));
    if (isAscending) {
      onSortDescending();
    } else {
      onSortAscending();
    }
    setIsAscending(!isAscending);
    setIsLoading(false);
  };

  return (
    <div className="flex items-center p-4 bg-transparent fixed top-0 left-0 right-0 z-50 backdrop-blur-lg transition-transform duration-300">
      {/* Buscador */}
      <div className="flex-1 mx-4">
        <div className="relative">
          <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-600" />

          <Input
            type="text"
            placeholder="Buscar"
            value={searchValue}
            onChange={onSearchChange}
            className="pl-10 pr-10 py-2 w-full bg-gray-200 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-400 border border-gray-300 text-gray-800 placeholder-gray-500"
          />

          {searchValue && (
            <button
              type="button"
              onClick={onClear}
              className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-800 focus:outline-none"
              aria-label="Clear search"
            >
              <X className="h-5 w-5" />
            </button>
          )}
        </div>
      </div>

      {/* Botón de Ordenar */}
      <div className="relative mx-4">
        <button
          onClick={handleSort}
          disabled={isLoading}
          aria-label={`Sort ${isAscending ? "Descending" : "Ascending"}`}
          className="flex items-center gap-1 px-2 py-1 bg-gray-300 text-gray-800 rounded-md
            hover:bg-gray-400 focus:ring-2 focus:ring-gray-400 focus:outline-none
            transform transition-transform duration-200 ease-in-out hover:scale-105
            disabled:opacity-50 disabled:cursor-not-allowed"
        >
          {isLoading ? (
            <div className="animate-spin rounded-full h-4 w-4 border-2 border-gray-800 border-t-transparent"></div>
          ) : (
            <>
              {isAscending ? <BiSortAZ size={16} /> : <BiSortZA size={16} />}
              <span className="text-sm">Ordenar</span>
            </>
          )}
        </button>
      </div>
    </div>
  );
};

export default Navbar;



File: src/components/PlaybackBar.tsx
====================================
import React from "react";
import {
  Shuffle,
  SkipBack,
  Play,
  Pause,
  SkipForward,
  Repeat,
  VolumeX,
  Volume1,
  Volume2,
  Maximize2,
} from "lucide-react";
import { Button } from "../ui/button";

interface PlaybackBarProps {
  currentSong: {
    title: string;
    artist: string;
    youtubeId?: string;
  };
  isPlaying: boolean;
  onPlayPause: () => void;
  onNextSong: () => void;
  onPreviousSong: () => void;
  onShuffle: () => void;
  onRepeat: () => void;
  onVolumeChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  volume: number;
  onFullScreen: () => void;
  currentTime: number;
  duration: number;
  onProgressChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  isShuffle: boolean;
}

const getYouTubeThumbnail = (youtubeId: string): string =>
  `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;

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

const PlaybackBar: React.FC<PlaybackBarProps> = ({
  currentSong,
  isPlaying,
  onPlayPause,
  onNextSong,
  onPreviousSong,
  onShuffle,
  onRepeat,
  onVolumeChange,
  volume,
  onFullScreen,
  currentTime,
  duration,
  onProgressChange,
  isShuffle,
}) => {
  const progressPercentage = duration ? (currentTime / duration) * 100 : 0;

  const progressStyle = {
    background: `linear-gradient(to right, red 0%, red ${progressPercentage}%, #4B5563 ${progressPercentage}%, #4B5563 100%)`,
  };

  return (
    <div className="flex flex-col lg:flex-row items-center justify-between p-4 bg-gray-900 border-t border-gray-800 fixed bottom-0 left-0 right-0 transition-margin duration-300 ease-in-out h-24 z-50">
      <div className="flex items-center mb-4 lg:mb-0 w-full lg:w-1/3 justify-center lg:justify-start">
        <img
          src={
            currentSong.youtubeId
              ? getYouTubeThumbnail(currentSong.youtubeId)
              : "/placeholder.svg"
          }
          alt="Album cover"
          className="w-10 h-10 mr-4 object-cover rounded-md"
        />
        <div>
          <p
            className={`font-medium ${
              currentSong.youtubeId === currentSong?.youtubeId
                ? "text-green-500"
                : "text-white"
            }`}
          >
            {currentSong.title}
          </p>
          <p className="text-sm text-gray-400">{currentSong.artist}</p>
        </div>
      </div>

      <div className="flex flex-col items-center w-full lg:w-1/3">
        <div className="flex items-center space-x-4 mb-2">
          <Button
            variant="ghost"
            size="icon"
            className={`text-gray-400 hover:text-white ${
              isShuffle ? "text-green-500" : ""
            }`}
            aria-label="Shuffle"
            onClick={onShuffle}
          >
            <Shuffle className="w-5 h-5" />
          </Button>
          <Button
            variant="ghost"
            size="icon"
            className="text-gray-400 hover:text-white"
            onClick={onPreviousSong}
            aria-label="Anterior"
          >
            <SkipBack className="w-5 h-5" />
          </Button>
          <Button
            size="icon"
            className={`rounded-full bg-white hover:bg-gray-200`}
            onClick={onPlayPause}
            aria-label={isPlaying ? "Pausar" : "Reproducir"}
          >
            {isPlaying ? (
              <Pause className="w-6 h-6 text-black" />
            ) : (
              <Play className="w-6 h-6 text-black" />
            )}
          </Button>
          <Button
            variant="ghost"
            size="icon"
            className="text-gray-400 hover:text-white"
            onClick={onNextSong}
            aria-label="Siguiente"
          >
            <SkipForward className="w-5 h-5" />
          </Button>
          <Button
            variant="ghost"
            size="icon"
            className="text-gray-400 hover:text-white"
            aria-label="Repetir"
            onClick={onRepeat}
          >
            <Repeat className="w-5 h-5" />
          </Button>
        </div>
        <div className="flex items-center w-full space-x-2">
          <span className="text-xs text-gray-400">
            {formatTime(currentTime)}
          </span>
          <input
            type="range"
            min={0}
            max={duration}
            value={currentTime}
            onChange={onProgressChange}
            style={progressStyle}
            className="flex-1 h-1 bg-gray-600 rounded-full cursor-pointer appearance-none focus:outline-none"
          />
          <span className="text-xs text-gray-400">{formatTime(duration)}</span>
        </div>
      </div>

      <div className="flex items-center justify-end w-full lg:w-1/3 mt-4 lg:mt-0">
        <Button
          variant="ghost"
          size="icon"
          className="text-gray-400 hover:text-white mr-2"
          onClick={() => {}}
          aria-label={volume === 0 ? "Desmutear" : "Mute"}
        >
          {volume === 0 ? (
            <VolumeX className="w-5 h-5" />
          ) : volume <= 50 ? (
            <Volume1 className="w-5 h-5" />
          ) : (
            <Volume2 className="w-5 h-5" />
          )}
        </Button>
        <input
          type="range"
          min={0}
          max={100}
          value={volume}
          onChange={onVolumeChange}
          className="w-24 h-1 bg-gray-600 rounded-full cursor-pointer appearance-none focus:outline-none"
        />
        <Button
          variant="ghost"
          size="icon"
          className="text-gray-400 hover:text-white ml-2"
          aria-label="Pantalla completa"
          onClick={onFullScreen}
        >
          <Maximize2 className="w-5 h-5" />
        </Button>
      </div>
    </div>
  );
};

export default PlaybackBar;



File: src/components/ExploreContainer.tsx
=========================================
import './ExploreContainer.css';

interface ContainerProps { }

const ExploreContainer: React.FC<ContainerProps> = () => {
  return (
    <div id="container">
      <strong>Ready to create an app?</strong>
      <p>Start with Ionic <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
    </div>
  );
};

export default ExploreContainer;



File: src/components/PlaybackControls.tsx
=========================================
import React from "react";
import { Play, Pause, MoreHorizontal } from "lucide-react";
import { Button } from "../ui/button";

interface PlaybackControlsProps {
  isPlaying: boolean;
  onPlayPause: () => void;
  onAddToQueue: () => void;
  onMoreOptions: () => void;
}

const PlaybackControls: React.FC<PlaybackControlsProps> = ({
  isPlaying,
  onPlayPause,
  onAddToQueue,
  onMoreOptions,
}) => {
  return (
    <div className="flex items-center space-x-4 mb-6 px-6">
      <Button
        size="lg"
        className="rounded-full bg-green-500 hover:bg-green-400"
        onClick={onPlayPause}
        aria-label={isPlaying ? "Pausar" : "Reproducir"}
      >
        {isPlaying ? (
          <Pause fill="black" className="w-6 h-6 text-black" />
        ) : (
          <Play fill="black" className="w-6 h-6 text-black" />
        )}
      </Button>

      <Button
        variant="outline"
        size="icon"
        className="rounded-full"
        aria-label="Agregar a la cola"
        onClick={onAddToQueue}
      >
        <svg
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            d="M4.5 12.75L19.5 12.75"
            stroke="currentColor"
            strokeWidth="1.5"
            strokeLinecap="round"
            strokeLinejoin="round"
          />
          <path
            d="M12 5.25L12 20.25"
            stroke="currentColor"
            strokeWidth="1.5"
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
      </Button>

      <Button
        variant="ghost"
        size="icon"
        className="rounded-full"
        aria-label="Más opciones"
        onClick={onMoreOptions}
      >
        <MoreHorizontal />
      </Button>
    </div>
  );
};

export default PlaybackControls;



File: src/components/FullScreenPlayer.tsx
=========================================
import React from "react";
import {
  Shuffle,
  SkipBack,
  Play,
  Pause,
  SkipForward,
  Repeat,
  Download,
  Mic2,
  Share2,
  ChevronDown,
  MoreHorizontal,
} from "lucide-react";
import { Button } from "../ui/button";
import { Slider } from "../ui/slider";

interface Song {
  title: string;
  artist: string;
  youtubeId?: string;
}

interface FullScreenPlayerProps {
  currentSong: Song;
  isPlaying: boolean;
  onPlayPause: () => void;
  onNextSong: () => void;
  onPreviousSong: () => void;
  onShuffle: () => void;
  onRepeat: () => void;
  onVolumeChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  volume: number;
  onProgressChange: (values: number[]) => void;
  currentTime: number;
  duration: number;
  onClose: () => void;
  isShuffle: boolean;
}

const getYouTubeThumbnail = (youtubeId: string): string =>
  `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;

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

const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({
  currentSong,
  isPlaying,
  onPlayPause,
  onNextSong,
  onPreviousSong,
  onShuffle,
  onRepeat,
  onVolumeChange,
  volume,
  onProgressChange,
  currentTime,
  duration,
  onClose,
  isShuffle,
}) => {
  return (
    <div className="flex flex-col h-screen bg-gradient-to-b from-gray-900 to-black text-white">
      <div className="flex justify-between items-center p-4">
        <Button
          variant="ghost"
          size="icon"
          className="text-white"
          onClick={onClose}
        >
          <ChevronDown className="h-6 w-6" />
        </Button>
        <span className="text-sm font-semibold">Time Free</span>
        <Button variant="ghost" size="icon" className="text-white">
          <MoreHorizontal className="h-6 w-6" />
        </Button>
      </div>

      <div className="flex-grow flex items-center justify-center p-4">
        <img
          src={
            currentSong.youtubeId
              ? getYouTubeThumbnail(currentSong.youtubeId)
              : "/placeholder.svg"
          }
          alt="Album Art"
          className="w-full h-full max-w-xs max-h-xs object-cover rounded-lg"
        />
      </div>

      <div className="px-4 py-2">
        <h2 className="text-2xl font-bold">{currentSong.title}</h2>
        <p className="text-gray-400">{currentSong.artist}</p>
      </div>

      <div className="px-4 py-2">
      <Slider
          value={[(currentTime / duration) * 100 || 0]}
          max={100}
          step={1}
          onValueChange={onProgressChange}
          className="w-full"
        />
        <div className="flex justify-between text-xs text-gray-400 mt-1">
          <span>{formatTime(currentTime)}</span>
          <span>-{formatTime(duration - currentTime)}</span>
        </div>
      </div>

      <div className="flex justify-between items-center px-4 py-6">
        <Button
          variant="ghost"
          size="icon"
          className={`text-white ${isShuffle ? "text-green-500" : ""}`}
          onClick={onShuffle}
          aria-label="Shuffle"
        >
          <Shuffle className="h-6 w-6" />
        </Button>
        <Button
          variant="ghost"
          size="icon"
          className="text-white"
          onClick={onPreviousSong}
          aria-label="Anterior"
        >
          <SkipBack className="h-6 w-6" />
        </Button>
        <Button
          size="icon"
          className="bg-white text-black rounded-full hover:bg-gray-200 h-16 w-16"
          onClick={onPlayPause}
          aria-label={isPlaying ? "Pausar" : "Reproducir"}
        >
          {isPlaying ? (
            <Pause className="h-8 w-8" fill="currentColor" />
          ) : (
            <Play className="h-8 w-8" fill="currentColor" />
          )}
        </Button>
        <Button
          variant="ghost"
          size="icon"
          className="text-white"
          onClick={onNextSong}
          aria-label="Siguiente"
        >
          <SkipForward className="h-6 w-6" />
        </Button>
        <Button
          variant="ghost"
          size="icon"
          className="text-white"
          onClick={onRepeat}
          aria-label="Repetir"
        >
          <Repeat className="h-6 w-6" />
        </Button>
      </div>

      <div className="flex justify-between items-center px-4 py-4">
        <Button variant="ghost" size="icon" className="text-white">
          <Download className="w-6 h-6" />
        </Button>
        <Button variant="ghost" size="icon" className="text-white">
          <Mic2 className="h-6 w-6" />
        </Button>
        <Button variant="ghost" size="icon" className="text-white">
          <Share2 className="h-6 w-6" />
        </Button>
      </div>
    </div>
  );
};

export default FullScreenPlayer;


File: src/components/NowPlayingBar.tsx
======================================
import React from "react";
import { Pause, Play, ChevronDown } from "lucide-react";
import { Button } from "../ui/button";

interface Song {
  title: string;
  artist: string;
  youtubeId?: string;
}

interface NowPlayingBarProps {
  currentSong: Song;
  isPlaying: boolean;
  onPlayPause: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
  onToggleFullScreen: () => void;
}

const getYouTubeThumbnail = (youtubeId: string): string =>
  `https://img.youtube.com/vi/${youtubeId}/hqdefault.jpg`;

const NowPlayingBar: React.FC<NowPlayingBarProps> = ({
  currentSong,
  isPlaying,
  onPlayPause,
  onToggleFullScreen,
}) => {
  return (
    <div
      className="bg-gray-900 p-2 fixed bottom-0 left-0 right-0 z-50 cursor-pointer"
      onClick={onToggleFullScreen}
    >
      <div className="flex items-center justify-between">
        <div className="flex items-center space-x-2 flex-grow">
          <img
            src={
              currentSong.youtubeId
                ? getYouTubeThumbnail(currentSong.youtubeId)
                : "/placeholder.svg"
            }
            alt="Now playing cover"
            className="w-12 h-12 rounded"
          />
          <div>
            <p className="font-medium">{currentSong.title}</p>
            <p className="text-sm text-gray-400">{currentSong.artist}</p>
          </div>
        </div>

        <div className="flex items-center space-x-4">
          <Button
            variant="ghost"
            size="icon"
            className="text-white"
            onClick={(e) => {
              e.stopPropagation();
              onPlayPause(e);
            }}
            aria-label={isPlaying ? "Pausar" : "Reproducir"}
          >
            {isPlaying ? (
              <Pause className="h-6 w-6" fill="currentColor" />
            ) : (
              <Play className="h-6 w-6" fill="currentColor" />
            )}
          </Button>
        </div>
      </div>
    </div>
  );
};

export default NowPlayingBar;



File: src/main.tsx
==================
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);


File: src/App.tsx
=================
import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import Home from './pages/Home';

import '@ionic/react/css/core.css';

import '@ionic/react/css/normalize.css';
import '@ionic/react/css/structure.css';
import '@ionic/react/css/typography.css';

import '@ionic/react/css/padding.css';
import '@ionic/react/css/float-elements.css';
import '@ionic/react/css/text-alignment.css';
import '@ionic/react/css/text-transformation.css';
import '@ionic/react/css/flex-utils.css';
import '@ionic/react/css/display.css';

import '@ionic/react/css/palettes/dark.system.css';

/* Theme variables */
import './theme/variables.css';

setupIonicReact();

const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonRouterOutlet>
        <Route exact path="/home">
          <Home />
        </Route>
        <Route exact path="/">
          <Redirect to="/home" />
        </Route>
      </IonRouterOutlet>
    </IonReactRouter>
  </IonApp>
);

export default App;



File: src/types/youtube.d.ts
============================
/**
 * @see https://developers.google.com/youtube/iframe_api_reference
 * @see https://developers.google.com/youtube/player_parameters
 */
declare namespace YT {
    /**
     * State of a video player.
     */
    export enum PlayerState {
        UNSTARTED = -1,
        ENDED = 0,
        PLAYING = 1,
        PAUSED = 2,
        BUFFERING = 3,
        CUED = 5,
    }

    /**
     * Known causes for player errors.
     */
    export enum PlayerError {
        /**
         * The request contained an invalid parameter value.
         */
        InvalidParam = 2,

        /**
         * The requested content cannot be played in an HTML5 player.
         */
        Html5Error = 5,

        /**
         * The video requested was not found.
         */
        VideoNotFound = 100,

        /**
         * The owner of the requested video does not allow it to be played in embedded players.
         */
        EmbeddingNotAllowed = 101,

        /**
         * This error is the same as 101. It's just a 101 error in disguise!
         */
        EmbeddingNotAllowed2 = 150,
    }

    /**
     * Whether to auto-hide video controls.
     */
    export enum AutoHide {
        /**
         * Controls are visible throughout the video
         */
        AlwaysVisible = 0,

        /**
         * Progress bar and player controls slide out of view after a couple of seconds.
         */
        HideAllControls = 1,

        /**
         * Progress bar fades out while the player controls remain visible.
         */
        HideProgressBar = 2,
    }

    /**
     * Whether to autoplay the video.
     */
    export enum AutoPlay {
        /**
         * Video does not autoplay.
         */
        NoAutoPlay = 0,

        /**
         * Video will autoplay when loaded.
         */
        AutoPlay = 1,
    }

    /**
     * Whether to use user-preferred or forced caption loading.
     */
    export enum ClosedCaptionsLoadPolicy {
        /**
         * Defaults to the user's preferences.
         */
        UserDefault = 0,

        /**
         * For closed captions to be shown.
         */
        ForceOn = 1,
    }

    /**
     * Allowed progress bar colors.
     */
    export type ProgressBarColor = "red" | "white";

    /**
     * How video controls are shown.
     */
    export enum Controls {
        /**
         * Player controls do not display.
         */
        Hide = 0,

        /**
         * Player controls display.
         */
        ShowLoadPlayer = 1,

        /**
         * Player controls display after a delay.
         */
        ShowDelayLoadPlayer = 2,
    }

    /**
     * Whether to allow keyboard controls.
     */
    export enum KeyboardControls {
        /**
         * Keyboard controls are enabled.
         */
        Enable = 0,

        /**
         * Keyboard controls are disabled.
         */
        Disable = 1,
    }

    /**
     * Whether the JavaScript API should be enabled.
     */
    export enum JsApi {
        /**
         * JavaScript API will be disabled.
         */
        Disable = 0,

        /**
         * JavaScript API will be enabled.
         */
        Enable = 1,
    }

    /**
     * Whether to display the full-screen button.
     */
    export enum FullscreenButton {
        /**
         * The full screen button is hidden.
         */
        Hide = 0,

        /**
         * The full screen button is visible.
         */
        Show = 1,
    }

    /**
     * Whether to show video annotations.
     */
    export enum IvLoadPolicy {
        /**
         * Video annotations will be shown.
         */
        Show = 1,

        /**
         * Video annotations will not be shown.
         */
        Hide = 3,
    }

    /**
     * Which type of content loads in the player.
     */
    export type ListType = ListTypeSearch | ListTypeUserUploads | ListTypePlaylist;

    /**
     * A search area should be shown in the player.
     * @deprecated
     */
    export type ListTypeSearch = "search";

    /**
     * The user's uploads should load in the player.
     */
    export type ListTypeUserUploads = "user_uploads";

    /**
     * A playlist should be shown in the player.
     */
    export type ListTypePlaylist = "playlist";

    /**
     * Whether a single video should be looped.
     */
    export enum Loop {
        /**
         * Video or playlist will be played only once.
         */
        SinglePlay = 0,

        /**
         * Video or playlist will be played over and over again.
         */
        Loop = 1,
    }

    /**
     * Comma separated list of video IDs to play after the URL path's video.
     */
    export enum ModestBranding {
        /**
         * Player will contain full YouTube branding.
         */
        Full = 0,

        /**
         * YouTube logo will not display in the control bar.
         */
        Modest = 1,
    }

    /**
     * Whether or not to start the video muted. Some browsers require this set to 1 for autoplay to work (e.g. Chrome).
     */
    export enum Mute {
        /**
         * Player will start not muted, with sound
         */
        NotMuted = 0,

        /**
         * Player will start muted
         */
        Muted = 1,
    }

    /**
     * Whether to playback video inline or full-screen in an HTML5 player on iOS
     */
    export enum PlaysInline {
        /**
         * Playback in fullscreen.
         */
        Fullscreen = 0,

        /**
         * Playback inline
         */
        Inline = 1,
    }

    /**
     * Whether to show related videos after the video finishes.
     */
    export enum RelatedVideos {
        /**
         * Hide related videos after playback is complete.
         */
        Hide = 0,

        /**
         * Show related videos after playback is complete.
         */
        Show = 1,
    }

    /**
     * Whether to show video information before playing.
     */
    export enum ShowInfo {
        /**
         * Hide video title and uploader before video starts playing.
         */
        Hide = 0,

        /**
         * Show video title and uploader before video starts playing.
         */
        Show = 1,
    }

    /**
     * Base interface for events triggered by a player.
     */
    export interface PlayerEvent {
        /**
         * Video player corresponding to the event.
         */
        target: Player;
    }

    /**
     * Event for player state change.
     */
    export interface OnStateChangeEvent extends PlayerEvent {
        /**
         * New player state.
         */
        data: PlayerState;
    }

    /**
     * Event for playback quality change.
     */
    export interface OnPlaybackQualityChangeEvent extends PlayerEvent {
        /**
         * New playback quality.
         */
        data: string;
    }

    /**
     * Event for playback rate change.
     */
    export interface OnPlaybackRateChangeEvent extends PlayerEvent {
        /**
         * New playback rate.
         */
        data: number;
    }

    /**
     * Event for a player error.
     */
    export interface OnErrorEvent extends PlayerEvent {
        /**
         * Which type of error occurred.
         */
        data: PlayerError;
    }

    /**
     * Handles a player event.
     *
     * @param event   The triggering event.
     */
    export interface PlayerEventHandler<TEvent extends PlayerEvent> {
        (event: TEvent): void;
    }

    /**
     * YouTube player options.
     */
    export interface PlayerOptions {
        /**
         * Player width.
         */
        width?: string | number | undefined;

        /**
         * Player height
         */
        height?: string | number | undefined;

        /**
         * ID of the video to load.
         */
        videoId?: string | undefined;

        /**
         * Player parameters.
         */
        playerVars?: PlayerVars | undefined;

        /**
         * Handlers for events fired by the player.
         */
        events?: Events | undefined;

        /**
         * Points host to correct origin for CORS
         */
        host?: string | undefined;
    }

    /**
     * Allowed suggested player video qualities.
     */
    export type SuggestedVideoQuality =
        | VideoQualityDefault
        | VideoQualitySmall
        | VideoQualityMedium
        | VideoQualityLarge
        | VideoQualityHD720
        | VideoQualityHD1080
        | VideoQualityHighRes;

    /**
     * Default video quality chosen by YouTube.
     */
    export type VideoQualityDefault = "default";

    /**
     * Player height is 240px, and player dimensions are at least 320px by 240px for 4:3 aspect ratio.
     */
    export type VideoQualitySmall = "small";

    /**
     * Player height is 360px, and player dimensions are 640px by 360px (for 16:9 aspect ratio) or 480px by 360px (for 4:3 aspect ratio).
     */
    export type VideoQualityMedium = "medium";

    /**
     * Player height is 480px, and player dimensions are 853px by 480px (for 16:9 aspect ratio) or 640px by 480px (for 4:3 aspect ratio).
     */
    export type VideoQualityLarge = "large";

    /**
     * Player height is 720px, and player dimensions are 1280px by 720px (for 16:9 aspect ratio) or 960px by 720px (for 4:3 aspect ratio).
     */
    export type VideoQualityHD720 = "hd720";

    /**
     * Player height is 1080px, and player dimensions are 1920px by 1080px (for 16:9 aspect ratio) or 1440px by 1080px (for 4:3 aspect ratio).
     */
    export type VideoQualityHD1080 = "hd1080";

    /**
     * Player height is greater than 1080px, which means that the player's aspect ratio is greater than 1920px by 1080px.
     */
    export type VideoQualityHighRes = "highres";

    /**
     * Player parameters.
     */
    export interface PlayerVars {
        /**
         * Whether to autohide video controls (by default, HideProgressBar).
         */
        autohide?: AutoHide | undefined;

        /**
         * Whether to autoplay the video (by default, NoAutoPlay).
         */
        autoplay?: AutoPlay | undefined;

        /**
         * Whether to use user-preferred or forced caption loading (by default, UserDefault).
         */
        cc_load_policy?: ClosedCaptionsLoadPolicy | undefined;

        /**
         * Default caption language as an ISO 639-1 two-letter language code.
         */
        cc_lang_pref?: string | undefined;

        /**
         * Player progress bar color
         */
        color?: ProgressBarColor | undefined;

        /**
         * How video controls are shown (by default, ShowLoadPlayer).
         */
        controls?: Controls | undefined;

        /**
         * Whether to allow keyboard controls (by default, Enable).
         */
        disablekb?: KeyboardControls | undefined;

        /**
         * Whether the JavaScript API should be enabled (by default, Disable).
         */
        enablejsapi?: JsApi | undefined;

        /**
         * Time, in seconds from the beginning of the video, when to stop playing.
         */
        end?: number | undefined;

        /**
         * Whether to display the full-screen button (by default, Show).
         */
        fs?: FullscreenButton | undefined;

        /**
         * Player language as an ISO 639-1 two-letter language code or fully-specified locale.
         */
        hl?: string | undefined;

        /**
         * Whether to show video annotations (by default, Show).
         */
        iv_load_policy?: IvLoadPolicy | undefined;

        /**
         * Identifies content that will load.
         * If the listType parameter value is user_uploads, then the list parameter value identifies the YouTube channel whose uploaded videos will be loaded.
         * If the listType parameter value is playlist, then the list parameter value specifies a YouTube playlist ID.
         */
        list?: string | undefined;

        /**
         * Which type of content loads in the player.
         */
        listType?: ListType | undefined;

        /**
         * Whether a single video should be looped (by default, SinglePlay).
         */
        loop?: Loop | undefined;

        /**
         * Whether to hide some YouTube branding (by default, Full).
         */
        modestbranding?: ModestBranding | undefined;

        /**
         * Whether to start the video muted (by default, NotMuted).
         */
        mute?: Mute | undefined;

        /**
         * Origin domain for additional security if using the JavaScript API.
         */
        origin?: string | undefined;

        /**
         * Comma separated list of video IDs to play after the URL path's video.
         */
        playlist?: string | undefined;

        /**
         * Whether videos play inline or fullscreen in an HTML5 player on iOS. (currently by default, Fullscreen).
         */
        playsinline?: PlaysInline | undefined;

        /**
         * Whether to show related videos after the video finishes (by default, Show).
         */
        rel?: RelatedVideos | undefined;

        /**
         * Whether to show video information before playing (by default, Show).
         */
        showinfo?: ShowInfo | undefined;

        /**
         * Time, in seconds from the beginning of the video, when to start playing.
         */
        start?: number | undefined;
    }

    /**
     * Handlers for events fired by the player.
     */
    export interface Events {
        /**
         * Event fired when a player has finished loading and is ready to begin receiving API calls.
         */
        onReady?: PlayerEventHandler<PlayerEvent> | undefined;

        /**
         * Event fired when the player's state changes.
         */
        onStateChange?: PlayerEventHandler<OnStateChangeEvent> | undefined;

        /**
         * Event fired when the playback quality of the player changes.
         */
        onPlaybackQualityChange?: PlayerEventHandler<OnPlaybackQualityChangeEvent> | undefined;

        /**
         * Event fired when the playback rate of the player changes.
         */
        onPlaybackRateChange?: PlayerEventHandler<OnPlaybackRateChangeEvent> | undefined;

        /**
         * Event fired when an error in the player occurs
         */
        onError?: PlayerEventHandler<OnErrorEvent> | undefined;

        /**
         * Event fired to indicate thath the player has loaded, or unloaded, a module
         * with exposed API methods. This currently only occurs for closed captioning.
         */
        onApiChange?: PlayerEventHandler<PlayerEvent> | undefined;
    }

    /**
     * Settings to load, play, or queue a video or playlist.
     */
    export interface VideoOrPlaylistSettings {
        /**
         * Time, in seconds from the beginning of the (first) video, when to start playing.
         */
        startSeconds?: number | undefined;

        /**
         * Time, in seconds from the end of the (first) video, when to end playing.
         */
        endSeconds?: number | undefined;

        /**
         * Suggested video player quality.
         */
        suggestedQuality?: SuggestedVideoQuality | undefined;
    }

    /**
     * Settings to play or queue a video by ID.
     */
    export interface VideoByIdSettings extends VideoOrPlaylistSettings {
        /**
         * Video ID.
         */
        videoId: string;
    }

    /**
     * Settings to play or queue a video by media content URL.
     */
    export interface VideoByMediaContentUrlSettings extends VideoOrPlaylistSettings {
        /**
         * Fully qualified player URL.
         */
        mediaContentUrl: string;
    }

    /**
     * Settings to load or queue a playlist.
     */
    export interface IPlaylistSettings extends VideoOrPlaylistSettings {
        /**
         * Identifier for the listType videos list.
         */
        list: string;

        /**
         * Which type of content loads in the player.
         */
        listType?: ListType | undefined;

        /**
         * Start index of the playlist, if not 0.
         */
        index?: number | undefined;
    }

    /**
     * The spherical video config object, including information about the
     * viewport headings and zoom level.
     */
    export interface SphericalProperties {
        enableOrientationSensor?: boolean | undefined;
        fov?: number | undefined;
        pitch?: number | undefined;
        roll?: number | undefined;
        yaw?: number | undefined;
    }

    /**
     * Creates and controls a YouTube player in an <iframe>.
     */
    export class Player {
        /**
         * Initializes a new instance of the Player class.
         *
         * @param container   DOM element to insert the player's <iframe>.
         * @param options   Player options.
         */
        constructor(elt: HTMLElement, options?: PlayerOptions);

        /**
         * Initializes a new instance of the Player class.
         *
         * @param id   ID of the DOM element to insert the player's <iframe>.
         * @param options   Player options.
         */
        constructor(id: string, options?: PlayerOptions);

        /**
         * Queues a video by ID.
         *
         * @param videoId   Video ID.
         * @param startSeconds   Time from which the video should start playing.
         * @param suggestedQuality   Suggested video player quality.
         */
        cueVideoById(videoId: string, startSeconds?: number, suggestedQuality?: SuggestedVideoQuality): void;

        /**
         * Queues a video by ID.
         *
         * @param args   Settings to queue the video.
         */
        cueVideoById(args: VideoByIdSettings): void;

        /**
         * Loads and plays a video by ID.
         *
         * @param videoId   Video ID.
         * @param startSeconds   Time from which the video should start playing.
         * @param suggestedQuality   Suggested video player quality.
         */
        loadVideoById(videoId: string, startSeconds?: number, suggestedQuality?: SuggestedVideoQuality): void;

        /**
         * Loads and plays a video by ID.
         *
         * @param args   Settings to play the video.
         */
        loadVideoById(args: VideoByIdSettings): void;

        /**
         * Queues a video by media content URL.
         *
         * @param mediaContentUrl   Fully qualified player URL.
         * @param startSeconds   Time from which the video should start playing.
         * @param suggestedQuality   Suggested video player quality.
         */
        cueVideoByUrl(mediaContentUrl: string, startSeconds?: number, suggestedQuality?: SuggestedVideoQuality): void;

        /**
         * Queues a video by media content URL.
         *
         * @param args   Settings to play the video.
         */
        cueVideoByUrl(args: VideoByMediaContentUrlSettings): void;

        /**
         * Loads a video by media content URL.
         *
         * @param mediaContentUrl   Fully qualified player URL.
         * @param startSeconds   Time from which the video should start playing.
         * @param suggestedQuality   Suggested video player quality.
         */
        loadVideoByUrl(mediaContentUrl: string, startSeconds?: number, suggestedQuality?: SuggestedVideoQuality): void;

        /**
         * Loads a video by media content URL.
         *
         * @param args   Settings to play the video.
         */
        loadVideoByUrl(
            args: {
                mediaContentUrl: string;
                startSeconds?: number | undefined;
                endSeconds?: number | undefined;
                suggestedQuality?: SuggestedVideoQuality | undefined;
            },
        ): void;

        /**
         * Queues a playlist of videos.
         *
         * @param playlist   Video ID(s) to play.
         * @param index   Start index of the playlist, if not 0.
         * @param startSeconds   Time from which the video should start playing.
         * @param suggestedQuality   Suggested video player quality.
         */
        cuePlaylist(
            playlist: string | string[],
            index?: number,
            startSeconds?: number,
            suggestedQuality?: SuggestedVideoQuality,
        ): void;

        /**
         * Queues a playlist of videos.
         *
         * @param args   Settings to queue the playlist.
         */
        cuePlaylist(args: IPlaylistSettings): void;

        /**
         * Loads a playlist of videos.
         *
         * @param playlist   Video ID(s) to play.
         * @param index   Start index of the playlist, if not 0.
         * @param startSeconds   Time from which the video should start playing.
         * @param suggestedQuality   Suggested video player quality.
         */
        loadPlaylist(
            playlist: string | string[],
            index?: number,
            startSeconds?: number,
            suggestedQuality?: SuggestedVideoQuality,
        ): void;

        /**
         * Loads a playlist of videos.
         *
         * @param args    Settings to queue the playlist.
         */
        loadPlaylist(args: IPlaylistSettings): void;

        /**
         * Plays the currently cued/loaded video.
         */
        playVideo(): void;

        /**
         * Pauses the currently playing video.
         */
        pauseVideo(): void;

        /**
         * Stops and cancels loading of the current video.
         */
        stopVideo(): void;

        /**
         * Seeks to a specified time in the video.
         *
         * @param seconds   Time, in seconds from the beginning of the video.
         * @param allowSeekAhead   Whether the player is allowed to make a new request for unbuffered data.
         */
        seekTo(seconds: number, allowSeekAhead: boolean): void;

        /**
         * Loads and plays the next video in the playlist.
         */
        nextVideo(): void;

        /**
         * Loads and plays the previous video in the playlist.
         */
        previousVideo(): void;

        /**
         * Loads and plays the specified video in the playlist.
         *
         * @param index   Index of the video to play.
         */
        playVideoAt(index: number): void;

        /**
         * Mutes the player.
         */
        mute(): void;

        /**
         * Unmutes the player.
         */
        unMute(): void;

        /**
         * @returns Whether the player is muted.
         */
        isMuted(): boolean;

        /**
         * Sets the player volume.
         *
         * @param volume   An integer between 0 and 100.
         */
        setVolume(volume: number): void;

        /**
         * @returns The player's current volume, an integer between 0 and 100.
         */
        getVolume(): number;

        /**
         * Sets the size in pixels of the <iframe> that contains the player.
         *
         * @param width   Width in pixels of the <iframe>.
         * @param height   Height in pixels of the <iframe>.
         */
        setSize(width: number, height: number): void;

        /**
         * @returns Playback rate of the currently playing video.
         */
        getPlaybackRate(): number;

        /**
         * Sets the suggested playback rate for the current video.
         *
         * @param suggestedRate   Suggested playback rate.
         */
        setPlaybackRate(suggestedRate: number): void;

        /**
         * @returns Available playback rates for the current video.
         */
        getAvailablePlaybackRates(): number[];

        /**
         * Sets whether the player should continuously play a playlist.
         *
         * @param loopPlaylists   Whether to continuously loop playlists.
         */
        setLoop(loopPlaylists: boolean): void;

        /**
         * Sets whether a playlist's videos should be shuffled.
         *
         * @param shufflePlaylist   Whether to shuffle playlist videos.
         */
        setShuffle(shufflePlaylist: boolean): void;

        /**
         * @returns A number between 0 and 1 of how much the player has buffered.
         */
        getVideoLoadedFraction(): number;

        /**
         * @returns Current player state.
         */
        getPlayerState(): PlayerState;

        /**
         * @returns Elapsed time in seconds since the video started playing.
         */
        getCurrentTime(): number;

        /**
         * @returns Actual video quality of the current video.
         */
        getPlaybackQuality(): SuggestedVideoQuality;

        /**
         * Sets the suggested video quality for the current video.
         *
         * @param suggestedQuality   Suggested video quality for the current video.
         */
        setPlaybackQuality(suggestedQuality: SuggestedVideoQuality): void;

        /**
         * @returns Quality formats in which the current video is available.
         */
        getAvailableQualityLevels(): SuggestedVideoQuality[];

        /**
         * @returns Duration in seconds of the currently playing video.
         */
        getDuration(): number;

        /**
         * @returns YouTube.com URL for the currently loaded/playing video.
         */
        getVideoUrl(): string;

        /**
         * @returns The spherical video config object, with information about the viewport
         * headings and zoom level.
         */
        getSphericalProperties(): SphericalProperties;

        /**
         * Sets the spherical video config object. The call will be No-Op for non-360
         * videos, and will change the view port according to the input for 360
         * videos.
         */
        setSphericalProperties(option: SphericalProperties): void;

        /**
         * @returns Embed code for the currently loaded/playing video.
         */
        getVideoEmbedCode(): string;

        /**
         * @returns Video IDs in the playlist as they are currently ordered.
         */
        getPlaylist(): string[];

        /**
         * @returns Index of the playlist video that is currently playing.
         */
        getPlaylistIndex(): number;

        /**
         * Adds an event listener for the specified event.
         *
         * @param eventName   Name of the event.
         * @param listener   Handler for the event.
         */
        addEventListener<EventName extends keyof Events>(
            eventName: EventName,
            listener: NonNullable<Events[EventName]>,
        ): void;

        /**
         * Remove an event listener for the specified event.
         *
         * @param eventName   Name of the event.
         * @param listener   Handler for the event.
         */
        removeEventListener<EventName extends keyof Events>(
            eventName: EventName,
            listener: NonNullable<Events[EventName]>,
        ): void;

        /**
         * @returns The DOM node for the embedded <iframe>.
         */
        getIframe(): HTMLIFrameElement;

        /**
         * Removes the <iframe> containing the player.
         */
        destroy(): void;
    }
}



File: src/data/songs.ts
=======================
export interface Song {
    title: string;
    artist: string;
    album: string;
    added: string;
    duration?: number;
    youtubeId?: string;
  }
  
  export const songs: Song[] = [
    {
      title: "CHOCOCONO",
      artist: "KRIS R",
      album: "CHOCOCONO",
      added: "Este año",
      duration: 216,
      youtubeId: "TS_boqTq3rY",
    },
    {
      title: " Khé?",
      artist: "Rauw Alejandro, Romeo Santos",
      album: "Cosa Nuestra",
      added: "Este año",
      duration: 206,
      youtubeId: "7SNycTMOF74",
    },
    {
      title: "Revolú",
      artist: "Rauw Alejandro & Feid",
      album: "Cosa Nuestra",
      added: "Este año",
      duration: 215,
      youtubeId: "XP1NGfz41AQ",
    },
    {
      title: "Qué Pasaría...",
      artist: "Rauw Alejandro, Bad Bunny",
      album: "Cosa Nuestra",
      added: "Este año",
      duration: 190,
      youtubeId: "g08y_x83d5E",
    },
    {
      title: "DEPORTIVO",
      artist: "Blessd, Anuel",
      album: "DEPORTIVO",
      added: "Este año",
      duration: 218,
      youtubeId: "vf2N-7AUHB4",
    },
    // Puedes agregar más canciones aquí
  ];
  


File: src/vite-env.d.ts
=======================
/// <reference types="vite/client" />



pero tengo un error, cuando le doy click a una cancion, la linea de tiempo no comienza hasta que yo pausae y despause la cancion, solo ocurre en ese momento, porque ya cuando comienza la linea del tiempo de la cancion, ahi si ya no hay problema, puedes solucionarme eso? que de una vez comience la linea de tiempo cuando se le da click, siendo la primera cancion que se escucha, no se si me hago entender