/**
 * Client-side Gutenberg registration for JOC Blockora Morph blocks.
 * PHP registers render_callback; this file provides edit UI + inserter visibility.
 */

import { addFilter } from '@wordpress/hooks';
import {
	BlockControls,
	InspectorControls,
	useBlockProps,
} from '@wordpress/block-editor';
import {
	PanelBody,
	TextControl,
	TextareaControl,
	ToolbarButton,
	ToolbarGroup,
} from '@wordpress/components';
import {
	useMemo,
	useEffect,
	useState,
	useRef,
	useCallback,
	createElement,
	Fragment,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { registerPlugin } from '@wordpress/plugins';
import { useSelect, useDispatch, select } from '@wordpress/data';
import {
	registerBlockType,
	unregisterBlockType,
	getBlockType,
	registerBlockCollection,
	getCategories,
	setCategories,
} from '@wordpress/blocks';
import domReady from '@wordpress/dom-ready';
import BlockInspectorPanel from '../shared/BlockInspectorPanel';
import { initBmTabsNav } from '../shared/bmTabsNav';
import './VariantInsertPicker';
import './BlockBrowser';
import './VariantSidebarTab';
import { injectInserterLibraryTags } from './inserterLibraryTags';
import {
	useButtonLibrary,
} from '../shared/buttonLibrary';
import { flushPendingLibraryCssVars } from '../shared/libraryCssVars';
import {
	createLinkedPlacementSetAttributes,
	resolvePlacementCssVars,
} from '../shared/instanceStyleFork';
import {
	isContentLinkedToMyBlock,
	resolveInstanceContentAttrs,
} from '../shared/instanceContentAttrs';
import { scrollInspectorToTop } from '../shared/inspectorScroll';
import { brandLogoUrl, brandBlockIcon } from '../shared/brandLogo';
import { libraryBlockIcon } from '../shared/blockDashicon';
import { gutenbergBlockIdentityIcon } from '../shared/blockIdentityIcon';
import {
	attributeForField,
	buttonHoverCss,
	defaultElementsVisibility,
	interpolate,
	visibilityKeysForField,
} from '../interpolate';
import {
	breakpointToWpDevice,
	elementFontFamilyCssFromVars,
	elementHoverCss,
	elementSpacingCssFromVars,
	mergeLibraryPreview,
	normalize,
	styleableFieldsInOrder,
	toCssForBreakpoint,
	wpDeviceToBreakpoint,
} from '../cssVars';
import { toCssForSlides } from '../shared/slideLayoutStyle';
import {
	elementsOrderDefaultsFromSchema,
	toElementsOrderCssForAttrs,
} from '../shared/elementsOrder';
import {
	serializeElementsVisibility,
	toElementsVisibilityCssForAttrs,
} from '../shared/elementsVisibility';
import {
	markStyleableAndSelected,
	slideIndexFromElement,
	cardIndexFromElement,
	isSliderChromeTarget,
	syncSelectionChrome,
	forceOpenDetailsForEditing,
	bindForceOpenDetails,
	activateBmTabForEditing,
} from '../shared/selectionChrome';
import {
	initBmAccordion,
	enforceAccordionExclusiveOpen,
} from '../shared/bmAccordion';
import { focusBmSliderSlide, releaseBmSliderFocus } from '../shared/sliderElements';
import {
	collectionItemChromeActionsAllowed,
	collectionMinItems,
	collectionRemoveConfirmCopy,
	duplicateCollectionItemAt,
	findCollectionRepeater,
	moveCollectionItemAt,
} from '../shared/collectionRepeater';
import ConfirmModal from '../components/ConfirmModal';
import {
	bindPreviewContentEdit,
	endPreviewContentEditForLinkEditor,
	isPreviewEditChromeTarget,
	PREVIEW_CONTENT_EDIT_CSS,
	subscribePreviewContentEditForceEnd,
} from '../shared/previewContentEdit';
import {
	appendAccordionItemToAttributes,
	appendCardToAttributes,
	appendTabToAttributes,
	injectPreviewAccordionAddButtons,
	injectPreviewCardsAddButtons,
	injectPreviewTabsAddButtons,
	isAccordionAddButtonTarget,
	isCardsAddButtonTarget,
	isTabsAddButtonTarget,
	PREVIEW_TABS_ADD_CSS,
} from '../shared/previewTabsAdd';
import { hydratePendingInlineIcons, prefetchInlineIconsFromValue } from '../shared/iconRegistry';
import { upgradePlacementAttrs } from '../shared/placementAttrsUpgrade';
import PreviewFormatToolbar from '../components/fields/PreviewFormatToolbar';
import './editor.scss';
/**
 * Current editor preview device as a Blockora Morph breakpoint.
 * WordPress resizes the canvas but does not change the real viewport, so
 * media-query CSS will not fire — we drive styles from this device type.
 */
function useEditorDeviceBreakpoint() {
	return useSelect( ( select ) => {
		let deviceType = 'Desktop';
		try {
			const editor = select( 'core/editor' );
			if ( editor && typeof editor.getDeviceType === 'function' ) {
				deviceType = editor.getDeviceType() || deviceType;
			} else {
				const editPost = select( 'core/edit-post' );
				if (
					editPost &&
					typeof editPost.__experimentalGetPreviewDeviceType === 'function'
				) {
					deviceType =
						editPost.__experimentalGetPreviewDeviceType() || deviceType;
				} else {
					const editSite = select( 'core/edit-site' );
					if (
						editSite &&
						typeof editSite.__experimentalGetPreviewDeviceType ===
							'function'
					) {
						deviceType =
							editSite.__experimentalGetPreviewDeviceType() ||
							deviceType;
					}
				}
			}
		} catch ( e ) {
			// Store may be unavailable outside the block editor.
		}
		return wpDeviceToBreakpoint( deviceType );
	}, [] );
}

function useSetEditorDevice() {
	const editorDispatch = useDispatch( 'core/editor' );
	const editPostDispatch = useDispatch( 'core/edit-post' );
	const editSiteDispatch = useDispatch( 'core/edit-site' );

	return ( breakpoint ) => {
		const device = breakpointToWpDevice( breakpoint );
		try {
			if (
				editorDispatch &&
				typeof editorDispatch.setDeviceType === 'function'
			) {
				editorDispatch.setDeviceType( device );
				return;
			}
			if (
				editPostDispatch &&
				typeof editPostDispatch.__experimentalSetPreviewDeviceType ===
					'function'
			) {
				editPostDispatch.__experimentalSetPreviewDeviceType( device );
				return;
			}
			if (
				editSiteDispatch &&
				typeof editSiteDispatch.__experimentalSetPreviewDeviceType ===
					'function'
			) {
				editSiteDispatch.__experimentalSetPreviewDeviceType( device );
			}
		} catch ( e ) {
			// Ignore when device preview API is unavailable.
		}
	};
}

function getEditorMeta() {
	return typeof jocbmEditor !== 'undefined'
		? jocbmEditor
		: {
				blocks: [],
				libraryBlocks: [],
				libraryBlockRegistry: [],
				isPro: false,
		  };
}

function editorIsProActive() {
	return Boolean( getEditorMeta().isPro );
}

function isLibraryBlockProLocked( meta ) {
	return Boolean( meta?.isPro ) && ! editorIsProActive();
}

function libraryPreviewClass( meta ) {
	// Match WP `wp_get_block_default_classname( joc-blockora-morph-lib/{slug}--{variant} )`
	// — keep `--` between slug and variant so preview / uploads CSS / wrapper align.
	const s = String( meta?.systemSlug || '' )
		.toLowerCase()
		.replace( /[^a-z0-9-]+/g, '-' )
		.replace( /^-+|-+$/g, '' );
	const v = String( meta?.variant || 'default' )
		.toLowerCase()
		.replace( /[^a-z0-9-]+/g, '-' )
		.replace( /^-+|-+$/g, '' );
	return `wp-block-joc-blockora-morph-lib-${ s }--${ v || 'default' }`;
}

const SIDEBAR_WIDTH = '420px';
const SIDEBAR_DEFAULT_WIDTH = '280px';
const SIDEBAR_TRANSITION_MS = 280;
const SIDEBAR_STYLE_ID = 'bm-wide-block-sidebar-style';
const WIDE_ATTR = 'data-bm-wide-sidebar';
const WIDE_BODY_CLASS = 'bm-wide-block-sidebar';

/** Right inspector only — never the left list-view / secondary sidebar. */
const PANEL_SELECTORS = [
	'.interface-interface-skeleton__sidebar .interface-complementary-area',
	'.interface-interface-skeleton__sidebar .editor-sidebar',
	'.interface-interface-skeleton__sidebar .edit-post-sidebar',
];

const PARENT_SELECTOR = '.interface-interface-skeleton__sidebar';

function isBlockoraMorphName( name ) {
	return (
		typeof name === 'string' &&
		( name.startsWith( 'joc-blockora-morph/' ) ||
			name.startsWith( 'joc-blockora-morph-lib/' ) )
	);
}

function getInspectorPanels() {
	const found = new Set();
	PANEL_SELECTORS.forEach( ( selector ) => {
		document.querySelectorAll( selector ).forEach( ( el ) => found.add( el ) );
	} );
	return Array.from( found );
}

function getSidebarParents() {
	return Array.from( document.querySelectorAll( PARENT_SELECTOR ) );
}

/**
 * Reserve full inspector width in the skeleton layout (content shrinks)
 * instead of overlaying the canvas.
 */
function applyWideSidebarLayout() {
	getSidebarParents().forEach( ( parent ) => {
		parent.style.setProperty( 'width', SIDEBAR_WIDTH, 'important' );
		parent.style.setProperty( 'min-width', SIDEBAR_WIDTH, 'important' );
		parent.style.setProperty( 'max-width', SIDEBAR_WIDTH, 'important' );
		parent.setAttribute( WIDE_ATTR, 'parent' );
	} );

	getInspectorPanels().forEach( ( panel ) => {
		panel.style.setProperty( 'width', '100%', 'important' );
		panel.style.setProperty( 'min-width', SIDEBAR_WIDTH, 'important' );
		panel.style.setProperty( 'max-width', SIDEBAR_WIDTH, 'important' );
		panel.style.setProperty( 'box-sizing', 'border-box', 'important' );
		panel.setAttribute( WIDE_ATTR, 'panel' );
	} );
}

/**
 * Pin an explicit width so CSS transitions have a real from → to pair.
 *
 * @param {string} width
 */
function pinSidebarWidth( width ) {
	getSidebarParents().forEach( ( parent ) => {
		parent.style.setProperty( 'width', width, 'important' );
		parent.style.setProperty( 'min-width', width, 'important' );
		parent.style.setProperty( 'max-width', width, 'important' );
	} );
	getInspectorPanels().forEach( ( panel ) => {
		panel.style.setProperty( 'width', '100%', 'important' );
		panel.style.setProperty( 'min-width', width, 'important' );
		panel.style.setProperty( 'max-width', width, 'important' );
		panel.style.setProperty( 'box-sizing', 'border-box', 'important' );
	} );
}

function clearWideSidebarInlineStyles() {
	document.querySelectorAll( `[${ WIDE_ATTR }]` ).forEach( ( el ) => {
		[
			'width',
			'min-width',
			'max-width',
			'box-sizing',
			'overflow',
			'position',
			'top',
			'right',
			'bottom',
			'left',
			'z-index',
			'box-shadow',
			'background',
		].forEach( ( prop ) => el.style.removeProperty( prop ) );
		el.removeAttribute( WIDE_ATTR );
	} );
	// Also clear width pins that may not have the attr yet (open prep).
	getSidebarParents().forEach( ( parent ) => {
		[ 'width', 'min-width', 'max-width' ].forEach( ( prop ) =>
			parent.style.removeProperty( prop )
		);
	} );
	getInspectorPanels().forEach( ( panel ) => {
		[ 'width', 'min-width', 'max-width', 'box-sizing' ].forEach( ( prop ) =>
			panel.style.removeProperty( prop )
		);
	} );
}

/**
 * Persist transition rules while widening / collapsing so open + close animate.
 * Wide widths are gated only by the body class (not the data-attr) so we can
 * pin 280px first, then flip the class and animate to 420px.
 */
function ensureSidebarStyleTag( enabled ) {
	let styleEl = document.getElementById( SIDEBAR_STYLE_ID );
	if ( ! enabled ) {
		styleEl?.remove();
		return;
	}
	if ( ! styleEl ) {
		styleEl = document.createElement( 'style' );
		styleEl.id = SIDEBAR_STYLE_ID;
		document.head.appendChild( styleEl );
	}
	const ease = `${ SIDEBAR_TRANSITION_MS }ms cubic-bezier(0.4, 0, 0.2, 1)`;
	styleEl.textContent = `
		.interface-interface-skeleton__sidebar {
			transition:
				width ${ ease },
				min-width ${ ease },
				max-width ${ ease } !important;
		}
		.interface-interface-skeleton__sidebar .interface-complementary-area,
		.interface-interface-skeleton__sidebar .editor-sidebar,
		.interface-interface-skeleton__sidebar .edit-post-sidebar {
			transition:
				min-width ${ ease },
				max-width ${ ease } !important;
			box-sizing: border-box !important;
		}
		body.${ WIDE_BODY_CLASS } .interface-interface-skeleton__sidebar {
			width: ${ SIDEBAR_WIDTH } !important;
			min-width: ${ SIDEBAR_WIDTH } !important;
			max-width: ${ SIDEBAR_WIDTH } !important;
		}
		body.${ WIDE_BODY_CLASS } .interface-interface-skeleton__sidebar .interface-complementary-area,
		body.${ WIDE_BODY_CLASS } .interface-interface-skeleton__sidebar .editor-sidebar,
		body.${ WIDE_BODY_CLASS } .interface-interface-skeleton__sidebar .edit-post-sidebar {
			width: 100% !important;
			min-width: ${ SIDEBAR_WIDTH } !important;
			max-width: ${ SIDEBAR_WIDTH } !important;
		}
		@media (prefers-reduced-motion: reduce) {
			.interface-interface-skeleton__sidebar,
			.interface-interface-skeleton__sidebar .interface-complementary-area,
			.interface-interface-skeleton__sidebar .editor-sidebar,
			.interface-interface-skeleton__sidebar .edit-post-sidebar {
				transition: none !important;
			}
		}
	`;
}

let widenRetryTimer = null;
let collapseTimer = null;
let widenFrame = null;
let applyingWidth = false;

/**
 * Widen the right inspector and shrink the canvas with a smooth width transition.
 * Retries briefly because the panel may mount after openGeneralSidebar().
 */
function widenBlockSidebar() {
	if ( collapseTimer ) {
		window.clearTimeout( collapseTimer );
		collapseTimer = null;
	}
	if ( widenFrame ) {
		window.cancelAnimationFrame( widenFrame );
		widenFrame = null;
	}
	if ( widenRetryTimer ) {
		window.clearInterval( widenRetryTimer );
		widenRetryTimer = null;
	}

	ensureSidebarStyleTag( true );

	// 1) Start from Gutenberg’s default width with transitions already active.
	document.body.classList.remove( WIDE_BODY_CLASS );
	document.querySelectorAll( `[${ WIDE_ATTR }]` ).forEach( ( el ) => {
		el.removeAttribute( WIDE_ATTR );
	} );
	applyingWidth = true;
	pinSidebarWidth( SIDEBAR_DEFAULT_WIDTH );
	applyingWidth = false;

	// Force the browser to commit 280px before we change to 420px.
	getSidebarParents().forEach( ( parent ) => {
		void parent.offsetWidth;
	} );

	const expand = () => {
		applyingWidth = true;
		document.body.classList.add( WIDE_BODY_CLASS );
		applyWideSidebarLayout();
		applyingWidth = false;
	};

	// 2) Next frame: expand so width actually transitions 280 → 420.
	widenFrame = window.requestAnimationFrame( () => {
		widenFrame = null;
		expand();
	} );

	// Keep layout applied if WP remounts the panel mid-animation / after open.
	let ticks = 0;
	widenRetryTimer = window.setInterval( () => {
		if ( ! document.body.classList.contains( WIDE_BODY_CLASS ) ) {
			return;
		}
		expand();
		ticks += 1;
		if ( ticks >= 20 ) {
			window.clearInterval( widenRetryTimer );
			widenRetryTimer = null;
		}
	}, 50 );
}

/**
 * Animate sidebar back to Gutenberg’s default width, then clear overrides.
 */
function resetBlockSidebarWidth() {
	if ( widenRetryTimer ) {
		window.clearInterval( widenRetryTimer );
		widenRetryTimer = null;
	}
	if ( widenFrame ) {
		window.cancelAnimationFrame( widenFrame );
		widenFrame = null;
	}
	if ( collapseTimer ) {
		window.clearTimeout( collapseTimer );
		collapseTimer = null;
	}

	// Keep transition CSS while collapsing so width animates to 280px.
	ensureSidebarStyleTag( true );
	document.body.classList.remove( WIDE_BODY_CLASS );
	document.querySelectorAll( `[${ WIDE_ATTR }]` ).forEach( ( el ) => {
		el.removeAttribute( WIDE_ATTR );
	} );

	pinSidebarWidth( SIDEBAR_DEFAULT_WIDTH );

	collapseTimer = window.setTimeout( () => {
		collapseTimer = null;
		clearWideSidebarInlineStyles();
		ensureSidebarStyleTag( false );
	}, SIDEBAR_TRANSITION_MS + 40 );
}

function buildAttributeConfig( meta ) {
	const attributes = {
		// Lock this placement to the My Blocks DB row (not display name).
		userBlockId: {
			type: 'number',
			default: Number( meta.id ) || 0,
		},
		// Keep empty — library Design ships via the user-block CSS file / live
		// libraryCssVars. Linked placements follow My Blocks until Customize forks.
		cssVars: {
			type: 'object',
			default: normalize( {} ),
		},
		styleScopeChoices: {
			type: 'object',
			default: {},
		},
		// Optional dirty flag for save UX; ignored at render.
		librarySaveRev: {
			type: 'number',
			default: 0,
		},
	};

	const defaults = meta.defaultAttrs || {};
	( meta.fieldSchema || [] ).forEach( ( field ) => {
		if ( ! field?.key ) {
			return;
		}
		const cfg = attributeForField( field, defaults );
		if ( cfg ) {
			attributes[ field.key ] = cfg;
		}
		if ( field.type === 'elementsOrder' ) {
			const visKeys = visibilityKeysForField( field, defaults );
			attributes.elementsVisibility = {
				type: 'object',
				default: serializeElementsVisibility(
					defaults.elementsVisibility &&
						typeof defaults.elementsVisibility === 'object' &&
						! Array.isArray( defaults.elementsVisibility )
						? defaults.elementsVisibility
						: defaultElementsVisibility( visKeys ),
					visKeys
				),
			};
		}
	} );

	return attributes;
}

function EditorTabs( {
	meta,
	attributes,
	setAttributes,
	selectedElementKey = null,
	onSelectElement,
	onClearElement,
	initialTabName = 'fields',
	tabPanelKey = 0,
	onTabSelect,
	onEditButtonStyles,
	focusButtonIndex = null,
	onViewSwitch,
	libraryCssVars,
	onEnsureSidebar,
	styleScopeSurfaceId = '',
	focusSlideIndex = null,
	onFocusSlideIndexChange,
	focusCardIndex = null,
	onFocusCardIndexChange,
	isLibraryBlock = false,
} ) {
	const styleBreakpoint = useEditorDeviceBreakpoint();
	const setEditorDevice = useSetEditorDevice();

	return createElement( BlockInspectorPanel, {
		meta,
		attributes,
		setAttributes,
		selectedElementKey,
		onSelectElement,
		onClearElement,
		initialTabName,
		tabPanelKey,
		onTabSelect,
		onEditButtonStyles,
		focusButtonIndex,
		styleBreakpoint,
		onStyleBreakpointChange: setEditorDevice,
		onViewSwitch,
		libraryCssVars,
		onEnsureSidebar,
		styleScopeSurfaceId,
		focusSlideIndex,
		onFocusSlideIndexChange,
		focusCardIndex,
		onFocusCardIndexChange,
		enableStyleScopeGate: ! isLibraryBlock,
		buttonLibrary: null,
		// Drag-reorder in Customization once unwired (gate hides UI while linked).
		allowElementReorder: true,
	} );
}

/**
 * Canvas-only selection styles. Injected into the block <style> tag so they
 * apply inside the iframed Gutenberg canvas (editor.css loads in the parent frame).
 */
const GUTENBERG_SELECTION_CSS = `
.bm-gutenberg-preview{position:relative}
.bm-gutenberg-preview .bm-hero{overflow:visible!important}
.bm-gutenberg-preview [data-bm-el][data-bm-styleable="true"],
.bm-gutenberg-preview .bm-card[data-bm-card-index][data-bm-styleable="true"],
.bm-gutenberg-preview .bm-pricing__card[data-bm-card-index][data-bm-styleable="true"]{cursor:pointer;box-shadow:none;outline:2px solid transparent;outline-offset:2px;transition:outline-color .12s ease}
.bm-gutenberg-preview [data-bm-el][data-bm-styleable="true"]:hover,
.bm-gutenberg-preview .bm-card[data-bm-card-index][data-bm-styleable="true"]:hover,
.bm-gutenberg-preview .bm-pricing__card[data-bm-card-index][data-bm-styleable="true"]:hover{outline-color:rgba(53,37,205,.45)}
.bm-gutenberg-preview [data-bm-el][data-bm-selected="true"],
.bm-gutenberg-preview .bm-card[data-bm-card-index][data-bm-selected="true"],
.bm-gutenberg-preview .bm-pricing__card[data-bm-card-index][data-bm-selected="true"]{outline-color:transparent!important;box-shadow:none!important}
.bm-gutenberg-preview .bm-el-hit{position:relative!important;display:block;width:fit-content;max-width:100%;box-sizing:border-box;line-height:0}
.bm-gutenberg-preview .bm-media-frame,.bm-gutenberg-preview .bm-el-hit--media{position:relative!important;width:var(--bm-mf-img-width, var(--bm-image-width, 100%));max-width:100%;margin-left:var(--bm-mf-media-margin-start, var(--bm-media-margin-inline-start, 0));margin-right:var(--bm-mf-media-margin-end, var(--bm-media-margin-inline-end, 0));box-sizing:border-box;line-height:0}
.bm-gutenberg-preview .bm-testimonial__meta>.bm-el-hit,.bm-gutenberg-preview .bm-testimonial__meta>.bm-media-frame{width:fit-content!important;flex-shrink:0!important;max-width:100%!important;order:var(--bm-author-image-order, 0)}
.bm-gutenberg-preview .bm-testimonial__meta>.bm-testimonial__avatar{order:var(--bm-author-image-order, 0)}
.bm-gutenberg-preview .bm-testimonial__meta .bm-testimonial__avatar{width:var(--bm-avatar-size,2.75rem)!important;height:var(--bm-avatar-size,2.75rem)!important;max-width:none!important}
.bm-gutenberg-preview .bm-media-frame>img,.bm-gutenberg-preview .bm-media-frame>video,.bm-gutenberg-preview .bm-media-frame>iframe,.bm-gutenberg-preview .bm-media-frame>.bm-hero__embed,.bm-gutenberg-preview .bm-el-hit--media>img,.bm-gutenberg-preview .bm-el-hit--media>video,.bm-gutenberg-preview .bm-el-hit--media>iframe{width:100%;max-width:100%;display:block;margin:0}
.bm-gutenberg-preview .bm-hero__embed,.bm-gutenberg-preview .bm-hero__embed.wp-block-embed{width:100%;max-width:100%;height:auto;aspect-ratio:auto;overflow:hidden;line-height:0;box-sizing:border-box}
.bm-gutenberg-preview .bm-hero__embed .wp-block-embed__wrapper{position:relative;display:block;width:100%;height:auto;aspect-ratio:auto;overflow:hidden;line-height:0;box-sizing:border-box}
.bm-gutenberg-preview .bm-hero__embed .bm-embed-ratio,.bm-gutenberg-preview .bm-hero__embed .wp-block-embed__wrapper>div[style*="padding-bottom"]{display:block;max-width:100%;box-sizing:border-box}
.bm-gutenberg-preview .bm-hero__embed .bm-embed-ratio iframe,.bm-gutenberg-preview .bm-hero__embed .wp-block-embed__wrapper>div[style*="padding-bottom"] iframe,.bm-gutenberg-preview .bm-hero__embed iframe{position:absolute!important;top:0!important;left:0!important;width:100%!important;max-width:none!important;height:100%!important;min-height:0!important;aspect-ratio:auto!important;border:0!important;margin:0!important;object-fit:unset!important}
.bm-gutenberg-preview .bm-el-hit>img,.bm-gutenberg-preview .bm-el-hit>video,.bm-gutenberg-preview .bm-el-hit>iframe,.bm-gutenberg-preview .bm-el-hit>embed,.bm-gutenberg-preview .bm-el-hit>object,.bm-gutenberg-preview .bm-media-frame>img,.bm-gutenberg-preview .bm-media-frame>video,.bm-gutenberg-preview .bm-media-frame>iframe,.bm-gutenberg-preview .bm-media-frame>embed,.bm-gutenberg-preview .bm-media-frame>object{max-width:100%}
.bm-gutenberg-preview .bm-media-frame>img,.bm-gutenberg-preview .bm-media-frame>video,.bm-gutenberg-preview .bm-media-frame>iframe,.bm-gutenberg-preview .bm-media-frame>embed,.bm-gutenberg-preview .bm-media-frame>object,.bm-gutenberg-preview .bm-el-hit--media>img,.bm-gutenberg-preview .bm-el-hit--media>video,.bm-gutenberg-preview .bm-el-hit--media>iframe,.bm-gutenberg-preview .bm-el-hit--media>embed,.bm-gutenberg-preview .bm-el-hit--media>object{margin-left:0;margin-right:0}
.bm-gutenberg-preview .bm-el-hit__overlay{position:absolute;inset:0;z-index:2;cursor:pointer;background:transparent;display:block}
.bm-gutenberg-preview .bm-hero--video > .bm-el-hit__overlay,
.bm-gutenberg-preview [data-layout="video-background"] > .bm-el-hit__overlay,
.bm-gutenberg-preview .bm-hero__video{pointer-events:none!important}
.bm-gutenberg-preview .bm-el-chrome--float{pointer-events:none!important;z-index:9999!important}
.bm-gutenberg-preview .bm-el-chrome__tools{pointer-events:auto!important;position:absolute;top:-2px;right:-2px;transform:translateY(-100%);display:flex;flex-direction:row;align-items:stretch;z-index:10000}
.bm-gutenberg-preview .bm-el-deselect,
.bm-gutenberg-preview .bm-el-delete,
.bm-gutenberg-preview .bm-el-duplicate,
.bm-gutenberg-preview .bm-el-drag{pointer-events:auto!important;box-sizing:border-box;width:22px;height:22px;margin:0;padding:0;cursor:pointer;display:flex;align-items:center;justify-content:center;flex:0 0 auto;line-height:1;color:#fff}
.bm-gutenberg-preview .bm-el-drag{cursor:grab!important}
.bm-gutenberg-preview .bm-el-drag:active{cursor:grabbing!important}
.bm-gutenberg-preview .bm-el-drag .dashicons,
.bm-gutenberg-preview .bm-el-duplicate .dashicons,
.bm-gutenberg-preview .bm-el-delete .dashicons{font-size:14px;width:14px;height:14px;line-height:1}
.bm-gutenberg-preview.is-bm-collection-dragging [data-bm-collection-drop="1"]{outline:2px dashed rgba(53,37,205,.28);outline-offset:3px;transition:outline-color .12s ease,background-color .12s ease}
.bm-gutenberg-preview.is-bm-collection-dragging .is-bm-drag-source{position:relative!important;opacity:.42!important;outline:2px dashed rgba(53,37,205,.55)!important;outline-offset:3px!important;background:rgba(53,37,205,.05)!important;filter:saturate(.35)}
.bm-gutenberg-preview.is-bm-collection-dragging .is-bm-drag-source::after{content:"Moving…";position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:5;pointer-events:none;font:700 12px/1.2 system-ui,sans-serif;letter-spacing:.02em;color:#3525cd;text-shadow:0 1px 0 rgba(255,255,255,.85);background:linear-gradient(180deg,rgba(255,255,255,.55),rgba(255,255,255,.28))}
.bm-gutenberg-preview.is-bm-collection-dragging .is-bm-drop-target{outline:2px solid rgba(53,37,205,.9)!important;outline-offset:3px;background:rgba(53,37,205,.07)}
${ PREVIEW_CONTENT_EDIT_CSS }
${ PREVIEW_TABS_ADD_CSS }
`;

function BlockPreview( {
	meta,
	attributes,
	setAttributes,
	clientId,
	selectableKeys = null,
	selectedElementKey = null,
	focusSlideIndex = null,
	focusCardIndex = null,
	focusButtonIndex = null,
	onElementSelect,
	libraryCssVars = null,
	onRemoveItem = null,
	canRemoveItem = false,
	removeItemConfirmTitle = '',
	removeItemConfirmLabel = '',
	removeItemConfirmDescription = '',
	onDuplicateItem = null,
	canDuplicateItem = false,
	onReorderItem = null,
	canReorderItem = false,
} ) {
	const breakpoint = useEditorDeviceBreakpoint();
	const rootRef = useRef( null );
	const onElementSelectRef = useRef( onElementSelect );
	onElementSelectRef.current = onElementSelect;
	const focusSlideIndexRef = useRef( focusSlideIndex );
	focusSlideIndexRef.current = focusSlideIndex;
	const focusCardIndexRef = useRef( focusCardIndex );
	focusCardIndexRef.current = focusCardIndex;
	const selectedKeyRef = useRef( selectedElementKey );
	selectedKeyRef.current = selectedElementKey;
	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 [ chromeDeleteOpen, setChromeDeleteOpen ] = useState( false );
	const requestChromeDeleteRef = useRef( () => {} );
	requestChromeDeleteRef.current = () => {
		if ( ! canRemoveItemRef.current || ! onRemoveItemRef.current ) {
			return;
		}
		setChromeDeleteOpen( true );
	};
	const chromeDeselect = () => {
		if ( selectedKeyRef.current ) {
			onElementSelectRef.current?.( null, { deselectElement: true } );
			return;
		}
		onElementSelectRef.current?.( null, {
			slideIndex: null,
			cardIndex: null,
		} );
	};
	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 attributesRef = useRef( attributes );
	attributesRef.current = attributes;
	const setAttributesRef = useRef( setAttributes );
	setAttributesRef.current = setAttributes;
	const contentEditSelectionRef = useRef( '' );
	const [ formatSession, setFormatSession ] = useState( null );
	const [ freezeEpoch, setFreezeEpoch ] = useState( 0 );
	const [ iconEpoch, setIconEpoch ] = useState( 0 );
	const editSurfaceActiveRef = useRef( false );
	const libraryBaselineRef = useRef( null );
	if ( libraryBaselineRef.current == null ) {
		libraryBaselineRef.current = normalize(
			libraryCssVars != null ? libraryCssVars : meta.cssVars || {}
		);
	}

	const blockEditorDispatch = useDispatch( 'core/block-editor' );
	const markEphemeralRef = useRef( () => {} );
	markEphemeralRef.current = () => {
		blockEditorDispatch?.__unstableMarkNextChangeAsNotPersistent?.();
	};

	// Format toolbar + canvas typing only after "Customize this instance".
	const contentLinked = isContentLinkedToMyBlock( meta, attributes );
	const allowFormatToolbar = ! contentLinked;

	const fieldAttrs = useMemo(
		() => resolveInstanceContentAttrs( meta, attributes ),
		[ meta, attributes, attributes.styleScopeChoices ]
	);

	const cssVars = useMemo( () => {
		// Same resolution for library + My Block: stylesheet defaults + sparse overrides.
		// Library placements never use StyleScope forks; mergeLibraryPreview still
		// strips accidental full-library attribute snapshots the same way.
		return resolvePlacementCssVars(
			attributes,
			libraryCssVars != null ? libraryCssVars : meta.cssVars || {},
			libraryBaselineRef.current,
			mergeLibraryPreview
		);
	}, [
		meta.isLibraryBlock,
		meta.cssVars,
		libraryCssVars,
		attributes.cssVars,
		attributes.styleScopeChoices,
	] );

	// Scope styles to this block instance so siblings of the same type don't share CSS.
	const instanceClass = useMemo( () => {
		const id = String( clientId || '' ).replace( /[^a-zA-Z0-9_-]/g, '' );
		return id ? `bm-gutenberg-i-${ id.slice( 0, 16 ) }` : 'bm-gutenberg-i-solo';
	}, [ clientId ] );

	const selectableSet = useMemo(
		() => new Set( ( selectableKeys || [] ).filter( Boolean ) ),
		[ selectableKeys ]
	);
	const selectableSetRef = useRef( selectableSet );
	selectableSetRef.current = selectableSet;

	// Re-render when Style reference catalog updates (incl. after My Blocks save).
	const buttonLibrary = useButtonLibrary( {
		refreshOnMount: true,
		refreshOnFocus: true,
	} );
	const libStyles = useMemo( () => {
		const map = {};
		( buttonLibrary || [] ).forEach( ( b ) => {
			const id = Number( b?.id );
			if ( Number.isFinite( id ) && id !== 0 && b.styles ) {
				map[ id ] = b.styles;
			}
		} );
		return map;
	}, [ buttonLibrary ] );

	useEffect( () => {
		let cancelled = false;
		prefetchInlineIconsFromValue( fieldAttrs ).then( ( count ) => {
			if ( ! cancelled && count > 0 ) {
				setIconEpoch( ( n ) => n + 1 );
			}
		} );
		return () => {
			cancelled = true;
		};
	}, [ fieldAttrs ] );

	void iconEpoch;
	const html = interpolate(
		meta.markup,
		{
			...fieldAttrs,
			cssVars,
			instanceId: String( clientId || instanceClass || '' )
				.replace( /[^a-zA-Z0-9_-]/g, '' )
				.slice( 0, 20 ),
		},
		libStyles
	);
	const htmlRef = useRef( html );
	htmlRef.current = html;
	const frozenHtmlRef = useRef( '' );
	void freezeEpoch;
	const previewHtml = editSurfaceActiveRef.current
		? frozenHtmlRef.current || html
		: html;

	// Inspector clicks leave the canvas contenteditable focused in some cases;
	// release freeze so Style reference / Design updates remount the preview.
	// Format toolbar is portaled to body — keep the freeze while using it.
	useEffect( () => {
		const releaseFreeze = () => {
			if ( ! editSurfaceActiveRef.current ) {
				return;
			}
			editSurfaceActiveRef.current = false;
			frozenHtmlRef.current = '';
			setFreezeEpoch( ( n ) => n + 1 );
			setFormatSession( null );
		};
		const unsubscribe = subscribePreviewContentEditForceEnd( releaseFreeze );
		const onPointerDownCapture = ( event ) => {
			if ( ! editSurfaceActiveRef.current ) {
				return;
			}
			const root = rootRef.current;
			if ( root && event.target instanceof Node && root.contains( event.target ) ) {
				return;
			}
			if ( isPreviewEditChromeTarget( event.target ) ) {
				return;
			}
			try {
				root?.ownerDocument?.activeElement?.blur?.();
			} catch ( e ) {
				// ignore
			}
			if ( ! editSurfaceActiveRef.current ) {
				return;
			}
			editSurfaceActiveRef.current = false;
			setFreezeEpoch( ( n ) => n + 1 );
		};
		window.addEventListener( 'pointerdown', onPointerDownCapture, true );
		return () => {
			unsubscribe();
			window.removeEventListener(
				'pointerdown',
				onPointerDownCapture,
				true
			);
		};
	}, [] );
	const previewClass = meta.isLibraryBlock
		? libraryPreviewClass( meta )
		: `wp-block-joc-blockora-morph-${ meta.slug }`;
	const selector = `.${ instanceClass }.${ previewClass }.bm-gutenberg-preview`;
	const css = toCssForBreakpoint( selector, cssVars, breakpoint );
	const slidesCss = toCssForSlides(
		selector,
		Array.isArray( fieldAttrs.slides ) ? fieldAttrs.slides : [],
		cssVars,
		breakpoint
	);
	const orderCss = toElementsOrderCssForAttrs(
		selector,
		fieldAttrs,
		elementsOrderDefaultsFromSchema( meta.fieldSchema || [], fieldAttrs ),
		breakpoint
	);
	const visCss = toElementsVisibilityCssForAttrs(
		selector,
		fieldAttrs,
		[],
		breakpoint
	);
	const hoverCss = elementHoverCss( selector, meta.fieldSchema || [] );
	const btnHover = buttonHoverCss( selector, fieldAttrs, 'buttons', libStyles );
	const fontCss = elementFontFamilyCssFromVars( selector, cssVars );
	const spacingCss = elementSpacingCssFromVars( selector, cssVars );
	const baseFont = `${ selector } {\n\tfont-family: var(--bm-font-family, "Inter", sans-serif);\n}\n${ selector } [data-bm-el],\n${ selector } :is(h1, h2, h3, h4, h5, h6) {\n\tfont-family: inherit;\n}`;
	const styleCss = [ baseFont, css, slidesCss, orderCss, visCss, hoverCss, btnHover, fontCss, spacingCss, GUTENBERG_SELECTION_CSS ]
		.filter( Boolean )
		.join( '\n\n' );

	// Mark styleable nodes, wrap media hit targets, sync floating selection chrome.
	useEffect( () => {
		const root = rootRef.current;
		if ( ! root ) {
			return undefined;
		}

		const found = markStyleableAndSelected(
			root,
			Array.from( selectableSetRef.current ),
			selectedElementKey,
			focusSlideIndex,
			focusCardIndex
		);

		syncSelectionChrome(
			root,
			found,
			chromeDeselect,
			chromeOptions()
		);

		const refreshChrome = () => {
			const selected = markStyleableAndSelected(
				root,
				Array.from( selectableSetRef.current ),
				selectedElementKey,
				focusSlideIndexRef.current,
				focusCardIndexRef.current
			);
			syncSelectionChrome(
				root,
				selected,
				chromeDeselect,
				chromeOptions()
			);
		};

		forceOpenDetailsForEditing( root, { focusCardIndex } );
		initBmAccordion( root.ownerDocument );
		enforceAccordionExclusiveOpen( root );
		if ( focusCardIndex != null ) {
			activateBmTabForEditing( root, focusCardIndex );
		}
		injectPreviewTabsAddButtons( root, {
			enabled: ! contentLinked,
			fieldSchema: meta.fieldSchema,
		} );
		injectPreviewAccordionAddButtons( root, {
			enabled: ! contentLinked,
			fieldSchema: meta.fieldSchema,
		} );
		injectPreviewCardsAddButtons( root, {
			enabled: ! contentLinked,
			fieldSchema: meta.fieldSchema,
		} );
		const unbindDetails = bindForceOpenDetails( root.ownerDocument );

		const ro =
			typeof ResizeObserver !== 'undefined'
				? new ResizeObserver( () => refreshChrome() )
				: null;
		if ( found && ro ) {
			ro.observe( found );
		}
		if ( root && ro ) {
			ro.observe( root );
		}

		window.addEventListener( 'resize', refreshChrome );
		const t = window.setTimeout( refreshChrome, 120 );
		const t2 = window.setTimeout( refreshChrome, 320 );
		return () => {
			unbindDetails?.();
			ro?.disconnect();
			window.removeEventListener( 'resize', refreshChrome );
			window.clearTimeout( t );
			window.clearTimeout( t2 );
		};
	}, [
		html,
		meta.fieldSchema,
		selectedElementKey,
		selectableSet,
		focusSlideIndex,
		focusCardIndex,
		contentLinked,
		canRemoveItem,
		canDuplicateItem,
		canReorderItem,
	] );

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

	useEffect( () => {
		if ( ! allowFormatToolbar && formatSession ) {
			setFormatSession( null );
		}
	}, [ allowFormatToolbar, formatSession ] );

	// Inline content editing for text / button labels in the canvas.
	useEffect( () => {
		const root = rootRef.current;
		if (
			! root ||
			typeof setAttributes !== 'function' ||
			! selectedElementKey ||
			contentLinked
		) {
			return undefined;
		}
		const selectionId = [
			selectedElementKey,
			focusSlideIndex,
			focusCardIndex,
			focusButtonIndex,
		].join( ':' );
		const autoFocus = contentEditSelectionRef.current !== selectionId;
		contentEditSelectionRef.current = selectionId;

		return bindPreviewContentEdit( root, {
			fieldSchema: meta.fieldSchema || [],
			getAttributes: () => attributesRef.current || {},
			setAttributes: ( patch ) => {
				setAttributesRef.current?.( patch );
			},
			markEphemeral: () => {
				markEphemeralRef.current?.();
			},
			selectedElementKey,
			focusSlideIndex,
			focusCardIndex,
			focusButtonIndex,
			autoFocus,
			onSessionStart: ( session ) => {
				if ( allowFormatToolbar ) {
					setFormatSession( session );
				}
			},
			onEditSurfaceActive: ( active ) => {
				const next = Boolean( active );
				if ( next ) {
					if ( ! editSurfaceActiveRef.current ) {
						frozenHtmlRef.current = htmlRef.current || '';
					}
					editSurfaceActiveRef.current = true;
					return;
				}
				if ( ! editSurfaceActiveRef.current ) {
					return;
				}
				editSurfaceActiveRef.current = false;
				setFreezeEpoch( ( n ) => n + 1 );
			},
			onSessionEnd: () => {
				setFormatSession( null );
				if ( editSurfaceActiveRef.current ) {
					editSurfaceActiveRef.current = false;
					setFreezeEpoch( ( n ) => n + 1 );
				}
			},
		} );
	}, [
		previewHtml,
		meta.fieldSchema,
		setAttributes,
		selectedElementKey,
		focusSlideIndex,
		focusCardIndex,
		focusButtonIndex,
		allowFormatToolbar,
		contentLinked,
	] );

	// Mount / remount BmSlider after interpolate updates (hero slider, etc.).
	useEffect( () => {
		const root = rootRef.current;
		if ( ! root || ! root.querySelector?.( '[data-bm-slider]' ) ) {
			return undefined;
		}
		const remount = () => {
			if ( window.jocbmSlider?.remount ) {
				window.jocbmSlider.remount( root );
			}
			const holdIdx = focusSlideIndexRef.current;
			if ( holdIdx != null || selectedElementKey ) {
				focusBmSliderSlide( holdIdx != null ? holdIdx : 0, root );
			}
		};
		remount();
		const t = window.setTimeout( remount, 80 );
		const t2 = window.setTimeout( remount, 250 );
		return () => {
			window.clearTimeout( t );
			window.clearTimeout( t2 );
		};
	}, [ previewHtml ] );

	// Hold / release autoplay when slide or element focus changes.
	useEffect( () => {
		const root = rootRef.current;
		if ( ! root?.querySelector?.( '[data-bm-slider]' ) ) {
			return;
		}
		if ( focusSlideIndex != null || selectedElementKey ) {
			focusBmSliderSlide(
				focusSlideIndex != null ? focusSlideIndex : 0,
				root
			);
		} else {
			releaseBmSliderFocus( root );
		}
	}, [ focusSlideIndex, selectedElementKey, previewHtml ] );

			// Click styleable elements → Styles; whitespace → Customize + clear selection.
	useEffect( () => {
		const root = rootRef.current;
		if ( ! root || ! onElementSelect ) {
			return undefined;
		}

		const interactiveSel =
			'a[href], button, input[type="submit"], input[type="button"], input[type="reset"], [role="button"]';

		const onClick = ( event ) => {
			if (
				event.target?.closest?.(
					'.bm-el-deselect, .bm-el-delete, .bm-el-duplicate, .bm-el-drag, .bm-el-chrome__tools'
				)
			) {
				return;
			}
			if ( isTabsAddButtonTarget( event.target ) ) {
				event.preventDefault();
				event.stopPropagation();
				if ( contentLinked ) {
					return;
				}
				const added = appendTabToAttributes(
					attributesRef.current,
					meta.fieldSchema
				);
				if ( ! added ) {
					return;
				}
				setAttributesRef.current?.( added.patch );
				onElementSelectRef.current?.( 'tabTitle', {
					slideIndex: null,
					cardIndex: added.newIndex,
					buttonIndex: null,
				} );
				return;
			}
			if ( isAccordionAddButtonTarget( event.target ) ) {
				event.preventDefault();
				event.stopPropagation();
				if ( contentLinked ) {
					return;
				}
				const added = appendAccordionItemToAttributes(
					attributesRef.current,
					meta.fieldSchema
				);
				if ( ! added ) {
					return;
				}
				setAttributesRef.current?.( added.patch );
				onElementSelectRef.current?.( 'accordionTitle', {
					slideIndex: null,
					cardIndex: added.newIndex,
					buttonIndex: null,
				} );
				return;
			}
			if ( isCardsAddButtonTarget( event.target ) ) {
				event.preventDefault();
				event.stopPropagation();
				if ( contentLinked ) {
					return;
				}
				const added = appendCardToAttributes(
					attributesRef.current,
					meta.fieldSchema
				);
				if ( ! added ) {
					return;
				}
				setAttributesRef.current?.( added.patch );
				onElementSelectRef.current?.( 'title', {
					slideIndex: null,
					cardIndex: added.newIndex,
					buttonIndex: null,
				} );
				return;
			}
			if ( isSliderChromeTarget( event.target ) ) {
				return;
			}

			const interactive = event.target?.closest?.( interactiveSel );
			if (
				interactive &&
				root.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();
			}

			const target = event.target?.closest?.( '[data-bm-el]' );
			const key =
				target &&
				root.contains( target ) &&
				selectableSetRef.current.has( target.getAttribute( 'data-bm-el' ) )
					? target.getAttribute( 'data-bm-el' )
					: null;

			const slideHit =
				event.target?.closest?.(
					'[data-bm-slide-index], .splide__slide, .bm-hero--slide'
				) || null;
			const slideIndex = slideHit
				? slideIndexFromElement( slideHit ) ??
				  slideIndexFromElement( target || event.target )
				: slideIndexFromElement( target || event.target );

			const cardHit =
				event.target?.closest?.( '[data-bm-card-index]' ) || null;
			const cardIndex = cardHit
				? cardIndexFromElement( cardHit ) ??
				  cardIndexFromElement( target || event.target )
				: cardIndexFromElement( target || event.target );

			const btnHit =
				event.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;
				}
			}

			if ( key || slideIndex != null || cardIndex != null ) {
				event.preventDefault();
				event.stopPropagation();
				if ( slideIndex != null ) {
					focusBmSliderSlide( slideIndex, root );
				}
				activateBmTabForEditing(
					target || event.target,
					cardIndex
				);
				onElementSelectRef.current?.( key, {
					slideIndex,
					cardIndex,
					buttonIndex:
						key === 'buttons' || buttonIndex != null
							? buttonIndex
							: null,
				} );
				return;
			}

			// Empty canvas / non-styleable area: clear selection and open Customize.
			event.preventDefault();
			event.stopPropagation();
			onElementSelectRef.current?.( null, {
				slideIndex: null,
				cardIndex: null,
			} );
		};

		const onPointerDown = ( event ) => {
			if ( isSliderChromeTarget( event.target ) ) {
				return;
			}
			const target = event.target?.closest?.(
				'[data-bm-el], [data-bm-slide-index], .splide__slide, .bm-hero--slide'
			);
			if ( target && root.contains( target ) ) {
				// Stop Splide drag (listens to mousedown/touchstart, not pointerdown).
				event.stopPropagation();
			}
		};

		const onDragStart = ( event ) => {
			const target = event.target?.closest?.( '[data-bm-el], img, video' );
			if ( target && root.contains( target ) ) {
				event.preventDefault();
			}
		};

		root.addEventListener( 'click', onClick, true );
		// Splide Drag binds to mousedown/touchstart (capture). pointerdown alone is not enough.
		root.addEventListener( 'pointerdown', onPointerDown, true );
		root.addEventListener( 'mousedown', onPointerDown, true );
		root.addEventListener( 'touchstart', onPointerDown, true );
		root.addEventListener( 'dragstart', onDragStart, true );
		return () => {
			root.removeEventListener( 'click', onClick, true );
			root.removeEventListener( 'pointerdown', onPointerDown, true );
			root.removeEventListener( 'mousedown', onPointerDown, true );
			root.removeEventListener( 'touchstart', onPointerDown, true );
			root.removeEventListener( 'dragstart', onDragStart, true );
		};
	}, [ previewHtml, selectableSet, onElementSelect, contentLinked, meta.fieldSchema ] );

	// Lucide / svg-inline icons resolve via REST — paint placeholders then hydrate.
	useEffect( () => {
		const root = rootRef.current;
		if ( ! root ) {
			return undefined;
		}
		let cancelled = false;
		hydratePendingInlineIcons( root ).then( () => {
			if ( cancelled || ! rootRef.current ) {
				return;
			}
			const found = markStyleableAndSelected(
				root,
				Array.from( selectableSetRef.current ),
				selectedElementKey,
				focusSlideIndex,
				focusCardIndex
			);
			syncSelectionChrome(
				root,
				found,
				chromeDeselect,
				chromeOptions()
			);
		} );
		return () => {
			cancelled = true;
		};
	}, [
		previewHtml,
		selectedElementKey,
		focusSlideIndex,
		focusCardIndex,
		canRemoveItem,
		canDuplicateItem,
		canReorderItem,
	] );

	return createElement(
		Fragment,
		null,
		styleCss
			? createElement( 'style', {
					dangerouslySetInnerHTML: { __html: styleCss },
			  } )
			: null,
		allowFormatToolbar && formatSession
			? createElement( PreviewFormatToolbar, {
					session: formatSession,
			  } )
			: null,
		createElement( 'div', {
			ref: rootRef,
			className: `${ previewClass } bm-gutenberg-preview ${ instanceClass }`,
			'data-bm-breakpoint': breakpoint,
			'data-bm-editor-preview': '1',
			dangerouslySetInnerHTML: { __html: previewHtml },
		} ),
		createElement( ConfirmModal, {
			open: chromeDeleteOpen,
			title:
				removeItemConfirmTitle ||
				__( '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:
				removeItemConfirmLabel || __( 'Delete', 'joc-blockora-morph' ),
			onCancel: () => setChromeDeleteOpen( false ),
			onConfirm: () => {
				setChromeDeleteOpen( false );
				onRemoveItemRef.current?.();
				onElementSelectRef.current?.( null, { deselectElement: true } );
			},
		} )
	);
}
function createEdit( meta ) {
	return function BlockoraMorphEdit( props ) {
		const { attributes, setAttributes: setAttributesRaw, clientId } = props;
		const attributesGuardRef = useRef( attributes );
		attributesGuardRef.current = attributes;
		const metaGuardRef = useRef( meta );
		metaGuardRef.current = meta;
		const setAttributes = useCallback(
			createLinkedPlacementSetAttributes( setAttributesRaw, {
				getMeta: () => metaGuardRef.current,
				getAttributes: () => attributesGuardRef.current,
				enabled: () => ! metaGuardRef.current?.isLibraryBlock,
			} ),
			[ setAttributesRaw ]
		);
		const blockProps = useBlockProps( {
			// Same class uploads CSS targets so library + My Block wrappers share cascade.
			className: `bm-gutenberg-block ${
				meta.isLibraryBlock
					? libraryPreviewClass( meta )
					: `wp-block-joc-blockora-morph-${ meta.slug }`
			}`,
		} );

		const libraryId = Number( meta.id ) || 0;
		const placementId = Number( attributes?.userBlockId ) || 0;
		const attrsUpgradedRef = useRef( false );

		// One-time attrs shape upgrades for saved placements (e.g. cards link → buttons).
		useEffect( () => {
			if ( attrsUpgradedRef.current ) {
				return;
			}
			const blockName = meta.isLibraryBlock
				? meta.name
				: `joc-blockora-morph/${ meta.slug }`;
			const { attrs: next, changed } = upgradePlacementAttrs(
				blockName,
				attributes,
				meta
			);
			if ( ! changed ) {
				return;
			}
			attrsUpgradedRef.current = true;
			setAttributesRaw( next );
		}, [ attributes, meta, setAttributesRaw ] );

		// Stamp legacy placements with the My Blocks row id once.
		useEffect( () => {
			if ( meta.isLibraryBlock || ! libraryId ) {
				return;
			}
			if ( placementId === libraryId ) {
				return;
			}
			if ( ! placementId ) {
				setAttributes( { userBlockId: libraryId } );
			}
		}, [
			meta.isLibraryBlock,
			libraryId,
			placementId,
			setAttributes,
		] );

		if (
			! meta.isLibraryBlock &&
			placementId > 0 &&
			libraryId > 0 &&
			placementId !== libraryId
		) {
			return createElement(
				'div',
				blockProps,
				createElement(
					'p',
					{ className: 'bm-gutenberg-block__missing' },
					__(
						'This My Block was removed from your library. Remove this block or re-insert it from the inserter.',
						'joc-blockora-morph'
					)
				)
			);
		}

		const { openGeneralSidebar } = useDispatch( 'core/edit-post' );
		const styleBreakpoint = useEditorDeviceBreakpoint();
		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 [ libraryCssVars, setLibraryCssVars ] = useState( () =>
			normalize( meta.cssVars || {} )
		);
		const [ libraryDefaultAttrs, setLibraryDefaultAttrs ] = useState(
			() => meta.defaultAttrs || {}
		);
		const inspectorTabRef = useRef( inspectorTab );
		inspectorTabRef.current = inspectorTab;
		const selectedElementKeyRef = useRef( selectedElementKey );
		selectedElementKeyRef.current = selectedElementKey;
		const viewSwitchedRef = useRef( false );

		const contentMeta = useMemo(
			() => ( {
				...meta,
				defaultAttrs: libraryDefaultAttrs,
			} ),
			[ meta, libraryDefaultAttrs ]
		);

		useEffect( () => {
			if ( meta.isLibraryBlock || ! meta.id ) {
				return undefined;
			}
			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.isLibraryBlock ] );

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

		const openSidebar = useCallback( () => {
			if ( openGeneralSidebar ) {
				openGeneralSidebar( 'edit-post/block' );
			}
			widenBlockSidebar();
		}, [ openGeneralSidebar ] );

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

		const goToTab = useCallback( ( tab ) => {
			if ( inspectorTabRef.current === tab ) {
				return;
			}
			setInspectorTab( tab );
			setTabPanelKey( ( k ) => k + 1 );
		}, [] );

		const onSelectElement = useCallback(
			( key, context = null ) => {
				const next = key || null;
				const prev = selectedElementKeyRef.current;
				if (
					next &&
					next === prev &&
					context?.slideIndex == null &&
					context?.cardIndex == null
				) {
					return;
				}
				setSelectedElementKey( next );
				if ( ! next ) {
					setFocusButtonIndex( null );
				} else {
					if (
						context?.slideIndex != null &&
						Number.isFinite( Number( context.slideIndex ) )
					) {
						setFocusSlideIndex( Number( context.slideIndex ) );
						setFocusCardIndex( null );
					}
					if (
						context?.cardIndex != null &&
						Number.isFinite( Number( context.cardIndex ) )
					) {
						setFocusCardIndex( Number( context.cardIndex ) );
						setFocusSlideIndex( null );
					}
					viewSwitchedRef.current = false;
					scrollInspectorToTop();
				}
			},
			[]
		);

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

		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 &&
			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;
			}
			// Avoid stale contenteditable commit writing into the reordered list.
			endPreviewContentEditForLinkEditor( {
				pauseCollectionWrites: true,
			} );
			setSelectedElementKey( null );
			setFocusButtonIndex( null );
			const removedIndex = Number( focusCardIndex );
			const list = Array.isArray(
				attributesGuardRef.current?.[ collectionKey ]
			)
				? attributesGuardRef.current[ collectionKey ]
				: collectionItems;
			const nextItems = list.filter( ( _, i ) => i !== removedIndex );
			const patch = { [ collectionKey ]: nextItems };
			const active = Number( attributesGuardRef.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(
				attributesGuardRef.current || attributes,
				collectionKey,
				sourceIndex
			);
			if ( ! result ) {
				return;
			}
			const patch = { ...result.patch };
			const active = Number(
				attributesGuardRef.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;
				}
				// Drop title selection so we never rebind contenteditable to a
				// new index against WP-stale preview DOM (duplicates titles).
				endPreviewContentEditForLinkEditor( {
					pauseCollectionWrites: true,
				} );
				setSelectedElementKey( null );
				setFocusButtonIndex( null );
				const result = moveCollectionItemAt(
					attributesGuardRef.current || attributes,
					collectionKey,
					from,
					to
				);
				if ( ! result ) {
					return;
				}
				setAttributes( result.patch );
				setFocusCardIndex( result.focusIndex );
			},
			[ canReorderItem, collectionKey, attributes, setAttributes ]
		);

		const onEditButtonStyles = useCallback(
			( fieldKey, index ) => {
				const key = fieldKey || 'buttons';
				const prev = selectedElementKeyRef.current;
				setSelectedElementKey( key );
				setFocusButtonIndex(
					typeof index === 'number' ? index : null
				);
				viewSwitchedRef.current = false;
				goToTab( 'styles' );
				if ( key !== prev ) {
					scrollInspectorToTop();
				}
				// Sidebar is already open when editing from the Fields tab.
				openSidebar();
			},
			[ openSidebar, goToTab ]
		);

		// Canvas clicks update inspector state only — never force-open the WP sidebar.
		const onCanvasElementSelect = useCallback(
			( key, context = null ) => {
				if ( context?.deselectElement ) {
					setSelectedElementKey( null );
					setFocusButtonIndex( null );
					return;
				}

				const prev = selectedElementKeyRef.current;
				const tab = inspectorTabRef.current;
				const slideIndex =
					context?.slideIndex != null &&
					Number.isFinite( Number( context.slideIndex ) )
						? Number( context.slideIndex )
						: null;
				const cardIndex =
					context?.cardIndex != null &&
					Number.isFinite( Number( context.cardIndex ) )
						? Number( context.cardIndex )
						: null;
				const buttonIndex =
					context?.buttonIndex != null &&
					Number.isFinite( Number( context.buttonIndex ) )
						? Number( context.buttonIndex )
						: null;

				// Clicked a slide (or element on it) → Design / Slides for that slide.
				if ( slideIndex != null ) {
					setFocusSlideIndex( slideIndex );
					setFocusCardIndex( null );
					viewSwitchedRef.current = false;
					if ( key ) {
						if ( key === prev && tab === 'styles' ) {
							goToTab( 'styles' );
							return;
						}
						setSelectedElementKey( key );
						setFocusButtonIndex(
							key === 'buttons' ? buttonIndex : null
						);
						goToTab( 'styles' );
						scrollInspectorToTop();
						return;
					}
					setSelectedElementKey( null );
					setFocusButtonIndex( null );
					goToTab( 'styles' );
					scrollInspectorToTop();
					return;
				}

				if ( cardIndex != null ) {
					setFocusCardIndex( cardIndex );
					setFocusSlideIndex( null );
					viewSwitchedRef.current = false;
					setSelectedElementKey( key || null );
					setFocusButtonIndex(
						key === 'buttons' ? buttonIndex : null
					);
					goToTab( 'styles' );
					scrollInspectorToTop();
					return;
				}

				if ( key ) {
					if ( key === prev && tab === 'styles' ) {
						if ( key === 'buttons' && buttonIndex != null ) {
							setFocusButtonIndex( buttonIndex );
						}
						return;
					}
					setSelectedElementKey( key );
					setFocusButtonIndex(
						key === 'buttons' ? buttonIndex : null
					);
					viewSwitchedRef.current = false;
					goToTab( 'styles' );
					scrollInspectorToTop();
					return;
				}

				// Main block / empty canvas → Items (or Slides) list.
				setSelectedElementKey( null );
				setFocusButtonIndex( null );
				setFocusSlideIndex( null );
				setFocusCardIndex( null );

				if ( viewSwitchedRef.current ) {
					viewSwitchedRef.current = false;
					goToTab( 'styles' );
					scrollInspectorToTop();
					return;
				}

				if ( ! prev && tab === 'styles' ) {
					return;
				}

				goToTab( 'styles' );
				scrollInspectorToTop();
			},
			[ goToTab ]
		);

		return createElement(
			Fragment,
			null,
			createElement(
				BlockControls,
				{ group: 'block' },
				createElement(
					ToolbarGroup,
					null,
					createElement( ToolbarButton, {
						icon: 'edit',
						label: __( 'Edit block settings', 'joc-blockora-morph' ),
						onClick: openSidebar,
					} )
				)
			),
			createElement(
				InspectorControls,
				null,
				createElement(
					PanelBody,
					{
						className: 'bm-inspector-panel',
						title: createElement(
							'span',
							{ className: 'bm-inspector-brand' },
							createElement( 'img', {
								className: 'bm-inspector-brand__logo',
								src: brandLogoUrl(),
								alt: '',
								width: 28,
								height: 28,
								decoding: 'async',
							} ),
							createElement(
								'span',
								{ className: 'bm-inspector-brand__text' },
								__( 'JOC Blockora Morph', 'joc-blockora-morph' )
							)
						),
						initialOpen: true,
					},
					createElement(
						'div',
						{ className: 'bm-gutenberg-inspector-tabs' },
						createElement( EditorTabs, {
							meta: contentMeta,
							attributes,
							setAttributes,
							selectedElementKey,
							onSelectElement,
							onClearElement,
							initialTabName: inspectorTab,
							tabPanelKey,
							onTabSelect: setInspectorTab,
							onEditButtonStyles,
							focusButtonIndex,
							onViewSwitch,
							libraryCssVars,
							onEnsureSidebar: openSidebar,
							styleScopeSurfaceId: clientId,
							focusSlideIndex,
							onFocusSlideIndexChange: setFocusSlideIndex,
							focusCardIndex,
							onFocusCardIndexChange: setFocusCardIndex,
							isLibraryBlock: Boolean( meta.isLibraryBlock ),
						} )
					)
				)
			),
			createElement(
				'div',
				blockProps,
				createElement( BlockPreview, {
					meta: contentMeta,
					attributes,
					setAttributes,
					clientId,
					selectableKeys,
					selectedElementKey,
					focusSlideIndex,
					focusCardIndex,
					focusButtonIndex,
					onElementSelect: onCanvasElementSelect,
					libraryCssVars,
					onRemoveItem: canRemoveItem ? onRemoveItem : undefined,
					canRemoveItem,
					onDuplicateItem: canDuplicateItem
						? onDuplicateItem
						: undefined,
					canDuplicateItem,
					onReorderItem: canReorderItem ? onReorderItem : undefined,
					canReorderItem,
					removeItemConfirmTitle: removeItemConfirmCopy.title,
					removeItemConfirmLabel: removeItemConfirmCopy.confirmLabel,
					removeItemConfirmDescription,
				} )
			)
		);
	};
}

