/**
 * Frontend Blockora Morph editor shell.
 * Pencil FAB + hover/select on BM blocks only + shared BlockInspectorPanel + Save.
 */
import apiFetch from '@wordpress/api-fetch';
import {
	Button,
	Dropdown,
	Popover,
	SlotFillProvider,
	Spinner,
} from '@wordpress/components';
import {
	createElement,
	createPortal,
	createRoot,
	Fragment,
	useCallback,
	useEffect,
	useMemo,
	useRef,
	useState,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import domReady from '@wordpress/dom-ready';
import BlockInspectorPanel from '../shared/BlockInspectorPanel';
import {
	getLibraryStylesMap,
	refreshButtonLibrary,
	subscribeButtonLibrary,
} from '../shared/buttonLibrary';
import {
	flushPendingLibraryCssVars,
	hasPendingLibraryCssVars,
} from '../shared/libraryCssVars';
import {
	createLinkedPlacementSetAttributes,
	resolvePlacementCssVars,
} from '../shared/instanceStyleFork';
import {
	isContentLinkedToMyBlock,
	resolveInstanceContentAttrs,
} from '../shared/instanceContentAttrs';
import { scrollInspectorToTop } from '../shared/inspectorScroll';
import { brandLogoUrl } from '../shared/brandLogo';
import { useDocumentHistory } from '../shared/useDocumentHistory';
import { cloneState } from '../shared/attributeHistory';
import {
	markStyleableAndSelected,
	slideIndexFromElement,
	cardIndexFromElement,
	isSliderChromeTarget,
	syncSelectionChrome,
	wrapMediaHitTargets,
	forceOpenDetailsForEditing,
	bindForceOpenDetails,
	activateBmTabForEditing,
} from '../shared/selectionChrome';
import { focusBmSliderSlide, releaseBmSliderFocus } from '../shared/sliderElements';
import {
	collectionItemChromeActionsAllowed,
	collectionMinItems,
	collectionRemoveConfirmCopy,
	duplicateCollectionItemAt,
	findCollectionRepeater,
	moveCollectionItemAt,
} from '../shared/collectionRepeater';
import ConfirmModal from '../components/ConfirmModal';
import { initBmTabsNav } from '../shared/bmTabsNav';
import {
	initBmAccordion,
	enforceAccordionExclusiveOpen,
} from '../shared/bmAccordion';
import {
	appendAccordionItemToAttributes,
	appendCardToAttributes,
	appendTabToAttributes,
	injectPreviewAccordionAddButtons,
	injectPreviewCardsAddButtons,
	injectPreviewTabsAddButtons,
	isAccordionAddButtonTarget,
	isCardsAddButtonTarget,
	isTabsAddButtonTarget,
} from '../shared/previewTabsAdd';
import {
	bindPreviewContentEdit,
	canInlineEditKey,
	endPreviewContentEditForLinkEditor,
} from '../shared/previewContentEdit';
import { hydratePendingInlineIcons } from '../shared/iconRegistry';
import PreviewFormatToolbar from '../components/fields/PreviewFormatToolbar';
import { buttonHoverCss, interpolate } from '../interpolate';
import {
	DEVICE_WIDTH_PX,
	elementFontFamilyCssFromVars,
	elementSpacingCssFromVars,
	merge,
	mergeLibraryPreview,
	normalize,
	styleableFieldsInOrder,
	toCss,
	toCssForBreakpoint,
} from '../cssVars';
import { toCssForSlides } from '../shared/slideLayoutStyle';
import {
	elementsOrderDefaultsFromSchema,
	toElementsOrderCssForAttrs,
} from '../shared/elementsOrder';
import { toElementsVisibilityCssForAttrs } from '../shared/elementsVisibility';
import './frontend-editor.scss';
function getConfig() {
	return typeof window !== 'undefined' && window.jocbmFront
		? window.jocbmFront
		: {
				restUrl: '',
				nonce: '',
				postId: 0,
				blocksByName: {},
				instances: [],
		  };
}

function findInstance( cfg, el ) {
	const surface = el.getAttribute( 'data-bm-surface' );
	const name = el.getAttribute( 'data-bm-name' ) || '';
	if ( surface === 'shortcode' ) {
		const contentId = Number( el.getAttribute( 'data-bm-content-id' ) || 0 );
		return (
			( cfg.instances || [] ).find(
				( i ) =>
					i.surface === 'shortcode' && Number( i.contentId ) === contentId
			) || {
				surface: 'shortcode',
				contentId,
				name,
				attributes: {},
			}
		);
	}
	const blockIndex = Number( el.getAttribute( 'data-bm-index' ) || 0 );
	return (
		( cfg.instances || [] ).find(
			( i ) =>
				i.surface === 'gutenberg' && Number( i.blockIndex ) === blockIndex
		) || {
			surface: 'gutenberg',
			blockIndex,
			name,
			attributes: {},
		}
	);
}

/**
 * Stable id for remembering library/instance style-scope choices.
 *
 * @param {Element|null} el
 * @return {string}
 */
function styleScopeSurfaceIdFromEl( el ) {
	if ( ! el ) {
		return '';
	}
	const surface = el.getAttribute( 'data-bm-surface' ) || 'gutenberg';
	if ( surface === 'shortcode' ) {
		return `sc-${ el.getAttribute( 'data-bm-content-id' ) || 0 }`;
	}
	return `gb-${ el.getAttribute( 'data-bm-index' ) || 0 }-${
		el.getAttribute( 'data-bm-name' ) || ''
	}`;
}

function mergeAttrs( meta, instanceAttrs ) {
	const out = { ...( meta?.defaultAttrs || {} ), ...( instanceAttrs || {} ) };
	if ( ! out.cssVars ) {
		out.cssVars = {};
	}
	return out;
}

function instanceCacheKey( inst ) {
	if ( ! inst ) {
		return '';
	}
	if ( inst.surface === 'shortcode' ) {
		return `shortcode:${ Number( inst.contentId ) || 0 }`;
	}
	return `gutenberg:${ Number( inst.blockIndex ) || 0 }`;
}

function cloneAttrs( attrs ) {
	try {
		return JSON.parse( JSON.stringify( attrs || {} ) );
	} catch ( e ) {
		return { ...( attrs || {} ) };
	}
}

/** Pop-out window — disabled until layout/portal issues are fully settled. */
const ENABLE_FE_POPOUT = false;

const SHELL_PREFS_KEY = 'joc-blockora-morph-fe-shell';

function loadShellPrefs() {
	try {
		const raw = window.localStorage?.getItem( SHELL_PREFS_KEY );
		if ( ! raw ) {
			return { side: 'right', minimized: false, editing: false };
		}
		const parsed = JSON.parse( raw );
		return {
			side: parsed?.side === 'left' ? 'left' : 'right',
			minimized: Boolean( parsed?.minimized ),
			editing: Boolean( parsed?.editing ),
		};
	} catch ( e ) {
		return { side: 'right', minimized: false, editing: false };
	}
}

function saveShellPrefs( prefs ) {
	try {
		window.localStorage?.setItem(
			SHELL_PREFS_KEY,
			JSON.stringify( {
				side: prefs.side === 'left' ? 'left' : 'right',
				minimized: Boolean( prefs.minimized ),
				editing: Boolean( prefs.editing ),
			} )
		);
	} catch ( e ) {
		// Ignore quota / private mode.
	}
}

function ShellHeaderButton( { icon, label, pressed, onClick } ) {
	return createElement(
		'button',
		{
			type: 'button',
			className:
				'bm-fe-shell__tool' + ( pressed ? ' is-active' : '' ),
			'aria-label': label,
			title: label,
			'aria-pressed':
				typeof pressed === 'boolean' ? pressed : undefined,
			onClick,
		},
		createElement( 'span', {
			className: `dashicons dashicons-${ icon }`,
			'aria-hidden': true,
		} )
	);
}

function ShellDockTools( { side, onSetSide } ) {
	return createElement(
		Fragment,
		null,
		createElement( ShellHeaderButton, {
			icon: 'align-pull-left',
			label: __( 'Dock left', 'joc-blockora-morph' ),
			pressed: side === 'left',
			onClick: () => onSetSide( 'left' ),
		} ),
		createElement( ShellHeaderButton, {
			icon: 'align-pull-right',
			label: __( 'Dock right', 'joc-blockora-morph' ),
			pressed: side === 'right',
			onClick: () => onSetSide( 'right' ),
		} )
	);
}

function ShellHeader( {
	title,
	side,
	onSetSide,
	onMinimize,
	onPopOut,
	isPopout = false,
	onDockBack,
	styleBreakpoint,
	onStyleBreakpointChange,
	onViewSwitch,
} ) {
	const bpButtons = [
		{ id: 'desktop', label: __( 'Desktop', 'joc-blockora-morph' ), icon: 'desktop' },
		{ id: 'tablet', label: __( 'Tablet', 'joc-blockora-morph' ), icon: 'tablet' },
		{ id: 'mobile', label: __( 'Mobile', 'joc-blockora-morph' ), icon: 'smartphone' },
	];

	return createElement(
		'header',
		{ className: 'bm-fe-shell__header' },
		createElement(
			'strong',
			{ className: 'bm-fe-shell__title' },
			createElement('img', {
				className: 'bm-fe-shell__logo',
				src: brandLogoUrl(),
				alt: '',
				width: 24,
				height: 24,
				decoding: 'async',
			}),
			createElement('span', { className: 'bm-fe-shell__title-text' }, title)
		),
		createElement(
			'div',
			{
				className: 'bm-fe-shell__tools',
				role: 'toolbar',
				'aria-label': __('Panel controls', 'joc-blockora-morph'),
			},
			!isPopout
				? bpButtons.map((bp) =>
						createElement(
							'button',
							{
								key: bp.id,
								type: 'button',
								className:
									'bm-gutenberg-seg-btn' +
									(styleBreakpoint === bp.id ? ' is-active' : ''),
								'aria-pressed': styleBreakpoint === bp.id,
								'aria-label': bp.label,
								title: bp.label,
								onClick: () => {
									if (styleBreakpoint === bp.id) {
										return;
									}
									onViewSwitch?.();
									onStyleBreakpointChange(bp.id);
								},
							},
							createElement('span', {
								className: `dashicons dashicons-${bp.icon}`,
								'aria-hidden': true,
							})
						)
				  )
				: null,
			! isPopout
				? createElement( ShellHeaderButton, {
						icon: 'editor-contract',
						label: __( 'Minimize panel', 'joc-blockora-morph' ),
						onClick: onMinimize,
				  } )
				: null,
			ENABLE_FE_POPOUT && !isPopout
				? createElement( ShellHeaderButton, {
						icon: 'external',
						label: __( 'Open in new window', 'joc-blockora-morph' ),
						onClick: onPopOut,
				  } )
				: null,
			ENABLE_FE_POPOUT && isPopout
				? createElement( ShellHeaderButton, {
						icon: 'layout',
						label: __( 'Dock back to page', 'joc-blockora-morph' ),
						onClick: onDockBack,
				  } )
				: null,
			! isPopout
				? createElement( Dropdown, {
						className: 'bm-fe-shell__dock-dropdown',
						contentClassName: 'bm-fe-shell__dock-menu',
						popoverProps: {
							placement: 'bottom-end',
							offset: 6,
							focusOnMount: 'container',
						},
						renderToggle: ( { isOpen, onToggle } ) =>
							createElement( ShellHeaderButton, {
								icon:
									side === 'left'
										? 'align-pull-left'
										: 'align-pull-right',
								label: __( 'Dock position', 'joc-blockora-morph' ),
								onClick: onToggle,
								pressed: isOpen,
							} ),
						renderContent: ( { onClose } ) =>
							createElement(
								'div',
								{
									className: 'bm-fe-shell__dock-menu-inner',
									role: 'menu',
								},
								createElement(
									'button',
									{
										type: 'button',
										role: 'menuitemradio',
										'aria-checked': side === 'left',
										className:
											'bm-fe-shell__dock-option' +
											( side === 'left'
												? ' is-active'
												: '' ),
										onClick: () => {
											onSetSide( 'left' );
											onClose();
										},
									},
									createElement( 'span', {
										className:
											'dashicons dashicons-align-pull-left',
										'aria-hidden': true,
									} ),
									__( 'Left', 'joc-blockora-morph' )
								),
								createElement(
									'button',
									{
										type: 'button',
										role: 'menuitemradio',
										'aria-checked': side === 'right',
										className:
											'bm-fe-shell__dock-option' +
											( side === 'right'
												? ' is-active'
												: '' ),
										onClick: () => {
											onSetSide( 'right' );
											onClose();
										},
									},
									createElement( 'span', {
										className:
											'dashicons dashicons-align-pull-right',
										'aria-hidden': true,
									} ),
									__( 'Right', 'joc-blockora-morph' )
								)
							),
				  } )
				: null
		)
	);
}

function ShellExpandButton( { onToggle } ) {
	return createElement( ShellHeaderButton, {
		icon: 'editor-expand',
		label: __( 'Maximize panel', 'joc-blockora-morph' ),
		onClick: onToggle,
	} );
}

function ShellHintBody( { side } ) {
	const pointsLeft = side !== 'left';
	return createElement(
		'div',
		{
			className:
				'bm-fe-hint' +
				( pointsLeft ? ' bm-fe-hint--point-left' : ' bm-fe-hint--point-right' ),
		},
		createElement(
			'div',
			{
				className: 'bm-fe-hint__pointer',
				'aria-hidden': true,
			},
			createElement(
				'div',
				{ className: 'bm-fe-hint__hand-wrap' },
				// Provided SVG points left; mirrored via CSS when pointing right.
				createElement(
					'svg',
					{
						className: 'bm-fe-hint__hand',
						viewBox: '0 0 56 56',
						width: 88,
						height: 88,
						fill: '#3525cd',
						xmlns: 'http://www.w3.org/2000/svg',
						'aria-hidden': true,
					},
					createElement( 'path', {
						d: 'M 52.6562 30.8945 C 52.6562 22.6211 48.2032 16.8320 39.4141 13.7148 L 27.8126 9.5898 C 24.8595 8.5586 22.4688 9.4727 21.5548 11.9336 C 20.7579 14.1367 21.6954 16.2461 24.1329 17.2774 L 27.8829 18.8476 C 27.9766 18.8711 28.0470 18.9414 28.0470 19.0352 C 28.0470 19.1523 27.9532 19.2227 27.8360 19.2227 L 8.2657 19.2227 C 5.3126 19.2227 3.3438 21.1914 3.3438 23.9805 C 3.3438 26.7461 5.3126 28.7148 8.2657 28.7148 L 16.0704 28.7148 C 15.9063 29.2774 15.8360 29.8398 15.8360 30.3789 C 15.8360 32.4180 16.9844 33.9648 18.8829 34.5742 C 18.6485 35.2774 18.5313 35.9570 18.5313 36.6133 C 18.5313 38.5820 19.5860 39.9883 21.2970 40.5274 C 21.3438 44.8867 24.5079 47.4414 29.9219 47.4414 L 35.0313 47.4414 C 46.0001 47.4414 52.6562 40.8086 52.6562 30.8945 Z M 49.4219 31.0117 C 49.4219 39.2852 44.0782 44.3477 34.6563 44.3477 L 30.3907 44.3477 C 26.6641 44.3477 24.5079 43.1523 24.5079 41.0664 L 27.2032 41.0664 C 28.0704 41.0664 28.6329 40.3867 28.6329 39.6133 C 28.6329 38.8164 28.0704 38.0898 27.2032 38.0898 L 23.5470 38.0898 C 22.3282 38.0898 21.5548 37.4101 21.5548 36.2148 C 21.5548 35.7696 21.6719 35.2305 21.8595 34.8320 L 26.2892 34.8320 C 27.1563 34.8320 27.7423 34.1289 27.7423 33.3555 C 27.7423 32.5586 27.1563 31.8555 26.2892 31.8555 L 20.8516 31.8555 C 19.6095 31.8555 18.8595 31.1523 18.8595 29.9805 C 18.8595 29.5117 18.9766 28.9727 19.1641 28.5742 L 25.3985 28.5742 C 26.2188 28.5742 26.8516 27.9180 26.8516 27.1211 C 26.8516 26.3008 26.2188 25.5976 25.3985 25.5976 L 8.0548 25.5976 C 7.0704 25.5976 6.3673 24.9414 6.3673 23.9805 C 6.3673 23.0196 7.0704 22.3398 8.0548 22.3398 L 32.3595 22.3398 C 33.4376 22.3398 34.2110 21.6367 34.2110 20.6523 C 34.2110 19.8320 33.8360 19.1055 32.6641 18.5898 L 25.5392 15.4023 C 24.5313 14.9570 23.9923 14.1367 24.3438 13.2461 C 24.6954 12.3086 25.6095 11.9805 26.8282 12.4258 L 38.4063 16.5742 C 46.3985 19.4336 49.4219 24.4492 49.4219 31.0117 Z',
					} )
				)
			)
		),
		createElement(
			'div',
			{ className: 'bm-fe-hint__copy' },
			createElement(
				'p',
				{ className: 'bm-fe-hint__eyebrow' },
				__( 'Ready to edit', 'joc-blockora-morph' )
			),
			createElement(
				'h2',
				{ className: 'bm-fe-hint__title' },
				__( 'Click any Blockora Morph block', 'joc-blockora-morph' )
			),
			createElement(
				'p',
				{ className: 'bm-fe-hint__text' },
				__(
					'Hover a block on the page, then click it to open its fields and styles here.',
					'joc-blockora-morph'
				)
			)
		)
	);
}

function seedPopoutDocument( win ) {
	const doc = win.document;
	doc.open();
	doc.write(
		'<!DOCTYPE html><html><head><meta charset="utf-8" />' +
			'<meta name="viewport" content="width=device-width, initial-scale=1" />' +
			'<title>Blockora Morph</title></head><body></body></html>'
	);
	doc.close();

	doc.documentElement.classList.add( 'bm-fe-popout-html' );
	doc.body.className = 'bm-fe-popout-body';
	doc.title = __( 'Blockora Morph editor', 'joc-blockora-morph' );

	const critical = doc.createElement( 'style' );
	critical.textContent = `
		html.bm-fe-popout-html, body.bm-fe-popout-body, #bm-fe-popout-root {
			box-sizing: border-box;
			width: 100%;
			height: 100%;
			margin: 0;
			padding: 0;
			overflow: hidden;
			background: #faf8ff;
		}
		*, *::before, *::after { box-sizing: border-box; }
		.bm-fe-shell--popout {
			position: fixed !important;
			inset: 0 !important;
			width: 100% !important;
			height: 100% !important;
			min-height: 0 !important;
			border: none !important;
			box-shadow: none !important;
			display: flex !important;
			flex-direction: column !important;
		}
		.bm-fe-shell--popout .bm-fe-shell__body {
			flex: 1 1 auto !important;
			min-height: 0 !important;
			overflow: auto !important;
		}
		.bm-fe-shell--popout .bm-fe-shell__header,
		.bm-fe-shell--popout .bm-fe-shell__footer {
			flex-shrink: 0 !important;
		}
	`;
	doc.head.appendChild( critical );

	const allow =
		/joc-blockora-morph|frontend-editor|dashicons|wp-components|wp-block-editor|buttons\.min|forms\.min|common\.min|editor\.min/i;
	const seen = new Set();
	document.querySelectorAll( 'link[rel="stylesheet"]' ).forEach( ( link ) => {
		const href = link.href || '';
		if ( ! href || seen.has( href ) || ! allow.test( href ) ) {
			return;
		}
		seen.add( href );
		const clone = doc.createElement( 'link' );
		clone.rel = 'stylesheet';
		clone.href = href;
		doc.head.appendChild( clone );
	} );

	// Inline sheets (some WP component CSS is printed as <style>).
	document.querySelectorAll( 'style' ).forEach( ( style ) => {
		const id = style.id || '';
		const text = style.textContent || '';
		if (
			! /joc-blockora-morph|bm-fe|components-|dashicons/i.test( id ) &&
			! /components-|bm-fe-|bm-gutenberg/i.test( text )
		) {
			return;
		}
		const clone = doc.createElement( 'style' );
		if ( id ) {
			clone.id = id;
		}
		clone.textContent = text;
		doc.head.appendChild( clone );
	} );

	const mount = doc.createElement( 'div' );
	mount.id = 'bm-fe-popout-root';
	mount.className = 'bm-fe-popout-root';
	doc.body.appendChild( mount );
	return mount;
}

function PopoutStatusBar( { onDockBack, onClose } ) {
	return createElement(
		'div',
		{
			className: 'bm-fe-popout-bar',
			role: 'status',
		},
		createElement(
			'span',
			{ className: 'bm-fe-popout-bar__text' },
			__( 'Editor is open in a separate window', 'joc-blockora-morph' )
		),
		createElement(
			'div',
			{ className: 'bm-fe-popout-bar__actions' },
			createElement(
				Button,
				{ variant: 'secondary', onClick: onDockBack },
				__( 'Dock back', 'joc-blockora-morph' )
			),
			createElement(
				Button,
				{ variant: 'tertiary', onClick: onClose },
				__( 'Close', 'joc-blockora-morph' )
			)
		)
	);
}

function EditorShell( {
	mode = 'docked',
	className,
	title,
	side,
	minimized,
	isHint,
	onSetSide,
	onMinimize,
	onPopOut,
	onDockBack,
	onClose,
	onSave,
	canSave,
	saving,
	saveMsg,
	body,
	styleBreakpoint,
	onStyleBreakpointChange,
	onViewSwitch,
} ) {
	const isPopout = mode === 'popout';
	const shellClass = [
		'bm-fe-shell',
		isPopout ? 'bm-fe-shell--popout' : `bm-fe-shell--dock-${ side }`,
		! isPopout && minimized ? 'is-minimized' : '',
		isHint ? 'bm-fe-shell--hint' : '',
		className || '',
	]
		.filter( Boolean )
		.join( ' ' );

	const showChrome = isPopout || ! minimized;

	const shell = createElement(
		'aside',
		{
			className: shellClass,
			'aria-label': __( 'Blockora Morph editor', 'joc-blockora-morph' ),
		},
		showChrome
			? createElement( ShellHeader, {
					title,
					side,
					onSetSide,
					onMinimize,
					onPopOut,
					isPopout,
					onDockBack,
					styleBreakpoint,
					onStyleBreakpointChange,
					onViewSwitch,
			  } )
			: null,
		showChrome ? body : null,
		createElement(
			'footer',
			{ className: 'bm-fe-shell__footer' },
			! isPopout && minimized
				? createElement(
						'strong',
						{ className: 'bm-fe-shell__title' },
						createElement( 'img', {
							className: 'bm-fe-shell__logo',
							src: brandLogoUrl(),
							alt: '',
							width: 24,
							height: 24,
							decoding: 'async',
						} ),
						createElement(
							'span',
							{ className: 'bm-fe-shell__title-text' },
							title
						)
				  )
				: null,
			! isPopout && minimized
				? createElement(
						'div',
						{
							className: 'bm-fe-shell__tools',
							role: 'toolbar',
							'aria-label': __( 'Panel size', 'joc-blockora-morph' ),
						},
						createElement( ShellExpandButton, {
							onToggle: onMinimize,
						} ),
						ENABLE_FE_POPOUT
							? createElement( ShellHeaderButton, {
									icon: 'external',
									label: __(
										'Open in new window',
										'joc-blockora-morph'
									),
									onClick: onPopOut,
							  } )
							: null,
						createElement( ShellDockTools, {
							side,
							onSetSide,
						} )
				  )
				: null,
			showChrome && saveMsg
				? createElement(
						'span',
						{ className: 'bm-fe-shell__status' },
						saveMsg
				  )
				: null,
			createElement(
				'div',
				{ className: 'bm-fe-shell__actions' },
				isPopout
					? createElement(
							Button,
							{
								variant: 'secondary',
								onClick: onDockBack,
							},
							__( 'Dock back', 'joc-blockora-morph' )
					  )
					: null,
				createElement(
					Button,
					{
						variant: isPopout ? 'tertiary' : 'secondary',
						onClick: onClose,
					},
					__( 'Close', 'joc-blockora-morph' )
				),
				showChrome && typeof onSave === 'function'
					? createElement(
							Button,
							{
								variant: 'primary',
								disabled: ! canSave || saving,
								onClick: onSave,
							},
							saving
								? createElement( Spinner )
								: __( 'Save', 'joc-blockora-morph' )
					  )
					: null
			)
		)
	);

	// SlotFill keeps dropdown/popover content inside the pop-out document.
	if ( ! isPopout ) {
		return shell;
	}
	return createElement(
		SlotFillProvider,
		null,
		createElement(
			Fragment,
			null,
			shell,
			Popover.Slot ? createElement( Popover.Slot ) : null
		)
	);
}

function attrsEqual( a, b ) {
	try {
		return JSON.stringify( a ) === JSON.stringify( b );
	} catch ( e ) {
		return false;
	}
}

const VIEWPORT_CLASS = {
	desktop: 'bm-fe-viewport-desktop',
	tablet: 'bm-fe-viewport-tablet',
	mobile: 'bm-fe-viewport-mobile',
};

/**
 * Write breakpoint-aware CSS into the block's style tag (no DOM rewrite).
 * When breakpoint is set, uses flat effective vars — real @media still sees
 * the browser window, so FE tablet/mobile preview cannot rely on media queries.
 *
 * @param {HTMLElement} wrapper
 * @param {object}      meta
 * @param {object}      attributes
 * @param {string|null} breakpoint  desktop|tablet|mobile, or null for full responsive CSS
 * @param {object|null} libraryCssVars
 * @param {object|null} libraryBaseline
 */
function applyLiveCss(
	wrapper,
	meta,
	attributes,
	breakpoint = null,
	libraryCssVars = null,
	libraryBaseline = null
) {
	if ( ! wrapper || ! meta ) {
		return;
	}
	const fieldAttrs = resolveInstanceContentAttrs( meta, attributes );

	const libStyles = getLibraryStylesMap();
	const library = libraryCssVars != null ? libraryCssVars : meta.cssVars || {};
	const baseline =
		libraryBaseline != null ? libraryBaseline : library;
	const cssVars = resolvePlacementCssVars(
		attributes,
		library,
		baseline,
		mergeLibraryPreview
	);
	const slug = meta.slug || '';
	const uid =
		wrapper.className
			.split( /\s+/ )
			.find( ( c ) => c.startsWith( 'bm-i-' ) ) || '';
	const base = `.wp-block-joc-blockora-morph-${ slug }`;
	const rootSel = uid ? `.${ uid }${ base }` : base;
	const previewBp = [ 'desktop', 'tablet', 'mobile' ].includes( breakpoint )
		? breakpoint
		: null;
	// Scope under html.bm-fe-viewport-* so forced vars beat linked library @media sheets.
	const selector = previewBp
		? `html.${ VIEWPORT_CLASS[ previewBp ] } ${ rootSel }`
		: rootSel;
	const css =
		( previewBp
			? toCssForBreakpoint( selector, cssVars, previewBp )
			: toCss( selector, cssVars ) ) +
		( buttonHoverCss( selector, fieldAttrs, 'buttons', libStyles )
			? '\n\n' + buttonHoverCss( selector, fieldAttrs, 'buttons', libStyles )
			: '' );
	const slidesCss = toCssForSlides(
		selector,
		Array.isArray( fieldAttrs.slides ) ? fieldAttrs.slides : [],
		cssVars,
		previewBp
	);
	const orderCss = toElementsOrderCssForAttrs(
		selector,
		fieldAttrs,
		elementsOrderDefaultsFromSchema( meta.fieldSchema || [], fieldAttrs ),
		previewBp
	);
	const visCss = toElementsVisibilityCssForAttrs(
		selector,
		fieldAttrs,
		[],
		previewBp
	);
	const fontCss = elementFontFamilyCssFromVars( selector, cssVars );
	const spacingCss = elementSpacingCssFromVars( selector, cssVars );
	const fullCss = [ css, slidesCss, orderCss, visCss, fontCss, spacingCss ]
		.filter( Boolean )
		.join( '\n\n' );

	if ( previewBp ) {
		wrapper.setAttribute( 'data-bm-breakpoint', previewBp );
	} else {
		wrapper.removeAttribute( 'data-bm-breakpoint' );
	}

	let styleEl = uid
		? document.getElementById( `${ uid }-css` )
		: wrapper.previousElementSibling?.tagName === 'STYLE'
		? wrapper.previousElementSibling
		: null;
	if ( ! styleEl && fullCss ) {
		styleEl = document.createElement( 'style' );
		if ( uid ) {
			styleEl.id = `${ uid }-css`;
		}
		styleEl.setAttribute( 'data-bm-fe-preview-css', '1' );
		wrapper.parentNode?.insertBefore( styleEl, wrapper );
	}
	if ( styleEl ) {
		styleEl.setAttribute( 'data-bm-fe-preview-css', '1' );
		styleEl.textContent = fullCss;
	}
}

function applyLivePreview(
	wrapper,
	meta,
	attributes,
	breakpoint = null,
	libraryCssVars = null,
	libraryBaseline = null
) {
	if ( ! wrapper || ! meta?.markup ) {
		return;
	}
	const fieldAttrs = resolveInstanceContentAttrs( meta, attributes );

	const libStyles = getLibraryStylesMap();
	// Only skip DOM rewrite while the user is actively typing in the canvas.
	// Style reference / Design changes happen from the inspector (focus leaves
	// the contenteditable) and must refresh the preview immediately.
	const editTarget = wrapper.querySelector( '[data-bm-content-edit="1"]' );
	const doc = wrapper.ownerDocument || document;
	const active = doc.activeElement;
	const isInlineEditing = Boolean(
		editTarget &&
			active &&
			( active === editTarget || editTarget.contains( active ) )
	);
	if ( ! isInlineEditing ) {
		const library = libraryCssVars != null ? libraryCssVars : meta.cssVars || {};
		const baseline =
			libraryBaseline != null ? libraryBaseline : library;
		const liveCssVars = resolvePlacementCssVars(
			attributes,
			library,
			baseline,
			mergeLibraryPreview
		);
		const html = interpolate(
			meta.markup,
			{
				...fieldAttrs,
				cssVars: liveCssVars,
				instanceId: String(
					wrapper.className
						.split( /\s+/ )
						.find( ( c ) => c.startsWith( 'bm-i-' ) ) ||
						wrapper.getAttribute( 'data-bm-instance' ) ||
						''
				)
					.replace( /[^a-zA-Z0-9_-]/g, '' )
					.slice( 0, 20 ),
			},
			libStyles
		);
		wrapper.innerHTML = html;
		hydratePendingInlineIcons( wrapper );
		injectPreviewTabsAddButtons( wrapper, {
			enabled: ! isContentLinkedToMyBlock( meta, attributes ),
			fieldSchema: meta.fieldSchema,
		} );
		injectPreviewAccordionAddButtons( wrapper, {
			enabled: ! isContentLinkedToMyBlock( meta, attributes ),
			fieldSchema: meta.fieldSchema,
		} );
		injectPreviewCardsAddButtons( wrapper, {
			enabled: ! isContentLinkedToMyBlock( meta, attributes ),
			fieldSchema: meta.fieldSchema,
		} );
	}

	applyLiveCss(
		wrapper,
		meta,
		attributes,
		breakpoint,
		libraryCssVars,
		libraryBaseline
	);

	if ( ! isInlineEditing && wrapper.querySelector?.( '[data-bm-slider]' ) ) {
		const api =
			typeof window !== 'undefined' ? window.jocbmSlider : null;
		if ( api?.remount ) {
			api.remount( wrapper );
		} else if ( api?.init ) {
			api.init( wrapper );
		}
	}
}

/**
 * Force every BM block on the page to preview a device (tablet/mobile) even
 * though the browser window is still wide — @media never fires otherwise.
 *
 * @param {object} cfg
 * @param {string} breakpoint
 * @param {Map}    attrsCache
 * @param {HTMLElement|null} skipEl  Already handled by selection preview
 */
function syncAllBlocksViewportCss( cfg, breakpoint, attrsCache, skipEl = null ) {
	const bp = [ 'desktop', 'tablet', 'mobile' ].includes( breakpoint )
		? breakpoint
		: 'desktop';
	// null → restore full media-query CSS for desktop editing.
	const previewBp = bp === 'desktop' ? null : bp;

	document.querySelectorAll( '[data-bm-edit="1"]' ).forEach( ( el ) => {
		if ( skipEl && el === skipEl ) {
			return;
		}
		const inst = findInstance( cfg, el );
		const meta =
			cfg.blocksByName?.[ inst.name ] ||
			( cfg.blocks || [] ).find( ( b ) => b.name === inst.name ) ||
			null;
		if ( ! meta ) {
			return;
		}
		const key = instanceCacheKey( inst );
		const cached = key && attrsCache?.get?.( key );
		const attrs = cached || mergeAttrs( meta, inst.attributes || {} );
		const lib = normalize( meta.cssVars || {} );
		applyLiveCss( el, meta, attrs, previewBp, lib, lib );
	} );
}

function syncViewportPreview( editing, breakpoint ) {
	const root = document.documentElement;
	Object.values( VIEWPORT_CLASS ).forEach( ( cls ) =>
		root.classList.remove( cls )
	);
	root.style.removeProperty( '--bm-fe-viewport-max' );
	if ( ! editing ) {
		return;
	}
	const bp = VIEWPORT_CLASS[ breakpoint ] ? breakpoint : 'desktop';
	root.classList.add( VIEWPORT_CLASS[ bp ] );
	if ( bp === 'tablet' ) {
		root.style.setProperty(
			'--bm-fe-viewport-max',
			`${ DEVICE_WIDTH_PX.tablet }px`
		);
	} else if ( bp === 'mobile' ) {
		root.style.setProperty(
			'--bm-fe-viewport-max',
			`${ DEVICE_WIDTH_PX.mobile }px`
		);
	}
}

/**
 * Shrink the page so docked FE panel does not cover content.
 * Honors left/right dock; skipped when minimized or popped out.
 *
 * @param {{ editing: boolean, minimized: boolean, poppedOut: boolean, side: string }} state
 */
function syncPanelPageInset( { editing, minimized, poppedOut, side } ) {
	const root = document.documentElement;
	root.classList.remove(
		'bm-fe-panel-open',
		'bm-fe-panel-dock-left',
		'bm-fe-panel-dock-right'
	);
	const open = Boolean( editing ) && ! minimized && ! poppedOut;
	if ( ! open ) {
		return;
	}
	root.classList.add( 'bm-fe-panel-open' );
	root.classList.add(
		side === 'left' ? 'bm-fe-panel-dock-left' : 'bm-fe-panel-dock-right'
	);
}

function PencilFab( { active, onToggle } ) {
	return createElement(
		'button',
		{
			type: 'button',
			className:
				'bm-fe-fab' + ( active ? ' is-active' : '' ),
			'aria-pressed': active,
			'aria-label': active
				? __( 'Exit Blockora Morph edit mode', 'joc-blockora-morph' )
				: __( 'Edit Blockora Morph blocks', 'joc-blockora-morph' ),
			onClick: onToggle,
		},
		createElement( 'span', {
			className: 'dashicons dashicons-edit',
			'aria-hidden': true,
		} )
	);
}

function FrontEditorApp() {
	const cfg = useMemo( () => getConfig(), [] );
	const initialPrefs = useMemo( () => loadShellPrefs(), [] );
	const [ editing, setEditing ] = useState( () => initialPrefs.editing );
	const [ selectedEl, setSelectedEl ] = useState( null );
	const [ instance, setInstance ] = useState( null );
	const [ attributes, setAttributesState ] = useState( {} );
	const [ baseline, setBaseline ] = useState( {} );
	const [ libraryCssVars, setLibraryCssVars ] = useState( {} );
	const [ libraryDefaultAttrs, setLibraryDefaultAttrs ] = useState( {} );
	const libraryBaselineRef = useRef( {} );
	const [ selectedElementKey, setSelectedElementKey ] = useState( null );
	const [ focusSlideIndex, setFocusSlideIndex ] = useState( null );
	const [ focusCardIndex, setFocusCardIndex ] = useState( null );
	const [ focusButtonIndex, setFocusButtonIndex ] = useState( null );
	const [ inspectorTab, setInspectorTab ] = useState( 'fields' );
	const [ tabPanelKey, setTabPanelKey ] = useState( 0 );
	const [ styleBreakpoint, setStyleBreakpoint ] = useState( 'desktop' );
	const styleBreakpointRef = useRef( styleBreakpoint );
	styleBreakpointRef.current = styleBreakpoint;
	const [ saving, setSaving ] = useState( false );
	const [ saveMsg, setSaveMsg ] = useState( '' );
	const [ libraryPending, setLibraryPending ] = useState( false );
	const [ buttonLibraryRev, setButtonLibraryRev ] = useState( 0 );
	const [ shellPrefs, setShellPrefs ] = useState( initialPrefs );
	const [ poppedOut, setPoppedOut ] = useState( false );
	const [ popoutMount, setPopoutMount ] = useState( null );
	const inspectorTabRef = useRef( inspectorTab );
	inspectorTabRef.current = inspectorTab;
	const selectedElementKeyRef = useRef( selectedElementKey );
	selectedElementKeyRef.current = selectedElementKey;
	const viewSwitchedRef = useRef( false );
	const attributesRef = useRef( attributes );
	attributesRef.current = attributes;
	const skipHistoryRef = useRef( false );
	const documentStateRef = useRef( { attributes } );
	documentStateRef.current = { attributes };
	const metaRefForGuard = useRef( null );
	const contentEditSelectionRef = useRef( '' );
	const [ inlineEditTick, setInlineEditTick ] = useState( 0 );
	const [ formatSession, setFormatSession ] = useState( null );
	// Page-load cfg.instances goes stale after edits/saves — keep latest in-memory.
	const attrsCacheRef = useRef( new Map() );
	const baselineCacheRef = useRef( new Map() );
	const selectedElRef = useRef( null );
	const popupWinRef = useRef( null );
	const popoutMountRef = useRef( null );
	selectedElRef.current = selectedEl;

	const meta = useMemo( () => {
		if ( ! instance?.name ) {
			return null;
		}
		return (
			cfg.blocksByName?.[ instance.name ] ||
			( cfg.blocks || [] ).find( ( b ) => b.name === instance.name ) ||
			null
		);
	}, [ cfg, instance ] );
	metaRefForGuard.current = meta;

	const contentMeta = useMemo( () => {
		if ( ! meta ) {
			return null;
		}
		return {
			...meta,
			defaultAttrs: libraryDefaultAttrs || meta.defaultAttrs || {},
		};
	}, [ meta, libraryDefaultAttrs ] );

	useEffect( () => {
		if ( ! meta?.id ) {
			return undefined;
		}
		const baseline = normalize( meta.cssVars || {} );
		libraryBaselineRef.current = baseline;
		setLibraryCssVars( baseline );
		setLibraryDefaultAttrs( meta.defaultAttrs || {} );
		const onLibraryCss = ( event ) => {
			if ( Number( event?.detail?.id ) !== Number( meta.id ) ) {
				return;
			}
			setLibraryCssVars( normalize( event.detail.cssVars || {} ) );
			if (
				event.detail.defaultAttrs != null &&
				typeof event.detail.defaultAttrs === 'object'
			) {
				setLibraryDefaultAttrs( event.detail.defaultAttrs );
			}
		};
		window.addEventListener( 'joc-blockora-morph-library-css', onLibraryCss );
		return () =>
			window.removeEventListener(
				'joc-blockora-morph-library-css',
				onLibraryCss
			);
	}, [ meta?.id, meta?.defaultAttrs ] );

	useEffect( () => {
		const onPending = () => setLibraryPending( true );
		const onFlushed = ( event ) => {
			if ( event?.detail?.ok ) {
				setLibraryPending( false );
			}
		};
		window.addEventListener( 'joc-blockora-morph-library-pending', onPending );
		window.addEventListener( 'joc-blockora-morph-library-flushed', onFlushed );
		return () => {
			window.removeEventListener(
				'joc-blockora-morph-library-pending',
				onPending
			);
			window.removeEventListener(
				'joc-blockora-morph-library-flushed',
				onFlushed
			);
		};
	}, [] );

	// Heroes using Style reference must re-preview when a Button Design updates.
	useEffect( () => {
		const unsub = subscribeButtonLibrary( () => {
			setButtonLibraryRev( ( n ) => n + 1 );
		} );
		const onStorage = ( event ) => {
			if ( event?.key !== 'joc-bm-button-library-rev' ) {
				return;
			}
			refreshButtonLibrary().then( () => {
				setButtonLibraryRev( ( n ) => n + 1 );
			} );
		};
		window.addEventListener( 'storage', onStorage );
		return () => {
			unsub();
			window.removeEventListener( 'storage', onStorage );
		};
	}, [] );

	useEffect( () => {
		if ( ! editing || buttonLibraryRev === 0 ) {
			return;
		}
		const bp =
			styleBreakpoint === 'tablet' || styleBreakpoint === 'mobile'
				? styleBreakpoint
				: null;
		document.querySelectorAll( '[data-bm-edit="1"]' ).forEach( ( el ) => {
			if ( el === selectedElRef.current ) {
				return;
			}
			const inst = findInstance( cfg, el );
			const blockMeta =
				cfg.blocksByName?.[ inst.name ] ||
				( cfg.blocks || [] ).find( ( b ) => b.name === inst.name ) ||
				null;
			if ( ! blockMeta ) {
				return;
			}
			const key = instanceCacheKey( inst );
			const cached = key && attrsCacheRef.current.get( key );
			const attrs = cached || mergeAttrs( blockMeta, inst.attributes || {} );
			const lib = normalize( blockMeta.cssVars || {} );
			applyLivePreview( el, blockMeta, attrs, bp, lib, lib );
		} );
	}, [ buttonLibraryRev, editing, styleBreakpoint, cfg ] );

	const dirty = useMemo(
		() => ! attrsEqual( attributes, baseline ) || libraryPending,
		[ attributes, baseline, libraryPending ]
	);

	// Keep per-instance caches current so re-select does not revert live edits.
	useEffect( () => {
		if ( ! instance ) {
			return;
		}
		const key = instanceCacheKey( instance );
		if ( ! key ) {
			return;
		}
		attrsCacheRef.current.set( key, attributes );
	}, [ instance, attributes ] );

	useEffect( () => {
		if ( ! instance ) {
			return;
		}
		const key = instanceCacheKey( instance );
		if ( ! key ) {
			return;
		}
		baselineCacheRef.current.set( key, baseline );
	}, [ instance, baseline ] );

	const historyStorageKey = instanceCacheKey( instance ) || 'none';

	const applyDocumentState = useCallback( ( next ) => {
		skipHistoryRef.current = true;
		if ( next && typeof next === 'object' && 'attributes' in next ) {
			setAttributesState( next.attributes || {} );
		}
		skipHistoryRef.current = false;
	}, [] );

	const {
		trackSetter,
		clear: clearDocumentHistory,
		beginBatch,
		endBatch,
		cancelBatch,
		undo: undoDocument,
		redo: redoDocument,
	} = useDocumentHistory( {
		storageKey: `bm-frontend-editor:${ historyStorageKey }`,
		getState: () => documentStateRef.current,
		applyState: applyDocumentState,
	} );

	const setAttributesStateTracked = useMemo( () => {
		const tracked = trackSetter( 'attributes', setAttributesState );
		return ( updater ) => {
			if ( skipHistoryRef.current ) {
				setAttributesState( updater );
				return;
			}
			tracked( updater );
		};
	}, [ trackSetter ] );

	const onContentEditSessionStart = useCallback( () => {
		skipHistoryRef.current = true;
		beginBatch( cloneState( documentStateRef.current ) );
	}, [ beginBatch ] );

	const onContentEditSessionCommit = useCallback(
		( _before, afterAttrs ) => {
			endBatch( {
				attributes: afterAttrs || documentStateRef.current.attributes,
			} );
			skipHistoryRef.current = false;
		},
		[ endBatch ]
	);

	const onContentEditSessionEnd = useCallback( () => {
		cancelBatch();
		skipHistoryRef.current = false;
	}, [ cancelBatch ] );

	const setAttributes = useCallback( ( patch ) => {
		const apply = createLinkedPlacementSetAttributes(
			( nextPatch ) => {
				setAttributesStateTracked( ( prev ) => {
					const resolved =
						typeof nextPatch === 'function'
							? nextPatch( prev )
							: { ...prev, ...nextPatch };
					return resolved;
				} );
			},
			{
				getMeta: () => metaRefForGuard.current,
				getAttributes: () => attributesRef.current,
				enabled: () => ! metaRefForGuard.current?.isLibraryBlock,
			}
		);
		apply( patch );
	}, [ setAttributesStateTracked ] );

	const selectableKeys = useMemo(
		() =>
			meta
				? styleableFieldsInOrder(
						meta.fieldSchema || [],
						attributes?.elementsOrder,
						{
							defaultAttrs: meta.defaultAttrs || {},
							markup: meta.markup || '',
							breakpoint: styleBreakpoint,
						}
				  ).map( ( f ) => f.key )
				: [],
		[ meta, attributes?.elementsOrder, styleBreakpoint ]
	);

	const collectionField = useMemo(
		() => findCollectionRepeater( meta?.fieldSchema || [] ),
		[ meta?.fieldSchema ]
	);
	const collectionKey = collectionField?.key || null;
	const collectionItems = useMemo( () => {
		if ( ! collectionKey ) {
			return [];
		}
		const raw = attributes?.[ collectionKey ];
		return Array.isArray( raw ) ? raw : [];
	}, [ attributes, collectionKey ] );
	// Delete/duplicate only after "Customize this instance" — wired My Blocks stay read-only.
	const collectionMutationsAllowed =
		! isContentLinkedToMyBlock( meta, attributes ) &&
		collectionItemChromeActionsAllowed(
			collectionField || {},
			selectedElementKey
		);
	const canRemoveItem =
		collectionMutationsAllowed &&
		focusCardIndex != null &&
		Boolean( collectionItems[ focusCardIndex ] ) &&
		collectionItems.length > collectionMinItems( collectionField || {} );
	const canDuplicateItem =
		collectionMutationsAllowed &&
		focusCardIndex != null &&
		Boolean( collectionKey ) &&
		Boolean( collectionItems[ focusCardIndex ] );
	const canReorderItem =
		collectionMutationsAllowed &&
		focusCardIndex != null &&
		Boolean( collectionKey ) &&
		collectionItems.length > 1 &&
		Boolean( collectionItems[ focusCardIndex ] );
	const removeItemConfirmCopy = collectionRemoveConfirmCopy( collectionField );
	const removeItemLabel = String(
		( focusCardIndex != null
			? collectionItems[ focusCardIndex ]
			: null
		)?.tabTitle ||
			( focusCardIndex != null
				? collectionItems[ focusCardIndex ]
				: null
			)?.accordionTitle ||
			( focusCardIndex != null
				? collectionItems[ focusCardIndex ]
				: null
			)?.title ||
			( focusCardIndex != null
				? collectionItems[ focusCardIndex ]
				: null
			)?.tag ||
			( focusCardIndex != null
				? collectionItems[ focusCardIndex ]
				: null
			)?.price ||
			''
	)
		.replace( /<[^>]+>/g, ' ' )
		.replace( /\s+/g, ' ' )
		.trim();
	const removeItemConfirmDescription = removeItemConfirmCopy.description(
		removeItemLabel || removeItemConfirmCopy.fallbackName
	);
	const onRemoveItem = useCallback( () => {
		if ( ! canRemoveItem || focusCardIndex == null || ! collectionKey ) {
			return;
		}
		endPreviewContentEditForLinkEditor( {
			pauseCollectionWrites: true,
		} );
		setSelectedElementKey( null );
		setFocusButtonIndex( null );
		const removedIndex = Number( focusCardIndex );
		const list = Array.isArray( attributesRef.current?.[ collectionKey ] )
			? attributesRef.current[ collectionKey ]
			: collectionItems;
		const nextItems = list.filter( ( _, i ) => i !== removedIndex );
		const patch = { [ collectionKey ]: nextItems };
		const active = Number( attributesRef.current?.activeTab );
		if ( Number.isFinite( active ) ) {
			if ( ! nextItems.length ) {
				patch.activeTab = 0;
			} else if ( active >= nextItems.length || active === removedIndex ) {
				patch.activeTab = Math.min(
					removedIndex,
					nextItems.length - 1
				);
			} else if ( active > removedIndex ) {
				patch.activeTab = active - 1;
			}
		}
		setAttributes( patch );
		if ( ! nextItems.length ) {
			setFocusCardIndex( null );
			setSelectedElementKey( null );
			return;
		}
		setFocusCardIndex( Math.min( removedIndex, nextItems.length - 1 ) );
	}, [
		canRemoveItem,
		focusCardIndex,
		collectionKey,
		collectionItems,
		attributes?.activeTab,
		setAttributes,
	] );
	const onDuplicateItem = useCallback( () => {
		if (
			! canDuplicateItem ||
			focusCardIndex == null ||
			! collectionKey
		) {
			return;
		}
		endPreviewContentEditForLinkEditor( {
			pauseCollectionWrites: true,
		} );
		setSelectedElementKey( null );
		setFocusButtonIndex( null );
		const sourceIndex = Number( focusCardIndex );
		const result = duplicateCollectionItemAt(
			attributesRef.current || attributes,
			collectionKey,
			sourceIndex
		);
		if ( ! result ) {
			return;
		}
		const patch = { ...result.patch };
		const active = Number(
			attributesRef.current?.activeTab ?? attributes?.activeTab
		);
		if ( Number.isFinite( active ) && active > sourceIndex ) {
			patch.activeTab = active + 1;
		}
		setAttributes( patch );
		setFocusCardIndex( result.newIndex );
		setSelectedElementKey( null );
	}, [
		canDuplicateItem,
		focusCardIndex,
		collectionKey,
		attributes,
		setAttributes,
	] );
	const onReorderItem = useCallback(
		( from, to ) => {
			if ( ! canReorderItem || ! collectionKey ) {
				return;
			}
			endPreviewContentEditForLinkEditor( {
				pauseCollectionWrites: true,
			} );
			setSelectedElementKey( null );
			setFocusButtonIndex( null );
			const result = moveCollectionItemAt(
				attributesRef.current || attributes,
				collectionKey,
				from,
				to
			);
			if ( ! result ) {
				return;
			}
			setAttributes( result.patch );
			setFocusCardIndex( result.focusIndex );
		},
		[ canReorderItem, collectionKey, attributes, setAttributes ]
	);
	const onRemoveItemRef = useRef( onRemoveItem );
	onRemoveItemRef.current = onRemoveItem;
	const canRemoveItemRef = useRef( canRemoveItem );
	canRemoveItemRef.current = canRemoveItem;
	const onDuplicateItemRef = useRef( onDuplicateItem );
	onDuplicateItemRef.current = onDuplicateItem;
	const canDuplicateItemRef = useRef( canDuplicateItem );
	canDuplicateItemRef.current = canDuplicateItem;
	const onReorderItemRef = useRef( onReorderItem );
	onReorderItemRef.current = onReorderItem;
	const canReorderItemRef = useRef( canReorderItem );
	canReorderItemRef.current = canReorderItem;
	const focusCardIndexRef = useRef( focusCardIndex );
	focusCardIndexRef.current = focusCardIndex;
	const [ chromeDeleteOpen, setChromeDeleteOpen ] = useState( false );
	const requestChromeDeleteRef = useRef( () => {} );
	requestChromeDeleteRef.current = () => {
		if ( ! canRemoveItemRef.current || ! onRemoveItemRef.current ) {
			return;
		}
		setChromeDeleteOpen( true );
	};
	const chromeOptions = () => ( {
		canRemove: Boolean( canRemoveItemRef.current ),
		onRemove: () => requestChromeDeleteRef.current?.(),
		canDuplicate: Boolean( canDuplicateItemRef.current ),
		onDuplicate: () => onDuplicateItemRef.current?.(),
		canReorder: Boolean( canReorderItemRef.current ),
		onReorder: ( from, to ) => onReorderItemRef.current?.( from, to ),
		reorderFromIndex: focusCardIndexRef.current,
	} );

	const onClearElement = useCallback( () => {
		setSelectedElementKey( null );
		setFocusButtonIndex( null );
		setFocusCardIndex( null );
	}, [] );

	const onClearElementRef = useRef( onClearElement );
	onClearElementRef.current = onClearElement;
	const previewRestoreRef = useRef( null );

	const restoreResponsivePreview = useCallback(
		( snap ) => {
			if ( ! snap?.el || ! snap?.meta ) {
				return;
			}
			const previewBp =
				styleBreakpoint === 'tablet' || styleBreakpoint === 'mobile'
					? styleBreakpoint
					: null;
			applyLivePreview(
				snap.el,
				snap.meta,
				snap.attrs || {},
				previewBp,
				snap.libraryCssVars,
				snap.libraryBaseline
			);
		},
		[ styleBreakpoint ]
	);

	// Constrain the page body to tablet/mobile width (no browser resize).
	useEffect( () => {
		syncViewportPreview( editing, styleBreakpoint );
		return () => {
			syncViewportPreview( false, 'desktop' );
		};
	}, [ editing, styleBreakpoint ] );

	// Force flat breakpoint CSS on all BM blocks — @media still sees a wide window.
	useEffect( () => {
		if ( ! editing ) {
			syncAllBlocksViewportCss( cfg, 'desktop', attrsCacheRef.current, null );
			return undefined;
		}
		syncAllBlocksViewportCss(
			cfg,
			styleBreakpoint,
			attrsCacheRef.current,
			selectedEl
		);
		return undefined;
	}, [ editing, styleBreakpoint, cfg, selectedEl ] );

	// Reserve horizontal space for the docked panel (left or right).
	useEffect( () => {
		const side = shellPrefs.side === 'left' ? 'left' : 'right';
		syncPanelPageInset( {
			editing,
			minimized: Boolean( shellPrefs.minimized ),
			poppedOut,
			side,
		} );
		return () => {
			syncPanelPageInset( {
				editing: false,
				minimized: true,
				poppedOut: false,
				side: 'right',
			} );
		};
	}, [ editing, shellPrefs.minimized, shellPrefs.side, poppedOut ] );

	// Live preview + selection chrome (chrome must re-attach after innerHTML refresh).
	useEffect( () => {
		if ( ! editing || ! selectedEl || ! meta ) {
			if ( previewRestoreRef.current ) {
				restoreResponsivePreview( previewRestoreRef.current );
				previewRestoreRef.current = null;
			}
			return undefined;
		}
		if (
			previewRestoreRef.current &&
			previewRestoreRef.current.el !== selectedEl
		) {
			restoreResponsivePreview( previewRestoreRef.current );
		}
		applyLivePreview(
			selectedEl,
			contentMeta || meta,
			attributes,
			styleBreakpoint,
			libraryCssVars,
			libraryBaselineRef.current
		);
		previewRestoreRef.current = {
			el: selectedEl,
			meta: contentMeta || meta,
			attrs: attributes,
			libraryCssVars,
			libraryBaseline: libraryBaselineRef.current,
		};
		forceOpenDetailsForEditing( selectedEl, { focusCardIndex } );
		initBmAccordion( selectedEl.ownerDocument );
		enforceAccordionExclusiveOpen( selectedEl );
		if ( focusCardIndex != null ) {
			activateBmTabForEditing( selectedEl, focusCardIndex );
		}
		const unbindDetails = bindForceOpenDetails( selectedEl.ownerDocument );

		const found = markStyleableAndSelected(
			selectedEl,
			selectableKeys,
			selectedElementKey,
			focusSlideIndex,
			focusCardIndex
		);
		syncSelectionChrome(
			selectedEl,
			found,
			() => {
				onClearElementRef.current?.();
			},
			chromeOptions()
		);

		const refreshChrome = () => {
			forceOpenDetailsForEditing( selectedEl, { focusCardIndex } );
		initBmAccordion( selectedEl.ownerDocument );
		enforceAccordionExclusiveOpen( selectedEl );
			if ( focusCardIndex != null ) {
				activateBmTabForEditing( selectedEl, focusCardIndex );
			}
			const selected = markStyleableAndSelected(
				selectedEl,
				selectableKeys,
				selectedElementKey,
				focusSlideIndex,
				focusCardIndex
			);
			syncSelectionChrome(
				selectedEl,
				selected,
				() => {
					onClearElementRef.current?.();
				},
				chromeOptions()
			);
		};
		const ro =
			typeof ResizeObserver !== 'undefined'
				? new ResizeObserver( () => refreshChrome() )
				: null;
		if ( found && ro ) {
			ro.observe( found );
		}
		window.addEventListener( 'resize', refreshChrome );
		return () => {
			unbindDetails?.();
			ro?.disconnect();
			window.removeEventListener( 'resize', refreshChrome );
		};
	}, [
		editing,
		selectedEl,
		meta,
		contentMeta,
		attributes,
		libraryCssVars,
		buttonLibraryRev,
		selectableKeys,
		selectedElementKey,
		focusSlideIndex,
		focusCardIndex,
		styleBreakpoint,
		restoreResponsivePreview,
		canRemoveItem,
		canDuplicateItem,
		canReorderItem,
	] );

	useEffect( () => {
		setChromeDeleteOpen( false );
	}, [ selectedElementKey, focusCardIndex, canRemoveItem ] );

	// Inline content editing on the selected block canvas (after live preview DOM).
	useEffect( () => {
		if ( ! editing || ! selectedEl || ! meta || ! selectedElementKey ) {
			return undefined;
		}
		if ( isContentLinkedToMyBlock( contentMeta || meta, attributesRef.current || {} ) ) {
			setFormatSession( null );
			return undefined;
		}
		const selectionId = [
			selectedElementKey,
			focusSlideIndex,
			focusCardIndex,
			focusButtonIndex,
			instanceCacheKey( instance ),
		].join( ':' );
		const autoFocus = contentEditSelectionRef.current !== selectionId;
		contentEditSelectionRef.current = selectionId;

		let cleanup = () => {};
		let rafId = 0;
		rafId = window.requestAnimationFrame( () => {
			cleanup = bindPreviewContentEdit( selectedEl, {
				fieldSchema: meta.fieldSchema || [],
				getAttributes: () => attributesRef.current || {},
				setAttributes,
				onSessionCommit: ( before, after ) => {
					onContentEditSessionCommit( before, after );
				},
				onDocumentUndo: () => Boolean( undoDocument() ),
				onDocumentRedo: () => Boolean( redoDocument() ),
				selectedElementKey,
				focusSlideIndex,
				focusCardIndex,
				focusButtonIndex,
				autoFocus,
				onSessionStart: ( session ) => {
					onContentEditSessionStart();
					setFormatSession( session );
				},
				onSessionEnd: () => {
					onContentEditSessionEnd();
					setFormatSession( null );
				},
			} );
		} );

		return () => {
			if ( rafId ) {
				window.cancelAnimationFrame( rafId );
			}
			cleanup();
		};
	}, [
		editing,
		selectedEl,
		meta,
		setAttributes,
		selectedElementKey,
		focusSlideIndex,
		focusCardIndex,
		focusButtonIndex,
		instance,
		inlineEditTick,
		attributes?.styleScopeChoices,
		contentMeta,
	] );

	// Keep autoplay halted on the focused slide while an element is selected.
	useEffect( () => {
		if ( ! editing || ! selectedEl ) {
			return;
		}
		if ( ! selectedEl.querySelector?.( '[data-bm-slider]' ) ) {
			return;
		}
		if ( focusSlideIndex != null || selectedElementKey ) {
			focusBmSliderSlide(
				focusSlideIndex != null ? focusSlideIndex : 0,
				selectedEl
			);
		} else {
			releaseBmSliderFocus( selectedEl );
		}
	}, [ editing, selectedEl, focusSlideIndex, selectedElementKey, attributes ] );

	// Body class for edit mode + media hit targets on every BM block.
	useEffect( () => {
		document.documentElement.classList.toggle( 'bm-fe-editing', editing );
		if ( ! editing ) {
			document.querySelectorAll( '.bm-el-chrome' ).forEach( ( n ) => {
				const parent = n.parentElement;
				n.remove();
				if ( parent?.dataset?.bmPosFix === '1' ) {
					parent.style.position = '';
					delete parent.dataset.bmPosFix;
				}
			} );
			setSelectedEl( null );
			setInstance( null );
			setSelectedElementKey( null );
			document
				.querySelectorAll( '[data-bm-edit].is-bm-fe-selected' )
				.forEach( ( n ) => n.classList.remove( 'is-bm-fe-selected' ) );
			return undefined;
		}
		document.querySelectorAll( '[data-bm-edit="1"]' ).forEach( ( root ) => {
			wrapMediaHitTargets( root );
			const inst = findInstance( cfg, root );
			const blockMeta =
				cfg.blocksByName?.[ inst?.name ] ||
				( cfg.blocks || [] ).find( ( b ) => b.name === inst?.name ) ||
				null;
			const cacheKey = inst ? instanceCacheKey( inst ) : '';
			const attrs =
				( cacheKey && attrsCacheRef.current.get( cacheKey ) ) ||
				( blockMeta
					? mergeAttrs( blockMeta, inst?.attributes || {} )
					: {} );
			injectPreviewTabsAddButtons( root, {
				enabled:
					Boolean( blockMeta ) &&
					! isContentLinkedToMyBlock( blockMeta, attrs ),
				fieldSchema: blockMeta?.fieldSchema,
			} );
			injectPreviewAccordionAddButtons( root, {
				enabled:
					Boolean( blockMeta ) &&
					! isContentLinkedToMyBlock( blockMeta, attrs ),
				fieldSchema: blockMeta?.fieldSchema,
			} );
			injectPreviewCardsAddButtons( root, {
				enabled:
					Boolean( blockMeta ) &&
					! isContentLinkedToMyBlock( blockMeta, attrs ),
				fieldSchema: blockMeta?.fieldSchema,
			} );
		} );
		return undefined;
	}, [ editing, cfg ] );

	// Click / hover selection while editing.
	useEffect( () => {
		if ( ! editing ) {
			return undefined;
		}

		const onClick = ( event ) => {
			const target = event.target;
			if ( ! ( target instanceof Element ) ) {
				return;
			}
			if ( target.closest( '.bm-fe-shell, .bm-fe-fab, .bm-fe-popout-bar' ) ) {
				return;
			}
			// × on selection chrome — handled by their own listeners.
			if ( target.closest( '.bm-el-deselect, .bm-el-delete, .bm-el-duplicate, .bm-el-drag, .bm-el-chrome__tools' ) ) {
				return;
			}
			if ( isTabsAddButtonTarget( target ) ) {
				event.preventDefault();
				event.stopPropagation();
				const wrapper = target.closest( '[data-bm-edit="1"]' );
				if ( ! wrapper ) {
					return;
				}
				const inst = findInstance( cfg, wrapper );
				const blockMeta =
					cfg.blocksByName?.[ inst?.name ] ||
					( cfg.blocks || [] ).find(
						( b ) => b.name === inst?.name
					) ||
					null;
				if ( ! blockMeta || ! inst ) {
					return;
				}
				const cacheKey = instanceCacheKey( inst );
				const cachedAttrs = cacheKey
					? attrsCacheRef.current.get( cacheKey )
					: null;
				const attrs =
					cachedAttrs ||
					mergeAttrs( blockMeta, inst.attributes || {} );
				if ( isContentLinkedToMyBlock( blockMeta, attrs ) ) {
					return;
				}
				const added = appendTabToAttributes(
					attrs,
					blockMeta.fieldSchema
				);
				if ( ! added ) {
					return;
				}
				const nextAttrs = { ...attrs, ...added.patch };
				if ( cacheKey ) {
					attrsCacheRef.current.set( cacheKey, nextAttrs );
				}
				document
					.querySelectorAll( '[data-bm-edit].is-bm-fe-selected' )
					.forEach( ( n ) =>
						n.classList.remove( 'is-bm-fe-selected' )
					);
				wrapper.classList.add( 'is-bm-fe-selected' );
				setSelectedEl( wrapper );
				setInstance( { ...inst, attributes: nextAttrs } );
				skipHistoryRef.current = true;
				setAttributesState( nextAttrs );
				skipHistoryRef.current = false;
				setFocusCardIndex( added.newIndex );
				setFocusSlideIndex( null );
				setSelectedElementKey( 'tabTitle' );
				setFocusButtonIndex( null );
				const previewBp =
					styleBreakpointRef.current === 'tablet' ||
					styleBreakpointRef.current === 'mobile'
						? styleBreakpointRef.current
						: null;
				applyLivePreview(
					wrapper,
					blockMeta,
					nextAttrs,
					previewBp,
					normalize( blockMeta.cssVars || {} ),
					normalize( blockMeta.cssVars || {} )
				);
				activateBmTabForEditing( wrapper, added.newIndex );
				viewSwitchedRef.current = false;
				if ( inspectorTabRef.current !== 'styles' ) {
					setInspectorTab( 'styles' );
					setTabPanelKey( ( k ) => k + 1 );
				}
				scrollInspectorToTop();
				return;
			}
			if ( isAccordionAddButtonTarget( target ) ) {
				event.preventDefault();
				event.stopPropagation();
				const wrapper = target.closest( '[data-bm-edit="1"]' );
				if ( ! wrapper ) {
					return;
				}
				const inst = findInstance( cfg, wrapper );
				const blockMeta =
					cfg.blocksByName?.[ inst?.name ] ||
					( cfg.blocks || [] ).find(
						( b ) => b.name === inst?.name
					) ||
					null;
				if ( ! blockMeta || ! inst ) {
					return;
				}
				const cacheKey = instanceCacheKey( inst );
				const cachedAttrs = cacheKey
					? attrsCacheRef.current.get( cacheKey )
					: null;
				const attrs =
					cachedAttrs ||
					mergeAttrs( blockMeta, inst.attributes || {} );
				if ( isContentLinkedToMyBlock( blockMeta, attrs ) ) {
					return;
				}
				const added = appendAccordionItemToAttributes(
					attrs,
					blockMeta.fieldSchema
				);
				if ( ! added ) {
					return;
				}
				const nextAttrs = { ...attrs, ...added.patch };
				if ( cacheKey ) {
					attrsCacheRef.current.set( cacheKey, nextAttrs );
				}
				document
					.querySelectorAll( '[data-bm-edit].is-bm-fe-selected' )
					.forEach( ( n ) =>
						n.classList.remove( 'is-bm-fe-selected' )
					);
				wrapper.classList.add( 'is-bm-fe-selected' );
				setSelectedEl( wrapper );
				setInstance( { ...inst, attributes: nextAttrs } );
				skipHistoryRef.current = true;
				setAttributesState( nextAttrs );
				skipHistoryRef.current = false;
				setFocusCardIndex( added.newIndex );
				setFocusSlideIndex( null );
				setSelectedElementKey( 'accordionTitle' );
				setFocusButtonIndex( null );
				const previewBp =
					styleBreakpointRef.current === 'tablet' ||
					styleBreakpointRef.current === 'mobile'
						? styleBreakpointRef.current
						: null;
				applyLivePreview(
					wrapper,
					blockMeta,
					nextAttrs,
					previewBp,
					normalize( blockMeta.cssVars || {} ),
					normalize( blockMeta.cssVars || {} )
				);
				forceOpenDetailsForEditing( wrapper, {
					focusCardIndex: added.newIndex,
				} );
				initBmAccordion( wrapper.ownerDocument || document );
				enforceAccordionExclusiveOpen( wrapper );
				viewSwitchedRef.current = false;
				if ( inspectorTabRef.current !== 'styles' ) {
					setInspectorTab( 'styles' );
					setTabPanelKey( ( k ) => k + 1 );
				}
				scrollInspectorToTop();
				return;
			}
			if ( isCardsAddButtonTarget( target ) ) {
				event.preventDefault();
				event.stopPropagation();
				const wrapper = target.closest( '[data-bm-edit="1"]' );
				if ( ! wrapper ) {
					return;
				}
				const inst = findInstance( cfg, wrapper );
				const blockMeta =
					cfg.blocksByName?.[ inst?.name ] ||
					( cfg.blocks || [] ).find(
						( b ) => b.name === inst?.name
					) ||
					null;
				if ( ! blockMeta || ! inst ) {
					return;
				}
				const cacheKey = instanceCacheKey( inst );
				const cachedAttrs = cacheKey
					? attrsCacheRef.current.get( cacheKey )
					: null;
				const attrs =
					cachedAttrs ||
					mergeAttrs( blockMeta, inst.attributes || {} );
				if ( isContentLinkedToMyBlock( blockMeta, attrs ) ) {
					return;
				}
				const added = appendCardToAttributes(
					attrs,
					blockMeta.fieldSchema
				);
				if ( ! added ) {
					return;
				}
				const nextAttrs = { ...attrs, ...added.patch };
				if ( cacheKey ) {
					attrsCacheRef.current.set( cacheKey, nextAttrs );
				}
				document
					.querySelectorAll( '[data-bm-edit].is-bm-fe-selected' )
					.forEach( ( n ) =>
						n.classList.remove( 'is-bm-fe-selected' )
					);
				wrapper.classList.add( 'is-bm-fe-selected' );
				setSelectedEl( wrapper );
				setInstance( { ...inst, attributes: nextAttrs } );
				skipHistoryRef.current = true;
				setAttributesState( nextAttrs );
				skipHistoryRef.current = false;
				setFocusCardIndex( added.newIndex );
				setFocusSlideIndex( null );
				setSelectedElementKey( 'title' );
				setFocusButtonIndex( null );
				const previewBp =
					styleBreakpointRef.current === 'tablet' ||
					styleBreakpointRef.current === 'mobile'
						? styleBreakpointRef.current
						: null;
				applyLivePreview(
					wrapper,
					blockMeta,
					nextAttrs,
					previewBp,
					normalize( blockMeta.cssVars || {} ),
					normalize( blockMeta.cssVars || {} )
				);
				viewSwitchedRef.current = false;
				if ( inspectorTabRef.current !== 'styles' ) {
					setInspectorTab( 'styles' );
					setTabPanelKey( ( k ) => k + 1 );
				}
				scrollInspectorToTop();
				return;
			}
			if ( isSliderChromeTarget( target ) ) {
				return;
			}

			const elNode = target.closest(
				'[data-bm-el], .bm-el-hit, img, video, iframe, embed, object'
			);
			const wrapper = target.closest( '[data-bm-edit="1"]' );
			if ( ! wrapper ) {
				return;
			}

			// Let the active inline editor receive clicks / caret placement.
			if ( target.closest( '[data-bm-content-edit="1"]' ) ) {
				return;
			}

			const interactiveSel =
				'a[href], button, input[type="submit"], input[type="button"], input[type="reset"], [role="button"]';
			const interactive = target.closest( interactiveSel );
			if (
				interactive &&
				wrapper.contains( interactive ) &&
				! interactive.classList?.contains( 'bm-el-deselect' ) &&
				! interactive.classList?.contains( 'bm-el-delete' ) &&
				! interactive.classList?.contains( 'bm-el-duplicate' ) &&
				! interactive.classList?.contains( 'bm-el-drag' )
			) {
				event.preventDefault();
			}

			document
				.querySelectorAll( '[data-bm-edit].is-bm-fe-selected' )
				.forEach( ( n ) => n.classList.remove( 'is-bm-fe-selected' ) );
			wrapper.classList.add( 'is-bm-fe-selected' );

			const resolveElKey = () => {
				if ( ! elNode || ! wrapper.contains( elNode ) ) {
					return null;
				}
				const direct = elNode.getAttribute?.( 'data-bm-el' );
				if ( direct ) {
					return direct;
				}
				const hit = elNode.closest?.( '[data-bm-el]' );
				return hit && wrapper.contains( hit )
					? hit.getAttribute( 'data-bm-el' )
					: null;
			};

			const applyElementFocus = () => {
				const elKey = resolveElKey();
				const prev = selectedElementKeyRef.current;
				const tab = inspectorTabRef.current;
				const inst = findInstance( cfg, wrapper );
				const blockMeta =
					cfg.blocksByName?.[ inst?.name ] ||
					( cfg.blocks || [] ).find(
						( b ) => b.name === inst?.name
					) ||
					null;
				const fieldSchema = blockMeta?.fieldSchema || [];
				const slideHit =
					target.closest(
						'[data-bm-slide-index], .splide__slide, .bm-hero--slide'
					) || null;
				const slideIndex = slideHit
					? slideIndexFromElement( slideHit ) ??
					  slideIndexFromElement(
							elNode?.closest?.( '[data-bm-el]' ) || elNode
					  )
					: slideIndexFromElement(
							elNode?.closest?.( '[data-bm-el]' ) || elNode
					  );
				const cardHit = target.closest( '[data-bm-card-index]' ) || null;
				const cardIndex = cardHit
					? cardIndexFromElement( cardHit ) ??
					  cardIndexFromElement(
							elNode?.closest?.( '[data-bm-el]' ) || elNode
					  )
					: cardIndexFromElement(
							elNode?.closest?.( '[data-bm-el]' ) || elNode
					  );
				const btnHit = target.closest( '[data-bm-btn-index]' ) || null;
				let buttonIndex = null;
				if ( btnHit ) {
					const raw = btnHit.getAttribute( 'data-bm-btn-index' );
					const parsed = raw != null ? Number( raw ) : NaN;
					if ( Number.isFinite( parsed ) && parsed >= 0 ) {
						buttonIndex = parsed;
					}
				}
				const nextButtonFocus =
					elKey === 'buttons' ? buttonIndex : null;

				if ( slideIndex != null ) {
					event.preventDefault();
					event.stopPropagation();
					setFocusSlideIndex( slideIndex );
					setFocusCardIndex( null );
					focusBmSliderSlide( slideIndex, wrapper );
					setSelectedElementKey( elKey || null );
					setFocusButtonIndex( nextButtonFocus );
					viewSwitchedRef.current = false;
					if ( tab !== 'styles' ) {
						setInspectorTab( 'styles' );
						setTabPanelKey( ( k ) => k + 1 );
					}
					scrollInspectorToTop();
					return;
				}

				if ( cardIndex != null ) {
					event.preventDefault();
					event.stopPropagation();
					setFocusCardIndex( cardIndex );
					setFocusSlideIndex( null );
					setSelectedElementKey( elKey || null );
					setFocusButtonIndex( nextButtonFocus );
					activateBmTabForEditing(
						elNode?.closest?.( '[data-bm-el]' ) ||
							elNode ||
							target,
						cardIndex
					);
					viewSwitchedRef.current = false;
					if ( tab !== 'styles' ) {
						setInspectorTab( 'styles' );
						setTabPanelKey( ( k ) => k + 1 );
					}
					scrollInspectorToTop();
					return;
				}

				if ( elKey ) {
					if ( elKey === prev && tab === 'styles' ) {
						if ( elKey === 'buttons' && buttonIndex != null ) {
							setFocusButtonIndex( buttonIndex );
						}
						if (
							canInlineEditKey(
								fieldSchema,
								elKey,
								nextButtonFocus
							)
						) {
							contentEditSelectionRef.current = '';
							setInlineEditTick( ( n ) => n + 1 );
						}
						return;
					}
					event.preventDefault();
					event.stopPropagation();
					setSelectedElementKey( elKey );
					setFocusButtonIndex( nextButtonFocus );
					viewSwitchedRef.current = false;
					if ( tab !== 'styles' ) {
						setInspectorTab( 'styles' );
						setTabPanelKey( ( k ) => k + 1 );
					}
					scrollInspectorToTop();
					return;
				}

				event.preventDefault();
				event.stopPropagation();
				setSelectedElementKey( null );
				setFocusButtonIndex( null );
				setFocusSlideIndex( null );
				setFocusCardIndex( null );

				if ( viewSwitchedRef.current ) {
					viewSwitchedRef.current = false;
					if ( tab !== 'styles' ) {
						setInspectorTab( 'styles' );
						setTabPanelKey( ( k ) => k + 1 );
					}
					scrollInspectorToTop();
					return;
				}

				// Already on main block Customize — stay put.
				if ( ! prev && tab === 'styles' ) {
					return;
				}

				if ( tab !== 'styles' ) {
					setInspectorTab( 'styles' );
					setTabPanelKey( ( k ) => k + 1 );
				}
				scrollInspectorToTop();
			};

			// Same block already open — only change element focus; do not reload attrs.
			if ( selectedElRef.current === wrapper ) {
				applyElementFocus();
				return;
			}

			const inst = findInstance( cfg, wrapper );
			const cacheKey = instanceCacheKey( inst );
			const blockMeta =
				cfg.blocksByName?.[ inst.name ] ||
				( cfg.blocks || [] ).find( ( b ) => b.name === inst.name );
			const cachedAttrs = cacheKey
				? attrsCacheRef.current.get( cacheKey )
				: null;
			const cachedBaseline = cacheKey
				? baselineCacheRef.current.get( cacheKey )
				: null;
			const merged = cachedAttrs
				? cachedAttrs
				: mergeAttrs( blockMeta, inst.attributes );
			const nextBaseline = cachedBaseline
				? cachedBaseline
				: cloneAttrs( merged );

			if ( cacheKey && ! cachedAttrs ) {
				attrsCacheRef.current.set( cacheKey, merged );
				baselineCacheRef.current.set( cacheKey, nextBaseline );
			}

			event.preventDefault();
			event.stopPropagation();
			setSelectedEl( wrapper );
			setInstance( { ...inst, attributes: merged } );
			skipHistoryRef.current = true;
			setAttributesState( merged );
			skipHistoryRef.current = false;
			clearDocumentHistory();
			setBaseline( cloneAttrs( nextBaseline ) );
			applyElementFocus();
		};

		document.addEventListener( 'click', onClick, true );
		return () => document.removeEventListener( 'click', onClick, true );
	}, [ editing, cfg, clearDocumentHistory ] );

	const onViewSwitch = useCallback( () => {
		viewSwitchedRef.current = true;
	}, [] );

	const onSelectElement = useCallback( ( key ) => {
		const next = key || null;
		const prev = selectedElementKeyRef.current;
		if ( next && next === prev ) {
			return;
		}
		setSelectedElementKey( next );
		if ( ! next ) {
			setFocusButtonIndex( null );
		} else {
			viewSwitchedRef.current = false;
			scrollInspectorToTop();
		}
	}, [] );

	const onEditButtonStyles = useCallback( ( fieldKey, index ) => {
		const key = fieldKey || 'buttons';
		const prev = selectedElementKeyRef.current;
		setSelectedElementKey( key );
		setFocusButtonIndex( typeof index === 'number' ? index : null );
		viewSwitchedRef.current = false;
		if ( inspectorTabRef.current !== 'styles' ) {
			setInspectorTab( 'styles' );
			setTabPanelKey( ( k ) => k + 1 );
		}
		if ( key !== prev ) {
			scrollInspectorToTop();
		}
	}, [] );

	const save = useCallback( async () => {
		if ( ! instance || saving ) {
			return;
		}
		const attrsDirty = ! attrsEqual( attributes, baseline );
		const libDirty = libraryPending || hasPendingLibraryCssVars();
		if ( ! attrsDirty && ! libDirty ) {
			return;
		}
		setSaving( true );
		setSaveMsg( '' );
		try {
			if ( libDirty ) {
				window.dispatchEvent(
					new CustomEvent( 'joc-blockora-morph-library-flush-start' )
				);
				await flushPendingLibraryCssVars();
				setLibraryPending( false );
			}

			if ( ! attrsDirty ) {
				setSaveMsg( __( 'Saved', 'joc-blockora-morph' ) );
				return;
			}

			const cssVars = normalize( attributes.cssVars || {} );
			const fieldAttrs = { ...attributes };
			delete fieldAttrs.cssVars;
			let nextAttrs = {
				...fieldAttrs,
				cssVars,
			};

			if ( instance.surface === 'shortcode' ) {
				const updated = await apiFetch( {
					path: `/joc-blockora-morph/v1/user-contents/${ instance.contentId }`,
					method: 'PUT',
					data: {
						contentAttrs: fieldAttrs,
						cssVars,
					},
				} );
				nextAttrs = mergeAttrs( meta, {
					...( updated.contentAttrs || fieldAttrs ),
					cssVars: normalize( updated.cssVars || cssVars ),
				} );
			} else {
				const updated = await apiFetch( {
					path: '/joc-blockora-morph/v1/front-edit/block',
					method: 'PUT',
					data: {
						postId: cfg.postId,
						blockIndex: instance.blockIndex,
						name: instance.name,
						attributes: fieldAttrs,
						cssVars,
					},
				} );
				const savedAttrs = updated.attributes || {};
				nextAttrs = mergeAttrs( meta, {
					...savedAttrs,
					cssVars: normalize( savedAttrs.cssVars || cssVars ),
				} );
			}

			skipHistoryRef.current = true;
			setAttributesState( nextAttrs );
			skipHistoryRef.current = false;
			clearDocumentHistory();
			setBaseline( cloneAttrs( nextAttrs ) );
			setInstance( ( prev ) =>
				prev ? { ...prev, attributes: nextAttrs } : prev
			);
			const cacheKey = instanceCacheKey( instance );
			if ( cacheKey ) {
				attrsCacheRef.current.set( cacheKey, nextAttrs );
				baselineCacheRef.current.set( cacheKey, cloneAttrs( nextAttrs ) );
			}

			if ( selectedEl && meta ) {
				applyLivePreview(
					selectedEl,
					meta,
					nextAttrs,
					styleBreakpoint,
					libraryCssVars,
					libraryBaselineRef.current
				);
				const found = markStyleableAndSelected(
					selectedEl,
					selectableKeys,
					selectedElementKey,
					focusSlideIndex,
					focusCardIndex
				);
				syncSelectionChrome(
					selectedEl,
					found,
					() => {
						onClearElementRef.current?.();
					},
					chromeOptions()
				);
			}

			setSaveMsg( __( 'Saved', 'joc-blockora-morph' ) );
		} catch ( e ) {
			setSaveMsg(
				e?.message || __( 'Save failed.', 'joc-blockora-morph' )
			);
		} finally {
			setSaving( false );
		}
	}, [
		instance,
		dirty,
		libraryPending,
		saving,
		attributes,
		baseline,
		cfg,
		meta,
		selectedEl,
		selectableKeys,
		selectedElementKey,
		styleBreakpoint,
		libraryCssVars,
		clearDocumentHistory,
	] );

	const setShellSide = useCallback( ( side ) => {
		setShellPrefs( ( prev ) => {
			const next = {
				...prev,
				side: side === 'left' ? 'left' : 'right',
			};
			saveShellPrefs( next );
			return next;
		} );
	}, [] );

	const toggleShellMinimized = useCallback( () => {
		setShellPrefs( ( prev ) => {
			const next = { ...prev, minimized: ! prev.minimized };
			saveShellPrefs( next );
			return next;
		} );
	}, [] );

	const setEditingMode = useCallback( ( nextEditing ) => {
		const on = Boolean( nextEditing );
		setEditing( on );
		setShellPrefs( ( prev ) => {
			if ( Boolean( prev.editing ) === on ) {
				return prev;
			}
			const next = { ...prev, editing: on };
			saveShellPrefs( next );
			return next;
		} );
	}, [] );

	const closePopout = useCallback( () => {
		const win = popupWinRef.current;
		popupWinRef.current = null;
		popoutMountRef.current = null;
		setPopoutMount( null );
		setPoppedOut( false );
		try {
			if ( win && ! win.closed ) {
				win.close();
			}
		} catch ( e ) {
			// Ignore.
		}
	}, [] );

	const openPopout = useCallback( () => {
		if ( ! ENABLE_FE_POPOUT ) {
			return;
		}
		let win = popupWinRef.current;
		if ( win && ! win.closed && popoutMountRef.current ) {
			win.focus();
			setPoppedOut( true );
			return;
		}
		const width = 440;
		const height = Math.min(
			820,
			Math.max( 560, window.screen?.availHeight - 80 || 760 )
		);
		const left = Math.max(
			0,
			( window.screenX || 0 ) + ( window.outerWidth || width ) - width - 24
		);
		const top = Math.max( 0, ( window.screenY || 0 ) + 48 );
		win = window.open(
			'',
			'joc-blockora-morph-fe-editor',
			`popup=yes,width=${ width },height=${ height },left=${ left },top=${ top },scrollbars=yes,resizable=yes`
		);
		if ( ! win ) {
			window.alert(
				__(
					'Pop-up blocked. Allow pop-ups for this site to open the editor in a separate window.',
					'joc-blockora-morph'
				)
			);
			return;
		}
		popupWinRef.current = win;
		const mount = seedPopoutDocument( win );
		popoutMountRef.current = mount;
		win.addEventListener( 'pagehide', () => {
			window.setTimeout( () => {
				if ( popupWinRef.current && ! popupWinRef.current.closed ) {
					return;
				}
				popupWinRef.current = null;
				popoutMountRef.current = null;
				setPopoutMount( null );
				setPoppedOut( false );
			}, 0 );
		} );
		setPopoutMount( mount );
		setPoppedOut( true );
		setShellPrefs( ( prev ) => {
			if ( ! prev.minimized ) {
				return prev;
			}
			const next = { ...prev, minimized: false };
			saveShellPrefs( next );
			return next;
		} );
	}, [] );

	const exitEditMode = useCallback( () => {
		closePopout();
		document.querySelectorAll( '.bm-el-chrome' ).forEach( ( n ) => {
			const parent = n.parentElement;
			n.remove();
			if ( parent?.dataset?.bmPosFix === '1' ) {
				parent.style.position = '';
				delete parent.dataset.bmPosFix;
			}
		} );
		document
			.querySelectorAll( '[data-bm-edit].is-bm-fe-selected' )
			.forEach( ( n ) => n.classList.remove( 'is-bm-fe-selected' ) );
		setSelectedEl( null );
		setInstance( null );
		setSelectedElementKey( null );
		setFocusButtonIndex( null );
		setStyleBreakpoint( 'desktop' );
		setSaveMsg( '' );
		setEditingMode( false );
	}, [ setEditingMode, closePopout ] );

	const hasBm =
		document.querySelector( '[data-bm-edit="1"]' ) ||
		( cfg.instances || [] ).length > 0;

	const showSidebar = editing && instance && meta;
	const showHint = editing && ! showSidebar;
	const shellSide = shellPrefs.side === 'left' ? 'left' : 'right';
	const shellMinimized = Boolean( shellPrefs.minimized );
	const panelTitle = showSidebar
		? meta.title || meta.slug || __( 'Block', 'joc-blockora-morph' )
		: __( 'Blockora Morph', 'joc-blockora-morph' );

	const inspectorBody = showSidebar
		? createElement(
				'div',
				{
					className:
						'bm-fe-shell__body bm-gutenberg-inspector-tabs',
				},
				createElement( BlockInspectorPanel, {
					meta: contentMeta || meta,
					attributes,
					setAttributes,
					selectedElementKey,
					onSelectElement,
					onClearElement,
					initialTabName: inspectorTab,
					tabPanelKey,
					onTabSelect: setInspectorTab,
					onEditButtonStyles,
					focusButtonIndex,
					styleBreakpoint,
					onStyleBreakpointChange: setStyleBreakpoint,
					onViewSwitch,
					breakpointHints: 'frontend',
					className: 'bm-gutenberg-tabs',
					enableStyleScopeGate: true,
					allowElementReorder: true,
					libraryCssVars,
					styleScopeSurfaceId: styleScopeSurfaceIdFromEl( selectedEl ),
					focusSlideIndex,
					onFocusSlideIndexChange: setFocusSlideIndex,
					focusCardIndex,
					onFocusCardIndexChange: setFocusCardIndex,
				} )
		  )
		: createElement(
				'div',
				{ className: 'bm-fe-shell__body' },
				createElement( ShellHintBody, { side: shellSide } )
		  );

	const shellProps = {
		title: panelTitle,
		side: shellSide,
		minimized: shellMinimized,
		isHint: Boolean( showHint ),
		onSetSide: setShellSide,
		onMinimize: toggleShellMinimized,
		onPopOut: ENABLE_FE_POPOUT ? openPopout : null,
		onDockBack: ENABLE_FE_POPOUT ? closePopout : null,
		onClose: exitEditMode,
		onSave: showSidebar ? save : null,
		canSave: dirty,
		saving,
		saveMsg: showSidebar ? saveMsg : '',
		body: inspectorBody,
		styleBreakpoint,
		onStyleBreakpointChange: setStyleBreakpoint,
		onViewSwitch,
	};

	// Detect user closing the OS window via the title-bar X.
	useEffect( () => {
		if ( ! poppedOut ) {
			return undefined;
		}
		const id = window.setInterval( () => {
			const win = popupWinRef.current;
			if ( win && ! win.closed ) {
				return;
			}
			popupWinRef.current = null;
			popoutMountRef.current = null;
			setPopoutMount( null );
			setPoppedOut( false );
		}, 400 );
		return () => window.clearInterval( id );
	}, [ poppedOut ] );

	useEffect( () => {
		if ( ! editing && poppedOut ) {
			closePopout();
		}
	}, [ editing, poppedOut, closePopout ] );

	useEffect( () => {
		return () => {
			const win = popupWinRef.current;
			popupWinRef.current = null;
			try {
				if ( win && ! win.closed ) {
					win.close();
				}
			} catch ( e ) {
				// Ignore.
			}
		};
	}, [] );

	if ( ! hasBm ) {
		return null;
	}

	return createElement(
		Fragment,
		null,
		! editing
			? createElement( PencilFab, {
					active: false,
					onToggle: () => setEditingMode( true ),
			  } )
			: null,
		ENABLE_FE_POPOUT && editing && poppedOut
			? createElement( PopoutStatusBar, {
					onDockBack: closePopout,
					onClose: exitEditMode,
			  } )
			: null,
		ENABLE_FE_POPOUT &&
			editing &&
			poppedOut &&
			popoutMount &&
			( showSidebar || showHint )
			? createPortal(
					createElement( EditorShell, {
						...shellProps,
						mode: 'popout',
						minimized: false,
					} ),
					popoutMount
			  )
			: null,
		editing && ! poppedOut && ( showSidebar || showHint )
			? createElement( EditorShell, {
					...shellProps,
					mode: 'docked',
			  } )
			: null,
		formatSession
			? createElement( PreviewFormatToolbar, {
					session: formatSession,
			  } )
			: null,
		createElement( ConfirmModal, {
			open: chromeDeleteOpen,
			title:
				removeItemConfirmCopy.title ||
				__( 'Delete this item?', 'joc-blockora-morph' ),
			description:
				removeItemConfirmDescription ||
				__(
					'This item and its content will be permanently removed from this block.',
					'joc-blockora-morph'
				),
			icon: 'trash',
			tone: 'danger',
			confirmLabel:
				removeItemConfirmCopy.confirmLabel ||
				__( 'Delete', 'joc-blockora-morph' ),
			onCancel: () => setChromeDeleteOpen( false ),
			onConfirm: () => {
				setChromeDeleteOpen( false );
				onRemoveItemRef.current?.();
				onClearElementRef.current?.();
			},
		} )
	);
}

domReady( () => {
	initBmTabsNav( document );
	initBmAccordion( document );
	const cfg = getConfig();
	if ( ! cfg.postId ) {
		return;
	}
	if ( cfg.restUrl ) {
		const root = String( cfg.restUrl ).replace( /\/joc-blockora-morph\/v1\/?$/, '/' );
		apiFetch.use( apiFetch.createRootURLMiddleware( root ) );
	}
	if ( cfg.nonce ) {
		apiFetch.use( apiFetch.createNonceMiddleware( cfg.nonce ) );
	}
	const mount = document.createElement( 'div' );
	mount.id = 'bm-fe-root';
	document.body.appendChild( mount );
	const root = createRoot( mount );
	root.render( createElement( FrontEditorApp ) );
} );
