// Vanday DAM — Quick-upload flow
//   • UploadDestinationModal  — appears when user hits any Upload button.
//     Confirms the current folder (or asks them to pick one), and lets them
//     set batch metadata (tags, license, photographer) and AI processing
//     options before files are queued.
//   • UploadDock              — floating bottom-right progress panel.
//     Persists across page navigation because it lives in App.

// Format chip helper — pulls the right label/color from the filename.
function fmtBadge(name) {
  const ext = (name || "").split(".").pop().toLowerCase();
  const map = {
    nef:  { label: "RAW",  kind: "raw"   },
    cr2:  { label: "RAW",  kind: "raw"   },
    arw:  { label: "RAW",  kind: "raw"   },
    psd:  { label: "PSD",  kind: "psd"   },
    pdf:  { label: "PDF",  kind: "doc"   },
    doc:  { label: "DOC",  kind: "doc"   },
    docx: { label: "DOCX", kind: "doc"   },
    ppt:  { label: "PPT",  kind: "ppt"   },
    pptx: { label: "PPTX", kind: "ppt"   },
    // HEIC files are auto-converted to JPG on upload, so showing a "HEIC"
    // badge in the picker misleads users into thinking the format sticks.
    // Treat them like regular images (no badge); the picker just shows the
    // thumbnail and filename, same as a native JPG.
    mp4:  { label: "MP4",  kind: "video" },
    mov:  { label: "MOV",  kind: "video" },
    webm: { label: "WEBM", kind: "video" },
  };
  return map[ext] || null;
}
window.fmtBadge = fmtBadge;

// Largest single file the server accepts (keep in sync with MAX_UPLOAD_BYTES in
// server/index.js). We check this in the browser BEFORE uploading so an
// oversized file is skipped up front — the good files in the same batch still
// upload, instead of the whole request being aborted mid-stream.
const MAX_UPLOAD_MB = 200;

const SAMPLE_UPLOADS = [
  { name: "spring-26-DSC_0411.NEF",   size: 38.2, thumb: "https://images.unsplash.com/photo-1500382017468-9049fed747ef?w=240&q=70&auto=format&fit=crop" },
  { name: "spring-26-lookbook.pdf",   size: 12.4, thumb: null },
  { name: "studio-hero-01.heic",       size: 5.1,  thumb: "https://images.unsplash.com/photo-1556228720-195a672e8a03?w=240&q=70&auto=format&fit=crop" },
  { name: "product-walkthrough.mp4",   size: 22.7, thumb: "https://images.unsplash.com/photo-1620916566398-39f1143ab7be?w=240&q=70&auto=format&fit=crop", duration: "1:24" },
  { name: "brand-guidelines-v3.docx",  size: 2.8,  thumb: null },
  { name: "investor-deck-q2.pptx",     size: 8.6,  thumb: null },
  { name: "behind-the-shoot.mov",      size: 48.1, thumb: "https://images.unsplash.com/photo-1529390079861-591de354faf5?w=240&q=70&auto=format&fit=crop", duration: "0:42" },
  { name: "ceramics-flatlay-08.jpg",   size: 3.9,  thumb: "https://images.unsplash.com/photo-1610701596007-11502861dcfa?w=240&q=70&auto=format&fit=crop" },
];

