/**
 * Reusable rich-text FormatToolbar — buttons adapt to field allowedCommand.
 * Works with contenteditables in the same document or inside preview iframes.
 */
import {
	ColorIndicator,
	ColorPicker,
	Dropdown,
	TextControl,
	__experimentalDropdownContentWrapper as DropdownContentWrapper,
} from '@wordpress/components';
import {
	useCallback,
	useEffect,
	useMemo,
	useRef,
	useState,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
	sanitizeCssColor,
	sanitizeHref,
	toolbarHasCommand,
} from '../../shared/allowedHtml';
import { useThemeTokens } from '../../hooks/useThemeTokens';
import { INLINE_FONT_WEIGHT_OPTIONS } from '../../styleFieldDefaults';
import { normalizeLink } from '../../interpolate';
import SearchableSelect from '../SearchableSelect';
import ThemeColorPalette from '../ThemeColorPalette';
import LinkEditorForm from './LinkEditorForm';

/**
 * Nested / non-focusable color UI that must not dismiss the parent Dropdown.
 * Include the popover shell — ColorPicker clicks often never move document focus
 * into these nodes, so focus-outside must still treat them as "inside".
 */
const COLOR_UI_SELECTOR = [
	'.bm-format-toolbar__color',
	'.bm-format-toolbar__color-panel',
	'.bm-format-toolbar__color-popover',
	'.bm-format-toolbar__fonts',
	'.bm-format-toolbar__fonts-panel',
	'.bm-format-toolbar__fonts-popover',
	'.bm-format-toolbar__select-menu',
	'.bm-styled-select__list--portal',
	'.bm-theme-color-palette',
	'.components-dropdown__content.bm-format-toolbar__color-popover',
	'.components-dropdown__content.bm-format-toolbar__fonts-popover',
	'.components-color-palette__custom-color-dropdown-content',
	'.components-circular-option-picker',
	'.components-color-picker',
	'.react-colorful',
].join( ', ' );

const FONTS_UI_SELECTOR = [
	'.bm-format-toolbar__fonts',
	'.bm-format-toolbar__fonts-panel',
	'.bm-format-toolbar__fonts-popover',
	'.components-dropdown__content.bm-format-toolbar__fonts-popover',
	'.bm-format-toolbar__select-menu',
	'.bm-styled-select__list--portal',
	'.bm-select-menu--fixed',
].join( ', ' );

/**
 * @param {Event|null|undefined} event
 * @param {string} selector
 * @return {boolean}
 */
function eventInsideSelector( event, selector ) {
	const path =
		typeof event?.composedPath === 'function' ? event.composedPath() : [];
	const nodes = path.length
		? path
		: [ event?.target, event?.relatedTarget, document.activeElement ];
	for ( let i = 0; i < nodes.length; i++ ) {
		const node = nodes[ i ];
		if ( node?.nodeType === 1 && node.closest?.( selector ) ) {
			return true;
		}
	}
	return false;
}

/**
 * @param {EventTarget|null|undefined} raw
 * @param {string} selector
 * @return {boolean}
 */
function nodeInsideSelector( raw, selector ) {
	const el =
		raw?.nodeType === 1 ? raw : raw?.parentElement || null;
	return Boolean( el?.closest?.( selector ) );
}

/** @type {Array<{ cmd: string, exec: string, icon: string, label: string }>} */
const FORMAT_BUTTONS = [
	{
		cmd: 'bold',
		exec: 'bold',
		icon: 'editor-bold',
		label: __( 'Bold', 'joc-blockora-morph' ),
	},
	{
		cmd: 'italic',
		exec: 'italic',
		icon: 'editor-italic',
		label: __( 'Italic', 'joc-blockora-morph' ),
	},
	{
		cmd: 'underline',
		exec: 'underline',
		icon: 'editor-underline',
		label: __( 'Underline', 'joc-blockora-morph' ),
	},
	{
		cmd: 'strikethrough',
		exec: 'strikeThrough',
		icon: 'editor-strikethrough',
		label: __( 'Strikethrough', 'joc-blockora-morph' ),
	},
	{
		cmd: 'subscript',
		exec: 'subscript',
		glyph: 'X₂',
		label: __( 'Subscript', 'joc-blockora-morph' ),
	},
	{
		cmd: 'superscript',
		exec: 'superscript',
		glyph: 'X²',
		label: __( 'Superscript', 'joc-blockora-morph' ),
	},
	{
		cmd: 'bullist',
		exec: 'insertUnorderedList',
		icon: 'editor-ul',
		label: __( 'Bulleted list', 'joc-blockora-morph' ),
	},
	{
		cmd: 'numlist',
		exec: 'insertOrderedList',
		icon: 'editor-ol',
		label: __( 'Numbered list', 'joc-blockora-morph' ),
	},
];

/** Marks the live color-picker selection so highlight survives focus loss. */
const SEL_ATTR = 'data-bm-format-sel';
const SEL_BG = 'rgba(53, 116, 240, 0.35)';

/**
 * @param {HTMLElement|null} target
 * @return {Document}
 */
function ownerDoc( target ) {
	return target?.ownerDocument || document;
}

/**
 * @param {HTMLElement|null} target
 * @return {Window}
 */
function ownerWin( target ) {
	return ownerDoc( target ).defaultView || window;
}

/**
 * @param {HTMLElement|null} target
 * @return {Range|null}
 */
function saveSelection( target ) {
	const win = ownerWin( target );
	const sel = win.getSelection?.();
	if ( ! sel || sel.rangeCount === 0 ) {
		return null;
	}
	const range = sel.getRangeAt( 0 );
	if ( target && ! target.contains( range.commonAncestorContainer ) ) {
		return null;
	}
	try {
		return range.cloneRange();
	} catch ( e ) {
		return null;
	}
}

/**
 * Store a live or preferred range (collapsed caret or highlight).
 *
 * @param {{ current: Range|null }} ref
 * @param {Range|null} range
 */
function storeSavedRange( ref, range ) {
	if ( ! ref || ! range ) {
		return;
	}
	try {
		ref.current = range.cloneRange();
	} catch ( e ) {
		ref.current = range;
	}
}

/**
 * Insert / reuse a style span at a collapsed caret so the next typed
 * characters pick up the style (document-editor behavior).
 *
 * @param {HTMLElement|null} target
 * @param {Range|null} preferredRange
 * @param {Record<string, string>} props
 * @return {Range|null}
 */
function applyCollapsedTypingStyles( target, preferredRange, props ) {
	if ( ! target || ! props || typeof props !== 'object' ) {
		return preferredRange;
	}
	const doc = ownerDoc( target );
	let range = preferredRange;
	try {
		target.focus( { preventScroll: true } );
		if ( range ) {
			restoreSelection( range, target, { focus: true } );
		}
		range = saveSelection( target ) || range;
		if ( ! range || ! range.collapsed ) {
			return preferredRange;
		}
		if ( ! target.contains( range.commonAncestorContainer ) ) {
			return preferredRange;
		}

		// Reuse an empty / ZWSP typing span already under the caret.
		let node = range.startContainer;
		if ( node.nodeType === Node.TEXT_NODE ) {
			node = node.parentElement;
		}
		while ( node && node !== target ) {
			if ( isStyleOnlySpan( node ) ) {
				const span = /** @type {HTMLElement} */ ( node );
				const text = String( span.textContent || '' ).replace(
					/\u200b/g,
					''
				);
				if ( text === '' ) {
					const cur = readTextStyleProps( span );
					writeTextStyleProps( span, {
						color:
							props.color !== undefined
								? props.color || ''
								: cur.color,
						fontFamily:
							props.fontFamily !== undefined
								? props.fontFamily || ''
								: cur.fontFamily,
						fontSize:
							props.fontSize !== undefined
								? props.fontSize || ''
								: cur.fontSize,
						fontWeight:
							props.fontWeight !== undefined
								? props.fontWeight || ''
								: cur.fontWeight,
						lineHeight:
							props.lineHeight !== undefined
								? props.lineHeight || ''
								: cur.lineHeight,
						letterSpacing:
							props.letterSpacing !== undefined
								? props.letterSpacing || ''
								: cur.letterSpacing,
					} );
					const next = doc.createRange();
					const tip =
						span.lastChild &&
						span.lastChild.nodeType === Node.TEXT_NODE
							? span.lastChild
							: span.appendChild( doc.createTextNode( '\u200b' ) );
					const len = tip.textContent?.length || 0;
					next.setStart( tip, len );
					next.collapse( true );
					restoreSelection( next, target, { focus: true } );
					return next;
				}
				break;
			}
			node = node.parentElement;
		}

		const span = doc.createElement( 'span' );
		writeTextStyleProps( span, {
			color: props.color || '',
			fontFamily: props.fontFamily || '',
			fontSize: props.fontSize || '',
			fontWeight: props.fontWeight || '',
			lineHeight: props.lineHeight || '',
			letterSpacing: props.letterSpacing || '',
		} );
		const tip = doc.createTextNode( '\u200b' );
		span.appendChild( tip );
		range.insertNode( span );
		explodeNestedStyleSpans( target );
		const live = span.isConnected
			? span
			: target.querySelector?.( 'span' );
		const host =
			live && target.contains( live ) && isStyleOnlySpan( live )
				? live
				: span.isConnected
				? span
				: null;
		if ( ! host ) {
			return saveSelection( target ) || preferredRange;
		}
		const textNode =
			host.lastChild && host.lastChild.nodeType === Node.TEXT_NODE
				? host.lastChild
				: host.appendChild( doc.createTextNode( '\u200b' ) );
		const next = doc.createRange();
		next.setStart( textNode, textNode.textContent?.length || 0 );
		next.collapse( true );
		restoreSelection( next, target, { focus: true } );
		return next;
	} catch ( e ) {
		return preferredRange;
	}
}

/**
 * @param {Range|null} range
 * @param {HTMLElement|null} target
 * @param {{ focus?: boolean }} [options]
 */
function restoreSelection( range, target, options = {} ) {
	if ( ! range || ! target ) {
		return;
	}
	const { focus = true } = options;
	try {
		if ( focus ) {
			target.focus?.( { preventScroll: true } );
		}
		const sel = ownerWin( target ).getSelection?.();
		if ( ! sel ) {
			return;
		}
		sel.removeAllRanges();
		sel.addRange( range );
	} catch ( e ) {
		// ignore
	}
}

/**
 * Inline text color at the caret / selection start (span[style], font[color]).
 *
 * @param {HTMLElement|null} target
 * @return {string}
 */
function readSelectionCssColor( target ) {
	if ( ! target ) {
		return '';
	}
	const win = ownerWin( target );
	const sel = win.getSelection?.();
	if ( ! sel || sel.rangeCount === 0 ) {
		return '';
	}
	const range = sel.getRangeAt( 0 );
	if ( ! target.contains( range.commonAncestorContainer ) ) {
		return '';
	}

	let node = range.startContainer;
	if ( node.nodeType === 3 ) {
		node = node.parentElement;
	}

	while ( node && node !== target ) {
		if ( node.nodeType === 1 ) {
			const el = /** @type {HTMLElement} */ ( node );
			const styleAttr = el.getAttribute( 'style' ) || '';
			const fromStyle = styleAttr.match(
				/(?:^|;)\s*color\s*:\s*([^;]+)/i
			);
			if ( fromStyle ) {
				const safe = sanitizeCssColor( fromStyle[ 1 ].trim() );
				if ( safe ) {
					return safe;
				}
			}
			if ( el.style?.color ) {
				const safe = sanitizeCssColor( el.style.color );
				if ( safe ) {
					return safe;
				}
			}
			const colorAttr = el.getAttribute( 'color' );
			if ( colorAttr ) {
				const safe = sanitizeCssColor( colorAttr );
				if ( safe ) {
					return safe;
				}
			}
		}
		node = node.parentElement;
	}
	return '';
}

/**
 * @param {HTMLElement} span
 * @return {Range|null}
 */
function rangeForWrap( span ) {
	if ( ! span ) {
		return null;
	}
	try {
		const next = ownerDoc( span ).createRange();
		next.selectNodeContents( span );
		return next;
	} catch ( e ) {
		return null;
	}
}

/**
 * Whether a live range sits fully inside the persist wrap.
 *
 * @param {HTMLElement|null} wrap
 * @param {Range|null} range
 * @return {boolean}
 */
function rangeInsideWrap( wrap, range ) {
	if ( ! wrap || ! range ) {
		return false;
	}
	try {
		return (
			wrap.contains( range.startContainer ) &&
			wrap.contains( range.endContainer )
		);
	} catch ( e ) {
		return false;
	}
}

/**
 * Wrap the saved range in a highlight span so the selection stays visible
 * after the editor loses focus (picker / iframe).
 *
 * @param {HTMLElement|null} target
 * @param {Range|null} range
 * @param {{ current: HTMLElement|null }} wrapRef
 * @param {{ force?: boolean }} [options] When force, replace any existing wrap.
 * @return {Range|null}
 */
