@muhgholy/next-drive 4.23.23 → 4.23.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/{chunk-IVSBLEMY.cjs → chunk-45FZNILV.cjs} +170 -1031
  2. package/dist/chunk-45FZNILV.cjs.map +1 -0
  3. package/dist/{chunk-CPMMHIQM.js → chunk-VBFXHZBY.js} +159 -1026
  4. package/dist/chunk-VBFXHZBY.js.map +1 -0
  5. package/dist/chunk-VTCBYFAK.cjs +765 -0
  6. package/dist/chunk-VTCBYFAK.cjs.map +1 -0
  7. package/dist/chunk-ZNWS7VJP.js +755 -0
  8. package/dist/chunk-ZNWS7VJP.js.map +1 -0
  9. package/dist/client/components/drive/dnd/context.d.ts.map +1 -1
  10. package/dist/client/components/drive/explorer.d.ts.map +1 -1
  11. package/dist/client/components/drive/file-grid.d.ts.map +1 -1
  12. package/dist/client/hooks/use-marquee.d.ts +14 -0
  13. package/dist/client/hooks/use-marquee.d.ts.map +1 -0
  14. package/dist/client/index.cjs +170 -150
  15. package/dist/client/index.cjs.map +1 -1
  16. package/dist/client/index.js +169 -150
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/upload.d.ts +9 -4
  19. package/dist/client/upload.d.ts.map +1 -1
  20. package/dist/server/actions/drive.d.ts.map +1 -1
  21. package/dist/server/controllers/drive.d.ts.map +1 -1
  22. package/dist/server/express.cjs +15 -14
  23. package/dist/server/express.cjs.map +1 -1
  24. package/dist/server/express.js +4 -2
  25. package/dist/server/express.js.map +1 -1
  26. package/dist/server/hono.cjs +15 -14
  27. package/dist/server/hono.cjs.map +1 -1
  28. package/dist/server/hono.js +4 -2
  29. package/dist/server/hono.js.map +1 -1
  30. package/dist/server/index.cjs +28 -27
  31. package/dist/server/index.js +2 -1
  32. package/dist/server/tus.d.ts +17 -0
  33. package/dist/server/tus.d.ts.map +1 -0
  34. package/dist/server/zod/schemas.d.ts +0 -53
  35. package/dist/server/zod/schemas.d.ts.map +1 -1
  36. package/dist/tus-6OAOA454.js +3 -0
  37. package/dist/tus-6OAOA454.js.map +1 -0
  38. package/dist/tus-P67YULWB.cjs +16 -0
  39. package/dist/tus-P67YULWB.cjs.map +1 -0
  40. package/dist/types/server/api.d.ts +1 -1
  41. package/dist/types/server/api.d.ts.map +1 -1
  42. package/package.json +4 -1
  43. package/dist/chunk-CPMMHIQM.js.map +0 -1
  44. package/dist/chunk-IVSBLEMY.cjs.map +0 -1
@@ -4,6 +4,7 @@ var chunkHQTC3554_cjs = require('../chunk-HQTC3554.cjs');
4
4
  var chunkA65ZAA2Z_cjs = require('../chunk-A65ZAA2Z.cjs');