/**
 * Current post type for inserter filtering (PHP localize, then editor store).
 *
 * @return {string}
 */
function getCurrentEditorPostType() {
	const fromPhp = String( getEditorMeta().currentPostType || '' ).trim();
	if ( fromPhp ) {
		return fromPhp;
	}
	try {
		const editor = select( 'core/editor' );
		if ( editor && typeof editor.getCurrentPostType === 'function' ) {
			return String( editor.getCurrentPostType() || '' ).trim();
		}
	} catch ( e ) {
		// Store unavailable outside the block editor.
	}
	return '';
}

/**
 * Advanced → Post types: empty/legacy defaults to post + page.
 *
 * @param {object} meta
 * @param {string} postType
 * @return {boolean}
 */
function isMyBlockAllowedForPostType( meta, postType ) {
	if ( ! postType ) {
		return true;
	}
	const raw = Array.isArray( meta?.postTypes ) ? meta.postTypes : [];
	const types = raw.map( ( t ) => String( t || '' ).trim() ).filter( Boolean );
	const effective = types.length ? types : [ 'post', 'page' ];
	return effective.includes( postType );
}

/**
 * Re-register each imported block on the client so the inserter always lists them.
 * PHP already registered the same names with render_callback.
 */
let didRegisterClientBlocks = false;
let registerAttempts = 0;

