diff --git a/docs/superpowers/plans/2026-06-05-cmusic-frontend.md b/docs/superpowers/plans/2026-06-05-cmusic-frontend.md new file mode 100644 index 0000000..b066613 --- /dev/null +++ b/docs/superpowers/plans/2026-06-05-cmusic-frontend.md @@ -0,0 +1,1401 @@ +# CMusic 3D Frontend Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a supermodern 3D-animated music website frontend with React Three Fiber, Framer Motion, GSAP, and Tailwind CSS — cyberpunk-luxury aesthetic. + +**Architecture:** Single-page React app with distinct section components each managing their own 3D canvas (via R3F) or animation layer (Framer Motion / GSAP). A global `useAudioPlayer` hook drives the persistent player bar state. Stitch MCP generates base UI screens; Context7 provides accurate library API docs. + +**Tech Stack:** React 18, Vite 5, Tailwind CSS 3, @react-three/fiber, @react-three/drei, Framer Motion, GSAP (+ ScrollTrigger), Stitch MCP (UI generation) + +--- + +## File Map + +``` +/srv/CMusic/ +├── index.html +├── vite.config.js +├── tailwind.config.js +├── postcss.config.js +├── package.json +└── src/ + ├── main.jsx + ├── App.jsx + ├── index.css # global styles, CSS vars + ├── data/ + │ └── mockData.js # tracks, albums, artists + ├── hooks/ + │ └── useAudioPlayer.js # global player state + └── components/ + ├── Navbar/ + │ └── Navbar.jsx + ├── Hero/ + │ ├── Hero.jsx + │ ├── AudioVisualizer3D.jsx # R3F bar visualizer + │ └── HeroParticles.jsx # R3F floating particles + ├── FeaturedAlbums/ + │ ├── FeaturedAlbums.jsx + │ └── AlbumCard3D.jsx # R3F card with flip + ├── TrendingTracks/ + │ ├── TrendingTracks.jsx + │ └── TrackRow.jsx + ├── ArtistSpotlight/ + │ ├── ArtistSpotlight.jsx + │ └── ArtistParticles.jsx # R3F particle field + └── Player/ + ├── PlayerBar.jsx + └── Waveform3D.jsx # R3F animated waveform +``` + +--- + +## Task 1: Scaffold Project + +**Files:** +- Create: `package.json`, `vite.config.js`, `tailwind.config.js`, `postcss.config.js`, `index.html`, `src/main.jsx` + +- [ ] **Step 1: Init Vite React project** + +```bash +cd /srv/CMusic +npm create vite@latest . -- --template react +``` + +- [ ] **Step 2: Install all dependencies** + +```bash +npm install +npm install three @react-three/fiber @react-three/drei +npm install framer-motion gsap +npm install tailwindcss@3 postcss autoprefixer +npm install @types/three +npx tailwindcss init -p +``` + +- [ ] **Step 3: Configure tailwind.config.js** + +```js +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,jsx}'], + theme: { + extend: { + colors: { + 'neon-purple': '#b829f5', + 'neon-cyan': '#00f5ff', + 'neon-pink': '#ff2d78', + 'dark-base': '#050508', + 'dark-card': '#0d0d18', + 'dark-surface': '#12121f', + 'glass': 'rgba(255,255,255,0.04)', + }, + fontFamily: { + sans: ['Inter', 'system-ui', 'sans-serif'], + display: ['Space Grotesk', 'system-ui', 'sans-serif'], + }, + backgroundImage: { + 'gradient-neon': 'linear-gradient(135deg, #b829f5 0%, #00f5ff 100%)', + 'gradient-dark': 'linear-gradient(180deg, #050508 0%, #0d0d18 100%)', + }, + boxShadow: { + 'neon-purple': '0 0 20px rgba(184,41,245,0.5), 0 0 60px rgba(184,41,245,0.2)', + 'neon-cyan': '0 0 20px rgba(0,245,255,0.5), 0 0 60px rgba(0,245,255,0.2)', + 'glass': 'inset 0 1px 0 rgba(255,255,255,0.1)', + }, + }, + }, + plugins: [], +} +``` + +- [ ] **Step 4: Configure vite.config.js** + +```js +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], +}) +``` + +- [ ] **Step 5: Commit** + +```bash +git init +git add . +git commit -m "feat: scaffold Vite+React+R3F+Tailwind project" +``` + +--- + +## Task 2: Fetch Docs via Context7 + +**Purpose:** Get accurate API signatures for React Three Fiber, Framer Motion, GSAP ScrollTrigger before writing 3D/animation code. + +- [ ] **Step 1: Resolve Context7 library IDs** + +Use `mcp__context7__resolve-library-id` with queries: +- `"react-three-fiber 3D scene"` +- `"framer-motion animation"` +- `"gsap scroll trigger"` + +- [ ] **Step 2: Query R3F docs** + +Use `mcp__context7__query-docs` with resolved ID and question: +`"How to create a Canvas with useFrame animation loop and mesh geometry in React Three Fiber?"` + +- [ ] **Step 3: Query Framer Motion docs** + +Use `mcp__context7__query-docs` with question: +`"How to use motion.div with variants, whileHover, and useInView scroll animations?"` + +- [ ] **Step 4: Query GSAP ScrollTrigger docs** + +Use `mcp__context7__query-docs` with question: +`"How to use GSAP ScrollTrigger with React useEffect for parallax and scrub animations?"` + +--- + +## Task 3: Global Styles + Design System + +**Files:** +- Modify: `src/index.css` +- Use Stitch MCP to generate design system + +- [ ] **Step 1: Create Stitch design system** + +Call `mcp__stitch__create_design_system` with: +```json +{ + "name": "CMusic Design System", + "description": "Cyberpunk-luxury dark theme. Deep black backgrounds (#050508), electric purple (#b829f5) and cyan (#00f5ff) neon accents, glassmorphism cards with backdrop-blur, Space Grotesk display font, Inter body. Neon glow box-shadows. No white backgrounds anywhere." +} +``` + +- [ ] **Step 2: Write src/index.css** + +```css +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap'); +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --neon-purple: #b829f5; + --neon-cyan: #00f5ff; + --neon-pink: #ff2d78; + --dark-base: #050508; + --dark-card: #0d0d18; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + background-color: var(--dark-base); + color: #e8e8f0; + font-family: 'Inter', sans-serif; + overflow-x: hidden; +} + +::-webkit-scrollbar { width: 4px; } +::-webkit-scrollbar-track { background: var(--dark-base); } +::-webkit-scrollbar-thumb { + background: var(--neon-purple); + border-radius: 2px; +} + +.glass { + background: rgba(255, 255, 255, 0.04); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +.neon-text-purple { + color: var(--neon-purple); + text-shadow: 0 0 10px rgba(184,41,245,0.8); +} + +.neon-text-cyan { + color: var(--neon-cyan); + text-shadow: 0 0 10px rgba(0,245,255,0.8); +} + +.neon-border { + border: 1px solid rgba(184,41,245,0.4); + box-shadow: 0 0 15px rgba(184,41,245,0.2), inset 0 0 15px rgba(184,41,245,0.05); +} + +canvas { display: block; } +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/index.css tailwind.config.js +git commit -m "feat: global styles and cyberpunk design system" +``` + +--- + +## Task 4: Mock Data + useAudioPlayer Hook + +**Files:** +- Create: `src/data/mockData.js` +- Create: `src/hooks/useAudioPlayer.js` + +- [ ] **Step 1: Write mockData.js** + +```js +export const tracks = [ + { id: 1, title: 'Neon Pulse', artist: 'Synthwave X', album: 'Electric Dreams', duration: '3:42', plays: '2.1M', cover: 'https://picsum.photos/seed/track1/400/400' }, + { id: 2, title: 'Void Walker', artist: 'Dark Matter', album: 'Singularity', duration: '4:15', plays: '1.8M', cover: 'https://picsum.photos/seed/track2/400/400' }, + { id: 3, title: 'Cyber Rain', artist: 'Neo Tokyo', album: 'Ghost Protocol', duration: '3:58', plays: '3.4M', cover: 'https://picsum.photos/seed/track3/400/400' }, + { id: 4, title: 'Quantum Drift', artist: 'Parallax', album: 'Infinite Loop', duration: '5:02', plays: '987K', cover: 'https://picsum.photos/seed/track4/400/400' }, + { id: 5, title: 'Aurora Blaze', artist: 'Synthwave X', album: 'Electric Dreams', duration: '4:33', plays: '1.2M', cover: 'https://picsum.photos/seed/track5/400/400' }, + { id: 6, title: 'Digital Soul', artist: 'Echo System', album: 'Phase Shift', duration: '3:21', plays: '756K', cover: 'https://picsum.photos/seed/track6/400/400' }, +]; + +export const albums = [ + { id: 1, title: 'Electric Dreams', artist: 'Synthwave X', year: 2024, genre: 'Synthwave', cover: 'https://picsum.photos/seed/album1/600/600', tracks: 12, color: '#b829f5' }, + { id: 2, title: 'Singularity', artist: 'Dark Matter', year: 2024, genre: 'Dark Electro', cover: 'https://picsum.photos/seed/album2/600/600', tracks: 9, color: '#00f5ff' }, + { id: 3, title: 'Ghost Protocol', artist: 'Neo Tokyo', year: 2023, genre: 'Cyberpunk', cover: 'https://picsum.photos/seed/album3/600/600', tracks: 11, color: '#ff2d78' }, + { id: 4, title: 'Infinite Loop', artist: 'Parallax', year: 2024, genre: 'Ambient', cover: 'https://picsum.photos/seed/album4/600/600', tracks: 8, color: '#b829f5' }, +]; + +export const artists = [ + { id: 1, name: 'Synthwave X', genre: 'Synthwave / Electro', followers: '4.2M', image: 'https://picsum.photos/seed/artist1/800/1000', bio: 'Pioneering the neon-drenched soundscapes of tomorrow.' }, + { id: 2, name: 'Dark Matter', genre: 'Dark Electro', followers: '2.8M', image: 'https://picsum.photos/seed/artist2/800/1000', bio: 'Where darkness meets digital frequency.' }, +]; +``` + +- [ ] **Step 2: Write useAudioPlayer.js** + +```js +import { useState, useCallback } from 'react' +import { tracks } from '../data/mockData' + +export function useAudioPlayer() { + const [currentTrack, setCurrentTrack] = useState(tracks[0]) + const [isPlaying, setIsPlaying] = useState(false) + const [progress, setProgress] = useState(0) + const [volume, setVolume] = useState(0.8) + + const play = useCallback((track) => { + setCurrentTrack(track) + setIsPlaying(true) + }, []) + + const togglePlay = useCallback(() => { + setIsPlaying(p => !p) + }, []) + + const next = useCallback(() => { + const idx = tracks.findIndex(t => t.id === currentTrack.id) + setCurrentTrack(tracks[(idx + 1) % tracks.length]) + }, [currentTrack]) + + const prev = useCallback(() => { + const idx = tracks.findIndex(t => t.id === currentTrack.id) + setCurrentTrack(tracks[(idx - 1 + tracks.length) % tracks.length]) + }, [currentTrack]) + + return { currentTrack, isPlaying, progress, volume, play, togglePlay, next, prev, setProgress, setVolume } +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/data/mockData.js src/hooks/useAudioPlayer.js +git commit -m "feat: mock data and audio player hook" +``` + +--- + +## Task 5: Navbar + +**Files:** +- Create: `src/components/Navbar/Navbar.jsx` + +- [ ] **Step 1: Generate Navbar screen via Stitch** + +Call `mcp__stitch__create_project` then `mcp__stitch__generate_screen_from_text` with: +``` +"Glassmorphism music app navbar. Dark background with backdrop-blur. Logo 'CMUSIC' in electric purple neon. Nav links: Discover, Artists, Albums, Charts. Right side: Search icon, notification bell, user avatar circle. Subtle purple neon bottom border that animates on scroll. Cyberpunk aesthetic." +``` + +- [ ] **Step 2: Write Navbar.jsx** + +```jsx +import { useState, useEffect } from 'react' +import { motion } from 'framer-motion' + +const navLinks = ['Discover', 'Artists', 'Albums', 'Charts'] + +export default function Navbar() { + const [scrolled, setScrolled] = useState(false) + + useEffect(() => { + const handler = () => setScrolled(window.scrollY > 20) + window.addEventListener('scroll', handler) + return () => window.removeEventListener('scroll', handler) + }, []) + + return ( + +
+ {/* Logo */} + + CMUSIC + + + {/* Links */} +
+ {navLinks.map((link) => ( + + {link} + + + ))} +
+ + {/* Right Actions */} +
+ + + + + + + U + +
+
+
+ ) +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/components/Navbar/ +git commit -m "feat: glassmorphism navbar with scroll effect" +``` + +--- + +## Task 6: Hero Section with 3D Visualizer + +**Files:** +- Create: `src/components/Hero/Hero.jsx` +- Create: `src/components/Hero/AudioVisualizer3D.jsx` +- Create: `src/components/Hero/HeroParticles.jsx` + +- [ ] **Step 1: Write AudioVisualizer3D.jsx** + +```jsx +import { useRef, useMemo } from 'react' +import { Canvas, useFrame } from '@react-three/fiber' +import * as THREE from 'three' + +const BAR_COUNT = 64 + +function Bars({ isPlaying }) { + const groupRef = useRef() + const barsRef = useRef([]) + const phases = useMemo(() => Array.from({ length: BAR_COUNT }, (_, i) => Math.random() * Math.PI * 2), []) + const speeds = useMemo(() => Array.from({ length: BAR_COUNT }, () => 0.8 + Math.random() * 1.5), []) + + useFrame(({ clock }) => { + const t = clock.getElapsedTime() + barsRef.current.forEach((mesh, i) => { + if (!mesh) return + const base = isPlaying + ? Math.abs(Math.sin(t * speeds[i] + phases[i])) * 1.8 + 0.1 + : Math.abs(Math.sin(t * 0.3 + phases[i])) * 0.3 + 0.05 + mesh.scale.y = THREE.MathUtils.lerp(mesh.scale.y, base, 0.12) + mesh.position.y = mesh.scale.y * 0.5 + const hue = (i / BAR_COUNT) * 0.2 + 0.75 + Math.sin(t * 0.2) * 0.05 + mesh.material.color.setHSL(hue, 1, 0.6) + mesh.material.emissive.setHSL(hue, 1, 0.3) + }) + }) + + const bars = useMemo(() => { + return Array.from({ length: BAR_COUNT }, (_, i) => { + const angle = (i / BAR_COUNT) * Math.PI * 2 + const radius = 3 + return { + x: Math.cos(angle) * radius, + z: Math.sin(angle) * radius, + } + }) + }, []) + + return ( + + {bars.map((pos, i) => ( + (barsRef.current[i] = el)} + position={[pos.x, 0.5, pos.z]} + > + + + + ))} + + + + + + ) +} + +function RotatingGroup({ isPlaying, children }) { + const groupRef = useRef() + useFrame((_, delta) => { + if (groupRef.current) { + groupRef.current.rotation.y += delta * (isPlaying ? 0.4 : 0.1) + } + }) + return {children} +} + +export default function AudioVisualizer3D({ isPlaying }) { + return ( + + + + + + + + + ) +} +``` + +- [ ] **Step 2: Write HeroParticles.jsx** + +```jsx +import { useRef, useMemo } from 'react' +import { Canvas, useFrame } from '@react-three/fiber' +import * as THREE from 'three' + +const PARTICLE_COUNT = 200 + +function Particles() { + const pointsRef = useRef() + const positions = useMemo(() => { + const arr = new Float32Array(PARTICLE_COUNT * 3) + for (let i = 0; i < PARTICLE_COUNT; i++) { + arr[i * 3] = (Math.random() - 0.5) * 20 + arr[i * 3 + 1] = (Math.random() - 0.5) * 20 + arr[i * 3 + 2] = (Math.random() - 0.5) * 20 + } + return arr + }, []) + + useFrame(({ clock }) => { + if (pointsRef.current) { + pointsRef.current.rotation.y = clock.getElapsedTime() * 0.04 + pointsRef.current.rotation.x = Math.sin(clock.getElapsedTime() * 0.02) * 0.1 + } + }) + + return ( + + + + + + + ) +} + +export default function HeroParticles() { + return ( + + + + ) +} +``` + +- [ ] **Step 3: Write Hero.jsx** + +```jsx +import { useEffect, useRef, useState } from 'react' +import { motion } from 'framer-motion' +import { gsap } from 'gsap' +import AudioVisualizer3D from './AudioVisualizer3D' +import HeroParticles from './HeroParticles' + +export default function Hero({ isPlaying, onTogglePlay }) { + const headingRef = useRef() + const subRef = useRef() + + useEffect(() => { + const ctx = gsap.context(() => { + gsap.from(headingRef.current, { y: 60, opacity: 0, duration: 1, ease: 'power3.out', delay: 0.3 }) + gsap.from(subRef.current, { y: 40, opacity: 0, duration: 1, ease: 'power3.out', delay: 0.6 }) + }) + return () => ctx.revert() + }, []) + + return ( +
+ {/* Particle background */} +
+ +
+ + {/* Gradient blobs */} +
+
+ + {/* 3D Visualizer */} +
+ +
+ + {/* Content */} +
+ + Now Streaming + + +

+ Feel The{' '} + Music +
+ + In 3D + +

+ +

+ Immerse yourself in a new dimension of music. Visualize every beat, every pulse, every frequency. +

+ +
+ + {isPlaying ? '⏸ Pause Vibe' : '▶ Start Listening'} + + + + Explore Now + +
+ + {/* Scroll indicator */} + + + + + +
+
+ ) +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/components/Hero/ +git commit -m "feat: hero section with 3D audio visualizer and particles" +``` + +--- + +## Task 7: Featured Albums with 3D Cards + +**Files:** +- Create: `src/components/FeaturedAlbums/FeaturedAlbums.jsx` +- Create: `src/components/FeaturedAlbums/AlbumCard3D.jsx` + +- [ ] **Step 1: Generate screen via Stitch** + +Call `mcp__stitch__generate_screen_from_text` with: +``` +"Music app featured albums grid. Dark background. 4 album cards with 3D perspective tilt on hover. Each card: album cover image (square), album title, artist name, genre tag. Neon purple/cyan glow border on hover. Glassmorphism card background. 'Featured Albums' section heading with neon accent." +``` + +- [ ] **Step 2: Write AlbumCard3D.jsx** + +```jsx +import { useRef } from 'react' +import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion' + +export default function AlbumCard3D({ album, onPlay }) { + const cardRef = useRef() + const x = useMotionValue(0) + const y = useMotionValue(0) + const rotateX = useSpring(useTransform(y, [-0.5, 0.5], [15, -15]), { stiffness: 300, damping: 30 }) + const rotateY = useSpring(useTransform(x, [-0.5, 0.5], [-15, 15]), { stiffness: 300, damping: 30 }) + + const handleMouseMove = (e) => { + const rect = cardRef.current.getBoundingClientRect() + x.set((e.clientX - rect.left) / rect.width - 0.5) + y.set((e.clientY - rect.top) / rect.height - 0.5) + } + + const handleMouseLeave = () => { + x.set(0) + y.set(0) + } + + return ( + + {/* Glow */} +
+ + {/* Cover */} +
+ {album.title} +
+ + {/* Play overlay */} + + onPlay(album)} + className="w-14 h-14 rounded-full flex items-center justify-center" + style={{ background: album.color, boxShadow: `0 0 25px ${album.color}` }} + > + ▶ + + +
+ + {/* Info */} +
+