5
5
  var React3 = require('react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
+ var tus = require('tus-js-client');
7
8
  var reactSlot = require('@radix-ui/react-slot');
8
9
  var classVarianceAuthority = require('class-variance-authority');
9
10
  var ProgressPrimitive = require('@radix-ui/react-progress');
@@ -37,6 +38,7 @@ function _interopNamespace(e) {
37
38
  }
38
39
 
39
40
  var React3__namespace = /*#__PURE__*/_interopNamespace(React3);
41
+ var tus__namespace = /*#__PURE__*/_interopNamespace(tus);
40
42
  var ProgressPrimitive__namespace = /*#__PURE__*/_interopNamespace(ProgressPrimitive);
41
43
  var SheetPrimitive__namespace = /*#__PURE__*/_interopNamespace(SheetPrimitive);
42
44
  var DropdownMenuPrimitive__namespace = /*#__PURE__*/_interopNamespace(DropdownMenuPrimitive);
@@ -332,8 +334,6 @@ var useDrive = () => {
332
334
  if (!context) throw new Error("useDrive must be used within a DriveProvider");
333
335
  return context;
334
336
  };
335
-
336
- // src/client/upload.ts
337
337
  var MAX_CONCURRENT_UPLOADS = 2;
338
338
  var getChunkSize = (fileSize) => {
339
339
  if (fileSize < 50 * 1024 * 1024) return 2 * 1024 * 1024;
@@ -367,161 +367,92 @@ var uploadFile = (options) => {
367
367
  onComplete
368
368
  } = options;
369
369
  const id = options.id ?? generateId();
370
- const endpoint = apiEndpoint.replace(/\/$/, "");
371
- const uploadUrl = `${endpoint}?action=upload`;
372
- const controller = new AbortController();
373
- let activeXhr = null;
374
- let driveId;
375
- let completedItem;
376
- let cancelled = false;
377
- const totalBytes = file.size;
370
+ const endpoint = `${apiEndpoint.replace(/\/$/, "")}?action=upload`;
378
371
  const chunkSize = getChunkSize(file.size);
379
- const totalChunks = Math.ceil(file.size / chunkSize);
372
+ const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
380
373
  const log = (type, message) => onLog?.({ id, type, message, timestamp: Date.now() });
381
- const emitProgress = (currentChunk, uploadedBytes) => {
382
- const percent = totalBytes === 0 ? 100 : Math.min(100, Math.round(uploadedBytes / totalBytes * 100));
383
- onProgress?.({ id, percent, uploadedBytes, totalBytes, currentChunk, totalChunks });
374
+ let settle = () => {
384
375
  };
385
- const sendChunk = (chunkIndex, formData, chunkStart) => new Promise((resolve) => {
386
- const xhr = new XMLHttpRequest();
387
- activeXhr = xhr;
388
- log("info", `Sending chunk to ${uploadUrl}`);
389
- xhr.upload.onprogress = (e) => {
390
- if (e.lengthComputable) emitProgress(chunkIndex, chunkStart + e.loaded);
391
- };
392
- xhr.onload = () => {
393
- activeXhr = null;
394
- const status = xhr.status;
395
- const text = xhr.responseText;
396
- if (status < 200 || status >= 300) {
397
- log("error", `HTTP ${status}: ${xhr.statusText}`);
398
- try {
399
- const json = JSON.parse(text);
400
- const msg = json.message || json.error?.message || `Status ${status}`;
401
- log("error", `Server response: ${msg}`);
402
- const canRetry = [408, 429, 502, 503, 504].includes(status);
403
- if (canRetry) log("warning", `Error is retryable (status ${status})`);
404
- resolve([false, msg, canRetry]);
405
- } catch {
406
- log("error", `Failed to parse error response: ${text.slice(0, 100)}`);
407
- const isRetryable = status >= 500 || status === 429;
408
- resolve([false, `Server error ${status}: ${text.slice(0, 100)}`, isRetryable]);
409
- }
410
- return;
376
+ const promise = new Promise((resolve) => {
377
+ settle = resolve;
378
+ });
379
+ let completedItem;
380
+ let cancelled = false;
381
+ const metadata = {
382
+ filename: file.name,
383
+ filetype: file.type || "application/octet-stream",
384
+ folderId: folderId || "root"
385
+ };
386
+ if (conflictAction) metadata.conflictAction = conflictAction;
387
+ const upload = new tus__namespace.Upload(file, {
388
+ endpoint,
389
+ chunkSize,
390
+ // ** Retry with backoff. On a dropped connection tus first re-checks the server offset (HEAD)
391
+ // ** and resumes from there this is what prevents the "session expired" failures at ~100%.
392
+ retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
393
+ removeFingerprintOnSuccess: true,
394
+ metadata,
395
+ headers: accountId ? { "x-drive-account": accountId } : {},
396
+ onBeforeRequest(req) {
397
+ if (withCredentials) {
398
+ const xhr = req.getUnderlyingObject();
399
+ if (xhr && "withCredentials" in xhr) xhr.withCredentials = true;
411
400
  }
412
- try {
413
- const data = JSON.parse(text);
414
- if (data.status !== 200) {
415
- log("error", `Upload API error: ${data.message || "Unknown error"}`);
416
- resolve([false, data.message || "Upload failed", false]);
417
- return;
418
- }
419
- log("success", `Chunk uploaded successfully`);
420
- resolve([true, data.data, false]);
421
- } catch {
422
- log("error", `Failed to parse response: ${text.slice(0, 100)}`);
423
- resolve([false, "Invalid server response", true]);
401
+ },
402
+ onShouldRetry(err) {
403
+ const status = err?.originalResponse?.getStatus?.() ?? 0;
404
+ if ([400, 401, 403, 413].includes(status)) {
405
+ log("error", `Non-retryable error (status ${status})`);
406
+ return false;
424
407
  }
425
- };
426
- xhr.onerror = () => {
427
- activeXhr = null;
428
- log("error", `Network/Fetch error: request failed`);
429
- resolve([false, `Upload connection failed`, true]);
430
- };
431
- xhr.onabort = () => {
432
- activeXhr = null;
433
- resolve([false, "Cancelled", false]);
434
- };
435
- xhr.open("POST", uploadUrl);
436
- if (accountId) xhr.setRequestHeader("x-drive-account", accountId);
437
- xhr.withCredentials = withCredentials;
438
- xhr.send(formData);
439
- });
440
- const run = async () => {
441
- log("info", `Starting upload: ${file.name} (${file.size} bytes)`);
442
- log("info", `File split into ${totalChunks} chunks of ${chunkSize} bytes each`);
443
- emitProgress(0, 0);
444
- try {
445
- for (let i = 0; i < totalChunks; i++) {
446
- if (controller.signal.aborted) throw new Error("Cancelled");
447
- log("info", `Processing chunk ${i + 1}/${totalChunks}`);
448
- const start = i * chunkSize;
449
- const end = Math.min(start + chunkSize, file.size);
450
- const chunk = file.slice(start, end);
451
- const formData = new FormData();
452
- formData.append("chunk", chunk);
453
- formData.append("chunkIndex", String(i));
454
- formData.append("totalChunks", String(totalChunks));
455
- formData.append("fileName", file.name);
456
- formData.append("fileSize", String(file.size));
457
- formData.append("fileType", file.type);
458
- formData.append("folderId", folderId || "root");
459
- if (conflictAction) formData.append("conflictAction", conflictAction);
460
- if (driveId) formData.append("driveId", driveId);
461
- let attempts = 0;
462
- let success = false;
463
- while (!success && attempts < 3 && !controller.signal.aborted) {
464
- const [ok, result, canRetry] = await sendChunk(i, formData, start);
465
- if (ok) {
466
- success = true;
467
- emitProgress(i + 1, end);
468
- if (result.type === "UPLOAD_STARTED" && result.driveId) {
469
- driveId = result.driveId;
470
- log("success", `Upload session started with ID: ${driveId}`);
471
- } else if (result.type === "UPLOAD_COMPLETE") {
472
- if (result.driveId) driveId = result.driveId;
473
- log("success", `Upload completed successfully`);
474
- completedItem = result.item;
475
- }
476
- } else {
477
- if (result === "Cancelled") throw new Error("Cancelled");
478
- if (!canRetry) {
479
- log("error", `Non-retryable error: ${result}`);
480
- throw new Error(result);
481
- }
482
- attempts++;
483
- log("warning", `Retry attempt ${attempts}/3 after error`);
484
- if (attempts === 3) {
485
- log("error", `Max retry attempts reached: ${result}`);
486
- throw new Error(result);
487
- }
488
- await new Promise((r) => setTimeout(r, 1e3 * attempts));
489
- }
408
+ log("warning", `Upload interrupted (status ${status}), retrying`);
409
+ return true;
410
+ },
411
+ onProgress(bytesUploaded, bytesTotal) {
412
+ const percent = bytesTotal === 0 ? 100 : Math.min(100, Math.round(bytesUploaded / bytesTotal * 100));
413
+ onProgress?.({
414
+ id,
415
+ percent,
416
+ uploadedBytes: bytesUploaded,
417
+ totalBytes: bytesTotal,
418
+ currentChunk: Math.min(totalChunks, Math.floor(bytesUploaded / chunkSize)),
419
+ totalChunks
420
+ });
421
+ },
422
+ onAfterResponse(_req, res) {
423
+ const header = res.getHeader("X-Drive-Item");
424
+ if (header) {
425
+ try {
426
+ completedItem = JSON.parse(decodeURIComponent(header));
427
+ } catch {
490
428
  }
491
- if (!success && controller.signal.aborted) throw new Error("Cancelled");
492
429
  }
493
- log("success", `All ${totalChunks} chunks uploaded successfully`);
494
- emitProgress(totalChunks, totalBytes);
430
+ },
431
+ onSuccess() {
432
+ log("success", "Upload completed successfully");
495
433
  if (completedItem !== void 0) onComplete?.(completedItem);
496
- return { id, status: "complete", driveId: driveId ?? "", file: toDriveFile(completedItem), item: completedItem };
497
- } catch (error) {
498
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
499
- if (errorMessage === "Cancelled") {
500
- log("warning", "Upload cancelled by user");
501
- return { id, status: "cancelled", driveId: driveId ?? null };
502
- }
503
- log("error", `Upload failed: ${errorMessage}`);
504
- return { id, status: "error", driveId: driveId ?? null, error: errorMessage };
434
+ settle({ id, status: "complete", driveId: completedItem?.id ?? "", file: toDriveFile(completedItem), item: completedItem });
435
+ },
436
+ onError(error) {
437
+ const message = error instanceof Error ? error.message : "Upload failed";
438
+ log("error", `Upload failed: ${message}`);
439
+ settle({ id, status: "error", driveId: null, error: message });
505
440
  }
506
- };
441
+ });
507
442
  const cancel = () => {
508
443
  if (cancelled) return;
509
444
  cancelled = true;
510
- controller.abort();
511
- activeXhr?.abort();
512
- if (driveId) {
513
- fetch(`${endpoint}?action=cancel&id=${driveId}`, {
514
- method: "POST",
515
- credentials: withCredentials ? "include" : "same-origin"
516
- }).catch(() => {
517
- });
518
- }
445
+ upload.abort(true).catch(() => {
446
+ });
447
+ settle({ id, status: "cancelled", driveId: null });
519
448
  };
520
449
  if (signal) {
521
450
  if (signal.aborted) cancel();
522
451
  else signal.addEventListener("abort", cancel, { once: true });
523
452
  }
524
- return { id, promise: run(), cancel };
453
+ log("info", `Starting upload: ${file.name} (${file.size} bytes)`);
454
+ upload.start();
455
+ return { id, promise, cancel };
525
456
  };
526
457
  var uploadFiles = (files, options) => {
527
458
  const { concurrency = MAX_CONCURRENT_UPLOADS, onFileComplete, ...base } = options;
@@ -1556,9 +1487,16 @@ var DriveDndContext = React3.createContext({
1556
1487
  var useDriveDnd = () => React3.useContext(DriveDndContext);
1557
1488
  var DriveDndProvider = (props) => {
1558
1489
  const { children } = props;
1559
- const { items, setItems, sortBy, setSortBy, moveItem, callAPI, currentView } = useDrive();
1490
+ const { items, setItems, sortBy, setSortBy, moveItem, callAPI, currentView, selectedFileIds, setSelectedFiles } = useDrive();
1560
1491
  const [dragOverFolderId, setDragOverFolderId] = React3.useState(null);
1561
1492
  const [draggingItemId, setDraggingItemId] = React3.useState(null);
1493
+ const moveSelection = React3.useCallback(async (activeId, targetFolderId) => {
1494
+ const ids = selectedFileIds.includes(activeId) && selectedFileIds.length > 1 ? selectedFileIds : [activeId];
1495
+ if (ids.length === 1) return moveItem(ids[0], targetFolderId);
1496
+ setItems((prev) => prev.filter((i) => !ids.includes(i.id)));
1497
+ setSelectedFiles([]);
1498
+ await callAPI("move", { method: "POST", body: JSON.stringify({ ids, targetFolderId }) });
1499
+ }, [selectedFileIds, moveItem, setItems, setSelectedFiles, callAPI]);
1562
1500
  const sensors = core.useSensors(
1563
1501
  core.useSensor(core.PointerSensor, { activationConstraint: { distance: 8 } }),
1564
1502
  core.useSensor(core.KeyboardSensor, { coordinateGetter: sortable.sortableKeyboardCoordinates })
@@ -1593,12 +1531,12 @@ var DriveDndProvider = (props) => {
1593
1531
  if (overId.startsWith("path-")) {
1594
1532
  const pathId = overId.slice(5);
1595
1533
  const targetFolderId = pathId === "root" || pathId === "null" ? "root" : pathId;
1596
- await moveItem(active.id, targetFolderId);
1534
+ await moveSelection(active.id, targetFolderId);
1597
1535
  return;
1598
1536
  }
1599
1537
  const overItem = items.find((i) => i.id === over.id);
1600
1538
  if (overItem?.information.type === "FOLDER") {
1601
- await moveItem(active.id, over.id);
1539
+ await moveSelection(active.id, over.id);
1602
1540
  return;
1603
1541
  }
1604
1542
  const oldIndex = items.findIndex((i) => i.id === active.id);
@@ -1608,7 +1546,7 @@ var DriveDndProvider = (props) => {
1608
1546
  setItems(reordered);
1609
1547
  if (sortBy.field !== "order") setSortBy({ field: "order", order: 1 });
1610
1548
  await callAPI("reorder", { method: "POST", body: JSON.stringify({ ids: reordered.map((i) => i.id) }) });
1611
- }, [items, setItems, sortBy, setSortBy, moveItem, callAPI]);
1549
+ }, [items, setItems, sortBy, setSortBy, moveSelection, callAPI]);
1612
1550
  const enableDnd = currentView === "BROWSE";
1613
1551
  return /* @__PURE__ */ jsxRuntime.jsx(DriveDndContext.Provider, { value: { dragOverFolderId, draggingItemId }, children: enableDnd ? /* @__PURE__ */ jsxRuntime.jsx(
1614
1552
  core.DndContext,
@@ -1974,6 +1912,49 @@ var DriveItemRename = (props) => {
1974
1912
  }
1975
1913
  ) });
1976
1914
  };
1915
+ var useMarquee = (onSelectIds) => {
1916
+ const [box, setBox] = React3.useState(null);
1917
+ const onMouseDown = (e) => {
1918
+ if (e.button !== 0 || e.target.closest("[data-marquee-id]")) return;
1919
+ const container = e.currentTarget;
1920
+ const start = { x: e.clientX, y: e.clientY };
1921
+ let moved = false;
1922
+ const handleMove = (ev) => {
1923
+ if (!moved && Math.abs(ev.clientX - start.x) < 4 && Math.abs(ev.clientY - start.y) < 4) return;
1924
+ moved = true;
1925
+ const left = Math.min(start.x, ev.clientX);
1926
+ const top = Math.min(start.y, ev.clientY);
1927
+ const right = Math.max(start.x, ev.clientX);
1928
+ const bottom = Math.max(start.y, ev.clientY);
1929
+ const cRect = container.getBoundingClientRect();
1930
+ setBox({
1931
+ left: left - cRect.left + container.scrollLeft,
1932
+ top: top - cRect.top + container.scrollTop,
1933
+ width: right - left,
1934
+ height: bottom - top
1935
+ });
1936
+ const ids = [];
1937
+ container.querySelectorAll("[data-marquee-id]").forEach((el) => {
1938
+ const r = el.getBoundingClientRect();
1939
+ if (r.left < right && r.right > left && r.top < bottom && r.bottom > top) ids.push(el.dataset.marqueeId);
1940
+ });
1941
+ onSelectIds(ids);
1942
+ };
1943
+ const handleUp = () => {
1944
+ window.removeEventListener("mousemove", handleMove);
1945
+ window.removeEventListener("mouseup", handleUp);
1946
+ if (!moved) onSelectIds([]);
1947
+ setBox(null);
1948
+ };
1949
+ window.addEventListener("mousemove", handleMove);
1950
+ window.addEventListener("mouseup", handleUp);
1951
+ };
1952
+ const overlay = box ? React3__namespace.default.createElement("div", {
1953
+ className: "nd:absolute nd:z-50 nd:rounded-sm nd:border nd:border-primary nd:bg-primary/10 nd:pointer-events-none",
1954
+ style: { left: box.left, top: box.top, width: box.width, height: box.height }
1955
+ }) : null;
1956
+ return { onMouseDown, overlay };
1957
+ };
1977
1958
  var SortableItem = (props) => {
1978
1959
  const { id, children, disabled, isDragOverTarget } = props;
1979
1960
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = sortable.useSortable({ id, disabled });
@@ -1984,7 +1965,7 @@ var SortableItem = (props) => {
1984
1965
  zIndex: isDragging ? 50 : "auto",
1985
1966
  position: "relative"
1986
1967
  };
1987
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: setNodeRef, style, ...attributes, ...listeners, children });
1968
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: setNodeRef, "data-marquee-id": id, style, ...attributes, ...listeners, children });
1988
1969
  };
1989
1970
  var FileItem = (props) => {
1990
1971
  const { item, isSelected, isDragOver, onSelect, onDoubleClick, onRename, onDelete, onRestore } = props;
@@ -2071,6 +2052,8 @@ var DriveFileGrid = (props) => {
2071
2052
  sortBy,
2072
2053
  selectedFileIds,
2073
2054
  toggleSelected,
2055
+ setSelectedFiles,
2056
+ selectionMode,
2074
2057
  navigateToFolder,
2075
2058
  hasMore,
2076
2059
  loadMore,
@@ -2082,6 +2065,19 @@ var DriveFileGrid = (props) => {
2082
2065
  restoreItem
2083
2066
  } = useDrive();
2084
2067
  const { dragOverFolderId } = useDriveDnd();
2068
+ const { onMouseDown: marqueeMouseDown, overlay: marqueeOverlay } = useMarquee((ids) => {
2069
+ let files = items.filter((i) => ids.includes(i.id)).map((i) => ({
2070
+ id: i.id,
2071
+ file: {
2072
+ name: i.name,
2073
+ mime: i.information.type === "FILE" ? i.information.mime : "",
2074
+ size: i.information.type === "FILE" ? i.information.sizeInBytes : 0
2075
+ }
2076
+ }));
2077
+ if (selectionMode.type === "SINGLE") files = files.slice(0, 1);
2078
+ else if (selectionMode.maxFile) files = files.slice(0, selectionMode.maxFile);
2079
+ setSelectedFiles(files);
2080
+ });
2085
2081
  const [dialogs, setDialogs] = React3__namespace.default.useState({ newFolder: false, rename: false, delete: false });
2086
2082
  const [itemToDelete, setItemToDelete] = React3__namespace.default.useState(null);
2087
2083
  const [itemToRename, setItemToRename] = React3__namespace.default.useState(null);
@@ -2204,7 +2200,8 @@ var DriveFileGrid = (props) => {
2204
2200
  indicatorClassName: "nd:bg-primary"
2205
2201
  }
2206
2202
  ) }),
2207
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkHQTC3554_cjs.cn("nd:flex-1 nd:overflow-y-auto nd:min-h-0 nd:p-2 nd:sm:p-3 nd:md:p-4", className), children: [
2203
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkHQTC3554_cjs.cn("nd:flex-1 nd:overflow-y-auto nd:min-h-0 nd:relative nd:p-2 nd:sm:p-3 nd:md:p-4", className), onMouseDown: marqueeMouseDown, children: [
2204
+ marqueeOverlay,
2208
2205
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nd:space-y-4 nd:sm:space-y-6", children: Object.entries(groupedItems).map(([groupName, groupItems]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2209
2206
  groupBy !== "NONE" && /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "nd:text-xs nd:font-semibold nd:text-muted-foreground nd:uppercase nd:tracking-wide nd:mb-2 nd:sm:mb-3 nd:px-1", children: [
2210
2207
  groupName,
@@ -3855,7 +3852,7 @@ var SortableItem2 = (props) => {
3855
3852
  zIndex: isDragging ? 50 : "auto",
3856
3853
  position: "relative"
3857
3854
  };
3858
- return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: setNodeRef, style, ...attributes, ...listeners, children });
3855
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: setNodeRef, "data-marquee-id": id, style, ...attributes, ...listeners, children });
3859
3856
  };
3860
3857
  var DriveExplorer = (props) => {
3861
3858
  const { onItemClick, onItemDoubleClick, mimeFilter, className, selectableFolders = false } = props;
@@ -3872,6 +3869,8 @@ var DriveExplorer = (props) => {
3872
3869
  setSortBy,
3873
3870
  selectedFileIds,
3874
3871
  toggleSelected,
3872
+ setSelectedFiles,
3873
+ selectionMode,
3875
3874
  navigateToFolder,
3876
3875
  hasMore,
3877
3876
  loadMore,
@@ -3901,6 +3900,13 @@ var DriveExplorer = (props) => {
3901
3900
  const handleDragStart = (event) => {
3902
3901
  setDraggingItemId(event.active.id);
3903
3902
  };
3903
+ const moveSelection = async (activeId, targetFolderId) => {
3904
+ const ids = selectedFileIds.includes(activeId) && selectedFileIds.length > 1 ? selectedFileIds : [activeId];
3905
+ if (ids.length === 1) return moveItem(ids[0], targetFolderId);
3906
+ setItems((prev) => prev.filter((i) => !ids.includes(i.id)));
3907
+ setSelectedFiles([]);
3908
+ await callAPI("move", { method: "POST", body: JSON.stringify({ ids, targetFolderId }) });
3909
+ };
3904
3910
  const handleDragOver = (event) => {
3905
3911
  const { over } = event;
3906
3912
  if (!over) {
@@ -3924,13 +3930,13 @@ var DriveExplorer = (props) => {
3924
3930
  const overId = over.id;
3925
3931
  if (overId.startsWith("path-")) {
3926
3932
  const targetFolderId = overId.replace("path-", "");
3927
- await moveItem(active.id, targetFolderId === "root" ? "root" : targetFolderId);
3933
+ await moveSelection(active.id, targetFolderId === "root" ? "root" : targetFolderId);
3928
3934
  return;
3929
3935
  }
3930
3936
  const overItem = items.find((i) => i.id === over.id);
3931
3937
  const activeItem = items.find((i) => i.id === active.id);
3932
3938
  if (overItem?.information.type === "FOLDER" && activeItem) {
3933
- await moveItem(active.id, over.id);
3939
+ await moveSelection(active.id, over.id);
3934
3940
  return;
3935
3941
  }
3936
3942
  setItems((prevItems) => {
@@ -4034,6 +4040,19 @@ var DriveExplorer = (props) => {
4034
4040
  } else onItemDoubleClick?.(item);
4035
4041
  };
4036
4042
  const enableDrag = currentView === "BROWSE";
4043
+ const { onMouseDown: marqueeMouseDown, overlay: marqueeOverlay } = useMarquee((ids) => {
4044
+ let files = items.filter((i) => ids.includes(i.id)).map((i) => ({
4045
+ id: i.id,
4046
+ file: {
4047
+ name: i.name,
4048
+ mime: i.information.type === "FILE" ? i.information.mime : "",
4049
+ size: i.information.type === "FILE" ? i.information.sizeInBytes : 0
4050
+ }
4051
+ }));
4052
+ if (selectionMode.type === "SINGLE") files = files.slice(0, 1);
4053
+ else if (selectionMode.maxFile) files = files.slice(0, selectionMode.maxFile);
4054
+ setSelectedFiles(files);
4055
+ });
4037
4056
  const stateContent = (() => {
4038
4057
  if (isLoading && items.length === 0) {
4039
4058
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nd:flex nd:items-center nd:justify-center nd:py-12 nd:flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "nd:size-6 nd:animate-spin nd:text-muted-foreground" }) });
@@ -4064,7 +4083,8 @@ var DriveExplorer = (props) => {
4064
4083
  ] }),
4065
4084
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nd:lg:hidden nd:px-3 nd:py-2 nd:border-b nd:bg-background/95 nd:dark:bg-background/80 nd:backdrop-blur-sm", children: /* @__PURE__ */ jsxRuntime.jsx(DrivePathBar, {}) }),
4066
4085
  stateContent || /* @__PURE__ */ jsxRuntime.jsxs(ContextMenu, { children: [
4067
- /* @__PURE__ */ jsxRuntime.jsx(ContextMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkHQTC3554_cjs.cn("nd:flex-1 nd:overflow-y-auto nd:min-h-0 nd:container nd:mx-auto nd:p-2 nd:sm:p-3 nd:md:p-4", className), children: [
4086
+ /* @__PURE__ */ jsxRuntime.jsx(ContextMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkHQTC3554_cjs.cn("nd:flex-1 nd:overflow-y-auto nd:min-h-0 nd:relative nd:container nd:mx-auto nd:p-2 nd:sm:p-3 nd:md:p-4", className), onMouseDown: marqueeMouseDown, children: [
4087
+ marqueeOverlay,
4068
4088
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "nd:space-y-4 nd:sm:space-y-6 nd:pb-8 nd:sm:pb-12", children: Object.entries(groupedItems).map(([groupName, groupItems]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nd:space-y-3", children: [
4069
4089
  groupBy !== "NONE" && /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "nd:text-sm nd:font-medium nd:text-muted-foreground nd:flex nd:items-center nd:gap-2", children: [
4070
4090
  groupName,