Ir al contenido
Evaluar mi proyecto
Componentes · Mundos de estiloDisponible

Mundo Aqua: reproductor de 2007

Ventana de metal cepillado, semáforo de gel, pantalla LCD verde, barra acanalada y botones de cristal; reproducción simulada.

Música
Horizonte nocturnoMarea Alta
0:48

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

mundo-aqua.tsx106 líneas
Descargar
"use client";

import { useEffect, useState, type CSSProperties } from "react";

/** Mundo Aqua / Frutiger Aero: reproductor de música al estilo de 2007 con ventana de metal cepillado, semáforo de gel, lista con rayas azules, barra de avance acanalada y botones de cristal. Reproducción simulada. React + TypeScript, sin dependencias. */
export type MundoAquaProps = {
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcmaq-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(18px, 4cqi, 40px); background: linear-gradient(180deg, #4aa3ff, #a8dcff 60%, #e9f8ff); font: 400 13px/1.3 "Lucida Grande", "Helvetica Neue", ui-sans-serif, system-ui, sans-serif; color: #111; }
.gcmaq-root *, .gcmaq-root *::before, .gcmaq-root *::after { box-sizing: border-box; }
.gcmaq-root button { font: inherit; }
.gcmaq-root :focus-visible { outline: 3px solid rgba(40, 120, 255, .7); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gcmaq-root * { animation: none !important; transition-duration: .01ms !important; } }
.gcmaq-win { max-width: 520px; margin: 0 auto; overflow: hidden; border-radius: 10px; border: 1px solid #6b6f78; background: repeating-linear-gradient(90deg, rgba(255, 255, 255, .18) 0 1px, rgba(0, 0, 0, .02) 1px 2px), linear-gradient(180deg, #e8eaee, #c4c8d0); box-shadow: 0 22px 40px -12px rgba(0, 40, 100, .55), 0 0 0 1px rgba(255, 255, 255, .7) inset; }
.gcmaq-title { display: flex; align-items: center; gap: 8px; height: 26px; padding: 0 10px; background: linear-gradient(180deg, #f4f5f8, #cdd1d9); border-bottom: 1px solid #8e939d; font-weight: 700; color: #3a3d44; text-shadow: 0 1px 0 #fff; }
.gcmaq-lights { display: flex; gap: 7px; }
.gcmaq-lights i { width: 13px; height: 13px; border-radius: 50%; background: radial-gradient(circle at 50% 25%, #fff 0 18%, transparent 40%), var(--c); box-shadow: inset 0 -3px 5px rgba(0, 0, 0, .35), 0 0 0 1px rgba(0, 0, 0, .4); }
.gcmaq-lcd { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 14px; margin: 12px; padding: 10px 14px; border-radius: 8px; background: linear-gradient(180deg, #c9d9c0, #b2c6a8); box-shadow: inset 0 2px 5px rgba(0, 0, 0, .35), 0 1px 0 #fff; color: #1f2a1a; }
.gcmaq-lcd b { display: block; font-size: 14px; }
.gcmaq-ctl { display: flex; gap: 6px; }
.gcmaq-b { display: grid; place-items: center; width: 34px; height: 30px; border: 1px solid #5d6168; border-radius: 15px; background: linear-gradient(180deg, #fff 0%, #dfe3ea 48%, #b9bec8 52%, #e6e9ef 100%); color: #2c3038; cursor: pointer; box-shadow: inset 0 1px 0 #fff, 0 1px 2px rgba(0, 0, 0, .3); }
.gcmaq-b:active { background: linear-gradient(180deg, #b9bec8, #dfe3ea); }
.gcmaq-b[data-main="true"] { width: 46px; background: linear-gradient(180deg, #b8e0ff 0%, #4aa3ff 48%, #1478e6 52%, #6bbaff 100%); color: #fff; border-color: #0b4fa3; text-shadow: 0 -1px 0 rgba(0, 40, 120, .6); }
.gcmaq-scrub { height: 7px; margin: 0 12px 10px; border-radius: 4px; background: linear-gradient(180deg, #8d929c, #bcc1ca); box-shadow: inset 0 1px 2px rgba(0, 0, 0, .4); overflow: hidden; }
.gcmaq-scrub i { display: block; height: 100%; background: repeating-linear-gradient(135deg, #4aa3ff 0 6px, #2f86e8 6px 12px), #4aa3ff; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .6); transition: width .5s linear; }
.gcmaq-list { margin: 0 12px 12px; padding: 0; list-style: none; overflow: hidden; border: 1px solid #8e939d; border-radius: 4px; background: #fff; }
.gcmaq-row { display: grid; grid-template-columns: 22px 1fr 52px; align-items: center; width: 100%; height: 24px; padding: 0 8px; border: 0; background: transparent; color: #111; text-align: left; cursor: default; }
.gcmaq-list li:nth-child(even) .gcmaq-row { background: #eef3fb; }
.gcmaq-row[aria-pressed="true"] { background: linear-gradient(180deg, #5eadff, #1a7be8) !important; color: #fff; text-shadow: 0 -1px 0 rgba(0, 40, 120, .5); }
.gcmaq-eq { display: inline-flex; align-items: flex-end; gap: 1px; height: 10px; }
.gcmaq-eq i { width: 2px; background: currentColor; animation: gcmaq-eq .7s ease-in-out infinite alternate; }
.gcmaq-eq i:nth-child(2) { animation-delay: -.2s; }
.gcmaq-eq i:nth-child(3) { animation-delay: -.4s; }
@keyframes gcmaq-eq { from { height: 2px; } to { height: 10px; } }`;

const TRACKS = [
  { t: "Horizonte nocturno", a: "Marea Alta", d: 214 },
  { t: "Luz de invierno", a: "Casa Azul", d: 187 },
  { t: "Cuarenta y dos", a: "Los Relojes", d: 241 },
  { t: "Mapa de sal", a: "Marea Alta", d: 199 },
];
const fmt = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;

export function MundoAqua({ className = "", style }: MundoAquaProps) {
  const [cur, setCur] = useState(0);
  const [playing, setPlaying] = useState(true);
  const [t, setT] = useState(48);
  useEffect(() => {
    if (!playing || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const id = window.setInterval(() => !document.hidden && setT((v) => {
      if (v + 1 >= TRACKS[cur].d) { setCur((c) => (c + 1) % TRACKS.length); return 0; }
      return v + 1;
    }), 1000);
    return () => window.clearInterval(id);
  }, [playing, cur]);
  const tr = TRACKS[cur];
  return (
    <div className={`gcmaq-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gcmaq-win">
        <div className="gcmaq-title">
          <span className="gcmaq-lights" aria-hidden="true">
            <i style={{ "--c": "#ff5f57" } as CSSProperties} />
            <i style={{ "--c": "#ffbd2e" } as CSSProperties} />
            <i style={{ "--c": "#28c840" } as CSSProperties} />
          </span>
          <span style={{ flex: 1, textAlign: "center", marginRight: 54 }}>Música</span>
        </div>
        <div className="gcmaq-lcd">
          <div className="gcmaq-ctl">
            <button className="gcmaq-b" type="button" aria-label="Anterior" onClick={() => { setCur((c) => (c - 1 + TRACKS.length) % TRACKS.length); setT(0); }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 5h2v14H6zM20 5v14L9 12z" /></svg>
            </button>
            <button className="gcmaq-b" data-main="true" type="button" aria-label={playing ? "Pausar" : "Reproducir"} aria-pressed={playing} onClick={() => setPlaying((v) => !v)}>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">{playing ? <path d="M7 5h3.500v14H7zM13.500 5H17v14h-3.500z" /> : <path d="M8 5v14l11-7z" />}</svg>
            </button>
            <button className="gcmaq-b" type="button" aria-label="Siguiente" onClick={() => { setCur((c) => (c + 1) % TRACKS.length); setT(0); }}>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16 5h2v14h-2zM4 5l11 7L4 19z" /></svg>
            </button>
          </div>
          <div>
            <b>{tr.t}</b>
            {tr.a}
          </div>
          <span style={{ fontVariantNumeric: "tabular-nums" }}>{fmt(t)}</span>
        </div>
        <div className="gcmaq-scrub" role="progressbar" aria-label="Posición" aria-valuemin={0} aria-valuemax={tr.d} aria-valuenow={t}><i style={{ width: `${(t / tr.d) * 100}%` }} /></div>
        <ul className="gcmaq-list">
          {TRACKS.map((track, i) => (
            <li key={track.t}>
              <button className="gcmaq-row" type="button" aria-pressed={cur === i} onClick={() => { setCur(i); setT(0); setPlaying(true); }}>
                <span>{cur === i && playing ? <span className="gcmaq-eq" aria-hidden="true"><i /><i /><i /></span> : null}</span>
                <span>{track.t} — {track.a}</span>
                <span style={{ textAlign: "right", fontVariantNumeric: "tabular-nums" }}>{fmt(track.d)}</span>
              </button>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/mundo-aqua

Ejemplo de uso de Mundo Aqua: reproductor de 2007

Después de copiar mundo-aqua.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { MundoAqua } from "./mundo-aqua";

export default function Ejemplo() {
  return <MundoAqua />;
}

Propiedades de MundoAqua

MundoAqua acepta 2 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 106 líneas (8 KB) y define 1 animación con @keyframes. Esto es lo que hace Mundo Aqua: reproductor de 2007 por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Declara roles ARIA para lectores de pantalla: progressbar.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)