This document provides guidelines for AI coding agents working on this Synced Lyrics Player project.
A Spotify-like lyrics player that syncs audio with word-by-word highlighting.
- Framework: TanStack Start (React meta-framework built on TanStack Router)
- Language: TypeScript (strict mode)
- Runtime: Bun (preferred), Node.js compatible
- Package Manager: bun
- Build Tool: Vite 7.x
- React Version: 19.x with React Compiler
- Animations: Framer Motion (
motionpackage) - Styling: Tailwind CSS v4
bun dev # Start dev server on port 3000
bun build # Build for production
bun preview # Preview production buildbun test # Run all tests (vitest run)
bun vitest run <file> # Run a single test file
bun vitest run <file> -t "name" # Run a specific test by name
bun vitest --watch # Run tests in watch modebun lint # Run Biome linter
bun format # Run Biome formatter
bun check # Run all Biome checks (lint + format)To auto-fix issues:
bun biome check --write| File | Purpose |
|---|---|
src/routes/index.tsx |
Main lyrics player page, orchestrates all components |
src/components/LyricsDisplay.tsx |
Renders lyrics with word highlighting & animations |
src/components/AudioControls.tsx |
Play/pause button and progress slider |
src/hooks/useAudioPlayer.ts |
Custom hook for audio playback control |
src/lib/lyrics.ts |
Lyrics parsing, line splitting, timing utilities |
lyrics.json → parseLyrics() → LyricsDisplay
↓
audio.mp3 → useAudioPlayer → currentTime → word highlighting
↓
AudioControls (play/pause/seek)
Line Splitting (src/lib/lyrics.ts):
- Groups words into lines by sentence-ending punctuation (
.,!,?) - Handles ellipsis
...as single unit - Filters out
audio_eventtypes ([singing],[music]) - Calculates
gapBeforefor section spacing
Word Highlighting (src/components/LyricsDisplay.tsx):
- Word is active when
currentTime >= word.start - Uses Framer Motion
animatefor smooth color transitions - Current line scales up with
scale: 1.15(spring animation) - Distance-based opacity: further lines fade out
- Indentation: Tabs (not spaces)
- Quotes: Double quotes for strings
- Semicolons: None (ASI)
- Auto-organize imports: Enabled via Biome
- Strict mode enabled (
strict: true) - No unused locals or parameters
- Use
typeimports for type-only imports:import type { Foo } from "bar" - Path alias:
@/*maps to./src/*
| Element | Convention | Example |
|---|---|---|
| Components | PascalCase | LyricsDisplay, AudioControls |
| Functions | camelCase | parseLyrics, isWordActive |
| Types/Interfaces | PascalCase | LyricWord, LyricLine |
| Hooks | camelCase with use prefix |
useAudioPlayer |
- Third-party libraries (
motion/react,react) - Framework imports (
@tanstack/*) - Local imports using
@/alias - Type-only imports (last)
import { motion } from "motion/react"
import { useEffect, useRef } from "react"
import { createFileRoute } from "@tanstack/react-router"
import { AudioControls } from "@/components/AudioControls"
import { cn } from "@/lib/utils"
import type { LyricLine } from "@/lib/lyrics"src/
├── components/
│ ├── AudioControls.tsx # Play button + progress slider
│ ├── LyricsDisplay.tsx # Main lyrics visualization
│ └── ui/ # Shadcn UI components
├── hooks/
│ └── useAudioPlayer.ts # Audio playback hook
├── lib/
│ ├── lyrics.ts # Lyrics parsing & utilities
│ └── utils.ts # Utility functions (cn)
├── routes/
│ ├── __root.tsx # Root layout
│ └── index.tsx # Main lyrics player page
├── router.tsx # Router configuration
├── styles.css # Global Tailwind styles
└── routeTree.gen.ts # Auto-generated (DO NOT EDIT)
public/
└── song/
├── audio.mp3 # The song audio
└── lyrics.json # Word-level timing data
This project uses Framer Motion for smooth animations. Key patterns:
// Good - uses transform, no layout reflow
animate={{ scale: isCurrent ? 1.15 : 1 }}
// Bad - causes layout reflow
className={isCurrent ? "text-2xl" : "text-lg"}transition={{
scale: { type: "spring", stiffness: 300, damping: 30 },
layout: { type: "spring", stiffness: 300, damping: 30 },
}}<motion.span
animate={{
color: shouldHighlight ? "#ffffff" : "#71717a",
textShadow: isActive ? "0 0 8px rgba(255,255,255,0.4)" : "none",
}}
transition={{ duration: 0.15, ease: "easeOut" }}
>Use Tailwind's responsive prefixes:
// Mobile-first approach
className="text-base sm:text-lg md:text-xl lg:text-2xl"
className="pt-4 sm:pt-8"
className="size-12 sm:size-16"Breakpoints:
sm: 640px+md: 768px+lg: 1024px+
- DO NOT EDIT
src/routeTree.gen.ts- it's auto-generated - DO NOT EDIT
src/styles.cssdirectly for component styles - use Tailwind classes - Run
bun checkbefore committing to catch lint/format issues - Use the
@/path alias for all local imports - The lyrics data structure must match
LyricWordinterface insrc/lib/lyrics.ts