function hideLockedProLibraryBlocks() {
	if ( editorIsProActive() ) {
		return;
	}

	const registry = getEditorMeta().libraryBlockRegistry || [];
	registry.forEach( ( meta ) => {
		if ( ! meta?.isPro || ! meta?.name ) {
			return;
		}

		const existing = getBlockType( meta.name );
		if ( ! existing || existing.supports?.inserter === false ) {
			return;
		}

		try {
			unregisterBlockType( meta.name );
			registerBlockType( meta.name, {
				...existing,
				supports: {
					...( existing.supports || {} ),
					inserter: false,
				},
			} );
		} catch ( err ) {
			// eslint-disable-next-line no-console
			console.error(
				'JOC Blockora Morph: failed to hide pro library block',
				meta.name,
				err
			);
		}
	} );
}

function collectLibraryBlocksForRegistration( libraryBlocks ) {
	const out = [];
	const seen = new Set();
	( libraryBlocks || [] ).forEach( ( meta ) => {
		if ( ! meta?.name || seen.has( meta.name ) ) {
			return;
		}
		seen.add( meta.name );
		out.push( meta );
		( meta.variants || [] ).forEach( ( variant ) => {
			if ( ! variant?.name || seen.has( variant.name ) ) {
				return;
			}
			seen.add( variant.name );
			out.push( {
				...variant,
				packType: 'variant',
			} );
		} );
	} );
	return out;
}

