diff --git a/cypress/e2e/item/bookmarks/bookmarks.cy.ts b/cypress/e2e/item/bookmarks/bookmarks.cy.ts
index f0a90c430..95bd4a796 100644
--- a/cypress/e2e/item/bookmarks/bookmarks.cy.ts
+++ b/cypress/e2e/item/bookmarks/bookmarks.cy.ts
@@ -47,7 +47,7 @@ describe('Bookmarked Item', () => {
     });
 
     it('Show empty table', () => {
-      i18n.changeLanguage(CURRENT_USER.extra.lang as string);
+      i18n.changeLanguage(CURRENT_USER.extra.lang);
       const text = i18n.t(BUILDER.BOOKMARKS_NO_ITEM, { ns: BUILDER_NAMESPACE });
       cy.get(`#${BOOKMARKED_ITEMS_ID}`).should('contain', text);
     });
@@ -59,7 +59,7 @@ describe('Bookmarked Item', () => {
         items: [...ITEMS, NON_BOOKMARKED_ITEM],
         bookmarkedItems: BOOKMARKED_ITEMS,
       });
-      i18n.changeLanguage(CURRENT_USER.extra.lang as string);
+      i18n.changeLanguage(CURRENT_USER.extra.lang);
       cy.visit(BOOKMARKED_ITEMS_PATH);
     });
 
