"use client"; import { useCallback, useRef, useState, type ReactNode } from "react"; const IMAGE_EXTS = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif"]); const AUDIO_EXTS = new Set([".mp3", ".wav", ".flac", ".m4a", ".aac", ".ogg"]); export function splitMediaFiles(files: FileList | File[]): { images: File[]; audios: File[]; ignored: string[]; } { const images: File[] = []; const audios: File[] = []; const ignored: string[] = []; for (const file of Array.from(files)) { const name = file.name.toLowerCase(); const dot = name.lastIndexOf("."); const ext = dot >= 0 ? name.slice(dot) : ""; const mime = file.type.toLowerCase(); if (IMAGE_EXTS.has(ext) || mime.startsWith("image/")) { images.push(file); } else if (AUDIO_EXTS.has(ext) || mime.startsWith("audio/")) { audios.push(file); } else { ignored.push(file.name); } } return { images, audios, ignored }; } type Props = { disabled?: boolean; acceptAudio: string; onFiles: (payload: { images: File[]; audios: File[] }) => void; }; const IMAGE_ACCEPT = "image/jpeg,image/png,image/webp,image/gif,.jpg,.jpeg,.png,.webp,.gif"; function FileHitArea({ disabled, accept, multiple, onFiles, children, className, }: { disabled?: boolean; accept: string; multiple?: boolean; onFiles: (list: FileList | null) => void; children: ReactNode; className?: string; }) { const inputRef = useRef(null); return (
{ onFiles(e.target.files); e.target.value = ""; }} />
{children}
); } /** * Mobile: two full-size native file inputs (cover vs audio). Mixed accept + hidden * inputs commonly fail to open the picker in iOS Safari / in-app browsers. * Desktop: one drop zone with overlay input + drag-and-drop. */ export function MediaDropzone({ disabled, acceptAudio, onFiles }: Props) { const [dragging, setDragging] = useState(false); const handleFiles = useCallback( (list: FileList | File[] | null) => { if (!list || disabled) return; const { images, audios, ignored } = splitMediaFiles(list); if (images.length || audios.length) { onFiles({ images, audios }); return; } if (ignored.length) { onFiles({ images: [], audios: [] }); } }, [disabled, onFiles], ); const dropClass = disabled ? "cursor-not-allowed border-gray-800 bg-surface-light/20 opacity-60" : dragging ? "border-accent bg-accent/10" : "border-gray-600 bg-surface-light/30 hover:border-accent/60 hover:bg-accent/[0.06]"; return (
handleFiles(list)} >

Tap to add cover image

JPG, PNG, WebP, or GIF

handleFiles(list)} >

Tap to add audio

Free: MP3. Pro: WAV / FLAC / AAC / OGG

{ e.preventDefault(); e.stopPropagation(); if (!disabled) setDragging(true); }} onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); if (!disabled) setDragging(true); }} onDragLeave={(e) => { e.preventDefault(); e.stopPropagation(); setDragging(false); }} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); setDragging(false); handleFiles(e.dataTransfer.files); }} > handleFiles(list)} >

Drag & drop audio and a cover image, or tap to browse

One cover and one or more tracks. Free: MP3 ≤50 MB. Pro: WAV/FLAC ≤500 MB.

); }