function registerClientLibraryBlocks( libraryBlocks ) {
	if ( ! Array.isArray( libraryBlocks ) || ! libraryBlocks.length ) {
		hideLockedProLibraryBlocks();
		return;
	}

	try {
		registerBlockCollection( 'joc-blockora-morph-library', {
			title: __( 'JBM Library', 'joc-blockora-morph' ),
			icon: brandBlockIcon( 24 ),
		} );
	} catch ( e ) {
		// Collection may already exist.
	}

	try {
		const nextCategories = getCategories().map( ( category ) => {
			if ( category?.slug === 'joc-blockora-morph-library' ) {
				return {
					...category,
					title: __( 'JBM Library', 'joc-blockora-morph' ),
					icon: brandBlockIcon( 24 ),
				};
			}
			if ( category?.slug === 'joc-blockora-morph' ) {
				return {
					...category,
					title: __( 'My Library', 'joc-blockora-morph' ),
					icon: brandBlockIcon( 24 ),
				};
			}
			return category;
		} );
		setCategories( nextCategories );
	} catch ( e ) {
		// Categories API may be unavailable in older editors.
	}

	collectLibraryBlocksForRegistration( libraryBlocks ).forEach( ( meta ) => {
		if ( ! meta?.name || isLibraryBlockProLocked( meta ) ) {
			return;
		}

		const showInInserter =
			meta.packType !== 'variant' && ! isLibraryBlockProLocked( meta );

		try {
			if ( getBlockType( meta.name ) ) {
				unregisterBlockType( meta.name );
			}

			registerBlockType( meta.name, {
				apiVersion: 3,
				title: meta.title || meta.name,
				description: __( 'JBM Library block', 'joc-blockora-morph' ),
				category: 'joc-blockora-morph-library',
				icon: libraryBlockIcon( meta ),
				keywords: [
					'blockora-morph',
					'jbm library',
					'jbm',
					'library',
					meta.systemSlug,
					meta.title,
					meta.variant,
					meta.systemName,
					meta.categorySlug,
					meta.categoryName,
				].filter( Boolean ),
				supports: {
					html: false,
					anchor: true,
					className: true,
					inserter: showInInserter,
					align: [ 'wide', 'full' ],
					alignWide: true,
				},
				attributes: buildAttributeConfig( meta ),
				edit: createEdit( meta ),
				save: () => null,
			} );
		} catch ( err ) {
			// eslint-disable-next-line no-console
			console.error(
				'JOC Blockora Morph: failed to register library block',
				meta.name,
				err
			);
		}
	} );

	hideLockedProLibraryBlocks();
}

