From d38d825e3484d822ea3c1f0fb1df37ecf386b18a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 04 Jan 2026 19:54:16 +0000
Subject: [PATCH] =TaxonomyCreator.js debugging

---
 assets/js/concise/UploadManagerOld.js | 5897 ++++++++++++++++++++++++----------------------------------
 1 files changed, 2,462 insertions(+), 3,435 deletions(-)

diff --git a/assets/js/concise/UploadManagerOld.js b/assets/js/concise/UploadManagerOld.js
index 7e34eab..3f1ae58 100644
--- a/assets/js/concise/UploadManagerOld.js
+++ b/assets/js/concise/UploadManagerOld.js
@@ -1,101 +1,101 @@
+/**
+ * UploadManager - Refactored for clarity
+ *
+ * Architecture:
+ * - DataStores (fieldStore, uploadStore) = Recovery cache only, cleared after successful upload
+ * - Maps (uploadElements, fieldElements) = Runtime DOM references
+ * - Upload data flows: File → Process → Queue → Server → Clean up stores
+ */
 class UploadManager {
 	constructor() {
-		//Load dependencies
+		// Load dependencies
 		this.queue = window.jvbQueue;
 		this.a11y = window.jvbA11y;
 		this.error = window.jvbError;
-		this.notifications = window.jvbNotifications;
+		this.fieldStoreReady = false;
+		this.uploadStoreReady = false;
+		this.hasCheckedForUploads = false;
+		const {fields, uploads} = window.jvbStore.register(
+			'uploads',
+			[
+				{
+					storeName: 'fields',
+					keyPath: 'id',
+					indexes: [
+						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'timestamp', keyPath: 'timestamp' },
+						{ name: 'content', keyPath: 'content' },
+						{ name: 'itemId', keyPath: 'itemId' },
+						{ name: 'status', keyPath: 'status' }
+					],
+					TTL: 7 * 24 * 60 * 60 * 1000, // 1 week
+					delayFetch: true
+				},
+				{
+					storeName: 'uploads',
+					keyPath: 'id',
+					storeBlobs: true,
+					indexes: [
+						{ name: 'fieldId', keyPath: 'fieldId' },
+						{ name: 'status', keyPath: 'status' },
+						{ name: 'groupId', keyPath: 'groupId' },
+						{ name: 'attachmentId', keyPath: 'attachmentId' }
+					],
+					delayFetch: true
+				}
+			]
+		);
+		this.fieldStore = fields;
+		this.uploadStore = uploads;
 
-		//Load Datastore
-		this.initDB();
+		window.jvbUploadBlobs = this.uploadStore;
 
-		//State management
-		this.fields = new Map();
-		this.uploads = new Map();
-		this.uploadBlobs = new Map();
-		this.timeouts = new Map();
+		// Subscribe to store events
+		this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this));
+		this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this));
+
+		// RUNTIME DATA - DOM references and ephemeral state
+		this.uploadElements = new Map();  // uploadId → { element, preview, location }
+		this.fieldElements = new Map();   // fieldId → { element, ui, config }
+		this.groupElements = new Map();   // groupId → { element, grid, fieldId }
+
+		// Selection and UI state
 		this.selected = new Map();
-		this.dragState = {
-			isDragging: false,
-			primaryItem: null,
-			draggedItems: [],
-			isMultiDrag: false,
-			fieldId: null,
-			sourceType: null,
-			startTime: null,
-			startPosition: { x: 0, y: 0 },
-			currentPosition: { x: 0, y: 0 },
-			currentTarget: null,
-			validTarget: null,
-			dragPreview: null,
-			touchId: null,
-			touchMoved: false
-		};
-		this.hasGroups = false;
-
 		this.selectionHandlers = new Map();
+		this.previewUrls = new Set();
+		this.sortableInstances = new Map();
 
-		//Worker
-		this.worker = {
-			worker: null,
-			timeout: null,
-			tasks: new Map(),
-			restart: {
-				count: 0,
-				max: 3,
-			},
-			settings: {
-				timeout: 10000, //10 seconds per image
-				batchSize: 1,
-				maxConcurrent: 3,
-				restartAfterTimeout: true
-			}
-		};
+		// Worker for image processing
+		this.initWorker();
 
-		//Groups!
-		this.touch = {
-			x: null,
-			y: null
-		}
-		this.hasBulkContext = document.querySelector('details.uploader')!==null;
-		this.isTouching = false;
-		this.groups = new Map();
-		this.groupsMeta = new Map();
-
-		//Notification and Subscribers
+		// Notification subscribers
 		this.subscribers = new Set();
 