function persistSelection( target, range, wrapRef, options = {} ) {
	if ( ! target ) {
		return null;
	}
	const { force = false } = options;
	const existing = wrapRef?.current;
	if ( existing && target.contains( existing ) ) {
		// Already locked on this selection — keep it (avoids unwrap on
		// mousedown + open double-call that detached the range).
		if ( ! force || rangeInsideWrap( existing, range ) ) {
			existing.style.backgroundColor = SEL_BG;
			existing.setAttribute( SEL_ATTR, '1' );
			return rangeForWrap( existing );
		}
		endSelectionPersist( wrapRef, { unwrapIfPlain: true } );
	}
	if ( ! range || range.collapsed ) {
		return null;
	}
	try {
		const doc = ownerDoc( target );
		let r;
		try {
			r = range.cloneRange();
		} catch ( e ) {
			return null;
		}
		if ( ! target.contains( r.commonAncestorContainer ) ) {
			return null;
		}
		const fragment = r.extractContents();
		const span = doc.createElement( 'span' );
		span.setAttribute( SEL_ATTR, '1' );
		span.style.backgroundColor = SEL_BG;
		span.appendChild( fragment );
		// Keep nested structure while the picker is open — exploding here would
		// inherit the parent color onto the mark and risk merging it away.
		// Explode runs on apply / sanitize / endSelectionPersist instead.
		r.insertNode( span );
		if ( wrapRef ) {
			wrapRef.current = span;
		}
		const next = rangeForWrap( span );
		// Re-apply native selection on the mark so it still looks selected.
		if ( next ) {
			restoreSelection( next, target, { focus: false } );
		}
		return next;
	} catch ( e ) {
		return null;
	}
}

/**
 * Remove the temporary highlight. Unwrap if no color was applied.
 *
 * @param {{ current: HTMLElement|null }} wrapRef
 * @param {{ unwrapIfPlain?: boolean }} [options]
 */
function endSelectionPersist( wrapRef, options = {} ) {
	const { unwrapIfPlain = true } = options;
	const span = wrapRef?.current;
	if ( ! span || ! span.isConnected ) {
		if ( wrapRef ) {
			wrapRef.current = null;
		}
		return;
	}
	span.style.backgroundColor = '';
	span.removeAttribute( SEL_ATTR );
	const parentRoot = span.parentElement;
	if ( parentRoot ) {
		explodeNestedStyleSpans( parentRoot );
	}
	const styles = readTextStyleProps( span );
	const hasStyle = Boolean(
		styles.color ||
			styles.fontFamily ||
			styles.fontSize ||
			styles.fontWeight ||
			styles.lineHeight ||
			styles.letterSpacing
	);
	if ( span.isConnected && ! hasStyle && unwrapIfPlain ) {
		const parent = span.parentNode;
		if ( parent ) {
			while ( span.firstChild ) {
				parent.insertBefore( span.firstChild, span );
			}
			parent.removeChild( span );
			parent.normalize?.();
		}
	}
	if ( wrapRef ) {
		wrapRef.current = null;
	}
}

/**
 * Strip all color-picker selection marks under a root (keep applied text color).
 *
 * @param {ParentNode|null} root
 */
function clearPersistMarksInRoot( root ) {
	if ( ! root || typeof root.querySelectorAll !== 'function' ) {
		return;
	}
	root.querySelectorAll( `[${ SEL_ATTR }]` ).forEach( ( node ) => {
		const span = /** @type {HTMLElement} */ ( node );
		span.style.backgroundColor = '';
		span.removeAttribute( SEL_ATTR );
		const parentRoot = span.parentElement || root;
		explodeNestedStyleSpans( parentRoot );
		if ( ! span.isConnected ) {
			return;
		}
		const styles = readTextStyleProps( span );
		const hasStyle = Boolean(
			styles.color ||
				styles.fontFamily ||
				styles.fontSize ||
				styles.fontWeight ||
				styles.lineHeight ||
				styles.letterSpacing
		);
		if ( ! hasStyle ) {
			const parent = span.parentNode;
			if ( parent ) {
				while ( span.firstChild ) {
					parent.insertBefore( span.firstChild, span );
				}
				parent.removeChild( span );
				parent.normalize?.();
			}
		}
	} );
}

/**
 * Remove text color from an element and its descendants (span/font).
 *
 * @param {Element|null} root
 */
function stripColorFromTree( root ) {
	if ( ! root || root.nodeType !== Node.ELEMENT_NODE ) {
		return;
	}
	const el = /** @type {HTMLElement} */ ( root );
	if ( el.style && typeof el.style.removeProperty === 'function' ) {
		el.style.removeProperty( 'color' );
	} else if ( el.style ) {
		el.style.color = '';
	}
	if ( el.hasAttribute?.( 'color' ) ) {
		el.removeAttribute( 'color' );
	}
	// Drop empty style="" left behind.
	const style = el.getAttribute?.( 'style' );
	if ( style != null && ! String( style ).replace( /\s|;/g, '' ) ) {
		el.removeAttribute( 'style' );
	}
	Array.from( el.children || [] ).forEach( ( child ) => {
		stripColorFromTree( child );
	} );
}

/**
 * Unwrap empty utility tags left after stripping color.
 *
 * @param {Element} root
 */
function unwrapEmptyColorHosts( root ) {
	if ( ! root?.querySelectorAll ) {
		return;
	}
	Array.from( root.querySelectorAll( 'font' ) ).forEach( ( node ) => {
		const font = /** @type {HTMLElement} */ ( node );
		const parent = font.parentNode;
		if ( ! parent ) {
			return;
		}
		while ( font.firstChild ) {
			parent.insertBefore( font.firstChild, font );
		}
		parent.removeChild( font );
	} );
	Array.from( root.querySelectorAll( 'span' ) ).forEach( ( node ) => {
		const span = /** @type {HTMLElement} */ ( node );
		if ( span.hasAttribute( SEL_ATTR ) ) {
			return;
		}
		const names = span.getAttributeNames?.() || [];
		const onlyStyle = names.every( ( n ) => n === 'style' );
		const style = String( span.getAttribute( 'style' ) || '' ).trim();
		if ( ! onlyStyle || style ) {
			return;
		}
		const parent = span.parentNode;
		if ( ! parent ) {
			return;
		}
		while ( span.firstChild ) {
			parent.insertBefore( span.firstChild, span );
		}
		parent.removeChild( span );
	} );
	root.normalize?.();
}

/**
 * Clear foreground color on the persisted wrap or the current selection.
 *
 * @param {HTMLElement|null} target
 * @param {Range|null} preferredRange
 * @param {{ current: HTMLElement|null }} wrapRef
 * @return {Range|null}
 */
function clearForeColor( target, preferredRange, wrapRef ) {
	if ( ! target ) {
		return preferredRange;
	}
	const doc = ownerDoc( target );
	const existing = wrapRef?.current;

	if ( existing && target.contains( existing ) ) {
		stripColorFromTree( existing );
		unwrapEmptyColorHosts( existing );
		existing.style.backgroundColor = SEL_BG;
		existing.setAttribute( SEL_ATTR, '1' );
		existing.normalize?.();
		return rangeForWrap( existing );
	}

	if ( preferredRange ) {
		restoreSelection( preferredRange, target, { focus: true } );
	}

	try {
		const sel = ownerWin( target ).getSelection?.();
		if ( sel && sel.rangeCount && ! sel.isCollapsed ) {
			const range = sel.getRangeAt( 0 );
			if ( target.contains( range.commonAncestorContainer ) ) {
				const fragment = range.extractContents();
				const holder = doc.createElement( 'div' );
				holder.appendChild( fragment );
				stripColorFromTree( holder );
				unwrapEmptyColorHosts( holder );
				const wrap = doc.createDocumentFragment();
				while ( holder.firstChild ) {
					wrap.appendChild( holder.firstChild );
				}
				range.insertNode( wrap );
				return saveSelection( target ) || preferredRange;
			}
		}
		const node =
			sel?.anchorNode?.nodeType === Node.ELEMENT_NODE
				? sel.anchorNode
				: sel?.anchorNode?.parentElement;
		let colored = node?.closest?.(
			'span[style*="color"], font[color]'
		);
		if ( ! colored && node ) {
			let cur = /** @type {Element|null} */ ( node );
			while ( cur && cur !== target ) {
				if (
					cur.nodeType === 1 &&
					/** @type {HTMLElement} */ ( cur ).style?.color
				) {
					colored = cur;
					break;
				}
				cur = cur.parentElement;
			}
		}
		if ( colored && target.contains( colored ) ) {
			stripColorFromTree( colored );
			unwrapEmptyColorHosts( colored );
			return saveSelection( target ) || preferredRange;
		}
	} catch ( e ) {
		// ignore
	}
	return preferredRange;
}

/**
 * Style properties we keep on a single formatting span (not bold/italic tags).
 */
const STYLE_SPAN_PROPS = [
	'color',
	'font-family',
	'font-size',
	'font-weight',
	'line-height',
	'letter-spacing',
];

/**
 * Normalize a font-weight value to a numeric string (or '').
 *
 * @param {string} raw
 * @return {string}
 */
function normalizeFontWeightValue( raw ) {
	const v = String( raw || '' )
		.trim()
		.toLowerCase();
	if ( ! v ) {
		return '';
	}
	if ( v === 'bold' ) {
		return '700';
	}
	if ( v === 'normal' ) {
		return '400';
	}
	if ( /^(100|200|300|400|500|600|700|800|900)$/.test( v ) ) {
		return v;
	}
	const n = parseInt( v, 10 );
	if ( Number.isFinite( n ) && n >= 100 && n <= 900 && n % 100 === 0 ) {
		return String( n );
	}
	return '';
}

/**
 * Safe line-height: unitless, or px / rem / %.
 *
 * @param {string} raw
 * @return {string}
 */