function registerClientBlocks() {
	if ( didRegisterClientBlocks ) {
		return;
	}

	const blocks = getEditorMeta().blocks || [];
	const libraryBlocks = getEditorMeta().libraryBlocks || [];
	if ( ! blocks.length && ! libraryBlocks.length ) {
		registerAttempts += 1;
		if ( registerAttempts < 20 ) {
			window.setTimeout( registerClientBlocks, 100 );
		}
		return;
	}

	didRegisterClientBlocks = true;

	try {
		registerBlockCollection( 'joc-blockora-morph', {
			title: __( 'My Library', 'joc-blockora-morph' ),
			icon: brandBlockIcon( 24 ),
		} );
	} catch ( e ) {
		// Collection may already exist.
	}

	try {
		const nextCategories = getCategories().map( ( category ) => {
			if ( category?.slug === 'joc-blockora-morph' ) {
				return {
					...category,
					title: __( 'My Library', 'joc-blockora-morph' ),
					icon: brandBlockIcon( 24 ),
				};
			}
			if ( category?.slug === 'joc-blockora-morph-library' ) {
				return {
					...category,
					title: __( 'JBM Library', 'joc-blockora-morph' ),
					icon: brandBlockIcon( 24 ),
				};
			}
			return category;
		} );
		setCategories( nextCategories );
	} catch ( e ) {
		// Categories API may be unavailable in older editors.
	}

	injectInserterLibraryTags();

	blocks.forEach( ( meta ) => {
		if ( ! meta?.name ) {
			return;
		}

		const postType = getCurrentEditorPostType();
		const showInInserter = isMyBlockAllowedForPostType( meta, postType );

		try {
			if ( getBlockType( meta.name ) ) {
				unregisterBlockType( meta.name );
			}

			registerBlockType( meta.name, {
				apiVersion: 3,
				title: meta.title || meta.name,
				description: __( 'My Library block', 'joc-blockora-morph' ),
				category: 'joc-blockora-morph',
				icon: gutenbergBlockIdentityIcon( meta ),
				keywords: [
					'blockora-morph',
					'joc blockora-morph',
					'joc',
					'my library',
					'my blocks',
					meta.slug,
					meta.title,
					meta.variant,
					meta.systemSlug,
					meta.systemName,
					meta.categorySlug,
					meta.categoryName,
				].filter( Boolean ),
				supports: {
					html: false,
					anchor: true,
					className: true,
					inserter: showInInserter,
					// Wide / full width (same toolbar as Cover / Group).
					align: [ 'wide', 'full' ],
					alignWide: true,
				},
				attributes: buildAttributeConfig( meta ),
				edit: createEdit( meta ),
				save: () => null,
			} );
		} catch ( err ) {
			// Keep other blocks registering if one fails.
			// eslint-disable-next-line no-console
			console.error( 'JOC Blockora Morph: failed to register', meta.name, err );
		}
	} );

	registerClientLibraryBlocks( libraryBlocks );
}

