Album Launch Hero

Giant condensed type, gradient-painted album art with a sliding vinyl, a play/pause morph and a pointer-reactive spectrum.

  • Heroes
  • Music & media
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Icon morph uses CSS `d` transitions (Chromium, Firefox); Safari swaps instantly.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/hero-music-waveform.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/heroes/hero-music-waveform.tsx
1/**
2 * HeroMusicWaveform — an album-launch hero for a streaming service. Giant
3 * condensed type, album art painted entirely with CSS gradients (a slowly
4 * turning conic "sun" over grain), a vinyl that slides out of the sleeve, and
5 * a full-width spectrum of bars that dances to a synthetic signal and bulges
6 * toward the pointer. The play button morphs to pause (SVG path morph) and
7 * drives the energy of the spectrum. On scroll the sleeve tilts back and the
8 * bars flatten.
9 *
10 * Props: artist, album, release, tracks, primaryCta, accent colours.
11 * CSS vars: --fd-wav-bg, --fd-wav-ink, --fd-wav-hot, --fd-wav-cool.
12 *
13 * Browser support: all evergreen browsers. No audio is played; the
14 * spectrum is procedural.
15 * Motion: one rAF loop (paused off-screen / hidden tab) writes scaleY on the
16 * bars and the sleeve transform; the play/pause morph is a CSS `d` transition
17 * on Chromium/Firefox, with a cross-fade fallback in Safari.
18 * prefers-reduced-motion: static bars, no spin, button still toggles.
19 * Accessibility: play button is a real <button aria-pressed>; track list is
20 * an <ol>; the spectrum is aria-hidden.
21 */
22import { useEffect, useRef, useState } from "react";
23
24export interface HeroMusicWaveformProps {
25 artist?: string;
26 album?: string;
27 release?: string;
28 tracks?: { title: string; time: string }[];
29 primaryCta?: { label: string; href: string };
30 bars?: number;
31}
32
33const DEFAULT_TRACKS = [
34 { title: "Saltwater Hymn", time: "3:41" },
35 { title: "Low Orbit (feat. Ana Brava)", time: "4:12" },
36 { title: "Nine Floors Up", time: "2:58" },
37];
38
39const PLAY = "M8 5 L8 19 L13.5 15.5 L13.5 8.5 Z M13.5 8.5 L13.5 15.5 L19 12 L19 12 Z";
40const PAUSE = "M7 5 L7 19 L10.5 19 L10.5 5 Z M13.5 5 L13.5 19 L17 19 L17 5 Z";
41
42export function HeroMusicWaveform({
43 artist = "Noor Okafor",
44 album = "TIDAL HEART",
45 release = "Out now · 11 tracks · 41 min",
46 tracks = DEFAULT_TRACKS,
47 primaryCta = { label: "Pre-save the vinyl", href: "#vinyl" },
48 bars = 72,
49}: HeroMusicWaveformProps) {
50 const root = useRef<HTMLElement>(null);
51 const barRefs = useRef<(HTMLSpanElement | null)[]>([]);
52 const sleeve = useRef<HTMLDivElement>(null);
53 const big = useRef<HTMLHeadingElement>(null);
54 const prog = useRef<HTMLSpanElement>(null);
55 const clock = useRef<HTMLSpanElement>(null);
56 const [playing, setPlaying] = useState(true);
57 const playingRef = useRef(true);
58 playingRef.current = playing;
59
60 useEffect(() => {
61 const el = root.current;
62 if (!el) return;
63 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
64 barRefs.current.forEach((b, i) => b && (b.style.transform = `scaleY(${(0.25 + 0.6 * Math.abs(Math.sin(i * 0.37) * Math.cos(i * 0.11))).toFixed(3)})`));
65 return;
66 }
67 const n = barRefs.current.length;
68 const level = new Float32Array(n);
69 let px = -10;
70 let pTarget = -10;
71 let energy = 1;
72 const onMove = (e: PointerEvent) => {
73 const r = el.getBoundingClientRect();
74 pTarget = (e.clientX - r.left) / r.width;
75 };
76 const onLeave = () => {
77 pTarget = -10;
78 };
79 el.addEventListener("pointermove", onMove);
80 el.addEventListener("pointerleave", onLeave);
81 let raf = 0;
82 let visible = true;
83 let last = performance.now();
84 let songT = 74;
85 const frame = (now: number) => {
86 raf = 0;
87 const dt = Math.min((now - last) / 1000, 0.05);
88 last = now;
89 const t = now / 1000;
90 energy += ((playingRef.current ? 1 : 0.12) - energy) * Math.min(1, dt * 4);
91 if (playingRef.current) songT += dt;
92 px = pTarget < -1 ? px + (-10 - px) * 0.05 : px + (pTarget - px) * 0.12;
93 // a beat at 96 bpm with a kick envelope
94 const beat = (t * 1.6) % 1;
95 const kick = Math.exp(-beat * 7);
96 const r = el.getBoundingClientRect();
97 const sc = Math.min(1, Math.max(0, -r.top / Math.max(1, r.height)));
98 for (let i = 0; i < n; i++) {
99 const u = i / (n - 1);
100 // spectrum envelope: bass heavy, mid hump, airy highs
101 const env = 0.55 * Math.exp(-u * 3.2) + 0.35 * Math.exp(-Math.pow((u - 0.42) / 0.16, 2)) + 0.12;
102 const wob = 0.5 + 0.5 * Math.sin(t * (3.1 + u * 9) + i * 1.7) * Math.sin(t * (1.3 + u * 3) + i * 0.45);
103 const k = u < 0.18 ? kick * (1 - u / 0.18) : kick * 0.15;
104 const near = Math.exp(-Math.pow((u - px) / 0.07, 2));
105 const target = Math.min(1, (env * (0.35 + 0.65 * wob) + k * 0.5) * energy * (1 - sc * 0.8) + near * 0.55 + 0.04);
106 level[i] += (target - level[i]) * (target > level[i] ? 0.45 : 0.12);
107 const b = barRefs.current[i];
108 if (b) b.style.transform = `scaleY(${level[i].toFixed(3)})`;
109 }
110 if (sleeve.current) sleeve.current.style.transform = `perspective(1400px) rotateX(${(sc * 28).toFixed(2)}deg) rotateY(${(-14 + (px > -1 ? (px - 0.5) * 10 : 0)).toFixed(2)}deg) translateY(${(sc * -60).toFixed(1)}px) scale(${(1 + kick * 0.008 * energy).toFixed(4)})`;
111 if (big.current) big.current.style.transform = `translate3d(${(sc * -8).toFixed(2)}vw, 0, 0)`;
112 if (prog.current) prog.current.style.transform = `scaleX(${((songT % 221) / 221).toFixed(4)})`;
113 if (clock.current) {
114 const s = Math.floor(songT % 221);
115 clock.current.textContent = `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
116 }
117 if (visible && !document.hidden) raf = requestAnimationFrame(frame);
118 };
119 const start = () => {
120 if (!raf && visible && !document.hidden) {
121 last = performance.now();
122 raf = requestAnimationFrame(frame);
123 }
124 };
125 const io = new IntersectionObserver(([e]) => {
126 visible = e.isIntersecting;
127 if (visible) start();
128 });
129 io.observe(el);
130 document.addEventListener("visibilitychange", start);
131 start();
132 return () => {
133 cancelAnimationFrame(raf);
134 io.disconnect();
135 document.removeEventListener("visibilitychange", start);
136 el.removeEventListener("pointermove", onMove);
137 el.removeEventListener("pointerleave", onLeave);
138 };
139 }, [bars]);
140
141 return (
142 <section ref={root} className="fd-wav" data-playing={playing ? "" : undefined} aria-label={`${artist} — ${album}`}>
143 <style>{css}</style>
144 <div className="fd-wav-glow" aria-hidden="true" />
145 <div className="fd-wav-top">
146 <p className="fd-wav-artist">{artist}</p>
147 <p className="fd-wav-release">{release}</p>
148 </div>
149 <h1 ref={big} className="fd-wav-big" aria-label={album}>
150 {album.split(" ").map((w, wi) => (
151 <span key={wi} className="fd-wav-word" aria-hidden="true">
152 {Array.from(w).map((c, ci) => (
153 <span key={ci} className="fd-wav-c" style={{ animationDelay: `${wi * 220 + ci * 45}ms` }}>
154 {c}
155 </span>
156 ))}
157 </span>
158 ))}
159 </h1>
160
161 <div className="fd-wav-stage">
162 <div ref={sleeve} className="fd-wav-sleeve" aria-hidden="true">
163 <div className="fd-wav-vinyl">
164 <div className="fd-wav-label">
165 <span>{artist.split(" ")[0].toUpperCase()}</span>
166 </div>
167 </div>
168 <div className="fd-wav-art">
169 <div className="fd-wav-sun" />
170 <div className="fd-wav-horizon" />
171 <div className="fd-wav-grain" />
172 <span className="fd-wav-art-t">{album}</span>
173 <span className="fd-wav-art-a">{artist}</span>
174 </div>
175 </div>
176
177 <div className="fd-wav-player">
178 <button
179 type="button"
180 className="fd-wav-play"
181 aria-pressed={playing}
182 aria-label={playing ? "Pause preview" : "Play preview"}
183 onClick={() => setPlaying((p) => !p)}
184 >
185 <svg viewBox="0 0 24 24" width="28" height="28" aria-hidden="true">
186 <path className="fd-wav-icon" d={playing ? PAUSE : PLAY} fill="currentColor" />
187 </svg>
188 </button>
189 <div className="fd-wav-now">
190 <span className="fd-wav-now-k">{playing ? "Now playing" : "Paused"}</span>
191 <b>{tracks[0]?.title}</b>
192 <div className="fd-wav-bar">
193 <span ref={prog} />
194 </div>
195 <div className="fd-wav-times">
196 <span ref={clock}>1:14</span>
197 <span>{tracks[0]?.time}</span>
198 </div>
199 </div>
200 <ol className="fd-wav-tracks">
201 {tracks.map((t, i) => (
202 <li key={t.title} className={i === 0 ? "is-on" : undefined}>
203 <span className="fd-wav-n">{String(i + 1).padStart(2, "0")}</span>
204 <span className="fd-wav-tt">{t.title}</span>
205 <span className="fd-wav-tm">{t.time}</span>
206 </li>
207 ))}
208 </ol>
209 <a className="fd-wav-cta" href={primaryCta.href}>
210 {primaryCta.label}
211 </a>
212 </div>
213 </div>
214
215 <div className="fd-wav-spec" aria-hidden="true">
216 {Array.from({ length: bars }, (_, i) => (
217 <span key={i} ref={(n) => { barRefs.current[i] = n; }} className="fd-wav-b" style={{ ["--u" as string]: i / (bars - 1) }} />
218 ))}
219 </div>
220 </section>
221 );
222}
223
224const noise = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .45 0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E")`;
225
226const css = `
227.fd-wav, .fd-wav *, .fd-wav *::before, .fd-wav *::after { box-sizing: border-box; }
228.fd-wav {
229 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
230 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
231 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
232 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;
233 --fd-wav-bg: #0E0A0B;
234 --fd-wav-ink: #F6EDE4;
235 --fd-wav-hot: #FF5A36;
236 --fd-wav-cool: #FFB547;
237 --fd-wav-cond: "Helvetica Neue", "Arial Narrow", "Roboto Condensed", "DejaVu Sans Condensed", ui-sans-serif, -apple-system, "Inter", sans-serif;
238 --fd-wav-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;
239 position: relative; display: block; overflow: hidden; margin: 0; padding: 0; min-height: 880px;
240 background: var(--fd-wav-bg); color: var(--fd-wav-ink); font: 400 1rem/1.5 var(--fd-wav-sans); isolation: isolate;
241}
242.fd-wav-glow { position: absolute; inset: auto -10% -30% -10%; height: 70%; background: radial-gradient(50% 60% at 50% 100%, rgba(255,90,54,0.35), transparent 70%); pointer-events: none; filter: blur(10px); }
243.fd-wav-top { position: relative; z-index: 2; display: flex; justify-content: space-between; gap: 16px; max-width: 1320px; margin: 0 auto; padding: 96px 40px 0; animation: fd-wav-fade var(--fd-dur-4) var(--fd-ease-out) 600ms both; }
244.fd-wav-artist, .fd-wav-release { margin: 0; padding: 0; font: 600 0.75rem/1 var(--fd-wav-sans); letter-spacing: 0.24em; text-transform: uppercase; color: rgba(246,237,228,0.7); }
245.fd-wav-artist { color: var(--fd-wav-hot); }
246.fd-wav-big {
247 position: relative; z-index: 1; display: flex; justify-content: center; gap: 0.18em; margin: 6px 0 0; padding: 0 20px; white-space: nowrap; will-change: transform;
248 font: 800 clamp(5rem, 17vw, 17rem)/0.82 var(--fd-wav-cond); font-stretch: condensed; letter-spacing: -0.055em; color: var(--fd-wav-ink);
249 transform-origin: center;
250}
251.fd-wav-word { display: inline-flex; overflow: hidden; padding: 0.02em 0 0.04em; }
252.fd-wav-c { display: inline-block; transform: scaleY(1.12); transform-origin: bottom; animation: fd-wav-in 900ms var(--fd-ease-out) both; }
253.fd-wav-word:last-child .fd-wav-c { -webkit-text-stroke: 2px var(--fd-wav-ink); color: transparent; }
254
255.fd-wav-stage { position: relative; z-index: 3; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 56px; align-items: start; max-width: 1140px; margin: -9vw auto 0; padding: 0 40px; }
256.fd-wav-sleeve { position: relative; width: 340px; aspect-ratio: 1; transform-style: preserve-3d; will-change: transform; animation: fd-wav-sleevein 1200ms var(--fd-ease-out) 300ms both; }
257.fd-wav-art {
258 position: absolute; inset: 0; overflow: hidden; border-radius: 6px;
259 background: linear-gradient(180deg, #2b0f12 0%, #6b1a1a 48%, #12090a 49.5%, #0b0708 100%);
260 box-shadow: 0 1px 0 rgba(255,255,255,0.15) inset, 0 40px 80px -30px rgba(0,0,0,0.9), 0 0 0 1px rgba(255,255,255,0.06);
261}
262.fd-wav-sun {
263 position: absolute; left: 50%; top: 48%; width: 76%; aspect-ratio: 1; border-radius: 50%; transform: translate(-50%, -50%);
264 background: linear-gradient(180deg, #FFE1A8 0%, #FFB547 30%, #FF5A36 52%);
265 -webkit-mask-image: linear-gradient(180deg, #000 0 30%, transparent 30% 33%, #000 33% 40%, transparent 40% 42.5%, #000 42.5% 47%, transparent 47% 49%, #000 49% 52%, transparent 52%);
266 mask-image: linear-gradient(180deg, #000 0 30%, transparent 30% 33%, #000 33% 40%, transparent 40% 42.5%, #000 42.5% 47%, transparent 47% 49%, #000 49% 52%, transparent 52%);
267 overflow: hidden;
268}
269.fd-wav-sun::after { content: ""; position: absolute; inset: 0; border-radius: 50%; background: conic-gradient(from 0deg, transparent, rgba(255,255,255,0.35), transparent 30%, rgba(255,60,40,0.4) 55%, transparent 70%); mix-blend-mode: overlay; animation: fd-wav-spin 18s linear infinite; }
270.fd-wav-horizon { position: absolute; left: 0; right: 0; top: 49.5%; bottom: 0; background: repeating-linear-gradient(180deg, rgba(255,120,60,0.55) 0 2px, transparent 2px 9px); -webkit-mask-image: linear-gradient(180deg, #000, transparent 85%); mask-image: linear-gradient(180deg, #000, transparent 85%); }
271.fd-wav-grain { position: absolute; inset: 0; background: ${noise}; mix-blend-mode: multiply; opacity: 0.8; }
272.fd-wav-art-t { position: absolute; left: 18px; bottom: 16px; font: 800 1.375rem/0.9 var(--fd-wav-cond); font-stretch: condensed; letter-spacing: -0.03em; color: #FFE1A8; }
273.fd-wav-art-a { position: absolute; right: 18px; top: 16px; font: 600 0.625rem/1 var(--fd-wav-sans); letter-spacing: 0.24em; text-transform: uppercase; color: rgba(255,225,168,0.8); }
274.fd-wav-vinyl {
275 position: absolute; inset: 3%; border-radius: 50%;
276 background: repeating-radial-gradient(circle at 50% 50%, #161112 0 1px, #0a0708 1px 3px), radial-gradient(circle, #1a1213, #050404);
277 box-shadow: 0 20px 50px -20px rgba(0,0,0,0.9), inset 0 0 0 1px rgba(255,255,255,0.05);
278 transform: translateX(0); animation: fd-wav-slide 1400ms var(--fd-ease-out) 1100ms both;
279}
280.fd-wav-vinyl::before { content: ""; position: absolute; inset: 0; border-radius: 50%; background: conic-gradient(from 20deg, transparent 0 10%, rgba(255,255,255,0.12) 14%, transparent 20% 55%, rgba(255,255,255,0.08) 60%, transparent 66%); }
281.fd-wav-label { position: absolute; left: 50%; top: 50%; width: 34%; aspect-ratio: 1; margin: -17% 0 0 -17%; border-radius: 50%; display: grid; place-items: center; background: radial-gradient(circle, #0E0A0B 0 5%, #FF5A36 6% 100%); animation: fd-wav-spin 1.8s linear infinite; animation-play-state: paused; }
282.fd-wav[data-playing] .fd-wav-label { animation-play-state: running; }
283.fd-wav-label span { font: 800 0.5625rem/1 var(--fd-wav-sans); letter-spacing: 0.2em; color: #0E0A0B; transform: translateY(-14px); }
284
285.fd-wav-player { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 18px 22px; align-items: center; max-width: 520px; margin: 11vw 0 0 auto; animation: fd-wav-rise var(--fd-dur-4) var(--fd-ease-out) 900ms both; }
286.fd-wav-play {
287 appearance: none; -webkit-appearance: none; cursor: pointer; display: grid; place-items: center; width: 84px; height: 84px; margin: 0; padding: 0; border: 0; border-radius: 9999px;
288 background: var(--fd-wav-ink); color: var(--fd-wav-bg); box-shadow: 0 0 0 8px rgba(246,237,228,0.06), 0 20px 50px -14px rgba(255,90,54,0.65);
289 transition: transform var(--fd-dur-2) var(--fd-spring), box-shadow var(--fd-dur-3) var(--fd-ease-out);
290}
291.fd-wav-play svg { display: block; }
292.fd-wav-icon { transition: d var(--fd-dur-3) var(--fd-ease-in-out); }
293.fd-wav-play:hover { box-shadow: 0 0 0 12px rgba(246,237,228,0.08), 0 24px 60px -14px rgba(255,90,54,0.8); }
294.fd-wav-play:active { transform: scale(0.96); transition-duration: var(--fd-dur-1); }
295.fd-wav-play:focus-visible, .fd-wav-cta:focus-visible { outline: 2px solid rgba(255,181,71,0.8); outline-offset: 3px; }
296.fd-wav-now { min-width: 0; }
297.fd-wav-now-k { display: block; margin: 0 0 6px; font: 600 0.6875rem/1 var(--fd-wav-sans); letter-spacing: 0.18em; text-transform: uppercase; color: var(--fd-wav-cool); }
298.fd-wav-now b { display: block; font: 600 1.375rem/1.1 var(--fd-wav-sans); letter-spacing: -0.02em; color: var(--fd-wav-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
299.fd-wav-bar { position: relative; height: 3px; margin: 14px 0 8px; border-radius: 9999px; background: rgba(246,237,228,0.14); overflow: hidden; }
300.fd-wav-bar span { position: absolute; inset: 0; background: linear-gradient(90deg, var(--fd-wav-hot), var(--fd-wav-cool)); transform-origin: left; transform: scaleX(0.33); }
301.fd-wav-times { display: flex; justify-content: space-between; font: 500 0.75rem/1 ui-monospace, "SF Mono", Menlo, monospace; color: rgba(246,237,228,0.5); font-variant-numeric: tabular-nums; }
302.fd-wav-tracks { grid-column: 1 / -1; margin: 6px 0 0; padding: 0; list-style: none; border-top: 1px solid rgba(246,237,228,0.1); }
303.fd-wav-tracks li { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; gap: 8px; margin: 0; padding: 12px 0; border-bottom: 1px solid rgba(246,237,228,0.08); font: 400 0.9375rem/1.2 var(--fd-wav-sans); color: rgba(246,237,228,0.6); }
304.fd-wav-tracks li.is-on { color: var(--fd-wav-ink); }
305.fd-wav-tracks li.is-on .fd-wav-n { color: var(--fd-wav-hot); }
306.fd-wav-n, .fd-wav-tm { font: 500 0.75rem/1.6 ui-monospace, "SF Mono", Menlo, monospace; color: rgba(246,237,228,0.4); }
307.fd-wav-tt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
308.fd-wav-cta { grid-column: 1 / -1; justify-self: start; display: inline-flex; align-items: center; height: 46px; margin: 4px 0 0; padding: 0 22px; border-radius: 9999px; border: 1px solid rgba(246,237,228,0.25); color: var(--fd-wav-ink); text-decoration: none; font: 600 0.875rem/1 var(--fd-wav-sans); letter-spacing: 0.02em; transition: background var(--fd-dur-2), color var(--fd-dur-2), transform var(--fd-dur-2) var(--fd-spring); }
309.fd-wav-cta:hover { background: var(--fd-wav-ink); color: var(--fd-wav-bg); }
310.fd-wav-cta:active { transform: scale(0.96); }
311
312.fd-wav-spec { position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; height: 200px; display: flex; align-items: flex-end; gap: 3px; padding: 0 3px; pointer-events: none; -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
313.fd-wav-b { flex: 1; height: 100%; border-radius: 3px 3px 0 0; transform-origin: bottom; transform: scaleY(0.1); will-change: transform; background: linear-gradient(180deg, color-mix(in srgb, var(--fd-wav-cool) calc(var(--u) * 100%), var(--fd-wav-hot)) 0%, rgba(255,90,54,0.05) 100%); opacity: 0.85; }
314
315@keyframes fd-wav-in { from { transform: translateY(100%) scaleY(1.6); } to { transform: scaleY(1.12); } }
316@keyframes fd-wav-fade { from { opacity: 0; } to { opacity: 1; } }
317@keyframes fd-wav-rise { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: none; } }
318@keyframes fd-wav-sleevein { from { opacity: 0; transform: perspective(1400px) rotateY(-50deg) translateX(-80px); filter: blur(10px); } to { opacity: 1; filter: none; } }
319@keyframes fd-wav-slide { from { transform: translateX(0) rotate(0deg); } to { transform: translateX(38%) rotate(140deg); } }
320@keyframes fd-wav-spin { to { transform: rotate(360deg); } }
321
322@media (max-width: 900px) {
323 .fd-wav { min-height: 844px; }
324 .fd-wav-top { padding: 84px 20px 0; }
325 .fd-wav-release { display: none; }
326 .fd-wav-big { flex-direction: column; align-items: center; gap: 0; font-size: 27vw; margin-top: 14px; }
327 .fd-wav-stage { grid-template-columns: 1fr; gap: 28px; margin-top: 24px; padding: 0 20px; }
328 .fd-wav-sleeve { width: 210px; margin: 0 auto 0 8px; }
329 .fd-wav-player { margin: 0; grid-template-columns: auto minmax(0, 1fr); }
330 .fd-wav-stage { align-items: start; }
331 .fd-wav-play { width: 64px; height: 64px; }
332 .fd-wav-tracks, .fd-wav-cta { display: none; }
333 .fd-wav-spec { height: 120px; }
334}
335@media (prefers-reduced-motion: reduce) {
336 .fd-wav *, .fd-wav *::before { animation: none !important; }
337 .fd-wav-c { transform: scaleY(1.12); }
338 .fd-wav-vinyl { transform: translateX(38%); }
339}
340`;
341
342/* ───────────────────────── Demo ───────────────────────── */
343
344export function Demo() {
345 return (
346 <div className="fd-wav-demo">
347 <style>{demoCss}</style>
348 <header className="fd-wav-demo-nav">
349 <a className="fd-wav-demo-logo" href="#top">
350 <svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
351 <path d="M3 14v-4M7 18V6M11 15V9M15 20V4M19 16V8" stroke="#FF5A36" strokeWidth="2.4" strokeLinecap="round" />
352 </svg>
353 Resonant
354 </a>
355 <nav aria-label="Primary">
356 <a href="#home">Home</a>
357 <a href="#new">New releases</a>
358 <a href="#live">Live</a>
359 <a href="#library">Library</a>
360 </nav>
361 <a className="fd-wav-demo-cta" href="#trial">Try 3 months free</a>
362 </header>
363 <HeroMusicWaveform />
364 </div>
365 );
366}
367
368const demoCss = `
369.fd-wav-demo { position: relative; margin: 0; padding: 0; min-height: 100vh; background: #0E0A0B; }
370.fd-wav-demo *, .fd-wav-demo *::before, .fd-wav-demo *::after { box-sizing: border-box; }
371.fd-wav-demo-nav { position: absolute; z-index: 10; top: 0; left: 0; right: 0; max-width: 1320px; height: 72px; margin: 0 auto; padding: 0 40px; display: flex; align-items: center; justify-content: space-between; gap: 20px; }
372.fd-wav-demo-logo { display: inline-flex; align-items: center; gap: 9px; margin: 0; color: #F6EDE4; text-decoration: none; font: 700 1.125rem/1 ui-sans-serif, -apple-system, "Inter", sans-serif; letter-spacing: -0.03em; }
373.fd-wav-demo-logo svg { display: block; }
374.fd-wav-demo-nav nav { display: flex; gap: 28px; }
375.fd-wav-demo-nav nav a { color: rgba(246,237,228,0.6); text-decoration: none; font: 500 0.875rem/1 ui-sans-serif, -apple-system, "Inter", sans-serif; transition: color 120ms; }
376.fd-wav-demo-nav nav a:hover { color: #F6EDE4; }
377.fd-wav-demo-cta { display: inline-flex; align-items: center; height: 36px; padding: 0 16px; border-radius: 9999px; background: #FF5A36; color: #0E0A0B; text-decoration: none; font: 700 0.8125rem/1 ui-sans-serif, -apple-system, "Inter", sans-serif; }
378.fd-wav-demo-nav a:focus-visible { outline: 2px solid rgba(255,181,71,.8); outline-offset: 2px; border-radius: 8px; }
379@media (max-width: 900px) { .fd-wav-demo-nav { padding: 0 20px; height: 64px; } .fd-wav-demo-nav nav { display: none; } }
380`;

More Heroes. Same file contract.

Ledger Card Hero

A photoreal CSS-3D metal card that tilts to the cursor, then flips and docks on scroll while spend stats count up.

Agent Terminal Hero

A terminal types a prompt and streams an agent's plan while the generated dashboard assembles itself beside it.

Maison Editorial Hero

A giant serif masthead and procedural duotone fabric frames revealed by clip-path wipes, drifting apart on scroll.