// ============================================================
// UploadDestinationModal
// ============================================================
function UploadDestinationModal({
  contextFolder,  // folder id the user is currently viewing (or "all")
  onCancel,
  onConfirm,      // ({ folderId, files }) => void
}) {
  // Pre-select the current folder if the user was viewing one. "All assets"
  // doesn't count — they should pick a real bucket then.
  const initialDest =
    contextFolder && contextFolder !== "all" && contextFolder.indexOf("__") !== 0
      ? contextFolder
      : "raw";
  const [destFolder, setDestFolder] = React.useState(initialDest);

  // Batch metadata that applies to every file in this upload.
  const [appliedTags, setAppliedTags] = React.useState([]);
  const [license, setLicense] = React.useState("internal");
  const [photographer, setPhotographer] = React.useState("");
  const [autoTag, setAutoTag] = React.useState(true);
  const [autoCrop, setAutoCrop] = React.useState(true);
  const [matchDup, setMatchDup] = React.useState(true);
  const [showAdvanced, setShowAdvanced] = React.useState(false);

  // Google Drive import — a second way in, alongside drag/drop + file picker.
  // The button only appears once the user's Drive is connected (per-user), so a
  // check on open decides whether to show it. driveBusy guards the click while
  // the Picker is open + files import; driveNotice surfaces a non-close result.
  const [driveConnected, setDriveConnected] = React.useState(false);
  const [driveBusy, setDriveBusy] = React.useState(false);
  const [driveNotice, setDriveNotice] = React.useState(null); // { ok, msg }
  // Files picked from Google Drive but NOT yet imported — [{ id, name }]. They
  // list alongside dropped files and import (to the chosen folder) on the
  // Upload click, so Drive matches the review-then-confirm flow.
  const [driveFiles, setDriveFiles] = React.useState([]);
  const [confirming, setConfirming] = React.useState(false);
  // Live progress while importing Drive picks one-by-one: { done, total }.
  const [driveProgress, setDriveProgress] = React.useState(null);

  // Real file objects picked by the user. Each is a browser `File` plus a
  // synthesised preview (object URL) so the modal can show a thumbnail.
  const [files, setFiles] = React.useState([]);
  // Files the user tried to add that exceed MAX_UPLOAD_MB — surfaced in a
  // banner and NOT queued for upload. [{ name, mb }]
  const [oversized, setOversized] = React.useState([]);
  const fileInputRef = React.useRef(null);

  // Drag-state for the drop zone. Tracking enter/leave counts because
  // dragenter/leave fire on every nested element, which would otherwise
  // flicker the highlight when a user drags over interior content.
  const [isDragOver, setIsDragOver] = React.useState(false);
  const dragDepth = React.useRef(0);

  // Safari has a well-known behavior that intercepts <input type="file">
  // image uploads, renames them to tempImage*.jpg, AND silently re-encodes
  // them to a lower quality / smaller file. For a DAM that's data loss —
  // users uploading a 20 MB original get a 2 MB JPEG with stripped IPTC.
  //
  // Drag-and-drop from Finder uses a different browser API path that
  // Safari does NOT touch, so files arrive intact. We detect Safari and
  // surface a banner that nudges users to drag instead of click.
  const isSafari = React.useMemo(() => {
    if (typeof navigator === "undefined") return false;
    const ua = navigator.userAgent || "";
    // Chrome and Edge both include "Safari" in their UA — exclude them.
    return /Safari/.test(ua) && !/Chrome|Chromium|Edg|OPR|Brave/.test(ua);
  }, []);

  // "New folder" inline create flow.
  const [newFolderOpen, setNewFolderOpen] = React.useState(false);
  const [newFolderName, setNewFolderName] = React.useState("");
  const [creatingFolder, setCreatingFolder] = React.useState(false);
  const [folderError, setFolderError] = React.useState(null);

  const submitNewFolder = React.useCallback(async () => {
    const name = newFolderName.trim();
    if (!name) return;
    setCreatingFolder(true);
    setFolderError(null);
    try {
      const created = await window.VandayAPI.createFolder(name);
      // Append the new folder to the global FOLDERS list so the modal's
      // list re-renders with it included.
      if (Array.isArray(window.FOLDERS)) {
        window.FOLDERS.push({
          id: created.id, name: created.name, count: 0, icon: "folder",
          parent: null, isServer: true,
        });
      }
      setDestFolder(created.id);
      setNewFolderName("");
      setNewFolderOpen(false);
    } catch (err) {
      setFolderError(err?.message || "Couldn't create folder");
    } finally {
      setCreatingFolder(false);
    }
  }, [newFolderName]);

  // Open the OS file picker so the user can actually select images.
  const openPicker = React.useCallback(() => {
    if (fileInputRef.current) fileInputRef.current.click();
  }, []);

  // When the picker returns files, wrap them with display fields and merge
  // into the current list. We keep both the underlying File (for upload) and
  // a small thumb URL (for the modal preview).
  const onFilesPicked = React.useCallback((eventOrList) => {
    const fl = eventOrList?.target?.files || eventOrList;
    if (!fl || fl.length === 0) return;
    const MAX_BYTES = MAX_UPLOAD_MB * 1024 * 1024;
    // Only build a preview thumbnail for formats the browser can actually
    // render — a PSD/RAW would otherwise show a broken <img> in the modal.
    const RENDERABLE = /^image\/(jpeg|png|gif|webp|svg\+xml|bmp)$/;
    const ok = [];
    const tooBig = [];
    for (const f of Array.from(fl)) {
      // Reject oversized files here so they never enter the batch. multer aborts
      // the WHOLE multipart request on the first too-big file, which would take
      // the good files down with it; skipping up front keeps the rest uploading.
      if (f.size > MAX_BYTES) {
        tooBig.push({ name: f.name, mb: f.size / (1024 * 1024) });
        continue;
      }
      ok.push({
        _file: f,
        name: f.name,
        size: f.size / (1024 * 1024),
        thumb: RENDERABLE.test(f.type) ? URL.createObjectURL(f) : null,
      });
    }
    if (ok.length) setFiles((fs) => [...fs, ...ok]);
    if (tooBig.length) setOversized((prev) => [...prev, ...tooBig]);
    // Reset the input so picking the same file again still fires onChange.
    if (fileInputRef.current) fileInputRef.current.value = "";
  }, []);

  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onCancel(); };
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [onCancel]);

  const contextWasFolder =
    contextFolder && contextFolder !== "all" && contextFolder.indexOf("__") !== 0;
  const contextFolderObj = FOLDERS.find((f) => f.id === contextFolder);

  const totalSize = files.reduce((s, f) => s + f.size, 0);

  // Drag-and-drop handlers on the modal body. Drop-only — we don't try to
  // open the file picker on drag, we just collect dropped files. Using a
  // depth counter so a drag over a child element doesn't toggle the
  // highlight off and on every frame (each child fires its own
  // dragenter/leave). Only the outermost enter/leave counts.
  const onDragEnter = React.useCallback((e) => {
    if (!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes("Files")) return;
    e.preventDefault();
    dragDepth.current += 1;
    setIsDragOver(true);
  }, []);
  const onDragOver = React.useCallback((e) => {
    if (!e.dataTransfer || !Array.from(e.dataTransfer.types || []).includes("Files")) return;
    e.preventDefault();
    e.dataTransfer.dropEffect = "copy";
  }, []);
  const onDragLeave = React.useCallback((e) => {
    dragDepth.current = Math.max(0, dragDepth.current - 1);
    if (dragDepth.current === 0) setIsDragOver(false);
  }, []);
  const onDrop = React.useCallback((e) => {
    if (!e.dataTransfer) return;
    e.preventDefault();
    dragDepth.current = 0;
    setIsDragOver(false);
    const fl = e.dataTransfer.files;
    if (fl && fl.length) onFilesPicked(fl);
  }, [onFilesPicked]);

  const authHeaders = React.useCallback(async () => {
    try {
      const t = window.Clerk?.session ? await window.Clerk.session.getToken() : null;
      return t ? { Authorization: `Bearer ${t}` } : {};
    } catch { return {}; }
  }, []);

  // Is this user's Google Drive connected? Decides whether to show the import
  // button. Best-effort — a failure just hides it.
  React.useEffect(() => {
    let alive = true;
    (async () => {
      try {
        const r = await fetch("/api/integrations", { headers: await authHeaders() });
        const b = await r.json().catch(() => ({}));
        if (alive && r.ok) {
          setDriveConnected((b.connections || []).some((c) => c.provider === "google_drive"));
        }
      } catch {}
    })();
    return () => { alive = false; };
  }, [authHeaders]);

  // Open the Google Picker and import the chosen files. On a real import we
  // refresh the library (bumpRev) and close the modal so the new assets show;
  // "nothing new" / errors stay in the modal as a small notice.
  // Open the Picker and add the chosen files to the list (deduped). Does NOT
  // import — that happens on the Upload click, to the selected folder.
  const pickFromDrive = async () => {
    setDriveBusy(true);
    setDriveNotice(null);
    try {
      const sel = await openDriveFilePicker({ authHeaders });
      if (sel.cancelled) return;
      setDriveFiles((prev) => {
        const seen = new Set(prev.map((f) => f.id));
        return [...prev, ...sel.files.filter((f) => !seen.has(f.id))];
      });
    } catch (e) {
      setDriveNotice({ ok: false, msg: e.message || "Couldn't open Google Drive — please try again." });
    } finally {
      setDriveBusy(false);
    }
  };

  // The Upload click: import any Drive picks to the chosen folder, then hand any
  // dropped/browsed files to the normal upload flow (which closes the modal).
  // Drive-only closes here after import (library refreshes via bumpRev).
  const confirmUpload = async () => {
    if (driveFiles.length) {
      const n = driveFiles.length;
      setConfirming(true);
      setDriveNotice(null);
      setDriveProgress({ done: 0, total: n });
      // Import one file at a time so we can show real progress ("3 of 5").
      let imported = 0, skipped = 0, errors = 0, hardError = null;
      for (let i = 0; i < driveFiles.length; i++) {
        try {
          const r = await importDriveFileIds({ authHeaders, fileIds: [driveFiles[i].id], folder: destFolder });
          imported += r.imported; skipped += r.skipped; errors += r.errors;
        } catch (e) {
          errors += 1;
          hardError = e.message || "Import failed — please try again.";
        }
        setDriveProgress({ done: i + 1, total: n });
      }
      setConfirming(false);
      setDriveProgress(null);
      // Nothing new landed — explain why instead of silently closing. The most
      // common case: the files are already in the library (import de-dupes by
      // Drive file, workspace-wide), so re-picking them imports nothing.
      if (!imported) {
        const msg = hardError && !skipped
          ? hardError
          : skipped
            ? (skipped === n
                ? `${n === 1 ? "That file is" : "Those files are"} already in your library.`
                : `${skipped} of those are already in your library — nothing new to add.`)
            : errors
              ? `${errors === n ? (n === 1 ? "That file" : "Those files") : `${errors} of those`} couldn't be imported — try a different file.`
              : "Nothing was imported.";
        setDriveNotice({ ok: false, msg });
        setDriveFiles([]);
        return; // keep the modal open with the explanation
      }
      try { window.VandayServer?.bumpRev?.(); } catch {}
      // Imported at least one — fall through to close (and upload any dropped
      // files). The new assets appear in the library when the modal closes.
    }
    if (files.length) {
      onConfirm({ folderId: destFolder, files }); // runs the upload dock + closes
      return;
    }
    onCancel();
  };

  return (
    <div className="modal-backdrop" onClick={onCancel}>
      <div
        className="modal qu-modal"
        onClick={(e) => e.stopPropagation()}
        onDragEnter={onDragEnter}
        onDragOver={onDragOver}
        onDragLeave={onDragLeave}
        onDrop={onDrop}
        style={isDragOver ? {
          outline: "2px dashed var(--accent)",
          outlineOffset: -8,
          background: "color-mix(in oklch, var(--accent) 5%, var(--surface))",
        } : undefined}
      >
        <div className="modal-head">
          <div>
            <div style={{ display: "inline-flex", alignItems: "center", gap: 8, fontSize: 11, textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--text-faint)", fontWeight: 500, marginBottom: 4 }}>
              <IcUpload size={11} /> Upload
            </div>
            <h2 className="modal-title">
              {contextWasFolder
                ? `Upload to ${contextFolderObj?.name}?`
                : "Choose a destination"}
            </h2>
            <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginTop: 4 }}>
              {(() => {
                const n = files.length + driveFiles.length;
                const label = `${n} file${n === 1 ? "" : "s"} ready`;
                return contextWasFolder
                  ? `${label}. Confirm or pick a different folder.`
                  : `${label}. Pick where to put them.`;
              })()}
            </div>
          </div>
          <button className="btn ghost sm" onClick={onCancel}><IcClose size={14} /></button>
        </div>

        <div className="qu-body">

          {oversized.length > 0 && (
            <div
              role="status"
              style={{
                display: "flex", flexDirection: "column", gap: 4,
                padding: "10px 12px", marginBottom: 14,
                background: "color-mix(in oklch, oklch(0.55 0.18 24) 10%, transparent)",
                border: "1px solid color-mix(in oklch, oklch(0.55 0.18 24) 32%, transparent)",
                borderRadius: 8, fontSize: 12.5, color: "var(--text)", lineHeight: 1.5,
              }}
            >
              <strong>
                {oversized.length} file{oversized.length === 1 ? "" : "s"} skipped — over the {MAX_UPLOAD_MB} MB limit
              </strong>
              <span style={{ color: "var(--text-muted)" }}>
                {oversized.map((o) => `${o.name} (${Math.round(o.mb)} MB)`).join(", ")}
              </span>
              <span style={{ color: "var(--text-muted)" }}>
                Shrink {oversized.length === 1 ? "it" : "them"} (or split into smaller files) and try again — the rest are ready to go.
              </span>
            </div>
          )}

          {isSafari && (
            <div
              role="status"
              style={{
                display: "flex", alignItems: "flex-start", gap: 10,
                padding: "10px 12px",
                marginBottom: 14,
                background: "color-mix(in oklch, var(--accent, #c8553d) 10%, transparent)",
                border: "1px solid color-mix(in oklch, var(--accent, #c8553d) 30%, transparent)",
                borderRadius: 8,
                fontSize: 12.5,
                color: "var(--text)",
                lineHeight: 1.5,
              }}
            >
              <span aria-hidden="true" style={{ fontSize: 14, lineHeight: 1, paddingTop: 1 }}>⚠️</span>
              <span>
                <strong style={{ fontWeight: 500 }}>Safari detected.</strong>{" "}
                For full-quality originals (and to keep IPTC metadata intact),
                <strong style={{ fontWeight: 500 }}> drag files from Finder directly into this window</strong>.
                Clicking "Choose files" in Safari may downsample images and strip metadata.
                Chrome, Firefox, and Edge don't have this limitation.
              </span>
            </div>
          )}

          {isDragOver && (
            <div
              aria-live="polite"
              style={{
                padding: "14px 16px",
                marginBottom: 14,
                background: "var(--accent-soft, #fbe6df)",
                border: "2px dashed var(--accent, #c8553d)",
                borderRadius: 10,
                color: "var(--accent, #c8553d)",
                fontWeight: 500,
                textAlign: "center",
                fontSize: 13,
              }}
            >
              Drop your files to add them to this upload.
            </div>
          )}

          {/* Files about to upload */}
          <div className="qu-section">
            <div className="qu-section-h">
              <span>Files</span>
              <span className="qu-section-meta">
                {files.length + driveFiles.length} file{(files.length + driveFiles.length) === 1 ? "" : "s"}
                {files.length > 0 ? ` · ${totalSize.toFixed(1)} MB` : ""}
              </span>
            </div>
            <div className="qu-files">
              {files.map((f, i) => {
                const badge = fmtBadge(f.name);
                return (
                  <div className="qu-file" key={i}>
                    {f.thumb ? (
                      <div className="qu-file-thumb" style={{ backgroundImage: `url(${f.thumb})` }}>
                        {badge && <span className={`qu-file-badge is-${badge.kind}`}>{badge.label}</span>}
                      </div>
                    ) : (
                      <div className={`qu-file-doc is-${(badge?.kind || "doc")}`}>
                        {badge?.label || "FILE"}
                      </div>
                    )}
                    <div style={{ minWidth: 0, flex: 1 }}>
                      <div className="qu-file-name">{f.name}</div>
                      <div className="qu-file-meta">
                        {f.size.toFixed(1)} MB{f.duration ? ` · ${f.duration}` : ""}
                      </div>
                    </div>
                    <button
                      className="qu-file-remove"
                      onClick={() => setFiles((fs) => fs.filter((_, j) => j !== i))}
                      title="Remove"
                    >
                      <IcClose size={12} />
                    </button>
                  </div>
                );
              })}
              {driveFiles.map((f) => (
                <div className="qu-file" key={"drive-" + f.id}>
                  <div className="qu-file-doc is-doc">GD</div>
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div className="qu-file-name">{f.name}</div>
                    <div className="qu-file-meta">From Google Drive</div>
                  </div>
                  <button
                    className="qu-file-remove"
                    onClick={() => setDriveFiles((fs) => fs.filter((x) => x.id !== f.id))}
                    title="Remove"
                  >
                    <IcClose size={12} />
                  </button>
                </div>
              ))}
              <input
                ref={fileInputRef}
                type="file"
                multiple
                // Explicit extension list rather than the image/* wildcard.
                // Safari's image-upload optimization (downsample +
                // metadata strip) triggers off the image/* MIME wildcard
                // in the accept attribute. Listing explicit extensions
                // sometimes bypasses that path. Even when it doesn't,
                // drag-and-drop (handled on the modal container) is
                // unaffected and is now the recommended Safari path.
                accept=".jpg,.jpeg,.png,.gif,.webp,.tif,.tiff,.heic,.heif,.svg,.psd,.psb,.pdf,.eps,.ai,.mp4,.mov,.webm,.zip"
                style={{ display: "none" }}
                onChange={onFilesPicked}
              />
              <button className="qu-add-more" onClick={openPicker}>
                <IcPlus size={12} /> {files.length === 0 ? "Choose files or drag from Finder" : "Add more files"}
              </button>
              {driveConnected && (
                <button
                  className="qu-add-more"
                  onClick={pickFromDrive}
                  disabled={driveBusy}
                  style={driveBusy ? { opacity: 0.6, cursor: "wait" } : undefined}
                >
                  <IcPlus size={12} /> {driveBusy ? "Opening Google Drive…" : "Choose from Google Drive"}
                </button>
              )}
            </div>
            {driveNotice && (
              <div
                role="status"
                style={{
                  marginTop: 10, padding: "9px 12px", borderRadius: 8, fontSize: 12.5,
                  background: driveNotice.ok ? "color-mix(in oklch, #16a34a 12%, transparent)" : "color-mix(in oklch, var(--accent, #c8553d) 12%, transparent)",
                  border: `1px solid ${driveNotice.ok ? "color-mix(in oklch, #16a34a 35%, transparent)" : "color-mix(in oklch, var(--accent, #c8553d) 35%, transparent)"}`,
                  color: "var(--text)",
                }}
              >
                {driveNotice.ok ? "" : "⚠️ "}{driveNotice.msg}
              </div>
            )}
            {driveProgress && (
              <div style={{ marginTop: 10 }} role="status" aria-live="polite">
                <div style={{ fontSize: 12.5, color: "var(--text-muted)", marginBottom: 6 }}>
                  Importing from Google Drive… {driveProgress.done} of {driveProgress.total}
                </div>
                <div style={{ height: 6, borderRadius: 999, background: "var(--border)", overflow: "hidden" }}>
                  <div
                    style={{
                      height: "100%",
                      width: `${Math.round((driveProgress.done / Math.max(1, driveProgress.total)) * 100)}%`,
                      background: "var(--accent, #c8553d)",
                      transition: "width 200ms ease",
                    }}
                  />
                </div>
              </div>
            )}
          </div>

          {/* Destination */}
          <div className="qu-section">
            <div className="qu-section-h">
              <span>Destination folder</span>
            </div>
            <div className="qu-folders">
              {(() => {
                // Flatten into a depth-aware list so we can render in tree order.
                const rows = [];
                const walk = (parent, depth) => {
                  (parent === null ? topLevelFolders() : childFolders(parent)).forEach((f) => {
                    rows.push({ f, depth });
                    walk(f.id, depth + 1);
                  });
                };
                walk(null, 0);
                return rows.map(({ f, depth }) => (
                  <button
                    key={f.id}
                    className={`qu-folder ${destFolder === f.id ? "is-on" : ""}`}
                    onClick={() => setDestFolder(f.id)}
                    style={{ paddingLeft: 10 + depth * 16 }}
                  >
                    <span className="qu-folder-ic">
                      <IcFolder size={depth === 0 ? 14 : 12} />
                    </span>
                    <span className="qu-folder-name">{f.name}</span>
                    {f.id === contextFolder && (
                      <span className="qu-folder-here">Current</span>
                    )}
                    <span className="qu-folder-count">{f.count}</span>
                  </button>
                ));
              })()}
            </div>
            {!newFolderOpen ? (
              <button className="qu-link" onClick={() => setNewFolderOpen(true)}>
                <IcPlus size={11} /> New folder
              </button>
            ) : (
              <div style={{ display: "flex", gap: 6, marginTop: 8, alignItems: "center" }}>
                <input
                  type="text"
                  autoFocus
                  placeholder="Folder name"
                  value={newFolderName}
                  onChange={(e) => setNewFolderName(e.target.value)}
                  onKeyDown={(e) => {
                    if (e.key === "Enter") submitNewFolder();
                    if (e.key === "Escape") { setNewFolderOpen(false); setNewFolderName(""); setFolderError(null); }
                  }}
                  maxLength={60}
                  disabled={creatingFolder}
                  style={{
                    flex: 1, padding: "6px 10px", fontSize: 13,
                    background: "var(--surface)", color: "var(--text)",
                    border: "1px solid var(--border)", borderRadius: 6, outline: "none",
                  }}
                />
                <button
                  className="btn primary sm"
                  onClick={submitNewFolder}
                  disabled={!newFolderName.trim() || creatingFolder}
                  style={{ padding: "6px 12px", fontSize: 12 }}
                >
                  {creatingFolder ? "Creating…" : "Create"}
                </button>
                <button
                  className="btn ghost sm"
                  onClick={() => { setNewFolderOpen(false); setNewFolderName(""); setFolderError(null); }}
                  disabled={creatingFolder}
                  style={{ padding: "6px 10px", fontSize: 12 }}
                >
                  Cancel
                </button>
              </div>
            )}
            {folderError && (
              <div style={{ color: "oklch(0.55 0.18 24)", fontSize: 12, marginTop: 6 }}>
                {folderError}
              </div>
            )}
          </div>
        </div>

        <div className="modal-foot" style={{ background: "var(--surface)" }}>
          <div style={{ fontSize: 12, color: "var(--text-muted)" }}>
            <IcSparkles size={11} style={{ verticalAlign: "middle", marginRight: 4, color: "var(--accent)" }} />
            Images & video are auto-tagged and cropped. Docs are indexed for search.
          </div>
          <div style={{ display: "flex", gap: 8 }}>
            <button className="btn" onClick={onCancel}>Cancel</button>
            <button
              className="btn accent"
              disabled={(files.length + driveFiles.length) === 0 || confirming || driveBusy}
              style={((files.length + driveFiles.length) === 0 || confirming || driveBusy) ? { opacity: 0.5, cursor: "not-allowed" } : undefined}
              onClick={confirmUpload}
            >
              <IcUpload size={13} />
              {confirming
                ? (driveProgress ? `Importing ${driveProgress.done} of ${driveProgress.total}…` : "Adding…")
                : `Upload ${files.length + driveFiles.length} to ${FOLDERS.find((f) => f.id === destFolder)?.name}`}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// UploadDock — bottom-right progress panel