domReady( registerClientBlocks );
initBmTabsNav( document );

if (
	typeof window !== 'undefined' &&
	document.readyState !== 'loading'
) {
	registerClientBlocks();
}

/**
 * Keep the native WordPress block sidebar widened while a Blockora Morph block
 * is selected AND the inspector sidebar is open. Canvas content shrinks to
 * match (animated) instead of being covered by an overlay.
 */
function WideSidebarController() {
	const { isBlockoraMorphSelected, isSidebarOpen } = useSelect( ( select ) => {
		const block = select( 'core/block-editor' ).getSelectedBlock();
		const selected = isBlockoraMorphName( block?.name );

		const editPost = select( 'core/edit-post' );
		const editSite = select( 'core/edit-site' );
		const iface = select( 'core/interface' );

		let open = false;
		if ( editPost?.isEditorSidebarOpened ) {
			open = !! editPost.isEditorSidebarOpened();
		} else if ( editSite?.isEditorSidebarOpened ) {
			open = !! editSite.isEditorSidebarOpened();
		} else if ( iface?.getActiveComplementaryArea ) {
			open = !!(
				iface.getActiveComplementaryArea( 'core/edit-post' ) ||
				iface.getActiveComplementaryArea( 'core/edit-site' )
			);
		}

		return {
			isBlockoraMorphSelected: selected,
			isSidebarOpen: open,
		};
	}, [] );

	const shouldWiden = isBlockoraMorphSelected && isSidebarOpen;

	useEffect( () => {
		if ( ! shouldWiden ) {
			resetBlockSidebarWidth();
			return undefined;
		}

		widenBlockSidebar();

		const observer = new MutationObserver( () => {
			if ( applyingWidth ) {
				return;
			}
			const panel = getInspectorPanels()[ 0 ];
			if ( ! panel ) {
				return;
			}
			const width =
				panel.style.width || window.getComputedStyle( panel ).width;
			if (
				width === SIDEBAR_DEFAULT_WIDTH ||
				( width !== SIDEBAR_WIDTH && width !== '100%' )
			) {
				applyingWidth = true;
				applyWideSidebarLayout();
				applyingWidth = false;
			}
		} );
		observer.observe( document.body, {
			childList: true,
			subtree: true,
			attributes: true,
			attributeFilter: [ 'style', 'class' ],
		} );

		return () => {
			observer.disconnect();
			resetBlockSidebarWidth();
		};
	}, [ shouldWiden ] );

	return null;
}

