/* =========================================================================
 *  CTWCAD — File browser view (the hero feature)
 * =======================================================================*/

/* Kind groups used by the filter chips. Mirrors the way file-preview.jsx
 * categorises kinds. Values match `file.kind` (the lowercased extension). */
const KIND_GROUPS = {
  step:   ["step", "stp", "iges", "igs"],
  stl:    ["stl"],
  threemf:["3mf"],
  image:  ["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"],
  pdf:    ["pdf"],
};
const KIND_GROUP_LABELS = {
  step:    "STEP",
  stl:     "STL",
  threemf: "3MF",
  image:   "Images",
  pdf:     "PDFs",
  other:   "Other",
};
const ALL_GROUPED_KINDS = new Set(
  Object.values(KIND_GROUPS).flat()
);
function fileMatchesKindFilter(file, activeGroups) {
  if (!activeGroups || activeGroups.size === 0) return true;
  const k = (file.kind || "").toLowerCase();
  for (const g of activeGroups) {
    if (g === "other") {
      if (!ALL_GROUPED_KINDS.has(k)) return true;
    } else if ((KIND_GROUPS[g] || []).includes(k)) {
      return true;
    }
  }
  return false;
}

/* Sort options. Persisted to localStorage so the user's last choice
 * survives reload. */
const SORT_OPTIONS = [
  { id: "modified-desc", label: "Last modified (newest first)", icon: "clock" },
  { id: "modified-asc",  label: "Last modified (oldest first)", icon: "clock" },
  { id: "name-asc",      label: "Name (A → Z)",                 icon: "arrow-down-a-z" },
  { id: "name-desc",     label: "Name (Z → A)",                 icon: "arrow-down-z-a" },
  { id: "size-desc",     label: "Size (largest first)",         icon: "arrow-down-wide-narrow" },
  { id: "kind",          label: "Kind",                         icon: "shapes" },
];
const SORT_KEY = "ctwcad.browserSort";

