diff --git a/src/gui/src/UI/Dashboard/TabFiles.js b/src/gui/src/UI/Dashboard/TabFiles.js
new file mode 100644
index 000000000..d61a801f0
--- /dev/null
+++ b/src/gui/src/UI/Dashboard/TabFiles.js
@@ -0,0 +1,3946 @@
+/**
+ * Copyright (C) 2024-present Puter Technologies Inc.
+ *
+ * This file is part of Puter.
+ *
+ * Puter is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+/* eslint-disable no-invalid-this */
+/* eslint-disable @stylistic/quotes */
+import path from '../../lib/path.js';
+import open_item from '../../helpers/open_item.js';
+import UIContextMenu from '../UIContextMenu.js';
+import UIWindowProgress from '../UIWindowProgress.js';
+import UIAlert from '../UIAlert.js';
+import generate_file_context_menu from '../../helpers/generate_file_context_menu.js';
+import truncate_filename from '../../helpers/truncate_filename.js';
+import update_title_based_on_uploads from '../../helpers/update_title_based_on_uploads.js';
+import item_icon from '../../helpers/item_icon.js';
+import new_context_menu_item from '../../helpers/new_context_menu_item.js';
+import ContextMenuModal from './ContextMenu/ContextMenu.js';
+
+const icons = {
+ document: ``,
+ files: ``,
+ folder: ``,
+ more: ``,
+ newFolder: ``,
+ upload: ``,
+ trash: ``,
+ download: ``,
+ cut: ``,
+ copy: ``,
+ restore: ``,
+ list: ``,
+ grid: ``,
+ sort: ``,
+ select: ``,
+ done: ``,
+ worker: ``,
+};
+
+const { html_encode, SelectionArea } = window;
+
+/**
+ * TabFiles - File browser tab component for the Puter Dashboard.
+ *
+ * Provides a full-featured file management interface including:
+ * - Directory navigation with breadcrumb path
+ * - List and grid view modes
+ * - File sorting by name, size, or modification date
+ * - Drag-and-drop file operations (move, copy, shortcut)
+ * - Context menus for file/folder operations
+ * - File upload with progress tracking
+ * - Trash folder support with restore/permanent delete
+ *
+ * @module TabFiles
+ */
+const TabFiles = {
+ id: 'files',
+ label: 'Files',
+ icon: icons.files,
+
+ /**
+ * Generates the HTML template for the files tab.
+ *
+ * @returns {string} HTML string containing the file browser structure
+ */
+ html () {
+ let h = `
+
+
+
+
+
Home
+
Desktop
+
Documents
+
Pictures
+
Public
+
Videos
+
Trash
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
${i18n('name')}
+
+
${i18n('size')}
+
+
${i18n('modified')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ return h;
+ },
+
+ /**
+ * Initializes the files tab with event listeners and state.
+ *
+ * Sets up folder click handlers, drag-and-drop zones, context menus,
+ * and restores persisted preferences (view mode, sort settings, column widths).
+ *
+ * @param {jQuery} $el_window - The jQuery-wrapped window/container element
+ * @returns {Promise}
+ */
+ async init ($el_window) {
+ this.showSpinner();
+ const _this = this;
+ window.dashboard_object = _this;
+
+ // Dashboard-compatible item creator for use by helpers.js and socket handlers.
+ // Wraps renderItem() with a directory check so items are only added
+ // when the user is viewing the relevant directory.
+ window.UIDashboardFileItem = async function (file) {
+ if ( ! _this.currentPath ) return;
+ if ( _this.renderingDirectory ) return;
+ if ( _this._creatingItem ) return;
+
+ const parentDir = path.dirname(file.path);
+ if ( _this.currentPath !== parentDir ) return;
+
+ // If item already exists in view, update in-place.
+ const $existingRow = $(`.files-tab .files .item[data-uid='${file.uid}']`);
+ if ( $existingRow.length > 0 ) {
+ // Match renderItem's display name: trashed items show
+ // metadata.original_name, not the UID that is their raw name.
+ let displayName = file.name || '';
+ try {
+ const meta = file.metadata ? JSON.parse(file.metadata) : null;
+ if ( meta && meta.original_name ) displayName = meta.original_name;
+ } catch { /* keep raw name */ }
+ $existingRow.attr('data-name', displayName);
+ $existingRow.attr('data-path', file.path || '');
+ $existingRow.attr('data-size', file.size || 0);
+ $existingRow.attr('data-modified', file.modified || 0);
+ $existingRow.attr('data-type', file.type || '');
+ $existingRow.find('.item-name').text(displayName);
+ $existingRow.find('.item-name-editor').val(displayName);
+ if (
+ _this.currentView === 'grid' &&
+ typeof file.thumbnail === 'string' &&
+ file.thumbnail.length > 0
+ ) {
+ $existingRow.find('.item-icon img').attr('src', file.thumbnail);
+ }
+ return;
+ }
+
+ await _this.renderItem(file);
+
+ // Get the newly appended row (it's always last after renderItem)
+ const $newRow = _this.$el_window.find(`.files-tab .files .item[data-uid='${file.uid}']`);
+ if ( $newRow.length === 0 ) return;
+
+ // Insert at correct sorted position
+ _this.insertAtSortedPosition($newRow, file);
+
+ // Apply column widths to match existing rows
+ _this.applyColumnWidths();
+
+ // Highlight animation to indicate newly added item
+ $newRow.addClass('item-newly-added');
+ };
+
+ this.renderingDirectory = false;
+ this._creatingItem = false;
+ this.activeMenuFileUid = null;
+ this.currentPath = null;
+ this.currentPath = null;
+ this.folderDwellTimer = null;
+ this.folderDwellTarget = null;
+ this.springLoadedActive = false;
+ this.springLoadedOriginalPath = null;
+ this.previewOpen = false;
+ this.previewCurrentUid = null;
+ this.typeSearchTerm = '';
+ this.typeSearchTimeout = null;
+ this.selectModeActive = false;
+ this.currentView = await puter.kv.get('view_mode') || 'list';
+
+ // Sorting state
+ this.sortColumn = await puter.kv.get('sort_column') || 'name';
+ this.sortDirection = await puter.kv.get('sort_direction') || 'asc';
+
+ // Column widths state (for resizing)
+ const savedWidths = await puter.kv.get('column_widths');
+ this.columnWidths = savedWidths ? JSON.parse(savedWidths) : {
+ name: null, // auto/flex
+ size: 100,
+ modified: 120,
+ };
+
+ // Add touch-device class for touch devices to show .item-more button
+ // Use multiple detection methods since user-agent sniffing can miss devices
+ if ( window.isMobile.phone || window.isMobile.tablet || navigator.maxTouchPoints > 0 ) {
+ $el_window.find('.files-tab').addClass('touch-device');
+ }
+
+ // Create click handler for each folder item
+ $el_window.find('[data-folder]').each(function () {
+ const folderElement = this;
+
+ folderElement.onclick = async () => {
+ const folderPath = folderElement.getAttribute('data-path');
+ _this.pushNavHistory(folderPath);
+ _this.renderDirectory(folderPath);
+ };
+
+ // Context menu for sidebar folders
+ $(folderElement).on('contextmenu taphold', async (e) => {
+ if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet ) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ $(folderElement).addClass('context-menu-active');
+ const folderPath = folderElement.getAttribute('data-path');
+ const items = _this.generateFolderContextMenu(folderPath);
+
+ if ( window.isMobile.phone || window.isMobile.tablet ) {
+ const modal = new ContextMenuModal({
+ onClose: () => $(folderElement).removeClass('context-menu-active'),
+ });
+ modal.show(items, folderElement.getBoundingClientRect());
+ } else {
+ const menu = UIContextMenu({ items: items, position: { left: e.pageX, top: e.pageY } });
+ menu.onClose = () => {
+ $(folderElement).removeClass('context-menu-active');
+ };
+ }
+ });
+
+ // Make sidebar folders droppable
+ $(folderElement).droppable({
+ accept: '.row',
+ tolerance: 'pointer',
+
+ drop: async function (event, ui) {
+ // Clear dwell timer to prevent folder from opening after drop
+ clearTimeout(_this.folderDwellTimer);
+ _this.folderDwellTimer = null;
+ _this.folderDwellTarget = null;
+
+ // Block if ctrl and trashed
+ const draggedPath = $(ui.draggable).attr('data-path');
+ if ( event.ctrlKey && draggedPath?.startsWith(`${window.trash_path}/`) ) {
+ return;
+ }
+
+ ui.helper.data('dropped', true);
+
+ // Get target folder path from the element itself. Using the
+ // element's own data-path covers folders (Public, Home) that
+ // aren't keyed in window.user.directories.
+ const targetPath = folderElement.getAttribute('data-path');
+
+ if ( ! targetPath ) return;
+
+ // Collect all items to move
+ const itemsToMove = [ui.draggable[0]];
+
+ // Add other selected items
+ $('.item-selected-clone').each(function () {
+ const sourceId = $(this).find('.row').attr('data-id');
+ const sourceItem = document.querySelector(`.row[data-id="${sourceId}"]`);
+ if ( sourceItem ) itemsToMove.push(sourceItem);
+ });
+
+ // Perform operation based on modifier keys
+ if ( event.ctrlKey ) {
+ // Copy
+ await window.copy_items(itemsToMove, targetPath);
+ }
+ else if ( event.altKey && window.feature_flags?.create_shortcut ) {
+ // Create shortcuts
+ for ( const item of itemsToMove ) {
+ const itemPath = $(item).attr('data-path');
+ const itemName = itemPath.split('/').pop();
+ const isDir = $(item).attr('data-is_dir') === '1';
+ const shortcutTo = $(item).attr('data-shortcut_to') || $(item).attr('data-uid');
+ const shortcutToPath = $(item).attr('data-shortcut_to_path') || itemPath;
+
+ await window.create_shortcut(itemName, isDir, targetPath, null, shortcutTo, shortcutToPath);
+ }
+ }
+ else {
+ // Move
+ await window.move_items(itemsToMove, targetPath);
+ }
+ },
+
+ over: function (_event, ui) {
+ if ( $(ui.draggable).hasClass('row') ) {
+ $(folderElement).addClass('active');
+
+ const folderPath = folderElement.getAttribute('data-path');
+
+ // Don't auto-open the current directory or trash
+ if ( folderPath === _this.currentPath ||
+ folderPath === window.trash_path ) {
+ return;
+ }
+
+ // Clear any existing dwell timer
+ clearTimeout(_this.folderDwellTimer);
+
+ // Add visual feedback animation
+ $(folderElement).addClass('dwell-opening');
+ _this.folderDwellTarget = folderElement;
+
+ // Start dwell timer — navigate into folder after 700ms
+ _this.folderDwellTimer = setTimeout(async () => {
+ _this.folderDwellTimer = null;
+ _this.folderDwellTarget = null;
+ if ( ! _this.springLoadedActive ) {
+ _this.springLoadedOriginalPath = _this.currentPath;
+ }
+ _this.springLoadedActive = true;
+ $('.drag-cancel-zone').show();
+ $(folderElement).removeClass('dwell-opening active');
+
+ _this.pushNavHistory(folderPath);
+ await _this.renderDirectory(folderPath);
+
+ // Refresh jQuery UI droppable detection for the active drag
+ if ( $.ui.ddmanager && $.ui.ddmanager.current ) {
+ $.ui.ddmanager.current.helper.addClass('ui-draggable-dragging');
+ $.ui.ddmanager.prepareOffsets($.ui.ddmanager.current);
+ }
+ }, 700);
+ }
+ },
+
+ out: function (_event, ui) {
+ if ( $(ui.draggable).hasClass('row') ) {
+ // Clear dwell timer
+ if ( _this.folderDwellTarget === folderElement ) {
+ clearTimeout(_this.folderDwellTimer);
+ _this.folderDwellTimer = null;
+ _this.folderDwellTarget = null;
+ }
+ $(folderElement).removeClass('dwell-opening');
+
+ // Only remove active if it's not the currently selected folder
+ const folderPath = folderElement.getAttribute('data-path');
+
+ if ( folderPath !== _this.currentPath ) {
+ $(folderElement).removeClass('active');
+ }
+ }
+ },
+ });
+
+ // Add native file drop support to sidebar folders
+ $(folderElement).dragster({
+ enter: function (_dragsterEvent, event) {
+ const e = event.originalEvent;
+ if ( ! e.dataTransfer?.types?.includes('Files') ) {
+ return;
+ }
+
+ const folderPath = folderElement.getAttribute('data-path');
+
+ // Don't allow drop on trash
+ if ( folderPath === window.trash_path ) {
+ return;
+ }
+
+ $(folderElement).addClass('native-drop-target');
+ },
+
+ leave: function (_dragsterEvent, _event) {
+ $(folderElement).removeClass('native-drop-target');
+ },
+
+ drop: async function (_dragsterEvent, event) {
+ const e = event.originalEvent;
+ $(folderElement).removeClass('native-drop-target');
+
+ if ( ! e.dataTransfer?.types?.includes('Files') ) {
+ return;
+ }
+
+ const folderPath = folderElement.getAttribute('data-path');
+
+ // Block uploads to trash
+ if ( folderPath === window.trash_path ) {
+ return;
+ }
+
+ if ( e.dataTransfer?.items?.length > 0 ) {
+ _this.uploadFiles(e.dataTransfer.items, folderPath);
+ }
+
+ e.stopPropagation();
+ e.preventDefault();
+ return false;
+ },
+ });
+ });
+
+ // Clear selection when clicking empty area (but not after rubber band selection)
+ $el_window.find('.dashboard-tab-content').on('click', (e) => {
+ // Skip if this click is the end of a rubber band selection
+ if ( _this.rubberBandSelectionJustEnded ) {
+ _this.rubberBandSelectionJustEnded = false;
+ return;
+ }
+ if ( e.target === e.currentTarget || e.target.classList.contains('files') ) {
+ document.querySelectorAll('.files-tab .row.selected').forEach(r => {
+ r.classList.remove('selected');
+ });
+ _this.updateFooterStats();
+ }
+ });
+
+ // Right-click on background shows folder context menu
+ $el_window.find('.files').on('contextmenu taphold', async (e) => {
+ // Dismiss taphold on non-touch devices
+ if ( e.type === 'taphold' && !window.isMobile.phone && !window.isMobile.tablet ) {
+ return;
+ }
+ // Only trigger if clicking directly on .files container (not on a row)
+ if ( e.target.classList.contains('files') ||
+ e.target.classList.contains('files-list-view') ||
+ e.target.classList.contains('files-grid-view') ) {
+ e.preventDefault();
+ e.stopPropagation();
+ // Clear selection when right-clicking background
+ document.querySelectorAll('.files-tab .row.selected').forEach(r => {
+ r.classList.remove('selected');
+ });
+ _this.updateFooterStats();
+ const items = await _this.generateFolderContextMenu();
+ if ( window.isMobile.phone || window.isMobile.tablet ) {
+ const modal = new ContextMenuModal();
+ modal.show(items, e.target.getBoundingClientRect());
+ } else {
+ UIContextMenu({ items: items, position: { left: e.pageX, top: e.pageY } });
+ }
+ }
+ });
+
+ // Store reference to $el_window for later use (must be before createHeaderEventListeners)
+ this.$el_window = $el_window;
+
+ this.createHeaderEventListeners($el_window);
+ this.createSelectionActionListeners($el_window);
+ this.initRubberBandSelection();
+ this.initNativeFileDrop();
+
+ // Apply initial view mode from persisted preferences
+
+ const $filesContainer = this.$el_window.find('.files-tab .files');
+ const $tabContent = this.$el_window.find('.files-tab');
+ if ( this.currentView === 'grid' ) {
+ $filesContainer.addClass('files-grid-view');
+ $tabContent.addClass('files-grid-mode');
+ this.$el_window.find('.view-toggle-btn').html(icons.list);
+ } else {
+ $filesContainer.addClass('files-list-view');
+ this.$el_window.find('.view-toggle-btn').html(icons.grid);
+ }
+
+ // Check for initial file path from URL routing
+ if ( window.dashboard_initial_file_path ) {
+ const initialPath = window.dashboard_initial_file_path;
+ delete window.dashboard_initial_file_path; // Clear so it only runs once
+ this.pushNavHistory(initialPath);
+ this.renderDirectory(initialPath, { skipUrlUpdate: true });
+ } else {
+ // Auto-select Documents folder on initialization
+ const documentsFolder = $el_window.find('[data-folder="Documents"]');
+ if ( documentsFolder.length ) {
+ documentsFolder.trigger('click');
+ }
+ }
+
+ // Setup keyboard shortcuts
+ this.setupKeyboardShortcuts();
+
+ // Refresh current directory when the user returns to this browser tab
+ document.addEventListener('visibilitychange', () => {
+ if ( document.visibilityState === 'visible' && this.currentPath ) {
+ this.renderDirectory(this.currentPath, { skipNavHistory: true, skipUrlUpdate: true });
+ }
+ });
+ },
+
+ /**
+ * Called when the Files tab becomes active.
+ * Updates the URL hash to reflect the current file path.
+ *
+ * @param {jQuery} _$el_window - The jQuery-wrapped window/container element (unused)
+ * @returns {void}
+ */
+ onActivate (_$el_window) {
+ // Update URL to show current path when Files tab becomes active
+ if ( this.currentPath && window.is_dashboard_mode ) {
+ this.updateDashboardUrl(this.currentPath);
+ }
+ },
+
+ /**
+ * Checks if the Dashboard Files tab is currently active and visible.
+ *
+ * @returns {boolean} True if Dashboard is visible and Files tab is active
+ */
+ isDashboardFilesActive () {
+ if ( !this.$el_window || !this.$el_window.is(':visible') ) return false;
+ const filesSection = this.$el_window.find('.dashboard-section-files');
+ return filesSection.hasClass('active');
+ },
+
+ /**
+ * Sets up Dashboard-specific keyboard shortcuts.
+ *
+ * Handles arrow navigation, selection, copy/cut/paste, delete, rename, etc.
+ */
+ setupKeyboardShortcuts () {
+ const _this = this;
+
+ $(document).on('keydown.tabfiles', async function (e) {
+ // Only handle if Dashboard Files tab is active
+ if ( ! _this.isDashboardFilesActive() ) return;
+
+ const focused_el = document.activeElement;
+
+ // Skip if user is typing in an input/textarea (except for Escape)
+ if ( $(focused_el).is('input, textarea') && e.which !== 27 ) return;
+
+ // When a context menu is open, yield control to keyboard.js
+ if ( $('.context-menu').length > 0 ) {
+ if ( (e.which >= 37 && e.which <= 40) || e.which === 13 || e.which === 27 ) {
+ return;
+ }
+ if ( !e.ctrlKey && !e.metaKey && e.key.length === 1 ) {
+ return;
+ }
+ }
+
+ const $container = _this.$el_window.find('.files-tab .files');
+ const $allRows = $container.find('.row');
+ const $selectedRows = $container.find('.row.selected');
+
+ // F2 - Rename selected item
+ if ( e.which === 113 ) {
+ const $selectedRow = $selectedRows.first();
+ if ( $selectedRow.length > 0 ) {
+ e.preventDefault();
+ e.stopPropagation();
+ const $nameEditor = $selectedRow.find('.item-name-editor');
+ const $itemName = $selectedRow.find('.item-name');
+ if ( $nameEditor.length > 0 ) {
+ $itemName.hide();
+ $nameEditor.show().addClass('item-name-editor-active').focus().select();
+ }
+ }
+ return false;
+ }
+
+ // Enter - Open selected items
+ if ( e.which === 13 && !$(focused_el).hasClass('item-name-editor') ) {
+ if ( $selectedRows.length > 0 ) {
+ e.preventDefault();
+ e.stopPropagation();
+ $selectedRows.each(function () {
+ const isDir = $(this).attr('data-is_dir') === '1';
+ const itemPath = $(this).attr('data-path');
+ if ( isDir ) {
+ _this.pushNavHistory(itemPath);
+ _this.renderDirectory(itemPath);
+ } else {
+ open_item({ item: this });
+ }
+ });
+ }
+ return false;
+ }
+
+ // Escape - Cancel drag, clear selection, or cancel rename
+ if ( e.which === 27 ) {
+ // Cancel active drag operation
+ if ( window.an_item_is_being_dragged ) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if ( _this.springLoadedActive ) {
+ _this.navigateBackFromSpringLoad();
+ }
+ _this.springLoadedActive = false;
+ _this.springLoadedOriginalPath = null;
+
+ // Force jQuery UI to end the drag
+ $(document).trigger('mouseup');
+
+ // Cleanup
+ $('.drag-cancel-zone').remove();
+ $('.item-selected-clone').remove();
+ $('.draggable-count-badge').remove();
+ window.an_item_is_being_dragged = false;
+ $('.window-app-iframe').css('pointer-events', 'auto');
+ return false;
+ }
+
+ if ( $(focused_el).hasClass('item-name-editor') ) {
+ // Cancel rename - handled by item's own keyup handler
+ return;
+ }
+ $selectedRows.removeClass('selected');
+ _this.updateFooterStats();
+ return false;
+ }
+
+ // Delete - Move to trash or permanently delete
+ if ( e.keyCode === 46 || (e.keyCode === 8 && (e.ctrlKey || e.metaKey)) ) {
+ if ( $selectedRows.length > 0 ) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Check if any items are in trash (for permanent delete)
+ const trashedItems = $selectedRows.filter(function () {
+ return $(this).attr('data-path')?.startsWith(`${window.trash_path}/`);
+ });
+
+ if ( trashedItems.length > 0 ) {
+ // Permanent delete with confirmation
+ const alert_resp = await UIAlert({
+ message: i18n('confirm_delete_multiple_items'),
+ buttons: [
+ { label: i18n('delete'), value: 'delete', type: 'primary' },
+ { label: i18n('cancel'), value: 'cancel' },
+ ],
+ });
+ if ( alert_resp === 'delete' ) {
+ for ( const row of trashedItems.toArray() ) {
+ await window.delete_item(row);
+ }
+ }
+ } else {
+ // Move to trash
+ await window.move_items($selectedRows.toArray(), window.trash_path);
+ }
+ }
+ return false;
+ }
+
+ // Ctrl/Cmd + A - Select all
+ if ( (e.ctrlKey || e.metaKey) && e.which === 65 ) {
+ e.preventDefault();
+ e.stopPropagation();
+ $allRows.addClass('selected');
+ if ( $allRows.length > 0 ) {
+ window.active_element = $allRows.last().get(0);
+ window.latest_selected_item = $allRows.last().get(0);
+ }
+ _this.updateFooterStats();
+ return false;
+ }
+
+ // Ctrl/Cmd + C - Copy
+ if ( (e.ctrlKey || e.metaKey) && e.which === 67 ) {
+ if ( $selectedRows.length > 0 ) {
+ e.preventDefault();
+ e.stopPropagation();
+ window.clipboard = [];
+ window.clipboard_op = 'copy';
+ $selectedRows.each(function () {
+ if ( $(this).attr('data-path') !== window.trash_path ) {
+ window.clipboard.push({
+ path: $(this).attr('data-path'),
+ uid: $(this).attr('data-uid'),
+ metadata: $(this).attr('data-metadata'),
+ });
+ }
+ });
+ }
+ return false;
+ }
+
+ // Ctrl/Cmd + X - Cut
+ if ( (e.ctrlKey || e.metaKey) && e.which === 88 ) {
+ if ( $selectedRows.length > 0 ) {
+ e.preventDefault();
+ e.stopPropagation();
+ window.clipboard = [];
+ window.clipboard_op = 'move';
+ $selectedRows.each(function () {
+ window.clipboard.push({
+ path: $(this).attr('data-path'),
+ uid: $(this).attr('data-uid'),
+ });
+ });
+ }
+ return false;
+ }
+
+ // Ctrl/Cmd + V - Paste
+ if ( (e.ctrlKey || e.metaKey) && e.which === 86 ) {
+ if ( window.clipboard.length > 0 && _this.currentPath ) {
+ e.preventDefault();
+ e.stopPropagation();
+ // Don't allow paste in Trash unless it's a move operation
+ if ( _this.currentPath.startsWith(window.trash_path) && window.clipboard_op !== 'move' ) {
+ return false;
+ }
+ if ( window.clipboard_op === 'copy' ) {
+ window.copy_clipboard_items(_this.currentPath, null);
+ } else {
+ _this.moveClipboardItems(_this.currentPath).then(() => {
+ _this.renderDirectory(_this.currentPath);
+ });
+ }
+ }
+ return false;
+ }
+
+ // Arrow keys - Navigate items
+ if ( e.which >= 37 && e.which <= 40 ) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if ( $allRows.length === 0 ) return false;
+
+ // If nothing selected, select first item
+ if ( $selectedRows.length === 0 ) {
+ const $first = $allRows.first();
+ $first.addClass('selected');
+ window.active_element = $first.get(0);
+ window.latest_selected_item = $first.get(0);
+ $first.get(0).scrollIntoView({ block: 'nearest' });
+ _this.updateFooterStats();
+ return false;
+ }
+
+ // Find current item and calculate next
+ const $current = $(window.latest_selected_item || $selectedRows.last().get(0));
+ const currentIndex = $allRows.index($current);
+ let nextIndex = currentIndex;
+
+ // Calculate grid dimensions for grid view
+ const isGridView = $container.hasClass('files-grid-view');
+ let cols = 1;
+ if ( isGridView && $allRows.length > 1 ) {
+ const firstTop = $allRows.eq(0).offset().top;
+ for ( let i = 1; i < $allRows.length; i++ ) {
+ if ( $allRows.eq(i).offset().top !== firstTop ) {
+ cols = i;
+ break;
+ }
+ }
+ if ( cols === 1 ) cols = $allRows.length; // All on one row
+ }
+
+ // Calculate next index based on arrow key
+ switch ( e.which ) {
+ case 37: // Left
+ nextIndex = Math.max(0, currentIndex - 1);
+ break;
+ case 38: // Up
+ nextIndex = Math.max(0, currentIndex - cols);
+ break;
+ case 39: // Right
+ nextIndex = Math.min($allRows.length - 1, currentIndex + 1);
+ break;
+ case 40: // Down
+ nextIndex = Math.min($allRows.length - 1, currentIndex + cols);
+ break;
+ }
+
+ if ( nextIndex !== currentIndex ) {
+ const $next = $allRows.eq(nextIndex);
+
+ if ( ! e.shiftKey ) {
+ // Normal navigation - clear selection
+ $allRows.removeClass('selected');
+ }
+
+ $next.addClass('selected');
+ window.active_element = $next.get(0);
+ window.latest_selected_item = $next.get(0);
+ $next.get(0).scrollIntoView({ block: 'nearest' });
+ _this.updateFooterStats();
+
+ // If preview is open, switch to newly selected file
+ if ( _this.previewOpen && !e.shiftKey ) {
+ const newUid = $next.attr('data-uid');
+ if ( newUid !== _this.previewCurrentUid ) {
+ _this.showImagePreview($next);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ // Space - Toggle image preview
+ if ( e.which === 32 ) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // If preview is open, close it
+ if ( _this.previewOpen ) {
+ _this.closeImagePreview();
+ return false;
+ }
+
+ // Open preview for single selected image file
+ if ( $selectedRows.length === 1 ) {
+ const $row = $selectedRows.first();
+ const isDir = $row.attr('data-is_dir') === '1';
+ if ( ! isDir ) {
+ _this.showImagePreview($row);
+ }
+ }
+ return false;
+ }
+
+ // Type-to-select: letter/number keys search items by name
+ if ( !e.ctrlKey && !e.metaKey && e.key.length === 1 ) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+
+ if ( _this.typeSearchTerm !== '' ) {
+ clearTimeout(_this.typeSearchTimeout);
+ }
+
+ _this.typeSearchTimeout = setTimeout(() => {
+ _this.typeSearchTerm = '';
+ }, 700);
+
+ _this.typeSearchTerm += e.key.toLocaleLowerCase();
+
+ let matches = [];
+ const $currentSelected = $selectedRows.first();
+
+ // If selected item already matches, keep it
+ if ( $currentSelected.length === 1 ) {
+ const selectedName = ($currentSelected.attr('data-name') || '').toLowerCase();
+ if ( selectedName.startsWith(_this.typeSearchTerm) ) {
+ return false;
+ }
+ }
+
+ // Search all rows for matches
+ for ( let j = 0; j < $allRows.length; j++ ) {
+ const name = ($allRows.eq(j).attr('data-name') || '').toLowerCase();
+ if ( name.startsWith(_this.typeSearchTerm) ) {
+ matches.push($allRows.get(j));
+ }
+ }
+
+ if ( matches.length > 0 ) {
+ // If multiple matches and one is selected, cycle past it
+ if ( $currentSelected.length > 0 && matches.length > 1 ) {
+ let match_index;
+ for ( let i = 0; i < matches.length - 1; i++ ) {
+ if ( $(matches[i]).is($currentSelected) ) {
+ match_index = i;
+ break;
+ }
+ }
+ if ( match_index !== undefined ) {
+ matches.splice(0, match_index + 1);
+ }
+ }
+
+ // Deselect all, select the match
+ $allRows.removeClass('selected');
+ $(matches[0]).addClass('selected');
+ window.active_element = matches[0];
+ window.latest_selected_item = matches[0];
+ matches[0].scrollIntoView({ block: 'nearest' });
+ _this.updateFooterStats();
+ }
+
+ return false;
+ }
+ });
+ },
+
+ /**
+ * Shows an image preview popover for the selected file.
+ *
+ * Fetches a signed URL for the actual image and displays it in a centered
+ * popover. The popover can be dismissed by pressing spacebar or clicking outside.
+ *
+ * @param {jQuery} $row - The selected row element
+ * @returns {Promise}
+ */
+ async showImagePreview ($row) {
+ const uid = $row.attr('data-uid');
+ const fileName = $row.attr('data-name');
+ const filePath = $row.attr('data-path');
+
+ // Check if it's an image file
+ const extension = fileName.split('.').pop().toLowerCase();
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
+ if ( ! imageExtensions.includes(extension) ) {
+ return;
+ }
+
+ // Get read URL for the actual image
+ const imageUrl = await puter.fs.getReadURL(filePath);
+
+ // Remove any existing preview
+ $('.image-preview-popover').remove();
+
+ const $filesContainer = this.$el_window.find('.files-tab .files');
+ const containerWidth = $filesContainer.width();
+ const containerOffset = $filesContainer.offset();
+
+ const previewHtml = `
+