// ============================================================
function UploadDock({ queue, onDismiss, onRetry, onOpenAsset }) {
  const [collapsed, setCollapsed] = React.useState(false);

  // Auto-dismiss once everything has finished uploading successfully. We
  // leave it on screen for a few seconds so the "all uploaded" state is
  // Auto-dismiss the dock once every file has settled into a terminal
  // state — whether it succeeded or failed. Previously we only dismissed
  // on full success, which meant a partial-failure dock stuck around
  // forever. Failed entries still surface in the recent-uploads modal /
  // retry flow, so leaving the dock up wasn't actually helping users.
  // Hook must run before the early return below to keep hook order stable.
  const allProcessed = queue.length > 0
    && queue.every((q) => q.status === "ready" || q.status === "failed");
  // Only auto-dismiss when EVERY file succeeded. If any failed, leave the dock
  // up so the error stays readable — the user clears it by hand (the "Clear"
  // button in the footer) or retries. Previously we dismissed on any terminal
  // state, so a failure flashed by too quickly to read.
  const allSucceeded = queue.length > 0 && queue.every((q) => q.status === "ready");
  React.useEffect(() => {
    if (!allSucceeded) return;
    // 2.5s after the last file settles — long enough to register the
    // "All N files processed" success state, short enough that the dock
    // doesn't linger after the user has visibly moved on.
    const t = setTimeout(() => { onDismiss && onDismiss(); }, 2500);
    return () => clearTimeout(t);
  }, [allSucceeded, onDismiss]);

  if (queue.length === 0) return null;

  const inFlight = queue.filter((q) => q.status === "uploading" || q.status === "analyzing");
  const done     = queue.filter((q) => q.status === "ready");
  const failed   = queue.filter((q) => q.status === "failed");
  const total    = queue.length;
  const summary =
    inFlight.length > 0       ? `Uploading ${inFlight.length} of ${total}` :
    allProcessed && failed.length === 0
                              ? `All ${total} file${total === 1 ? "" : "s"} processed` :
    allProcessed              ? `${done.length} of ${total} uploaded · ${failed.length} failed` :
    failed.length > 0         ? `${done.length} of ${total} uploaded · ${failed.length} failed` :
                                `${done.length} of ${total} uploaded`;

  // Average progress for the headline bar.
  const avgProgress = total === 0
    ? 0
    : queue.reduce((s, q) => s + (q.status === "ready" ? 100 : q.progress || 0), 0) / total;

  return (
    <div className={`upload-dock ${collapsed ? "is-collapsed" : ""}`}>
      <button
        className="dock-head"
        onClick={() => setCollapsed((c) => !c)}
      >
        <div className="dock-head-info">
          <div className="dock-summary">{summary}</div>
          <div className="dock-progress-bar">
            <div className="dock-progress-fill" style={{ width: `${avgProgress}%` }} />
          </div>
        </div>
        <div className="dock-head-actions">
          {failed.length > 0 && (
            <span className="dock-fail-badge">{failed.length}</span>
          )}
          <span className="dock-chev">
            <IcChevD size={14} style={{ transform: collapsed ? "rotate(180deg)" : "rotate(0)" }} />
          </span>
        </div>
      </button>

      {!collapsed && (
        <>
          <div className="dock-list">
            {queue.map((q) => {
              const folder = FOLDERS.find((f) => f.id === q.folder);
              const inFlight = q.status === "uploading" || q.status === "analyzing";
              return (
                <div key={q.id} className={`dock-row is-${q.status}`}>
                  <div className="dock-row-thumb" style={{ backgroundImage: `url(${q.thumb})` }} />
                  <div className="dock-row-main">
                    <div className="dock-row-name" title={q.name}>{q.name}</div>
                    <div className="dock-row-meta">
                      {q.status === "ready" && (
                        <>
                          <IcCheck size={10} />
                          <span>Uploaded to {folder?.name}</span>
                        </>
                      )}
                      {q.status === "uploading" && (
                        <>
                          <span>Uploading…</span>
                          <span className="dock-row-pct">{Math.round(q.progress)}%</span>
                        </>
                      )}
                      {q.status === "analyzing" && (
                        <>
                          <IcSparkles size={10} />
                          <span>Auto-tagging…</span>
                        </>
                      )}
                      {q.status === "failed" && (
                        <>
                          <IcInfo size={10} />
                          <span>{q.error || "Upload failed"}</span>
                        </>
                      )}
                    </div>
                    {inFlight && (
                      <div className="dock-row-bar">
                        <div
                          className={`dock-row-fill ${q.status === "analyzing" ? "is-analyzing" : ""}`}
                          style={{ width: `${q.progress}%` }}
                        />
                      </div>
                    )}
                  </div>
                  <div className="dock-row-actions">
                    {q.status === "failed" && !q.noRetry && (
                      <button
                        className="dock-row-btn"
                        onClick={() => onRetry(q.id)}
                        title="Retry"
                      >
                        Retry
                      </button>
                    )}
                    {q.status === "ready" && q.asset && (
                      <button
                        className="dock-row-btn"
                        onClick={() => onOpenAsset(q.asset)}
                        title="Open in library"
                      >
                        View
                      </button>
                    )}
                  </div>
                </div>
              );
            })}
          </div>

          <div className="dock-foot">
            <span style={{ fontSize: 11.5, color: "var(--text-faint)" }}>
              {done.length} uploaded · {failed.length} failed
            </span>
            <button
              className="dock-link"
              onClick={onDismiss}
              disabled={inFlight.length > 0}
              style={inFlight.length > 0 ? { opacity: 0.4, cursor: "not-allowed" } : undefined}
            >
              {inFlight.length > 0 ? "Uploading…" : "Clear"}
            </button>
          </div>
        </>
      )}
    </div>
  );
}

Object.assign(window, {
  UploadDestinationModal,
  UploadDock,
  SAMPLE_UPLOADS,
});