function FileBrowserView({ initialFolderId, onOpenFile, onChangeFolder }) {
  const [folderId, setFolderId] = useState(initialFolderId);
  const [folder, setFolder] = useState(null);
  const [folderError, setFolderError] = useState(null);
  // Track folder IDs whose getFolder failed this session. We never try
  // to fetch the same one twice — that's the loop that froze the tab
  // when CTWCAD_NAV("/files") would re-trigger the My Files resolver,
  // which would return the same broken ID, which would fail again, etc.
  const failedFolderIds = useRef(new Set());
  // Sync from the prop so URL-driven navigation (browser back/forward,
  // CTWCAD_NAV, refresh on /files/<id>) actually moves us into that
  // folder. The local setFolderId is still used for in-component clicks
  // because it's synchronously fast — onChangeFolder bubbles back up.
  useEffect(() => {
    if (initialFolderId && initialFolderId !== folderId) {
      setFolderId(initialFolderId);
    }
  }, [initialFolderId]);
  const [breadcrumb, setBreadcrumb] = useState([]);
  const [view, setView] = useState("grid");
  const [sort, setSort] = useState(() => {
    try { return localStorage.getItem(SORT_KEY) || "modified-desc"; }
    catch { return "modified-desc"; }
  });
  const [sortOpen, setSortOpen] = useState(false);
  const [activeKindGroups, setActiveKindGroups] = useState(() => new Set());
  // Wave-5: tag filter. Set of lowercased tag strings; a file matches when
  // it has *every* tag in the set (intersection semantics — feels right
  // for a labeling system). Cleared on folder change.
  const [activeTags, setActiveTags] = useState(() => new Set());
  const [query, setQuery] = useState("");
  const [debouncedQuery, setDebouncedQuery] = useState("");
  const [contextMenu, setContextMenu] = useState(null);
  const [allFolders, setAllFolders] = useState([]);
  const [allProjects, setAllProjects] = useState([]);
  const [foldersByProject, setFBP] = useState({});
  const [dragOver, setDragOver] = useState(false);

  // Multi-select. Stored as an object {id: true} for O(1) lookups; cleared on
  // folder change, view change, and Esc. Also tracks the last-clicked id so
  // shift-click can compute a range.
  const [selected, setSelected] = useState({});
  const [lastClickedId, setLastClickedId] = useState(null);
  // Inline rename state. {id} of the file/folder currently being renamed
  // in-place. Esc cancels, Enter saves via window.api.
  const [editingId, setEditingId] = useState(null);

  // Persist sort choice.
  useEffect(() => {
    try { localStorage.setItem(SORT_KEY, sort); } catch {}
  }, [sort]);

  // Debounce the search query (~150ms) so typing doesn't thrash the
  // sortedChildren memo on every keystroke.
  useEffect(() => {
    const t = setTimeout(() => setDebouncedQuery(query), 150);
    return () => clearTimeout(t);
  }, [query]);

  useEffect(() => {
    window.api.listAllProjects().then(async (ps) => {
      setAllProjects(ps);
      // Parallel fan-out — one round-trip per project but issued
      // concurrently. With N projects this used to take N * RTT serially.
      const trees = await Promise.all(
        ps.map(p => window.api.getFolderTree(p.id).catch(() => []))
      );
      const map = {};
      ps.forEach((p, i) => { map[p.id] = trees[i]; });
      setFBP(map);
    });
  }, []);

  useEffect(() => {
    if (!folderId) { setFolder(null); setFolderError(null); return; }
    // Don't re-fetch a folder we already failed on this session — that
    // was the loop. The error UI shows alternate folders the user can
    // click into via the rail.
    if (failedFolderIds.current.has(folderId)) {
      setFolder(null);
      setFolderError({ folderId, code: "previously-failed" });
      return;
    }
    setFolder(null);
    setFolderError(null);
    let cancelled = false;
    window.api.getFolder(folderId).then(async (f) => {
      if (cancelled) return;
      // Defensive: getFolder should always return children: [], but if
      // a partial doc somehow gets here, render an empty folder rather
      // than crashing the entire app on `[...folder.children]`.
      const safe = f ? { ...f, children: f.children || [] } : null;
      setFolder(safe);
      // Build breadcrumb. Cap depth + check for self-reference so a
      // corrupted parent chain (folder.parentId === folder.id) doesn't
      // hang the tab in an infinite loop.
      const path = [];
      let cur = safe;
      const seen = new Set();
      let depth = 0;
      while (cur && depth < 50 && !seen.has(cur.id)) {
        seen.add(cur.id);
        path.unshift(cur);
        if (cur.parentId && cur.parentId !== cur.id) {
          cur = await window.api.getFolder(cur.parentId).catch(() => null);
        } else { cur = null; }
        depth++;
      }
      if (cancelled) return;
      setBreadcrumb(path);
      onChangeFolder?.(safe);
    }).catch((err) => {
      if (cancelled) return;
      const code = err?.code || err?.message || "unknown";
      console.warn("[CTWCAD] getFolder failed for", folderId, "→", code);
      // Remember this ID is broken so we never re-fetch it. Cripple the
      // self-heal loop: don't auto-navigate, don't clear App's folderId
      // — just render an inline error and let the user click into the
      // rail to pick a working folder. We also drop the localStorage
      // cache so the next page load re-resolves cleanly.
      failedFolderIds.current.add(folderId);
      try {
        const cachedUser = JSON.parse(localStorage.getItem("ctwcad.user") || "{}");
        const cacheKey = `ctwcad.myFolder.${cachedUser.id || cachedUser.email}`;
        if (cacheKey) localStorage.removeItem(cacheKey);
      } catch {}
      setFolder(null);
      setFolderError({ folderId, code });
    });
    return () => { cancelled = true; };
  }, [folderId]);

  // Clear selection when navigating to a different folder. Selection is
  // an in-folder concept; carrying it across navigation would be confusing.
  useEffect(() => {
    setSelected({});
    setLastClickedId(null);
    setEditingId(null);
  }, [folderId]);

  // Refresh the open folder when a file is uploaded into it (from anywhere).
  useEffect(() => {
    const onChanged = (e) => {
      if (!folderId) return;
      if (e.detail?.folderId && e.detail.folderId !== folderId) return;
      window.api.getFolder(folderId).then((f) => {
        // Same defensive normalisation as the main fetch above.
        setFolder(f ? { ...f, children: f.children || [] } : null);
      }).catch((err) => {
        console.warn("[CTWCAD] folder refresh failed:", err?.code || err?.message);
      });
    };
    window.addEventListener("ctwcad:folder-changed", onChanged);
    return () => window.removeEventListener("ctwcad:folder-changed", onChanged);
  }, [folderId]);

  // Refresh project tree when a project is created.
  useEffect(() => {
    const onCreated = async () => {
      const ps = await window.api.listAllProjects();
      setAllProjects(ps);
      const trees = await Promise.all(
        ps.map(p => window.api.getFolderTree(p.id).catch(() => []))
      );
      const map = {};
      ps.forEach((p, i) => { map[p.id] = trees[i]; });
      setFBP(map);
    };
    window.addEventListener("ctwcad:project-created", onCreated);
    return () => window.removeEventListener("ctwcad:project-created", onCreated);
  }, []);

  const sortedChildren = useMemo(() => {
    if (!folder) return [];
    // Defensive copy — folder.children is always an array out of
    // getFolder, but a stale cache or partial-write could leave it
    // missing. Spreading undefined throws, which would crash the tab.
    let kids = [...(folder.children || [])];
    if (debouncedQuery) {
      const q = debouncedQuery.toLowerCase();
      kids = kids.filter(k => k.name.toLowerCase().includes(q));
    }
    if (activeKindGroups.size > 0) {
      kids = kids.filter(k => k._kind === "folder" || fileMatchesKindFilter(k, activeKindGroups));
    }
    if (activeTags.size > 0) {
      // Folders aren't tagged. Hiding them while a tag filter is active
      // matches user intent ("show me files with #x"). If the user wants
      // folders back they can clear the tag filter.
      kids = kids.filter(k => {
        if (k._kind === "folder") return false;
        const ts = Array.isArray(k.tags) ? k.tags : [];
        for (const t of activeTags) if (!ts.includes(t)) return false;
        return true;
      });
    }
    kids.sort((a, b) => {
      // folders always first
      if (a._kind !== b._kind) return a._kind === "folder" ? -1 : 1;
      switch (sort) {
        case "name-asc":      return a.name.localeCompare(b.name);
        case "name-desc":     return b.name.localeCompare(a.name);
        case "modified-asc":  return new Date(a.updatedAt) - new Date(b.updatedAt);
        case "size-desc":     return (b.sizeBytes || 0) - (a.sizeBytes || 0);
        case "kind": {
          const ak = (a.kind || "").toLowerCase();
          const bk = (b.kind || "").toLowerCase();
          if (ak !== bk) return ak.localeCompare(bk);
          return a.name.localeCompare(b.name);
        }
        default: return new Date(b.updatedAt) - new Date(a.updatedAt);
      }
    });
    return kids;
  }, [folder, debouncedQuery, sort, activeKindGroups, activeTags]);

  // Listen for ctwcad:filter-by-tag (dispatched by tag-chip clicks in
  // both the file browser and the file drawer). Adds the tag to the
  // active filter set; clicking the same tag again removes it.
  useEffect(() => {
    const onFilterByTag = (e) => {
      const tag = (e.detail?.tag || "").toLowerCase();
      if (!tag) return;
      setActiveTags(prev => {
        const next = new Set(prev);
        if (next.has(tag)) next.delete(tag);
        else next.add(tag);
        return next;
      });
    };
    window.addEventListener("ctwcad:filter-by-tag", onFilterByTag);
    return () => window.removeEventListener("ctwcad:filter-by-tag", onFilterByTag);
  }, []);

  // Clear tag filter when navigating folders.
  useEffect(() => { setActiveTags(new Set()); }, [folderId]);

  // ---- selection helpers --------------------------------------------------
  const selectedIds = useMemo(
    () => Object.keys(selected).filter(k => selected[k]),
    [selected]
  );
  const selectionCount = selectedIds.length;
  const selectedItems = useMemo(
    () => sortedChildren.filter(c => selected[c.id]),
    [sortedChildren, selected]
  );

  const onItemClick = (e, item) => {
    // Modifier-aware selection. Mirrors Drive/Finder semantics.
    //   plain click → open
    //   ctrl/cmd-click → toggle in selection (don't open)
    //   shift-click → range-select from lastClickedId to this item
    if (editingId) return; // ignore while inline-renaming
    const items = sortedChildren;
    if (e.shiftKey && lastClickedId) {
      e.preventDefault();
      const a = items.findIndex(x => x.id === lastClickedId);
      const b = items.findIndex(x => x.id === item.id);
      if (a >= 0 && b >= 0) {
        const [lo, hi] = a < b ? [a, b] : [b, a];
        const next = { ...selected };
        for (let i = lo; i <= hi; i++) next[items[i].id] = true;
        setSelected(next);
      }
      return;
    }
    if (e.ctrlKey || e.metaKey) {
      e.preventDefault();
      setSelected(prev => {
        const next = { ...prev };
        if (next[item.id]) delete next[item.id];
        else next[item.id] = true;
        return next;
      });
      setLastClickedId(item.id);
      return;
    }
    // Plain click: if there's an active multi-selection (>1 items) and
    // the clicked item is part of it, treat as "open the folder/file".
    // Otherwise reduce selection to just this one and OPEN it. This keeps
    // single-click behaviour on first click, but lets the user reduce a
    // multi-select with a normal click.
    setSelected({});
    setLastClickedId(item.id);
    if (item._kind === "folder") setFolderId(item.id);
    else onOpenFile(item.id);
  };

  // Click empty space → deselect.
  const onBackgroundMouseDown = (e) => {
    // Only react if the click target is the body container itself, not a
    // descendant card/row.
    if (e.target === e.currentTarget) {
      setSelected({});
      setLastClickedId(null);
    }
  };

  // Esc clears selection (also closes the rename input).
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") {
        if (editingId) { setEditingId(null); return; }
        if (selectionCount > 0) { setSelected({}); setLastClickedId(null); }
      }
      // F2 = rename the focused / lone-selected item.
      if (e.key === "F2" && selectionCount === 1 && !editingId) {
        e.preventDefault();
        setEditingId(selectedIds[0]);
      }
      // Ctrl/Cmd+A inside the file browser body → select all visible items.
      // Skipped if the user is typing in an input.
      if ((e.ctrlKey || e.metaKey) && (e.key === "a" || e.key === "A")) {
        const tag = (document.activeElement && document.activeElement.tagName) || "";
        if (tag === "INPUT" || tag === "TEXTAREA") return;
        if (sortedChildren.length === 0) return;
        e.preventDefault();
        const next = {};
        sortedChildren.forEach(c => { next[c.id] = true; });
        setSelected(next);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [selectionCount, selectedIds, editingId, sortedChildren]);

  // ---- inline rename ------------------------------------------------------
  const submitRename = async (item, newName) => {
    setEditingId(null);
    const trimmed = (newName || "").trim();
    if (!trimmed || trimmed === item.name) return;
    try {
      if (item._kind === "folder") {
        await window.api.renameFolder(item.id, trimmed);
        window.dispatchEvent(new CustomEvent("ctwcad:folder-changed", { detail: { folderId } }));
        window.dispatchEvent(new CustomEvent("ctwcad:toast", { detail: { message: `Renamed → ${trimmed}`, icon: "edit-3" } }));
      } else {
        await window.api.renameFile(item.id, trimmed);
        window.dispatchEvent(new CustomEvent("ctwcad:folder-changed", { detail: { folderId } }));
        window.dispatchEvent(new CustomEvent("ctwcad:toast", { detail: { message: `Renamed → ${trimmed}`, icon: "edit-3" } }));
      }
    } catch (err) {
      window.dispatchEvent(new CustomEvent("ctwcad:toast", {
        detail: { message: `Couldn't rename: ${err?.code || err?.message || "error"}`, icon: "alert-circle" }
      }));
    }
  };

  // ---- bulk action wiring -------------------------------------------------
  // Most bulk operations dispatch a per-item ctwcad:item-action — that
  // already has download/share/move/trash plumbing in app.jsx, so we
  // get cancellation toasts and folder refresh for free.
  const dispatchBulk = (action) => {
    const items = selectedItems;
    if (items.length === 0) return;
    items.forEach((it) => {
      window.dispatchEvent(new CustomEvent("ctwcad:item-action", {
        detail: {
          action,
          kind: it._kind === "folder" ? "folder" : "file",
          file: it._kind === "folder" ? null : it,
          folder: it._kind === "folder" ? it : null,
        },
      }));
    });
  };

  const onContext = (e, file) => {
    setContextMenu({
      kind: "file", file,
      x: Math.min(e.clientX, window.innerWidth - 240),
      y: Math.min(e.clientY, window.innerHeight - 320),
      ox: 0, oy: 0,
    });
  };
  const onFolderContext = (e, folderItem) => {
    e.preventDefault();
    setContextMenu({
      kind: "folder", folder: folderItem,
      x: Math.min(e.clientX, window.innerWidth - 240),
      y: Math.min(e.clientY, window.innerHeight - 380),
      ox: 0, oy: 0,
    });
  };
  const onTreeContext = (e, info) => {
    setContextMenu({
      ...info,
      x: Math.min(e.clientX, window.innerWidth - 240),
      y: Math.min(e.clientY, window.innerHeight - 380),
      ox: 0, oy: 0,
    });
  };

  const onDrop = (e) => {
    e.preventDefault();
    setDragOver(false);
    window.dispatchEvent(new CustomEvent("ctwcad:upload", { detail: { folderId, files: e.dataTransfer.files } }));
  };

  // Toggle a kind group on the filter chip row.
  const toggleKindGroup = (g) => {
    setActiveKindGroups(prev => {
      const next = new Set(prev);
      if (next.has(g)) next.delete(g);
      else next.add(g);
      return next;
    });
  };

  const M = window.FramerMotion.motion;
  const A = window.FramerMotion.AnimatePresence;
  const currentSort = SORT_OPTIONS.find(s => s.id === sort) || SORT_OPTIONS[0];
  return (
    <div className="ct-browser"
         onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
         onDragLeave={() => setDragOver(false)}
         onDrop={onDrop}>
      <aside className="ct-browser-rail">
        <div className="ct-rail-head">
          <div className="ct-eyebrow ct-mono">PROJECTS</div>
          <button className="ct-iconbtn" title="New project"
                  onClick={() => window.dispatchEvent(new CustomEvent("ctwcad:new-project"))}>
            <Icon name="plus" size={13}/>
          </button>
        </div>
        {allProjects && allProjects.length === 0 ? (
          /* Empty-rail card. Used to render nothing here, which left a
             confusing blank column on first sign-in. The "+" button at
             the top works too — this is just an obvious second affordance. */
          <div className="ct-rail-empty">
            <Icon name="box" size={20}/>
            <div className="ct-rail-empty-title">No projects yet</div>
            <div className="ct-dim ct-rail-empty-sub">Create one to start organizing files.</div>
            <button className="ct-btn ct-btn-primary ct-rail-empty-cta"
                    onClick={() => window.dispatchEvent(new CustomEvent("ctwcad:new-project"))}>
              <Icon name="plus" size={13}/>New project
            </button>
          </div>
        ) : (
          <FolderTree
            projects={allProjects}
            foldersByProject={foldersByProject}
            currentFolderId={folderId}
            onSelectFolder={(f) => setFolderId(f.id)}
            onContext={onTreeContext}
          />
        )}
      </aside>

      <main className="ct-browser-main">
        <header className="ct-browser-bar">
          {/* MobileRailToggle self-gates on useIsMobile() — returns null
              on desktop, renders the hamburger on phones. Inline render
              replaces the old MutationObserver-driven injection from
              mobile-bootstrap, which was the cause of Files-tab freezes. */}
          <MobileRailToggle />
          <nav className="ct-crumbs">
            {breadcrumb.length === 0 && <span className="ct-skeleton-text" style={{width: 200}}/>}
            {breadcrumb.map((b, i) => (
              <React.Fragment key={b.id}>
                <button className="ct-crumb"
                  onClick={() => setFolderId(b.id)}
                  onContextMenu={(e) => onFolderContext(e, b)}>
                  <span>{b.name}</span>
                </button>
                {i < breadcrumb.length - 1 && <Icon name="chevron-right" size={12} className="ct-crumb-sep"/>}
              </React.Fragment>
            ))}
          </nav>
          <div className="ct-bar-tools">
            <div className="ct-search">
              <Icon name="search" size={13}/>
              <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search in folder…"/>
              {query
                ? <button className="ct-search-clear" title="Clear search" onClick={() => setQuery("")}>
                    <Icon name="x" size={12}/>
                  </button>
                : <kbd className="ct-mono">⌘K</kbd>}
            </div>
            <div className="ct-sort-wrap">
              <button className={"ct-iconbtn " + (sortOpen ? "is-active" : "")}
                      title={currentSort.label}
                      onClick={() => setSortOpen(o => !o)}>
                <Icon name={currentSort.icon} size={14}/>
              </button>
              <A>
                {sortOpen && (
                  <>
                    {/* Click-away catcher. Below the menu in z-order so the
                        menu still receives clicks. */}
                    <div className="ct-sort-catch" onClick={() => setSortOpen(false)} />
                    <M.div className="ct-sort-menu"
                      initial={{ opacity: 0, y: -4, scale: 0.96 }}
                      animate={{ opacity: 1, y: 0, scale: 1 }}
                      exit={{ opacity: 0, y: -4, scale: 0.96 }}
                      transition={{ duration: 0.14 }}
                    >
                      <div className="ct-sort-menu-head">SORT BY</div>
                      {SORT_OPTIONS.map(opt => (
                        <button key={opt.id}
                                className={"ct-sort-item " + (opt.id === sort ? "is-active" : "")}
                                onClick={() => { setSort(opt.id); setSortOpen(false); }}>
                          <Icon name={opt.icon} size={12}/>
                          <span>{opt.label}</span>
                          {opt.id === sort && <Icon name="check" size={12} className="ct-sort-check"/>}
                        </button>
                      ))}
                    </M.div>
                  </>
                )}
              </A>
            </div>
            <div className="ct-toggle-group" role="tablist">
              <button className={view === "grid" ? "is-active" : ""} onClick={() => setView("grid")}><Icon name="layout-grid" size={13}/></button>
              <button className={view === "list" ? "is-active" : ""} onClick={() => setView("list")}><Icon name="list" size={13}/></button>
            </div>
            <button className="ct-btn ct-btn-primary"
                    onClick={() => window.dispatchEvent(new CustomEvent("ctwcad:pick-files", { detail: { folderId } }))}>
              <Icon name="upload" size={13}/>Upload
            </button>
          </div>
        </header>

        {/* Filter chips by kind. Always visible while in a folder so the
            chips don't pop in/out as files load. The whole row hides if
            we're showing the folder error or a skeleton. */}
        {folder && !folderError && (
          <div className="ct-filter-chips">
            <button className={"ct-filter-chip " + (activeKindGroups.size === 0 ? "is-active" : "")}
                    onClick={() => setActiveKindGroups(new Set())}>
              All
            </button>
            {["step","stl","threemf","image","pdf","other"].map(g => (
              <button key={g}
                      className={"ct-filter-chip " + (activeKindGroups.has(g) ? "is-active" : "")}
                      onClick={() => toggleKindGroup(g)}>
                {KIND_GROUP_LABELS[g]}
              </button>
            ))}
            {Array.from(activeTags).map(t => (
              <button key={"tag-" + t}
                      className="ct-filter-chip ct-filter-chip-tag is-active"
                      onClick={() => setActiveTags(prev => {
                        const next = new Set(prev); next.delete(t); return next;
                      })}
                      title={`Remove #${t} filter`}>
                #{t}
                <Icon name="x" size={10} />
              </button>
            ))}
            {(activeKindGroups.size > 0 || debouncedQuery || activeTags.size > 0) && (
              <span className="ct-filter-summary ct-mono ct-dim">
                {sortedChildren.length} match{sortedChildren.length === 1 ? "" : "es"}
              </span>
            )}
          </div>
        )}

        <div className="ct-browser-body" onMouseDown={onBackgroundMouseDown}>
          {folderError ? (
            <div className="ct-empty">
              <Icon name="alert-triangle" size={36}/>
              <div className="ct-empty-title">This folder isn't available</div>
              <div className="ct-dim" style={{maxWidth: 420, textAlign: "center"}}>
                <span className="ct-mono">{folderError.folderId}</span>{" "}
                couldn't be loaded ({folderError.code}). It may have been
                deleted, or you don't have access. Pick another folder from
                the rail on the left to continue.
              </div>
              <button className="ct-btn ct-btn-primary" style={{marginTop: 14}}
                onClick={() => {
                  // Drop the broken ID and let App's My Files resolver
                  // pick a working folder for this user. Safe to do here
                  // because the user explicitly asked.
                  failedFolderIds.current.delete(folderId);
                  setFolderId(null);
                  if (typeof window.CTWCAD_NAV === "function") window.CTWCAD_NAV("/files");
                }}>
                <Icon name="undo-2" size={13}/>Take me to my files
              </button>
            </div>
          ) : !folder ? (
            <div className="ct-grid">
              {Array.from({length: 8}).map((_, i) => <div key={i} className="ct-card ct-skeleton" style={{height: 240}}/>)}
            </div>
          ) : sortedChildren.length === 0 ? (
            (debouncedQuery || activeKindGroups.size > 0 || activeTags.size > 0)
              ? <NoMatchesEmpty
                  query={debouncedQuery}
                  onClear={() => { setQuery(""); setActiveKindGroups(new Set()); setActiveTags(new Set()); }}
                />
              : <EmptyDropzoneCTA folderId={folderId} folderName={folder.name} />
          ) : view === "grid" ? (
            <div className="ct-grid">
              {sortedChildren.map((c, i) => (
                c._kind === "folder"
                  ? <FolderCard key={c.id} folder={c} index={i}
                                selected={!!selected[c.id]}
                                editing={editingId === c.id}
                                highlight={debouncedQuery}
                                onSubmitRename={(name) => submitRename(c, name)}
                                onCancelRename={() => setEditingId(null)}
                                onSelect={(e) => onItemClick(e, c)}
                                onStartRename={() => { setEditingId(c.id); }}
                                onContextMenu={onFolderContext} />
                  : <FileCard key={c.id} file={c} index={i}
                              selected={!!selected[c.id]}
                              editing={editingId === c.id}
                              highlight={debouncedQuery}
                              onSubmitRename={(name) => submitRename(c, name)}
                              onCancelRename={() => setEditingId(null)}
                              onSelect={(e) => onItemClick(e, c)}
                              onStartRename={() => { setEditingId(c.id); }}
                              onContextMenu={onContext} />
              ))}
            </div>
          ) : (
            <div className="ct-list">
              <div className="ct-list-head">
                <div></div>
                <div>Name</div>
                <div>Size</div>
                <div>Modified</div>
                <div>Sharing</div>
              </div>
              {sortedChildren.map((c, i) => (
                c._kind === "folder"
                  ? <FolderRow key={c.id} folder={c} index={i}
                               selected={!!selected[c.id]}
                               editing={editingId === c.id}
                               highlight={debouncedQuery}
                               onSubmitRename={(name) => submitRename(c, name)}
                               onCancelRename={() => setEditingId(null)}
                               onSelect={(e) => onItemClick(e, c)}
                               onStartRename={() => setEditingId(c.id)}
                               onContextMenu={onFolderContext} />
                  : <FileRow key={c.id} file={c} index={i}
                             selected={!!selected[c.id]}
                             editing={editingId === c.id}
                             highlight={debouncedQuery}
                             onSubmitRename={(name) => submitRename(c, name)}
                             onCancelRename={() => setEditingId(null)}
                             onSelect={(e) => onItemClick(e, c)}
                             onStartRename={() => setEditingId(c.id)}
                             onContextMenu={onContext} />
              ))}
            </div>
          )}
        </div>
      </main>

      <A>
        {dragOver && (
          <M.div className="ct-drop-overlay"
            initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
            transition={{ duration: 0.18 }}>
            <div className="ct-drop-card">
              <Icon name="upload-cloud" size={28}/>
              <div className="ct-drop-title">Drop to upload</div>
              <div className="ct-dim">into <span className="ct-mono">{folder?.name}</span></div>
            </div>
          </M.div>
        )}
      </A>

      <BulkActionBar
        count={selectionCount}
        onCancel={() => { setSelected({}); setLastClickedId(null); }}
        onMove={() => dispatchBulk("move")}
        onTrash={() => {
          const n = selectionCount;
          // window.confirm avoids a custom modal here. Bulk-trash is
          // recoverable from /trash for files, so this is a soft guard.
          if (!window.confirm(`Move ${n} item${n === 1 ? "" : "s"} to Trash?`)) return;
          dispatchBulk("trash");
          setSelected({});
        }}
        onShare={() => dispatchBulk("share")}
        onDownload={() => dispatchBulk("download")}
      />

      <ContextMenu menu={contextMenu} onClose={() => setContextMenu(null)} />
    </div>
  );
}

