diff --git a/components/dialogs/ProjectsDialog.tsx b/components/dialogs/ProjectsDialog.tsx
new file mode 100644
index 0000000..b8cd5bd
--- /dev/null
+++ b/components/dialogs/ProjectsDialog.tsx
@@ -0,0 +1,142 @@
+'use client';
+
+import * as React from 'react';
+import { X, Plus, Trash2, Copy, FolderOpen } from 'lucide-react';
+import { Button } from '@/components/ui/Button';
+import type { ProjectMetadata } from '@/lib/storage/db';
+import { formatDuration } from '@/lib/audio/decoder';
+
+export interface ProjectsDialogProps {
+ open: boolean;
+ onClose: () => void;
+ projects: ProjectMetadata[];
+ onNewProject: () => void;
+ onLoadProject: (projectId: string) => void;
+ onDeleteProject: (projectId: string) => void;
+ onDuplicateProject: (projectId: string) => void;
+}
+
+export function ProjectsDialog({
+ open,
+ onClose,
+ projects,
+ onNewProject,
+ onLoadProject,
+ onDeleteProject,
+ onDuplicateProject,
+}: ProjectsDialogProps) {
+ if (!open) return null;
+
+ const formatDate = (timestamp: number) => {
+ return new Date(timestamp).toLocaleString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
Projects
+
+
+
+
+
+
+ {/* Projects List */}
+
+ {projects.length === 0 ? (
+
+
+
+ No projects yet
+
+
+ Create your first project to get started
+
+
+
+ ) : (
+
+ {projects.map((project) => (
+
+
+
+
+ {project.name}
+
+ {project.description && (
+
+ {project.description}
+
+ )}
+
+ {project.trackCount} tracks
+ {formatDuration(project.duration)}
+ {project.sampleRate / 1000}kHz
+ Updated {formatDate(project.updatedAt)}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/lib/storage/db.ts b/lib/storage/db.ts
new file mode 100644
index 0000000..cbe532b
--- /dev/null
+++ b/lib/storage/db.ts
@@ -0,0 +1,193 @@
+/**
+ * IndexedDB database for project storage
+ */
+
+export const DB_NAME = 'audio-editor-db';
+export const DB_VERSION = 1;
+
+export interface ProjectMetadata {
+ id: string;
+ name: string;
+ description?: string;
+ createdAt: number;
+ updatedAt: number;
+ duration: number; // Total project duration in seconds
+ sampleRate: number;
+ trackCount: number;
+ thumbnail?: string; // Base64 encoded waveform thumbnail
+}
+
+export interface SerializedAudioBuffer {
+ sampleRate: number;
+ length: number;
+ numberOfChannels: number;
+ channelData: Float32Array[]; // Array of channel data
+}
+
+export interface SerializedTrack {
+ id: string;
+ name: string;
+ color: string;
+ volume: number;
+ pan: number;
+ muted: boolean;
+ soloed: boolean;
+ collapsed: boolean;
+ height: number;
+ audioBuffer: SerializedAudioBuffer | null;
+ effects: any[]; // Effect chain
+ automation: any; // Automation data
+ recordEnabled: boolean;
+}
+
+export interface ProjectData {
+ metadata: ProjectMetadata;
+ tracks: SerializedTrack[];
+ settings: {
+ zoom: number;
+ currentTime: number;
+ sampleRate: number;
+ };
+}
+
+/**
+ * Initialize IndexedDB database
+ */
+export function initDB(): Promise {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
+
+ request.onerror = () => reject(request.error);
+ request.onsuccess = () => resolve(request.result);
+
+ request.onupgradeneeded = (event) => {
+ const db = (event.target as IDBOpenDBRequest).result;
+
+ // Create projects object store
+ if (!db.objectStoreNames.contains('projects')) {
+ const projectStore = db.createObjectStore('projects', { keyPath: 'metadata.id' });
+ projectStore.createIndex('updatedAt', 'metadata.updatedAt', { unique: false });
+ projectStore.createIndex('name', 'metadata.name', { unique: false });
+ }
+
+ // Create audio buffers object store (for large files)
+ if (!db.objectStoreNames.contains('audioBuffers')) {
+ db.createObjectStore('audioBuffers', { keyPath: 'id' });
+ }
+ };
+ });
+}
+
+/**
+ * Get all projects (metadata only for list view)
+ */
+export async function getAllProjects(): Promise {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(['projects'], 'readonly');
+ const store = transaction.objectStore('projects');
+ const index = store.index('updatedAt');
+ const request = index.openCursor(null, 'prev'); // Most recent first
+
+ const projects: ProjectMetadata[] = [];
+
+ request.onsuccess = () => {
+ const cursor = request.result;
+ if (cursor) {
+ projects.push(cursor.value.metadata);
+ cursor.continue();
+ } else {
+ resolve(projects);
+ }
+ };
+
+ request.onerror = () => reject(request.error);
+ });
+}
+
+/**
+ * Save project to IndexedDB
+ */
+export async function saveProject(project: ProjectData): Promise {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(['projects'], 'readwrite');
+ const store = transaction.objectStore('projects');
+ const request = store.put(project);
+
+ request.onsuccess = () => resolve();
+ request.onerror = () => reject(request.error);
+ });
+}
+
+/**
+ * Load project from IndexedDB
+ */
+export async function loadProject(projectId: string): Promise {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(['projects'], 'readonly');
+ const store = transaction.objectStore('projects');
+ const request = store.get(projectId);
+
+ request.onsuccess = () => resolve(request.result || null);
+ request.onerror = () => reject(request.error);
+ });
+}
+
+/**
+ * Delete project from IndexedDB
+ */
+export async function deleteProject(projectId: string): Promise {
+ const db = await initDB();
+
+ return new Promise((resolve, reject) => {
+ const transaction = db.transaction(['projects'], 'readwrite');
+ const store = transaction.objectStore('projects');
+ const request = store.delete(projectId);
+
+ request.onsuccess = () => resolve();
+ request.onerror = () => reject(request.error);
+ });
+}
+
+/**
+ * Serialize AudioBuffer for storage
+ */
+export function serializeAudioBuffer(buffer: AudioBuffer): SerializedAudioBuffer {
+ const channelData: Float32Array[] = [];
+
+ for (let i = 0; i < buffer.numberOfChannels; i++) {
+ channelData.push(new Float32Array(buffer.getChannelData(i)));
+ }
+
+ return {
+ sampleRate: buffer.sampleRate,
+ length: buffer.length,
+ numberOfChannels: buffer.numberOfChannels,
+ channelData,
+ };
+}
+
+/**
+ * Deserialize AudioBuffer from storage
+ */
+export function deserializeAudioBuffer(
+ serialized: SerializedAudioBuffer,
+ audioContext: AudioContext
+): AudioBuffer {
+ const buffer = audioContext.createBuffer(
+ serialized.numberOfChannels,
+ serialized.length,
+ serialized.sampleRate
+ );
+
+ for (let i = 0; i < serialized.numberOfChannels; i++) {
+ buffer.copyToChannel(new Float32Array(serialized.channelData[i]), i);
+ }
+
+ return buffer;
+}
diff --git a/lib/storage/projects.ts b/lib/storage/projects.ts
new file mode 100644
index 0000000..3477152
--- /dev/null
+++ b/lib/storage/projects.ts
@@ -0,0 +1,190 @@
+/**
+ * Project management service
+ */
+
+import type { Track } from '@/types/track';
+import {
+ saveProject,
+ loadProject,
+ getAllProjects,
+ deleteProject,
+ serializeAudioBuffer,
+ deserializeAudioBuffer,
+ type ProjectData,
+ type ProjectMetadata,
+ type SerializedTrack,
+} from './db';
+import { getAudioContext } from '../audio/context';
+import { generateId } from '../audio/effects/chain';
+
+/**
+ * Generate unique project ID
+ */
+export function generateProjectId(): string {
+ return `project_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+}
+
+/**
+ * Convert tracks to serialized format
+ */
+function serializeTracks(tracks: Track[]): SerializedTrack[] {
+ return tracks.map(track => ({
+ id: track.id,
+ name: track.name,
+ color: track.color,
+ volume: track.volume,
+ pan: track.pan,
+ muted: track.mute,
+ soloed: track.solo,
+ collapsed: track.collapsed,
+ height: track.height,
+ audioBuffer: track.audioBuffer ? serializeAudioBuffer(track.audioBuffer) : null,
+ effects: track.effectChain?.effects || [],
+ automation: track.automation,
+ recordEnabled: track.recordEnabled,
+ }));
+}
+
+/**
+ * Convert serialized tracks back to Track format
+ */
+function deserializeTracks(serialized: SerializedTrack[]): Track[] {
+ const audioContext = getAudioContext();
+
+ return serialized.map(track => ({
+ id: track.id,
+ name: track.name,
+ color: track.color,
+ volume: track.volume,
+ pan: track.pan,
+ mute: track.muted,
+ solo: track.soloed,
+ collapsed: track.collapsed,
+ height: track.height,
+ audioBuffer: track.audioBuffer ? deserializeAudioBuffer(track.audioBuffer, audioContext) : null,
+ effectChain: {
+ id: generateId(),
+ name: `${track.name} FX`,
+ effects: track.effects,
+ },
+ automation: track.automation,
+ recordEnabled: track.recordEnabled,
+ selected: false,
+ showEffects: false,
+ selection: null, // Reset selection on load
+ }));
+}
+
+/**
+ * Calculate total project duration
+ */
+function calculateDuration(tracks: Track[]): number {
+ let maxDuration = 0;
+
+ for (const track of tracks) {
+ if (track.audioBuffer) {
+ maxDuration = Math.max(maxDuration, track.audioBuffer.duration);
+ }
+ }
+
+ return maxDuration;
+}
+
+/**
+ * Save current project state
+ */
+export async function saveCurrentProject(
+ projectId: string | null,
+ projectName: string,
+ tracks: Track[],
+ settings: {
+ zoom: number;
+ currentTime: number;
+ sampleRate: number;
+ },
+ description?: string
+): Promise {
+ const id = projectId || generateProjectId();
+ const now = Date.now();
+
+ const metadata: ProjectMetadata = {
+ id,
+ name: projectName,
+ description,
+ createdAt: projectId ? (await loadProject(id))?.metadata.createdAt || now : now,
+ updatedAt: now,
+ duration: calculateDuration(tracks),
+ sampleRate: settings.sampleRate,
+ trackCount: tracks.length,
+ };
+
+ const projectData: ProjectData = {
+ metadata,
+ tracks: serializeTracks(tracks),
+ settings,
+ };
+
+ await saveProject(projectData);
+ return id;
+}
+
+/**
+ * Load project and restore state
+ */
+export async function loadProjectById(projectId: string): Promise<{
+ tracks: Track[];
+ settings: {
+ zoom: number;
+ currentTime: number;
+ sampleRate: number;
+ };
+ metadata: ProjectMetadata;
+} | null> {
+ const project = await loadProject(projectId);
+ if (!project) return null;
+
+ return {
+ tracks: deserializeTracks(project.tracks),
+ settings: project.settings,
+ metadata: project.metadata,
+ };
+}
+
+/**
+ * Get list of all projects
+ */
+export async function listProjects(): Promise {
+ return getAllProjects();
+}
+
+/**
+ * Delete a project
+ */
+export async function removeProject(projectId: string): Promise {
+ return deleteProject(projectId);
+}
+
+/**
+ * Duplicate a project
+ */
+export async function duplicateProject(sourceProjectId: string, newName: string): Promise {
+ const project = await loadProject(sourceProjectId);
+ if (!project) throw new Error('Project not found');
+
+ const newId = generateProjectId();
+ const now = Date.now();
+
+ const newProject: ProjectData = {
+ ...project,
+ metadata: {
+ ...project.metadata,
+ id: newId,
+ name: newName,
+ createdAt: now,
+ updatedAt: now,
+ },
+ };
+
+ await saveProject(newProject);
+ return newId;
+}