diff --git a/cypress/e2e/item/copy/copy.cy.ts b/cypress/e2e/item/copy/copy.cy.ts
index 634dddb55..2a7afac86 100644
--- a/cypress/e2e/item/copy/copy.cy.ts
+++ b/cypress/e2e/item/copy/copy.cy.ts
@@ -5,12 +5,24 @@ import {
 
 import { HOME_PATH, buildItemPath } from '../../../../src/config/paths';
 import {
+  COPY_MANY_ITEMS_BUTTON_SELECTOR,
   ITEM_MENU_COPY_BUTTON_CLASS,
   MY_GRAASP_ITEM_PATH,
   buildItemCard,
   buildItemsGridMoreButtonSelector,
 } from '../../../../src/config/selectors';
 
+const copyItems = ({
+  toItemPath,
+  rootId,
+}: {
+  toItemPath: string;
+  rootId?: string;
+}) => {
+  cy.get(COPY_MANY_ITEMS_BUTTON_SELECTOR).click();
+  cy.handleTreeMenu(toItemPath, rootId);
+};
+
 const copyItem = ({
   id,
   toItemPath,
@@ -82,4 +94,90 @@ describe('Copy Item', () => {
       expect(url).to.contain(id);
     });
   });
+
+  it('copy many items on Home', () => {
+    const folders = [
+      PackedFolderItemFactory(),
+      PackedFolderItemFactory(),
+      PackedFolderItemFactory(),
+    ];
+    cy.setUpApi({
+      items: folders,
+    });
+
+    // go to children item
+    cy.visit('/');
+
+    folders.forEach((item) => {
+      cy.selectItem(item.id);
+    });
+
+    // copy on home
+    copyItems({ toItemPath: '' });
+
+    cy.wait('@copyItems').then(({ request: { url } }) => {
+      folders.forEach((item) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
+
+  it('copy many items from Home to folder', () => {
+    const folders = [
+      PackedFolderItemFactory(),
+      PackedFolderItemFactory(),
+      PackedFolderItemFactory(),
+    ];
+    const toItem = PackedFolderItemFactory();
+    cy.setUpApi({
+      items: [...folders, toItem],
+    });
+
+    // go to children item
+    cy.visit('/');
+
+    folders.forEach((item) => {
+      cy.selectItem(item.id);
+    });
+
+    // copy on home
+    copyItems({ toItemPath: toItem.path });
+
+    cy.wait('@copyItems').then(({ request: { url, body } }) => {
+      expect(body.parentId).to.eq(toItem.id);
+      folders.forEach((item) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
+
+  it('copy many items from folder to folder', () => {
+    const parentItem = PackedFolderItemFactory();
+    const folders = [
+      PackedFolderItemFactory({ parentItem }),
+      PackedFolderItemFactory({ parentItem }),
+      PackedFolderItemFactory({ parentItem }),
+    ];
+    const toItem = PackedFolderItemFactory();
+    cy.setUpApi({
+      items: [...folders, parentItem, toItem],
+    });
+
+    // go to children item
+    cy.visit(buildItemPath(parentItem.id));
+
+    folders.forEach((item) => {
+      cy.selectItem(item.id);
+    });
+
+    // copy on home
+    copyItems({ toItemPath: toItem.path });
+
+    cy.wait('@copyItems').then(({ request: { url, body } }) => {
+      expect(body.parentId).to.eq(toItem.id);
+      folders.forEach((item) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
 });
diff --git a/cypress/e2e/item/move/moveItem.cy.ts b/cypress/e2e/item/move/move.cy.ts
similarity index 55%
rename from cypress/e2e/item/move/moveItem.cy.ts
rename to cypress/e2e/item/move/move.cy.ts
index b6a379d1f..8d3dd7a1a 100644
--- a/cypress/e2e/item/move/moveItem.cy.ts
+++ b/cypress/e2e/item/move/move.cy.ts
@@ -6,6 +6,7 @@ import {
 import { HOME_PATH, buildItemPath } from '../../../../src/config/paths';
 import {
   ITEM_MENU_MOVE_BUTTON_CLASS,
+  MOVE_MANY_ITEMS_BUTTON_SELECTOR,
   MY_GRAASP_ITEM_PATH,
   buildItemsGridMoreButtonSelector,
   buildNavigationModalItemId,
@@ -24,6 +25,17 @@ const openMoveModal = ({ id: movedItemId }: { id: string }) => {
   cy.get(`.${ITEM_MENU_MOVE_BUTTON_CLASS}`).click();
 };
 
+const moveItems = ({
+  toItemPath,
+  rootId,
+}: {
+  toItemPath: string;
+  rootId?: string;
+}) => {
+  cy.get(MOVE_MANY_ITEMS_BUTTON_SELECTOR).click();
+  cy.handleTreeMenu(toItemPath, rootId);
+};
+
 const moveItem = ({
   id: movedItemId,
   toItemPath,
@@ -37,7 +49,7 @@ const moveItem = ({
   cy.handleTreeMenu(toItemPath, rootId);
 };
 
-describe('Move Item', () => {
+describe('Move Items', () => {
   it('move item on Home', () => {
     cy.setUpApi({ items });
     cy.visit(HOME_PATH);
@@ -118,4 +130,90 @@ describe('Move Item', () => {
       expect(url).to.contain(movedItem);
     });
   });
+
+  it('move many items from Home to folder', () => {
+    const folders = [
+      PackedFolderItemFactory(),
+      PackedFolderItemFactory(),
+      PackedFolderItemFactory(),
+    ];
+    const toItem = PackedFolderItemFactory();
+    cy.setUpApi({
+      items: [...folders, toItem],
+    });
+
+    // go to children item
+    cy.visit('/');
+
+    folders.forEach((item) => {
+      cy.selectItem(item.id);
+    });
+
+    moveItems({ toItemPath: toItem.path });
+
+    cy.wait('@moveItems').then(({ request: { url, body } }) => {
+      expect(body.parentId).to.eq(toItem.id);
+      folders.forEach((item) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
+
+  it('move many items from folder to folder', () => {
+    const parentItem = PackedFolderItemFactory();
+    const folders = [
+      PackedFolderItemFactory({ parentItem }),
+      PackedFolderItemFactory({ parentItem }),
+      PackedFolderItemFactory({ parentItem }),
+    ];
+    const toItem = PackedFolderItemFactory();
+    cy.setUpApi({
+      items: [...folders, parentItem, toItem],
+    });
+
+    // go to children item
+    cy.visit(buildItemPath(parentItem.id));
+
+    folders.forEach((item) => {
+      cy.selectItem(item.id);
+    });
+
+    moveItems({ toItemPath: toItem.path });
+
+    cy.wait('@moveItems').then(({ request: { url, body } }) => {
+      expect(body.parentId).to.eq(toItem.id);
+      folders.forEach((item) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
+
+  it('move many items from folder to Home', () => {
+    const parentItem = PackedFolderItemFactory();
+    const folders = [
+      PackedFolderItemFactory({ parentItem }),
+      PackedFolderItemFactory({ parentItem }),
+      PackedFolderItemFactory({ parentItem }),
+    ];
+    cy.setUpApi({
+      items: [...folders, parentItem],
+    });
+
+    // go to children item
+    cy.visit(buildItemPath(parentItem.id));
+
+    folders.forEach((item) => {
+      cy.selectItem(item.id);
+    });
+
+    moveItems({ toItemPath: '' });
+
+    cy.wait('@moveItems').then(({ request: { url, body } }) => {
+      // eslint-disable-next-line no-unused-expressions
+      expect(body.parentId).to.be.undefined;
+      folders.forEach((item) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
 });
diff --git a/cypress/e2e/item/trash/deleteItem.cy.ts b/cypress/e2e/item/trash/delete.cy.ts
similarity index 54%
rename from cypress/e2e/item/trash/deleteItem.cy.ts
rename to cypress/e2e/item/trash/delete.cy.ts
index d0bdc875f..5176f7a99 100644
--- a/cypress/e2e/item/trash/deleteItem.cy.ts
+++ b/cypress/e2e/item/trash/delete.cy.ts
@@ -3,6 +3,7 @@ import { PackedRecycledItemDataFactory } from '@graasp/sdk';
 import { RECYCLE_BIN_PATH } from '../../../../src/config/paths';
 import {
   CONFIRM_DELETE_BUTTON_ID,
+  RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID,
   buildItemCard,
 } from '../../../../src/config/selectors';
 
@@ -11,18 +12,26 @@ const deleteItem = (id: string) => {
   cy.get(`#${CONFIRM_DELETE_BUTTON_ID}`).click();
 };
 
-describe('Delete Item', () => {
-  it('delete item', () => {
-    const recycledItemData = [
-      PackedRecycledItemDataFactory(),
-      PackedRecycledItemDataFactory(),
-    ];
+const deleteItems = () => {
+  cy.get(`#${RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID}`).click();
+  cy.get(`#${CONFIRM_DELETE_BUTTON_ID}`).click();
+};
+
+const recycledItemData = [
+  PackedRecycledItemDataFactory(),
+  PackedRecycledItemDataFactory(),
+];
+
+describe('Delete Items', () => {
+  beforeEach(() => {
     cy.setUpApi({
       items: recycledItemData.map(({ item }) => item),
       recycledItemData,
     });
     cy.visit(RECYCLE_BIN_PATH);
+  });
 
+  it('delete item', () => {
     const { id } = recycledItemData[0].item;
 
     // delete
@@ -32,4 +41,18 @@ describe('Delete Item', () => {
     });
     cy.wait('@getRecycledItems');
   });
+
+  it('delete many items', () => {
+    recycledItemData.forEach(({ item }) => {
+      cy.selectItem(item.id);
+    });
+
+    deleteItems();
+
+    cy.wait('@deleteItems').then(({ request: { url } }) => {
+      recycledItemData.forEach(({ item }) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
 });
diff --git a/cypress/e2e/item/trash/recycle.cy.ts b/cypress/e2e/item/trash/recycle.cy.ts
new file mode 100644
index 000000000..f780f6852
--- /dev/null
+++ b/cypress/e2e/item/trash/recycle.cy.ts
@@ -0,0 +1,103 @@
+import { PackedFolderItemFactory } from '@graasp/sdk';
+
+import { HOME_PATH, buildItemPath } from '../../../../src/config/paths';
+import {
+  ITEM_MENU_RECYCLE_BUTTON_CLASS,
+  ITEM_RECYCLE_BUTTON_CLASS,
+  buildItemsGridMoreButtonSelector,
+} from '../../../../src/config/selectors';
+
+const recycleItem = (id: string) => {
+  cy.get(buildItemsGridMoreButtonSelector(id)).click();
+  cy.get(`.${ITEM_MENU_RECYCLE_BUTTON_CLASS}`).click();
+};
+
+const FOLDER = PackedFolderItemFactory();
+const CHILD = PackedFolderItemFactory({ parentItem: FOLDER });
+const items = [FOLDER, CHILD, PackedFolderItemFactory()];
+
+const recycleItems = () => {
+  cy.get(`.${ITEM_RECYCLE_BUTTON_CLASS}`).click();
+};
+
+describe('Recycle Items', () => {
+  it('recycle item on Home', () => {
+    cy.setUpApi({ items });
+    cy.visit(HOME_PATH);
+
+    const { id } = items[0];
+
+    recycleItem(id);
+    cy.wait('@recycleItems').then(({ request: { url } }) => {
+      expect(url).to.contain(id);
+    });
+    cy.wait('@getAccessibleItems');
+  });
+
+  it('recycle item inside parent', () => {
+    cy.setUpApi({ items });
+    const { id } = FOLDER;
+    const { id: idToDelete } = CHILD;
+
+    // go to children item
+    cy.visit(buildItemPath(id));
+
+    // delete
+    recycleItem(idToDelete);
+    cy.wait('@recycleItems').then(({ request: { url } }) => {
+      expect(url).to.contain(idToDelete);
+    });
+  });
+
+  it('recycle many items from Home', () => {
+    const folders = [
+      PackedFolderItemFactory(),
+      PackedFolderItemFactory(),
+      PackedFolderItemFactory(),
+    ];
+    cy.setUpApi({
+      items: folders,
+    });
+
+    cy.visit('/');
+
+    folders.forEach((item) => {
+      cy.selectItem(item.id);
+    });
+
+    recycleItems();
+
+    cy.wait('@recycleItems').then(({ request: { url } }) => {
+      folders.forEach((item) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
+
+  it('recycle many items from folder', () => {
+    const parentItem = PackedFolderItemFactory();
+    const folders = [
+      PackedFolderItemFactory({ parentItem }),
+      PackedFolderItemFactory({ parentItem }),
+      PackedFolderItemFactory({ parentItem }),
+    ];
+    cy.setUpApi({
+      items: [...folders, parentItem],
+    });
+
+    // go to children item
+    cy.visit(buildItemPath(parentItem.id));
+
+    folders.forEach((item) => {
+      cy.selectItem(item.id);
+    });
+
+    recycleItems();
+
+    cy.wait('@recycleItems').then(({ request: { url } }) => {
+      folders.forEach((item) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
+});
diff --git a/cypress/e2e/item/trash/recycleItem.cy.ts b/cypress/e2e/item/trash/recycleItem.cy.ts
deleted file mode 100644
index 4c2086fb7..000000000
--- a/cypress/e2e/item/trash/recycleItem.cy.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { PackedFolderItemFactory } from '@graasp/sdk';
-
-import { HOME_PATH, buildItemPath } from '../../../../src/config/paths';
-import {
-  ITEM_MENU_RECYCLE_BUTTON_CLASS,
-  buildItemsGridMoreButtonSelector,
-} from '../../../../src/config/selectors';
-
-const recycleItem = (id: string) => {
-  cy.get(buildItemsGridMoreButtonSelector(id)).click();
-  cy.get(`.${ITEM_MENU_RECYCLE_BUTTON_CLASS}`).click();
-};
-
-const FOLDER = PackedFolderItemFactory();
-const CHILD = PackedFolderItemFactory({ parentItem: FOLDER });
-const items = [FOLDER, CHILD, PackedFolderItemFactory()];
-
-describe('Recycle Item', () => {
-  it('recycle item on Home', () => {
-    cy.setUpApi({ items });
-    cy.visit(HOME_PATH);
-
-    const { id } = items[0];
-
-    recycleItem(id);
-    cy.wait('@recycleItems').then(({ request: { url } }) => {
-      expect(url).to.contain(id);
-    });
-    cy.wait('@getAccessibleItems');
-  });
-
-  it('recycle item inside parent', () => {
-    cy.setUpApi({ items });
-    const { id } = FOLDER;
-    const { id: idToDelete } = CHILD;
-
-    // go to children item
-    cy.visit(buildItemPath(id));
-
-    // delete
-    recycleItem(idToDelete);
-    cy.wait('@recycleItems').then(({ request: { url } }) => {
-      expect(url).to.contain(idToDelete);
-    });
-  });
-});
diff --git a/cypress/e2e/item/trash/restoreItem.cy.ts b/cypress/e2e/item/trash/restore.cy.ts
similarity index 57%
rename from cypress/e2e/item/trash/restoreItem.cy.ts
rename to cypress/e2e/item/trash/restore.cy.ts
index 5f5b062de..55e00a214 100644
--- a/cypress/e2e/item/trash/restoreItem.cy.ts
+++ b/cypress/e2e/item/trash/restore.cy.ts
@@ -2,6 +2,7 @@ import { PackedRecycledItemDataFactory } from '@graasp/sdk';
 
 import { RECYCLE_BIN_PATH } from '../../../../src/config/paths';
 import {
+  RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID,
   RESTORE_ITEMS_BUTTON_CLASS,
   buildItemCard,
 } from '../../../../src/config/selectors';
@@ -9,19 +10,25 @@ import {
 const restoreItem = (id: string) => {
   cy.get(`#${buildItemCard(id)} .${RESTORE_ITEMS_BUTTON_CLASS}`).click();
 };
+const restoreItems = () => {
+  cy.get(`#${RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID}`).click();
+};
+
+const recycledItemData = [
+  PackedRecycledItemDataFactory(),
+  PackedRecycledItemDataFactory(),
+];
 
 describe('Restore Items', () => {
-  it('restore one item', () => {
-    const recycledItemData = [
-      PackedRecycledItemDataFactory(),
-      PackedRecycledItemDataFactory(),
-    ];
+  beforeEach(() => {
     cy.setUpApi({
       items: recycledItemData.map(({ item }) => item),
       recycledItemData,
     });
     cy.visit(RECYCLE_BIN_PATH);
+  });
 
+  it('restore one item', () => {
     const { id } = recycledItemData[0].item;
 
     // restore
@@ -31,4 +38,18 @@ describe('Restore Items', () => {
     });
     cy.wait('@getRecycledItems');
   });
+
+  it('restore many items', () => {
+    recycledItemData.forEach(({ item }) => {
+      cy.selectItem(item.id);
+    });
+
+    restoreItems();
+
+    cy.wait('@restoreItems').then(({ request: { url } }) => {
+      recycledItemData.forEach(({ item }) => {
+        expect(url).to.contain(item.id);
+      });
+    });
+  });
 });
diff --git a/cypress/e2e/item/view/viewLink.cy.ts b/cypress/e2e/item/view/viewLink.cy.ts
index 23bad2ed2..c97b9e723 100644
--- a/cypress/e2e/item/view/viewLink.cy.ts
+++ b/cypress/e2e/item/view/viewLink.cy.ts
@@ -71,7 +71,7 @@ describe('Links', () => {
     cy.get(`[src="${extra.embeddedLink.thumbnails[0]}"]`);
   });
 
-  it.only('view youtube', () => {
+  it('view youtube', () => {
     const { id, extra } = YOUTUBE_LINK_ITEM;
     cy.visit(buildItemPath(id));
 
diff --git a/cypress/support/commands/item.ts b/cypress/support/commands/item.ts
index c31a9cce1..6329c10a8 100644
--- a/cypress/support/commands/item.ts
+++ b/cypress/support/commands/item.ts
@@ -1,4 +1,9 @@
-import { ItemType, getAppExtra, getDocumentExtra } from '@graasp/sdk';
+import {
+  DiscriminatedItem,
+  ItemType,
+  getAppExtra,
+  getDocumentExtra,
+} from '@graasp/sdk';
 
 import {
   CUSTOM_APP_CYPRESS_ID,
@@ -15,6 +20,7 @@ import {
   SHARE_ITEM_EMAIL_INPUT_ID,
   SHARE_ITEM_SHARE_BUTTON_ID,
   TREE_MODAL_CONFIRM_BUTTON_ID,
+  buildFolderItemCardThumbnail,
   buildItemFormAppOptionId,
   buildItemRowArrowId,
   buildNavigationModalItemId,
@@ -203,3 +209,7 @@ Cypress.Commands.add('dragAndDrop', (subject, x, y) => {
         .trigger('mouseup');
     });
 });
+
+Cypress.Commands.add('selectItem', (id: DiscriminatedItem['id']) => {
+  cy.get(buildFolderItemCardThumbnail(id)).click();
+});
diff --git a/cypress/support/index.ts b/cypress/support/index.ts
index 7eb6bda45..85ce30a14 100644
--- a/cypress/support/index.ts
+++ b/cypress/support/index.ts
@@ -1,5 +1,6 @@
 import {
   AppItemExtra,
+  DiscriminatedItem,
   DocumentItemExtra,
   ItemType,
   PermissionLevel,
@@ -77,6 +78,9 @@ declare global {
       ): void;
 
       dragAndDrop(subject: string, x: number, y: number): void;
+
+      selectItem(id: DiscriminatedItem['id']): void;
+
       // TODO
       setUpApi(args?: any): any;
 
diff --git a/package.json b/package.json
index 774eb9b1d..8be63fd8a 100644
--- a/package.json
+++ b/package.json
@@ -16,15 +16,16 @@
   "dependencies": {
     "@ag-grid-community/core": "31.3.4",
     "@ag-grid-community/styles": "31.3.4",
+    "@air/react-drag-to-select": "5.0.8",
     "@emotion/cache": "11.13.0",
     "@emotion/react": "11.13.0",
     "@emotion/styled": "11.13.0",
     "@graasp/chatbox": "3.1.0",
     "@graasp/map": "1.16.0",
     "@graasp/query-client": "3.16.0",
-    "@graasp/sdk": "4.19.0",
+    "@graasp/sdk": "4.20.0",
     "@graasp/translations": "1.32.0",
-    "@graasp/ui": "4.21.0",
+    "@graasp/ui": "4.22.0",
     "@mui/icons-material": "5.16.4",
     "@mui/lab": "5.0.0-alpha.172",
     "@mui/material": "5.16.4",
diff --git a/src/components/common/DeleteButton.tsx b/src/components/common/DeleteButton.tsx
index 5411ff769..52f2b25d7 100644
--- a/src/components/common/DeleteButton.tsx
+++ b/src/components/common/DeleteButton.tsx
@@ -1,5 +1,6 @@
 import { useState } from 'react';
 
+import { PackedItem } from '@graasp/sdk';
 import {
   ActionButtonVariant,
   ColorVariants,
@@ -12,11 +13,12 @@ import { BUILDER } from '../../langs/constants';
 import DeleteItemDialog from '../main/DeleteItemDialog';
 
 type Props = {
-  itemIds: string[];
+  items: PackedItem[];
   color?: ColorVariants;
   id?: string;
   type?: ActionButtonVariant;
-  onClick?: () => void;
+  onConfirm?: () => void;
+  onClose?: () => void;
 };
 
 /**
@@ -24,11 +26,12 @@ type Props = {
  * This button opens a dialog to confirm the action
  */
 const DeleteButton = ({
-  itemIds,
+  items,
   color,
   id,
   type,
-  onClick,
+  onConfirm,
+  onClose,
 }: Props): JSX.Element => {
   const { t: translateBuilder } = useBuilderTranslation();
 
@@ -36,11 +39,11 @@ const DeleteButton = ({
 
   const handleClickOpen = () => {
     setOpen(true);
-    onClick?.();
   };
 
   const handleClose = () => {
     setOpen(false);
+    onClose?.();
   };
 
   const text = translateBuilder(BUILDER.DELETE_BUTTON);
@@ -58,9 +61,10 @@ const DeleteButton = ({
         className={ITEM_DELETE_BUTTON_CLASS}
       />
       <DeleteItemDialog
+        onConfirm={onConfirm}
         open={open}
         handleClose={handleClose}
-        itemIds={itemIds}
+        items={items}
       />
     </>
   );
diff --git a/src/components/common/RestoreButton.tsx b/src/components/common/RestoreButton.tsx
index 7458bfd8c..9bfd99d67 100644
--- a/src/components/common/RestoreButton.tsx
+++ b/src/components/common/RestoreButton.tsx
@@ -10,12 +10,14 @@ type Props = {
   itemIds: string[];
   color?: IconButtonProps['color'];
   id?: string;
+  onClick?: () => void;
 };
 
 const RestoreButton = ({
   itemIds,
   color = 'default',
   id,
+  onClick: onClickFn,
 }: Props): JSX.Element => {
   const { t: translateBuilder } = useBuilderTranslation();
   const { mutate: restoreItems } = mutations.useRestoreItems();
@@ -23,6 +25,7 @@ const RestoreButton = ({
   const onClick = () => {
     // restore items
     restoreItems(itemIds);
+    onClickFn?.();
   };
 
   const title = translateBuilder(BUILDER.RESTORE_ITEM_BUTTON);
diff --git a/src/components/file/SmallUploadFile.tsx b/src/components/file/SmallUploadFile.tsx
new file mode 100644
index 000000000..cba6b7e0d
--- /dev/null
+++ b/src/components/file/SmallUploadFile.tsx
@@ -0,0 +1,18 @@
+import { Stack } from '@mui/material';
+
+import { Upload } from 'lucide-react';
+
+const SmallUploadFile = ({ text }: { text: string }): JSX.Element => (
+  <Stack
+    direction="row"
+    gap={1}
+    boxSizing="border-box"
+    border="2px dashed grey"
+    borderRadius={2}
+    p={1}
+  >
+    <Upload /> {text}
+  </Stack>
+);
+
+export default SmallUploadFile;
diff --git a/src/components/item/FolderContent.tsx b/src/components/item/FolderContent.tsx
index d0c2d9d91..cbe7503e2 100644
--- a/src/components/item/FolderContent.tsx
+++ b/src/components/item/FolderContent.tsx
@@ -21,14 +21,19 @@ import SelectTypes from '../common/SelectTypes';
 import { useFilterItemsContext } from '../context/FilterItemsContext';
 import { useLayoutContext } from '../context/LayoutContext';
 import FileUploader from '../file/FileUploader';
-import ItemsTable from '../main/ItemsTable';
 import NewItemButton from '../main/NewItemButton';
+import ItemsTable from '../main/list/ItemsTable';
+import {
+  SelectionContextProvider,
+  useSelectionContext,
+} from '../main/list/SelectionContext';
 import { DesktopMap } from '../map/DesktopMap';
 import NoItemFilters from '../pages/NoItemFilters';
 import SortingSelect from '../table/SortingSelect';
 import { SortingOptionsForFolder } from '../table/types';
 import { useSorting } from '../table/useSorting';
 import FolderDescription from './FolderDescription';
+import FolderToolbar from './FolderSelectionToolbar';
 import { useItemSearch } from './ItemSearch';
 import ModeButton from './header/ModeButton';
 
@@ -42,6 +47,8 @@ type Props = {
 const Content = ({ item, searchText, items, sortBy }: Props) => {
   const { mode } = useLayoutContext();
   const { itemTypes } = useFilterItemsContext();
+  const { selectedIds, clearSelection, toggleSelection } =
+    useSelectionContext();
 
   const enableEditing = item.permission
     ? PermissionLevelCompare.lte(PermissionLevel.Write, item.permission)
@@ -62,9 +69,12 @@ const Content = ({ item, searchText, items, sortBy }: Props) => {
     return (
       <>
         <ItemsTable
+          selectedIds={selectedIds}
           enableMoveInBetween={sortBy === SortingOptionsForFolder.Order}
           id={buildItemsTableId(item.id)}
           items={items ?? []}
+          onCardClick={toggleSelection}
+          onMove={clearSelection}
         />
         {Boolean(enableEditing && !searchText && !itemTypes?.length) && (
           <Stack alignItems="center" mb={2}>
@@ -107,6 +117,7 @@ const FolderContent = ({ item }: { item: PackedItem }): JSX.Element => {
   const { t: translateEnums } = useEnumsTranslation();
   const { shouldDisplayItem } = useFilterItemsContext();
   const { t: translateBuilder } = useBuilderTranslation();
+  const { selectedIds } = useSelectionContext();
 
   const {
     data: children,
@@ -133,6 +144,10 @@ const FolderContent = ({ item }: { item: PackedItem }): JSX.Element => {
     )
     .sort(sortFn);
 
+  const sortingOptions = Object.values(SortingOptionsForFolder).sort((t1, t2) =>
+    translateEnums(t1).localeCompare(translateEnums(t2)),
+  );
+
   if (children) {
     return (
       <>
@@ -168,29 +183,30 @@ const FolderContent = ({ item }: { item: PackedItem }): JSX.Element => {
           gap={1}
           width="100%"
         >
-          <Stack
-            spacing={1}
-            direction="row"
-            justifyContent="space-between"
-            alignItems="center"
-          >
-            <SelectTypes />
-            <Stack direction="row" gap={1}>
-              {sortBy && setSortBy && (
-                <SortingSelect
-                  ordering={ordering}
-                  sortBy={sortBy}
-                  setSortBy={setSortBy}
-                  options={Object.values(SortingOptionsForFolder).sort(
-                    (t1, t2) =>
-                      translateEnums(t1).localeCompare(translateEnums(t2)),
-                  )}
-                  setOrdering={setOrdering}
-                />
-              )}
-              <ModeButton />
+          {selectedIds.length && folderChildren?.length ? (
+            <FolderToolbar items={folderChildren} />
+          ) : (
+            <Stack
+              spacing={1}
+              direction="row"
+              justifyContent="space-between"
+              alignItems="center"
+            >
+              <SelectTypes />
+              <Stack direction="row" gap={1}>
+                {sortBy && setSortBy && (
+                  <SortingSelect
+                    ordering={ordering}
+                    sortBy={sortBy}
+                    setSortBy={setSortBy}
+                    options={sortingOptions}
+                    setOrdering={setOrdering}
+                  />
+                )}
+                <ModeButton />
+              </Stack>
             </Stack>
-          </Stack>
+          )}
         </Stack>
         <Content
           sortBy={sortBy}
@@ -217,4 +233,14 @@ const FolderContent = ({ item }: { item: PackedItem }): JSX.Element => {
   );
 };
 
-export default FolderContent;
+export const FolderContentWrapper = ({
+  item,
+}: {
+  item: PackedItem;
+}): JSX.Element => (
+  <SelectionContextProvider>
+    <FolderContent item={item} />
+  </SelectionContextProvider>
+);
+
+export default FolderContentWrapper;
diff --git a/src/components/item/FolderSelectionToolbar.tsx b/src/components/item/FolderSelectionToolbar.tsx
new file mode 100644
index 000000000..a3bae2ae0
--- /dev/null
+++ b/src/components/item/FolderSelectionToolbar.tsx
@@ -0,0 +1,63 @@
+import { PackedItem } from '@graasp/sdk';
+
+import RecycleButton from '@/components/common/RecycleButton';
+import useModalStatus from '@/components/hooks/useModalStatus';
+import CopyButton from '@/components/item/copy/CopyButton';
+import { CopyModal } from '@/components/item/copy/CopyModal';
+import MoveButton from '@/components/item/move/MoveButton';
+import { MoveModal } from '@/components/item/move/MoveModal';
+import { useSelectionContext } from '@/components/main/list/SelectionContext';
+import SelectionToolbar from '@/components/main/list/SelectionToolbar';
+
+const FolderSelectionToolbar = ({
+  items,
+}: {
+  items: PackedItem[];
+}): JSX.Element => {
+  const { selectedIds, clearSelection } = useSelectionContext();
+
+  const {
+    isOpen: isCopyModalOpen,
+    openModal: openCopyModal,
+    closeModal: closeCopyModal,
+  } = useModalStatus();
+  const {
+    isOpen: isMoveModalOpen,
+    openModal: openMoveModal,
+    closeModal: closeMoveModal,
+  } = useModalStatus();
+
+  return (
+    <>
+      <CopyModal
+        onClose={() => {
+          closeCopyModal();
+          clearSelection();
+        }}
+        open={isCopyModalOpen}
+        itemIds={selectedIds}
+      />
+      <MoveModal
+        onClose={() => {
+          closeMoveModal();
+          clearSelection();
+        }}
+        open={isMoveModalOpen}
+        items={items?.filter(({ id }) => selectedIds.includes(id))}
+      />
+      <SelectionToolbar>
+        <>
+          <MoveButton onClick={openMoveModal} />
+          <CopyButton onClick={openCopyModal} />
+          <RecycleButton
+            onClick={clearSelection}
+            color="primary"
+            itemIds={selectedIds}
+          />
+        </>
+      </SelectionToolbar>
+    </>
+  );
+};
+
+export default FolderSelectionToolbar;
diff --git a/src/components/main/DeleteItemDialog.tsx b/src/components/main/DeleteItemDialog.tsx
index f7b859806..c4b46dff4 100644
--- a/src/components/main/DeleteItemDialog.tsx
+++ b/src/components/main/DeleteItemDialog.tsx
@@ -6,6 +6,7 @@ import {
   DialogTitle,
 } from '@mui/material';
 
+import { PackedItem } from '@graasp/sdk';
 import { Button } from '@graasp/ui';
 
 import { useBuilderTranslation } from '../../config/i18n';
@@ -20,23 +21,32 @@ const descriptionId = 'alert-dialog-description';
 type Props = {
   open?: boolean;
   handleClose: () => void;
-  itemIds: string[];
+  items: PackedItem[];
+  onConfirm?: () => void;
 };
 
 const DeleteItemDialog = ({
-  itemIds,
+  items,
   open = false,
   handleClose,
+  onConfirm,
 }: Props): JSX.Element => {
   const { t: translateBuilder } = useBuilderTranslation();
 
   const { mutate: deleteItems } = mutations.useDeleteItems();
 
+  const itemIds = items.map(({ id }) => id);
+
   const onDelete = () => {
     deleteItems(itemIds);
+    onConfirm?.();
     handleClose();
   };
 
+  const names = items
+    .sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1))
+    .map(({ name }) => <li>{name}</li>);
+
   return (
     <Dialog
       open={open}
@@ -52,6 +62,7 @@ const DeleteItemDialog = ({
           {translateBuilder(BUILDER.DELETE_ITEM_MODAL_CONTENT, {
             count: itemIds.length,
           })}
+          <ul>{names}</ul>
         </DialogContentText>
       </DialogContent>
       <DialogActions>
diff --git a/src/components/main/ItemMenuContent.tsx b/src/components/main/ItemMenuContent.tsx
index 0e25beb81..0a3592151 100644
--- a/src/components/main/ItemMenuContent.tsx
+++ b/src/components/main/ItemMenuContent.tsx
@@ -117,19 +117,17 @@ const ItemMenuContent = ({ item }: Props): JSX.Element => {
             type={ActionButton.MENU_ITEM}
           />
         )}
-        {member?.id && (
-          <>
-            <CopyButton
-              key="copy"
-              type={ActionButton.MENU_ITEM}
-              onClick={() => {
-                openCopyModal();
-                closeMenu();
-              }}
-            />
-            <DuplicateButton key="duplicate" item={item} />
-          </>
-        )}
+        {member?.id && [
+          <CopyButton
+            key="copy"
+            type={ActionButton.MENU_ITEM}
+            onClick={() => {
+              openCopyModal();
+              closeMenu();
+            }}
+          />,
+          <DuplicateButton key="duplicate" item={item} />,
+        ]}
         {canAdmin && (
           <MoveButton
             key="move"
@@ -142,39 +140,36 @@ const ItemMenuContent = ({ item }: Props): JSX.Element => {
         )}
         <Divider />
 
-        {canWrite && (
-          <>
-            <HideButton key="hide" type={ActionButton.MENU_ITEM} item={item} />
-            <PinButton key="pin" type={ActionButton.MENU_ITEM} item={item} />
-            {item.type !== ItemType.FOLDER && (
-              <CollapseButton
-                key="collapse"
-                type={ActionButton.MENU_ITEM}
-                item={item}
-              />
-            )}
-          </>
+        {canWrite && [
+          <HideButton key="hide" type={ActionButton.MENU_ITEM} item={item} />,
+          <PinButton key="pin" type={ActionButton.MENU_ITEM} item={item} />,
+        ]}
+
+        {canWrite && item.type !== ItemType.FOLDER && (
+          <CollapseButton
+            key="collapse"
+            type={ActionButton.MENU_ITEM}
+            item={item}
+          />
         )}
 
         <Divider />
 
-        {member?.id && (
-          <>
-            <CreateShortcutButton
-              key="shortcut"
-              onClick={() => {
-                openCreateShortcutModal();
-                closeMenu();
-              }}
-            />
-            <BookmarkButton
-              size="medium"
-              key="bookmark"
-              type={ActionButton.MENU_ITEM}
-              item={item}
-            />
-          </>
-        )}
+        {member?.id && [
+          <CreateShortcutButton
+            key="shortcut"
+            onClick={() => {
+              openCreateShortcutModal();
+              closeMenu();
+            }}
+          />,
+          <BookmarkButton
+            size="medium"
+            key="bookmark"
+            type={ActionButton.MENU_ITEM}
+            item={item}
+          />,
+        ]}
         {canWrite && (
           <ItemSettingsButton
             key="settings"
@@ -184,15 +179,15 @@ const ItemMenuContent = ({ item }: Props): JSX.Element => {
         )}
 
         {canAdmin ? (
-          <>
-            <Divider />
+          [
+            <Divider />,
             <RecycleButton
               key="recycle"
               type={ActionButton.MENU_ITEM}
               itemIds={[item.id]}
               onClick={closeMenu}
-            />
-          </>
+            />,
+          ]
         ) : (
           <Divider />
         )}
diff --git a/src/components/main/TableHead.tsx b/src/components/main/TableHead.tsx
deleted file mode 100644
index 77aece4cc..000000000
--- a/src/components/main/TableHead.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { MouseEvent } from 'react';
-
-import {
-  Checkbox,
-  TableCell,
-  TableCellProps,
-  TableHead,
-  TableRow,
-  TableSortLabel,
-  TableSortLabelProps,
-} from '@mui/material';
-
-import { useBuilderTranslation } from '../../config/i18n';
-import { Ordering } from '../../enums';
-import { BUILDER } from '../../langs/constants';
-
-type Props = {
-  classes: {
-    visuallyHidden: string;
-  };
-  numSelected: number;
-  onRequestSort: (event: MouseEvent, property: string) => void;
-  onSelectAllClick: () => void;
-  order: 'asc' | 'desc';
-  orderBy: string;
-  rowCount: number;
-  headCells: (TableCellProps & { id: string; label: string })[];
-};
-
-const CustomTableHead = (props: Props): JSX.Element => {
-  const {
-    classes,
-    onSelectAllClick,
-    order,
-    orderBy,
-    numSelected,
-    rowCount,
-    onRequestSort,
-    headCells,
-  } = props;
-  const { t: translateBuilder } = useBuilderTranslation();
-  const createSortHandler =
-    (property: string): TableSortLabelProps['onClick'] =>
-    (event) => {
-      onRequestSort(event, property);
-    };
-
-  return (
-    <TableHead>
-      <TableRow>
-        <TableCell padding="checkbox">
-          <Checkbox
-            indeterminate={numSelected > 0 && numSelected < rowCount}
-            checked={rowCount > 0 && numSelected === rowCount}
-            onChange={onSelectAllClick}
-            inputProps={{
-              'aria-label': translateBuilder(BUILDER.TABLE_SELECT_ALL_LABEL),
-            }}
-            color="primary"
-          />
-        </TableCell>
-        {headCells.map((headCell) => (
-          <TableCell
-            key={headCell.id}
-            align={headCell.align}
-            sortDirection={orderBy === headCell.id ? order : undefined}
-          >
-            <TableSortLabel
-              active={orderBy === headCell.id}
-              direction={orderBy === headCell.id ? order : 'asc'}
-              onClick={createSortHandler(headCell.id)}
-            >
-              {headCell.label}
-              {orderBy === headCell.id ? (
-                <span className={classes.visuallyHidden}>
-                  {order === Ordering.DESC
-                    ? translateBuilder(BUILDER.TABLE_DESC_SORT_LABEL)
-                    : translateBuilder(BUILDER.TABLE_ASC_SORT_LABEL)}
-                </span>
-              ) : null}
-            </TableSortLabel>
-          </TableCell>
-        ))}
-      </TableRow>
-    </TableHead>
-  );
-};
-
-export default CustomTableHead;
diff --git a/src/components/main/itemSelectionModal/RootNavigationTree.tsx b/src/components/main/itemSelectionModal/RootNavigationTree.tsx
index a7db12bad..fc9f11a29 100644
--- a/src/components/main/itemSelectionModal/RootNavigationTree.tsx
+++ b/src/components/main/itemSelectionModal/RootNavigationTree.tsx
@@ -32,11 +32,15 @@ const RootNavigationTree = ({
   onNavigate,
   rootMenuItems,
   selectedId,
-}: RootNavigationTreeProps): JSX.Element => {
+}: RootNavigationTreeProps): JSX.Element | null => {
   const { t: translateBuilder } = useBuilderTranslation();
 
-  // todo: to change with real recent items (most used)
-  const { data: recentItems, isLoading } = hooks.useAccessibleItems(
+  // TODO: to change with real recent items (most used)
+  const {
+    data: recentItems,
+    isLoading,
+    isSuccess,
+  } = hooks.useAccessibleItems(
     // you can move into an item you have at least write permission
     {
       permissions: [PermissionLevel.Admin, PermissionLevel.Write],
@@ -51,7 +55,7 @@ const RootNavigationTree = ({
     enabled: Boolean(items[0]),
   });
 
-  if (recentItems?.data?.length) {
+  if (isSuccess) {
     return (
       <>
         <Typography color="darkgrey" variant="subtitle2">
@@ -65,7 +69,7 @@ const RootNavigationTree = ({
           buildRowMenuId={buildNavigationModalItemId}
           buildRowMenuArrowId={buildItemRowArrowId}
         />
-        {recentItems && (
+        {Boolean(recentItems.data.length) && (
           <>
             <Typography color="darkgrey" variant="subtitle2">
               {translateBuilder(BUILDER.ITEM_SELECTION_NAVIGATION_RECENT_ITEMS)}
diff --git a/src/components/main/ItemForbiddenScreen.tsx b/src/components/main/list/ItemForbiddenScreen.tsx
similarity index 79%
rename from src/components/main/ItemForbiddenScreen.tsx
rename to src/components/main/list/ItemForbiddenScreen.tsx
index d40170ec0..725289403 100644
--- a/src/components/main/ItemForbiddenScreen.tsx
+++ b/src/components/main/list/ItemForbiddenScreen.tsx
@@ -5,10 +5,10 @@ import { Button, ForbiddenContent } from '@graasp/ui';
 
 import { hooks } from '@/config/queryClient';
 
-import { useBuilderTranslation } from '../../config/i18n';
-import { ITEM_LOGIN_SCREEN_FORBIDDEN_ID } from '../../config/selectors';
-import { BUILDER } from '../../langs/constants';
-import UserSwitchWrapper from '../common/UserSwitchWrapper';
+import { useBuilderTranslation } from '../../../config/i18n';
+import { ITEM_LOGIN_SCREEN_FORBIDDEN_ID } from '../../../config/selectors';
+import { BUILDER } from '../../../langs/constants';
+import UserSwitchWrapper from '../../common/UserSwitchWrapper';
 
 const ItemForbiddenScreen = (): JSX.Element => {
   const { data: member } = hooks.useCurrentMember();
diff --git a/src/components/main/ItemsTable.tsx b/src/components/main/list/ItemsTable.tsx
similarity index 67%
rename from src/components/main/ItemsTable.tsx
rename to src/components/main/list/ItemsTable.tsx
index 5eb2e270e..a00ea3749 100644
--- a/src/components/main/ItemsTable.tsx
+++ b/src/components/main/list/ItemsTable.tsx
@@ -7,20 +7,21 @@ import { DialogActions, DialogContent, Skeleton } from '@mui/material';
 import Dialog from '@mui/material/Dialog';
 import DialogTitle from '@mui/material/DialogTitle';
 
-import { ItemType, PackedItem } from '@graasp/sdk';
-import { COMMON } from '@graasp/translations';
+import { DiscriminatedItem, ItemType, PackedItem } from '@graasp/sdk';
+import { COMMON, FAILURE_MESSAGES } from '@graasp/translations';
 import { Button, DraggingWrapper } from '@graasp/ui';
 
 import {
   useBuilderTranslation,
   useCommonTranslation,
   useEnumsTranslation,
+  useMessagesTranslation,
 } from '@/config/i18n';
 import { BUILDER } from '@/langs/constants';
 
-import { hooks, mutations } from '../../config/queryClient';
-import { useUploadWithProgress } from '../hooks/uploadWithProgress';
-import { useItemsStatuses } from '../table/Badges';
+import { hooks, mutations } from '../../../config/queryClient';
+import { useUploadWithProgress } from '../../hooks/uploadWithProgress';
+import { useItemsStatuses } from '../../table/Badges';
 import ItemsTableCard from './ItemsTableCard';
 
 const { useItem } = hooks;
@@ -31,6 +32,9 @@ export type ItemsTableProps = {
   showThumbnails?: boolean;
   canMove?: boolean;
   enableMoveInBetween?: boolean;
+  onCardClick?: (id: DiscriminatedItem['id']) => void;
+  selectedIds?: string[];
+  onMove?: () => void;
 };
 
 const ItemsTable = ({
@@ -39,10 +43,14 @@ const ItemsTable = ({
   showThumbnails = true,
   canMove = true,
   enableMoveInBetween = true,
+  selectedIds = [],
+  onCardClick,
+  onMove,
 }: ItemsTableProps): JSX.Element => {
   const [open, setOpen] = useState(false);
   const { t: translateCommon } = useCommonTranslation();
   const { t: translateBuilder } = useBuilderTranslation();
+  const { t: translateMessage } = useMessagesTranslation();
   const { t: translateEnums } = useEnumsTranslation();
 
   const { itemId } = useParams();
@@ -55,7 +63,7 @@ const ItemsTable = ({
   const { mutate: moveItems } = mutations.useMoveItems();
   const { mutateAsync: uploadItems } = mutations.useUploadFiles();
   const [moveData, setMoveData] = useState<{
-    movedItem: PackedItem;
+    movedItems: PackedItem[];
     to: PackedItem;
   }>();
 
@@ -67,7 +75,10 @@ const ItemsTable = ({
     items: rows,
   });
 
-  const onDropInRow = (movedItem: PackedItem | any, targetItem: PackedItem) => {
+  const onDropInRow = (
+    movedItem: PackedItem | { files: File[] },
+    targetItem: PackedItem,
+  ) => {
     // prevent drop in non-folder item
     if (targetItem.type !== ItemType.FOLDER) {
       toast.error(
@@ -79,7 +90,7 @@ const ItemsTable = ({
     }
 
     // upload files in item
-    if (movedItem.files) {
+    if ('files' in movedItem) {
       uploadItems({
         files: movedItem.files,
         id: targetItem.id,
@@ -91,21 +102,39 @@ const ItemsTable = ({
         .catch((e) => {
           close(e);
         });
-    } else if (movedItem.id !== targetItem.id) {
-      setOpen(true);
-      setMoveData({ movedItem, to: targetItem });
+      return;
+    }
+
+    // cannot move item into itself, or target cannot be part of selection if moving selection
+    if (
+      movedItem.id === targetItem.id ||
+      (selectedIds.includes(movedItem?.id) &&
+        selectedIds.includes(targetItem.id))
+    ) {
+      toast.error(translateMessage(FAILURE_MESSAGES.INVALID_MOVE_TARGET));
+      return;
+    }
+
+    let movedItems: PackedItem[] = [];
+    // use selected ids on drag move if moved item is part of selected ids
+    if (selectedIds.includes(movedItem?.id)) {
+      movedItems = rows.filter(({ id }) => selectedIds.includes(id));
+    } else if (movedItem) {
+      movedItems = [movedItem];
     }
+    setMoveData({ movedItems, to: targetItem });
+    setOpen(true);
   };
 
   // warning: this won't work anymore with pagination!
   const onDropBetweenRow = (
-    { files, id }: PackedItem | any,
+    el: PackedItem | { files: File[] },
     previousItem?: PackedItem,
   ) => {
     // upload files at row
-    if (files) {
+    if ('files' in el) {
       uploadItems({
-        files,
+        files: el.files,
         id: parentItem?.id,
         previousItemId: previousItem?.id,
         onUploadProgress: update,
@@ -120,6 +149,7 @@ const ItemsTable = ({
       console.error('cannot move in root');
       toast.error(BUILDER.ERROR_MESSAGE);
     } else {
+      const { id } = el;
       setMovingId(id);
       reorder({
         id,
@@ -133,12 +163,16 @@ const ItemsTable = ({
 
   const handleMoveItems = () => {
     if (moveData) {
-      moveItems({ items: [moveData.movedItem], to: moveData.to.id });
+      moveItems({ items: moveData.movedItems, to: moveData.to.id });
       setMoveData(undefined);
       handleClose();
+      onMove?.();
     }
   };
 
+  const isSelected = (droppedEl: PackedItem | { files: File[] }): boolean =>
+    'id' in droppedEl ? selectedIds.includes(droppedEl.id) : false;
+
   return (
     <>
       <DraggingWrapper
@@ -154,6 +188,12 @@ const ItemsTable = ({
             enableMoveInBetween={enableMoveInBetween}
             itemsStatuses={itemsStatuses}
             showThumbnails={showThumbnails}
+            isSelected={isSelected(droppedEl)}
+            onThumbnailClick={() => {
+              if ('id' in droppedEl) {
+                onCardClick?.(droppedEl.id);
+              }
+            }}
           />
         )}
         rows={rows}
@@ -169,7 +209,7 @@ const ItemsTable = ({
                 t={translateBuilder}
                 i18nKey={BUILDER.MOVE_CONFIRM_TITLE}
                 values={{
-                  name: moveData.movedItem.name,
+                  count: moveData.movedItems.length,
                   targetName: moveData.to.name,
                 }}
                 components={{ 1: <strong /> }}
@@ -177,9 +217,12 @@ const ItemsTable = ({
             </DialogTitle>
 
             <DialogContent>
-              {translateBuilder(BUILDER.MOVE_WARNING, {
-                name: moveData.movedItem.name,
-              })}
+              {translateBuilder(BUILDER.MOVE_WARNING)}
+              <ul>
+                {moveData.movedItems.map(({ name, id }) => (
+                  <li key={id}>{name}</li>
+                ))}
+              </ul>
             </DialogContent>
           </>
         ) : (
diff --git a/src/components/main/ItemsTableCard.tsx b/src/components/main/list/ItemsTableCard.tsx
similarity index 68%
rename from src/components/main/ItemsTableCard.tsx
rename to src/components/main/list/ItemsTableCard.tsx
index 3dd5c830c..daf0a74a6 100644
--- a/src/components/main/ItemsTableCard.tsx
+++ b/src/components/main/list/ItemsTableCard.tsx
@@ -3,17 +3,16 @@ import { Box, Stack } from '@mui/material';
 import { PackedItem } from '@graasp/sdk';
 import type { DroppedFile } from '@graasp/ui';
 
-import { Upload } from 'lucide-react';
-
+import SmallUploadFile from '@/components/file/SmallUploadFile';
 import { useBuilderTranslation } from '@/config/i18n';
 import { ItemLayoutMode } from '@/enums';
 import { BUILDER } from '@/langs/constants';
 
-import { useLayoutContext } from '../context/LayoutContext';
-import Badges, { ItemsStatuses } from '../table/Badges';
-import ItemActions from '../table/ItemActions';
-import ItemCard from '../table/ItemCard';
-import ItemMenuContent from './ItemMenuContent';
+import { useLayoutContext } from '../../context/LayoutContext';
+import Badges, { ItemsStatuses } from '../../table/Badges';
+import ItemActions from '../../table/ItemActions';
+import ItemCard from '../../table/ItemCard';
+import ItemMenuContent from '../ItemMenuContent';
 
 type Props = {
   item: PackedItem | DroppedFile;
@@ -23,6 +22,9 @@ type Props = {
   showThumbnails: boolean;
   itemsStatuses: ItemsStatuses;
   enableMoveInBetween: boolean;
+  onClick?: (id: string) => void;
+  isSelected?: boolean;
+  onThumbnailClick?: () => void;
 };
 
 const ItemsTableCard = ({
@@ -33,6 +35,9 @@ const ItemsTableCard = ({
   showThumbnails,
   itemsStatuses,
   enableMoveInBetween,
+  onClick,
+  isSelected,
+  onThumbnailClick,
 }: Props): JSX.Element => {
   const { mode } = useLayoutContext();
 
@@ -42,27 +47,20 @@ const ItemsTableCard = ({
 
   if ('files' in item) {
     return (
-      <Stack
-        direction="row"
-        gap={1}
-        boxSizing="border-box"
-        border="2px dashed grey"
-        borderRadius={2}
-        p={1}
-      >
-        <Upload /> {translateBuilder(BUILDER.UPLOAD_BETWEEN_FILES)}
-      </Stack>
+      <SmallUploadFile text={translateBuilder(BUILDER.UPLOAD_BETWEEN_FILES)} />
     );
   }
 
   return (
-    <Box px={1}>
+    <Box px={1} onClick={() => onClick?.(item.id)}>
       <ItemCard
+        onThumbnailClick={onThumbnailClick}
         dense={dense}
         item={item}
         isOver={isOver}
         disabled={!isMovable && enableMoveInBetween}
         isDragging={isDragging}
+        isSelected={isSelected}
         showThumbnail={showThumbnails}
         menu={<ItemMenuContent item={item} />}
         footer={
diff --git a/src/components/main/list/SelectionContext.tsx b/src/components/main/list/SelectionContext.tsx
new file mode 100644
index 000000000..61051cf61
--- /dev/null
+++ b/src/components/main/list/SelectionContext.tsx
@@ -0,0 +1,130 @@
+import {
+  createContext,
+  useCallback,
+  useContext,
+  useMemo,
+  useRef,
+  useState,
+} from 'react';
+
+import { PRIMARY_COLOR } from '@graasp/ui';
+
+import {
+  Box,
+  boxesIntersect,
+  useSelectionContainer,
+} from '@air/react-drag-to-select';
+
+import { ITEM_CARD_CLASS } from '@/config/selectors';
+
+type SelectionContextValue = {
+  selectedIds: string[];
+  toggleSelection: (id: string) => void;
+  clearSelection: () => void;
+};
+
+export const SelectionContext = createContext<SelectionContextValue>({
+  selectedIds: [],
+  toggleSelection: () => {},
+  clearSelection: () => {},
+});
+
+export const SelectionContextProvider = ({
+  children,
+  elementClass = ITEM_CARD_CLASS,
+}: {
+  children: JSX.Element;
+  elementClass?: string;
+}): JSX.Element => {
+  const [selection, setSelection] = useState(new Set<string>());
+  const elementsContainerRef = useRef<HTMLDivElement | null>(null);
+
+  const clearSelection = useCallback(() => {
+    setSelection(new Set());
+  }, []);
+
+  const toggleSelection = useCallback(
+    (id: string) => {
+      if (selection.has(id)) {
+        selection.delete(id);
+      } else {
+        selection.add(id);
+      }
+      setSelection(new Set(selection));
+    },
+    [selection],
+  );
+
+  const { DragSelection } = useSelectionContainer({
+    eventsElement: document.getElementById('root'),
+    onSelectionChange: (box) => {
+      /**
+       * Here we make sure to adjust the box's left and top with the scroll position of the window
+       * @see https://github.com/AirLabsTeam/react-drag-to-select/#scrolling
+       */
+      const scrollAwareBox: Box = {
+        ...box,
+        top: box.top + window.scrollY,
+        left: box.left + window.scrollX,
+      };
+
+      Array.from(document.getElementsByClassName(elementClass)).forEach(
+        (item) => {
+          const bb = item.getBoundingClientRect();
+          if (
+            boxesIntersect(scrollAwareBox, bb) &&
+            item.parentNode instanceof HTMLElement
+          ) {
+            const itemId = item.parentNode.dataset.id;
+            if (itemId) {
+              selection.add(itemId);
+            }
+          }
+        },
+      );
+
+      setSelection(new Set(selection));
+    },
+    shouldStartSelecting: (e) => {
+      // does not trigger drag selection if mousedown on card
+      if (e instanceof HTMLElement) {
+        return !e?.closest(`.${ITEM_CARD_CLASS}`);
+      }
+      return true;
+    },
+    onSelectionStart: () => {
+      // clear selection on new dragging action
+      clearSelection();
+    },
+    onSelectionEnd: () => {},
+    selectionProps: {
+      style: {
+        border: `2px dashed ${PRIMARY_COLOR}`,
+        borderRadius: 4,
+        backgroundColor: 'lightblue',
+        opacity: 0.5,
+      },
+    },
+    isEnabled: true,
+  });
+
+  const value: SelectionContextValue = useMemo(
+    () => ({
+      selectedIds: [...selection.values()],
+      toggleSelection,
+      clearSelection,
+      elementsContainerRef,
+    }),
+    [selection, toggleSelection, clearSelection, elementsContainerRef],
+  );
+
+  return (
+    <SelectionContext.Provider value={value}>
+      <DragSelection />
+      <div ref={elementsContainerRef}>{children}</div>
+    </SelectionContext.Provider>
+  );
+};
+
+export const useSelectionContext = (): SelectionContextValue =>
+  useContext<SelectionContextValue>(SelectionContext);
diff --git a/src/components/main/list/SelectionToolbar.tsx b/src/components/main/list/SelectionToolbar.tsx
new file mode 100644
index 000000000..5cf6eb331
--- /dev/null
+++ b/src/components/main/list/SelectionToolbar.tsx
@@ -0,0 +1,33 @@
+import { Close } from '@mui/icons-material';
+import { IconButton, Stack } from '@mui/material';
+
+import { useSelectionContext } from './SelectionContext';
+
+const SelectionToolbar = ({
+  children,
+}: {
+  children: JSX.Element;
+}): JSX.Element => {
+  const { selectedIds, clearSelection } = useSelectionContext();
+
+  return (
+    <Stack
+      spacing={1}
+      direction="row"
+      justifyContent="space-between"
+      alignItems="center"
+      sx={{ backgroundColor: '#efefef' }}
+      borderRadius={2}
+    >
+      <Stack direction="row" alignItems="center">
+        <IconButton onClick={clearSelection}>
+          <Close />
+        </IconButton>
+        <Stack>{selectedIds.length} selected</Stack>
+      </Stack>
+      <Stack direction="row">{children}</Stack>
+    </Stack>
+  );
+};
+
+export default SelectionToolbar;
diff --git a/src/components/pages/BookmarkedItemsScreen.tsx b/src/components/pages/BookmarkedItemsScreen.tsx
index f167b1aa9..ca86d0dfe 100644
--- a/src/components/pages/BookmarkedItemsScreen.tsx
+++ b/src/components/pages/BookmarkedItemsScreen.tsx
@@ -15,7 +15,7 @@ import { useFilterItemsContext } from '../context/FilterItemsContext';
 import { useItemSearch } from '../item/ItemSearch';
 import ModeButton from '../item/header/ModeButton';
 import LoadingScreen from '../layout/LoadingScreen';
-import ItemsTable from '../main/ItemsTable';
+import ItemsTable from '../main/list/ItemsTable';
 import SortingSelect from '../table/SortingSelect';
 import { SortingOptions } from '../table/types';
 import { useSorting, useTranslatedSortingOptions } from '../table/useSorting';
diff --git a/src/components/pages/PublishedItemsScreen.tsx b/src/components/pages/PublishedItemsScreen.tsx
index 66db29200..8d992e428 100644
--- a/src/components/pages/PublishedItemsScreen.tsx
+++ b/src/components/pages/PublishedItemsScreen.tsx
@@ -15,7 +15,7 @@ import { useFilterItemsContext } from '../context/FilterItemsContext';
 import { useItemSearch } from '../item/ItemSearch';
 import ModeButton from '../item/header/ModeButton';
 import LoadingScreen from '../layout/LoadingScreen';
-import ItemsTable from '../main/ItemsTable';
+import ItemsTable from '../main/list/ItemsTable';
 import SortingSelect from '../table/SortingSelect';
 import { SortingOptions } from '../table/types';
 import { useSorting, useTranslatedSortingOptions } from '../table/useSorting';
diff --git a/src/components/pages/RecycledItemsScreen.tsx b/src/components/pages/RecycledItemsScreen.tsx
index 0245e96cf..d32b42b77 100644
--- a/src/components/pages/RecycledItemsScreen.tsx
+++ b/src/components/pages/RecycledItemsScreen.tsx
@@ -17,11 +17,16 @@ import { useFilterItemsContext } from '../context/FilterItemsContext';
 import { useItemSearch } from '../item/ItemSearch';
 import ModeButton from '../item/header/ModeButton';
 import LoadingScreen from '../layout/LoadingScreen';
+import {
+  SelectionContextProvider,
+  useSelectionContext,
+} from '../main/list/SelectionContext';
 import ItemCard from '../table/ItemCard';
 import SortingSelect from '../table/SortingSelect';
 import { SortingOptions } from '../table/types';
 import { useSorting, useTranslatedSortingOptions } from '../table/useSorting';
 import PageWrapper from './PageWrapper';
+import RecycleBinToolbar from './recycleBin/RecycleBinSelectionToolbar';
 
 const RecycledItemsScreenContent = ({
   searchText,
@@ -32,20 +37,23 @@ const RecycledItemsScreenContent = ({
   const { data: recycledItems, isLoading, isError } = hooks.useRecycledItems();
   const options = useTranslatedSortingOptions();
   const { shouldDisplayItem } = useFilterItemsContext();
-  const filteredData = recycledItems?.filter(
-    (d) =>
-      shouldDisplayItem(d.type) &&
-      d.name.toLowerCase().includes(searchText.toLocaleLowerCase()),
-  );
   const { sortBy, setSortBy, ordering, setOrdering, sortFn } =
     useSorting<SortingOptions>({
       sortBy: SortingOptions.ItemUpdatedAt,
       ordering: Ordering.DESC,
     });
-  filteredData?.sort(sortFn);
+  const filteredData = recycledItems
+    ?.filter(
+      (d) =>
+        shouldDisplayItem(d.type) &&
+        d.name.toLowerCase().includes(searchText.toLocaleLowerCase()),
+    )
+    ?.sort(sortFn);
+  const { selectedIds, toggleSelection } = useSelectionContext();
 
   // render this when there is data from the query
   if (recycledItems?.length) {
+    const hasSelection = selectedIds.length && filteredData?.length;
     return (
       <Stack gap={1}>
         <Stack
@@ -54,26 +62,30 @@ const RecycledItemsScreenContent = ({
           gap={1}
           width="100%"
         >
-          <Stack
-            spacing={1}
-            direction="row"
-            justifyContent="space-between"
-            alignItems="center"
-          >
-            <SelectTypes />
-            <Stack direction="row" gap={1}>
-              {sortBy && setSortBy && (
-                <SortingSelect
-                  sortBy={sortBy}
-                  options={options}
-                  setSortBy={setSortBy}
-                  ordering={ordering}
-                  setOrdering={setOrdering}
-                />
-              )}
-              <ModeButton />
+          {hasSelection ? (
+            <RecycleBinToolbar items={filteredData} />
+          ) : (
+            <Stack
+              spacing={1}
+              direction="row"
+              justifyContent="space-between"
+              alignItems="center"
+            >
+              <SelectTypes />
+              <Stack direction="row" gap={1}>
+                {sortBy && setSortBy && (
+                  <SortingSelect
+                    sortBy={sortBy}
+                    options={options}
+                    setSortBy={setSortBy}
+                    ordering={ordering}
+                    setOrdering={setOrdering}
+                  />
+                )}
+                <ModeButton />
+              </Stack>
             </Stack>
-          </Stack>
+          )}
         </Stack>
         {
           // render the filtered data and when it is empty display that nothing matches the search
@@ -82,11 +94,14 @@ const RecycledItemsScreenContent = ({
               <ItemCard
                 // todo: should not be able to click on the card
                 item={item}
+                onThumbnailClick={() => toggleSelection(item.id)}
+                isSelected={selectedIds.includes(item.id)}
                 showThumbnail={false}
+                allowNavigation={false}
                 footer={
                   <Stack justifyContent="right" direction="row">
                     <RestoreButton itemIds={[item.id]} />
-                    <DeleteButton itemIds={[item.id]} />
+                    <DeleteButton items={[item]} />
                   </Stack>
                 }
               />
@@ -135,7 +150,9 @@ const RecycledItemsScreen = (): JSX.Element | null => {
         </Stack>
       }
     >
-      <RecycledItemsScreenContent searchText={itemSearch.text} />
+      <SelectionContextProvider>
+        <RecycledItemsScreenContent searchText={itemSearch.text} />
+      </SelectionContextProvider>
     </PageWrapper>
   );
 };
diff --git a/src/components/pages/home/HomeScreen.tsx b/src/components/pages/home/HomeScreen.tsx
index d7c86f2dd..f7381d1a4 100644
--- a/src/components/pages/home/HomeScreen.tsx
+++ b/src/components/pages/home/HomeScreen.tsx
@@ -5,6 +5,10 @@ import { Alert, Box, LinearProgress, Stack } from '@mui/material';
 import { Button } from '@graasp/ui';
 
 import LoadingScreen from '@/components/layout/LoadingScreen';
+import {
+  SelectionContextProvider,
+  useSelectionContext,
+} from '@/components/main/list/SelectionContext';
 import { ITEM_PAGE_SIZE } from '@/config/constants';
 import { ShowOnlyMeChangeType } from '@/config/types';
 import { ItemLayoutMode, Ordering } from '@/enums';
@@ -22,8 +26,8 @@ import { useLayoutContext } from '../../context/LayoutContext';
 import FileUploader from '../../file/FileUploader';
 import { useItemSearch } from '../../item/ItemSearch';
 import ModeButton from '../../item/header/ModeButton';
-import ItemsTable from '../../main/ItemsTable';
 import NewItemButton from '../../main/NewItemButton';
+import ItemsTable from '../../main/list/ItemsTable';
 import { DesktopMap } from '../../map/DesktopMap';
 import ShowOnlyMeButton from '../../table/ShowOnlyMeButton';
 import SortingSelect from '../../table/SortingSelect';
@@ -31,6 +35,7 @@ import { SortingOptions } from '../../table/types';
 import { useSorting } from '../../table/useSorting';
 import NoItemFilters from '../NoItemFilters';
 import PageWrapper from '../PageWrapper';
+import HomeSelectionToolbar from './HomeSelectionToolbar';
 
 const HomeScreenContent = ({ searchText }: { searchText: string }) => {
   const { t: translateBuilder } = useBuilderTranslation();
@@ -39,6 +44,8 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => {
   const { itemTypes } = useFilterItemsContext();
   const [showOnlyMe, setShowOnlyMe] = useState(false);
 
+  const { selectedIds, toggleSelection, clearSelection } =
+    useSelectionContext();
   const { mode } = useLayoutContext();
   const { sortBy, setSortBy, ordering, setOrdering } =
     useSorting<SortingOptions>({
@@ -74,13 +81,14 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => {
     );
   }
 
-  if (data && data.pages.length) {
+  if (data?.pages?.length) {
     // default show upload zone
     let content = (
       <Box mt={2}>
         <FileUploader buttons={<NewItemButton />} />
       </Box>
     );
+
     if (data.pages[0].data.length) {
       const totalFetchedItems = data
         ? data.pages.map(({ data: d }) => d.length).reduce((a, b) => a + b, 0)
@@ -92,6 +100,9 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => {
             id={ACCESSIBLE_ITEMS_TABLE_ID}
             items={data.pages.flatMap(({ data: i }) => i)}
             enableMoveInBetween={false}
+            onCardClick={toggleSelection}
+            selectedIds={selectedIds}
+            onMove={clearSelection}
           />
           {!isFetching && data.pages[0].totalCount > totalFetchedItems && (
             <Stack textAlign="center" alignItems="center">
@@ -112,6 +123,10 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => {
       content = <NoItemFilters searchText={searchText} />;
     }
 
+    const sortingOptions = Object.values(SortingOptions).sort((t1, t2) =>
+      translateEnums(t1).localeCompare(translateEnums(t2)),
+    );
+
     return (
       <>
         <Stack
@@ -127,28 +142,33 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => {
               enabled={showOnlyMe}
             />
           </Stack>
-          <Stack
-            spacing={1}
-            direction="row"
-            justifyContent="space-between"
-            alignItems="center"
-          >
-            <SelectTypes />
-            <Stack direction="row" gap={1}>
-              {sortBy && setSortBy && (
-                <SortingSelect<SortingOptions>
-                  sortBy={sortBy}
-                  setSortBy={setSortBy}
-                  ordering={ordering}
-                  setOrdering={setOrdering}
-                  options={Object.values(SortingOptions).sort((t1, t2) =>
-                    translateEnums(t1).localeCompare(translateEnums(t2)),
-                  )}
-                />
-              )}
-              <ModeButton />
+
+          {selectedIds.length ? (
+            <HomeSelectionToolbar
+              items={data.pages.flatMap(({ data: i }) => i)}
+            />
+          ) : (
+            <Stack
+              spacing={1}
+              direction="row"
+              justifyContent="space-between"
+              alignItems="center"
+            >
+              <SelectTypes />
+              <Stack direction="row" gap={1}>
+                {sortBy && setSortBy && (
+                  <SortingSelect<SortingOptions>
+                    sortBy={sortBy}
+                    setSortBy={setSortBy}
+                    ordering={ordering}
+                    setOrdering={setOrdering}
+                    options={sortingOptions}
+                  />
+                )}
+                <ModeButton />
+              </Stack>
             </Stack>
-          </Stack>
+          )}
         </Stack>
         <Stack>
           {content}
@@ -191,7 +211,9 @@ const HomeScreen = (): JSX.Element => {
         </Stack>
       }
     >
-      <HomeScreenContent searchText={itemSearch.text} />
+      <SelectionContextProvider>
+        <HomeScreenContent searchText={itemSearch.text} />
+      </SelectionContextProvider>
     </PageWrapper>
   );
 };
diff --git a/src/components/pages/home/HomeSelectionToolbar.tsx b/src/components/pages/home/HomeSelectionToolbar.tsx
new file mode 100644
index 000000000..16eb1b1b3
--- /dev/null
+++ b/src/components/pages/home/HomeSelectionToolbar.tsx
@@ -0,0 +1,63 @@
+import { PackedItem } from '@graasp/sdk';
+
+import RecycleButton from '@/components/common/RecycleButton';
+import useModalStatus from '@/components/hooks/useModalStatus';
+import CopyButton from '@/components/item/copy/CopyButton';
+import { CopyModal } from '@/components/item/copy/CopyModal';
+import MoveButton from '@/components/item/move/MoveButton';
+import { MoveModal } from '@/components/item/move/MoveModal';
+import { useSelectionContext } from '@/components/main/list/SelectionContext';
+import SelectionToolbar from '@/components/main/list/SelectionToolbar';
+
+const HomeSelectionToolbar = ({
+  items,
+}: {
+  items: PackedItem[];
+}): JSX.Element => {
+  const { selectedIds, clearSelection } = useSelectionContext();
+
+  const {
+    isOpen: isCopyModalOpen,
+    openModal: openCopyModal,
+    closeModal: closeCopyModal,
+  } = useModalStatus();
+  const {
+    isOpen: isMoveModalOpen,
+    openModal: openMoveModal,
+    closeModal: closeMoveModal,
+  } = useModalStatus();
+
+  return (
+    <>
+      <CopyModal
+        onClose={() => {
+          closeCopyModal();
+          clearSelection();
+        }}
+        open={isCopyModalOpen}
+        itemIds={selectedIds}
+      />
+      <MoveModal
+        onClose={() => {
+          closeMoveModal();
+          clearSelection();
+        }}
+        open={isMoveModalOpen}
+        items={items?.filter(({ id }) => selectedIds.includes(id))}
+      />
+      <SelectionToolbar>
+        <>
+          <MoveButton onClick={openMoveModal} />
+          <CopyButton onClick={openCopyModal} />
+          <RecycleButton
+            onClick={clearSelection}
+            color="primary"
+            itemIds={selectedIds}
+          />
+        </>
+      </SelectionToolbar>
+    </>
+  );
+};
+
+export default HomeSelectionToolbar;
diff --git a/src/components/pages/item/ItemLoginWrapper.tsx b/src/components/pages/item/ItemLoginWrapper.tsx
index e0ec69502..f7a2f7cde 100644
--- a/src/components/pages/item/ItemLoginWrapper.tsx
+++ b/src/components/pages/item/ItemLoginWrapper.tsx
@@ -10,7 +10,7 @@ import {
   ITEM_LOGIN_SIGN_IN_USERNAME_ID,
 } from '@/config/selectors';
 
-import ItemForbiddenScreen from '../../main/ItemForbiddenScreen';
+import ItemForbiddenScreen from '../../main/list/ItemForbiddenScreen';
 
 const { useItem, useCurrentMember, useItemLoginSchemaType } = hooks;
 
diff --git a/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx b/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx
new file mode 100644
index 000000000..275887de9
--- /dev/null
+++ b/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx
@@ -0,0 +1,37 @@
+import { PackedItem } from '@graasp/sdk';
+
+import DeleteButton from '@/components/common/DeleteButton';
+import RestoreButton from '@/components/common/RestoreButton';
+import { useSelectionContext } from '@/components/main/list/SelectionContext';
+import SelectionToolbar from '@/components/main/list/SelectionToolbar';
+import {
+  RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID,
+  RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID,
+} from '@/config/selectors';
+
+const RecycleBinSelectionToolbar = ({
+  items,
+}: {
+  items: PackedItem[];
+}): JSX.Element => {
+  const { selectedIds, clearSelection } = useSelectionContext();
+
+  return (
+    <SelectionToolbar>
+      <>
+        <RestoreButton
+          id={RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID}
+          itemIds={selectedIds}
+          onClick={clearSelection}
+        />
+        <DeleteButton
+          id={RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID}
+          items={items.filter(({ id }) => selectedIds.includes(id))}
+          onConfirm={clearSelection}
+        />
+      </>
+    </SelectionToolbar>
+  );
+};
+
+export default RecycleBinSelectionToolbar;
diff --git a/src/components/table/ItemCard.tsx b/src/components/table/ItemCard.tsx
index a76d95840..d6d7a62e6 100644
--- a/src/components/table/ItemCard.tsx
+++ b/src/components/table/ItemCard.tsx
@@ -1,4 +1,4 @@
-import { Typography } from '@mui/material';
+import { Box, Typography } from '@mui/material';
 import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
 
 import {
@@ -14,7 +14,7 @@ import { Card, TextDisplay } from '@graasp/ui';
 import i18n, { useCommonTranslation } from '@/config/i18n';
 import { buildItemPath } from '@/config/paths';
 import { hooks } from '@/config/queryClient';
-import { buildItemCard } from '@/config/selectors';
+import { ITEM_CARD_CLASS, buildItemCard } from '@/config/selectors';
 
 type Props = {
   item: PackedItem;
@@ -25,6 +25,9 @@ type Props = {
   isDragging?: boolean;
   disabled?: boolean;
   menu?: JSX.Element;
+  isSelected?: boolean;
+  allowNavigation?: boolean;
+  onThumbnailClick?: () => void;
 };
 
 const ItemCard = ({
@@ -33,9 +36,12 @@ const ItemCard = ({
   dense = true,
   isDragging = false,
   isOver = false,
+  isSelected = false,
   showThumbnail = true,
   disabled,
   menu,
+  allowNavigation = true,
+  onThumbnailClick,
 }: Props): JSX.Element => {
   const { t: translateCommon } = useCommonTranslation();
   const { data: thumbnailUrl } = hooks.useItemThumbnailUrl({
@@ -90,24 +96,29 @@ const ItemCard = ({
   }
 
   return (
-    <Card
-      id={buildItemCard(item.id)}
-      sx={{ background: disabled ? 'lightgrey' : undefined }}
-      dense={dense}
-      elevation={false}
-      thumbnail={thumbnail}
-      to={to}
-      name={item.name}
-      alt={item.name}
-      type={item.type}
-      footer={footer}
-      creator={item.creator?.name}
-      content={content}
-      fullWidth
-      menu={menu}
-      isDragging={isDragging}
-      isOver={isOver}
-    />
+    <Box data-id={item.id}>
+      <Card
+        onThumbnailClick={onThumbnailClick}
+        className={ITEM_CARD_CLASS}
+        id={buildItemCard(item.id)}
+        sx={{ background: disabled ? 'lightgrey' : undefined }}
+        dense={dense}
+        elevation={false}
+        thumbnail={thumbnail}
+        to={allowNavigation ? to : undefined}
+        name={item.name}
+        alt={item.name}
+        type={item.type}
+        footer={footer}
+        creator={item.creator?.name}
+        content={content}
+        fullWidth
+        menu={menu}
+        isDragging={isDragging}
+        isOver={isOver}
+        isSelected={isSelected}
+      />
+    </Box>
   );
 };
 export default ItemCard;
diff --git a/src/config/selectors.ts b/src/config/selectors.ts
index f9d6fc26a..4cd767e1b 100644
--- a/src/config/selectors.ts
+++ b/src/config/selectors.ts
@@ -424,3 +424,13 @@ export const BOOKMARK_ICON_SELECTOR =
 export const MEMBER_VALIDATION_BANNER_ID = 'memberValidationBanner';
 export const MEMBER_VALIDATION_BANNER_CLOSE_BUTTON_ID =
   'memberValidationBannerCloseButton';
+export const ITEM_CARD_CLASS = 'item-card';
+export const buildFolderItemCardThumbnail = (id: string): string =>
+  `#${buildItemCard(id)} .lucide-folder`;
+export const RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID =
+  'recycleBinDeleteManyButton';
+
+export const RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID =
+  'recycleBinRestoreManyButton';
+export const COPY_MANY_ITEMS_BUTTON_SELECTOR = `[data-testid="FilterNoneIcon"]`;
+export const MOVE_MANY_ITEMS_BUTTON_SELECTOR = `[data-testid="OpenWithIcon"]`;
diff --git a/src/langs/de.json b/src/langs/de.json
index 2ff7d45bd..185ffe51b 100644
--- a/src/langs/de.json
+++ b/src/langs/de.json
@@ -461,7 +461,6 @@
   "UPLOAD_BETWEEN_FILES": "Laden Sie Ihre Datei(en) hier hoch",
   "MOVE_WARNING": "Dieser Vorgang könnte mehr Personen als zuvor Zugriff auf {{name}} gewähren. Möchten Sie fortfahren?",
   "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "Elemente in {{type}} können nicht hinzugefügt werden",
-  "MOVE_CONFIRM_TITLE": "Bestätigen Sie das Verschieben von <1>{{name}}</1> innerhalb von <1>{{targetName}}</1>",
   "ITEM_SEARCH_NOTHING_FOUND": "Kein Artikel mit diesen Parametern gefunden",
   "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "suchen",
   "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "Typen",
diff --git a/src/langs/en.json b/src/langs/en.json
index 40081c78b..a991c3f60 100644
--- a/src/langs/en.json
+++ b/src/langs/en.json
@@ -459,9 +459,10 @@
   "BOOKMARKS_NO_ITEM": "No bookmarked item",
   "BOOKMARKS_NO_ITEM_SEARCH": "No bookmarked item for {{search}}",
   "UPLOAD_BETWEEN_FILES": "Upload your file(s) here",
-  "MOVE_WARNING": "This operation might give access to {{name}} to more persons than previously. Do you want to proceed?",
+  "MOVE_WARNING": "This operation might give access to more persons than previously to the items below. Do you want to proceed?",
   "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "Cannot add items in {{type}}",
-  "MOVE_CONFIRM_TITLE": "Confirm moving <1>{{name}}</1> inside <1>{{targetName}}</1>",
+  "MOVE_CONFIRM_TITLE_one": "Confirm moving one item inside <1>{{targetName}}</1>?",
+  "MOVE_CONFIRM_TITLE": "Confirm moving <1>{{count}}</1> items inside <1>{{targetName}}</1>?",
   "ITEM_SEARCH_NOTHING_FOUND": "No item found with these parameters",
   "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "search",
   "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "types",
diff --git a/src/langs/es.json b/src/langs/es.json
index f9211bf76..8b8ffbd4e 100644
--- a/src/langs/es.json
+++ b/src/langs/es.json
@@ -457,7 +457,6 @@
   "UPLOAD_BETWEEN_FILES": "Sube tu(s) archivo(s) aquí",
   "MOVE_WARNING": "Esta operación podría dar acceso a {{name}} a más personas que antes. Quieres proceder?",
   "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "No se pueden agregar elementos en {{type}}",
-  "MOVE_CONFIRM_TITLE": "Confirma mover <1>{{name}}</1> dentro de <1>{{targetName}}</1>",
   "ITEM_SEARCH_NOTHING_FOUND": "No se encontró ningún artículo con estos parámetros.",
   "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "buscar",
   "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "tipos",
diff --git a/src/langs/fr.json b/src/langs/fr.json
index aa28a7067..7fcaa5125 100644
--- a/src/langs/fr.json
+++ b/src/langs/fr.json
@@ -461,7 +461,8 @@
   "UPLOAD_BETWEEN_FILES": "Téléchargez votre (vos) fichier(s) ici",
   "MOVE_WARNING": "Cette opération pourrait donner accès à {{name}} à plus de personnes que précédemment. Voulez-vous poursuivre?",
   "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "Impossible d'ajouter des éléments dans {{type}}",
-  "MOVE_CONFIRM_TITLE": "Confirmez le déplacement de <1>{{name}}</1> à l'intérieur de <1>{{targetName}}</1>",
+  "MOVE_CONFIRM_TITLE_one": "Confirmez le déplacement d'un élément à l'intérieur de <1>{{targetName}}</1>",
+  "MOVE_CONFIRM_TITLE": "Confirmez le déplacement de <1>{{count}}</1> éléments à l'intérieur de <1>{{targetName}}</1>",
   "ITEM_SEARCH_NOTHING_FOUND": "Aucun élément trouvé avec ces paramètres",
   "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "recherche",
   "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "les types",
diff --git a/src/langs/it.json b/src/langs/it.json
index 31cbbf882..433cda578 100644
--- a/src/langs/it.json
+++ b/src/langs/it.json
@@ -457,7 +457,6 @@
   "UPLOAD_BETWEEN_FILES": "Carica i tuoi file qui",
   "MOVE_WARNING": "Questa operazione potrebbe consentire l'accesso a {{name}} a più persone rispetto a prima. Vuoi procedere?",
   "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "Impossibile aggiungere elementi in {{type}}",
-  "MOVE_CONFIRM_TITLE": "Conferma lo spostamento di <1>{{name}}</1> all'interno di <1>{{targetName}}</1>",
   "ITEM_SEARCH_NOTHING_FOUND": "Nessun elemento trovato con questi parametri",
   "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "ricerca",
   "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "tipi",
diff --git a/yarn.lock b/yarn.lock
index f361c2305..b9dfd5303 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -81,6 +81,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@air/react-drag-to-select@npm:5.0.8":
+  version: 5.0.8
+  resolution: "@air/react-drag-to-select@npm:5.0.8"
+  dependencies:
+    react-style-object-to-css: "npm:^1.1.2"
+  peerDependencies:
+    react: 16 - 18
+    react-dom: 16 - 18
+  checksum: 10/37ca191e981793e1357833238a48749426ca17782e99c7e5a64eed0a1a1473d2255cead716d6af8e9f605f633a2801a9e1bfe36583a3fc10e961c9a89f340772
+  languageName: node
+  linkType: hard
+
 "@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0":
   version: 2.3.0
   resolution: "@ampproject/remapping@npm:2.3.0"
@@ -1877,9 +1889,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@graasp/sdk@npm:4.19.0":
-  version: 4.19.0
-  resolution: "@graasp/sdk@npm:4.19.0"
+"@graasp/sdk@npm:4.20.0":
+  version: 4.20.0
+  resolution: "@graasp/sdk@npm:4.20.0"
   dependencies:
     "@faker-js/faker": "npm:8.4.1"
     filesize: "npm:10.1.4"
@@ -1888,7 +1900,7 @@ __metadata:
   peerDependencies:
     date-fns: ^3
     uuid: ^9 || ^10.0.0
-  checksum: 10/d2fb382f5669ab1b8e265612208d6766672be8b6fa833fe1af8e4f17d2c3f7f384e4bf3f5fd004ec76c8c1ab1ad382c37730477e90ec1ae85b3079ca6a8e68aa
+  checksum: 10/81e336d68094bcd234b4983217ce9f05d41501032fac00810bf927b8289ef933e18cdfb0e1b0275df6af5a7683b54a8ebf509134aa95b4c7ecfce4a5f313752d
   languageName: node
   linkType: hard
 
@@ -1966,9 +1978,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@graasp/ui@npm:4.21.0":
-  version: 4.21.0
-  resolution: "@graasp/ui@npm:4.21.0"
+"@graasp/ui@npm:4.22.0":
+  version: 4.22.0
+  resolution: "@graasp/ui@npm:4.22.0"
   dependencies:
     "@ag-grid-community/client-side-row-model": "npm:31.3.2"
     "@ag-grid-community/react": "npm:^31.3.2"
@@ -1978,14 +1990,14 @@ __metadata:
     interweave: "npm:13.1.0"
     katex: "npm:0.16.11"
     lodash.truncate: "npm:4.4.2"
-    lucide-react: "npm:0.402.0"
+    lucide-react: "npm:0.408.0"
     react-cookie-consent: "npm:9.0.0"
     react-dnd: "npm:16.0.1"
     react-dnd-html5-backend: "npm:16.0.1"
     react-quill: "npm:2.0.0"
     react-rnd: "npm:10.4.11"
     uuid: "npm:10.0.0"
-    vitest: "npm:1.6.0"
+    vitest: "npm:2.0.2"
   peerDependencies:
     "@emotion/cache": ~11.10.7 || ~11.11.0
     "@emotion/react": ~11.10.6 || ~11.11.0
@@ -2002,7 +2014,7 @@ __metadata:
     react-router-dom: ^6.11.0
     stylis: ^4.1.3
     stylis-plugin-rtl: ^2.1.1
-  checksum: 10/6a8778ae27ebb534204a44e1f97b08fdba6d7524fe3417fd7eaab8f4cf66e19800a38812f0b10c8796c885d6cabc5823bed137aebfbf36f29308ac7f04525314
+  checksum: 10/af8bcd06fd743d84739bf1c20aaceca67321c12456bcd905e7233d50c51c3fed25da41c835f8e9d1543a26a46f1da851c45222763dcbc7ad884cf0abb61b63d3
   languageName: node
   linkType: hard
 
@@ -2383,23 +2395,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@mui/private-theming@npm:^5.16.1":
-  version: 5.16.1
-  resolution: "@mui/private-theming@npm:5.16.1"
-  dependencies:
-    "@babel/runtime": "npm:^7.23.9"
-    "@mui/utils": "npm:^5.16.1"
-    prop-types: "npm:^15.8.1"
-  peerDependencies:
-    "@types/react": ^17.0.0 || ^18.0.0
-    react: ^17.0.0 || ^18.0.0
-  peerDependenciesMeta:
-    "@types/react":
-      optional: true
-  checksum: 10/fdfbc5e55bc1c980a00faab952e37280f437826788d8b1908d3eed75d053a4848b6927ea650a976b23419997113509e7ae0f21f325df6ba3a394ccdb32aa4f84
-  languageName: node
-  linkType: hard
-
 "@mui/private-theming@npm:^5.16.4":
   version: 5.16.4
   resolution: "@mui/private-theming@npm:5.16.4"
@@ -2438,27 +2433,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@mui/styled-engine@npm:^5.16.1":
-  version: 5.16.1
-  resolution: "@mui/styled-engine@npm:5.16.1"
-  dependencies:
-    "@babel/runtime": "npm:^7.23.9"
-    "@emotion/cache": "npm:^11.11.0"
-    csstype: "npm:^3.1.3"
-    prop-types: "npm:^15.8.1"
-  peerDependencies:
-    "@emotion/react": ^11.4.1
-    "@emotion/styled": ^11.3.0
-    react: ^17.0.0 || ^18.0.0
-  peerDependenciesMeta:
-    "@emotion/react":
-      optional: true
-    "@emotion/styled":
-      optional: true
-  checksum: 10/3f1d39b48a437179e96ffe82b51f19e45aeffa51dae2644ac218e4e1700945680caa680ff009658b41cfe2a05ae4e756c9b9f59a4df0ce6ada82e727597949f6
-  languageName: node
-  linkType: hard
-
 "@mui/styled-engine@npm:^5.16.4":
   version: 5.16.4
   resolution: "@mui/styled-engine@npm:5.16.4"
@@ -2536,35 +2510,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@mui/system@npm:^5.16.1":
-  version: 5.16.1
-  resolution: "@mui/system@npm:5.16.1"
-  dependencies:
-    "@babel/runtime": "npm:^7.23.9"
-    "@mui/private-theming": "npm:^5.16.1"
-    "@mui/styled-engine": "npm:^5.16.1"
-    "@mui/types": "npm:^7.2.15"
-    "@mui/utils": "npm:^5.16.1"
-    clsx: "npm:^2.1.0"
-    csstype: "npm:^3.1.3"
-    prop-types: "npm:^15.8.1"
-  peerDependencies:
-    "@emotion/react": ^11.5.0
-    "@emotion/styled": ^11.3.0
-    "@types/react": ^17.0.0 || ^18.0.0
-    react: ^17.0.0 || ^18.0.0
-  peerDependenciesMeta:
-    "@emotion/react":
-      optional: true
-    "@emotion/styled":
-      optional: true
-    "@types/react":
-      optional: true
-  checksum: 10/e49852b203b61bf4166ba4f1c1e56c846ea204219a6c9e725cbadcad264bff52cec5a10a0cd6516e1772958022b0d204bd8c7674e29d63ea212f5aef7e2814c5
-  languageName: node
-  linkType: hard
-
-"@mui/system@npm:^5.16.4":
+"@mui/system@npm:^5.16.1, @mui/system@npm:^5.16.4":
   version: 5.16.4
   resolution: "@mui/system@npm:5.16.4"
   dependencies:
@@ -2634,25 +2580,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@mui/utils@npm:^5.16.1":
-  version: 5.16.1
-  resolution: "@mui/utils@npm:5.16.1"
-  dependencies:
-    "@babel/runtime": "npm:^7.23.9"
-    "@types/prop-types": "npm:^15.7.12"
-    prop-types: "npm:^15.8.1"
-    react-is: "npm:^18.3.1"
-  peerDependencies:
-    "@types/react": ^17.0.0 || ^18.0.0
-    react: ^17.0.0 || ^18.0.0
-  peerDependenciesMeta:
-    "@types/react":
-      optional: true
-  checksum: 10/d3294dfc9953b8f1697c4837bf57a81a97b26fdfc6dd4d28747b3126a4ae8d9f4160e03326d42fbb2e1885ea4d9c56301516e13c8b50d81ea4db2455d9f18f3b
-  languageName: node
-  linkType: hard
-
-"@mui/utils@npm:^5.16.4":
+"@mui/utils@npm:^5.16.1, @mui/utils@npm:^5.16.4":
   version: 5.16.4
   resolution: "@mui/utils@npm:5.16.4"
   dependencies:
@@ -4413,6 +4341,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vitest/expect@npm:2.0.2":
+  version: 2.0.2
+  resolution: "@vitest/expect@npm:2.0.2"
+  dependencies:
+    "@vitest/spy": "npm:2.0.2"
+    "@vitest/utils": "npm:2.0.2"
+    chai: "npm:^5.1.1"
+    tinyrainbow: "npm:^1.2.0"
+  checksum: 10/67ebe5dcc083cbaf152fa1845da5ab4cd5a37fcc8657caaec214878c145516cf270998ad300ab9c3e7d8b4fc9ab41cbc4606af3341ae06d08c5cf44354ba5a56
+  languageName: node
+  linkType: hard
+
 "@vitest/expect@npm:2.0.4":
   version: 2.0.4
   resolution: "@vitest/expect@npm:2.0.4"
@@ -4425,7 +4365,16 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@vitest/pretty-format@npm:2.0.4, @vitest/pretty-format@npm:^2.0.4":
+"@vitest/pretty-format@npm:2.0.2":
+  version: 2.0.2
+  resolution: "@vitest/pretty-format@npm:2.0.2"
+  dependencies:
+    tinyrainbow: "npm:^1.2.0"
+  checksum: 10/30ae021ea3b36271e00aac5a49084de9403900ae574b1ce1c26385ee792a7fed700f2deb2cd841b64724a4e428e908a5d3ffc1b4e6ca83daa351d76de925e9a6
+  languageName: node
+  linkType: hard
+
+"@vitest/pretty-format@npm:2.0.4, @vitest/pretty-format@npm:^2.0.2, @vitest/pretty-format@npm:^2.0.4":
   version: 2.0.4
   resolution: "@vitest/pretty-format@npm:2.0.4"
   dependencies:
@@ -4445,6 +4394,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vitest/runner@npm:2.0.2":
+  version: 2.0.2
+  resolution: "@vitest/runner@npm:2.0.2"
+  dependencies:
+    "@vitest/utils": "npm:2.0.2"
+    pathe: "npm:^1.1.2"
+  checksum: 10/f3f9f15b5a3d0b5fe5815ed0ad04bd3fceab0768c441baf20931d78f2599261c172724955e9de35020ff79950e1fd5398d0d5aad2c5ee8a91e4cc2b85943ac81
+  languageName: node
+  linkType: hard
+
 "@vitest/runner@npm:2.0.4":
   version: 2.0.4
   resolution: "@vitest/runner@npm:2.0.4"
@@ -4466,6 +4425,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vitest/snapshot@npm:2.0.2":
+  version: 2.0.2
+  resolution: "@vitest/snapshot@npm:2.0.2"
+  dependencies:
+    "@vitest/pretty-format": "npm:2.0.2"
+    magic-string: "npm:^0.30.10"
+    pathe: "npm:^1.1.2"
+  checksum: 10/c0d41c3ff71ada909b34a8cbfe4ae9d59126fdae243b89e4eba5110db8eeb41234897159de20050a18aac2cbb7694e3fddd94bf7c79c1e9b169f1f4cf642bf07
+  languageName: node
+  linkType: hard
+
 "@vitest/snapshot@npm:2.0.4":
   version: 2.0.4
   resolution: "@vitest/snapshot@npm:2.0.4"
@@ -4486,6 +4456,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vitest/spy@npm:2.0.2":
+  version: 2.0.2
+  resolution: "@vitest/spy@npm:2.0.2"
+  dependencies:
+    tinyspy: "npm:^3.0.0"
+  checksum: 10/feca3d26b824350d2f4f11a1e5881f1c7eeba5b903399ee8fbc2aceb4bf4201da61088783cf56bd5a2850b3e2380905f69128106655d7d849c62c52861b5af1a
+  languageName: node
+  linkType: hard
+
 "@vitest/spy@npm:2.0.4":
   version: 2.0.4
   resolution: "@vitest/spy@npm:2.0.4"
@@ -4507,6 +4486,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@vitest/utils@npm:2.0.2":
+  version: 2.0.2
+  resolution: "@vitest/utils@npm:2.0.2"
+  dependencies:
+    "@vitest/pretty-format": "npm:2.0.2"
+    estree-walker: "npm:^3.0.3"
+    loupe: "npm:^3.1.1"
+    tinyrainbow: "npm:^1.2.0"
+  checksum: 10/771a1579c9d11bf02ed5d641619bdb9ee06f4096a2965183298c8610476316f899561dabf48e589eecccd76c75155131dc7a90d98d7519e07483b7ed09e0a5b9
+  languageName: node
+  linkType: hard
+
 "@vitest/utils@npm:2.0.4":
   version: 2.0.4
   resolution: "@vitest/utils@npm:2.0.4"
@@ -8134,6 +8125,7 @@ __metadata:
   dependencies:
     "@ag-grid-community/core": "npm:31.3.4"
     "@ag-grid-community/styles": "npm:31.3.4"
+    "@air/react-drag-to-select": "npm:5.0.8"
     "@commitlint/cli": "npm:19.3.0"
     "@commitlint/config-conventional": "npm:19.2.2"
     "@cypress/code-coverage": "npm:3.12.44"
@@ -8143,9 +8135,9 @@ __metadata:
     "@graasp/chatbox": "npm:3.1.0"
     "@graasp/map": "npm:1.16.0"
     "@graasp/query-client": "npm:3.16.0"
-    "@graasp/sdk": "npm:4.19.0"
+    "@graasp/sdk": "npm:4.20.0"
     "@graasp/translations": "npm:1.32.0"
-    "@graasp/ui": "npm:4.21.0"
+    "@graasp/ui": "npm:4.22.0"
     "@mui/icons-material": "npm:5.16.4"
     "@mui/lab": "npm:5.0.0-alpha.172"
     "@mui/material": "npm:5.16.4"
@@ -9904,12 +9896,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"lucide-react@npm:0.402.0":
-  version: 0.402.0
-  resolution: "lucide-react@npm:0.402.0"
+"lucide-react@npm:0.408.0":
+  version: 0.408.0
+  resolution: "lucide-react@npm:0.408.0"
   peerDependencies:
     react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
-  checksum: 10/fe8a7d5b65e469ee9f376e65942a1cb66aa39fc650265be4153e0f0235a0a5b09cdb03e8f51ed1f2319bdf91e1ae0859c61fe3a25c68e08ecb289a692bf52a17
+  checksum: 10/3849f76a3aec5a60008606d661ba8b540f2769b117a797c4c5300f303f18971a725b9de0410f278b89004bff99d1596e3669acb46c6452aa9bd7b029acd1c758
   languageName: node
   linkType: hard
 
@@ -12254,6 +12246,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-style-object-to-css@npm:^1.1.2":
+  version: 1.1.2
+  resolution: "react-style-object-to-css@npm:1.1.2"
+  checksum: 10/1f854bf5c7fabcc0be3db3e35e4b346dd0d3cb2b09079f100380ab299d2db94ba75ab225254446958d820933c7be67b609219746610a14cda2c42e708711cc78
+  languageName: node
+  linkType: hard
+
 "react-text-mask@npm:5.5.0":
   version: 5.5.0
   resolution: "react-text-mask@npm:5.5.0"
@@ -14208,6 +14207,21 @@ __metadata:
   languageName: node
   linkType: hard
 
+"vite-node@npm:2.0.2":
+  version: 2.0.2
+  resolution: "vite-node@npm:2.0.2"
+  dependencies:
+    cac: "npm:^6.7.14"
+    debug: "npm:^4.3.5"
+    pathe: "npm:^1.1.2"
+    tinyrainbow: "npm:^1.2.0"
+    vite: "npm:^5.0.0"
+  bin:
+    vite-node: vite-node.mjs
+  checksum: 10/9335168dc5a20c1d0c6b53cf20f098875c7556b0eb1e1ae871aedcc796edd5906f06ab259d9b57ec12719041838cac8186e54e597c0012ee77b03a4e2be84722
+  languageName: node
+  linkType: hard
+
 "vite-node@npm:2.0.4":
   version: 2.0.4
   resolution: "vite-node@npm:2.0.4"
@@ -14421,6 +14435,55 @@ __metadata:
   languageName: node
   linkType: hard
 
+"vitest@npm:2.0.2":
+  version: 2.0.2
+  resolution: "vitest@npm:2.0.2"
+  dependencies:
+    "@ampproject/remapping": "npm:^2.3.0"
+    "@vitest/expect": "npm:2.0.2"
+    "@vitest/pretty-format": "npm:^2.0.2"
+    "@vitest/runner": "npm:2.0.2"
+    "@vitest/snapshot": "npm:2.0.2"
+    "@vitest/spy": "npm:2.0.2"
+    "@vitest/utils": "npm:2.0.2"
+    chai: "npm:^5.1.1"
+    debug: "npm:^4.3.5"
+    execa: "npm:^8.0.1"
+    magic-string: "npm:^0.30.10"
+    pathe: "npm:^1.1.2"
+    std-env: "npm:^3.7.0"
+    tinybench: "npm:^2.8.0"
+    tinypool: "npm:^1.0.0"
+    tinyrainbow: "npm:^1.2.0"
+    vite: "npm:^5.0.0"
+    vite-node: "npm:2.0.2"
+    why-is-node-running: "npm:^2.2.2"
+  peerDependencies:
+    "@edge-runtime/vm": "*"
+    "@types/node": ^18.0.0 || >=20.0.0
+    "@vitest/browser": 2.0.2
+    "@vitest/ui": 2.0.2
+    happy-dom: "*"
+    jsdom: "*"
+  peerDependenciesMeta:
+    "@edge-runtime/vm":
+      optional: true
+    "@types/node":
+      optional: true
+    "@vitest/browser":
+      optional: true
+    "@vitest/ui":
+      optional: true
+    happy-dom:
+      optional: true
+    jsdom:
+      optional: true
+  bin:
+    vitest: vitest.mjs
+  checksum: 10/d92053b0d6e3e800d56cbe5eb860625fb9d50e66857da189ac19a68e511bbb0c59baf6a6b3a8ecb0b46c011567723e16e550136655e93767f228fb91caf4e16f
+  languageName: node
+  linkType: hard
+
 "vitest@npm:2.0.4":
   version: 2.0.4
   resolution: "vitest@npm:2.0.4"