/* The pre-existing EmptyDropzone, kept as a fallback. The browser uses the
 * action-rich variant below now, but other consumers may reference this. */
function EmptyDropzone() {
  return (
    <div className="ct-empty">
      <svg width="120" height="120" viewBox="0 0 120 120" className="ct-empty-svg">
        <g stroke="currentColor" strokeWidth="0.8" fill="none">
          <rect x="20" y="34" width="80" height="56" rx="4"/>
          <path d="M20 50 h80 M44 34 v56"/>
          <path d="M60 60 l10 10 M70 60 l-10 10" stroke="var(--ct-accent)" strokeWidth="1.2"/>
        </g>
      </svg>
      <div className="ct-empty-title">Empty folder</div>
      <div className="ct-dim">Drop files here, or click upload above. .ctws, .step, .stl, .iges, .pdf are supported.</div>
    </div>
  );
}

/* Empty-folder card with first-class actions. Replaces the static
 * EmptyDropzone in the file browser. The drop-anywhere overlay still
 * works, but offering explicit Upload + New-folder buttons here means
 * the user never gets stuck staring at an empty surface wondering how
 * to start. */
function EmptyDropzoneCTA({ folderId, folderName }) {
  return (
    <div className="ct-empty ct-empty-cta">
      <svg width="120" height="120" viewBox="0 0 120 120" className="ct-empty-svg">
        <g stroke="currentColor" strokeWidth="0.8" fill="none">
          <rect x="20" y="34" width="80" height="56" rx="4"/>
          <path d="M20 50 h80 M44 34 v56"/>
          <path d="M60 60 l10 10 M70 60 l-10 10" stroke="var(--ct-accent)" strokeWidth="1.2"/>
        </g>
      </svg>
      <div className="ct-empty-title">This folder is empty</div>
      <div className="ct-dim ct-empty-sub">
        Drag files in, or use one of the buttons below to get started.
      </div>
      <div className="ct-empty-actions">
        <button className="ct-btn ct-btn-primary"
                onClick={() => window.dispatchEvent(new CustomEvent("ctwcad:pick-files", { detail: { folderId } }))}>
          <Icon name="upload" size={13}/>Upload files
        </button>
        <button className="ct-btn"
                onClick={() => {
                  // Reuses the context-menu new-folder dispatch path.
                  window.dispatchEvent(new CustomEvent("ctwcad:item-action", {
                    detail: { action: "new-folder", kind: "folder", folder: { id: folderId, name: folderName } },
                  }));
                }}>
          <Icon name="folder-plus" size={13}/>New folder
        </button>
      </div>
      <div className="ct-empty-hint ct-mono ct-dim">
        Supported: .ctws, .step, .stl, .3mf, .iges, .pdf, images
      </div>
    </div>
  );
}

/* Empty-state used when the user has filtered everything out. Different
 * messaging and a single-click "clear" button. */
function NoMatchesEmpty({ query, onClear }) {
  return (
    <div className="ct-empty">
      <Icon name="search-x" size={36}/>
      <div className="ct-empty-title">No matches</div>
      <div className="ct-dim" style={{maxWidth: 380, textAlign: "center"}}>
        {query
          ? <>Nothing in this folder matches <span className="ct-mono">"{query}"</span>.</>
          : <>No files match the active filters.</>}
        {" "}Try clearing the filter or searching elsewhere.
      </div>
      <button className="ct-btn" style={{marginTop: 14}} onClick={onClear}>
        <Icon name="x" size={13}/>Clear filters
      </button>
    </div>
  );
}

Object.assign(window, { FileBrowserView, EmptyDropzone, EmptyDropzoneCTA, NoMatchesEmpty });