registerPlugin( 'joc-blockora-morph-wide-sidebar', {
	render: WideSidebarController,
	icon: brandBlockIcon( 20 ),
} );

/**
 * Flush queued library cssVars when the post is Updated / Published
 * (not on autosave).
 */
function LibraryStylesSaveController() {
	const { isSaving, isAutosaving } = useSelect( ( select ) => {
		const editor = select( 'core/editor' );
		if ( ! editor ) {
			return { isSaving: false, isAutosaving: false };
		}
		return {
			isSaving: Boolean( editor.isSavingPost?.() ),
			isAutosaving: Boolean( editor.isAutosavingPost?.() ),
		};
	}, [] );
	const wasSavingRef = useRef( false );

	useEffect( () => {
		const started = isSaving && ! wasSavingRef.current;
		wasSavingRef.current = isSaving;
		if ( ! started || isAutosaving ) {
			return undefined;
		}
		if ( typeof window !== 'undefined' ) {
			window.dispatchEvent(
				new CustomEvent( 'joc-blockora-morph-library-flush-start' )
			);
		}
		flushPendingLibraryCssVars().catch( () => {
			// Errors re-queue; banner/UI can show on next edit.
		} );
		return undefined;
	}, [ isSaving, isAutosaving ] );

	return null;
}

registerPlugin( 'joc-blockora-morph-library-save', {
	render: LibraryStylesSaveController,
	icon: 'update',
} );

addFilter(
	'blocks.registerBlockType',
	'joc-blockora-morph/force-category',
	( settings, name ) => {
		if ( ! name || ! name.startsWith( 'joc-blockora-morph/' ) ) {
			return settings;
		}
		if ( name.startsWith( 'joc-blockora-morph-lib/' ) ) {
			return settings;
		}
		return {
			...settings,
			category: 'joc-blockora-morph',
		};
	}
);