function normalizeLineHeightValue( raw ) {
	const v = String( raw || '' )
		.trim()
		.toLowerCase()
		.replace( /["']/g, '' );
	if ( ! v || v === 'normal' || v === 'inherit' ) {
		return '';
	}
	// Unitless multiplier (e.g. 1.2).
	if ( /^[\d.]+$/.test( v ) ) {
		const n = parseFloat( v );
		if ( Number.isFinite( n ) && n > 0 && n <= 10 ) {
			return String( n );
		}
		return '';
	}
	// Explicit units: px, rem, %.
	if ( /^[\d.]+\s*(px|rem|%)$/i.test( v ) ) {
		const compact = v.replace( /\s/g, '' );
		const n = parseFloat( compact );
		if ( ! Number.isFinite( n ) || n <= 0 ) {
			return '';
		}
		if ( compact.endsWith( '%' ) && n > 1000 ) {
			return '';
		}
		if ( ( compact.endsWith( 'px' ) || compact.endsWith( 'rem' ) ) && n > 500 ) {
			return '';
		}
		return compact.slice( 0, 24 );
	}
	return '';
}

/**
 * Safe letter-spacing (length or 0 / normal).
 *
 * @param {string} raw
 * @return {string}
 */
function normalizeLetterSpacingValue( raw ) {
	const v = String( raw || '' )
		.trim()
		.toLowerCase()
		.replace( /["']/g, '' );
	if ( ! v || v === 'normal' || v === 'inherit' ) {
		return '';
	}
	if ( v === '0' ) {
		return '0';
	}
	if ( /^-?[\d.]+\s*(px|rem|em)?$/.test( v ) ) {
		const compact = v.replace( /\s/g, '' );
		if ( /[a-z]$/i.test( compact ) ) {
			return compact;
		}
		const n = parseFloat( compact );
		return Number.isFinite( n ) ? `${ n }px` : '';
	}
	return '';
}

/**
 * Whether a node is a span used only for CSS text styles (not semantic markup).
 *
 * @param {Node|null} node
 * @return {boolean}
 */
function isStyleOnlySpan( node ) {
	if ( ! node || node.nodeType !== Node.ELEMENT_NODE ) {
		return false;
	}
	const el = /** @type {HTMLElement} */ ( node );
	if ( el.tagName !== 'SPAN' ) {
		return false;
	}
	const names = el.getAttributeNames?.() || [];
	return names.every(
		( n ) => n === 'style' || n === SEL_ATTR || n === 'data-bm-format-sel'
	);
}

/**
 * @param {string} styleDecl e.g. `font-family: Inter` or `color: #f00`
 * @return {{ prop: string, value: string }|null}
 */
function parseStyleDecl( styleDecl ) {
	const m = String( styleDecl || '' ).match(
		/^\s*([a-z-]+)\s*:\s*(.+?)\s*;?\s*$/i
	);
	if ( ! m ) {
		return null;
	}
	return { prop: m[ 1 ].toLowerCase(), value: m[ 2 ].trim() };
}

/**
 * Read color / font-family / font-size / font-weight from an element.
 *
 * @param {HTMLElement} el
 * @return {{ color: string, fontFamily: string, fontSize: string, fontWeight: string }}
 */
function readTextStyleProps( el ) {
	return {
		color: String( el.style?.color || '' ).trim(),
		fontFamily: String( el.style?.fontFamily || '' ).trim(),
		fontSize: String( el.style?.fontSize || '' ).trim(),
		fontWeight: normalizeFontWeightValue( el.style?.fontWeight || '' ),
		lineHeight: normalizeLineHeightValue( el.style?.lineHeight || '' ),
		letterSpacing: normalizeLetterSpacingValue(
			el.style?.letterSpacing || ''
		),
	};
}

/**
 * Write merged text styles onto a span (drops empty props).
 *
 * @param {HTMLElement} el
 * @param {{ color?: string, fontFamily?: string, fontSize?: string, fontWeight?: string, lineHeight?: string, letterSpacing?: string }} props
 */
function writeTextStyleProps( el, props ) {
	const color = String( props.color || '' ).trim();
	const fontFamily = String( props.fontFamily || '' ).trim();
	const fontSize = String( props.fontSize || '' ).trim();
	const fontWeight = normalizeFontWeightValue( props.fontWeight || '' );
	const lineHeight = normalizeLineHeightValue( props.lineHeight || '' );
	const letterSpacing = normalizeLetterSpacingValue(
		props.letterSpacing || ''
	);
	if ( color ) {
		el.style.color = color;
	} else {
		el.style.removeProperty?.( 'color' );
		el.style.color = '';
	}
	if ( fontFamily ) {
		el.style.fontFamily = fontFamily;
	} else {
		el.style.removeProperty?.( 'font-family' );
		el.style.fontFamily = '';
	}
	if ( fontSize ) {
		el.style.fontSize = fontSize;
	} else {
		el.style.removeProperty?.( 'font-size' );
		el.style.fontSize = '';
	}
	if ( fontWeight ) {
		el.style.fontWeight = fontWeight;
	} else {
		el.style.removeProperty?.( 'font-weight' );
		el.style.fontWeight = '';
	}
	if ( lineHeight ) {
		el.style.lineHeight = lineHeight;
		// Inline spans ignore line-height for layout; block makes it take effect.
		el.style.display = 'block';
	} else {
		el.style.removeProperty?.( 'line-height' );
		el.style.lineHeight = '';
		if ( String( el.style?.display || '' ).toLowerCase() === 'block' ) {
			el.style.removeProperty?.( 'display' );
			el.style.display = '';
		}
	}
	if ( letterSpacing ) {
		el.style.letterSpacing = letterSpacing;
	} else {
		el.style.removeProperty?.( 'letter-spacing' );
		el.style.letterSpacing = '';
	}
	const style = el.getAttribute?.( 'style' );
	if ( style != null && ! String( style ).replace( /\s|;/g, '' ) ) {
		el.removeAttribute( 'style' );
	}
}

/**
 * Unwrap nested style-only spans under `root`, merging their styles onto `root`.
 * Use only when the whole `root` is one formatting unit (same selection).
 * Leaves semantic tags (strong/em/u/…) intact.
 *
 * @param {Element} root Outermost style span (not unwrapped).
 * @return {{ color: string, fontFamily: string, fontSize: string, fontWeight: string }}
 */
function flattenNestedStyleSpans( root ) {
	const acc = readTextStyleProps( /** @type {HTMLElement} */ ( root ) );

	const walk = ( parent ) => {
		Array.from( parent.childNodes ).forEach( ( child ) => {
			if ( child.nodeType !== Node.ELEMENT_NODE ) {
				return;
			}
			const el = /** @type {HTMLElement} */ ( child );
			if ( isStyleOnlySpan( el ) ) {
				const nested = readTextStyleProps( el );
				if ( nested.color ) {
					acc.color = nested.color;
				}
				if ( nested.fontFamily ) {
					acc.fontFamily = nested.fontFamily;
				}
				if ( nested.fontSize ) {
					acc.fontSize = nested.fontSize;
				}
				if ( nested.fontWeight ) {
					acc.fontWeight = nested.fontWeight;
				}
				if ( nested.lineHeight ) {
					acc.lineHeight = nested.lineHeight;
				}
				if ( nested.letterSpacing ) {
					acc.letterSpacing = nested.letterSpacing;
				}
				walk( el );
				while ( el.firstChild ) {
					parent.insertBefore( el.firstChild, el );
				}
				parent.removeChild( el );
				return;
			}
			walk( el );
		} );
	};

	walk( root );
	writeTextStyleProps( /** @type {HTMLElement} */ ( root ), acc );
	root.normalize?.();
	return acc;
}

/**
 * @param {{ color: string, fontFamily: string, fontSize: string, fontWeight: string }} a
 * @param {{ color: string, fontFamily: string, fontSize: string, fontWeight: string }} b
 * @return {boolean}
 */
function textStylesEqual( a, b ) {
	return (
		a.color === b.color &&
		a.fontFamily === b.fontFamily &&
		a.fontSize === b.fontSize &&
		a.fontWeight === b.fontWeight &&
		a.lineHeight === b.lineHeight &&
		a.letterSpacing === b.letterSpacing
	);
}

/**
 * @param {{ color: string, fontFamily: string, fontSize: string, fontWeight: string, lineHeight: string, letterSpacing: string }} props
 * @return {boolean}
 */
function hasTextStyle( props ) {
	return Boolean(
		props.color ||
			props.fontFamily ||
			props.fontSize ||
			props.fontWeight ||
			props.lineHeight ||
			props.letterSpacing
	);
}

/**
 * Split a style parent so a nested style child becomes a sibling:
 * `<span style="A">Build <span style="B">Better</span></span>`
 * → `<span style="A">Build </span><span style="B">Better</span>`
 *
 * @param {HTMLElement} parent
 * @param {HTMLElement} child
 */
function splitStyleParentAroundChild( parent, child ) {
	const host = parent.parentNode;
	if ( ! host || child.parentNode !== parent ) {
		return;
	}
	const doc = ownerDoc( parent );
	const parentStyles = readTextStyleProps( parent );

	const beforeFrag = doc.createDocumentFragment();
	while ( parent.firstChild && parent.firstChild !== child ) {
		beforeFrag.appendChild( parent.firstChild );
	}
	parent.removeChild( child );
	const afterFrag = doc.createDocumentFragment();
	while ( parent.firstChild ) {
		afterFrag.appendChild( parent.firstChild );
	}

	const wrapWithParentStyles = ( frag ) => {
		if ( ! frag.childNodes.length ) {
			return null;
		}
		if ( ! hasTextStyle( parentStyles ) ) {
			const flat = doc.createDocumentFragment();
			while ( frag.firstChild ) {
				flat.appendChild( frag.firstChild );
			}
			return flat;
		}
		const span = doc.createElement( 'span' );
		writeTextStyleProps( span, parentStyles );
		span.appendChild( frag );
		return span;
	};

	const beforeNode = wrapWithParentStyles( beforeFrag );
	const childStyles = readTextStyleProps( child );
	writeTextStyleProps( child, {
		color: childStyles.color || parentStyles.color,
		fontFamily: childStyles.fontFamily || parentStyles.fontFamily,
		fontSize: childStyles.fontSize || parentStyles.fontSize,
		fontWeight: childStyles.fontWeight || parentStyles.fontWeight,
		lineHeight: childStyles.lineHeight || parentStyles.lineHeight,
		letterSpacing:
			childStyles.letterSpacing || parentStyles.letterSpacing,
	} );
	const afterNode = wrapWithParentStyles( afterFrag );

	if ( beforeNode ) {
		host.insertBefore( beforeNode, parent );
	}
	host.insertBefore( child, parent );
	if ( afterNode ) {
		host.insertBefore( afterNode, parent );
	}
	host.removeChild( parent );
}

/**
 * Merge adjacent style-only spans that share the same styles.
 * Never merge the live color-picker selection mark — that clears the highlight.
 *
 * @param {ParentNode} root
 */
function mergeAdjacentIdenticalStyleSpans( root ) {
	if ( ! root?.querySelectorAll ) {
		return;
	}
	let changed = true;
	let guard = 0;
	while ( changed && guard++ < 40 ) {
		changed = false;
		const spans = Array.from( root.querySelectorAll( 'span' ) ).filter(
			isStyleOnlySpan
		);
		for ( const span of spans ) {
			if ( span.hasAttribute?.( SEL_ATTR ) ) {
				continue;
			}
			const next = span.nextSibling;
			if ( ! isStyleOnlySpan( next ) ) {
				continue;
			}
			if (
				/** @type {HTMLElement} */ ( next ).hasAttribute?.( SEL_ATTR )
			) {
				continue;
			}
			const a = readTextStyleProps( span );
			const b = readTextStyleProps(
				/** @type {HTMLElement} */ ( next )
			);
			if ( ! textStylesEqual( a, b ) ) {
				continue;
			}
			while ( next.firstChild ) {
				span.appendChild( next.firstChild );
			}
			next.parentNode?.removeChild( next );
			span.normalize?.();
			changed = true;
			break;
		}
	}
}

/**
 * Convert nested style spans into siblings (never span-within-span for styles).
 *
 * @param {ParentNode|null} root
 */
function explodeNestedStyleSpans( root ) {
	if ( ! root?.querySelectorAll ) {
		return;
	}
	let guard = 0;
	while ( guard++ < 40 ) {
		const candidates = Array.from( root.querySelectorAll( 'span' ) ).filter(
			isStyleOnlySpan
		);
		// Deepest first.
		candidates.sort( ( a, b ) => {
			let da = 0;
			let db = 0;
			let n = a;
			while ( n && n !== root ) {
				da += 1;
				n = n.parentElement;
			}
			n = b;
			while ( n && n !== root ) {
				db += 1;
				n = n.parentElement;
			}
			return db - da;
		} );
		const child = candidates.find( ( span ) =>
			isStyleOnlySpan( span.parentElement )
		);
		if ( ! child ) {
			break;
		}
		splitStyleParentAroundChild(
			/** @type {HTMLElement} */ ( child.parentElement ),
			/** @type {HTMLElement} */ ( child )
		);
	}
	mergeAdjacentIdenticalStyleSpans( root );
}

/**
 * If the range exactly covers a single style-only span's contents, return it.
 *
 * @param {Range} range
 * @param {HTMLElement} target
 * @return {HTMLElement|null}
 */
function styleSpanCoveringRange( range, target ) {
	if ( ! range || range.collapsed ) {
		return null;
	}
	try {
		let node = range.commonAncestorContainer;
		if ( node.nodeType === Node.TEXT_NODE ) {
			node = node.parentElement;
		}
		while ( node && node !== target ) {
			if ( isStyleOnlySpan( node ) ) {
				const span = /** @type {HTMLElement} */ ( node );
				const around = ownerDoc( target ).createRange();
				around.selectNodeContents( span );
				if (
					range.compareBoundaryPoints(
						Range.START_TO_START,
						around
					) === 0 &&
					range.compareBoundaryPoints(
						Range.END_TO_END,
						around
					) === 0
				) {
					return span;
				}
			}
			node = node.parentElement;
		}
	} catch ( e ) {
		return null;
	}
	return null;
}

/**
 * Wrap / update selection with text styles on a single span (no span nesting).
 *
 * @param {HTMLElement|null} target
 * @param {Range|null} preferredRange
 * @param {{ color?: string, fontFamily?: string, fontSize?: string }} props
 * @param {{ current?: HTMLElement|null }|null} [wrapRef] Persist mark ref (color picker)
 * @return {Range|null}
 */
function applyTextStyles( target, preferredRange, props, wrapRef = null ) {
	if ( ! target ) {
		return preferredRange;
	}
	const doc = ownerDoc( target );
	const win = ownerWin( target );

	const mergeProps = ( el ) => {
		const cur = readTextStyleProps( el );
		writeTextStyleProps( el, {
			color:
				props.color !== undefined
					? props.color || ''
					: cur.color,
			fontFamily:
				props.fontFamily !== undefined
					? props.fontFamily || ''
					: cur.fontFamily,
			fontSize:
				props.fontSize !== undefined
					? props.fontSize || ''
					: cur.fontSize,
			fontWeight:
				props.fontWeight !== undefined
					? props.fontWeight || ''
					: cur.fontWeight,
			lineHeight:
				props.lineHeight !== undefined
					? props.lineHeight || ''
					: cur.lineHeight,
			letterSpacing:
				props.letterSpacing !== undefined
					? props.letterSpacing || ''
					: cur.letterSpacing,
		} );
	};

	// Reuse the live color-picker persist wrap when present.
	const persist = wrapRef?.current;
	if ( persist && target.contains( persist ) ) {
		mergeProps( persist );
		if ( wrapRef ) {
			persist.style.backgroundColor = SEL_BG;
			persist.setAttribute( SEL_ATTR, '1' );
		}
		explodeNestedStyleSpans( target );
		const live =
			( wrapRef?.current && target.contains( wrapRef.current )
				? wrapRef.current
				: null ) ||
			target.querySelector?.( `[${ SEL_ATTR }]` ) ||
			persist;
		if ( wrapRef && live && target.contains( live ) ) {
			wrapRef.current = live;
			live.style.backgroundColor = SEL_BG;
			live.setAttribute( SEL_ATTR, '1' );
		}
		return rangeForWrap( live );
	}

	try {
		target.focus( { preventScroll: true } );
		if ( preferredRange ) {
			restoreSelection( preferredRange, target, { focus: true } );
		}

		const sel = win.getSelection?.();
		let range = null;
		if ( sel && sel.rangeCount ) {
			const live = sel.getRangeAt( 0 );
			if (
				! live.collapsed &&
				target.contains( live.commonAncestorContainer )
			) {
				range = live;
			}
		}
		if (
			! range &&
			preferredRange &&
			! preferredRange.collapsed &&
			target.contains( preferredRange.commonAncestorContainer )
		) {
			range = preferredRange.cloneRange();
			restoreSelection( range, target, { focus: true } );
		}
		if ( ! range || range.collapsed ) {
			return applyCollapsedTypingStyles( target, range || preferredRange, props );
		}

		const existing = styleSpanCoveringRange( range, target );
		if ( existing && target.contains( existing ) ) {
			mergeProps( existing );
			explodeNestedStyleSpans( target );
			const live = target.contains( existing )
				? existing
				: target.querySelector?.( `[${ SEL_ATTR }]` );
			const next = live ? rangeForWrap( live ) : saveSelection( target );
			if ( next && sel ) {
				sel.removeAllRanges();
				sel.addRange( next );
			}
			if ( wrapRef && live ) {
				wrapRef.current = live;
				live.style.backgroundColor = SEL_BG;
				live.setAttribute( SEL_ATTR, '1' );
			}
			return next;
		}

		const work = range.cloneRange();
		const fragment = work.extractContents();
		const span = doc.createElement( 'span' );
		span.appendChild( fragment );
		// Pull styles out of any nested style spans in the extracted content.
		const nested = flattenNestedStyleSpans( span );
		writeTextStyleProps( span, {
			color:
				props.color !== undefined
					? props.color || ''
					: nested.color,
			fontFamily:
				props.fontFamily !== undefined
					? props.fontFamily || ''
					: nested.fontFamily,
			fontSize:
				props.fontSize !== undefined
					? props.fontSize || ''
					: nested.fontSize,
			fontWeight:
				props.fontWeight !== undefined
					? props.fontWeight || ''
					: nested.fontWeight,
			lineHeight:
				props.lineHeight !== undefined
					? props.lineHeight || ''
					: nested.lineHeight,
			letterSpacing:
				props.letterSpacing !== undefined
					? props.letterSpacing || ''
					: nested.letterSpacing,
		} );
		if ( wrapRef ) {
			span.setAttribute( SEL_ATTR, '1' );
			span.style.backgroundColor = SEL_BG;
			wrapRef.current = span;
		}
		work.insertNode( span );
		// If we inserted inside another style span, split into siblings.
		explodeNestedStyleSpans( target );
		// wrapRef may have been split — retarget to the styled span we care about.
		if ( wrapRef?.current && ! target.contains( wrapRef.current ) ) {
			wrapRef.current = null;
		}
		const styled =
			( wrapRef?.current && target.contains( wrapRef.current )
				? wrapRef.current
				: null ) ||
			( span.isConnected ? span : null ) ||
			target.querySelector?.( `[${ SEL_ATTR }]` );
		const focusEl =
			styled && target.contains( styled )
				? styled
				: span.isConnected
				? span
				: null;
		const next = focusEl
			? rangeForWrap( focusEl )
			: saveSelection( target );
		if ( next && sel ) {
			sel.removeAllRanges();
			sel.addRange( next );
		}
		if ( wrapRef && focusEl && isStyleOnlySpan( focusEl ) ) {
			wrapRef.current = focusEl;
			focusEl.style.backgroundColor = SEL_BG;
			focusEl.setAttribute( SEL_ATTR, '1' );
		}
		return next;
	} catch ( e ) {
		return preferredRange;
	}
}

/**
 * Apply foreground color to a saved range. Reuses the persist wrap while the
 * picker is open so opacity/hue drags don't nest spans or lose the range.
 *
 * @param {string} color
 * @param {HTMLElement|null} target
 * @param {Range|null} preferredRange
 * @param {{ current: HTMLElement|null }} wrapRef
 * @return {Range|null} Range covering the colored content
 */
function applyForeColor( color, target, preferredRange, wrapRef ) {
	const safe = sanitizeCssColor( color );
	if ( ! safe || ! target ) {
		return null;
	}

	let range = preferredRange;
	if ( range?.collapsed ) {
		return applyCollapsedTypingStyles( target, range, { color: safe } );
	}

	let existing = wrapRef?.current;
	if ( ! existing || ! target.contains( existing ) ) {
		persistSelection( target, preferredRange, wrapRef );
		existing = wrapRef?.current;
		if ( existing ) {
			// Flatten anything that was already nested inside the mark.
			flattenNestedStyleSpans( existing );
		}
	}

	return applyTextStyles(
		target,
		preferredRange,
		{ color: safe },
		wrapRef
	);
}

/**
 * Wrap the current (or saved) selection in a span with the given style.
 * Keeps the word selected afterward — does not collapse the caret.
 * Merges into one style span (no span-within-span).
 *
 * @param {string} styleDecl e.g. `font-family: Inter`
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @param {string} [fallbackExec] execCommand when selection is collapsed
 * @param {string} [fallbackValue]
 * @return {Range|null}
 */
function applyInlineStyle(
	styleDecl,
	target,
	preferredRange = null,
	fallbackExec,
	fallbackValue
) {
	if ( ! styleDecl || ! target ) {
		return preferredRange;
	}
	const parsed = parseStyleDecl( styleDecl );
	if ( ! parsed || ! STYLE_SPAN_PROPS.includes( parsed.prop ) ) {
		return preferredRange;
	}

	const props =
		parsed.prop === 'color'
			? { color: parsed.value }
			: parsed.prop === 'font-family'
			? { fontFamily: parsed.value }
			: parsed.prop === 'font-weight'
			? { fontWeight: normalizeFontWeightValue( parsed.value ) }
			: parsed.prop === 'line-height'
			? { lineHeight: normalizeLineHeightValue( parsed.value ) }
			: parsed.prop === 'letter-spacing'
			? { letterSpacing: normalizeLetterSpacingValue( parsed.value ) }
			: { fontSize: parsed.value };

	const next = applyTextStyles( target, preferredRange, props, null );
	if ( next ) {
		return next;
	}

	// Collapsed caret: pending typing styles (document-like).
	if ( ! preferredRange || preferredRange.collapsed ) {
		return applyCollapsedTypingStyles( target, preferredRange, props );
	}

	if ( fallbackExec ) {
		try {
			const doc = ownerDoc( target );
			target.focus( { preventScroll: true } );
			if ( preferredRange ) {
				restoreSelection( preferredRange, target, { focus: true } );
			}
			doc.execCommand( fallbackExec, false, fallbackValue || '' );
			return saveSelection( target ) || preferredRange;
		} catch ( e ) {
			// ignore
		}
	}
	return preferredRange;
}

/**
 * @param {string} family
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {Range|null}
 */
function applyFontFamily( family, target, preferredRange = null ) {
	const clean = String( family || '' )
		.replace( /[<>;{}]/g, '' )
		.trim()
		.slice( 0, 120 );
	if ( ! clean ) {
		return preferredRange;
	}
	return applyInlineStyle(
		`font-family: ${ clean }`,
		target,
		preferredRange,
		'fontName',
		clean
	);
}

/**
 * @param {string} size
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {Range|null}
 */
function applyFontSize( size, target, preferredRange = null ) {
	const raw = String( size || '' )
		.trim()
		.replace( /["']/g, '' )
		.toLowerCase();
	if ( ! /^[\d.]+\s*(px|rem|em|%)?$/.test( raw ) ) {
		return preferredRange;
	}
	const clean = /[a-z%]$/i.test( raw.replace( /\s/g, '' ) )
		? raw.replace( /\s/g, '' )
		: `${ parseFloat( raw ) }px`;
	if ( ! Number.isFinite( parseFloat( clean ) ) ) {
		return preferredRange;
	}
	return applyInlineStyle(
		`font-size: ${ clean }`,
		target,
		preferredRange
	);
}

/**
 * @param {string} weight
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {Range|null}
 */
function applyFontWeight( weight, target, preferredRange = null ) {
	const clean = normalizeFontWeightValue( weight );
	if ( ! clean ) {
		return preferredRange;
	}
	return applyInlineStyle(
		`font-weight: ${ clean }`,
		target,
		preferredRange
	);
}

/**
 * @param {string} lineHeight
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {Range|null}
 */
function applyLineHeight( lineHeight, target, preferredRange = null ) {
	const clean = normalizeLineHeightValue( lineHeight );
	if ( ! target ) {
		return preferredRange;
	}
	const doc = ownerDoc( target );
	let range = preferredRange || saveSelection( target );
	if ( range ) {
		restoreSelection( range, target, { focus: true } );
	} else {
		target.focus( { preventScroll: true } );
		range = saveSelection( target );
	}

	const heading = findHeadingAncestor(
		( range || saveSelection( target ) )?.commonAncestorContainer,
		target
	);

	// Headings are block boxes — set line-height on the heading itself.
	if ( heading && target.contains( heading ) ) {
		if ( clean ) {
			heading.style.lineHeight = clean;
		} else {
			heading.style.removeProperty?.( 'line-height' );
			heading.style.lineHeight = '';
		}
		if ( range ) {
			restoreSelection( range, target, { focus: true } );
			return range;
		}
		return saveSelection( target ) || preferredRange;
	}

	// Caret-only: pending typing style for the next characters.
	if ( ! range || range.collapsed ) {
		if ( ! clean ) {
			return preferredRange;
		}
		return applyCollapsedTypingStyles( target, range || preferredRange, {
			lineHeight: clean,
		} );
	}

	if ( clean ) {
		return applyInlineStyle(
			`line-height: ${ clean }`,
			target,
			range
		);
	}
	return applyTextStyles( target, range, { lineHeight: '' }, null );
}

/**
 * @param {string} letterSpacing
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {Range|null}
 */
function applyLetterSpacing( letterSpacing, target, preferredRange = null ) {
	const clean = normalizeLetterSpacingValue( letterSpacing );
	if ( ! clean && String( letterSpacing || '' ).trim() !== '0' ) {
		return preferredRange;
	}
	const value =
		clean ||
		( String( letterSpacing || '' ).trim() === '0' ? '0' : '' );
	if ( ! value ) {
		return preferredRange;
	}
	return applyInlineStyle(
		`letter-spacing: ${ value }`,
		target,
		preferredRange
	);
}

/**
 * Font-weight under the caret / selection start.
 *
 * @param {HTMLElement|null} target
 * @return {string}
 */
function readSelectionFontWeight( target ) {
	if ( ! target ) {
		return '';
	}
	const win = ownerWin( target );
	const sel = win.getSelection?.();
	if ( ! sel || ! sel.rangeCount ) {
		return '';
	}
	const range = sel.getRangeAt( 0 );
	if ( ! target.contains( range.commonAncestorContainer ) ) {
		return '';
	}
	let node = range.startContainer;
	if ( node.nodeType === 3 ) {
		node = node.parentElement;
	}
	while ( node && node !== target ) {
		if ( node.nodeType === 1 ) {
			const el = /** @type {HTMLElement} */ ( node );
			const fromStyle = normalizeFontWeightValue( el.style?.fontWeight || '' );
			if ( fromStyle ) {
				return fromStyle;
			}
		}
		node = node.parentElement;
	}
	return '';
}

/**
 * @param {HTMLElement|null} target
 * @param {(el: HTMLElement) => string} reader
 * @return {string}
 */
function readSelectionStyleProp( target, reader ) {
	if ( ! target || typeof reader !== 'function' ) {
		return '';
	}
	const win = ownerWin( target );
	const sel = win.getSelection?.();
	if ( ! sel || ! sel.rangeCount ) {
		return '';
	}
	const range = sel.getRangeAt( 0 );
	if ( ! target.contains( range.commonAncestorContainer ) ) {
		return '';
	}
	let node = range.startContainer;
	if ( node.nodeType === 3 ) {
		node = node.parentElement;
	}
	while ( node && node !== target ) {
		if ( node.nodeType === 1 ) {
			const value = reader( /** @type {HTMLElement} */ ( node ) );
			if ( value ) {
				return value;
			}
		}
		node = node.parentElement;
	}
	return '';
}

/**
 * @param {HTMLElement|null} target
 * @return {string}
 */
function readSelectionFontFamily( target ) {
	return readSelectionStyleProp( target, ( el ) =>
		String( el.style?.fontFamily || '' )
			.replace( /["']/g, '' )
			.trim()
	);
}

/**
 * @param {HTMLElement|null} target
 * @return {string}
 */
function readSelectionFontSize( target ) {
	return readSelectionStyleProp( target, ( el ) =>
		String( el.style?.fontSize || '' ).trim()
	);
}

/**
 * @param {HTMLElement|null} target
 * @return {string}
 */
function readSelectionLineHeight( target ) {
	if ( ! target ) {
		return '';
	}
	const range = saveSelection( target );
	const heading = findHeadingAncestor(
		range?.commonAncestorContainer,
		target
	);
	if ( heading ) {
		const fromHeading = normalizeLineHeightValue(
			heading.style?.lineHeight || ''
		);
		if ( fromHeading ) {
			return fromHeading;
		}
	}
	return readSelectionStyleProp( target, ( el ) =>
		normalizeLineHeightValue( el.style?.lineHeight || '' )
	);
}

/**
 * @param {HTMLElement|null} target
 * @return {string}
 */
function readSelectionLetterSpacing( target ) {
	return readSelectionStyleProp( target, ( el ) =>
		normalizeLetterSpacingValue( el.style?.letterSpacing || '' )
	);
}

const HEADING_TAGS = new Set( [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ] );

/**
 * @param {Node|null|undefined} node
 * @param {HTMLElement|null} root
 * @return {HTMLElement|null}
 */
function findHeadingAncestor( node, root ) {
	let el =
		node?.nodeType === 1
			? /** @type {HTMLElement} */ ( node )
			: node?.parentElement || null;
	while ( el && el !== root ) {
		if ( HEADING_TAGS.has( String( el.tagName || '' ).toLowerCase() ) ) {
			return el;
		}
		el = el.parentElement;
	}
	return null;
}

/**
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {string} '' | 'h1'…'h6'
 */
function readSelectionHeading( target, preferredRange = null ) {
	if ( ! target ) {
		return '';
	}
	const range = preferredRange || saveSelection( target );
	if ( ! range || ! target.contains( range.commonAncestorContainer ) ) {
		return '';
	}
	const heading = findHeadingAncestor(
		range.commonAncestorContainer,
		target
	);
	return heading ? String( heading.tagName ).toLowerCase() : '';
}

/**
 * @param {HTMLElement} el
 */
function unwrapElementKeepChildren( el ) {
	const parent = el.parentNode;
	if ( ! parent ) {
		return;
	}
	while ( el.firstChild ) {
		parent.insertBefore( el.firstChild, el );
	}
	parent.removeChild( el );
	parent.normalize?.();
}

/**
 * Apply or clear an h1–h6 around the selection. Empty tag = NA (unwrap).
 *
 * @param {string} tag '' | 'h1'…'h6'
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {Range|null}
 */
function applyHeading( tag, target, preferredRange = null ) {
	if ( ! target ) {
		return preferredRange;
	}
	const clean = String( tag || '' )
		.trim()
		.toLowerCase();
	const nextTag = HEADING_TAGS.has( clean ) ? clean : '';
	const doc = ownerDoc( target );
	const range = preferredRange || saveSelection( target );
	if ( range ) {
		restoreSelection( range, target, { focus: true } );
	} else {
		target.focus( { preventScroll: true } );
	}

	const existing = findHeadingAncestor(
		( saveSelection( target ) || range )?.commonAncestorContainer,
		target
	);

	if ( ! nextTag ) {
		if ( existing && target.contains( existing ) ) {
			unwrapElementKeepChildren( existing );
		}
		return saveSelection( target ) || preferredRange;
	}

	if ( existing && target.contains( existing ) ) {
		if ( String( existing.tagName ).toLowerCase() === nextTag ) {
			return saveSelection( target ) || preferredRange;
		}
		const replacement = doc.createElement( nextTag );
		while ( existing.firstChild ) {
			replacement.appendChild( existing.firstChild );
		}
		existing.parentNode?.replaceChild( replacement, existing );
		try {
			const next = doc.createRange();
			next.selectNodeContents( replacement );
			restoreSelection( next, target, { focus: true } );
			return next;
		} catch ( e ) {
			return saveSelection( target ) || preferredRange;
		}
	}

	try {
		doc.execCommand( 'formatBlock', false, nextTag );
	} catch ( e ) {
		try {
			doc.execCommand( 'formatBlock', false, `<${ nextTag }>` );
		} catch ( e2 ) {
			// ignore
		}
	}

	// Browsers may insert <div>/<p>; rename to the requested heading.
	const after = findHeadingAncestor(
		( saveSelection( target ) || range )?.commonAncestorContainer,
		target
	);
	if ( after && String( after.tagName ).toLowerCase() === nextTag ) {
		return saveSelection( target ) || preferredRange;
	}

	let block = after;
	if ( ! block ) {
		let node =
			( saveSelection( target ) || range )?.commonAncestorContainer ||
			null;
		if ( node?.nodeType === 3 ) {
			node = node.parentElement;
		}
		while ( node && node !== target ) {
			const name = String(
				/** @type {Element} */ ( node ).tagName || ''
			).toLowerCase();
			if ( name === 'div' || name === 'p' ) {
				block = /** @type {HTMLElement} */ ( node );
				break;
			}
			node = node.parentElement;
		}
	}

	if ( block && target.contains( block ) ) {
		const replacement = doc.createElement( nextTag );
		while ( block.firstChild ) {
			replacement.appendChild( block.firstChild );
		}
		block.parentNode?.replaceChild( replacement, block );
		try {
			const next = doc.createRange();
			next.selectNodeContents( replacement );
			restoreSelection( next, target, { focus: true } );
			return next;
		} catch ( e ) {
			// fall through
		}
	}

	return saveSelection( target ) || preferredRange;
}

/**
 * Nearest `<a>` under the selection / caret inside target.
 *
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {HTMLAnchorElement|null}
 */
function findAnchorInSelection( target, preferredRange = null ) {
	if ( ! target ) {
		return null;
	}
	const range = preferredRange || saveSelection( target );
	if ( ! range ) {
		return null;
	}
	let node = range.commonAncestorContainer;
	if ( node.nodeType === 3 ) {
		node = node.parentElement;
	}
	while ( node && node !== target ) {
		if (
			node.nodeType === 1 &&
			/** @type {Element} */ ( node ).tagName === 'A'
		) {
			return /** @type {HTMLAnchorElement} */ ( node );
		}
		node = node.parentElement;
	}
	let start = range.startContainer;
	if ( start.nodeType === 3 ) {
		start = start.parentElement;
	}
	while ( start && start !== target ) {
		if (
			start.nodeType === 1 &&
			/** @type {Element} */ ( start ).tagName === 'A'
		) {
			return /** @type {HTMLAnchorElement} */ ( start );
		}
		start = start.parentElement;
	}
	return null;
}

/**
 * Link values for the current selection.
 *
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {{ title: string, url: string, opensInNewTab: boolean }}
 */
function readSelectionLink( target, preferredRange = null ) {
	const anchor = findAnchorInSelection( target, preferredRange );
	const range = preferredRange || saveSelection( target );
	if ( anchor ) {
		return normalizeLink( {
			title: String( anchor.textContent || '' ),
			url: String( anchor.getAttribute( 'href' ) || '' ),
			opensInNewTab: anchor.getAttribute( 'target' ) === '_blank',
		} );
	}
	let title = '';
	try {
		title = range ? String( range.toString() || '' ) : '';
	} catch ( e ) {
		title = '';
	}
	return normalizeLink( { title, url: '', opensInNewTab: false } );
}

/**
 * Apply / update an inline link from our custom editor values.
 *
 * @param {{ title?: string, url?: string, opensInNewTab?: boolean }} link
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {Range|null}
 */
function applyInlineLink( link, target, preferredRange = null ) {
	if ( ! target ) {
		return preferredRange;
	}
	const doc = ownerDoc( target );
	const clean = normalizeLink( link || {} );
	const href = sanitizeHref( clean.url );
	if ( ! href ) {
		return preferredRange;
	}

	restoreSelection( preferredRange, target, { focus: true } );
	let range = preferredRange || saveSelection( target );
	if ( ! range ) {
		return null;
	}

	const existing = findAnchorInSelection( target, range );
	if ( existing ) {
		existing.setAttribute( 'href', href );
		if ( clean.opensInNewTab ) {
			existing.setAttribute( 'target', '_blank' );
			existing.setAttribute( 'rel', 'noopener noreferrer' );
		} else {
			existing.removeAttribute( 'target' );
			existing.removeAttribute( 'rel' );
		}
		const nextLabel = String( clean.title || '' ).trim();
		const prevLabel = String( existing.textContent || '' ).trim();
		if ( nextLabel && nextLabel !== prevLabel ) {
			existing.textContent = nextLabel;
		}
		try {
			const next = doc.createRange();
			next.selectNodeContents( existing );
			restoreSelection( next, target, { focus: true } );
			return next;
		} catch ( e ) {
			return saveSelection( target );
		}
	}

	const a = doc.createElement( 'a' );
	a.setAttribute( 'href', href );
	if ( clean.opensInNewTab ) {
		a.setAttribute( 'target', '_blank' );
		a.setAttribute( 'rel', 'noopener noreferrer' );
	}

	const selectedText = String( range.toString() || '' );
	const label = String( clean.title || '' ).trim() || selectedText;

	if ( range.collapsed ) {
		if ( ! label ) {
			return range;
		}
		a.textContent = label;
		range.insertNode( a );
	} else {
		try {
			range.surroundContents( a );
		} catch ( e ) {
			const frag = range.extractContents();
			a.appendChild( frag );
			range.insertNode( a );
		}
		if ( label && label !== String( a.textContent || '' ).trim() ) {
			a.textContent = label;
		}
	}

	try {
		const next = doc.createRange();
		next.selectNodeContents( a );
		restoreSelection( next, target, { focus: true } );
		return next;
	} catch ( e ) {
		return saveSelection( target );
	}
}

/**
 * Unwrap the anchor under the selection.
 *
 * @param {HTMLElement|null} target
 * @param {Range|null} [preferredRange]
 * @return {Range|null}
 */
function removeInlineLink( target, preferredRange = null ) {
	if ( ! target ) {
		return preferredRange;
	}
	restoreSelection( preferredRange, target, { focus: true } );
	const range = preferredRange || saveSelection( target );
	const anchor = findAnchorInSelection( target, range );
	if ( ! anchor || ! anchor.parentNode ) {
		return range;
	}
	const parent = anchor.parentNode;
	while ( anchor.firstChild ) {
		parent.insertBefore( anchor.firstChild, anchor );
	}
	parent.removeChild( anchor );
	parent.normalize?.();
	return saveSelection( target );
}

/**
 * @param {object}   props
 * @param {string[]|null} props.commands  From toolbarCommandsFromField (null = full)
 * @param {() => (HTMLElement|null)} props.getTarget
 * @param {Function} [props.onChange] Called after a format is applied
 * @param {Function} [props.onPointerDown] Keep preview edit session alive (iframe blur)
 * @param {Function} [props.onBlurLock] (locked: boolean) — hold preview blur while popovers open
 * @param {string}   [props.className]
 */
/**
 * Expand a collapsed caret to the current visual line (for list formatting).
 *
 * @param {HTMLElement|null} target
 * @param {Range|null} caret
 * @return {Range|null}
 */
function rangeForCurrentLine( target, caret ) {
	if ( ! target || ! caret ) {
		return caret;
	}
	const win = ownerWin( target );
	const sel = win.getSelection?.();
	if ( ! sel?.modify ) {
		return caret;
	}
	try {
		restoreSelection( caret, target, { focus: true } );
		sel.modify( 'move', 'backward', 'lineboundary' );
		sel.modify( 'extend', 'forward', 'lineboundary' );
		if ( ! sel.rangeCount ) {
			return caret;
		}
		const line = sel.getRangeAt( 0 ).cloneRange();
		if ( ! target.contains( line.commonAncestorContainer ) ) {
			restoreSelection( caret, target, { focus: true } );
			return caret;
		}
		return line;
	} catch ( e ) {
		try {
			restoreSelection( caret, target, { focus: true } );
		} catch ( e2 ) {
			// ignore
		}
		return caret;
	}
}

/**
 * Apply ul/ol at the caret or selection without reformatting unrelated lines.
 *
 * @param {HTMLElement|null} target
 * @param {Range|null} preferredRange
 * @param {string} exec insertUnorderedList | insertOrderedList
 * @return {Range|null}
 */
function applyListCommand( target, preferredRange, exec ) {
	if ( ! target ) {
		return preferredRange;
	}
	const doc = ownerDoc( target );
	let range = preferredRange || saveSelection( target );
	if ( range ) {
		restoreSelection( range, target, { focus: true } );
	} else {
		target.focus( { preventScroll: true } );
		range = saveSelection( target );
	}

	if ( range?.collapsed ) {
		const line = rangeForCurrentLine( target, range );
		const lineText = String( line?.toString?.() || '' )
			.replace( /\u200b/g, '' )
			.trim();
		if ( ! lineText ) {
			// Empty line: insert a fresh list item at the caret only.
			try {
				restoreSelection( range, target, { focus: true } );
				const list = doc.createElement(
					exec === 'insertOrderedList' ? 'ol' : 'ul'
				);
				const li = doc.createElement( 'li' );
				li.appendChild( doc.createElement( 'br' ) );
				list.appendChild( li );
				range.insertNode( list );
				const next = doc.createRange();
				next.setStart( li, 0 );
				next.collapse( true );
				restoreSelection( next, target, { focus: true } );
				return next;
			} catch ( e ) {
				// Fall through to execCommand.
			}
		} else if ( line && ! line.collapsed ) {
			restoreSelection( line, target, { focus: true } );
		}
	}

	try {
		doc.execCommand( exec, false, null );
	} catch ( e ) {
		// ignore
	}
	return saveSelection( target ) || preferredRange;
}

export default function FormatToolbar( {
	commands = null,
	getTarget,
	onChange,
	onPointerDown,
	onBlurLock,
	className = '',
} ) {
	const { fontFamilyOptions, fontSizes } = useThemeTokens();

	const fontFamilySelectOptions = useMemo(
		() => [
			{
				label: __( 'Font family', 'joc-blockora-morph' ),
				value: '',
			},
			...( fontFamilyOptions || [] ).map( ( opt ) => ( {
				label: opt.label,
				value: opt.value,
			} ) ),
		],
		[ fontFamilyOptions ]
	);
	const headingSelectOptions = useMemo(
		() => [
			{ label: 'NA', value: '' },
			{ label: 'H1', value: 'h1' },
			{ label: 'H2', value: 'h2' },
			{ label: 'H3', value: 'h3' },
			{ label: 'H4', value: 'h4' },
			{ label: 'H5', value: 'h5' },
			{ label: 'H6', value: 'h6' },
		],
		[]
	);
	const fontSizeSelectOptions = useMemo(
		() => [
			{
				label: __( 'Font size', 'joc-blockora-morph' ),
				value: '',
			},
			...( fontSizes || [] ).map( ( fs ) => ( {
				label: fs.name
					? `${ fs.name } (${ fs.size })`
					: String( fs.size || '' ),
				value: String( fs.size || '' ),
			} ) ),
		],
		[ fontSizes ]
	);
	const fontWeightSelectOptions = useMemo(
		() => [
			{
				label: __( 'Font weight', 'joc-blockora-morph' ),
				value: '',
			},
			...INLINE_FONT_WEIGHT_OPTIONS,
		],
		[]
	);
	const [ active, setActive ] = useState( {} );
	const [ colorValue, setColorValue ] = useState( '' );
	const [ colorOpen, setColorOpen ] = useState( false );
	const [ fontsOpen, setFontsOpen ] = useState( false );
	const [ fontFamilyValue, setFontFamilyValue ] = useState( '' );
	const [ headingValue, setHeadingValue ] = useState( '' );
	const [ fontSizeValue, setFontSizeValue ] = useState( '' );
	const [ fontWeightValue, setFontWeightValue ] = useState( '' );
	const [ lineHeightValue, setLineHeightValue ] = useState( '' );
	const [ lineHeightDraft, setLineHeightDraft ] = useState( '' );
	const [ letterSpacingValue, setLetterSpacingValue ] = useState( '' );
	const [ letterSpacingDraft, setLetterSpacingDraft ] = useState( '' );
	const [ linkValue, setLinkValue ] = useState( () =>
		normalizeLink( {} )
	);
	const [ linkOpen, setLinkOpen ] = useState( false );
	const [ pickerKey, setPickerKey ] = useState( 0 );
	const toolbarRef = useRef( null );
	const savedRangeRef = useRef( null );
	/** Span created for the current color-picker session (live hue/opacity). */
	const colorWrapRef = useRef( null );
	/** True while pointer is down inside color UI (ColorPicker is non-focusable). */
	const colorUiPointerRef = useRef( false );
	/** Skip ColorPicker onChange right after an explicit clear. */
	const ignorePickerChangeRef = useRef( false );
	/** After clear, don't re-seed swatch from selection while picker stays open. */
	const colorClearedRef = useRef( false );
	/** Skip selectionchange while we remount the persist mark. */
	const retargetingRef = useRef( false );
	const boundTargetRef = useRef( null );

	const closeColorPicker = useCallback( () => {
		const target = boundTargetRef.current || getTarget?.() || null;
		endSelectionPersist( colorWrapRef, { unwrapIfPlain: true } );
		clearPersistMarksInRoot( target );
		setColorOpen( false );
		onBlurLock?.( false );
		onChange?.();
	}, [ getTarget, onBlurLock, onChange ] );

	// Drop leftover picker marks if this toolbar unmounts.
	useEffect( () => {
		return () => {
			clearPersistMarksInRoot( boundTargetRef.current );
			endSelectionPersist( colorWrapRef, { unwrapIfPlain: true } );
		};
	}, [] );

	const visibleButtons = useMemo(
		() =>
			FORMAT_BUTTONS.filter( ( btn ) => {
				// Bold is the font-weight dropdown (300–900), not a B toggle.
				if ( btn.cmd === 'bold' ) {
					return false;
				}
				return toolbarHasCommand( commands, btn.cmd );
			} ),
		[ commands ]
	);
	const showColor = toolbarHasCommand( commands, 'color' );
	const showHeading = toolbarHasCommand( commands, 'heading' );
	const showFont =
		toolbarHasCommand( commands, 'fontfamily' ) ||
		toolbarHasCommand( commands, 'font' );
	const showFontSize = toolbarHasCommand( commands, 'fontsize' );
	const showFontWeight =
		toolbarHasCommand( commands, 'fontweight' ) ||
		toolbarHasCommand( commands, 'bold' );
	const showFonts =
		showHeading || showFont || showFontSize || showFontWeight;
	const showLink = toolbarHasCommand( commands, 'link' );
	const showClear = toolbarHasCommand( commands, 'removeformat' );

	useEffect( () => {
		if ( ! colorOpen && ! linkOpen && ! fontsOpen ) {
			onBlurLock?.( false );
			return undefined;
		}
		onBlurLock?.( true );
		onPointerDown?.();
		return () => onBlurLock?.( false );
	}, [ colorOpen, linkOpen, fontsOpen, onBlurLock, onPointerDown ] );

	// Close + clear highlights when the bound editor target changes.
	useEffect( () => {
		const target = getTarget?.() || null;
		const prev = boundTargetRef.current;
		if ( prev && target && prev !== target ) {
			clearPersistMarksInRoot( prev );
			endSelectionPersist( colorWrapRef, { unwrapIfPlain: true } );
			setColorOpen( false );
			setLinkOpen( false );
			setFontsOpen( false );
			onBlurLock?.( false );
			onChange?.();
		}
		boundTargetRef.current = target;
	}, [ getTarget, onBlurLock, onChange ] );

	// Close when clicking outside the color UI (incl. other toolbar buttons).
	useEffect( () => {
		if ( ! colorOpen ) {
			colorUiPointerRef.current = false;
			return undefined;
		}
		const target = getTarget?.() || null;
		const docs = new Set( [ document ] );
		if ( target?.ownerDocument ) {
			docs.add( target.ownerDocument );
		}

		const onPointerDownCapture = ( event ) => {
			if ( eventInsideSelector( event, COLOR_UI_SELECTOR ) ) {
				colorUiPointerRef.current = true;
				onPointerDown?.( event );
				return;
			}
			colorUiPointerRef.current = false;
			// Clicks on the rest of the format bar (bold, fonts, …) close color.
			clearPersistMarksInRoot( target );
			closeColorPicker();
		};

		const onPointerUp = () => {
			// Keep the flag through the same-tick focus-outside that follows
			// ColorPicker pointer interaction (controls are often non-focusable).
			window.setTimeout( () => {
				colorUiPointerRef.current = false;
			}, 300 );
		};

		docs.forEach( ( doc ) => {
			doc.addEventListener( 'pointerdown', onPointerDownCapture, true );
		} );
		document.addEventListener( 'pointerup', onPointerUp, true );
		document.addEventListener( 'pointercancel', onPointerUp, true );
		return () => {
			docs.forEach( ( doc ) => {
				doc.removeEventListener(
					'pointerdown',
					onPointerDownCapture,
					true
				);
			} );
			document.removeEventListener( 'pointerup', onPointerUp, true );
			document.removeEventListener( 'pointercancel', onPointerUp, true );
		};
	}, [ colorOpen, getTarget, closeColorPicker, onPointerDown ] );

	const refreshActive = useCallback( () => {
		const target = getTarget?.() || null;
		const doc = ownerDoc( target );
		const next = {};
		FORMAT_BUTTONS.forEach( ( btn ) => {
			try {
				next[ btn.cmd ] = doc.queryCommandState( btn.exec );
			} catch ( e ) {
				next[ btn.cmd ] = false;
			}
		} );
		next.link = Boolean(
			findAnchorInSelection( target, saveSelection( target ) )
		);
		setActive( next );
		// Sync swatch / picker to the color under the caret (not while picking).
		if ( ! colorOpen && ! colorClearedRef.current ) {
			setColorValue( readSelectionCssColor( target ) );
		}
		setFontWeightValue( readSelectionFontWeight( target ) );
		setHeadingValue( readSelectionHeading( target ) );
		setFontFamilyValue( readSelectionFontFamily( target ) );
		setFontSizeValue( readSelectionFontSize( target ) );
		setLineHeightValue( readSelectionLineHeight( target ) );
		setLineHeightDraft( readSelectionLineHeight( target ) );
		setLetterSpacingValue( readSelectionLetterSpacing( target ) );
		setLetterSpacingDraft( readSelectionLetterSpacing( target ) );
		if ( ! linkOpen ) {
			setLinkValue(
				readSelectionLink( target, savedRangeRef.current )
			);
		}
	}, [ getTarget, colorOpen, linkOpen ] );

	useEffect( () => {
		const target = getTarget?.() || null;
		const doc = ownerDoc( target );
		const onSel = () => {
			if ( retargetingRef.current ) {
				return;
			}
			refreshActive();
			const live = saveSelection( target );
			if ( ! live ) {
				return;
			}

			// Always track caret + highlight so toolbar actions hit the
			// current position (never a stale prior selection).
			storeSavedRange( savedRangeRef, live );

			if ( live.collapsed ) {
				return;
			}

			// Color picker open: if the user selects a different word, move the mark.
			if ( colorOpen ) {
				const wrap = colorWrapRef.current;
				if ( rangeInsideWrap( wrap, live ) ) {
					return;
				}
				retargetingRef.current = true;
				try {
					endSelectionPersist( colorWrapRef, {
						unwrapIfPlain: true,
					} );
					colorWrapRef.current = null;
					storeSavedRange( savedRangeRef, live );
					const next = persistSelection(
						target,
						savedRangeRef.current,
						colorWrapRef,
						{ force: true }
					);
					if ( next ) {
						storeSavedRange( savedRangeRef, next );
					}
					if ( ! colorClearedRef.current ) {
						setColorValue( readSelectionCssColor( target ) );
					}
				} finally {
					window.requestAnimationFrame( () => {
						retargetingRef.current = false;
					} );
				}
			}
		};
		doc.addEventListener( 'selectionchange', onSel );
		return () => doc.removeEventListener( 'selectionchange', onSel );
	}, [ getTarget, refreshActive, colorOpen ] );

	const snapshotSelection = useCallback( () => {
		const target = getTarget?.() || null;
		const live = saveSelection( target );
		if ( live ) {
			storeSavedRange( savedRangeRef, live );
		}
	}, [ getTarget ] );

	const lockSelectionHighlight = useCallback( () => {
		const target = getTarget?.() || null;
		snapshotSelection();
		const existing = colorWrapRef.current;
		if (
			existing &&
			target?.contains( existing ) &&
			rangeInsideWrap( existing, savedRangeRef.current )
		) {
			existing.style.backgroundColor = SEL_BG;
			existing.setAttribute( SEL_ATTR, '1' );
			const next = rangeForWrap( existing );
			if ( next ) {
				savedRangeRef.current = next;
				restoreSelection( next, target, { focus: false } );
			}
			return;
		}
		const next = persistSelection(
			target,
			savedRangeRef.current,
			colorWrapRef,
			{ force: true }
		);
		if ( next ) {
			savedRangeRef.current = next;
		}
	}, [ getTarget, snapshotSelection ] );

	const guardPointer = useCallback(
		( event ) => {
			onPointerDown?.( event );
			// Snapshot before preventDefault / focus change.
			snapshotSelection();
			if (
				nodeInsideSelector(
					event?.target,
					'.components-color-picker, .react-colorful, input, textarea, select, .bm-styled-select__search'
				)
			) {
				return;
			}
			if (
				nodeInsideSelector(
					event?.target,
					'button, .components-button, .components-circular-option-picker__option, .components-dropdown, .bm-format-toolbar__btn, .bm-styled-select__toggle, .bm-styled-select__option, .bm-styled-select__list--portal, .bm-searchable-select'
				)
			) {
				event.preventDefault();
			}
		},
		[ onPointerDown, snapshotSelection ]
	);

	/** Color panel: keep session alive; selection is already persisted as a mark. */
	const onColorPanelPointerDown = useCallback(
		( event ) => {
			colorUiPointerRef.current = true;
			onPointerDown?.( event );
		},
		[ onPointerDown ]
	);

	const onColorToggle = useCallback(
		( nextOpen ) => {
			if ( nextOpen ) {
				const target = getTarget?.() || null;
				colorClearedRef.current = false;
				setFontsOpen( false );
				setLinkOpen( false );
				const fromSel = readSelectionCssColor( target );
				if ( fromSel ) {
					setColorValue( fromSel );
				}
				// mousedown already locks; only lock here if wrap is missing
				// (e.g. keyboard open).
				if (
					! colorWrapRef.current ||
					! target?.contains( colorWrapRef.current )
				) {
					lockSelectionHighlight();
				}
				onPointerDown?.();
				onBlurLock?.( true );
			} else {
				endSelectionPersist( colorWrapRef, { unwrapIfPlain: true } );
				colorClearedRef.current = false;
				onChange?.();
				window.requestAnimationFrame( () => {
					setColorValue(
						readSelectionCssColor( getTarget?.() || null )
					);
				} );
			}
			setColorOpen( Boolean( nextOpen ) );
		},
		[
			getTarget,
			lockSelectionHighlight,
			onPointerDown,
			onBlurLock,
			onChange,
		]
	);

	const onColorFocusOutside = useCallback( ( event ) => {
		// ColorPicker / swatches are largely non-focusable — don't close just
		// because document.activeElement never moved into the panel.
		if ( colorUiPointerRef.current ) {
			return;
		}
		if ( eventInsideSelector( event, COLOR_UI_SELECTOR ) ) {
			return;
		}
		if ( nodeInsideSelector( document.activeElement, COLOR_UI_SELECTOR ) ) {
			return;
		}
		if (
			document.querySelector(
				'.components-color-palette__custom-color-dropdown-content'
			)
		) {
			return;
		}
		closeColorPicker();
	}, [ closeColorPicker ] );

	const runAfter = useCallback( () => {
		refreshActive();
		onChange?.();
	}, [ onChange, refreshActive ] );

	const onFormat = useCallback(
		( exec ) => {
			const target = getTarget?.() || null;
			// Prefer live caret/selection; fall back to the snapshotted range
			// (toolbar mousedown can clear the live Selection across iframes).
			const live = saveSelection( target );
			const range =
				( live &&
				target?.contains?.( live.commonAncestorContainer )
					? live
					: null ) ||
				savedRangeRef.current ||
				null;
			if ( range ) {
				restoreSelection( range, target, { focus: true } );
				storeSavedRange( savedRangeRef, range );
			}
			try {
				if (
					exec === 'insertUnorderedList' ||
					exec === 'insertOrderedList'
				) {
					const after = applyListCommand( target, range, exec );
					if ( after ) {
						storeSavedRange( savedRangeRef, after );
					}
				} else {
					ownerDoc( target ).execCommand( exec, false, null );
					const after = saveSelection( target );
					if ( after ) {
						storeSavedRange( savedRangeRef, after );
					} else if ( range ) {
						storeSavedRange( savedRangeRef, range );
					}
				}
			} catch ( e ) {
				// ignore
			}
			runAfter();
			// Parent sanitize must not leave the caret at the start.
			window.requestAnimationFrame( () => {
				const keep =
					saveSelection( target ) || savedRangeRef.current;
				if ( keep ) {
					restoreSelection( keep, target, { focus: true } );
					const next = saveSelection( target );
					if ( next ) {
						storeSavedRange( savedRangeRef, next );
					}
				}
				refreshActive();
			} );
		},
		[ getTarget, runAfter, refreshActive ]
	);

	const applyColorClear = useCallback( () => {
		const target = getTarget?.() || null;
		const range = savedRangeRef.current || saveSelection( target );
		ignorePickerChangeRef.current = true;
		colorClearedRef.current = true;
		setColorValue( '' );
		setPickerKey( ( n ) => n + 1 );
		const nextRange = clearForeColor( target, range, colorWrapRef );
		if ( nextRange ) {
			savedRangeRef.current = nextRange.cloneRange();
			restoreSelection( nextRange, target, { focus: false } );
		}
		window.setTimeout( () => {
			ignorePickerChangeRef.current = false;
		}, 0 );
	}, [ getTarget ] );

	const onColor = useCallback(
		( next ) => {
			// Only Clear / empty — never treat alpha 0 (`transparent` / rgba(...,0)) as clear.
			if ( next == null || String( next ).trim() === '' ) {
				applyColorClear();
				return;
			}
			if ( ignorePickerChangeRef.current ) {
				return;
			}
			colorClearedRef.current = false;
			const target = getTarget?.() || null;
			const range = savedRangeRef.current || saveSelection( target );
			setColorValue( String( next ) );
			const nextRange = applyForeColor(
				next,
				target,
				range,
				colorWrapRef
			);
			if ( nextRange ) {
				savedRangeRef.current = nextRange.cloneRange();
			}
			// Don't sanitize while the picker is open — it would strip the
			// persisted selection mark. Commit on close instead.
		},
		[ getTarget, applyColorClear ]
	);

	const onClear = useCallback( () => {
		const target = getTarget?.() || null;
		const range = savedRangeRef.current || saveSelection( target );
		if ( range ) {
			restoreSelection( range, target, { focus: true } );
		}
		try {
			ownerDoc( target ).execCommand( 'removeFormat', false, null );
		} catch ( e ) {
			// ignore
		}
		setColorValue( '' );
		endSelectionPersist( colorWrapRef, { unwrapIfPlain: true } );
		colorWrapRef.current = null;
		const after = saveSelection( target );
		savedRangeRef.current = after || range;
		runAfter();
		window.requestAnimationFrame( () => {
			const keep = saveSelection( target ) || savedRangeRef.current;
			if ( keep ) {
				restoreSelection( keep, target, { focus: true } );
			}
		} );
	}, [ getTarget, runAfter ] );

	const onFont = useCallback(
		( next ) => {
			const target = getTarget?.() || null;
			const range = savedRangeRef.current || saveSelection( target );
			setFontFamilyValue( next || '' );
			if ( next ) {
				const after = applyFontFamily( next, target, range );
				if ( after ) {
					savedRangeRef.current = after;
					restoreSelection( after, target, { focus: true } );
				}
			} else if ( range ) {
				restoreSelection( range, target, { focus: true } );
				savedRangeRef.current = range;
			}
			runAfter();
			window.requestAnimationFrame( () => {
				const keep =
					savedRangeRef.current || saveSelection( target );
				if ( keep ) {
					restoreSelection( keep, target, { focus: true } );
					const live = saveSelection( target );
					if ( live ) {
						storeSavedRange( savedRangeRef, live );
					}
				}
			} );
		},
		[ getTarget, runAfter ]
	);

	const onHeading = useCallback(
		( next ) => {
			const target = getTarget?.() || null;
			const range = savedRangeRef.current || saveSelection( target );
			const tag = String( next || '' ).toLowerCase();
			setHeadingValue( HEADING_TAGS.has( tag ) ? tag : '' );
			const after = applyHeading( tag, target, range );
			if ( after ) {
				savedRangeRef.current = after;
				restoreSelection( after, target, { focus: true } );
			}
			runAfter();
			window.requestAnimationFrame( () => {
				const keep =
					savedRangeRef.current || saveSelection( target );
				if ( keep ) {
					restoreSelection( keep, target, { focus: true } );
					setHeadingValue( readSelectionHeading( target, keep ) );
					const live = saveSelection( target );
					if ( live ) {
						storeSavedRange( savedRangeRef, live );
					}
				}
			} );
		},
		[ getTarget, runAfter ]
	);

	const onFontSize = useCallback(
		( next ) => {
			const target = getTarget?.() || null;
			const range = savedRangeRef.current || saveSelection( target );
			setFontSizeValue( next || '' );
			if ( next ) {
				const after = applyFontSize( next, target, range );
				if ( after ) {
					savedRangeRef.current = after;
					restoreSelection( after, target, { focus: true } );
				}
			} else if ( range ) {
				restoreSelection( range, target, { focus: true } );
				savedRangeRef.current = range;
			}
			runAfter();
			window.requestAnimationFrame( () => {
				const keep =
					savedRangeRef.current || saveSelection( target );
				if ( keep ) {
					restoreSelection( keep, target, { focus: true } );
					const live = saveSelection( target );
					if ( live ) {
						storeSavedRange( savedRangeRef, live );
					}
				}
			} );
		},
		[ getTarget, runAfter ]
	);

	const onFontWeight = useCallback(
		( next ) => {
			const target = getTarget?.() || null;
			const range = savedRangeRef.current || saveSelection( target );
			const weight = normalizeFontWeightValue( next || '' );
			setFontWeightValue( weight );
			if ( weight ) {
				const after = applyFontWeight( weight, target, range );
				if ( after ) {
					savedRangeRef.current = after;
					restoreSelection( after, target, { focus: true } );
				}
			} else if ( range ) {
				restoreSelection( range, target, { focus: true } );
				savedRangeRef.current = range;
			}
			runAfter();
			window.requestAnimationFrame( () => {
				const keep =
					savedRangeRef.current || saveSelection( target );
				if ( keep ) {
					restoreSelection( keep, target, { focus: true } );
					const live = saveSelection( target );
					if ( live ) {
						storeSavedRange( savedRangeRef, live );
					}
				}
			} );
		},
		[ getTarget, runAfter ]
	);

	const onLineHeight = useCallback(
		( next ) => {
			const target = getTarget?.() || null;
			const range = savedRangeRef.current || saveSelection( target );
			const clean = normalizeLineHeightValue( next );
			setLineHeightValue( clean );
			setLineHeightDraft( clean );
			const after = applyLineHeight( next, target, range );
			if ( after ) {
				savedRangeRef.current = after;
				restoreSelection( after, target, { focus: true } );
			}
			runAfter();
			window.requestAnimationFrame( () => {
				const keep =
					savedRangeRef.current || saveSelection( target );
				if ( keep ) {
					restoreSelection( keep, target, { focus: true } );
					setLineHeightDraft( readSelectionLineHeight( target ) );
					setLineHeightValue( readSelectionLineHeight( target ) );
					const live = saveSelection( target );
					if ( live ) {
						storeSavedRange( savedRangeRef, live );
					}
				}
			} );
		},
		[ getTarget, runAfter ]
	);

	const onLetterSpacing = useCallback(
		( next ) => {
			const target = getTarget?.() || null;
			const range = savedRangeRef.current || saveSelection( target );
			const raw = String( next ?? '' ).trim();
			const clean =
				raw === '0' ? '0' : normalizeLetterSpacingValue( next );
			setLetterSpacingValue( clean );
			setLetterSpacingDraft( clean );
			if ( clean ) {
				const after = applyLetterSpacing( clean, target, range );
				if ( after ) {
					savedRangeRef.current = after;
					restoreSelection( after, target, { focus: true } );
				}
			} else if ( range ) {
				restoreSelection( range, target, { focus: true } );
				const after = applyTextStyles(
					target,
					range,
					{ letterSpacing: '' },
					null
				);
				if ( after ) {
					savedRangeRef.current = after;
				}
			}
			runAfter();
			window.requestAnimationFrame( () => {
				const keep =
					savedRangeRef.current || saveSelection( target );
				if ( keep ) {
					restoreSelection( keep, target, { focus: true } );
					const live = saveSelection( target );
					if ( live ) {
						storeSavedRange( savedRangeRef, live );
					}
				}
			} );
		},
		[ getTarget, runAfter ]
	);

	const closeFontsPanel = useCallback( () => {
		setFontsOpen( false );
		onBlurLock?.( false );
		onChange?.();
	}, [ onBlurLock, onChange ] );

	const onFontsFocusOutside = useCallback( ( event ) => {
		// Nested SearchableSelect portals to document.body — never let Popover
		// treat those (or the Fonts panel itself) as an outside click.
		event.preventDefault?.();
	}, [] );

	const onFontsToggle = useCallback(
		( nextOpen ) => {
			if ( nextOpen ) {
				snapshotSelection();
				setColorOpen( false );
				setLinkOpen( false );
				onBlurLock?.( true );
				onPointerDown?.();
				setFontsOpen( true );
				return;
			}
			// Ignore spurious closes while a nested select menu is open / opening.
			if ( document.querySelector( '.bm-format-toolbar__select-menu' ) ) {
				return;
			}
			closeFontsPanel();
		},
		[
			snapshotSelection,
			onBlurLock,
			onChange,
			onPointerDown,
			closeFontsPanel,
		]
	);

	// Close Fonts only on true outside pointerdowns (not nested select portals).
	useEffect( () => {
		if ( ! fontsOpen ) {
			return undefined;
		}
		const onPointerDownCapture = ( event ) => {
			if ( eventInsideSelector( event, FONTS_UI_SELECTOR ) ) {
				return;
			}
			if (
				event.target?.closest?.(
					'.bm-format-toolbar__btn--fonts, .bm-format-toolbar__fonts'
				)
			) {
				return;
			}
			closeFontsPanel();
		};
		document.addEventListener( 'pointerdown', onPointerDownCapture, true );
		return () => {
			document.removeEventListener(
				'pointerdown',
				onPointerDownCapture,
				true
			);
		};
	}, [ fontsOpen, closeFontsPanel ] );

	const closeLinkEditor = useCallback( () => {
		setLinkOpen( false );
		onBlurLock?.( false );
	}, [ onBlurLock ] );

	const onLinkToggle = useCallback(
		( nextOpen ) => {
			const target = getTarget?.() || null;
			if ( nextOpen ) {
				snapshotSelection();
				setLinkValue(
					readSelectionLink( target, savedRangeRef.current )
				);
				setLinkOpen( true );
				onBlurLock?.( true );
				onPointerDown?.();
			} else {
				closeLinkEditor();
			}
		},
		[
			getTarget,
			snapshotSelection,
			closeLinkEditor,
			onBlurLock,
			onPointerDown,
		]
	);

	const onLinkApply = useCallback(
		( next ) => {
			const target = getTarget?.() || null;
			const range = savedRangeRef.current || saveSelection( target );
			if ( ! sanitizeHref( next?.url ) ) {
				setLinkValue( normalizeLink( next ) );
				return;
			}
			const after = applyInlineLink( next, target, range );
			setLinkValue( normalizeLink( next ) );
			if ( after ) {
				savedRangeRef.current = after;
			}
			setActive( ( prev ) => ( { ...prev, link: true } ) );
			setLinkOpen( false );
			onBlurLock?.( false );
			runAfter();
		},
		[ getTarget, runAfter, onBlurLock ]
	);

	const onLinkRemove = useCallback( () => {
		const target = getTarget?.() || null;
		const range = savedRangeRef.current || saveSelection( target );
		const after = removeInlineLink( target, range );
		setLinkValue( normalizeLink( {} ) );
		if ( after ) {
			savedRangeRef.current = after;
		}
		setActive( ( prev ) => ( { ...prev, link: false } ) );
		setLinkOpen( false );
		onBlurLock?.( false );
		runAfter();
	}, [ getTarget, runAfter, onBlurLock ] );

	if (
		! visibleButtons.length &&
		! showColor &&
		! showFonts &&
		! showLink &&
		! showClear
	) {
		return null;
	}

	return (
		<div
			ref={ toolbarRef }
			className={ `bm-format-toolbar ${ className }`.trim() }
			role="toolbar"
			aria-label={ __( 'Text formatting', 'joc-blockora-morph' ) }
			onMouseDown={ guardPointer }
			onPointerDown={ guardPointer }
		>
			{ showFonts ? (
				<Dropdown
					className="bm-format-toolbar__fonts"
					contentClassName="bm-format-toolbar__fonts-popover"
					open={ fontsOpen }
					onToggle={ onFontsToggle }
					onClose={ closeFontsPanel }
					popoverProps={ {
						placement: 'bottom-start',
						offset: 8,
						focusOnMount: false,
						shift: true,
						onFocusOutside: onFontsFocusOutside,
					} }
					renderToggle={ ( { isOpen, onToggle } ) => (
						<button
							type="button"
							className={
								'bm-format-toolbar__btn bm-format-toolbar__btn--fonts' +
								( isOpen ? ' is-open' : '' )
							}
							aria-expanded={ isOpen }
							aria-label={ __( 'Fonts', 'joc-blockora-morph' ) }
							title={ __( 'Fonts', 'joc-blockora-morph' ) }
							onMouseDown={ ( event ) => {
								guardPointer( event );
								if ( ! isOpen ) {
									snapshotSelection();
								}
							} }
							onPointerDown={ ( event ) => {
								guardPointer( event );
								if ( ! isOpen ) {
									snapshotSelection();
								}
							} }
							onClick={ onToggle }
						>
							<span
								className="dashicons dashicons-editor-textcolor"
								aria-hidden="true"
							/>
							<span className="bm-format-toolbar__btn-label">
								{ __( 'Fonts', 'joc-blockora-morph' ) }
							</span>
						</button>
					) }
					renderContent={ () => (
						<DropdownContentWrapper paddingSize="medium">
							<div
								className="bm-format-toolbar__fonts-panel"
								onMouseDown={ ( event ) => {
									// Let nested selects receive clicks; only guard the editor selection.
									if (
										event.target?.closest?.(
											'.bm-styled-select__toggle, .bm-searchable-select, .bm-styled-select'
										)
									) {
										return;
									}
									guardPointer( event );
								} }
								onPointerDown={ ( event ) => {
									if (
										event.target?.closest?.(
											'.bm-styled-select__toggle, .bm-searchable-select, .bm-styled-select'
										)
									) {
										onPointerDown?.( event );
										return;
									}
									guardPointer( event );
									onPointerDown?.( event );
								} }
							>
								<p className="bm-format-toolbar__fonts-title">
									{ __( 'Fonts', 'joc-blockora-morph' ) }
								</p>
								{ showHeading ? (
									<div className="bm-format-toolbar__fonts-field">
										<SearchableSelect
											label={ __(
												'Heading',
												'joc-blockora-morph'
											) }
											value={ headingValue }
											options={ headingSelectOptions }
											onChange={ onHeading }
											searchableThreshold={ 99 }
											menuClassName="bm-format-toolbar__select-menu bm-format-toolbar__heading-menu"
											menuMinWidth={ 120 }
										/>
									</div>
								) : null }
								{ showFont ? (
									<div className="bm-format-toolbar__fonts-field">
										<SearchableSelect
											label={ __(
												'Font family',
												'joc-blockora-morph'
											) }
											value={ fontFamilyValue }
											options={ fontFamilySelectOptions }
											onChange={ onFont }
											searchableThreshold={ 16 }
											menuClassName="bm-format-toolbar__select-menu"
											previewFontFamily
										/>
									</div>
								) : null }
								{ showFontWeight ? (
									<div className="bm-format-toolbar__fonts-field">
										<SearchableSelect
											label={ __(
												'Font weight',
												'joc-blockora-morph'
											) }
											value={ fontWeightValue }
											options={ fontWeightSelectOptions }
											onChange={ onFontWeight }
											searchableThreshold={ 99 }
											menuClassName="bm-format-toolbar__select-menu"
										/>
									</div>
								) : null }
								{ showFontSize ? (
									<div className="bm-format-toolbar__fonts-field">
										<SearchableSelect
											label={ __(
												'Font size',
												'joc-blockora-morph'
											) }
											value={ fontSizeValue }
											options={ fontSizeSelectOptions }
											onChange={ onFontSize }
											searchableThreshold={ 16 }
											menuClassName="bm-format-toolbar__select-menu"
										/>
									</div>
								) : null }
								<div className="bm-format-toolbar__fonts-field">
									<TextControl
										label={ __(
											'Line height',
											'joc-blockora-morph'
										) }
										value={ lineHeightDraft }
										placeholder={ __(
											'e.g. 1.2, 24px, 1.5rem, 120%',
											'joc-blockora-morph'
										) }
										onChange={ setLineHeightDraft }
										onBlur={ () =>
											onLineHeight( lineHeightDraft )
										}
										onKeyDown={ ( event ) => {
											if ( event.key === 'Enter' ) {
												event.preventDefault();
												onLineHeight( lineHeightDraft );
											}
										} }
										__nextHasNoMarginBottom
										__next40pxDefaultSize
									/>
								</div>
								<div className="bm-format-toolbar__fonts-field">
									<TextControl
										label={ __(
											'Letter spacing',
											'joc-blockora-morph'
										) }
										value={ letterSpacingDraft }
										placeholder={ __(
											'e.g. 0.02em, 1px',
											'joc-blockora-morph'
										) }
										onChange={ setLetterSpacingDraft }
										onBlur={ () =>
											onLetterSpacing(
												letterSpacingDraft
											)
										}
										onKeyDown={ ( event ) => {
											if ( event.key === 'Enter' ) {
												event.preventDefault();
												onLetterSpacing(
													letterSpacingDraft
												);
											}
										} }
										__nextHasNoMarginBottom
										__next40pxDefaultSize
									/>
								</div>
							</div>
						</DropdownContentWrapper>
					) }
				/>
			) : null }

			{ visibleButtons.length ? (
				<div className="bm-format-toolbar__group" role="group">
					{ visibleButtons.map( ( btn ) => (
						<button
							key={ btn.cmd }
							type="button"
							className={
								'bm-format-toolbar__btn' +
								( active[ btn.cmd ] ? ' is-active' : '' )
							}
							aria-label={ btn.label }
							aria-pressed={ Boolean( active[ btn.cmd ] ) }
							title={ btn.label }
							onClick={ () => onFormat( btn.exec ) }
						>
							{ btn.glyph ? (
								<span
									className="bm-format-toolbar__glyph"
									aria-hidden="true"
								>
									{ btn.glyph }
								</span>
							) : (
								<span
									className={ `dashicons dashicons-${ btn.icon }` }
									aria-hidden="true"
								/>
							) }
						</button>
					) ) }
				</div>
			) : null }

			{ showLink ? (
				<Dropdown
					className="bm-format-toolbar__link"
					contentClassName="bm-format-toolbar__link-popover"
					open={ linkOpen }
					onToggle={ onLinkToggle }
					onClose={ closeLinkEditor }
					popoverProps={ {
						placement: 'bottom-start',
						offset: 8,
						focusOnMount: 'firstElement',
						shift: true,
					} }
					renderToggle={ ( { isOpen, onToggle } ) => (
						<button
							type="button"
							className={
								'bm-format-toolbar__btn' +
								( isOpen ? ' is-open' : '' ) +
								( active.link ? ' is-active' : '' )
							}
							aria-expanded={ isOpen }
							aria-label={ __( 'Link', 'joc-blockora-morph' ) }
							title={ __( 'Link', 'joc-blockora-morph' ) }
							onMouseDown={ guardPointer }
							onPointerDown={ guardPointer }
							onClick={ onToggle }
						>
							<span
								className="dashicons dashicons-admin-links"
								aria-hidden="true"
							/>
						</button>
					) }
					renderContent={ () => (
						<DropdownContentWrapper paddingSize="medium">
							<div
								className="bm-format-toolbar__link-panel"
								onPointerDown={ ( event ) =>
									onPointerDown?.( event )
								}
							>
								<LinkEditorForm
									compact
									value={ linkValue }
									showApply
									onChange={ ( next ) =>
										setLinkValue( normalizeLink( next ) )
									}
									onApply={ onLinkApply }
									onRemove={
										active.link || linkValue.url
											? onLinkRemove
											: undefined
									}
									onDone={ closeLinkEditor }
								/>
							</div>
						</DropdownContentWrapper>
					) }
				/>
			) : null }

			{ showColor ? (
				<Dropdown
					className="bm-format-toolbar__color"
					contentClassName="bm-format-toolbar__color-popover"
					open={ colorOpen }
					onToggle={ onColorToggle }
					onClose={ closeColorPicker }
					popoverProps={ {
						placement: 'bottom-start',
						offset: 8,
						focusOnMount: false,
						shift: true,
						// ColorPicker uses non-focusable pointer UI; default
						// closeIfFocusOutside would dismiss the whole panel.
						onFocusOutside: onColorFocusOutside,
					} }
					renderToggle={ ( { isOpen, onToggle } ) => (
						<button
							type="button"
							className={
								'bm-format-toolbar__btn bm-format-toolbar__btn--color' +
								( isOpen ? ' is-open' : '' ) +
								( colorValue ? ' is-active' : '' )
							}
							aria-expanded={ isOpen }
							aria-label={ __(
								'Text color',
								'joc-blockora-morph'
							) }
							title={ __( 'Text color', 'joc-blockora-morph' ) }
							onMouseDown={ ( event ) => {
								// Capture selection + preventDefault before the
								// dropdown steals focus / clears the range.
								guardPointer( event );
								if ( ! isOpen ) {
									lockSelectionHighlight();
								}
							} }
							onPointerDown={ ( event ) => {
								guardPointer( event );
								if ( ! isOpen ) {
									lockSelectionHighlight();
								}
							} }
							onClick={ onToggle }
						>
							<span
								className="dashicons dashicons-editor-textcolor"
								aria-hidden="true"
							/>
							<ColorIndicator
								className="bm-format-toolbar__swatch"
								colorValue={ colorValue || undefined }
							/>
						</button>
					) }
					renderContent={ () => (
						<DropdownContentWrapper paddingSize="medium">
							<div
								className="bm-format-toolbar__color-panel"
								onPointerDown={ onColorPanelPointerDown }
							>
								<ThemeColorPalette
									className="bm-format-toolbar__color-tabs"
									value={ colorValue || '' }
									onChange={ onColor }
									clearable={ false }
									enableAlpha
									disableCustomColors
									asSidebar={ false }
								/>
								<p className="bm-format-toolbar__color-label bm-format-toolbar__color-label--custom">
									{ __(
										'Custom color',
										'joc-blockora-morph'
									) }
								</p>
								<ColorPicker
									key={ `bm-color-picker-${ pickerKey }` }
									color={ colorValue || '#000000' }
									onChange={ onColor }
									enableAlpha
								/>
							</div>
						</DropdownContentWrapper>
					) }
				/>
			) : null }

			{ showClear ? (
				<button
					type="button"
					className="bm-format-toolbar__btn"
					aria-label={ __( 'Clear formatting', 'joc-blockora-morph' ) }
					title={ __( 'Clear formatting', 'joc-blockora-morph' ) }
					onClick={ onClear }
				>
					<span
						className="dashicons dashicons-editor-removeformatting"
						aria-hidden="true"
					/>
				</button>
			) : null }
		</div>
	);
}
