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:48Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"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.
npx shadcn@latest add https://www.gassierconsulting.com/r/mundo-aquaEjemplo 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.
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.
| Propiedad | Tipo | Descripción |
|---|---|---|
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
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".