# Track Drag-to-Playlist Design ## Goal Drag a single track from the track table onto a playlist chip in the bottom bar to add that track to the playlist. ## Scope - Single track only (no multi-select drag) - Regular playlists only (smart playlists are read-only) - Duplicate adds silently ignored (DB `UNIQUE(playlistId, trackId)` constraint) ## Implementation ### 1. Custom Pasteboard Type Define a custom `NSPasteboard.PasteboardType` for track IDs to avoid collisions with the existing `.string` type used for row-reorder and the `.fileURL` type used for Finder file drops. Location: top of `TrackTableView.swift` (alongside existing private constants). ```swift private let trackIdPasteboardType = NSPasteboard.PasteboardType("com.music.trackID") ``` ### 2. Drag Source — TrackTableView Coordinator **File:** `TrackTableView.swift` **Change `pasteboardWriterForRow`:** Currently only writes when `onReorder != nil`. Change to always write the track ID under the custom type. When `onReorder` is also set, additionally write the row index under `.string` (preserving existing reorder behavior). ``` func pasteboardWriterForRow(row) -> NSPasteboardWriting? let item = NSPasteboardItem() // Always write track ID for cross-view drag if let trackId = parent.tracks[row].id { item.setString(String(trackId), forType: trackIdPasteboardType) } // Also write row index if reorder is enabled if parent.onReorder != nil { item.setString(String(row), forType: .string) } return item ``` **Register for drag types:** Add `trackIdPasteboardType` to `registerForDraggedTypes` alongside `.string`. **`validateDrop` / `acceptDrop`:** Update to read from `.string` type specifically (not just first pasteboard item string), so they continue to work for reorder and ignore the track-ID type. ### 3. Drop Target — PlaylistBarView **File:** `PlaylistBarView.swift` **New callback prop:** ```swift var onDropTrack: ((Int64, Playlist) -> Void)? ``` **Drop modifier on each regular playlist chip:** Add `.onDrop(of:isTargeted:perform:)` to each `PlaylistButton` for regular playlists only. The `isTargeted` binding drives a visual highlight (accent-colored border/background). Since SwiftUI's `.onDrop` works with UTTypes, we register the custom type as a UTType and read the track ID from the `NSItemProvider`. **Visual feedback:** When `isTargeted` is true, show a highlighted border/background on the chip (e.g. `tintColor.opacity(0.3)` background + `tintColor` border at 2pt). **PlaylistButton changes:** Add an `isDropTarget: Bool` parameter to `PlaylistButton` that controls the highlight styling. Default `false`. ### 4. Wiring — ContentView **File:** `ContentView.swift` Pass the `onDropTrack` closure to `PlaylistBarView`. The closure calls `PlaylistViewModel.addTrack(_:to:)`, wrapping in try/catch to silently handle duplicates. ## Files Changed | File | Change | |------|--------| | `TrackTableView.swift` | Custom pasteboard type constant; `pasteboardWriterForRow` writes track ID always + row index when reordering; register custom drag type; update `validateDrop`/`acceptDrop` to read `.string` explicitly | | `PlaylistBarView.swift` | `onDropTrack` callback; `.onDrop` modifier on regular playlist chips; `isDropTarget` highlight state on `PlaylistButton` | | `ContentView.swift` | Wire `onDropTrack` closure through to `PlaylistBarView` | ## Behaviors Preserved - Right-click "Add to Playlist" context menu unchanged - Drag reorder within a playlist unchanged - File drops from Finder unchanged - Smart playlists don't accept drops