{album.title}

+

{album.artist}

+
+ + {album.genre} + + {album.tracks} tracks +
+
+ + ) +} +``` + +- [ ] **Step 3: Write FeaturedAlbums.jsx** + +```jsx +import { useRef } from 'react' +import { motion, useInView } from 'framer-motion' +import AlbumCard3D from './AlbumCard3D' +import { albums } from '../../data/mockData' + +export default function FeaturedAlbums({ onPlay }) { + const ref = useRef() + const inView = useInView(ref, { once: true, margin: '-100px' }) + + return ( +
+
+ +
+ +

Collection

+

+ Featured Albums +

+
+ +
+ {albums.map((album, i) => ( + + + + ))} +
+
+
+ ) +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/components/FeaturedAlbums/ +git commit -m "feat: featured albums with 3D perspective tilt cards" +``` + +--- + +## Task 8: Trending Tracks with Parallax + +**Files:** +- Create: `src/components/TrendingTracks/TrendingTracks.jsx` +- Create: `src/components/TrendingTracks/TrackRow.jsx` + +- [ ] **Step 1: Write TrackRow.jsx** + +```jsx +import { motion } from 'framer-motion' + +export default function TrackRow({ track, index, isActive, onPlay }) { + return ( + onPlay(track)} + > + {/* Rank */} + + {isActive ? '♫' : index + 1} + + + {/* Cover */} +
+ {track.title} + + ▶ + +
+ + {/* Info */} +
+

{track.title}

+

{track.artist}

+
+ + {/* Plays */} + {track.plays} + + {/* Duration */} + {track.duration} + + {/* Active bar */} + {isActive && ( +
+ {[1, 2, 3].map(i => ( + + ))} +
+ )} +
+ ) +} +``` + +- [ ] **Step 2: Write TrendingTracks.jsx** + +```jsx +import { useRef, useEffect } from 'react' +import { motion, useInView } from 'framer-motion' +import { gsap } from 'gsap' +import { ScrollTrigger } from 'gsap/ScrollTrigger' +import TrackRow from './TrackRow' +import { tracks } from '../../data/mockData' + +gsap.registerPlugin(ScrollTrigger) + +export default function TrendingTracks({ currentTrack, onPlay }) { + const sectionRef = useRef() + const ref = useRef() + const inView = useInView(ref, { once: true, margin: '-100px' }) + + useEffect(() => { + const ctx = gsap.context(() => { + gsap.from('.track-parallax', { + x: -40, + opacity: 0, + stagger: 0.08, + scrollTrigger: { + trigger: sectionRef.current, + start: 'top 80%', + end: 'bottom 20%', + }, + }) + }, sectionRef) + return () => ctx.revert() + }, []) + + return ( +
+
+ +
+ +

Hot Right Now

+

+ Trending Tracks +

+
+ +
+ {tracks.map((track, i) => ( +
+ +
+ ))} +
+
+
+ ) +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/components/TrendingTracks/ +git commit -m "feat: trending tracks section with GSAP parallax" +``` + +--- + +## Task 9: Artist Spotlight with R3F Particle Field + +**Files:** +- Create: `src/components/ArtistSpotlight/ArtistSpotlight.jsx` +- Create: `src/components/ArtistSpotlight/ArtistParticles.jsx` + +- [ ] **Step 1: Write ArtistParticles.jsx** + +```jsx +import { useRef, useMemo } from 'react' +import { Canvas, useFrame } from '@react-three/fiber' +import * as THREE from 'three' + +const COUNT = 300 + +function Particles() { + const meshRef = useRef() + const dummy = useMemo(() => new THREE.Object3D(), []) + const particles = useMemo(() => { + return Array.from({ length: COUNT }, (_, i) => ({ + phi: Math.acos(-1 + (2 * i) / COUNT), + theta: Math.sqrt(COUNT * Math.PI) * (2 * i / COUNT) * Math.PI, + speed: 0.2 + Math.random() * 0.3, + radius: 2 + Math.random() * 1.5, + })) + }, []) + + useFrame(({ clock }) => { + const t = clock.getElapsedTime() + particles.forEach((p, i) => { + const r = p.radius + Math.sin(t * p.speed + i) * 0.3 + dummy.position.set( + r * Math.sin(p.phi) * Math.cos(p.theta + t * p.speed * 0.1), + r * Math.cos(p.phi), + r * Math.sin(p.phi) * Math.sin(p.theta + t * p.speed * 0.1) + ) + dummy.scale.setScalar(0.015 + Math.abs(Math.sin(t + i)) * 0.01) + dummy.updateMatrix() + meshRef.current.setMatrixAt(i, dummy.matrix) + }) + meshRef.current.instanceMatrix.needsUpdate = true + }) + + return ( + + + + + ) +} + +export default function ArtistParticles() { + return ( + + + + ) +} +``` + +- [ ] **Step 2: Write ArtistSpotlight.jsx** + +```jsx +import { useRef } from 'react' +import { motion, useInView } from 'framer-motion' +import ArtistParticles from './ArtistParticles' +import { artists } from '../../data/mockData' + +export default function ArtistSpotlight() { + const artist = artists[0] + const ref = useRef() + const inView = useInView(ref, { once: true, margin: '-100px' }) + + return ( +
+
+ + {/* Particle field */} +
+ +
+ + {/* Gradient overlay */} +
+ +
+
+ {/* Text */} + +

Artist Spotlight

+

+ {artist.name} +

+

{artist.genre}

+

{artist.bio}

+ +
+
+

{artist.followers}

+

Followers

+
+
+
+

48

+

Tracks

+
+
+
+

6

+

Albums

+
+
+ + + Follow Artist + + + + {/* Artist image */} + +
+ {artist.name} +
+
+ +
+
+
+ ) +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/components/ArtistSpotlight/ +git commit -m "feat: artist spotlight with R3F particle sphere" +``` + +--- + +## Task 10: Persistent Player Bar with 3D Waveform + +**Files:** +- Create: `src/components/Player/Waveform3D.jsx` +- Create: `src/components/Player/PlayerBar.jsx` + +- [ ] **Step 1: Write Waveform3D.jsx** + +```jsx +import { useRef, useMemo } from 'react' +import { Canvas, useFrame } from '@react-three/fiber' +import * as THREE from 'three' + +const BAR_COUNT = 32 + +function Waveform({ isPlaying }) { + const barsRef = useRef([]) + const phases = useMemo(() => Array.from({ length: BAR_COUNT }, () => Math.random() * Math.PI * 2), []) + + useFrame(({ clock }) => { + const t = clock.getElapsedTime() + barsRef.current.forEach((mesh, i) => { + if (!mesh) return + const target = isPlaying + ? Math.abs(Math.sin(t * 3 + phases[i])) * 0.8 + 0.05 + : 0.05 + Math.abs(Math.sin(t * 0.5 + phases[i])) * 0.1 + mesh.scale.y = THREE.MathUtils.lerp(mesh.scale.y, target, 0.15) + mesh.position.y = mesh.scale.y * 0.5 + }) + }) + + return ( + + {Array.from({ length: BAR_COUNT }, (_, i) => ( + (barsRef.current[i] = el)} + position={[(i - BAR_COUNT / 2) * 0.07, 0.05, 0]} + > + + + + ))} + + ) +} + +export default function Waveform3D({ isPlaying }) { + return ( + + + + ) +} +``` + +- [ ] **Step 2: Write PlayerBar.jsx** + +```jsx +import { motion, AnimatePresence } from 'framer-motion' +import Waveform3D from './Waveform3D' + +export default function PlayerBar({ currentTrack, isPlaying, progress, volume, onToggle, onNext, onPrev, onSeek, onVolume }) { + if (!currentTrack) return null + + return ( + + +
+ {/* Track info */} +
+
+ {currentTrack.title} + {isPlaying && ( +
+ )} +
+
+

{currentTrack.title}

+

{currentTrack.artist}

+
+
+ + {/* Center: controls + waveform */} +
+ {/* Controls */} +
+ + + + + + + + {isPlaying ? ( + + + + ) : ( + + + + )} + + + + + + + +
+ + {/* Progress bar */} +
+ 0:00 +
{ + const rect = e.currentTarget.getBoundingClientRect() + onSeek((e.clientX - rect.left) / rect.width) + }} + > +
+
+
+
+ {currentTrack.duration} +
+
+ + {/* Right: waveform + volume */} +
+
+ +
+
+ + + + onVolume(parseFloat(e.target.value))} + className="w-20 accent-neon-purple" + /> +
+
+
+ + + ) +} +``` + +- [ ] **Step 3: Commit** + +```bash +git add src/components/Player/ +git commit -m "feat: persistent player bar with 3D waveform" +``` + +--- + +## Task 11: Assemble App + Run + +**Files:** +- Modify: `src/App.jsx` +- Modify: `src/main.jsx` + +- [ ] **Step 1: Write src/main.jsx** + +```jsx +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + +) +``` + +- [ ] **Step 2: Write src/App.jsx** + +```jsx +import { useState } from 'react' +import { useAudioPlayer } from './hooks/useAudioPlayer' +import Navbar from './components/Navbar/Navbar' +import Hero from './components/Hero/Hero' +import FeaturedAlbums from './components/FeaturedAlbums/FeaturedAlbums' +import TrendingTracks from './components/TrendingTracks/TrendingTracks' +import ArtistSpotlight from './components/ArtistSpotlight/ArtistSpotlight' +import PlayerBar from './components/Player/PlayerBar' + +export default function App() { + const player = useAudioPlayer() + + return ( +
+ + + + + + +
+ ) +} +``` + +- [ ] **Step 3: Update index.html with correct title** + +```html + + + + + + + CMusic — Feel The Music In 3D + + +
+ + + +``` + +- [ ] **Step 4: Start dev server and verify** + +```bash +npm run dev +``` +Expected: Server starts at http://localhost:5173. Verify all sections render, 3D visualizer animates, player bar appears, album cards tilt on hover. + +- [ ] **Step 5: Final commit** + +```bash +git add src/App.jsx src/main.jsx index.html +git commit -m "feat: assemble full CMusic 3D music website" +```