-		this.settings = {
-			allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'],
-			maxFileSize: 5242880,
-			maxProcessingTime: 120000, // 2 minutes max for processing
-			processingCheckInterval: 5000, // Check every 5 seconds
-			smartCompression: true,
-			fieldTypes: {
-				'single': { maxFiles: 1, allowMultiple: false },
-				'gallery': { maxFiles: 20, allowMultiple: true },
-				'groupable': { maxFiles: 20, allowMultiple: true }
+		// Selectors
+		this.selectors = {
+			field: {
+				field: '[data-upload-field]',
+				input: 'input[type="file"]',
+				dropZone: '.file-upload-container',
+				preview: '.item-grid.preview',
+				progress: '.image-progress'
+			},
+			groups: {
+				container: '.upload-group',
+				grid: '.item-grid.group',
+				header: '.group-header',
+				selectAll: '[name="select-all-group"]',
+				actions: '.group-actions',
+				count: '.selection-controls .info'
+			},
+			items: {
+				item: '[data-upload-id]',
+				checkbox: '[name*="select-item"]',
+				featured: '[name="featured"]',
+				details: 'details'
 			}
 		};
 
-		this.acceptedTypes = {
-			image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
-			video: ['video/mp4', 'video/webm', 'video/ogg', 'video/ogv'],
-			document: [
-				'application/pdf',
-				'application/msword',
-				'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-				'text/plain',
-				'text/csv'
-			]
-		};
-
-		this.maxSizes = {
-			image: 5 * 1024 * 1024,    // 5MB
-			video: 100 * 1024 * 1024,  // 100MB
-			document: 10 * 1024 * 1024 // 10MB
-		};
-
 		this.statusMapping = {
 			'received': 'Image Received',
 			'local_processing': 'Processing Image...',
@@ -112,1603 +112,574 @@
 	}
 
 	async init() {
-		this.initElements();
+		// this.initializeFields();
 		this.initListeners();
-		this.initCompressionWorker();
+
+		// Queue integration - handle completion/failure
 		this.queue.subscribe((event, operation) => {
-			if (operation.endpoint !== 'uploads') {
+			if (!['uploads', 'uploads/meta', 'uploads/groups'].includes(operation.endpoint)) {
 				return;
 			}
+
+			const fieldId = operation.data instanceof FormData
+				? operation.data.get('fieldId')
+				: operation.data?.fieldId;
+
 			switch(event) {
 				case 'cancel-operation':
-					this.clearField(operation.data.get('field_key'));
+					if (fieldId) this.handleOperationCancelled(fieldId);
 					break;
 				case 'operation-status':
-					const fieldId = operation.data?.field_key ||
-						(operation.data instanceof FormData ?
-							operation.data.get('field_key') : null);
-
-					if (fieldId) {
-						this.updateFieldStatus(fieldId, operation.status);
-					}
+					if (fieldId) this.updateFieldStatus(fieldId, operation.status);
+					break;
+				case 'operation-complete':
+					this.handleOperationComplete(operation, fieldId);
+					break;
+				case 'operation-failed':
+				case 'operation-failed-permanent':
+					this.handleOperationFailed(operation, fieldId);
 					break;
 			}
 		});
-		this.scanFields();
+
+		window.addEventListener('beforeunload', () => {
+			this.cleanupAllPreviewUrls();
+		});
 	}
 
-	initElements() {
-		this.selectors = {
-			field: {
-				field: '.field.upload',
-				dropZone: '.file-upload-container',
-				preview: '.item-grid.preview',
-				previewWrap: '.preview-wrap',
-				selectAll: '[type=checkbox]#select-all-uploads',
-				selectActions: '.selection-actions',
-				selectCount: '.selected .info',
-				hiddenValue: 'input[type="hidden"]',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
-			},
-			item: {
-				img: 'img',
-				progress: {
-					progress: '.progress',
-					details: '.progress .details',
-					fill: '.progress .fill',
-					count: '.progress .count'
-				},
-				status: '.status',
-				select: '[name*="select-item"]',
-				actions: '.item-actions',
-				featured: '[name="featured"]',
-				meta: '.upload-meta'
-			},
-			groups: {
-				container: '.item-grid.groups',
-				display: '.group-display',
-				selectAll: '#select-all-group',
-				actions: '.selection-actions',
-				info: '.selection-controls .info',
-				count: '.selection-count',
-				group: '.upload-group',
-				empty: '.empty-group'
+	initWorker() {
+		this.worker = {
+			worker: null,
+			timeout: null,
+			tasks: new Map(),
+			restart: { count: 0, max: 3 },
+			settings: {
+				timeout: 10000,
+				batchSize: 1,
+				maxConcurrent: 3,
+				restartAfterTimeout: true
 			}
 		};
-		this.ui = {};
 	}
 
-	scanFields() {
-		document.querySelectorAll(this.selectors.field.field).forEach(uploader => {
-			this.registerUploader(uploader);
-		});
+	/*******************************************************************************
+	 * FIELD MANAGEMENT
+	 *******************************************************************************/
+	scanFields(container, autoUpload) {
+		console.log(autoUpload, 'autoUpload');
+		const fields = container.querySelectorAll(this.selectors.field.field);
+		fields.forEach(uploader => this.registerUploader(uploader, autoUpload));
 	}
 
-	/**
-	 *
-	 * @param {HTMLElement} uploader
-	 * @param {object} options
-	 * @param {string} options.id Uploader field ID: defaults to uploader.dataset.fieldId
-	 * @param {string} options.type Uploader type: defaults to uploader.dataset.type
-	 * @param {number} options.maxFiles Maximum files to allow: defaults to type defaults
-	 * @param {boolean} options.multiple Whether to allow multiple uploads
-	 * @param {number} options.itemID The post or term ID this is for.
-	 * @param {string} options.mode
-	 * @returns {string}
-	 */
-	registerUploader(uploader, options = {}) {
-		//Determine if this is for a post, term, content uploader, or option
-		let key = uploader.dataset['uploader']??this.determineKey(uploader);
+	registerUploader(uploader, autoUpload) {
+		const fieldId = this.determineFieldId(uploader);
+		const config = this.extractFieldConfig(uploader, autoUpload);
+		const ui = this.buildFieldUI(uploader);
 
-		uploader.dataset['uploader'] = key;
+		console.log(config, 'registering with config');
+		// Store field data with Sets for runtime
+		const fieldData = {
+			id: fieldId,
+			config: config,
+			uploads: new Set(),
+			groups: [],
+			state: 'ready',
+			timestamp: Date.now()
+		};
 
-		if (!this.fields.has(key)) {
-			let type = uploader.dataset.type??'single';
+		// Save to store (will convert Sets to Arrays automatically)
+		this.fieldStore.save(fieldData);
 
-			let typeConfig = this.settings.fieldTypes[type]??this.settings.fieldTypes['single'];
-			let config = {
-				key: key,
-				name: uploader.dataset.field,
-				ui: {},
-				type: type,
-				subtype: uploader.dataset.subtype??'image',
-				maxFiles: typeConfig.maxFiles,
-				multiple: typeConfig.allowMultiple,
-				content: uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??false,
-				itemID: uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??false,
-				context: uploader.dataset.context??uploader.closest('dialog')?.dataset.context??false,
-				mode: uploader.dataset.mode??'direct',
-				destination: uploader.dataset.destination ?? 'meta',
-				... options
+		// Store DOM references separately
+		this.fieldElements.set(fieldId, { element: uploader, ui, config });
+
+		uploader.dataset.uploader = fieldId;
+		this.addFieldSelectionHandler(fieldId);
+
+		if (config.type !== 'single') {
+			this.initSortable(fieldId);
+		}
+
+		return fieldId;
+	}
+
+	extractFieldConfig(fieldElement, autoUpload) {
+		return {
+			autoUpload: autoUpload,
+			destination: fieldElement.dataset.destination || 'meta',
+			content: fieldElement.dataset.content || null,
+			mode: fieldElement.dataset.mode || 'direct',
+			type: fieldElement.dataset.type || 'single',
+			name: fieldElement.dataset.field,
+			itemID: fieldElement.dataset.itemId || 0,
+			maxFiles: parseInt(fieldElement.dataset.maxFiles) || 999,
+			subtype: fieldElement.dataset.subtype || 'image'
+		};
+	}
+
+	buildFieldUI(fieldElement) {
+		let UI = {
+			field: fieldElement,
+			input: fieldElement.querySelector(this.selectors.field.input),
+			dropZone: fieldElement.querySelector(this.selectors.field.dropZone),
+			preview: fieldElement.querySelector(this.selectors.field.preview),
+			progress: {
+				progress: fieldElement.querySelector(this.selectors.field.progress),
+				bar: fieldElement.querySelector('.bar'),
+				fill: fieldElement.querySelector('.fill'),
+				details: fieldElement.querySelector('.details'),
+				text: fieldElement.querySelector('.details .text'),
+				count: fieldElement.querySelector('.details .count')
+			}
+		};
+
+		let display = fieldElement.querySelector('.group-display');
+		if (display) {
+			UI.groups = {
+				display: display,
+				container: fieldElement.querySelector('.item-grid.groups'),
+				empty: fieldElement.querySelector('.empty-group'),
+				groups: new Map()
 			};
-
-			config.ui = window.uiFromSelectors(this.selectors, uploader);
-			config.ui.groups.groups = new Map();
-
-			this.selected.set(key, new Set());
-			this.fields.set(key, config);
-			if(config.destination === 'post_group' && !this.hasGroups) {
-				this.initGroupListeners();
-			}
-			// Initialize selection handler for this field
-			this.initSelectionHandler(key, config);
 		}
-		return key;
+
+		return UI;
 	}
 
-	initSelectionHandler(fieldKey) {
-		const field = this.fields.get(fieldKey);
-		if (!field) return;
+	/*******************************************************************************
+	 * SORTABLE INITIALIZATION
+	 *******************************************************************************/
+	initSortable(fieldId) {
+		if (!window.Sortable) return;
 
-		// Don't reinitialize if already exists
-		if (this.selectionHandlers.has(fieldKey)) {
-			return this.selectionHandlers.get(fieldKey);
+		// Mount MultiDrag plugin once
+		if (!Sortable._multiDragMounted && Sortable.MultiDrag) {
+			Sortable.mount(new Sortable.MultiDrag());
+			Sortable._multiDragMounted = true;
 		}
 
-		// Get the container - use preview for uploads in preview, or field for all uploads
-		const container = field.ui.field.previewWrap;
-		if (!container) {
-			console.warn('No container found for selection handler:', fieldKey);
-			return;
-		}
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
 
-		const handler = new window.jvbHandleSelection({
-			container: container,
-			ui: {
-				selectAll: field.ui.field.selectAll,
-				bulkControls: field.ui.field.selectActions,
-				count: field.ui.field.selectCount
-			},
-			itemSelector: '[data-upload-id]',
-			checkboxSelector: '[name*="select-item"]',
+		// Initialize sortable on all existing grids
+		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+		grids.forEach(grid => {
+			const groupId = grid.classList.contains('group')
+				? grid.closest('.upload-group')?.dataset.groupId
+				: null;
+			this.createSortableForGrid(grid, fieldId, groupId);
 		});
 
-		handler.subscribe((event, data) => {
-			switch(event) {
-				case 'item-selected':
-				case 'item-deselected':
-				case 'range-selected':
-					this.selected.set(fieldKey, data.selectedItems);
-					break;
-				case 'select-all':
-					this.handleSelectAll(data.container, data.selected);
-					break;
+		// Special handler for empty-group
+		const emptyGroup = fieldEl.element.querySelector('.empty-group');
+		if (emptyGroup && !emptyGroup.sortableInstance) {
+			emptyGroup.sortableInstance = new Sortable(emptyGroup, {
+				animation: 150,
+				draggable: '.item',
+				multiDrag: true,
+				selectedClass: 'selected-for-drag',
+				avoidImplicitDeselect: true,
+				group: { name: fieldId, pull: false, put: true },
+				ghostClass: 'sortable-ghost',
+				chosenClass: 'sortable-chosen',
+				dragClass: 'sortable-drag',
+				onEnd: (evt) => this.handleDrop(evt, fieldId)
+			});
+		}
+	}
+
+	syncSortableSelection(fieldId, selectedItems) {
+		// Update Sortable's selection state to match checkboxes
+		this.sortableInstances.forEach((instance, key) => {
+			if (key.startsWith(fieldId)) {
+				const grid = instance.el;
+				const items = grid.querySelectorAll('.item');
+
+				items.forEach(item => {
+					const uploadId = item.dataset.uploadId;
+					const shouldBeSelected = selectedItems.has(uploadId);
+
+					if (shouldBeSelected) {
+						Sortable.utils.select(item);
+					} else {
+						Sortable.utils.deselect(item);
+					}
+				});
 			}
 		});
-
-		this.selectionHandlers.set(fieldKey, handler);
-
-		return handler;
 	}
 
-	addGroupSelectionHandler(fieldId, groupId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
+	handleDrop(evt, fieldId) {
+		const dropTarget = evt.to;
+		const sourceTarget = evt.from;
+		const items = evt.items?.length > 0 ? evt.items : [evt.item];
+		const uploadIds = items.map(item => item.dataset.uploadId);
 
-		const group = this.groups.get(groupId);
-		if (!group) return;
+		// Determine drop target type
+		const targetType = this.getDropTargetType(dropTarget);
 
-		let handlerKey = fieldId+'_'+groupId;
-		// Don't reinitialize if already exists
-		if (this.selectionHandlers.has(handlerKey)) {
-			return this.selectionHandlers.get(handlerKey);
-		}
+		switch (targetType) {
+			case 'empty-group':
+				this.handleDropToEmptyGroup(items, uploadIds, fieldId);
+				break;
 
-		// Get the container - use preview for uploads in preview, or field for all uploads
-		const container = group.element;
-		if (!container) {
-			console.warn('No container found for selection handler:', fieldKey);
-			return;
-		}
+			case 'preview':
+				this.handleDropToPreview(items, uploadIds, fieldId);
+				break;
 
-		const handler = new window.jvbHandleSelection({
-			container: container,
-			ui: {
-				selectAll: container.querySelector(this.selectors.groups.selectAll),
-				bulkControls: container.querySelector(this.selectors.groups.actions),
-				count: container.querySelector(this.selectors.groups.count)
-			},
-			itemSelector: '[data-upload-id]',
-			checkboxSelector: '[name*="select-item"]',
-		});
-
-		handler.subscribe((event, data) => {
-			switch(event) {
-				case 'item-selected':
-				case 'item-deselected':
-				case 'range-selected':
-					this.selected.set(fieldId, data.selectedItems);
-					break;
-				case 'select-all':
-					this.handleSelectAll(data.container, data.selected);
-					break;
-			}
-		});
-
-		this.selectionHandlers.set(handlerKey, handler);
-		return handler;
-	}
-
-	removeSelectionHandler(fieldId, groupId = null) {
-		let key = fieldId;
-		if (groupId) {
-			key = key+'_'+groupId;
-		}
-		if (this.selectionHandlers.has(key)) {
-			let handler = this.selectionHandlers.get(key);
-			handler.destroy();
-			this.selectionHandlers.delete(key);
-		}
-	}
-
-	/**
-	 * Builds a key from the uploader, built from the Content Type, ItemID, and FieldName
-	 * @param uploader
-	 * @returns {string}
-	 */
-	determineKey(uploader) {
-		let content = uploader.dataset.content??uploader.closest('dialog')?.dataset.content??uploader.closest('form').dataset.save??'';
-		let itemID = uploader.dataset.itemID??uploader.closest('dialog')?.dataset.itemID??'';
-		let field = uploader.dataset.field;
-		return `${content}_${itemID}_${field}`;
-	}
-
-	/**
-	 *
-	 * @param {HTMLElement} element
-	 */
-	getFieldIdFromElement(element) {
-		let field = element.closest(this.selectors.field.field);
-		if (!field) {
-			return;
-		}
-		return field.dataset.uploader??this.determineKey(field);
-	}
-
-	getFieldFromElement(element) {
-		let id = this.getFieldIdFromElement(element);
-		return (this.fields.has(id)) ? this.fields.get(id) : false;
-	}
-
-	getUploadFromElement(element) {
-		let id = this.getUploadIdFromElement(element);
-		return (this.uploads.has(id)) ? this.uploads.get(id) : false;
-	}
-
-	getUploadIdFromElement(element) {
-		let upload = element.closest('[data-upload-id]');
-		return upload?.dataset.uploadId || null;
-	}
-
-	getGroupFromElement(element) {
-		let groupId = this.getGroupIdFromElement(element);
-		return (this.groups.has(groupId)) ? this.groups.get(groupId) : false;
-	}
-	getGroupIdFromElement(element) {
-		return element.dataset.groupId??element.closest('[data-group-id]')?.dataset.groupId??element.closest(':has([data-group-id])')?.querySelector('[data-group-id]')?.dataset.groupId??null;
-	}
-
-	getModalType(field) {
-		// Safety check for field.ui
-		if (!field || !field.ui || !field.ui.field || !field.ui.field.field) {
-			return null;
-		}
-
-		const dialog = field.ui.field.field.closest('dialog');
-		if (!dialog) return null;
-
-		if (dialog.classList.contains('edit')) return 'edit';
-		if (dialog.classList.contains('create')) return 'create';
-		if (dialog.classList.contains('bulkEdit')) return 'bulkEdit';
-
-		return dialog.className;
-	}
-
-	getStatusText(status) {
-		return this.statusMapping[status] || status;
-	}
-
-	getStatusIcon(status) {
-		return window.getIcon(this.queue.icons[status]);
-	}
-	getStatusProgress(status) {
-		switch (status) {
-			case 'local_processing':
-				return 28;
-			case 'queued':
-				return 50;
-			case 'uploading':
-				return 66;
-			case 'pending':
-				return 75;
-			case 'processing':
-				return 89;
-			case 'completed':
-				return 100;
+			case 'group':
+				this.handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId);
+				break;
 			default:
-				return 0;
+				// Fallback: return to preview
+				this.handleDropToPreview(items, uploadIds, fieldId);
+				break;
+		}
+
+		// Update UI
+		this.updateSortableState(dropTarget);
+		if (sourceTarget !== dropTarget) {
+			this.updateSortableState(sourceTarget);
 		}
 	}
 
-	/******************************************************************************
-	 LISTENERS
-	******************************************************************************/
-	initListeners() {
-		this.clickHandler 		= this.handleClick.bind(this);
-		this.changeHandler 		= this.handleChange.bind(this);
-
-		if (this.hasBulkContext) {
-			this.pasteHandler 		= this.handlePaste.bind(this);
-			document.addEventListener('paste', this.pasteHandler);
+	/**
+	 * Determine what type of drop target this is
+	 */
+	getDropTargetType(target) {
+		if (target.classList.contains('empty-group')) {
+			return 'empty-group';
 		}
 
+		if (target.classList.contains('preview')) {
+			return 'preview';
+		}
+
+		if (target.classList.contains('group')) {
+			return 'group';
+		}
+
+		return 'unknown';
+	}
+
+	/**
+	 * Handle drop to group: add to existing group
+	 */
+	handleDropToGroup(items, uploadIds, dropTarget, sourceTarget, fieldId) {
+		try {
+			// If same container, it's just a reorder
+			if (dropTarget === sourceTarget) {
+				this.handleReorder({ to: dropTarget, items: items });
+				return;
+			}
+
+			// Moving to different group
+			uploadIds.forEach(uploadId => {
+				this.addToGroup(uploadId, dropTarget, false);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Moved ${items.length} items to group`
+				: 'Moved item to group';
+			this.a11y.announce(message);
+
+			// Clear selection
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		}  catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Handle drop to preview: remove from groups
+	 */
+	handleDropToPreview(items, uploadIds, fieldId) {
+		try {
+			uploadIds.forEach(uploadId => {
+				this.removeFromGroup(uploadId);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Moved ${items.length} items to preview`
+				: 'Moved item to preview';
+			this.a11y.announce(message);
+
+			// Clear selection
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		} catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Handle drop errors consistently
+	 */
+	handleDropError(items, fieldId, error, message = 'An error occurred') {
+		console.error('Drop error:', error);
+
+		// Return items to preview as fallback
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (fieldEl?.ui?.preview) {
+			items.forEach(item => fieldEl.ui.preview.appendChild(item));
+		}
+
+		this.a11y.announce(`${message}. Items returned to preview.`);
+	}
+
+	/**
+	 * Handle drop to group: add to existing group
+	 */
+	handleDropToEmptyGroup(items, uploadIds, fieldId) {
+		try {
+			const group = this.createGroup(fieldId);
+			if (!group) {
+				this.handleDropError(items, fieldId, new Error('Group creation failed'), 'Failed to create group');
+				return;
+			}
+
+			// Move items to new group
+			items.forEach((item, index) => {
+				group.grid.appendChild(item);
+				this.addToGroup(uploadIds[index], group.grid, false);
+			});
+
+			this.schedulePersistance(fieldId);
+
+			const message = items.length > 1
+				? `Created group with ${items.length} items`
+				: 'Created group with item';
+			this.a11y.announce(message);
+
+			// Clear selection after move
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+		} catch (error) {
+			this.handleDropError(items, fieldId, error);
+		}
+	}
+
+	/**
+	 * Update sortable enabled/disabled state based on item count
+	 */
+	updateSortableState(grid) {
+		const sortable = grid?.sortableInstance;
+		if (!sortable) return;
+
+		// const hasItems = grid.querySelectorAll('.item').length > 0;
+		sortable.option('disabled', false);
+	}
+
+	/**
+	 * Refresh sortable for a field (call after adding/removing items dynamically)
+	 */
+	refreshSortable(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl) return;
+
+		const grids = fieldEl.element.querySelectorAll('.item-grid.preview, .item-grid.group');
+		grids.forEach(grid => this.updateSortableState(grid));
+	}
+
+	handleReorder(evt) {
+		const grid = evt.to;
+		const fieldWrapper = grid.closest('.field, .upload');
+		if (!fieldWrapper) return;
+
+		// Get current order from DOM
+		let items = Array.from(grid.querySelectorAll('.item:not(.sortable-ghost):not(.sortable-clone)'))
+			.map(upload => upload.dataset.uploadId)
+			.filter(id => id);
+
+
+		// Update hidden input (for form submission)
+		let hiddenInput = fieldWrapper.querySelector('input[type="hidden"]');
+		if (hiddenInput && items.length > 0) {
+			hiddenInput.value = items.join(',');
+		}
+
+		// Update fieldState with new order
+		const fieldId = this.getFieldIdFromElement(grid);
+		if (fieldId) {
+			const fieldData = this.getFieldData(fieldId);
+
+			// If reordering within a group, update that group's uploads array
+			if (grid.classList.contains('group')) {
+				const groupId = grid.dataset.groupId;
+				const group = fieldData?.groups?.find(g => g.id === groupId);
+				if (group) {
+					group.uploads = items; // Update order
+				}
+			}
+			// If reordering in preview, the order is implicit by DOM position
+			// (we don't store preview order separately)
+
+			this.schedulePersistance(fieldId);
+		}
+
+		this.a11y.announce('Item reordered');
+
+		fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', {
+			detail: {
+				from: evt.from,
+				to: evt.to,
+				oldIndex: evt.oldIndex,
+				newIndex: evt.newIndex,
+				items: items
+			},
+			bubbles: true
+		}));
+	}
+
+	/*******************************************************************************
+	 * FILE DROP HANDLERS
+	 *******************************************************************************/
+
+	initListeners() {
+		this.clickHandler = this.handleClick.bind(this);
+		this.changeHandler = this.handleChange.bind(this);
 
 		document.addEventListener('click', this.clickHandler);
 		document.addEventListener('change', this.changeHandler);
-		window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
+
+		this.dragEnterHandler = this.handleExternalDragEnter.bind(this);
+		this.dragLeaveHandler = this.handleExternalDragLeave.bind(this);
+		this.dragOverHandler = this.handleExternalDragOver.bind(this);
+		this.dropHandler = this.handleExternalDrop.bind(this);
+
+		document.addEventListener('dragenter', this.dragEnterHandler);
+		document.addEventListener('dragleave', this.dragLeaveHandler);
+		document.addEventListener('dragover', this.dragOverHandler);
+		document.addEventListener('drop', this.dropHandler);
 	}
-	clearListeners() {
-		document.removeEventListener('click', this.clickHandler);
-		document.removeEventListener('change', this.changeHandler);
-		if (this.hasBulkContext) {
-			document.removeEventListener('paste', this.pasteHandler);
+
+	handleExternalDragLeave(e) {
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone && !dropZone.contains(e.relatedTarget)) {
+			dropZone.classList.remove('dragover');
 		}
 	}
 
-	initGroupListeners() {
-		this.hasGroups = true;
-
-		this.dragStartHandler 	= this.handleDragStart.bind(this);
-		this.dragEndHandler 	= this.handleDragEnd.bind(this);
-		this.dragEnterHandler 	= this.handleDragEnter.bind(this);
-		this.dragOverHandler 	= this.handleDragOver.bind(this);
-		this.dragLeaveHandler 	= this.handleDragLeave.bind(this);
-		this.dropHandler 		= this.handleDrop.bind(this);
-
-		this.touchStartHandler 	= this.handleTouchStart.bind(this);
-		this.touchMoveHandler 	= this.handleTouchMove.bind(this);
-		this.touchEndHandler 	= this.handleTouchEnd.bind(this);
-		this.touchCancelHandler	= this.handleTouchCancel.bind(this);
-
-		document.addEventListener('dragstart', this.dragStartHandler);
-		document.addEventListener('dragend', this.dragEndHandler);
-		document.addEventListener('dragenter', this.dragEnterHandler);
-		document.addEventListener('dragover', this.dragOverHandler);
-		document.addEventListener('dragleave', this.dragLeaveHandler);
-		document.addEventListener('drop', this.dropHandler);
-
-		document.addEventListener('touchstart', this.touchStartHandler, { passive: false });
-		document.addEventListener('touchmove', this.touchMoveHandler, { passive: false });
-		document.addEventListener('touchend', this.touchEndHandler, { passive: false });
-		document.addEventListener('touchcancel', this.touchCancelHandler, { passive: false });
-
-		document.addEventListener('input', (e) => {
-			if (e.target.matches('.fields.group input, .fields.group textarea')) {
-				this.handleGroupMetadataChange(e);
-			}
-		});
+	handleExternalDragEnter(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			dropZone.classList.add('dragover');
+		}
 	}
-	handleGroupMetadataChange(e) {
-		if (!e.target.closest('.fields.group')) return;
 
-		const groupElement = e.target.closest('[data-group-id]');
-		if (!groupElement) return;
-
-		const fieldId = groupElement.dataset.fieldId;
-		this.persistFieldState(fieldId);
+	handleExternalDragOver(e) {
+		if (!e.dataTransfer.types.includes('Files')) return;
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (dropZone) {
+			e.preventDefault();
+			e.dataTransfer.dropEffect = 'copy';
+		}
 	}
-	clearGroupListeners() {
-		document.removeEventListener('dragstart', this.dragStartHandler);
-		document.removeEventListener('dragend', this.dragEndHandler);
-		document.removeEventListener('dragenter', this.dragEnterHandler);
-		document.removeEventListener('dragover', this.dragOverHandler);
-		document.removeEventListener('dragleave', this.dragLeaveHandler);
-		document.removeEventListener('drop', this.dropHandler);
 
-		document.removeEventListener('touchstart', this.touchStartHandler, { passive: false });
-		document.removeEventListener('touchmove', this.touchMoveHandler, { passive: false });
-		document.removeEventListener('touchend', this.touchEndHandler, { passive: false });
-		document.removeEventListener('touchcancel', this.touchCancelHandler, { passive: false });
+	handleExternalDrop(e) {
+		const dropZone = e.target.closest(this.selectors.field.dropZone);
+		if (!dropZone) return;
+
+		e.preventDefault();
+		dropZone.classList.remove('dragover');
+
+		const files = Array.from(e.dataTransfer.files);
+		if (files.length === 0) return;
+
+		const fieldId = this.getFieldIdFromElement(dropZone);
+		if (fieldId) {
+			this.processFiles(fieldId, files);
+			this.a11y.announce(`${files.length} file(s) dropped for upload`);
+		}
 	}
 
+	/*******************************************************************************
+	 * CLICK & CHANGE HANDLERS
+	 *******************************************************************************/
+
 	handleClick(e) {
-		if (!e.target.closest(this.selectors.field.field)) {
-			return;
-		}
-		let actionButton = window.targetCheck(e, '[data-action]');
-
-		if (!actionButton) {
-			return;
-		}
-		let action = actionButton.dataset.action;
-
-		let field = this.getFieldFromElement(actionButton);
-		let selected = this.getCurrentSelection(field.key);
-		let group = this.getGroupFromElement(actionButton);
-		let groupId = (group) ? group.id : false;
-		let isItem = actionButton.closest('[data-upload-id]');
-		let items = 'upload';
-		let reference = 'it';
-		if (isItem) {
-			selected = [isItem.dataset.uploadId];
-		} else {
-			if (selected.length > 1) {
-				items = 'uploads';
-				reference = 'them';
+		// Trigger file input
+		if (e.target.matches(this.selectors.field.dropZone) ||
+			e.target.closest(this.selectors.field.dropZone)) {
+			const dropZone = e.target.closest(this.selectors.field.dropZone);
+			if (dropZone && !e.target.matches('input, button, a')) {
+				const input = dropZone.querySelector(this.selectors.field.input);
+				input?.click();
 			}
 		}
 
-		let deleteUploads;
-
-		switch (action) {
-			case 'add-to-group':
-				//Create from selection
-				//Check for groupId, if no group id, create new group with selection
-				if (selected.length === 0) {
-					//Nothing to move
-					return;
-				}
-				if (!groupId) {
-					group = this.createGroup(field.key);
-					groupId = group.id;
-				}
-				this.addSelectionToGroup(group.element);
-
-				break;
-			case 'remove-from-group':
-				if (selected.length === 0) {
-					return;
-				}
-				//confirm if they want to keep uploads
-				//remove selection from group
-
-				deleteUploads = !confirm(`Would you like to keep the ${items}, just remove ${reference} from this group?`);
-				selected.forEach(upload => {
-					this.removeFromGroup(field.key, upload, groupId);
-					if (deleteUploads) {
-						this.removeUpload(field.key, upload);
-					}
-				});
-				break;
-			case 'delete-upload':
-				if (selected.length === 0) {
-					return;
-				}
-				//delete selection
-				deleteUploads = false;
-				reference = (reference === 'them') ? 'these' : 'this';
-				if (confirm(`Are you sure you want to delete ${reference} ${items}?`)) {
-					deleteUploads = true;
-				}
-				selected.forEach(upload => {
-					this.removeFromGroup(field.key, upload, groupId);
-					if (deleteUploads) {
-						this.removeUpload(field.key, upload);
-					}
-				});
-				break;
-			case 'delete-group':
-				//delete entire group
-				if (group.uploads.length > 0) {
-
-					deleteUploads = confirm(`Do you want to remove all uploads in the group, too?`);
-					if (deleteUploads) {
-						group.uploads.forEach(upload => {
-							this.removeUpload(field.key, upload);
-						});
-					} else {
-						group.uploads.forEach(upload => {
-							this.addImageToGroup(upload);
-						})
-					}
-				}
-				this.removeGroup(groupId, false);
-				break;
-			case 'upload':
-				//upload groups
-				e.preventDefault();
-				this.submitUploads(field.key);
-				break;
-			case 'restore':
-				let notification = document.querySelector('dialog.restore-uploads');
-				if (!notification) {
-					return;
-				}
-				//restore selected uploads
-				const selectedUploads = this.getSelectedRestorationUploads(notification);
-				if (selectedUploads.length === 0) {
-					// this.notifications.add('No uploads selected for restoration', 'warning');
-					return;
-				}
-				this.restoreSelectedUploads(selectedUploads);
-
-				this.restoreModal.handleClose();
-				this.restoreSelection.destroy();
-				this.restoreSelection = null;
-				// Clean up blob URLs before removing notification
-				this.cleanupRestoreNotificationUrls(notification);
-				notification.remove();
-				break;
-			case 'clear-cache':
-				if (!confirm(`Save these uploads for later?`)) {
-					//clear cached uploads
-					this.cleanupStoredRestoration();
-				}
-
-				this.restoreModal.handleClose();
-				this.restoreSelection.destroy();
-				this.restoreSelection = null;
-				this.restoreModal.destroy();
-				this.restoreModal.modal.remove();
-
-				break;
+		// Group actions
+		const actionButton = e.target.closest('[data-action]');
+		if (actionButton) {
+			this.handleAction(actionButton);
 		}
 	}
-	handleChange(e) {
-		if (!e.target.closest(this.selectors.field.field) || e.target.classList.contains(this.selectors.field.hiddenValue)) {
-			return;
-		}
-		e.preventDefault();
 
-		if (window.targetCheck(e, '[type="file"]')) {
-			let field = this.getFieldFromElement(e.target);
-			if (!field) {
-				console.warn('File change on unregistered field: ', field.key)
+	handleChange(e) {
+		const fieldId = this.getFieldIdFromElement(e.target);
+
+		// File input change
+		if (e.target.matches(this.selectors.field.input)) {
+			const files = Array.from(e.target.files);
+			if (files.length > 0 && fieldId) {
+				this.processFiles(fieldId, files);
+			}
+		}
+
+		// Meta field changes
+		if (fieldId) {
+			const fieldData = this.getFieldData(fieldId);
+			if (!fieldData.config.autoUpload) {
 				return;
 			}
-
-			const files = Array.from(e.target.files);
-			if (files.length === 0) return;
-
-			this.processFiles(field.key, files);
-			e.target.value = '';
-		} else if (e.target.closest('.upload-meta')) {
-			e.preventDefault();
-			let name = e.target.name;
-			let value = e.target.value;
-			let upload = this.getUploadFromElement(e.target);
-			upload.changes[name] = value;
-			this.uploads.set(upload.id, upload);
-			this.persistFieldState(upload.fieldId);
-
-			//It's meta!
-			//TODO:
-			//Step 1) determine whether the images have already been sent to the server. If not, we must wait until they have been
-			//Step 2) Queue the Meta changes. No need to wait, the Queue.js will handle any debouncing/timeouts
-			//Ensure the dependencies have all operations stored to the field that the images were uploaded with (can be multiple)
-			//Send to server for processing
-		} else if (e.target.closest('.group.fields')) {
-			let group = this.getGroupFromElement(e.target);
-			let name = e.target.name;
-			group.changes[name] = e.target.value;
-
-			this.persistFieldState(group.fieldId);
-			this.groups.set(group.id, group);
-		}
-	}
-
-	handlePaste(e) {
-		window.debouncer.schedule(
-			'imagePaste',
-			() => {
-				const items = Array.from(e.clipboardData.items);
-				const imageItems = items.filter(item => item.type.startsWith('image/'));
-
-				if (imageItems.length === 0) return;
-
-				e.preventDefault();
-
-				const fieldId = this.getFieldIdFromElement(e.target);
-				if (!fieldId) return;
-
-				// Convert clipboard items to files
-				const files = [];
-				imageItems.forEach((item, index) => {
-					const file = item.getAsFile();
-					if (file) {
-						// Rename for clarity
-						const newFile = new File([file], `pasted_image_${index + 1}.png`, {
-							type: file.type,
-							lastModified: Date.now()
-						});
-						files.push(newFile);
-					}
-				});
-
-				if (files.length > 0) {
-					this.processFiles(fieldId, files);
-				}
-			},
-			100
-		);
-	}
-
-	isTouchOnFormElement(target) {
-		// Check if target is a form element or inside one
-		const formElements = [
-			'input', 'button', 'label', 'select', 'textarea',
-		];
-
-		return formElements.some(selector => {
-			return target.matches(selector) || target.closest(selector);
-		});
-	}
-	/**** DRAG AND TOUCH *****/
-	startDragOperation(config) {
-		const {
-			primaryElement,
-			sourceType,
-			startPosition,
-			event
-		} = config;
-
-		const uploadId = this.getUploadIdFromElement(primaryElement);
-		const fieldId = this.getFieldIdFromElement(primaryElement);
-
-		// Determine what items to drag
-		const draggedItems = this.getDraggedItems(primaryElement);
-
-		// Initialize drag state
-		this.dragState = {
-			primaryItem: uploadId,
-			draggedItems: draggedItems,
-			isDragging: true,
-			isMultiDrag: draggedItems.length > 1,
-			fieldId: fieldId,
-			sourceType: sourceType,
-			startTime: Date.now(),
-			startPosition: startPosition,
-			currentPosition: startPosition,
-			currentTarget: null,
-			validTarget: null,
-			dragPreview: null,
-			touchId: sourceType === 'touch' ? event.touches[0]?.identifier : null,
-			touchMoved: false
-		};
-
-		// Create drag preview
-		this.createDragPreview(primaryElement);
-
-		// Apply dragging state
-		this.applyDraggingState(true);
-
-		const announceText = this.dragState.isMultiDrag
-			? `Started dragging ${draggedItems.length} items`
-			: 'Started dragging item';
-
-		this.a11y.announce(announceText);
-		this.provideDragFeedback('start');
-
-		return true;
-	}
-
-	updateDragOperation(position, elementUnderPointer) {
-		if (!this.dragState.isDragging) return;
-
-		const { sourceType, startPosition } = this.dragState;
-
-		// Update position
-		this.dragState.currentPosition = position;
-
-		// Check for significant movement (touch)
-		if (sourceType === 'touch' && !this.dragState.touchMoved) {
-			const deltaX = Math.abs(position.x - startPosition.x);
-			const deltaY = Math.abs(position.y - startPosition.y);
-
-			if (deltaX > 10 || deltaY > 10) {
-				this.dragState.touchMoved = true;
-			}
-		}
-
-		// Update preview and target
-		this.updateDragPreview(position);
-		this.updateDropTarget(elementUnderPointer);
-	}
-
-	endDragOperation(elementUnderPointer = null) {
-		if (!this.dragState.isDragging) return;
-
-		const wasSuccessful = (this.dragState.sourceType === 'drag' || this.dragState.touchMoved) &&
-			this.dragState.validTarget;
-
-		// Process drop if valid - but only here, not in handleDrop
-		if (wasSuccessful && this.dragState.validTarget) {
-			this.processItemDrop({
-				itemIds: this.dragState.draggedItems,
-				targetElement: this.dragState.validTarget,
-				fieldId: this.dragState.fieldId,
-				dropType: this.dragState.isMultiDrag ? 'multiple' : 'single',
-				sourceType: this.dragState.sourceType
-			});
-		}
-
-		// Cleanup
-		this.cleanupDragOperation();
-
-		const announceText = wasSuccessful
-			? (this.dragState.isMultiDrag ? `Moved ${this.dragState.draggedItems.length} items` : 'Item moved')
-			: 'Drag cancelled';
-
-		this.a11y.announce(announceText);
-	}
-
-	/**
-	 * Shared method to process any drop operation (drag or touch)
-	 * @param {Object} dropData - Standardized drop data
-	 * @returns {boolean} Success status
-	 */
-	processItemDrop(dropData) {
-		const { itemIds, targetElement, fieldId, dropType, sourceType } = dropData;
-
-		if (!itemIds?.length || !targetElement || !fieldId) {
-			return false;
-		}
-
-		let isPreviewDrop = targetElement.classList.contains('preview') &&
-			targetElement.classList.contains('item-grid');
-		let actualTarget = targetElement;
-
-		// Handle empty group drops
-		if (targetElement.classList.contains('empty-group')) {
-			let group = this.createGroup(fieldId);
-			if (!group) {
-				console.error('Failed to create group');
-				return false;
-			}
-			actualTarget = group.grid;
-			isPreviewDrop = false;
-		}
-
-		itemIds.forEach(uploadId => {
-			this.addImageToGroup(uploadId, isPreviewDrop ? null : actualTarget, false);
-		});
-
-		const field = this.fields.get(fieldId);
-		if (field) {
-			this.clearAllSelections(field);
-		}
-
-		this.persistFieldState(fieldId);
-
-		const announceText = dropType === 'multiple'
-			? `Moved ${itemIds.length} images to ${isPreviewDrop ? 'main area' : 'group'}`
-			: `Image moved to ${isPreviewDrop ? 'main area' : 'group'}`;
-
-		this.a11y.announce(announceText);
-		this.provideFeedback(sourceType, 'success', {
-			count: itemIds.length,
-			isMultiple: dropType === 'multiple'
-		});
-
-		return true;
-	}
-
-
-
-	cleanupDragOperation() {
-		if (this.dragState.dragPreview) {
-			this.dragState.dragPreview.remove();
-		}
-
-		this.applyDraggingState(false);
-		this.clearDropTargetStates();
-
-		// Reset state
-		this.dragState.isDragging = false;
-		this.dragState.dragPreview = null;
-		this.dragState.draggedItems = [];
-	}
-
-	/**
-	 * Determine what items to drag (single or multiple selection)
-	 */
-	getDraggedItems(element) {
-		const selectedUploads = this.getSelectedUploads(element);
-		const primaryUploadId = element.dataset.uploadId;
-
-		// If we have multiple selections and primary is selected, drag all
-		if (selectedUploads.length > 1 && selectedUploads.includes(primaryUploadId)) {
-			return selectedUploads;
-		}
-
-		// Otherwise, just drag the primary item
-		return [primaryUploadId];
-	}
-
-	/**
-	 * Apply/remove dragging visual state to items
-	 */
-	applyDraggingState(isDragging) {
-		this.dragState.draggedItems.forEach(uploadId => {
-			const element = document.querySelector(`[data-upload-id="${uploadId}"]`);
-			if (element) {
-				element.classList.toggle('dragging', isDragging);
-			}
-		});
-	}
-
-	/**
-	 * Create drag preview element
-	 */
-	/**
-	 * Create drag preview element from template
-	 */
-	createDragPreview() {
-		const { draggedItems, sourceType } = this.dragState;
-
-		// Get the template
-		const template = window.getTemplate('dragPreview');
-		if (!template) {
-			console.error('Drag preview template not found');
-			return;
-		}
-
-		this.dragState.dragPreview = template;
-		const itemsContainer = template.querySelector('.drag-items');
-		const countBadge = template.querySelector('.drag-count');
-
-		// Set data attributes for CSS targeting
-		template.dataset.source = sourceType;
-
-		// Handle single vs multi-item
-		const itemCount = draggedItems.length;
-
-		if (itemCount > 1) {
-			// Multi-item: show count and stack up to 3 items
-			template.dataset.count = itemCount;
-			countBadge.dataset.count = itemCount;
-			countBadge.hidden = false;
-
-			const displayCount = Math.min(itemCount, 3);
-			for (let i = 0; i < displayCount; i++) {
-				const uploadId = draggedItems[i];
-				const uploadElement = document.querySelector(`[data-upload-id="${uploadId}"]`);
-
-				if (uploadElement) {
-					const clonedItem = uploadElement.cloneNode(true);
-					clonedItem.dataset.uploadId = `${uploadId}-preview`;
-					// Remove interactive elements from clone
-					clonedItem.querySelectorAll('input, button, details').forEach(el => el.remove());
-					itemsContainer.appendChild(clonedItem);
-				}
-			}
-		} else {
-			// Single item: just clone it
-			const uploadElement = document.querySelector(`[data-upload-id="${draggedItems[0]}"]`);
-			if (uploadElement) {
-				const clonedItem = uploadElement.cloneNode(true);
-				clonedItem.dataset.uploadId = `${draggedItems[0]}-preview`;
-				// Remove interactive elements from clone
-				clonedItem.querySelectorAll('input, button, details').forEach(el => el.remove());
-				itemsContainer.appendChild(clonedItem);
-			}
-		}
-
-		// Add to DOM
-		document.body.appendChild(this.dragState.dragPreview);
-
-		// Position immediately at start position
-		this.updateDragPreview(this.dragState.startPosition);
-	}
-
-	/**
-	 * Update drag preview position
-	 */
-	updateDragPreview(position) {
-		if (!this.dragState.dragPreview) return;
-
-		const preview = this.dragState.dragPreview;
-
-		// Determine offset based on source type
-		let offset;
-		if (this.dragState.sourceType === 'touch') {
-			// For touch, offset up and to the left so finger doesn't cover preview
-			offset = this.dragState.isMultiDrag
-				? { x: -60, y: -80 }
-				: { x: -50, y: -60 };
-		} else {
-			// For mouse, smaller offset
-			offset = this.dragState.isMultiDrag
-				? { x: 15, y: 15 }
-				: { x: 10, y: 10 };
-		}
-
-		// Position the preview at the current pointer position with offset
-		preview.style.left = `${position.x + offset.x}px`;
-		preview.style.top = `${position.y + offset.y}px`;
-	}
-
-	/**
-	 * Update drop target highlighting
-	 */
-	updateDropTarget(elementUnderPointer) {
-		// Clear previous target
-		if (this.dragState.currentTarget) {
-			this.clearDropTargetState(this.dragState.currentTarget);
-		}
-
-		// Find valid drop target
-		const validTarget = this.findValidDropTarget(elementUnderPointer);
-
-		// Update state
-		this.dragState.currentTarget = elementUnderPointer;
-		this.dragState.validTarget = validTarget;
-
-		// Apply visual feedback
-		if (validTarget) {
-			this.applyDropTargetState(validTarget);
-
-			// Haptic feedback for touch
-			if (this.dragState.sourceType === 'touch' && navigator.vibrate) {
-				const pattern = this.dragState.isMultiDrag ? [25, 10, 25] : [25];
-				navigator.vibrate(pattern);
-			}
-		}
-	}
-
-	/**
-	 * Find valid drop target from element
-	 */
-	findValidDropTarget(element) {
-		const target = element?.closest('.item-grid.group, .empty-group, .item-grid.preview');
-		return target && this.getFieldIdFromElement(target) === this.dragState.fieldId ? target : null;
-	}
-
-	/**
-	 * Apply drop target visual state
-	 */
-	applyDropTargetState(target) {
-		target.classList.add('dragover');
-
-		if (this.dragState.isMultiDrag) {
-			target.classList.add('multi-drop');
-			target.setAttribute('data-item-count', this.dragState.draggedItems.length);
-		}
-	}
-
-	/**
-	 * Clear drop target state from element
-	 */
-	clearDropTargetState(target) {
-		target.classList.remove('dragover', 'multi-drop');
-		target.removeAttribute('data-item-count');
-	}
-
-	/**
-	 * Clear all drop target states
-	 */
-	clearDropTargetStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-
-
-	/**
-	 * Provide feedback for drag operations
-	 */
-	provideDragFeedback(type) {
-		const hapticPatterns = {
-			start: [50],
-			success: this.dragState.isMultiDrag ? [30, 20, 30] : [50],
-			error: [100, 50, 100],
-			warning: [50]
-		};
-
-		// Haptic feedback (vibration on supported devices)
-		if (navigator.vibrate && hapticPatterns[type]) {
-			navigator.vibrate(hapticPatterns[type]);
-		}
-
-		// Visual feedback
-		const feedback = document.createElement('div');
-		feedback.className = `drag-feedback ${type}`;
-		feedback.style.cssText = `
-		position: fixed;
-		top: 50%;
-		left: 50%;
-		transform: translate(-50%, -50%);
-		padding: 1rem 2rem;
-		background: var(--${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'warning'});
-		color: white;
-		border-radius: var(--radius);
-		z-index: 10001;
-		animation: feedbackPulse 0.3s ease;
-		pointer-events: none;
-	`;
-
-		const icons = {
-			start: '↕️',
-			success: '✓',
-			error: '✗',
-			warning: '⚠'
-		};
-
-		feedback.textContent = icons[type] || '';
-		document.body.appendChild(feedback);
-
-		setTimeout(() => {
-			feedback.style.animation = 'fadeOut 0.3s ease';
-			setTimeout(() => feedback.remove(), 300);
-		}, 500);
-	}
-
-	/**
-	 * Provide consistent feedback for different input methods
-	 */
-	provideFeedback(sourceType, feedbackType, data = {}) {
-		const hapticPatterns = {
-			success: data.isMultiple ? [50, 25, 50, 25, 50] : [50, 25, 50],
-			error: [100, 50, 100]
-		};
-
-		if (sourceType === 'touch' && navigator.vibrate && hapticPatterns[feedbackType]) {
-			navigator.vibrate(hapticPatterns[feedbackType]);
-		}
-	}
-
-	clearDragoverStates() {
-		document.querySelectorAll('.dragover').forEach(el => {
-			el.classList.remove('dragover', 'multi-drop');
-			el.removeAttribute('data-item-count');
-		});
-	}
-	/*********
-	 *  DRAG HANDLERS
-	 ********/
-	handleDragEnter(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		// Only handle external files
-		if (e.dataTransfer.types.includes('Files')) {
-			e.preventDefault();
-			const uploadContainer = e.target.closest('.file-upload-container');
-			if (uploadContainer) {
-				uploadContainer.classList.add('dragover');
-			}
-		}
-	}
-	handleDragLeave(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer && !uploadContainer.contains(e.relatedTarget)) {
-			uploadContainer.classList.remove('dragover');
-		}
-	}
-	handleDragStart(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'drag',
-			startPosition: { x: e.clientX, y: e.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.dataTransfer.setData('text/plain', this.dragState.primaryItem);
-			e.dataTransfer.effectAllowed = 'move';
-		} else {
-			e.preventDefault();
-		}
-	}
-
-	handleDragOver(e) {
-		if (!this.dragState.isDragging) return;
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		e.preventDefault();
-		e.dataTransfer.dropEffect = 'move';
-
-		const elementUnderPointer = document.elementFromPoint(e.clientX, e.clientY);
-		this.updateDragOperation(
-			{ x: e.clientX, y: e.clientY },
-			elementUnderPointer
-		);
-	}
-
-	handleDrop(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-
-		e.preventDefault();
-		this.clearDragoverStates();
-
-		// Handle external files (new uploads)
-		const uploadContainer = e.target.closest('.file-upload-container');
-		if (uploadContainer) {
-			const files = Array.from(e.dataTransfer.files);
-			if (files.length > 0) {
-				const fieldId = this.getFieldIdFromElement(uploadContainer);
-				if (fieldId) {
-					this.processFiles(fieldId, files);
-					this.a11y.announce(`${files.length} file(s) dropped for upload`);
-				}
-			}
-		}
-	}
-
-	handleDragEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		// Find the element under the final drop position
-		const elementUnderDrop = document.elementFromPoint(
-			this.dragState.currentPosition?.x || e.clientX,
-			this.dragState.currentPosition?.y || e.clientY
-		);
-
-		this.endDragOperation(elementUnderDrop);
-	}
-	/*********
-	 * TOUCH HANDLERS
-	 ********/
-	handleTouchStart(e) {
-		if (!window.targetCheck(e, '.field.upload')) return;
-		if (this.isTouchOnFormElement(e.target)) {
-			return;
-		}
-
-		const uploadItem = e.target.closest('[data-upload-id]');
-		if (!uploadItem) return;
-
-		const touch = e.touches[0];
-
-		const result = this.startDragOperation({
-			primaryElement: uploadItem,
-			sourceType: 'touch',
-			startPosition: { x: touch.clientX, y: touch.clientY },
-			event: e
-		});
-
-		if (result) {
-			e.preventDefault(); // Prevent scrolling
-		}
-	}
-
-	handleTouchMove(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.touches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.updateDragOperation(
-			{ x: touch.clientX, y: touch.clientY },
-			elementUnderTouch
-		);
-	}
-
-	handleTouchEnd(e) {
-		if (!this.dragState.isDragging) return;
-
-		e.preventDefault();
-		const touch = e.changedTouches[0];
-		const elementUnderTouch = document.elementFromPoint(touch.clientX, touch.clientY);
-
-		this.endDragOperation(elementUnderTouch);
-	}
-
-	handleTouchCancel(e) {
-		if (!this.dragState.isDragging) {
-			return;
-		}
-		if (this.dragState.isDragging) {
-			this.cleanupDragOperation();
-			this.a11y.announce('Drag cancelled');
-		}
-	}
-	/*******************************************************************************
-	 QUEUE INTEGRATION
-	 *******************************************************************************/
-	async submitUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		// Check if there are uploads to submit
-		const pendingUploads = Array.from(field.uploads || [])
-			.map(id => this.uploads.get(id))
-			.filter(upload => upload &&
-				(upload.status === 'processed' ||
-					upload.status === 'processed-original'));
-
-		if (pendingUploads.length === 0) {
-			// this.notifications.add('No uploads ready to submit', 'warning');
-			return;
-		}
-
-		// Queue the uploads
-		try {
-			await this.queueUpload(fieldId);
-			// this.notifications.add(`Submitting ${pendingUploads.length} upload(s)`, 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'submitUploads',
-				fieldId
-			});
-			// this.notifications.add('Failed to submit uploads', 'error');
-		}
-	}
-	async retryUpload(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		try {
-			// Reset status
-			this.updateUploadStatus(uploadId, 'received');
-
-			// If we have the processed file, skip to queuing
-			if (upload.processedFile) {
-				this.updateUploadStatus(uploadId, 'processed');
-				await this.queueUpload(upload.fieldId);
-			} else if (upload.originalFile) {
-				// Reprocess the file
-				const reprocessed = await this.processFile(upload.originalFile, field);
-				if (reprocessed) {
-					await this.queueUpload(upload.fieldId);
-				}
+			if (fieldData?.config.destination === 'post_group') {
+				this.handleGroupMetaChange(e.target);
 			} else {
-				throw new Error('No file data available for retry');
+				this.queueUploadMeta(e);
 			}
-
-			// this.notifications.add('Retrying upload...', 'info');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'retryUpload',
-				uploadId
-			});
-			// this.notifications.add('Failed to retry upload', 'error');
 		}
 	}
 
-	async queueUpload(fieldId) {
-		//Further cache it, or is it already cached at this point?
-		const field = this.fields.get(fieldId);
-		if (!field?.uploads) return;
-
-		const uploads = Array.from(field.uploads);
-		if (uploads.length === 0) {
-			return;
-		}
-
-		const data = this.prepareUploadData(field, uploads);
-		this.a11y.announce('Queuing for upload');
-		let img = (uploads.length === 1) ? 'image' : 'images';
-		const operation = {
-			endpoint: 'uploads',
-			method: 'POST',
-			data: data,
-			title: `Uploading ${uploads.length} ${img} to server...`,
-			popup: `Uploading ${uploads.length} ${img}...`,
-			canMerge: false,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			},
-			append: '_upload'
-		}
-		try {
-			const operationId = await this.queue.addToQueue(operation);
-
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (!upload) {
-					return;
-				}
-				upload.operationId = operationId;
-				this.updateUploadStatus(uploadId, 'queued');
-			});
-			field.operationId = operationId;
-
-			return operationId;
-		} catch (error) {
-			throw error;
-		} finally {
-			this.persistFieldState(field.key);
-		}
-	}
-
-	prepareUploadData(field, uploads) {
-
-		const formData = new FormData();
-		formData.append('content', field.content);
-		formData.append('mode', field.mode);
-		formData.append('field_name', field.name);
-		formData.append('field_key', field.key);
-		formData.append('field_type', field.type);
-		formData.append('subtype', field.subtype);
-		formData.append('item_id', field.itemID);		//post, term, or user id
-		formData.append('context', field.context);	//post, term, or user
-		formData.append('destination', field.destination || 'meta'); //meta, post, post_group
-		let uploadMap = [];
-
-		const fieldGroups = this.getFieldGroups(field.key);
-		if (field.destination === 'post_group' && fieldGroups.length > 0) {
-			// User has created groups
-			let groups = [];
-			let titles = [];
-			let featuredImages = [];
-
-			fieldGroups.forEach(group => {
-				let groupUploadIndices = [];
-				let featuredIndex = null;
-
-				group.uploads.forEach(uploadId => {
-					let upload = this.uploads.get(uploadId);
-					if (upload) {
-						const fileToUpload = upload.processedFile || upload.originalFile;
-						if (fileToUpload) {
-							formData.append('files[]', fileToUpload);
-							const fileIndex = uploadMap.length;
-							uploadMap.push(upload.id);
-							groupUploadIndices.push(upload.id);
-
-							// Check if this is the featured image
-							const radioInput = upload.element?.querySelector('[name="featured"]');
-							if (radioInput?.checked) {
-								featuredIndex = upload.id;
-							}
-						}
-					}
-				});
-
-				groups.push(groupUploadIndices);
-				titles.push(group.title || '');
-				featuredImages.push(featuredIndex);
-			});
-
-			formData.append('groups', JSON.stringify(groups));
-			formData.append('group_titles', JSON.stringify(titles));
-			formData.append('featured_images', JSON.stringify(featuredImages));
-		} else {
-			// No groups - just append all files
-			uploads.forEach(uploadId => {
-				let upload = this.uploads.get(uploadId);
-				if (upload) {
-					const fileToUpload = upload.processedFile || upload.originalFile;
-					if (fileToUpload) {
-						formData.append('files[]', fileToUpload);
-						uploadMap.push(upload.id);
-					}
-				}
-			});
-		}
-		formData.append('upload_ids', JSON.stringify(uploadMap));
-
-		// console.log('Final FormData:');
-		// for (let pair of formData.entries()) {
-		// 	console.log(pair[0], pair[1]);
-		// }
-
-		return formData;
-	}
-
-	getFieldGroups(fieldId) {
-		const groups = [];
-
-		this.groups.forEach((groupData, groupId) => {
-			if (groupData.fieldId === fieldId) {
-				const field = this.fields.get(fieldId);
-				const groupElement = field?.ui?.groups?.groups?.get(groupId);
-
-				groups.push({
-					id: groupId,
-					uploads: Array.from(groupData.uploads || new Set()),
-					meta: this.groupsMeta.get(groupId) || {},
-					element: groupElement || null
-				});
-			}
-		});
-
-		return groups;
-	}
-
-	/**
-	 * Build groups data from field state
-	 */
-	buildGroupsData(field, uploads) {
-		const groups = [];
-		const titles = [];
-		const uploadMap = [];
-
-		if (field.groups && field.groups.length > 0) {
-			// User has explicitly created groups
-			field.groups.forEach(group => {
-				const groupUploads = [];
-				group.uploads.forEach(uploadId => {
-					groupUploads.push(uploadId);
-					uploadMap.push(uploadId);
-				});
-				groups.push(groupUploads);
-				titles.push(group.title || '');
-			});
-		} else {
-			// No explicit groups - treat all as one group
-			const allUploads = [];
-			uploads.forEach(uploadId => {
-				allUploads.push(uploadId);
-				uploadMap.push(uploadId);
-			});
-			groups.push(allUploads);
-			titles.push('');
-		}
-
-		return { groups, titles, uploadMap };
-	}
-
-	async queueImageMeta(e) {
-		const upload = this.getUploadFromElement(element);
-		if (!upload) return;
-
-		const field = this.fields.get(upload.fieldId);
-		if (!field) return;
-
-		// Collect meta data from the form
-		const metaContainer = element.closest('.upload-meta');
-		if (!metaContainer) return;
-
-		const metaData = {
-			title: metaContainer.querySelector('[name="title"]')?.value || '',
-			alt_text: metaContainer.querySelector('[name="alt_text"]')?.value || '',
-			caption: metaContainer.querySelector('[name="caption"]')?.value || '',
-			description: metaContainer.querySelector('[name="description"]')?.value || ''
-		};
-
-		// Update upload meta
-		upload.meta = { ...upload.meta, ...metaData };
-		this.uploads.set(upload.id, upload);
-
-		// Mark that we have meta changes
-		this.hasMetaChanges = true;
-
-		// Determine if upload has been sent to server
-		const isOnServer = upload.status === 'completed' && upload.attachmentId;
-
-		if (isOnServer) {
-			// Queue immediate update
-			await this.sendMetaUpdate(upload);
-		} else if (upload.operationId) {
-			// Wait for upload to complete, then send meta
-			this.queueDependentMetaUpdate(upload);
-		} else {
-			// Upload hasn't been queued yet, meta will be sent with initial upload
-			this.persistFieldState(field.key);
-		}
-	}
-
-	/**
-	 * Send meta update to server
-	 */
-	async sendMetaUpdate(upload) {
-		const formData = new FormData();
-		formData.append('attachment_id', upload.attachmentId);
-		formData.append('title', upload.meta.title);
-		formData.append('alt_text', upload.meta.alt_text);
-		formData.append('caption', upload.meta.caption);
-		formData.append('description', upload.meta.description);
-		//TODO:
-		// Send an array of attachment IDs with the changes, similar to the post editing logic
-		/**
-		 *  let data = {
-		 *  	items: {
-		 *      	uploadID: {
-		 *          	title: '',
-		 *          	alt: '',
-		 *          	caption: '',
-		 *         		depends_on: ''  <-- only necessary if uploadID is the generated upload_id
-		 *      	}
-		 *  	},
-		 *  	user: userID
-		 *  }
-		 *
-		 *  WHERE uploadID = attachment_id (if already uploaded) or our generated upload_id if the file hasn't been processed yet
-		 *
-		 */
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			data: formData,
-			title: `Updating metadata for ${upload.meta.originalName}`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			}
-		};
-
-		try {
-			await this.queue.addToQueue(operation);
-			// this.notifications.add('Metadata updated', 'success');
-		} catch (error) {
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'sendMetaUpdate',
-				uploadId: upload.id
-			});
-		}
-	}
-
-	/**
-	 * Queue meta update that depends on upload completion
-	 */
-	queueDependentMetaUpdate(upload) {
-		const operation = {
-			endpoint: 'uploads/meta',
-			method: 'POST',
-			dependencies: [upload.operationId],
-			data: () => {
-				// This function will be called when dependencies are resolved
-				const formData = new FormData();
-				formData.append('operation_id', upload.operationId);
-				formData.append('upload_id', upload.id);
-				formData.append('title', upload.meta.title);
-				formData.append('alt_text', upload.meta.alt_text);
-				formData.append('caption', upload.meta.caption);
-				formData.append('description', upload.meta.description);
-				return formData;
-			},
-			title: `Updating metadata after upload`,
-			canMerge: true,
-			headers: {
-				'action_nonce': jvbSettings.dash
-			}
-		};
-
-		this.queue.addToQueue(operation);
-	}
 	/*******************************************************************************
-	 IMAGE PROCESSING
-	*******************************************************************************/
-	async processFiles(fieldId, files) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
+	 * FILE PROCESSING
+	 *******************************************************************************/
 
-		// Hide upload container, show group display
-		if (field.ui.field.dropZone) {
-			field.ui.field.dropZone.hidden = true;
+	async processFiles(fieldId, files) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData || !fieldEl) return;
+
+		// Show group display, hide upload zone
+		if (fieldEl.ui.dropZone) {
+			fieldEl.ui.dropZone.hidden = true;
 		}
-		if (field.ui.groups.display) {
-			field.ui.groups.display.hidden = false;
+		if (fieldEl.ui.groups?.display) {
+			fieldEl.ui.groups.display.hidden = false;
 		}
 
 		const totalFiles = files.length;
 		let processedCount = 0;
 
-		// Show initial progress
 		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
 
-		// Initialize field uploads set if needed
-		if (!field.uploads) {
-			field.uploads = new Set();
-		}
-
-		// Process files
-		const processPromises = Array.from(files).map(async (file, index) => {
+		const processPromises = Array.from(files).map(async (file) => {
 			try {
-				// Create upload ID
 				const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
 
-				// Create upload data
+				// Create initial upload data
 				const uploadData = {
 					id: uploadId,
+					attachmentId: null,
 					fieldId: fieldId,
-					originalFile: file,
-					processedFile: null,
-					preview: null,
 					status: 'local_processing',
-					element: null,
-					location: null,
+					groupId: null,
 					meta: {
 						originalName: file.name,
 						size: file.size,
@@ -1716,56 +687,61 @@
 					}
 				};
 
-				// Create preview URL
-				uploadData.preview = URL.createObjectURL(file);
+				// Save initial data
+				await this.uploadStore.save(uploadData);
 
-				// Process the file (resize if image)
-				if (file.type.startsWith('image/')) {
-					uploadData.processedFile = await this.processImage(file, field.subtype);
-				} else {
-					uploadData.processedFile = file;
-				}
+				// Process file
+				const preview = this.createPreviewUrl(file);
+				const processedFile = file.type.startsWith('image/')
+					? await this.processImage(file, fieldData.config.subtype)
+					: file;
 
-				// Store blob data separately in IndexedDB
-				if (this.db) {
-					try {
-						await this.storeBlobData(uploadId, uploadData.processedFile || file);
-					} catch (error) {
-						console.warn('Failed to store blob data:', error);
-					}
-				}
-
-				// Create DOM element
-				const subtype = this.getSubtypeFromMime(file.type);
-				uploadData.element = this.createImageElement({
-					...uploadData,
-					subtype: subtype
-				}, field.destination === 'post_group');
-
-				// Show progress on the item
+				// Show progress
 				this.showUploadProgress(uploadId, true);
 				this.updateUploadItemProgress(uploadId, 50, 'local_processing');
 
+				// Store blob data (this updates the existing uploadData)
+				await this.saveBlobData(uploadId, processedFile || file);
+
+				// Create DOM element
+				const subtype = this.getSubtypeFromMime(file.type);
+				const element = this.createUploadElement({
+					id: uploadId,
+					preview: preview,
+					meta: uploadData.meta,
+					subtype: subtype
+				}, fieldData.config.destination === 'post_group');
+
 				// Add to preview grid
-				if (field.ui.field.preview) {
-					field.ui.field.preview.appendChild(uploadData.element);
-					uploadData.location = field.ui.field.preview;
+				if (fieldEl.ui.preview) {
+					fieldEl.ui.preview.appendChild(element);
+
+					// Store runtime element data
+					this.uploadElements.set(uploadId, {
+						element: element,
+						preview: preview,
+						location: fieldEl.ui.preview
+					});
 				}
 
-				// Store upload
-				this.uploads.set(uploadId, uploadData);
-				field.uploads.add(uploadId);
+				// Update status (gets existing data with blobData intact)
+				const storedUpload = this.uploadStore.get(uploadId);
+				if (storedUpload) {
+					storedUpload.status = 'processed';
+					await this.uploadStore.save(storedUpload);
+				}
+
+				// Add to field
+				fieldData.uploads.add(uploadId);
+				await this.saveFieldData(fieldData);
 
 				// Update progress
 				processedCount++;
 				this.updateUploadProgress(fieldId, processedCount, totalFiles, 'Processing files...');
 				this.updateUploadItemProgress(uploadId, 100, 'processed');
-				uploadData.status = 'processed';
 
-				// Fade out item progress after a moment
-				setTimeout(() => {
-					this.showUploadProgress(uploadId, false);
-				}, 1000);
+				// Fade out progress
+				setTimeout(() => this.showUploadProgress(uploadId, false), 1000);
 
 				return uploadId;
 
@@ -1777,313 +753,21 @@
 			}
 		});
 
-		// Wait for all files to process
 		await Promise.all(processPromises);
 
 		this.updateFieldState(fieldId);
-		// Cache the state (now without DOM references)
-		await this.persistFieldState(fieldId);
+		this.refreshSortable(fieldId);
 
 		// Queue for upload if in direct mode
-		if (field.mode === 'direct' && field.destination !== 'post_group') {
+		if (fieldData.config.autoUpload && fieldData.config.destination !== 'post_group') {
 			await this.queueUpload(fieldId);
-		}
-
-		// Lock uploads if max reached
-		this.maybeLockUploads(fieldId);
-	}
-
-	updateFieldState(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.ui.field.field) return;
-
-		const container = field.ui.field.field;
-		const uploadCount = field.uploads?.size || 0;
-		const hasGroups = field.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
-
-		// Set data attributes for CSS targeting
-		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
-		container.dataset.uploadCount = uploadCount.toString();
-		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
-
-		// Update ARIA labels for accessibility
-		if (field.ui.field.preview) {
-			field.ui.field.preview.setAttribute('aria-label',
-				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
-			);
+			this.maybeLockUploads(fieldId);
 		}
 	}
 
-	/**
-	 * Store file blob data in IndexedDB
-	 */
-	async storeBlobData(uploadId, file) {
-		if (!this.db) return;
-
-		const blobData = {
-			uploadId: uploadId,
-			data: file,
-			name: file.name,
-			type: file.type,
-			lastModified: file.lastModified,
-			timestamp: Date.now()
-		};
-
-		try {
-			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
-			await tx.objectStore('uploadBlobs').put(blobData);
-		} catch (error) {
-			console.error('Failed to store blob data:', error);
-			throw error;
-		}
-	}
-
-	/**
-	 * Show/hide progress indicator on individual upload items
-	 */
-	showUploadProgress(uploadId, show = true) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload || !upload.element) return;
-
-		const progressEl = upload.element.querySelector('.progress');
-		if (progressEl) {
-			if (show) {
-				progressEl.style.removeProperty('animation');
-				progressEl.hidden = false;
-			} else {
-				progressEl.style.animation = 'fadeOut var(--transition-base)';
-				setTimeout(() => {
-					progressEl.hidden = true;
-				}, 300);
-			}
-		}
-	}
-
-	/**
-	 * Update individual upload progress bar
-	 */
-	updateUploadItemProgress(uploadId, percent, status = null) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload || !upload.element) return;
-
-		const progressEl = upload.element.querySelector('.progress');
-		if (!progressEl) return;
-
-		const fill = progressEl.querySelector('.fill');
-		const details = progressEl.querySelector('.details');
-		const icon = progressEl.querySelector('.icon');
-
-		if (fill) {
-			fill.style.width = `${percent}%`;
-		}
-
-		if (status && details) {
-			details.textContent = this.getStatusText(status);
-		}
-
-		if (status && icon) {
-			icon.innerHTML = this.getStatusIcon(status).outerHTML;
-		}
-	}
-	checkFieldLimits(fieldId, additionalFiles) {
-		const field = this.fields.get(fieldId);
-		if (!field) return false;
-
-		const currentCount = field.uploads?.size || 0;
-		const totalCount = currentCount + additionalFiles;
-
-		if (totalCount > field.maxFiles) {
-			// this.notifications.add(
-			// 	`Cannot add ${additionalFiles} files. Max ${field.maxFiles} allowed, currently have ${currentCount}.`,
-			// 	'warning'
-			// );
-			return false;
-		}
-
-		return true;
-	}
-	generateUploadId() {
-		return `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-	}
-	validateFile(file, field) {
-		// Type validation
-		if (!this.settings.allowedTypes.includes(file.type)) {
-			this.notify(`Invalid file type: ${file.type}`, 'error');
-			return false;
-		}
-
-		// Size validation
-		if (file.size > this.settings.maxFileSize) {
-			this.notify(`File too large: ${this.formatBytes(file.size)}`, 'error');
-			return false;
-		}
-
-		return true;
-	}
-
-	formatBytes(bytes, decimals = 2) {
-		if (bytes === 0) return '0 Bytes';
-
-		const k = 1024;
-		const dm = decimals < 0 ? 0 : decimals;
-		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
-
-		const i = Math.floor(Math.log(bytes) / Math.log(k));
-
-		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
-	}
-
-	shouldProcessClientSide(file, subtype) {
-		// Only process images client-side
-		if (subtype === 'image' && file.type.startsWith('image/')) {
-			return true;
-		}
-
-		// Videos and documents go straight to server
-		return false;
-	}
-
-	async processBatch(fieldId, files) {
-		const results = [];
-		const processingQueue = [];
-		const maxConcurrent = this.worker.settings.maxConcurrent;
-
-		let total = files.length;
-		let processedCount = 0;
-
-		// Show initial progress
-		this.updateUploadProgress(fieldId, 0, totalFiles, 'Processing files...');
-		let field = this.fields.get(fieldId);
-		// Initialize field uploads set if needed
-		if (!field.uploads) {
-			field.uploads = new Set();
-		}
-
-
-		for (let i = 0; i < files.length; i++) {
-			this.showUploadProgress(uploadId, true);
-			this.updateUploadProgress(fieldId, i, total);
-			// Wait if we've reached max concurrent processing
-			if (processingQueue.length >= maxConcurrent) {
-				await Promise.race(processingQueue);
-			}
-
-			const processPromise = this.processFile(files[i], field)
-				.then(upload => {
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-
-					if (upload) results.push(upload);
-					return upload;
-				})
-				.catch(error => {
-					console.error(`Failed to process ${files[i].name}:`, error);
-					// Remove from processing queue
-					const index = processingQueue.indexOf(processPromise);
-					if (index > -1) processingQueue.splice(index, 1);
-					return null;
-				});
-
-			processingQueue.push(processPromise);
-		}
-
-		// Wait for remaining files
-		await Promise.all(processingQueue);
-		return results;
-	}
-
-	async processFile(file, field, uploadId = null) {
-		if (!field || !file) {
-			console.error('Missing required parameters:', { file, field });
-			return null;
-		}
-
-		if (!this.shouldProcessClientSide(file, field.subtype)) {
-			return upload;
-		}
-
-		const id = uploadId || `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
-		try {
-			// Create upload object
-			const upload = {
-				id,
-				fieldId: field.key,
-				originalFile: file,
-				processedFile: null,
-				preview: null,
-				status: 'local_processing',
-				element: null,
-				location: null,
-				groupId: null,
-				changes: {},
-				meta: {
-					originalName: file.name,
-					size: file.size,
-					type: file.type
-				}
-			};
-
-			// Create preview URL
-			upload.preview = URL.createObjectURL(file);
-
-			// Process the file
-			let processedFile = null;
-			let processingFailed = false;
-
-			if (file.type.startsWith('image/')) {
-				try {
-					processedFile = await this.processImage(file, id);
-				} catch (error) {
-					console.warn(`Image processing failed for ${file.name}, using original:`, error);
-					processingFailed = true;
-					processedFile = file;
-				}
-			} else {
-				processedFile = file; // Videos/documents use original
-			}
-
-			upload.processedFile = processedFile;
-			upload.processingFailed = processingFailed;
-
-			// Store in uploads map
-			this.uploads.set(id, upload);
-
-			// Add to field's uploads
-			if (!field.uploads) {
-				field.uploads = new Set();
-			}
-			field.uploads.add(id);
-
-			// Update status
-			this.updateUploadStatus(id, 'processed');
-
-			// Persist state
-			await this.persistFieldState(field.key);
-
-			// Announce to screen readers
-			const message = processingFailed
-				? `${file.name} added (original format)`
-				: `${file.name} processed and ready`;
-			this.a11y.announce(message);
-
-			return upload;
-
-		} catch (error) {
-			// Clean up failed upload
-			this.cleanupFailedUpload(id, field.key);
-
-			this.error.log(error, {
-				component: 'UploadManager',
-				action: 'processFile',
-				uploadId: id,
-				fileName: file.name
-			});
-
-			return null;
-		}
-	}
+	/*******************************************************************************
+	 * IMAGE PROCESSING
+	 *******************************************************************************/
 
 	async processImage(file, uploadId) {
 		const timeout = this.worker.settings.timeout;
@@ -2092,27 +776,19 @@
 			let timeoutId;
 			let taskCompleted = false;
 
-			// Set timeout
 			timeoutId = setTimeout(() => {
 				if (!taskCompleted) {
 					taskCompleted = true;
-
-					// Remove from active tasks
 					this.worker.tasks.delete(uploadId);
-
-					// Maybe restart worker if configured
 					if (this.worker.settings.restartAfterTimeout) {
 						this.restartCompressionWorker();
 					}
-
 					reject(new Error(`Processing timeout for ${file.name}`));
 				}
 			}, timeout);
 
-			// Track this task
 			this.worker.tasks.set(uploadId, { file, timeoutId });
 
-			// Process image
 			this.handleProcess(file, uploadId)
 				.then(result => {
 					if (!taskCompleted) {
@@ -2134,7 +810,6 @@
 	}
 
 	async handleProcess(file, uploadId) {
-		// Skip non-images
 		if (!file.type.startsWith('image/')) {
 			return file;
 		}
@@ -2142,14 +817,11 @@
 		const maxDimension = this.getMaxDimension();
 		const quality = 0.85;
 
-		// Try worker first if available
 		if (this.shouldUseWorker(file)) {
 			try {
-				// Ensure worker is initialized
 				if (!this.worker.worker) {
 					this.initCompressionWorker();
 				}
-
 				if (this.worker.worker) {
 					return await this.processWithWorker(file, uploadId, maxDimension, quality);
 				}
@@ -2158,13 +830,9 @@
 			}
 		}
 
-		// Fallback to main thread
 		return await this.processOnMainThread(file, maxDimension, quality);
 	}
 
-	/**
-	 * Process image on main thread with better error handling
-	 */
 	async processOnMainThread(file, maxDimension, quality) {
 		return new Promise((resolve, reject) => {
 			const img = new Image();
@@ -2179,7 +847,6 @@
 					URL.revokeObjectURL(objectUrl);
 					objectUrl = null;
 				}
-				// Explicitly clean up canvas
 				canvas.width = 1;
 				canvas.height = 1;
 				ctx.clearRect(0, 0, 1, 1);
@@ -2191,7 +858,6 @@
 					canvas.width = width;
 					canvas.height = height;
 
-					// Enhanced image smoothing
 					ctx.imageSmoothingEnabled = true;
 					ctx.imageSmoothingQuality = 'high';
 					ctx.drawImage(img, 0, 0, width, height);
@@ -2229,7 +895,7 @@
 			};
 
 			try {
-				objectUrl = URL.createObjectURL(file);
+				objectUrl = this.createPreviewUrl(file);
 				img.src = objectUrl;
 			} catch (error) {
 				cleanup();
@@ -2238,67 +904,41 @@
 		});
 	}
 
-	/**
-	 * Get optimal output format
-	 */
 	getOptimalFormat(file) {
-		// Keep original format for certain types
 		if (file.type === 'image/gif' || file.type === 'image/svg+xml') {
 			return file.type;
 		}
-
-		// Use WebP if supported, otherwise JPEG
 		return this.supportsWebP() ? 'image/webp' : 'image/jpeg';
 	}
 
-	/**
-	 * Get optimal quality setting
-	 */
 	getOptimalQuality(file, requestedQuality) {
-		// Higher quality for smaller files
 		if (file.size < 500 * 1024) return Math.max(requestedQuality, 0.9);
 		if (file.size < 2 * 1024 * 1024) return requestedQuality;
-
-		// Lower quality for very large files
 		return Math.min(requestedQuality, 0.8);
 	}
 
-	/**
-	 * Generate processed file name
-	 */
 	getProcessedFileName(originalFile, outputFormat) {
 		const baseName = originalFile.name.replace(/\.[^/.]+$/, '');
-
 		const extensions = {
 			'image/webp': '.webp',
 			'image/jpeg': '.jpg',
 			'image/png': '.png',
 			'image/gif': '.gif'
 		};
-
 		return baseName + (extensions[outputFormat] || '.jpg');
 	}
 
-	/**
-	 * Get maximum dimension based on device capabilities
-	 */
 	getMaxDimension() {
 		const screenWidth = window.screen.width;
 		const devicePixelRatio = window.devicePixelRatio || 1;
-
-		// Scale based on device capabilities
 		if (screenWidth * devicePixelRatio > 2560) return 2400;
 		if (screenWidth * devicePixelRatio > 1920) return 1920;
 		return 1200;
 	}
 
-	/**
-	 * Determine if we should use Web Worker
-	 */
 	shouldUseWorker(file) {
-		// Use worker for large files or when available
 		return this.worker.worker &&
-			file.size > 1024 * 1024 && // > 1MB
+			file.size > 1024 * 1024 &&
 			typeof OffscreenCanvas !== 'undefined';
 	}
 
@@ -2309,14 +949,11 @@
 				return;
 			}
 
-			// Create unique message ID for this task
 			const messageId = `${uploadId}_${Date.now()}`;
 
-			// Handler for this specific message
 			const messageHandler = (e) => {
 				if (e.data.messageId !== messageId) return;
 
-				// Remove handler
 				this.worker.worker.removeEventListener('message', messageHandler);
 				this.worker.worker.removeEventListener('error', errorHandler);
 
@@ -2338,11 +975,9 @@
 				reject(new Error(`Worker error: ${error.message}`));
 			};
 
-			// Add handlers
 			this.worker.worker.addEventListener('message', messageHandler);
 			this.worker.worker.addEventListener('error', errorHandler);
 
-			// Send message to worker
 			this.worker.worker.postMessage({
 				messageId,
 				file,
@@ -2353,88 +988,48 @@
 		});
 	}
 
-	/**
-	 * Restart compression worker
-	 */
 	restartCompressionWorker() {
-		// Terminate existing worker
 		if (this.worker.worker) {
 			this.worker.worker.terminate();
 			this.worker.worker = null;
 		}
-
-		// Clear active tasks
 		this.worker.tasks.clear();
-
-		// Check restart limit
 		if (this.worker.restart.count >= this.worker.restart.max) {
 			console.error('Max worker restarts reached, disabling worker');
 			return;
 		}
-
 		this.worker.restart.count++;
-
-		// Reinitialize
 		this.initCompressionWorker();
 	}
 
-	/**
-	 * Initialize Web Worker for image compression
-	 */
 	initCompressionWorker() {
 		if (this.worker.worker || typeof Worker === 'undefined') return;
 
 		try {
 			const workerScript = `
-            self.onmessage = async function(e) {
-                const { messageId, file, maxDimension, quality, outputFormat } = e.data;
-
-                try {
-                    // Create ImageBitmap from file
-                    const bitmap = await createImageBitmap(file);
-
-                    // Calculate dimensions
-                    const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
-                    const width = Math.round(bitmap.width * scale);
-                    const height = Math.round(bitmap.height * scale);
-
-                    // Create OffscreenCanvas
-                    const canvas = new OffscreenCanvas(width, height);
-                    const ctx = canvas.getContext('2d');
-
-                    // Draw and resize
-                    ctx.imageSmoothingEnabled = true;
-                    ctx.imageSmoothingQuality = 'high';
-                    ctx.drawImage(bitmap, 0, 0, width, height);
-
-                    // Clean up bitmap
-                    bitmap.close();
-
-                    // Convert to blob
-                    const blob = await canvas.convertToBlob({
-                        type: outputFormat,
-                        quality: quality
-                    });
-
-                    self.postMessage({
-                        messageId,
-                        success: true,
-                        blob: blob,
-                        format: outputFormat
-                    });
-
-                } catch (error) {
-                    self.postMessage({
-                        messageId,
-                        success: false,
-                        error: error.message
-                    });
-                }
-            };
-        `;
+				self.onmessage = async function(e) {
+					const { messageId, file, maxDimension, quality, outputFormat } = e.data;
+					try {
+						const bitmap = await createImageBitmap(file);
+						const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);
+						const width = Math.round(bitmap.width * scale);
+						const height = Math.round(bitmap.height * scale);
+						const canvas = new OffscreenCanvas(width, height);
+						const ctx = canvas.getContext('2d');
+						ctx.imageSmoothingEnabled = true;
+						ctx.imageSmoothingQuality = 'high';
+						ctx.drawImage(bitmap, 0, 0, width, height);
+						bitmap.close();
+						const blob = await canvas.convertToBlob({ type: outputFormat, quality: quality });
+						self.postMessage({ messageId, success: true, blob: blob, format: outputFormat });
+					} catch (error) {
+						self.postMessage({ messageId, success: false, error: error.message });
+					}
+				};
+			`;
 
 			const blob = new Blob([workerScript], { type: 'application/javascript' });
-			this.worker.worker = new Worker(URL.createObjectURL(blob));
+			this.worker.worker = new Worker(this.createPreviewUrl(blob));
 
 		} catch (error) {
 			console.warn('Failed to initialize compression worker:', error);
@@ -2442,241 +1037,1564 @@
 		}
 	}
 
-	/**
-	 * Calculate optimal dimensions with aspect ratio preservation
-	 */
 	calculateOptimalDimensions(img, maxDimension) {
 		let { width, height } = img;
-
-		// Don't upscale
 		if (width <= maxDimension && height <= maxDimension) {
 			return { width, height };
 		}
-
-		// Calculate scale factor
 		const scale = Math.min(maxDimension / width, maxDimension / height);
-
 		return {
 			width: Math.round(width * scale),
 			height: Math.round(height * scale)
 		};
 	}
 
-
-	/**
-	 * Check WebP support
-	 */
 	supportsWebP() {
 		const canvas = document.createElement('canvas');
 		return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
 	}
 
-	/**
-	 * Clean up failed upload
-	 */
-	cleanupFailedUpload(uploadId, fieldId) {
-		const field = this.fields.get(fieldId);
-		if (field?.uploads) {
-			field.uploads.delete(uploadId);
-		}
-
-		const upload = this.uploads.get(uploadId);
-		if (upload) {
-			// Clean up preview URL
-			if (upload.preview?.startsWith('blob:')) {
-				URL.revokeObjectURL(upload.preview);
-			}
-
-			// Remove element
-			upload.element?.remove();
-
-			// Remove from uploads
-			this.uploads.delete(uploadId);
-		}
-
-		// Remove from active tasks
-		this.worker.tasks.delete(uploadId);
+	createPreviewUrl(file) {
+		const url = URL.createObjectURL(file);
+		if (!this.previewUrls) this.previewUrls = new Set();
+		this.previewUrls.add(url);
+		return url;
 	}
+
+	revokePreviewUrl(url) {
+		if (url?.startsWith('blob:')) {
+			URL.revokeObjectURL(url);
+			this.previewUrls?.delete(url);
+		}
+	}
+
 	/*******************************************************************************
-	 UI FUNCTIONALITY
-	*******************************************************************************/
-	/**
-	 * Update upload status correctly
-	 */
-	updateUploadStatus(uploadId, status) {
-		let upload = this.uploads.get(uploadId);
-		if(!upload) {
+	 * QUEUE INTEGRATION
+	 *******************************************************************************/
+
+	async submitUploads(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData?.uploads || fieldData.uploads.size === 0) {
 			return;
 		}
-		upload.status = status;
 
-		this.updateImageUI(upload.id);
-		this.persistFieldState(upload.fieldId);
-	}
-	updateImageUI(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload?.element) return;
-
-
-		const progressEl = upload.element.querySelector('.progress');
-		const itemEl = upload.element;
-
-		// Update status class on item for CSS styling
-		if (itemEl) {
-			itemEl.className = itemEl.className.replace(/status-[\w-]+/g, '');
-			itemEl.classList.add(`status-${upload.status}`);
+		let uploadIds = Array.from(fieldData.uploads);
+		if (uploadIds.length === 0) {
+			this.error.log('No uploads to upload', {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			return;
 		}
 
-		if (progressEl) {
-			let icon = this.getStatusIcon(upload.status);
-			let message = this.getStatusText(upload.status);
-			let progress = this.getStatusProgress(upload.status);
+		const fieldGroups = this.getFieldGroups(fieldId);
 
-			const fill = progressEl.querySelector('.fill');
-			const itemIcon = progressEl.querySelector('span.icon');
-			const itemMessage = progressEl.querySelector('span.details');
+		if (fieldGroups.length === 0) {
+			this.error.log('No groups created for post_group upload', {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			return;
+		}
 
-			if (fill) {
-				fill.style.width = `${progress}%`;
-			}
-			if (itemMessage) itemMessage.textContent = message;
-			if (itemIcon) {
-				window.removeChildren(itemIcon);
-				itemIcon.append(icon);
+		// Build posts array from groups
+		const posts = [];
+		const formData = new FormData();
+		let uploadMap = [];
+
+		// Process each group
+		for (const group of fieldGroups) {
+			const post = {
+				images: [],
+				fields: {}
+			};
+
+			// Add group metadata
+			for (let [name, value] of Object.entries(group.changes)) {
+				post.fields[name] = value;
 			}
 
-			if (upload.status === 'completed') {
-				setTimeout(() => {
-					if (progressEl) {
-						window.fade(progressEl, false);
+			// Get uploads for this group
+			const groupUploadIds = uploadIds.filter(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				return upload?.groupId === group.id;
+			});
+
+			// Add files for this group
+			for (const uploadId of groupUploadIds) {
+				const file = await this.getBlobData(uploadId);
+				if (file) {
+					formData.append('files[]', file);
+
+					const imageData = {
+						upload_id: uploadId,
+						index: uploadMap.length
+					};
+
+					// Check if featured
+					const uploadEl = this.uploadElements.get(uploadId);
+					const radioInput = uploadEl?.element?.querySelector('[name="featured"]');
+					if (radioInput?.checked) {
+						post.fields.featured = uploadId;
 					}
-				}, 1000);
+
+					post.images.push(imageData);
+					uploadMap.push(uploadId);
+				}
 			}
+
+			posts.push(post);
+		}
+
+		// Handle remaining uploads (without groupId) - each becomes its own post
+		const remainingUploadIds = uploadIds.filter(uploadId => {
+			const upload = this.uploadStore.get(uploadId);
+			return !upload?.groupId;
+		});
+
+		for (const uploadId of remainingUploadIds) {
+			const post = {
+				images: [],
+				fields: {}
+			};
+
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				formData.append('files[]', file);
+
+				const imageData = {
+					upload_id: uploadId,
+					index: uploadMap.length
+				};
+				post.images.push(imageData);
+				uploadMap.push(uploadId);
+			}
+
+			posts.push(post);
+		}
+
+		// Add metadata to FormData
+		formData.append('content', fieldData.config.content);
+		formData.append('user', fieldData.config.itemID);
+		formData.append('posts', JSON.stringify(posts));
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+
+		const operation = {
+			endpoint: 'uploads/groups',
+			method: 'POST',
+			data: formData,
+			title: `Creating ${posts.length} ${fieldData.config.content}${posts.length > 1 ? 's' : ''} from uploads...`,
+			popup: `Creating ${posts.length} post${posts.length > 1 ? 's' : ''}...`,
+			canMerge: false,
+			headers: {
+				'action_nonce': window.auth.getNonce('dash')
+			},
+			append: '_upload',
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			// Update upload statuses
+			uploadIds.forEach(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				if (upload) {
+					upload.operationId = operationId;
+					upload.status = 'queued';
+					this.uploadStore.save(upload);
+					this.updateUploadStatus(uploadId, 'queued');
+				}
+			});
+
+			fieldData.operationId = operationId;
+			await this.saveFieldData(fieldData);
+
+			this.a11y.announce(`Creating ${posts.length} post${posts.length > 1 ? 's' : ''} from your uploads`);
+
+			return operationId;
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'submitGroupedUploads',
+				fieldId: fieldId
+			});
+			throw error;
 		}
 	}
-	/**
-	 * Hide the uploader drop zone if we have reached our limit
-	 */
-	maybeLockUploads(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
 
-		if (field.ui.field.dropZone) {
-			const hasUploads = field.uploads && field.uploads.size > 0;
-			const atMaxFiles = field.uploads && field.uploads.size >= field.maxFiles;
+	async queueUpload(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.uploads || fieldData.uploads.size === 0) return;
 
-			// Hide if we have uploads OR if we're at max files
-			field.ui.field.dropZone.hidden = hasUploads || atMaxFiles;
+		const uploads = Array.from(fieldData.uploads);
+		const data = this.prepareUploadData(fieldData, uploads);
+
+		this.a11y.announce('Queuing for upload');
+
+		const operation = {
+			endpoint: 'uploads',
+			method: 'POST',
+			data: data,
+			title: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''} to server...`,
+			popup: `Uploading ${uploads.length} file${uploads.length > 1 ? 's' : ''}...`,
+			canMerge: false,
+			headers: { 'action_nonce': window.auth.getNonce('dash') },
+			append: '_upload'
+		};
+
+		try {
+			const operationId = await this.queue.addToQueue(operation);
+
+			// Update upload statuses
+			uploads.forEach(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				if (upload) {
+					upload.operationId = operationId;
+					upload.status = 'queued';
+					this.uploadStore.save(upload);
+					this.updateUploadStatus(uploadId, 'queued');
+				}
+			});
+
+			fieldData.operationId = operationId;
+			await this.saveFieldData(fieldData);
+
+			return operationId;
+		} catch (error) {
+			throw error;
 		}
 	}
-	createImageElement(upload, draggable = false) {
-		let image = window.getTemplate('uploadItem');
-		if (!image) {
-			console.error('Image template not found');
-			return;
-		}
-		image.dataset.uploadId = upload.id;
-		if (upload.originalFile) {
-			image.dataset.subtype = this.getSubtypeFromMime(upload.originalFile.type);
-		}
+
+	async prepareUploadData(fieldData, uploads) {
+		const formData = new FormData();
+		formData.append('content', fieldData.config.content);
+		formData.append('mode', fieldData.config.mode);
+		formData.append('field_name', fieldData.config.name);
+		formData.append('fieldId', fieldData.id);
+		formData.append('field_type', fieldData.config.type);
+		formData.append('subtype', fieldData.config.subtype);
+		formData.append('item_id', fieldData.config.itemID);
+		formData.append('destination', fieldData.config.destination || 'meta');
+
+		let uploadMap = [];
 
 
-		image.querySelector('[name="featured"]').value = upload.id;
-		let [
-			featured,
-			img,
-			video,
-			preview,
-			details
-		] = [
-			image.querySelector('[name="featured"]'),
-			image.querySelector('img'),
-			image.querySelector('video'),
-			image.querySelector('label > span'),
-			image.querySelector('details')
-		];
-		[
-			featured.value,
-			img.src,
-			img.alt
-		] = [
-			upload.id,
-			upload.preview,
-			upload.originalFile?.name ?? upload.meta?.originalName ?? '',
-		];
+		const blobPromises = uploads.map(async (uploadId) => {
+			const upload = this.uploadStore.get(uploadId);
+			if (!upload) return;
 
-		switch (image.dataset.subtype) {
-			case 'image':
-				[
-					img.src,
-					img.alt
-				] = [
-					upload.preview,
-					upload.originalFile?.name ?? upload.meta?.originalName?? ''
-				];
-				video.remove();
-				preview.remove();
-				break;
-			case 'video':
-				video.src = upload.preview;
-				img.remove();
-				preview.remove();
-				break;
-			case 'document':
-				const fileName = upload.originalFile?.name ?? upload.meta?.originalName ?? '';
-				const extension = fileName.split('.').pop()?.toLowerCase() ?? '';
-				let icon;
-				switch (extension) {
-					case 'pdf':
-						icon = window.getIcon('file-pdf');
-						break;
-					case 'csv':
-						icon = window.getIcon('file-csv');
-						break;
-					case 'doc':
-						icon = window.getIcon('file-doc');
-						break;
-					case 'txt':
-						icon = window.getIcon('file-txt');
-						break;
-					case 'xls':
-						icon = window.getIcon('file-xls');
-						break;
-					default:
-						icon = window.getIcon('file');
-						break;
-				}
-
-				preview.innerText = upload.originalFile.name;
-				preview.prepend(icon);
-				img.remove();
-				video.remove();
-				break;
-		}
-		if (details) {
-			let template = window.getTemplate('uploadMeta');
-			if (template){
-				details.append(template);
-			}
-		}
-		image.draggable = draggable;
-
-		// Update input IDs safely
-		image.querySelectorAll('input').forEach(input => {
-			let id = input.id;
-			if (id) {
-				let newId = id + upload.id;
-				let label = input.parentNode.querySelector(`label[for="${id}"]`);
-				input.id = newId;
-				if (label) {
-					label.htmlFor = newId;
-				}
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				formData.append('files[]', file);
+				uploadMap.push(upload.id);
 			}
 		});
 
-		return image;
+		await Promise.all(blobPromises);
+
+		formData.append('upload_ids', JSON.stringify(uploadMap));
+		return formData;
 	}
 
+	async queueUploadMeta(e) {
+		const uploadId = this.getUploadIdFromElement(e.target);
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload) return;
+
+		const fieldData = this.getFieldData(upload.fieldId);
+		if (!fieldData) return;
+
+		let data = {};
+		data[e.target.name] = e.target.value;
+
+		upload.meta = { ...upload.meta, ...data };
+		await this.uploadStore.save(upload);
+
+		let queueData = {};
+		queueData[upload.attachmentId ?? upload.id] = upload.meta;
+
+		const operation = {
+			endpoint: 'uploads/meta',
+			method: 'POST',
+			data: queueData,
+			title: 'Updating meta',
+			canMerge: true,
+			headers: { 'action_nonce': window.auth.getNonce('dash') }
+		};
+
+		try {
+			await this.queue.addToQueue(operation);
+		} catch (error) {
+			this.error.log(error, {
+				component: 'UploadManager',
+				action: 'sendMetaUpdate',
+				uploadId: upload.id
+			});
+		}
+	}
+
+	/*******************************************************************************
+	 * QUEUE EVENT HANDLERS - CLEANUP AFTER SUCCESS
+	 *******************************************************************************/
+
+	/**
+	 * Handle successful operation completion - CLEAR STORES
+	 */
+	async handleOperationComplete(operation, fieldId) {
+		const results = operation.result?.data || operation.serverData?.data || [];
+
+		// Update upload statuses with attachment IDs
+		results.forEach(result => {
+			const upload = this.uploadStore.get(result.upload_id);
+			if (upload) {
+				upload.attachmentId = result.attachment_id;
+				upload.status = 'completed';
+				this.uploadStore.save(upload);
+				this.updateUploadStatus(result.upload_id, 'completed');
+			}
+		});
+
+		if (!fieldId) return;
+
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		// Clean up completed uploads from stores
+		const completedUploads = Array.from(fieldData.uploads).filter(uploadId => {
+			const upload = this.uploadStore.get(uploadId);
+			return upload?.status === 'completed';
+		});
+
+		for (const uploadId of completedUploads) {
+			await this.clearUpload(uploadId, false);
+			fieldData.uploads.delete(uploadId);
+		}
+
+		// If all uploads complete, clear entire field from stores
+		if (fieldData.uploads.size === 0) {
+			await this.clearFieldFromStores(fieldId);
+			this.a11y.announce('All uploads completed successfully');
+		} else {
+			// Otherwise just update field state
+			await this.saveFieldData(fieldData);
+		}
+
+		this.updateFieldState(fieldId);
+	}
+
+	/**
+	 * Handle operation failure
+	 */
+	handleOperationFailed(operation, fieldId) {
+		const uploadIds = operation.data instanceof FormData
+			? JSON.parse(operation.data.get('upload_ids') || '[]')
+			: operation.data.upload_ids || [];
+
+		uploadIds.forEach(uploadId => {
+			const upload = this.uploadStore.get(uploadId);
+			if (upload) {
+				upload.status = operation.status === 'operation-failed-permanent'
+					? 'failed_permanent'
+					: 'failed';
+				this.uploadStore.save(upload);
+				this.updateUploadStatus(uploadId, upload.status);
+			}
+		});
+
+		if (fieldId) {
+			this.updateFieldState(fieldId);
+		}
+	}
+
+	/**
+	 * Handle operation cancellation
+	 */
+	async handleOperationCancelled(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		const uploadsArray = fieldData.uploads instanceof Set
+			? Array.from(fieldData.uploads)
+			: fieldData.uploads;
+
+		for (const uploadId of uploadsArray) {
+			await this.clearUpload(uploadId, false);
+		}
+
+		await this.clearFieldFromStores(fieldId);
+		this.updateFieldState(fieldId);
+		this.a11y.announce('Upload cancelled');
+	}
+
+	getFieldGroups(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.groups) return [];
+
+		return fieldData.groups.map(group => ({
+			id: group.id,
+			uploads: group.uploads || [],
+			changes: group.changes || {}
+		}));
+	}
+
+	getSelectedRestorationUploads(notificationEl) {
+		let selected = [];
+		const checkboxes = notificationEl.querySelectorAll('[type=checkbox]:checked');
+
+		checkboxes.forEach(checkbox => {
+			const item = checkbox.closest('.item');
+			if (item) {
+				selected.push({
+					uploadId: item.dataset.uploadId,
+					fieldId: item.dataset.fieldId
+				});
+			}
+		});
+
+		return selected;
+	}
+
+	async restoreSelectedUploads(selectedUploads) {
+		const byField = new Map();
+		selectedUploads.forEach(item => {
+			if (!byField.has(item.fieldId)) {
+				byField.set(item.fieldId, []);
+			}
+			byField.get(item.fieldId).push(item.uploadId);
+		});
+
+		for (const [fieldId, uploadIds] of byField.entries()) {
+			const fieldState = this.fieldStore.get(fieldId);
+			if (fieldState) {
+				fieldState.uploads = uploadIds;
+				await this.restoreField(fieldState);
+			}
+		}
+	}
+
+	async restoreField(fieldState) {
+		const { config, context, uploads, groups, id } = fieldState;
+
+		// If in a modal, open it first
+		if (context?.modalType) {
+			await this.openModalForRestore(context);
+		}
+
+		// Find field element
+		let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
+
+		if (!fieldElement) {
+			const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
+			fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
+		}
+
+		if (!fieldElement) {
+			console.warn(`Field ${config.name} not found for restoration`, config);
+			return;
+		}
+
+		// Register the field if not already registered
+		let fieldKey = fieldElement.dataset.uploader;
+		if (!fieldKey || !this.fieldElements.has(fieldKey)) {
+			fieldKey = this.registerUploader(fieldElement);
+		}
+
+		const fieldEl = this.fieldElements.get(fieldKey);
+		const fieldData = this.getFieldData(fieldKey);
+
+		if (!fieldEl || !fieldData) {
+			console.error('Failed to register field for restoration');
+			return;
+		}
+
+		// Merge saved state back into field
+		fieldData.state = fieldState.state || 'ready';
+
+		// Rebuild UI references if needed
+		if (!fieldEl.ui) {
+			fieldEl.ui = this.buildFieldUI(fieldElement);
+		}
+
+		if (fieldEl.ui.groups?.display) {
+			fieldEl.ui.groups.display.hidden = false;
+		}
+		if (fieldEl.ui.dropZone) {
+			fieldEl.ui.dropZone.hidden = true;
+		}
+
+		// Restore groups first
+		if (groups && groups.length > 0) {
+			await this.restoreGroups(fieldKey, groups);
+		}
+
+		// Handle both Array and Set for uploads
+		const uploadsArray = uploads instanceof Set
+			? Array.from(uploads)
+			: Array.isArray(uploads)
+				? uploads
+				: [];
+
+		// Restore uploads
+		for (const uploadId of uploadsArray) {
+			// Get upload data from store
+			const uploadData = this.uploadStore.get(uploadId);
+			if (uploadData) {
+				await this.restoreUpload(fieldKey, uploadData);
+			}
+		}
+
+		// Update field state
+		await this.saveFieldData(fieldData);
+		this.updateFieldState(fieldKey);
+		this.maybeLockUploads(fieldKey);
+		this.refreshSortable(fieldKey);
+
+		// Queue for upload if needed
+		console.log(config);
+		if (config.autoUpload && config.mode === 'direct' && config.destination !== 'post_group') {
+			await this.queueUpload(fieldKey);
+		}
+	}
+
+	async restoreUpload(fieldId, uploadData) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+
+		if (!fieldEl || !fieldData) {
+			console.error('Field not found for upload restoration:', fieldId);
+			return;
+		}
+
+		// Get reconstructed File from blob data
+		const file = await this.getBlobData(uploadData.id);
+
+		if (!file) {
+			console.warn('Blob data not found for upload:', uploadData.id);
+			return;
+		}
+
+		// Create preview URL
+		const previewUrl = this.createPreviewUrl(file);
+
+		// Recreate DOM element
+		const subtype = this.getSubtypeFromMime(file.type);
+		const element = this.createUploadElement({
+			id: uploadData.id,
+			preview: previewUrl,
+			meta: uploadData.meta || {
+				originalName: file.name,
+				size: file.size,
+				type: file.type
+			},
+			subtype: subtype
+		}, fieldData.config.destination === 'post_group');
+
+		// Determine correct location
+		let location;
+		if (uploadData.groupId) {
+			// Check if group exists
+			const groupEl = this.groupElements.get(uploadData.groupId);
+			if (groupEl?.grid) {
+				location = groupEl.grid;
+
+				// Add to group's upload list
+				const group = fieldData.groups?.find(g => g.id === uploadData.groupId);
+				if (group) {
+					if (!group.uploads) group.uploads = [];
+					if (!group.uploads.includes(uploadData.id)) {
+						group.uploads.push(uploadData.id);
+					}
+				}
+			} else {
+				// Group doesn't exist, add to preview
+				location = fieldEl.ui.preview;
+				uploadData.groupId = null;
+			}
+		} else {
+			// No group, add to preview
+			location = fieldEl.ui.preview;
+		}
+
+		// Add element to DOM
+		if (location) {
+			location.appendChild(element);
+		} else if (fieldEl.ui.preview) {
+			fieldEl.ui.preview.appendChild(element);
+			location = fieldEl.ui.preview;
+		}
+
+		// Store runtime element data
+		this.uploadElements.set(uploadData.id, {
+			element: element,
+			preview: previewUrl,
+			location: location
+		});
+
+		// Add to field uploads
+		if (!fieldData.uploads) fieldData.uploads = new Set();
+		fieldData.uploads.add(uploadData.id);
+
+		// Update upload data in store
+		uploadData.status = 'processed';
+		await this.uploadStore.save(uploadData);
+
+		// Update sortable state for the grid
+		if (location) {
+			this.updateSortableState(location);
+		}
+	}
+
+	async restoreGroups(fieldId, groups) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+
+		if (!fieldEl || !fieldData) {
+			console.error('Field not found for group restoration:', fieldId);
+			return;
+		}
+
+		for (const groupData of groups) {
+			const group = this.createGroup(fieldId, groupData.id);
+			if (!group) {
+				console.warn('Failed to create group:', groupData.id);
+				continue;
+			}
+
+			const storedGroup = fieldData.groups?.find(g => g.id === groupData.id);
+			if (storedGroup) {
+				// Restore metadata
+				if (groupData.changes) {
+					storedGroup.changes = { ...groupData.changes };
+				}
+
+				// Preserve upload order
+				if (groupData.uploads) {
+					storedGroup.uploads = [...groupData.uploads];
+				}
+
+				// Restore form field values
+				if (groupData.changes) {
+					const titleInput = group.element.querySelector('[name*="post_title"]');
+					const excerptInput = group.element.querySelector('[name*="post_excerpt"]');
+
+					if (titleInput && groupData.changes.post_title) {
+						titleInput.value = groupData.changes.post_title;
+					}
+					if (excerptInput && groupData.changes.post_excerpt) {
+						excerptInput.value = groupData.changes.post_excerpt;
+					}
+				}
+			}
+		}
+
+		await this.saveFieldData(fieldData);
+	}
+
+	async openModalForRestore(context) {
+		if (!context) return;
+
+		const { modalType, itemId } = context;
+
+		// Find and click the appropriate button to open the modal
+		let trigger = null;
+
+		switch(modalType) {
+			case 'create':
+				trigger = document.querySelector('[data-action="create"]');
+				break;
+			case 'edit':
+				// Need to find the specific edit button
+				if (itemId) {
+					trigger = document.querySelector(`[data-action="edit"][data-id="${itemId}"]`);
+				}
+				break;
+			case 'bulkEdit':
+				trigger = document.querySelector('[data-action="bulk-edit"]');
+				break;
+		}
+
+		if (trigger) {
+			trigger.click();
+
+			// Wait for modal to open and render
+			await new Promise(resolve => setTimeout(resolve, 300));
+		} else {
+			console.warn('Modal trigger not found for restoration:', context);
+		}
+	}
+
+	formatBytes(bytes, decimals = 2) {
+		if (bytes === 0) return '0 Bytes';
+		const k = 1024;
+		const dm = decimals < 0 ? 0 : decimals;
+		const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+		const i = Math.floor(Math.log(bytes) / Math.log(k));
+		return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+	}
+
+	/*******************************************************************************
+	 * CLEANUP METHODS - AGGRESSIVE CLEANUP AFTER SUCCESS
+	 *******************************************************************************/
+
+	/**
+	 * Clear individual upload from stores (called after successful upload)
+	 */
+	async clearUpload(uploadId, persist = true) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (uploadEl) {
+			this.revokePreviewUrl(uploadEl.preview);
+			if (uploadEl.element) {
+				const previewUrl = uploadEl.element.dataset.previewUrl;
+				this.revokePreviewUrl(previewUrl);
+				delete uploadEl.element.dataset.previewUrl;
+			}
+		}
+
+		// Remove from runtime memory
+		this.uploadElements.delete(uploadId);
+
+		// Remove from store (no separate blob store - it's part of the upload object)
+		await this.uploadStore.delete(uploadId);
+
+		// Update field if needed
+		if (persist) {
+			const upload = this.uploadStore.get(uploadId);
+			if (upload?.fieldId) {
+				await this.schedulePersistance(upload.fieldId);
+			}
+		}
+	}
+
+	/**
+	 * Clear entire field from stores (called when all uploads complete)
+	 */
+	async clearFieldFromStores(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+
+		// Clear all related uploads
+		if (fieldData?.uploads) {
+			const uploadsArray = fieldData.uploads instanceof Set
+				? Array.from(fieldData.uploads)
+				: fieldData.uploads;
+
+			for (const uploadId of uploadsArray) {
+				await this.uploadStore.delete(uploadId);
+			}
+		}
+
+		// Clear field from store
+		await this.fieldStore.delete(fieldId);
+
+		// Keep runtime references (fieldElements, etc) intact for reuse
+	}
+
+	cleanupAllPreviewUrls() {
+		if (this.previewUrls) {
+			this.previewUrls.forEach(url => {
+				try {
+					URL.revokeObjectURL(url);
+				} catch (e) {
+					// Ignore errors during cleanup
+				}
+			});
+			this.previewUrls.clear();
+		}
+	}
+
+	/*******************************************************************************
+	 * UI UPDATE METHODS
+	 *******************************************************************************/
+
+	updateFieldState(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldEl || !fieldData) return;
+
+		const container = fieldEl.element;
+		const uploadCount = fieldData.uploads?.size || 0;
+		const hasGroups = fieldEl.ui.groups?.container?.querySelectorAll('.upload-group').length > 0;
+
+		container.dataset.hasUploads = uploadCount > 0 ? 'true' : 'false';
+		container.dataset.uploadCount = uploadCount.toString();
+		container.dataset.hasGroups = hasGroups ? 'true' : 'false';
+
+		if (fieldEl.ui.preview) {
+			fieldEl.ui.preview.setAttribute('aria-label',
+				`Upload preview area with ${uploadCount} item${uploadCount !== 1 ? 's' : ''}`
+			);
+		}
+	}
+
+	updateUploadProgress(fieldId, current, total, message) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl?.ui?.progress?.progress) return;
+
+		const progress = fieldEl.ui.progress;
+		const percent = total > 0 ? (current / total) * 100 : 0;
+
+		if (progress.fill) progress.fill.style.width = `${percent}%`;
+		if (progress.text) progress.text.textContent = message;
+		if (progress.count) progress.count.textContent = `${current}/${total}`;
+
+		progress.progress.hidden = (current === total);
+	}
+
+	updateFieldStatus(fieldId, status) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		fieldData.state = status;
+		this.saveFieldData(fieldData);
+	}
+
+	updateUploadStatus(uploadId, status) {
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload) return;
+
+		upload.status = status;
+		this.uploadStore.save(upload);
+		this.updateUploadUI(uploadId);
+	}
+
+	updateUploadUI(uploadId) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload || !uploadEl?.element) return;
+
+		uploadEl.element.className = uploadEl.element.className.replace(/status-[\w-]+/g, '');
+		uploadEl.element.classList.add(`status-${upload.status}`);
+
+		const progress = uploadEl.element.querySelector('.progress');
+		if (progress) {
+			this.updateUploadItemProgress(uploadId,
+				this.getStatusProgress(upload.status),
+				upload.status
+			);
+		}
+	}
+
+	showUploadProgress(uploadId, show = true) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!uploadEl?.element) return;
+
+		const progressEl = uploadEl.element.querySelector('.progress');
+		if (progressEl) {
+			if (show) {
+				progressEl.style.removeProperty('animation');
+				progressEl.hidden = false;
+			} else {
+				progressEl.style.animation = 'fadeOut var(--transition-base)';
+				setTimeout(() => { progressEl.hidden = true; }, 300);
+			}
+		}
+	}
+
+	updateUploadItemProgress(uploadId, percent, status = null) {
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!uploadEl?.element) return;
+
+		const progressEl = uploadEl.element.querySelector('.progress');
+		if (!progressEl) return;
+
+		const fill = progressEl.querySelector('.fill');
+		const details = progressEl.querySelector('.details');
+		const icon = progressEl.querySelector('.icon');
+
+		if (fill) fill.style.width = `${percent}%`;
+		if (status && details) details.textContent = this.getStatusText(status);
+		if (status && icon) icon.innerHTML = this.getStatusIcon(status).outerHTML;
+	}
+
+	maybeLockUploads(fieldId) {
+		const fieldEl = this.fieldElements.get(fieldId);
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldEl?.ui?.dropZone || !fieldData) return;
+
+		const uploadCount = fieldData.uploads?.size || 0;
+
+		// For groupable uploads, set max to 20
+		const maxFiles = fieldData.config.destination === 'post_group'
+			? 20
+			: (fieldData.config?.maxFiles || 999);
+
+		fieldEl.ui.dropZone.hidden = uploadCount >= maxFiles;
+		fieldEl.element.classList.toggle('at-max-uploads', uploadCount >= maxFiles);
+
+		// Show helpful message for groupable uploads
+		if (fieldData.config.destination === 'post_group' && uploadCount >= maxFiles) {
+			this.a11y.announce('Maximum of 20 uploads reached. Please submit current uploads before adding more.');
+		}
+	}
+
+	/*******************************************************************************
+	 * GROUP MANAGEMENT
+	 *******************************************************************************/
+	/**
+	 * Create sortable instance for a grid
+	 */
+	createSortableForGrid(grid, fieldId, groupId = null) {
+		if (!grid || grid.sortableInstance) return;
+
+		const sortableInstance = new Sortable(grid, {
+			animation: 150,
+			draggable: '.item',
+			multiDrag: true,
+			selectedClass: 'selected-for-drag',
+			avoidImplicitDeselect: true,
+			group: { name: fieldId, pull: true, put: true },
+			ghostClass: 'sortable-ghost',
+			chosenClass: 'sortable-chosen',
+			dragClass: 'sortable-drag',
+
+			// Centralized drop handler
+			onEnd: (evt) => this.handleDrop(evt, fieldId),
+
+			// Selection sync
+			onSelect: (evt) => {
+				const checkbox = evt.item.querySelector('[name*="select-item"]');
+				if (checkbox && !checkbox.checked) {
+					checkbox.checked = true;
+					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+				}
+			},
+
+			onDeselect: (evt) => {
+				const checkbox = evt.item.querySelector('[name*="select-item"]');
+				if (checkbox && checkbox.checked) {
+					checkbox.checked = false;
+					checkbox.dispatchEvent(new Event('change', { bubbles: true }));
+				}
+			},
+
+			onAdd: (evt) => this.updateSortableState(evt.to),
+			onRemove: (evt) => this.updateSortableState(evt.from)
+		});
+
+		grid.sortableInstance = sortableInstance;
+
+		const gridId = groupId
+			? `${fieldId}-group-${groupId}`
+			: `${fieldId}-preview`;
+
+		this.sortableInstances.set(gridId, sortableInstance);
+
+		return sortableInstance;
+	}
+	createGroup(fieldId, groupId = null) {
+		const fieldData = this.getFieldData(fieldId);
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldData || !fieldEl) return null;
+
+		if (!groupId) {
+			groupId = `group_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
+		}
+
+		const groupElement = this.createGroupElement(groupId, fieldId);
+		if (!groupElement) return null;
+
+		// Store in field UI Map
+		if (!fieldEl.ui.groups) {
+			fieldEl.ui.groups = {
+				groups: new Map(),
+				container: null,
+				empty: null,
+				display: null
+			};
+		}
+
+		fieldEl.ui.groups.groups.set(groupId, groupElement);
+
+		// Insert into DOM
+		if (fieldEl.ui.groups.container && fieldEl.ui.groups.empty) {
+			fieldEl.ui.groups.container.insertBefore(groupElement, fieldEl.ui.groups.empty);
+		} else if (fieldEl.ui.groups.container) {
+			fieldEl.ui.groups.container.appendChild(groupElement);
+		}
+
+		// Store group element reference
+		const grid = groupElement.querySelector('.item-grid.group');
+		this.groupElements.set(groupId, {
+			element: groupElement,
+			grid: grid,
+			fieldId: fieldId
+		});
+
+		// Add to field groups
+		if (!fieldData.groups) fieldData.groups = [];
+		const existingGroup = fieldData.groups.find(g => g.id === groupId);
+		if (!existingGroup) {
+			fieldData.groups.push({
+				id: groupId,
+				uploads: [],
+				changes: {}
+			});
+			this.saveFieldData(fieldData);
+		}
+
+		// Initialize selection handler
+		this.addGroupSelectionHandler(fieldId, groupId);
+
+		if (grid) {
+			this.createSortableForGrid(grid, fieldId, groupId);
+		}
+
+		return { id: groupId, element: groupElement, grid: grid };
+	}
+	createGroupElement(groupId, fieldId) {
+		let groupElement = window.getTemplate('imageGroup');
+		if (!groupElement) return;
+
+		groupElement.dataset.groupId = groupId;
+		groupElement.dataset.fieldId = fieldId;
+
+		let fields = window.getTemplate('groupMetadata');
+		const fieldsContainer = groupElement.querySelector('.fields');
+		if (fieldsContainer && fields) {
+			fieldsContainer.append(fields);
+
+			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
+			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
+
+			if (titleInput) {
+				titleInput.id = `${groupId}_title`;
+				titleInput.name = `${groupId}[post_title]`;
+			}
+			if (excerptInput) {
+				excerptInput.id = `${groupId}_excerpt`;
+				excerptInput.name = `${groupId}[post_excerpt]`;
+			}
+
+			const fieldData = this.getFieldData(fieldId);
+			if (fieldData && fieldData.config.content !== '') {
+				let summary = groupElement.querySelector('summary');
+				if (summary) summary.textContent = fieldData.config.content + ' Fields';
+			}
+		} else {
+			groupElement.querySelector('details')?.remove();
+		}
+
+		const gridContainer = groupElement.querySelector('.item-grid.group');
+		if (gridContainer) {
+			gridContainer.dataset.groupId = groupId;
+		}
+
+		return groupElement;
+	}
+
+	deleteGroup(groupId, confirm = true) {
+		const groupEl = this.groupElements.get(groupId);
+		if (!groupEl) return;
+
+		const fieldData = this.getFieldData(groupEl.fieldId);
+		if (!fieldData) return;
+
+		const group = fieldData.groups?.find(g => g.id === groupId);
+		let keepUploads = true;
+
+		if (confirm && group?.uploads?.length > 0) {
+			keepUploads = !window.confirm('Delete uploads in group?');
+		}
+
+		if (confirm && keepUploads && group?.uploads) {
+			// Move uploads back to preview
+			group.uploads.forEach(uploadId => {
+				this.removeFromGroup(uploadId);
+			});
+		}
+
+		// Remove from field groups
+		if (fieldData.groups) {
+			fieldData.groups = fieldData.groups.filter(g => g.id !== groupId);
+			this.saveFieldData(fieldData);
+		}
+
+		// Remove DOM element
+		if (groupEl.element) {
+			groupEl.element.remove();
+			this.a11y.announce('Group removed');
+		}
+
+		// Remove from maps
+		this.groupElements.delete(groupId);
+
+		// Clean up sortable
+		const sortableKey = `${groupEl.fieldId}-group-${groupId}`;
+		const sortable = this.sortableInstances.get(sortableKey);
+		if (sortable?.destroy) {
+			sortable.destroy();
+		}
+		this.sortableInstances.delete(sortableKey);
+
+		this.schedulePersistance(groupEl.fieldId);
+	}
+
+	addToGroup(uploadId, target = null, persist = true) {
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!upload || !uploadEl) return;
+
+		const fieldData = this.getFieldData(upload.fieldId);
+		const fieldEl = this.fieldElements.get(upload.fieldId);
+		if (!fieldData || !fieldEl) return;
+
+		// Already in correct location
+		if ((!target && uploadEl.location === fieldEl.ui.preview) || target === uploadEl.location) {
+			return;
+		}
+
+		// Remove from previous group
+		if (upload.groupId) {
+			const group = fieldData.groups?.find(g => g.id === upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					this.deleteGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clear selection checkbox
+		const checkbox = uploadEl.element.querySelector('[name*="select-item"]');
+		if (checkbox) checkbox.checked = false;
+
+		let featured = uploadEl.element.querySelector('[name="featured"]');
+		if (featured) featured.hidden = !target;
+
+		// Moving to preview or to group
+		if (!target || target.classList.contains('preview')) {
+			target = fieldEl.ui.preview;
+			upload.groupId = null;
+		} else {
+			// Moving to group
+			const groupId = target.dataset.groupId;
+			if (featured) featured.name = groupId + '_' + featured.name;
+
+			const group = fieldData.groups?.find(g => g.id === groupId);
+			if (group) {
+				if (!group.uploads) group.uploads = [];
+				group.uploads.push(uploadId);
+				upload.groupId = groupId;
+			}
+		}
+
+		// Update location
+		uploadEl.location = target;
+		target.append(uploadEl.element);
+
+		// Update stores
+		this.uploadStore.save(upload);
+		if (persist) {
+			this.saveFieldData(fieldData);
+		}
+
+		// Update sortable state
+		this.updateSortableState(target);
+		if (uploadEl.location && uploadEl.location !== target) {
+			this.updateSortableState(uploadEl.location);
+		}
+	}
+
+	removeFromGroup(uploadId) {
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+		if (!upload || !uploadEl) return;
+
+		const fieldData = this.getFieldData(upload.fieldId);
+		const fieldEl = this.fieldElements.get(upload.fieldId);
+		if (!fieldData || !fieldEl) return;
+
+		// Remove from current group
+		if (upload.groupId) {
+			const group = fieldData.groups?.find(g => g.id === upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					this.deleteGroup(upload.groupId, false);
+				}
+			}
+			upload.groupId = null;
+		}
+
+		// Move back to preview
+		if (fieldEl.ui?.preview) {
+			fieldEl.ui.preview.appendChild(uploadEl.element);
+			uploadEl.location = fieldEl.ui.preview;
+		}
+
+		// Hide featured radio
+		const featured = uploadEl.element.querySelector('[name="featured"]');
+		if (featured) {
+			featured.hidden = true;
+			featured.checked = false;
+		}
+
+		this.uploadStore.save(upload);
+		this.updateSortableState(fieldEl.ui.preview);
+	}
+
+	removeUpload(fieldId, uploadId) {
+		const fieldData = this.getFieldData(fieldId);
+		const upload = this.uploadStore.get(uploadId);
+		const uploadEl = this.uploadElements.get(uploadId);
+
+		if (!fieldData || !upload) return;
+
+		// Remove from field
+		fieldData.uploads?.delete(uploadId);
+
+		// Remove from group if grouped
+		if (upload.groupId) {
+			const group = fieldData.groups?.find(g => g.id === upload.groupId);
+			if (group) {
+				group.uploads = group.uploads.filter(id => id !== uploadId);
+				if (group.uploads.length === 0) {
+					this.deleteGroup(upload.groupId);
+				}
+			}
+		}
+
+		// Clean up element
+		uploadEl?.element?.remove();
+
+		// Clean up memory
+		this.clearUpload(uploadId);
+
+		// Update field state
+		this.saveFieldData(fieldData);
+		this.updateFieldState(fieldId);
+		this.maybeLockUploads(fieldId);
+
+		const handler = this.selectionHandlers.get(fieldId);
+		if (handler) {
+			handler.deselect(uploadId);
+		}
+
+		this.a11y.announce('Upload removed');
+	}
+
+	handleGroupMetaChange(input) {
+		const groupEl = this.getGroupFromElement(input);
+		if (!groupEl) return;
+
+		const fieldData = this.getFieldData(groupEl.fieldId);
+		const group = fieldData?.groups?.find(g => g.id === groupEl.element.dataset.groupId);
+		if (!group) return;
+
+		if (!group.changes) group.changes = {};
+
+		let name = input.name;
+		if (name.includes('group')) {
+			name = name.replace(`${group.id}_`, '').replace(`${group.id}[`, '').replace(']', '');
+		}
+
+		group.changes[name] = input.value;
+		this.saveFieldData(fieldData);
+		this.schedulePersistance(groupEl.fieldId);
+	}
+
+	/*******************************************************************************
+	 * ACTION HANDLERS
+	 *******************************************************************************/
+
+	handleAction(button) {
+		const action = button.dataset.action;
+		const fieldId = this.getFieldIdFromElement(button);
+
+		switch(action) {
+			case 'add-to-group':
+				this.handleAddToGroup(button);
+				break;
+			case 'delete-group':
+				this.handleDeleteGroup(button);
+				break;
+			case 'delete-upload':
+			case 'remove-from-group':
+				this.handleRemoveItem(button);
+				break;
+			case 'upload':
+				const fieldEl = this.fieldElements.get(fieldId);
+				if (fieldEl) {
+					fieldEl.element.closest('details').open = false;
+					document.body.classList.add('uploading');
+					this.submitUploads(fieldId);
+				}
+				break;
+			case 'restore':
+				this.handleRestoreUploads().then(() => {});
+				break;
+			case 'restore-all':
+				this.handleRestoreAll().then(() => {});
+				break;
+			case 'clear-cache':
+				if (!confirm('Save these uploads for later?')) {
+					this.cleanupStoredUploads();
+				}
+				this.cleanupRestore();
+				break;
+		}
+	}
+
+	handleAddToGroup(button) {
+		const fieldElement = button.closest(this.selectors.field.field);
+		const fieldId = fieldElement?.dataset.uploader;
+		if (!fieldId) return;
+
+		const selected = this.selected.get(fieldId);
+
+		if (!selected || selected.size === 0) {
+			this.createGroup(fieldId);
+		} else {
+			const group = this.createGroup(fieldId);
+			if (!group) return;
+
+			selected.forEach(uploadId => {
+				this.addToGroup(uploadId, group.grid);
+			});
+
+			const handler = this.selectionHandlers.get(fieldId);
+			handler?.clearSelection();
+
+			this.a11y.announce(`Created group with ${selected.size} items`);
+		}
+
+		this.schedulePersistance(fieldId);
+	}
+
+	handleDeleteGroup(button) {
+		const group = button.closest(this.selectors.groups.container);
+		if (!group) return;
+
+		const groupId = group.dataset.groupId;
+		const fieldId = this.getFieldIdFromElement(group);
+
+		if (!confirm('Delete this group? Items will be moved back to the upload area.')) {
+			return;
+		}
+
+		const items = group.querySelectorAll(this.selectors.items.item);
+		items.forEach(item => {
+			const uploadId = item.dataset.uploadId;
+			this.removeFromGroup(uploadId);
+		});
+
+		this.deleteGroup(groupId);
+		this.a11y.announce('Group deleted, items returned to upload area');
+		this.schedulePersistance(fieldId);
+	}
+
+	handleRemoveItem(button) {
+		const item = button.closest(this.selectors.items.item);
+		if (!item) return;
+
+		const uploadId = item.dataset.uploadId;
+		const fieldId = this.getFieldIdFromElement(item);
+
+		if (!confirm('Remove this item?')) return;
+
+		this.removeUpload(fieldId, uploadId);
+		this.a11y.announce('Item removed');
+		this.schedulePersistance(fieldId);
+	}
+
+	/*******************************************************************************
+	 * SELECTION MANAGEMENT
+	 *******************************************************************************/
+
+	addFieldSelectionHandler(fieldId) {
+		if (this.selectionHandlers.has(fieldId)) {
+			return this.selectionHandlers.get(fieldId);
+		}
+
+		const fieldEl = this.fieldElements.get(fieldId);
+		if (!fieldEl?.element) return;
+
+		const handler = new window.jvbHandleSelection({
+			container: fieldEl.element,
+			ui: {
+				selectAll: fieldEl.element.querySelector('[name="select-all-uploads"]'),
+				bulkControls: fieldEl.element.querySelector('.selection-actions'),
+				count: fieldEl.element.querySelector('.selection-count')
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]'
+		});
+
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+					// Sync with Sortable
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'item-deselected':
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'range-selected':
+					this.syncSortableSelection(fieldId, data.selectedItems);
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(fieldId, handler);
+		return handler;
+	}
+
+	addGroupSelectionHandler(fieldId, groupId) {
+		const handlerKey = `${fieldId}_${groupId}`;
+		if (this.selectionHandlers.has(handlerKey)) {
+			return this.selectionHandlers.get(handlerKey);
+		}
+
+		const groupEl = this.groupElements.get(groupId);
+		if (!groupEl?.element) return;
+
+		const handler = new window.jvbHandleSelection({
+			container: groupEl.element,
+			ui: {
+				selectAll: groupEl.element.querySelector(this.selectors.groups.selectAll),
+				bulkControls: groupEl.element.querySelector(this.selectors.groups.actions),
+				count: groupEl.element.querySelector(this.selectors.groups.count)
+			},
+			itemSelector: '[data-upload-id]',
+			checkboxSelector: '[name*="select-item"]'
+		});
+
+		handler.subscribe((event, data) => {
+			switch(event) {
+				case 'item-selected':
+				case 'item-deselected':
+				case 'range-selected':
+					this.selected.set(fieldId, data.selectedItems);
+					break;
+				case 'select-all':
+					this.handleSelectAll(data.container, data.selected);
+					break;
+			}
+		});
+
+		this.selectionHandlers.set(handlerKey, handler);
+		return handler;
+	}
+
+	handleSelectAll(container, selected) {
+		// Can add custom logic here if needed
+	}
+
+	/*******************************************************************************
+	 * HELPER METHODS
+	 *******************************************************************************/
+
+	/**
+	 * Get field data from store and normalize it
+	 * Always use this instead of directly accessing fieldStore.get()
+	 */
+	getFieldData(fieldId) {
+		const fieldData = this.fieldStore.get(fieldId);
+		if (!fieldData) return null;
+
+		// Only convert uploads back to Set (DataStore returns Arrays)
+		if (Array.isArray(fieldData.uploads)) {
+			fieldData.uploads = new Set(fieldData.uploads);
+		} else if (!fieldData.uploads) {
+			fieldData.uploads = new Set();
+		}
+
+		// Ensure groups is an array
+		if (!Array.isArray(fieldData.groups)) {
+			fieldData.groups = [];
+		}
+
+		return fieldData;
+	}
+
+	/**
+	 * Save field data to store, converting Sets to Arrays
+	 */
+	async saveFieldData(fieldData) {
+		await this.fieldStore.save({
+			...fieldData,
+			timestamp: Date.now()
+		});
+	}
+
+	determineFieldId(fieldElement) {
+		const content = fieldElement.dataset.content+'_' ||
+			fieldElement.closest('dialog')?.dataset.content+'_' ||
+			fieldElement.closest('form')?.dataset.save+'_' || '';
+		const itemID = fieldElement.dataset.itemId+'_' ||
+			fieldElement.closest('dialog')?.dataset.itemId+'_' || '';
+		const field = fieldElement.dataset.field || '';
+
+		return `${content}${itemID}${field}`;
+	}
+
+	getFromElement(element, type) {
+		const map = {
+			'field': {
+				selector: this.selectors.field.field,
+				key: 'uploader',
+				getRuntimeData: (id) => this.fieldElements.get(id),
+				getStoreData: (id) => this.getFieldData(id)
+			},
+			'upload': {
+				selector: this.selectors.items.item,
+				key: 'uploadId',
+				getRuntimeData: (id) => this.uploadElements.get(id),
+				getStoreData: (id) => this.uploadStore.get(id)
+			},
+			'group': {
+				selector: this.selectors.groups.container,
+				key: 'groupId',
+				getRuntimeData: (id) => this.groupElements.get(id),
+				getStoreData: (id) => {
+					// Groups are stored in field.groups array
+					const groupEl = this.groupElements.get(id);
+					if (!groupEl) return null;
+					const fieldData = this.getFieldData(groupEl.fieldId);
+					return fieldData?.groups?.find(g => g.id === id);
+				}
+			}
+		};
+
+		const config = map[type];
+		if (!config) return null;
+
+		const el = element.closest(config.selector);
+		if (!el) return null;
+
+		const id = el.dataset[config.key];
+
+		// Return combined runtime + store data for convenience
+		const runtime = config.getRuntimeData(id);
+		const store = config.getStoreData(id);
+
+		return { ...runtime, ...store };
+	}
+
+	getFieldFromElement(el) { return this.getFromElement(el, 'field'); }
+	getUploadFromElement(el) { return this.getFromElement(el, 'upload'); }
+	getGroupFromElement(el) { return this.getFromElement(el, 'group'); }
+
+	getFieldIdFromElement(el) {
+		const field = this.getFromElement(el, 'field');
+		return field?.id ?? null;
+	}
+	getUploadIdFromElement(el) {
+		const upload = this.getFromElement(el, 'upload');
+		return upload?.id ?? null;
+	}
+	getGroupIdFromElement(el) {
+		const group = this.getFromElement(el, 'group');
+		return group?.id ?? null;
+	}
 
 	getSubtypeFromMime(mimeType) {
 		if (mimeType.startsWith('image/')) return 'image';
@@ -2684,423 +2602,252 @@
 		return 'document';
 	}
 
-	updateUploadProgress(fieldId, current, total, message) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		let progressBar = field.ui.field.progress.progress;
-
-		// Create progress bar if it doesn't exist
-		if (!progressBar) {
-			progressBar = window.getTemplate('imageProgress');
-
-			if (!progressBar) {
-				console.warn('Progress bar template not found');
-				return;
-			}
-
-			// Insert after drop zone or at top of container
-			const container = field.ui.field.field;
-			const insertAfter = field.ui.field.dropZone;
-
-			if (insertAfter) {
-				insertAfter.insertAdjacentElement('afterend', progressBar);
-			} else if (container) {
-				container.prepend(progressBar);
-			}
-
-			// Update the field UI reference to match actual structure
-			if (!field.ui.field.progress) {
-				field.ui.field.progress = {};
-			}
-			field.ui.field.progress = {
-				progress: progressBar,
-				bar: progressBar.querySelector('.bar'),
-				fill: progressBar.querySelector('.fill'),
-				details: progressBar.querySelector('.details'),
-				text: progressBar.querySelector('.details .text'),
-				count: progressBar.querySelector('.details .count')
-			};
-		}
-
-
-		progressBar.hidden = false;
-		progressBar.style.display = 'flex';
-		progressBar.style.animation = 'none';
-		progressBar.style.opacity = '1';
-
-		// Update progress bar
-		const progressPercent = total > 0 ? Math.round((current / total) * 100) : 0;
-		const progressFill = field.ui.field.progress.fill;
-		const progressText = field.ui.field.progress.text;
-		const progressCount = field.ui.field.progress.count;
-
-		if (progressFill) {
-			progressFill.style.width = `${progressPercent}%`;
-		}
-
-		if (progressText) {
-			progressText.textContent = message;
-		}
-
-		if (progressCount) {
-			progressCount.textContent = `${current}/${total}`;
-		}
-
-		// Hide when complete
-		if (current >= total) {
-			setTimeout(() => {
-				progressBar.style.animation = 'fadeOut var(--transition-base)';
-				setTimeout(() => {
-					progressBar.hidden = true;
-					progressBar.style.display = 'none';
-				}, 300);
-			}, 1000);
-		}
+	getStatusText(status) {
+		return this.statusMapping[status] || status;
 	}
 
-	hideUploadProgress(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		const progressBar = field.ui.field.progress.progress;
-		if (progressBar) {
-			window.fade(progressBar, false);
-		}
+	getStatusIcon(status) {
+		return window.getIcon(this.queue.icons[status]);
 	}
-	/*******************************************************************************
-	 INDEXEDDB CACHE FUNCTIONALITY
-	*******************************************************************************/
-	async initDB() {
-		if (!('indexedDB' in window)) return;
 
-		const request = indexedDB.open(`jvb_uploads_db`, 1);
-
-		request.onupgradeneeded = (e) => {
-			const db = e.target.result;
-			if (!db.objectStoreNames.contains('fieldStates')) {
-				const store = db.createObjectStore('fieldStates', { keyPath: 'fieldId' });
-				store.createIndex('timestamp', 'timestamp', { unique: false });
-				store.createIndex('content', 'content', { unique: false });
-				store.createIndex('itemId', 'itemId', { unique: false });
-			}
-
-			// Blob storage remains separate for performance
-			if (!db.objectStoreNames.contains('uploadBlobs')) {
-				db.createObjectStore('uploadBlobs', { keyPath: 'uploadId' });
-			}
+	getStatusProgress(status) {
+		const progress = {
+			'local_processing': 28,
+			'queued': 50,
+			'uploading': 66,
+			'pending': 75,
+			'processing': 89,
+			'completed': 100
 		};
-
-		request.onsuccess = (e) => {
-			this.db = e.target.result;
-			this.loadFields();
-			this.checkPendingUploads();
-		};
-
-		request.onerror = (e) => {
-			console.error('IndexedDB error:', e);
-		};
+		return progress[status] || 0;
 	}
 
-	async loadFields() {
-		if (!this.db) return;
 
-		return new Promise((resolve) => {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readonly');
-			const fieldStates = tx.objectStore('fieldStates');
-			const blobStore = tx.objectStore('uploadBlobs');
-			const request = fieldStates.getAll();
+	createUploadElement(upload, draggable = false) {
+		let image = window.getTemplate('uploadItem');
+		if (!image) return;
 
-			request.onsuccess = (e) => {
-				e.target.result.forEach(field => {
-					let uploads = field.uploads;
-					let uploadIds = uploads.map(upload => upload.id);
-					field.uploads = new Set(uploadIds);
-					this.fields.set(field.key, field);
-					uploads.forEach(upload => {
-						this.uploads.set(upload.id, upload);
-					});
-				});
-				this.notify('uploads-loaded', { items: Array.from(this.uploads.values()) });
-				resolve();
-			};
+		image.dataset.uploadId = upload.id;
+		image.dataset.subtype = upload.subtype || 'image';
 
-			const blobRequest = blobStore.getAll();
+		let [featured, img, video, preview, details] = [
+			image.querySelector('[name="featured"]'),
+			image.querySelector('img'),
+			image.querySelector('video'),
+			image.querySelector('label > span'),
+			image.querySelector('details')
+		];
 
-			blobRequest.onsuccess = (e) => {
-				e.target.result.forEach(item => {
-					this.uploadBlobs.set(item.id, item);
-				});
-				this.notify('blobs-loaded', { items: Array.from(this.uploadBlobs.values()) });
-				resolve();
-			};
-		});
-	}
+		if (featured) featured.value = upload.id;
 
-	getUpload(uploadId) {
-		return this.uploads.get(uploadId);
-	}
-
-	updateFieldStatus(fieldId, status) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
-
-		field.uploads.forEach(upload => {
-			this.updateUploadStatus(upload, status);
-		});
-
-		// Update UI based on status
-		const container = field.ui.field.field;
-		if (container) {
-			container.dataset.uploadStatus = status;
-
-			// Show/hide relevant UI elements
-			const submitBtn = container.querySelector('.submit-uploads');
-			if (submitBtn) {
-				submitBtn.disabled = status === 'uploading' || status === 'processing';
-			}
-		}
-	}
-
-	/**
-	 * Handle successful upload completion
-	 */
-	handleUploadComplete(operation) {
-		const response = operation.response;
-		if (!response?.uploads) return;
-
-		response.uploads.forEach(serverUpload => {
-			const upload = this.uploads.get(serverUpload.upload_id);
-			if (upload) {
-				upload.attachmentId = serverUpload.attachment_id;
-				this.updateUploadStatus(serverUpload.upload_id, 'completed');
-				this.uploads.set(upload.id, upload);
-
-				// **ADD: Cleanup after successful upload**
-				this.clearUpload(upload.id);
-			}
-		});
-
-		const fieldKey = operation.data.get('field_key');
-		if (fieldKey) {
-			// **ADD: Clear field cache after all uploads complete**
-			const field = this.fields.get(fieldKey);
-			const allComplete = Array.from(field.uploads).every(id => {
-				const upload = this.uploads.get(id);
-				return upload?.status === 'completed';
-			});
-
-			if (allComplete) {
-				this.clearField(fieldKey);
-			}
-		}
-	}
-
-	/**
-	 * Store upload with DataStore integration
-	 */
-	async setUpload(fieldId, file, uploadId = null) {
-		if (!uploadId) {
-			uploadId = this.generateUploadId();
-		}
-		const upload = {
-			id: uploadId,
-			fieldId: fieldId,
-			groupId: null,
-			originalFile: file,
-			processedFile: null,
-			status: 'received',
-			progress: { percent: 0, message: 'Received...' },
-			preview: URL.createObjectURL(file),
-			createdAt: Date.now(),
-			meta: {
-				title: '',
-				alt_text: '',
-				caption: '',
-				originalName: file.name,
-				originalType: file.type,
-				originalSize: file.size
-			},
-			changes: {}
-		};
-
-		// Add to field
-		const field = this.fields.get(fieldId);
-		if (!field) {
-			console.error(`Field ${fieldId} not found`);
-			return null;
-		}
-		if (!field.uploads) field.uploads = new Set();
-		field.uploads.add(uploadId);
-
-		upload.element = this.createImageElement(upload, field.type==='groupable');
-		upload.ui = window.uiFromSelectors(this.selectors.item, upload.element);
-
-		// Store in memory
-		this.uploads.set(uploadId, upload);
-		this.updateImageUI(uploadId);
-
-		// Persist to DataStore
-		await this.persistFieldState(fieldId);
-
-		return upload;
-	}
-
-	/**
-	 * Get uploads for a field, optionally cleaned for storage
-	 * @param {string} fieldId
-	 * @param {boolean} clean - Remove DOM references for IndexedDB storage
-	 * @returns {Array}
-	 */
-	getFieldUploads(fieldId, clean = false) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.uploads) return [];
-
-		return Array.from(field.uploads)
-			.map(uploadId => {
-				const upload = this.uploads.get(uploadId);
-				if (!upload) return null;
-
-				if (clean) {
-					// Return cleaned version without DOM references
-					return {
-						id: upload.id,
-						fieldId: upload.fieldId,
-						status: upload.status,
-						preview: upload.preview,
-						attachmentId: upload.attachmentId,
-						operationId: upload.operationId,
-						groupId: upload.groupId || null,
-						meta: {
-							originalName: upload.meta?.originalName || upload.originalFile?.name,
-							size: upload.meta?.size || upload.originalFile?.size,
-							type: upload.meta?.type || upload.originalFile?.type,
-							title: upload.meta?.title,
-							alt: upload.meta?.alt,
-							caption: upload.meta?.caption
-						}
-					};
+		switch (upload.subtype) {
+			case 'image':
+				if (img) {
+					img.src = upload.preview;
+					img.alt = upload.meta?.originalName || '';
 				}
+				video?.remove();
+				preview?.remove();
+				break;
+			case 'video':
+				if (video) video.src = upload.preview;
+				img?.remove();
+				preview?.remove();
+				break;
+			case 'document':
+				const fileName = upload.meta?.originalName || '';
+				const extension = fileName.split('.').pop()?.toLowerCase() || '';
+				const iconMap = {
+					'pdf': 'file-pdf', 'csv': 'file-csv',
+					'doc': 'file-doc', 'docx': 'file-doc',
+					'txt': 'file-txt', 'xls': 'file-xls', 'xlsx': 'file-xls'
+				};
+				const icon = window.getIcon(iconMap[extension] || 'file');
+				if (preview) {
+					preview.innerText = fileName;
+					preview.prepend(icon);
+				}
+				img?.remove();
+				video?.remove();
+				break;
+		}
 
-				// Return full upload object
-				return upload;
-			})
-			.filter(Boolean);
+		if (details) {
+			let template = window.getTemplate('uploadMeta');
+			if (template) details.append(template);
+		}
+
+		image.draggable = draggable;
+
+		// Update input IDs
+		image.querySelectorAll('input').forEach(input => {
+			let id = input.id;
+			if (id) {
+				let newId = id + upload.id;
+				let label = input.parentNode.querySelector(`label[for="${id}"]`);
+				input.id = newId;
+				if (label) label.htmlFor = newId;
+			}
+		});
+
+		return image;
+	}
+
+	/*******************************************************************************
+	 * PERSISTENCE
+	 *******************************************************************************/
+
+	schedulePersistance(fieldId) {
+		const key = `persist_${fieldId}`;
+		window.debouncer.schedule(
+			key,
+			() => this.persistFieldState(fieldId),
+			250
+		);
+	}
+
+	async persistFieldState(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData) return;
+
+		// Save with updated timestamp
+		await this.saveFieldData(fieldData);
+	}
+
+	// In UploadManager, add blob conversion helpers
+	async saveBlobData(uploadId, file) {
+		const arrayBuffer = await file.arrayBuffer();
+
+		const uploadData = this.uploadStore.get(uploadId) || { id: uploadId };
+
+		// Store blob data as ArrayBuffer with metadata
+		uploadData.blobData = {
+			buffer: arrayBuffer,
+			name: file.name,
+			type: file.type,
+			size: file.size,
+			lastModified: file.lastModified || Date.now()
+		};
+
+		await this.uploadStore.save(uploadData);
+	}
+
+	async getBlobData(uploadId) {
+		const upload = this.uploadStore.get(uploadId);
+		if (!upload?.blobData) return null;
+
+		// Reconstruct File from ArrayBuffer
+		const blob = new Blob([upload.blobData.buffer], { type: upload.blobData.type });
+		return new File([blob], upload.blobData.name, {
+			type: upload.blobData.type,
+			lastModified: upload.blobData.lastModified
+		});
+	}
+
+	/*******************************************************************************
+	 HELPER to GET UPLOADED FILES
+	*******************************************************************************/
+	/**
+	 * Get all files for a form (searches all upload fields within form)
+	 */
+	async getFilesForForm(formElement) {
+		const uploadFields = formElement.querySelectorAll('[data-upload-field]');
+		const allFiles = [];
+
+		for (const field of uploadFields) {
+			const fieldId = this.determineFieldId(field);
+			const files = await this.getFilesForField(fieldId);
+			allFiles.push(...files);
+		}
+
+		return allFiles;
 	}
 
 	/**
-	 * Persist upload to DataStore
+	 * Get all files for a specific field
 	 */
-	async persistFieldState(fieldId) {
-		if (!this.db) return;
+	async getFilesForField(fieldId) {
+		const fieldData = this.getFieldData(fieldId);
+		if (!fieldData?.uploads) return [];
 
-		const field = this.fields.get(fieldId);
-		if (!field) return;
+		const files = [];
+		const uploadsArray = fieldData.uploads instanceof Set
+			? Array.from(fieldData.uploads)
+			: fieldData.uploads;
 
-		// Create clean field config
-		const { ui, ...cleanConfig } = field;
+		for (const uploadId of uploadsArray) {
+			const upload = this.uploadStore.get(uploadId);
+			if (!upload) continue;
 
-		const fieldState = {
-			fieldId: fieldId,
-			timestamp: Date.now(),
-
-			config: {
-				...cleanConfig,
-				fieldName: field.name,
-				dataField: field.ui?.field?.field?.dataset?.field
-			},
-
-			// Recovery context with normalized URL
-			context: {
-				url: this.normalizeUrl(window.location.href),
-				fullUrl: window.location.href, // Keep for reference
-				modalType: this.getModalType(field),
-				formId: field.formId,
-				// **FIX**: Store additional identifiers
-				fieldSelector: `.field.upload[data-field="${field.name}"]`
-			},
-
-			// Uploads (cleaned of DOM references and blob URLs)
-			uploads: this.getFieldUploads(fieldId, true).map(upload => {
-				// **FIX**: Don't store blob URLs as they become invalid
-				const { preview, element, location, ...cleanUpload } = upload;
-				return cleanUpload;
-			}),
-
-			// Groups structure
-			groups: Array.from(this.groups.entries())
-				.filter(([id, data]) => data.fieldId === fieldId && data.uploads && data.uploads.size > 0)
-				.map(([id, data]) => ({
-					id: data.id,
-					uploads: Array.from(data.uploads),
-					meta: data.meta || {},
-					changes: data.changes || {}
-				}))
-		};
-
-		try {
-			const tx = this.db.transaction(['fieldStates'], 'readwrite');
-			await tx.objectStore('fieldStates').put(fieldState);
-		} catch (error) {
-			console.error('Failed to persist field state:', error);
-		}
-	}
-
-	normalizeUrl(url) {
-		try {
-			const urlObj = new URL(url);
-			// Return just the origin + pathname (no query string or hash)
-			return urlObj.origin + urlObj.pathname;
-		} catch (e) {
-			return url;
-		}
-	}
-	/*******************************************************************************
-	 RESTORE FUNCTIONALITY
-	*******************************************************************************/
-	async checkPendingUploads() {
-		if (!this.db) return;
-
-		const tx = this.db.transaction(['fieldStates'], 'readonly');
-		const fieldStore = tx.objectStore('fieldStates');
-
-		const allFieldStates = await new Promise(resolve => {
-			const request = fieldStore.getAll();
-			request.onsuccess = () => resolve(request.result);
-		});
-
-
-		allFieldStates.forEach(field => {
-			console.log(`Field ${field.fieldId} has ${field.uploads.length} uploads:`);
-			field.uploads.forEach((upload, idx) => {
-				console.log(`  Upload ${idx}:`, {
-					id: upload.id,
-					status: upload.status,
-					operationId: upload.operationId,
-					hasOperationId: !!upload.operationId
+			// Get the actual File object from blob data
+			const file = await this.getBlobData(uploadId);
+			if (file) {
+				files.push({
+					file: file,
+					uploadId: uploadId,
+					fieldName: fieldData.config.name,
+					meta: upload.meta || {}
 				});
+			}
+		}
+
+		return files;
+	}
+
+	/*******************************************************************************
+	 * RECOVERY & RESTORATION
+	 *******************************************************************************/
+
+	handleFieldStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				this.fieldStoreReady = true;
+				this.checkIfBothStoresReady();
+				break;
+		}
+	}
+
+	handleUploadStoreEvent(event, data) {
+		switch(event) {
+			case 'data-loaded':
+				this.uploadStoreReady = true;
+				this.checkIfBothStoresReady();
+				break;
+			case 'item-saved':
+				this.showSaveIndicator(data.key);
+				break;
+		}
+	}
+
+	checkIfBothStoresReady() {
+		if (this.fieldStoreReady && this.uploadStoreReady && !this.hasCheckedForUploads) {
+			this.hasCheckedForUploads = true;
+			this.checkForStoredUploads();
+		}
+	}
+
+	async checkForStoredUploads() {
+		const allFieldStates = this.fieldStore.getAll();
+
+		const pendingFields = allFieldStates.filter(field => {
+			if (!field.uploads) return false;
+
+			// Handle both Set and Array (from IndexedDB)
+			const uploadsArray = field.uploads instanceof Set
+				? Array.from(field.uploads)
+				: Array.isArray(field.uploads)
+					? field.uploads
+					: [];
+
+			return uploadsArray.some(uploadId => {
+				const upload = this.uploadStore.get(uploadId);
+				return upload && !upload.operationId &&
+					['completed', 'processed', 'local_processing', 'processed-original'].includes(upload.status);
 			});
 		});
-
-		// Filter for pending uploads (not yet sent to server)
-		const pendingFields = allFieldStates.filter(field =>
-			field.uploads.some(upload =>
-				// If no operationId, it hasn't been sent to server yet
-				!upload.operationId &&
-				// And it's been processed locally
-				(upload.status === 'completed' ||
-					upload.status === 'processed' ||
-					upload.status === 'local_processing' ||
-					upload.status === 'processed-original')
-			)
-		);
-
-		console.log('Pending Fields: ', pendingFields);
-
 		if (pendingFields.length === 0) return;
 
-		// Show recovery notification
-		this.showRecoveryNotification(pendingFields);
+		await this.showRecoveryNotification(pendingFields);
 	}
 
 	async showRecoveryNotification(pendingFields) {
@@ -3115,7 +2862,7 @@
 		}
 
 		// Build appropriate message
-		let message = '';
+		let message;
 		if (totalGroups > 0) {
 			let group = totalGroups > 1 ? 'groups' : 'group';
 			let upload = totalUploads > 1 ? 'uploads' : 'upload';
@@ -3143,22 +2890,20 @@
 			const itemGrid = fieldTemplate.querySelector('.item-grid.restore');
 
 			// Process each upload
-			for (const upload of field.uploads) {
-
+			for (let uploadId of field.uploads) {
+				const upload = this.uploadStore.get(uploadId);
 				let uploadItem = window.getTemplate('uploadItem');
 				if (!uploadItem) continue;
-			//
-			// 	const imgEl = uploadItem.querySelector('img');
-			// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
-			//
-				const blobData = await this.getBlobData(upload.id);
+				//
+				// 	const imgEl = uploadItem.querySelector('img');
+				// 	const placeholderEl = uploadItem.querySelector('.image-placeholder');
+				//
+				const file = await this.getBlobData(upload.id);
+				if (file) {
 
-
-				if (blobData) {
 					try {
 						// Create new blob URL from stored data
-						const blob = new Blob([blobData.data], { type: blobData.type });
-						const previewUrl = URL.createObjectURL(blob);
+						const previewUrl = this.createPreviewUrl(file);
 
 						let [
 							featured,
@@ -3175,9 +2920,11 @@
 						];
 
 						uploadItem.dataset.uploadId = upload.id;
-						uploadItem.dataset.fieldId = field.config.key;
 
-						let subtype = this.getSubtypeFromMime(blobData.type);
+
+						uploadItem.dataset.fieldId = field.id;
+
+						let subtype = this.getSubtypeFromMime(file.type);
 						uploadItem.dataset.subtype = subtype;
 						switch (subtype) {
 							case 'image':
@@ -3186,7 +2933,7 @@
 									img.alt
 								] = [
 									previewUrl,
-									upload.originalFile?.name ?? upload.meta?.originalName?? ''
+									file.name ?? upload.meta?.originalName ?? ''
 								];
 								video.remove();
 								preview.remove();
@@ -3281,834 +3028,114 @@
 
 	}
 
-	async cleanupStoredRestoration() {
-		if (!this.db) return;
-
-		const notification = document.querySelector('dialog.restore-uploads');
-		if (!notification) return;
-
-		// Get all upload IDs from the notification
-		const items = notification.querySelectorAll('[data-upload-id]');
-		const uploadIds = Array.from(items).map(item => item.dataset.uploadId);
-
-		// Clean up blob URLs in the notification
-		this.cleanupRestoreNotificationUrls(notification);
-
-		// **Delete blob data from IndexedDB**
-		if (uploadIds.length > 0) {
-			const tx = this.db.transaction(['uploadBlobs', 'fieldStates'], 'readwrite');
-
-			// Delete all blob data
-			uploadIds.forEach(uploadId => {
-				tx.objectStore('uploadBlobs').delete(uploadId);
-			});
-
-			// Also delete field states
-			const fieldIds = Array.from(items).map(item => item.dataset.fieldId);
-			const uniqueFieldIds = [...new Set(fieldIds)];
-
-			uniqueFieldIds.forEach(fieldId => {
-				if (fieldId) {
-					tx.objectStore('fieldStates').delete(fieldId);
-				}
-			});
-
-			await tx.complete;
-		}
-	}
-
-	cleanupRestoreNotificationUrls(notification) {
-		if (!notification) return;
-
-		// Find all elements with preview URLs
-		const items = notification.querySelectorAll('[data-preview-url]');
-		items.forEach(item => {
-			const url = item.dataset.previewUrl;
-			if (url && url.startsWith('blob:')) {
-				URL.revokeObjectURL(url);
-				delete item.dataset.previewUrl;
-			}
-		});
-	}
-
-	getSelectedRestorationUploads(notificationEl) {
-		let selected = [];
-		const checkboxes = notificationEl.querySelectorAll('[type=checkbox]:checked');
-
-		checkboxes.forEach(checkbox => {
-			const item = checkbox.closest('.item');
-			if (item) {
-				selected.push({
-					uploadId: item.dataset.uploadId,
-					fieldId: item.dataset.fieldId
-				});
-			}
-		});
-
-		return selected;
-	}
-
-	async restoreSelectedUploads(selectedUploads) {
-		// Group by field
-		const byField = new Map();
-		selectedUploads.forEach(item => {
-			if (!byField.has(item.fieldId)) {
-				byField.set(item.fieldId, []);
-			}
-			byField.get(item.fieldId).push(item.uploadId);
-		});
-
-		// Get full field states from IndexedDB
-		if (!this.db) {
-			// this.notifications.add('Cannot restore: Database not available', 'error');
+	async handleRestoreUploads() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
 			return;
 		}
 
-		const tx = this.db.transaction(['fieldStates'], 'readonly');
-		const store = tx.objectStore('fieldStates');
-
-		for (const [fieldId, uploadIds] of byField.entries()) {
-			const request = store.get(fieldId);
-			const fieldState = await new Promise(resolve => {
-				request.onsuccess = () => resolve(request.result);
-				request.onerror = () => resolve(null);
-			});
-
-			if (fieldState) {
-				// Filter to only selected uploads
-				fieldState.uploads = fieldState.uploads.filter(u => uploadIds.includes(u.id));
-				await this.restoreField(fieldState);
-			}
-		}
-
-		// this.notifications.add(`Restored ${selectedUploads.length} upload(s)`, 'success');
-	}
-
-	async restoreField(fieldState) {
-		const { config, context, uploads, groups } = fieldState;
-
-		// If in a modal, open it first
-		if (context.modalType) {
-			await this.openModalForRestore(context);
-		}
-
-		// Find field element
-		let fieldElement = document.querySelector(`.field.upload[data-field="${config.name}"]`);
-
-		if (!fieldElement) {
-			const uploaderKey = `${config.content}_${config.itemID}_${config.name}`;
-			fieldElement = document.querySelector(`.field.upload[data-uploader="${uploaderKey}"]`);
-		}
-
-		if (!fieldElement) {
-			console.warn(`Field ${config.name} not found for restoration`, config);
-			return;
-		}
-
-		// Register the field if not already registered
-		let fieldKey = fieldElement.dataset.uploader;
-		if (!fieldKey || !this.fields.has(fieldKey)) {
-			fieldKey = this.registerUploader(fieldElement, config);
-		}
-
-		const field = this.fields.get(fieldKey);
-		if (!field) {
-			console.error('Failed to register field for restoration');
-			return;
-		}
-
-		if (!field.ui.groups) {
-			field.ui.groups = {};
-		}
-		if (!field.ui.groups.groups) {
-			field.ui.groups.groups = new Map();
-		}
-
-		// Make sure we have the container and empty group references
-		if (!field.ui.groups.container) {
-			field.ui.groups.container = fieldElement.querySelector('.item-grid.groups');
-		}
-		if (!field.ui.groups.empty) {
-			field.ui.groups.empty = fieldElement.querySelector('.empty-group');
-		}
-		let display = fieldElement.querySelector('.group-display');
-		if (display) {
-			display.hidden = false;
-		}
-
-		// Restore uploads
-		for (const uploadData of uploads) {
-			await this.restoreUpload(field, uploadData);
-		}
-
-		// Restore groups
-		if (groups && groups.length > 0) {
-			await this.restoreGroups(field, groups, uploads);
-		}
-
-		// Update UI
-		this.updateFieldState(fieldKey);
-		this.maybeLockUploads(fieldKey);
-
-		await this.persistFieldState(fieldKey);
-
-		// Queue for upload if needed (should not happen for post_group)
-		if (config.mode === 'direct' && config.destination !== 'post_group') {
-			await this.queueUpload(fieldKey);
-		}
-	}
-
-	async restoreUpload(field, uploadData) {
-		// Try to get blob data from IndexedDB
-		const blobData = await this.getBlobData(uploadData.id);
-
-		if (blobData) {
-			const file = blobData.data instanceof File
-				? blobData.data
-				: new File(
-					[blobData.data],
-					blobData.name,
-					{ type: blobData.type, lastModified: blobData.lastModified }
-				);
-
-			uploadData.originalFile = file;
-			uploadData.processedFile = file;
-			uploadData.preview = URL.createObjectURL(file);
-		} else {
-			console.warn('Blob data not found for upload:', uploadData.id);
-			return; // Skip this upload if we can't restore the file
-		}
-
-		// Add to field
-		if (!field.uploads) field.uploads = new Set();
-		field.uploads.add(uploadData.id);
-
-		// Recreate DOM element
-		const subtype = this.getSubtypeFromMime(uploadData.originalFile.type);
-		uploadData.element = this.createImageElement({
-			...uploadData,
-			subtype: subtype
-		}, field.destination === 'post_group');
-
-		// Restore to correct location
-		let location;
-		if (uploadData.groupId && field.ui.groups.groups.has(uploadData.groupId)) {
-			location = field.ui.groups.groups.get(uploadData.groupId).querySelector('.item-grid');
-		} else {
-			location = field.ui.field.preview;
-		}
-
-		if (location) {
-			location.appendChild(uploadData.element);
-			uploadData.location = location;
-		}
-
-		// Store in memory
-		this.uploads.set(uploadData.id, uploadData);
-	}
-
-	async restoreFieldStates(fieldStates) {
-		// Group by URL
-		const byUrl = new Map();
-		fieldStates.forEach(field => {
-			if (!byUrl.has(field.context.url)) {
-				byUrl.set(field.context.url, []);
-			}
-			byUrl.get(field.context.url).push(field);
-		});
-
-		// If all on current page, restore directly
-		if (byUrl.size === 1 && byUrl.has(window.location.href)) {
-			for (const fieldState of fieldStates) {
-				await this.restoreField(fieldState);
-			}
-			// this.notifications.add(`Restored ${fieldStates.length} field(s)`, 'success');
-		} else {
-			// Store intent to restore and navigate
-			sessionStorage.setItem('jvb_restore_uploads', JSON.stringify(fieldStates));
-
-			// Navigate to first URL
-			const firstUrl = byUrl.keys().next().value;
-			if (window.location.href !== firstUrl) {
-				window.location.href = firstUrl;
-			}
-		}
-	}
-
-	async restoreGroups(field, groups, uploads) {
-		// Ensure the groups.groups Map exists
-		if (!field.ui.groups.groups) {
-			field.ui.groups.groups = new Map();
-		}
-
-		for (const groupData of groups) {
-			// Create group element
-			const groupElement = this.createGroupElement(groupData.id, field.key);
-
-			// Store in field UI Map
-			field.ui.groups.groups.set(groupData.id, groupElement);
-
-			// Insert into DOM
-			if (field.ui.groups.container && field.ui.groups.empty) {
-				field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
-			} else if (field.ui.groups.container) {
-				field.ui.groups.container.appendChild(groupElement);
-			}
-
-			this.groups.set(groupData.id, {
-				id: groupData.id,
-				fieldId: field.key,
-				element: groupElement,
-				uploads: new Set(groupData.uploads), // FIXED: was groupData.uploadIds
-				meta: groupData.meta || {},
-				changes: groupData.changes || {}
-			});
-
-			// Move uploads to group
-			groupData.uploads.forEach(uploadId => {
-				const upload = uploads.find(u => u.id === uploadId);
-				if (upload && upload.element) {
-					const groupGrid = groupElement.querySelector('.item-grid');
-					if (groupGrid) {
-						groupGrid.appendChild(upload.element);
-						upload.location = groupGrid;
-						upload.groupId = groupData.id;
-					}
-				}
-			});
-		}
-	}
-
-	async getBlobData(uploadId) {
-		if (!this.db) return null;
-
-		const tx = this.db.transaction(['uploadBlobs'], 'readonly');
-		const request = tx.objectStore('uploadBlobs').get(uploadId);
-
-		return new Promise(resolve => {
-			request.onsuccess = () => resolve(request.result);
-			request.onerror = () => resolve(null);
-		});
-	}
-
-	async openModalForRestore(context) {
-		const { modalType, formId } = context;
-
-		// Find and click the appropriate button to open the modal
-		let trigger = null;
-
-		switch(modalType) {
-			case 'create':
-				trigger = document.querySelector('[data-action="create"]');
-				break;
-			case 'edit':
-				// Need to find the specific edit button
-				trigger = document.querySelector(`[data-action="edit"][data-id="${context.itemId}"]`);
-				break;
-			case 'bulkEdit':
-				trigger = document.querySelector('[data-action="bulk-edit"]');
-				break;
-		}
-
-		if (trigger) {
-			trigger.click();
-
-			// Wait for modal to open
-			await new Promise(resolve => setTimeout(resolve, 300));
-		}
-	}
-	/*******************************************************************************
-	 GROUP FUNCTIONALITY
-	 Includes selection, dragging, and grouping logic
-	*******************************************************************************/
-	/**
-	 *
-	 * @param {string} uploadId as defined by setUpload
-	 * @param {HTMLElement|null} target The target location
-	 * @param {boolean} persist whethet to cache this change
-	 */
-	addImageToGroup(uploadId, target = null, persist = true) {
-		let upload = this.getUpload(uploadId);
-		if(!upload) {
-			return;
-		}
-		let field = this.fields.get(upload.fieldId);
-		if (!field) {
-			return;
-		}
-
-		//Already in the Preview Grid, or already in the group we're moving to
-		if ((!target && upload.location === field.ui.field.preview) || target === upload.location) {
-			return;
-		}
-
-		// Remove from previous location
-		if (upload.location) {
-			let groupId = upload.location.dataset.groupId;
-			if (groupId) {
-				let group = this.groups.get(groupId);
-				if (group && group.uploads) {
-					group.uploads.delete(uploadId);
-
-					if (group.uploads.size === 0) {
-						this.removeGroup(groupId);
-					}
-				}
-			}
-		}
-
-		const checkbox = upload.element.querySelector('[name*="select-item"]');
-		if (checkbox) {
-			checkbox.checked = false;
-		}
-
-		upload.element.querySelector('[name="featured"]').hidden = !target;
-
-		//If no target, it's going to the preview grid
-		if (!target) {
-			target = field.ui.field.preview;
-		} else if (!target.classList.contains('item-grid') || !target.classList.contains('preview')) {
-			// It's a group target
-			let groupId = target.dataset.groupId;
-			let group = this.groups.get(groupId);
-			if (!group) {
-				group = this.createGroup(upload.fieldId);
-				target = group.grid;
-			}
-			if (group) {
-				group.uploads.add(uploadId);
-			}
-		}
-
-		upload.location = target;
-		target.append(upload.element);
-
-		if (persist) {
-			this.persistFieldState(field.key);
-		}
-	}
-
-	addSelectionToGroup(target) {
-		let field = this.getFieldFromElement(target);
-		if (!field) {
-			return;
-		}
-		let currentSelection = this.getCurrentSelection(field.key);
-		if (currentSelection.length === 0 ) {
-			return;
-		}
-
-		let group = this.getGroupFromElement(target);
-		if (!group && target !== field.ui.field.preview) {
-			group = this.createGroup(field.key);
-		}
-
-		currentSelection.forEach(uploadId => {
-			this.addImageToGroup(uploadId, group.grid??null, false);
-		});
-
-		this.persistFieldState(group.fieldId);
-	}
-
-	getCurrentSelection(fieldId) {
-		let selected = [];
-		for (var [key, handler] of this.selectionHandlers) {
-			if ((fieldId === key || key.includes(fieldId)) && handler.selectedItems.size > 0) {
-				selected = selected.concat([... handler.selectedItems]);
-			}
-		}
-		return selected;
-	}
-
-	/**
-	 * Remove an empty group from the field
-	 * @param {string} groupId - The group to remove
-	 * @param {boolean} confirm - ask for confirmation
-	 */
-	removeGroup(groupId, confirm = false) {
-		let group = this.groups.get(groupId);
-		if (!group) {
-			return;
-		}
-
-		if (confirm && group.uploads && group.uploads.size > 0) {
-			if(!window.confirm('This will delete this group. Any uploads in this group will return to the main grid. Are you sure?')){
-				return;
-			}
-		}
-
-		// Move any remaining uploads back to preview
-		if (group.uploads && group.uploads.size > 0) {
-			Array.from(group.uploads).forEach(uploadId => {
-				this.addImageToGroup(uploadId, null, false);
-			});
-		}
-
-		// Remove from groups Map
-		this.groups.delete(groupId);
-
-		// Remove DOM element
-		let groupElement = group.element;
-		if (groupElement) {
-			groupElement.remove();
-			this.a11y.announce('Group removed');
-		}
-
-		this.persistFieldState(group.fieldId);
-	}
-
-	/**
-	 * Create a new group
-	 */
-	createGroup(fieldKey) {
-		const field = this.fields.get(fieldKey);
-		if (!field) {
-			console.error('Field not found:', fieldKey);
-			return null;
-		}
-
-		const groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
-		const groupElement = this.createGroupElement(groupId, fieldKey);
-		if (!groupElement) {
-			console.error('Failed to create group element');
-			return null;
-		}
-
-		// Store in field UI Map
-		if (!field.ui.groups) {
-			field.ui.groups = {
-				groups: new Map(),
-				container: null,
-				empty: null,
-				display: null
-			};
-		}
-
-		field.ui.groups.groups.set(groupId, groupElement);
-
-		// Insert into DOM
-		if (field.ui.groups.container && field.ui.groups.empty) {
-			field.ui.groups.container.insertBefore(groupElement, field.ui.groups.empty);
-		} else if (field.ui.groups.container) {
-			field.ui.groups.container.appendChild(groupElement);
-		}
-
-		// Create group object
-		const group = {
-			id: groupId,
-			fieldId: fieldKey,
-			element: groupElement,
-			grid: groupElement.querySelector('.item-grid.group'),
-			uploads: new Set(),
-			meta: {},
-			changes: {}
-		};
-
-		// Store group
-		this.groups.set(groupId, group);
-
-		// Initialize selection handler for this group
-		this.addGroupSelectionHandler(fieldKey, groupId);
-
-		// Persist state
-		this.persistFieldState(fieldKey);
-
-		return group;
-	}
-
-
-	/**
-	 * Remove upload from group
-	 */
-	removeFromGroup(fieldId, uploadId, groupId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		const group = field.groups.find(g => g.id === groupId);
-		if (!group) return;
-
-		group.uploads = group.uploads.filter(id => id !== uploadId);
-
-		this.renderGroupUI(fieldId);
-		this.persistFieldState(field.key);
-	}
-
-	/**
-	 * Update group title
-	 */
-	updateGroupTitle(fieldId, groupId, title) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		const group = field.groups.find(g => g.id === groupId);
-		if (!group) return;
-
-		group.title = title;
-		this.persistFieldState(field.key);
-	}
-
-	/**
-	 * Delete group
-	 */
-	deleteGroup(fieldId, groupId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		field.groups = field.groups.filter(g => g.id !== groupId);
-
-		this.renderGroupUI(fieldId);
-		this.removeSelectionHandler(fieldId, groupId);
-		this.persistFieldState(field.key);
-	}
-
-	/**
-	 * Render group UI
-	 */
-	renderGroupUI(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field || !field.groups) return;
-
-		const container = field.ui.group.container;
-		if (!container) {
-			console.warn('Groups container not found for field:', fieldId);
-			return;
-		}
-
-		// Clear existing
-		window.removeChildren(container);
-
-		// Render each group
-		field.groups.forEach(group => {
-			const groupEl = this.createGroupElement(fieldId, group);
-			container.appendChild(groupEl);
-		});
-	}
-
-	createGroupElement(groupId, fieldId) {
-		let groupElement = window.getTemplate('imageGroup');
-		if (!groupElement) return;
-
-		groupElement.dataset.groupId = groupId;
-		groupElement.dataset.fieldId = fieldId;
-
-		let fields = window.getTemplate('groupMetadata');
-		const fieldsContainer = groupElement.querySelector('.fields');
-		if (fieldsContainer && fields) {
-			fieldsContainer.append(fields);
-
-			// Set unique IDs and names for form fields
-			const titleInput = fieldsContainer.querySelector('[name="post_title"]');
-			const excerptInput = fieldsContainer.querySelector('[name="post_excerpt"]');
-
-			if (titleInput) {
-				titleInput.id = `${groupId}_title`;
-				titleInput.name = `${groupId}[post_title]`;
-			}
-			if (excerptInput) {
-				excerptInput.id = `${groupId}_excerpt`;
-				excerptInput.name = `${groupId}[post_excerpt]`;
-			}
-			let field = this.fields.get(fieldId);
-			if (field.content !== '') {
-				let summary = groupElement.querySelector('summary');
-				summary.textContent = field.content + ' Fields';
-			}
-		} else {
-			groupElement.querySelector('details').remove();
-		}
-
-		const gridContainer = groupElement.querySelector('.item-grid.group');
-		if (gridContainer) {
-			gridContainer.dataset.groupId = groupId;
-		}
-
-		return groupElement;
-	}
-
-	handleSelectAll(element, checked = null) {
-		this.a11y.announce(checked ? 'All uploads selected' : 'All uploads deselected');
-	}
-
-	clearAllSelections(field) {
-		const handler = this.selectionHandlers.get(field.key);
-		if (handler) {
-			handler.clearSelection();
-		}
-	}
-
-	getSelectedUploads(element) {
-		const field = this.getFieldFromElement(element);
-		if (!field) return [];
-
-		const handler = this.selectionHandlers.get(field.key);
-		return handler ? handler.getSelected() : [];
-	}
-
-	removeSelection(button) {
-		let fieldId = this.getFieldIdFromElement(button);
-
-		const selectedUploads = this.getSelectedUploads(button);
+		const selectedUploads = this.getSelectedRestorationUploads(notification);
 		if (selectedUploads.length === 0) {
-			this.notify('No uploads selected', 'warning');
 			return;
 		}
+		await this.restoreSelectedUploads(selectedUploads);
 
-		selectedUploads.forEach(upload => {
-			this.removeUpload(fieldId, upload);
+		this.cleanupRestore();
+	}
+
+	async handleRestoreAll() {
+		let notification = document.querySelector('dialog.restore-uploads');
+		if (!notification) {
+			return;
+		}
+		// Gets ALL uploads from notification without checking selection
+		const allUploads = [];
+		notification.querySelectorAll('.item.upload').forEach(item => {
+			let uploadId = item.dataset.uploadId;
+			let fieldId = item.dataset.fieldId;
+			allUploads.push({ uploadId, fieldId });
 		});
+
+		await this.restoreSelectedUploads(allUploads);
+		this.cleanupRestore();
 	}
 
-	removeUpload(fieldId, uploadId) {
-		const field = this.fields.get(fieldId);
-		const upload = this.uploads.get(uploadId);
-
-		if (!field || !upload) return;
-
-		// Remove from field
-		field.uploads?.delete(uploadId);
-
-		// Remove from group if grouped
-		if (upload.groupId) {
-			const group = this.groups.get(upload.groupId);
-			if (group && group.uploads) {
-				group.uploads.delete(uploadId);
-
-				if (group.uploads.size === 0) {
-					this.removeGroup(upload.groupId);
-				}
-			}
-		}
-
-		// Clean up element
-		upload.element?.remove();
-
-		// Clean up memory
-		this.clearUpload(uploadId);
-
-		// Update field state after removal
-		this.updateFieldState(fieldId);
-
-		// Update UI
-		this.maybeLockUploads(fieldId);
-		const handler = this.selectionHandlers.get(field.key);
-		if (handler) {
-			handler.deselect(uploadId);
-		}
-
-		this.a11y.announce('Upload removed');
+	showSaveIndicator(key) {
+		// Optional: show user that state is being saved
 	}
 
-	/**************************************************************************
-	 META
-	 Handled separately, in case it is edited in the middle of processing images
-	**************************************************************************/
+	cleanupRestore() {
+		this.restoreModal.handleClose();
+		this.restoreSelection.destroy();
+		this.restoreSelection = null;
+		this.restoreModal.destroy();
+		this.restoreModal.modal.remove();
+		this.restoreModal = null;
+	}
 
-	/**************************************************************************
-	 SUBSCRIBERS
-	**************************************************************************/
-	/**
-	 * Event system
-	 */
+	async cleanupStoredUploads() {
+		await this.fieldStore.clear();
+		await this.uploadStore.clear();
+	}
+
+	/*******************************************************************************
+	 * EVENT SYSTEM
+	 *******************************************************************************/
+
 	subscribe(callback) {
 		this.subscribers.add(callback);
 		return () => this.subscribers.delete(callback);
 	}
 
-	notify(event, data) {
-		this.subscribers.forEach(cb => cb(event, data));
-	}
-
-	handleBeforeUnload(e) {
-		// Check for any uploads in processing or pending state
-		const unsavedUploads = Array.from(this.uploads.values()).filter(upload =>
-			upload.status === 'processing' ||
-			upload.status === 'pending' ||
-			upload.status === 'uploading'
-		);
-
-		if (unsavedUploads.length > 0) {
-			const message = 'You have uploads in progress. Are you sure you want to leave?';
-			e.preventDefault();
-			e.returnValue = message;
-			return message;
-		}
-	}
-	/**************************************************************************
-	 CLEANUP
-	**************************************************************************/
-	cleanup() {
-		this.clearListeners();
-		if (this.hasGroups) {
-			this.clearGroupListeners();
-		}
-		this.compressionWorker = null;
-		this.subscribers.clear();
-	}
-
-	/**
-	 * Clear individual upload from cache after successful server upload
-	 */
-	async clearUpload(uploadId) {
-		const upload = this.uploads.get(uploadId);
-		if (!upload) return;
-
-		// Clean up preview URL
-		if (upload.preview && upload.preview.startsWith('blob:')) {
-			URL.revokeObjectURL(upload.preview);
-			upload.preview = null;
-		}
-
-		// Clean up element preview URL
-		if (upload.element) {
-			const previewUrl = upload.element.dataset.previewUrl;
-			if (previewUrl && previewUrl.startsWith('blob:')) {
-				URL.revokeObjectURL(previewUrl);
-				delete upload.element.dataset.previewUrl;
+	notify(event, data = {}) {
+		this.subscribers.forEach(cb => {
+			try {
+				cb(event, data);
+			} catch (error) {
+				console.error('Subscriber error:', error);
 			}
-		}
-
-		this.persistFieldState(upload.fieldId);
-		// Remove from memory
-		this.uploads.delete(uploadId);
-		this.uploadBlobs.delete(uploadId);
-
-		// Remove from IndexedDB
-		if (this.db) {
-			const tx = this.db.transaction(['uploadBlobs'], 'readwrite');
-			await tx.objectStore('uploadBlobs').delete(uploadId);
-		}
+		});
 	}
 
-	/**
-	 * Clear all uploads for a field and cleanup resources
-	 */
-	clearField(fieldId) {
-		const field = this.fields.get(fieldId);
-		if (!field) return;
+	/*******************************************************************************
+	 * DESTROY & CLEANUP
+	 *******************************************************************************/
 
-		const uploads = Array.from(field.uploads || []);
+	destroy() {
+		document.removeEventListener('click', this.clickHandler);
+		document.removeEventListener('change', this.changeHandler);
+		document.removeEventListener('dragenter', this.dragEnterHandler);
+		document.removeEventListener('dragleave', this.dragLeaveHandler);
+		document.removeEventListener('dragover', this.dragOverHandler);
+		document.removeEventListener('drop', this.dropHandler);
 
-		// Cleanup each upload's resources
-		uploads.forEach(uploadId => {
-			this.clearUpload(uploadId);
-			this.uploads.delete(uploadId);
-		});
-
-		// Clear field state
-		this.fields.delete(fieldId);
-
-		// Cleanup IndexedDB
-		if (this.db) {
-			const tx = this.db.transaction(['fieldStates', 'uploadBlobs'], 'readwrite');
-			tx.objectStore('fieldStates').delete(fieldId);
-			uploads.forEach(uploadId => {
-				tx.objectStore('uploadBlobs').delete(uploadId);
-			});
+		if (this.dragController) {
+			this.dragController.destroy();
 		}
+
+		this.selectionHandlers.forEach(handler => handler.destroy());
+		this.selectionHandlers.clear();
+
+		this.cleanupAllPreviewUrls();
+
+		this.sortableInstances.forEach(instance => {
+			if (instance?.destroy) instance.destroy();
+		});
+		this.sortableInstances.clear();
+
+		this.uploadElements.clear();
+		this.fieldElements.clear();
+		this.groupElements.clear();
+		this.selected.clear();
+		this.subscribers.clear();
 	}
 }
 
-document.addEventListener('DOMContentLoaded', () => {
-	window.jvbUploads = new UploadManager();
+// Initialize when DOM is ready
+document.addEventListener('DOMContentLoaded', async function () {
+	window.auth.subscribe((event) => {
+		if (event === 'auth-loaded') {
+			window.jvbUploads = new UploadManager();
+		}
+	});
 });

--
Gitblit v1.10.0