diff --git a/apps/web/src/components/common/url-import-modal.tsx b/apps/web/src/components/common/url-import-modal.tsx
new file mode 100644
index 00000000..3c6a7f98
--- /dev/null
+++ b/apps/web/src/components/common/url-import-modal.tsx
@@ -0,0 +1,215 @@
+import { AlertCircle, Check, Clock, Link, Loader2, RotateCw, X } from "lucide-react";
+import { useCallback, useState } from "react";
+import { type UrlImportEntry, useUrlImport } from "@/hooks/use-url-import";
+import { extractUrls } from "@/lib/url-parser";
+
+// ── Types ──────────────────────────────────────────────────────
+
+interface UrlImportModalProps {
+ onClose: () => void;
+ onImport: (files: File[]) => void;
+}
+
+// ── Helpers ────────────────────────────────────────────────────
+
+function StatusIcon({ status }: { status: UrlImportEntry["status"] }) {
+ switch (status) {
+ case "pending":
+ return ;
+ case "fetching":
+ return ;
+ case "ready":
+ return ;
+ case "failed":
+ return ;
+ }
+}
+
+function formatSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+function filenameFromUrl(url: string): string {
+ try {
+ return new URL(url).pathname.split("/").pop() || url;
+ } catch {
+ return url;
+ }
+}
+
+// ── Component ──────────────────────────────────────────────────
+
+export function UrlImportModal({ onClose, onImport }: UrlImportModalProps) {
+ const [text, setText] = useState("");
+ const [adding, setAdding] = useState(false);
+
+ const { entries, importing, importUrls, addReadyFiles, retryUrl, cancel, reset, readyCount } =
+ useUrlImport();
+
+ const hasResults = entries.length > 0;
+
+ const handleImport = useCallback(() => {
+ const urls = extractUrls(text);
+ if (urls.length === 0) return;
+ importUrls(urls);
+ }, [text, importUrls]);
+
+ const handleAdd = useCallback(async () => {
+ if (readyCount === 0) return;
+ setAdding(true);
+ try {
+ const files = await addReadyFiles();
+ if (files.length > 0) {
+ onImport(files);
+ onClose();
+ }
+ } finally {
+ setAdding(false);
+ }
+ }, [readyCount, addReadyFiles, onImport, onClose]);
+
+ const handleBack = useCallback(() => {
+ reset();
+ }, [reset]);
+
+ const handleClose = useCallback(() => {
+ cancel();
+ onClose();
+ }, [cancel, onClose]);
+
+ return (
+
+ {/* Overlay */}
+
+
+ {/* Modal card */}
+
+ {/* Header */}
+
+
+
Import from URLs
+
+
+
+ {/* Body */}
+
+
+ {/* Footer */}
+
+
+ {hasResults && !importing ? `${readyCount} of ${entries.length} ready` : ""}
+
+
+ {hasResults && !importing ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ );
+}