import { getI18n } from '../../core/i18n';
import type { FunctionalComponent } from 'preact';
import { useT } from '../../hooks/useT';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { AsukaIcon } from '../core/AsukaIcon';
import { IconButton } from '../core/IconButton';
import asukaDesignerMarkUrl from '../../assets/brand/portal/asuka-designer-mark.svg';

import type { LayoutZoneId } from '../../types/layout';
import type { ModuleId } from '../../types/moduleIds';

import { DEFAULT_PERMISSIONS } from '../../core/permissions';
import { canOpenModuleId } from '../../core/permissions';
import { getVanillaUiHelpers } from '../../core/moduleUi';

import { useAsukaStore } from '../../store/asukaStore';

import { ModuleHost } from '../modules/ModuleHost';
import { ModuleUnavailableState } from '../modules/ModuleUnavailableState';
import { ensureModuleAvailable, getModule, hasModule, isModuleCompatibleWithEngine, normalizeModuleRequiredCapabilities, type AsukaModuleContext, useModuleRegistryVersion } from '../../core/modules';
import { isModuleBlockedWithoutView, NO_VIEW_MODULE_TOOLTIP_KEY } from '../../core/moduleAvailability';

import { Carousel, type CarouselPaginationMode } from '../core/Carousel';
import { ScrollArea } from '../core/ScrollArea';
import { Tooltip } from '../core/Tooltip';
import { isStateRestoringNow } from '../../core/viewSnapshots';

import { SelectionToolbar } from './SelectionToolbar';
import { RulerOverlay } from './RulerOverlay';
import { setActivePrintingMethodId } from '../../core/printing';
import { defineDefaultListeners } from '../../core/listeners';
import { onAsukaEvent } from '../../core/events';
import { asukaHistoryDebug, getAsukaHistoryDebugBuild, isAsukaHistoryDebugEnabled, summarize3DScene, summarizeHistoryInfo, summarizeHistoryTrack } from '../../core/historyDebug';
import { areRenderEngineCapabilitiesSatisfied, emitRenderEngineEvent, getActiveViewEngineKind, getRenderEngineCapabilitiesForKind, getRenderEngineSelectionAnchorBox, getRuntimeRenderEngine, type Asuka3DSceneState, type AsukaCanvas2DInteractionEvent, type AsukaObjectSummary, type AsukaPlacementStatus, type AsukaRenderEngine, type AsukaRenderEngineKind } from '../../core/renderEngine';
import { create3DSceneState } from '../../core/scene3d';
import { beginAsuka3DPerfSpan, logAsuka3DPerf } from '../../core/perf3dDebug';
import { getProduct3DCanvasSelectionInstanceId, resolveProduct3DCanvasSelectionInstance } from '../../core/product3dIdentity';
import { resolveCanvasFrameSize } from '../../core/canvasFrameSize';
import {
  createLiveModuleRenderEngine,
  getRenderEngineOptionsForKind,
  loadRenderEngineFromOptions,
  mountBuiltInFabricRenderEngine,
  registerMountedRuntimeEngine,
} from '../../core/engineManager';

const DOCUMENT_FABRIC_HISTORY_KEY = '__document__';
const DEFAULT_HISTORY_MAX_SNAPSHOTS = 8;

interface AsukaRootProps {
  className?: string;
  width?: string | null;
  height?: string | null;
  maxWidth?: string | null;
  maxHeight?: string | null;
}

const dndLog = (..._args: any[]) => {};
const DEFAULT_MENU_ICON = 'views';

const MOBILE_NAV_DEFAULT_PRIORITY: Record<string, number> = {
  views: 1,
  image: 2,
  designs: 3,
  text: 4,
  elements: 5,
  layers: 6,
  download: 7,
  studio: 8,
  drawingAreas: 9,
  marker: 10,
  pricingRules: 11,
  modelOptimizer: 12,
  colors: 12,
  history: 13,
  license: 90,
};

const getMobileNavPriority = (block: any): number => {
  const explicit = Number(block?.priority);
  if (Number.isFinite(explicit)) return explicit;
  return MOBILE_NAV_DEFAULT_PRIORITY[String(block?.kind ?? '')] ?? 999;
};


const clone3DSceneValue = <T,>(value: T): T => {
  try { return JSON.parse(JSON.stringify(value)) as T; } catch { return value; }
};

const merge3DSceneState = (...states: any[]): Asuka3DSceneState => {
  const out: Partial<Asuka3DSceneState> = {};
  for (const state of states) {
    if (!state || typeof state !== 'object') continue;
    Object.assign(out, clone3DSceneValue(state));
    if (state.camera || out.camera) out.camera = { ...(out.camera ?? {}), ...(state.camera ?? {}) };
    if (state.materials || out.materials) out.materials = { ...(out.materials ?? {}), ...(state.materials ?? {}) };
    if (Array.isArray(state.products)) out.products = clone3DSceneValue(state.products);
    if (Array.isArray(state.decorations)) out.decorations = clone3DSceneValue(state.decorations);
    if (Array.isArray(state.printAreas)) out.printAreas = clone3DSceneValue(state.printAreas);
  }
  return create3DSceneState(out);
};

const resolve3DSceneForView = (
  view: any,
  storedByView: Record<string, Asuka3DSceneState>,
  options: any,
): Asuka3DSceneState => {
  const viewId = String(view?.id ?? '').trim();
  const optionScene = options?.scene && typeof options.scene === 'object' ? options.scene : {};
  const viewScene = view?.scene3d && typeof view.scene3d === 'object' ? view.scene3d : {};
  const storedScene = viewId && storedByView?.[viewId] && typeof storedByView[viewId] === 'object' ? storedByView[viewId] : {};
  return merge3DSceneState(optionScene, viewScene, storedScene);
};


export const AsukaRoot: FunctionalComponent<AsukaRootProps> = ({ className, width = null, height = null, maxWidth = null, maxHeight = null }) => {
  const moduleRegistryVersion = useModuleRegistryVersion();
  // Important: populate the listener registry before any `isListenerEnabled()` checks.
  // Otherwise, listeners default to "disabled" and selection-based features (like the selection menu)
  // never wire up.
  useEffect(() => {
    defineDefaultListeners();
  }, []);

  useEffect(() => {
    asukaHistoryDebug('root.mounted', {
      build: getAsukaHistoryDebugBuild(),
      note: 'AsukaRoot from diagnostic history build is mounted.',
    });
  }, []);


  const t = useT();
  const getMenuLabel = useCallback((block: any) => {
    const fallback = String(block?.label ?? block?.kind ?? block?.id ?? '');
    const api = getI18n();
    const bundles = api.getTranslations?.() ?? {};
    const locale = api.getLocale?.() ?? 'en';
    const fallbackLocale = api.getFallbackLocale?.() ?? 'en';

    const getPath = (bundle: any, key: string): string | undefined => {
      if (!bundle || typeof bundle !== 'object') return undefined;
      if (typeof bundle[key] === 'string') return bundle[key];
      const firstDot = key.indexOf('.');
      if (firstDot > 0) {
        const ns = key.slice(0, firstDot);
        const scoped = bundle[ns];
        if (scoped && typeof scoped === 'object') {
          if (typeof scoped[key] === 'string') return scoped[key];
          const short = key.slice(firstDot + 1);
          if (typeof scoped[short] === 'string') return scoped[short];
        }
      }
      const parts = key.split('.');
      let cur: any = bundle;
      for (const part of parts) {
        if (!cur || typeof cur !== 'object' || !(part in cur)) return undefined;
        cur = cur[part];
      }
      return typeof cur === 'string' ? cur : undefined;
    };

    const resolve = (key: string | null) => {
      if (!key) return undefined;
      return (
        getPath(bundles[locale], key) ??
        getPath(bundles[fallbackLocale], key)
      );
    };

    const byId = block?.id ? resolve(`menu.${String(block.id)}`) : undefined;
    if (byId) return byId;
    const byKind = block?.kind ? resolve(`menu.${String(block.kind)}`) : undefined;
    if (byKind) return byKind;
    return fallback;
  }, []);

  const getMenuIcon = useCallback((block: any) => {
    const icon = typeof block?.icon === 'string' ? block.icon.trim() : '';
    return icon && icon !== '❔' ? icon : DEFAULT_MENU_ICON;
  }, []);
  const permissions = useAsukaStore((state) => (state as any).permissions ?? DEFAULT_PERMISSIONS);
  const bootReady = useAsukaStore((state) => (state as any).bootReady ?? false);
  const bootStatus = useAsukaStore((state) => (state as any).bootStatus ?? 'Preparing editor…');
  const activeRenderEngineKind = useAsukaStore((state) => (state as any).activeRenderEngineKind ?? 'fabric');
  const [external3dEngineReady, setExternal3dEngineReady] = useState(false);
  const [external3dEngineError, setExternal3dEngineError] = useState<string | null>(null);
  const [renderEngineRetryVersion, setRenderEngineRetryVersion] = useState(0);

  // Keep a lightweight live zoom indicator for the header.
  const [zoomPct, setZoomPct] = useState(100);
  useEffect(() => {
    const engine = createLiveModuleRenderEngine(activeRenderEngineKind === '3d' ? '3d' : 'fabric');
    let raf = 0;
    const tick = () => {
      try {
        const z = engine.viewport.getZoom?.() ?? 1;
        const pct = Math.round((Number(z) || 1) * 100);
        setZoomPct(pct);
      } catch {
        setZoomPct(100);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [activeRenderEngineKind]);


  const topBlocks = useAsukaStore((state) => state.blocksByZone.top);
  const bottomBlocks = useAsukaStore((state) => state.blocksByZone.bottom);
  const leftBlocks = useAsukaStore((state) => state.blocksByZone.left);
  const rightBlocks = useAsukaStore((state) => state.blocksByZone.right);

  const zoneCollapsed = useAsukaStore((state) => state.zoneCollapsed);
  const zoneCollapsible = useAsukaStore((state) => state.zoneCollapsible);
  const zoneType = useAsukaStore(
    (state) => (state as any).zoneType ?? { top: 'default', bottom: 'default', left: 'default', right: 'panels' },
  );
  const toggleZoneCollapsed = useAsukaStore((state) => state.toggleZoneCollapsed);
  const toggleBlockCollapsed = useAsukaStore((state) => (state as any).toggleBlockCollapsed);

  const topCollapsed = zoneCollapsed.top;
  const bottomCollapsed = zoneCollapsed.bottom;
  const leftCollapsed = zoneCollapsed.left;
  const rightCollapsed = zoneCollapsed.right;

  const leftZoneType = (zoneType as any).left ?? 'default';
  const rightZoneType = (zoneType as any).right ?? 'panels';
  const isLeftPanels = leftZoneType === 'panels';
  const isRightPanels = rightZoneType === 'panels';
  const isLeftInline = leftZoneType === 'inline';
  const isRightInline = rightZoneType === 'inline';

  const toggleMenu = useAsukaStore((state) => state.toggleMenu);
  const closeMenu = useAsukaStore((state) => state.closeMenu);
  // Opening an overlay used to make the whole AsukaRoot component render again
  // solely so sidebar buttons could toggle their `is-active` class. AsukaRoot is
  // deliberately large (canvas, zones, responsive navigation, views, selection
  // helpers), so that subscription made every module opening visibly stall once
  // a large Product 3D catalog/state was loaded. Keep the active module outside
  // the Preact render path and update the tiny menu affordances imperatively.
  const activeModuleIdRef = useRef<ModuleId | null>((useAsukaStore.getState() as any)?.activeModuleId ?? null);
  const moveBlock = useAsukaStore((state) => state.moveBlock);
  const draggedBlockId = useAsukaStore((state) => state.draggedBlockId);
  const startDraggingBlock = useAsukaStore((state) => state.startDraggingBlock);
  const endDraggingBlock = useAsukaStore((state) => state.endDraggingBlock);
  const defaultRenderEngineKind = useAsukaStore((state) => (state as any).defaultRenderEngineKind ?? 'fabric');
  const renderEngineOptionsByKind = useAsukaStore((state) => (state as any).renderEngineOptionsByKind ?? {});
  const setActiveRenderEngineKind = useAsukaStore((state) => (state as any).setActiveRenderEngineKind);
  const threeDOptions = useAsukaStore((state) => ((state as any).renderEngineOptionsByKind?.['3d'] ?? null));
  const setDocument3dSceneForView = useAsukaStore((state) => (state as any).setDocument3dSceneForView);
  const setDocument3dCameraForView = useAsukaStore((state) => (state as any).setDocument3dCameraForView);
  const setDocument3dHistoryForView = useAsukaStore((state) => (state as any).setDocument3dHistoryForView);
  const setDocumentFabricHistoryForView = useAsukaStore((state) => (state as any).setDocumentFabricHistoryForView);
  const setDocumentHistoryInfo = useAsukaStore((state) => (state as any).setDocumentHistoryInfo);


// Magnifier
const magnifierEnabled = useAsukaStore((state) => (state as any).magnifierEnabled);
const magnifierActive = useAsukaStore((state) => (state as any).magnifierActive);
const magnifierSize = useAsukaStore((state) => (state as any).magnifierSize);
const magnifierZoom = useAsukaStore((state) => (state as any).magnifierZoom);
const setMagnifierActive = useAsukaStore((state) => (state as any).setMagnifierActive);
const setMagnifierPosition = useAsukaStore((state) => (state as any).setMagnifierPosition);

const magnifierOverlayRef = useRef<HTMLCanvasElement | null>(null);

// Ruler
const rulerEnabled = useAsukaStore((state) => (state as any).rulerEnabled);


  // Selection menu API (F7)
  const openSelectionMenu = useAsukaStore((state) => state.openSelectionMenu);
  const closeSelectionMenu = useAsukaStore((state) => state.closeSelectionMenu);
  const resetSelectionMenuNav = useAsukaStore((state) => state.resetSelectionMenuNav);
  const setSelectionMenuItems = useAsukaStore((state) => state.setSelectionMenuItems);
  const setSelectionMenuAnchorBox = useAsukaStore((state: any) => state.setSelectionMenuAnchorBox);

  const moduleContext = useMemo<AsukaModuleContext>(() => {
    const liveEngine = createLiveModuleRenderEngine(
      activeRenderEngineKind === '3d' ? '3d' : 'fabric',
    );
    return {
      getEngine: () => liveEngine,
      ui: getVanillaUiHelpers(),
    };
  }, [activeRenderEngineKind]);

  const views = useAsukaStore((state) => state.views);
  const activeViewId = useAsukaStore((state) => state.activeViewId);
  const documentHistoryInfo = useAsukaStore((state: any) => state.documentHistoryInfo ?? { canUndo: false, canRedo: false, length: 0, index: -1, maxSnapshots: DEFAULT_HISTORY_MAX_SNAPSHOTS });
  const active3dHistoryTrack = useAsukaStore((state: any) => {
    const viewId = String(state?.activeViewId ?? '').trim();
    return viewId ? (state?.document3dHistoryByView ?? {})[viewId] ?? null : null;
  });
  const activeFabricHistoryTrack = useAsukaStore((state: any) => {
    const viewId = String(state?.activeViewId ?? '').trim();
    return (state?.documentFabricHistoryByView ?? {})[DOCUMENT_FABRIC_HISTORY_KEY] ?? (viewId ? (state?.documentFabricHistoryByView ?? {})[viewId] ?? null : null);
  });
  const hasViews = (views?.length ?? 0) > 0;
  const noViewModuleTooltip = t(NO_VIEW_MODULE_TOOLTIP_KEY, 'No view available. Create a view before using this tool.');
  const isModuleUnavailableWithoutView = useCallback((kind: string) => {
    // The registry revision invalidates module-derived availability memoization.
    void moduleRegistryVersion;
    const def = getModule(kind as any);
    return !!def && isModuleBlockedWithoutView(def, hasViews);
  }, [hasViews, moduleRegistryVersion]);
  const addView = useAsukaStore((state) => (state as any).addView);
  const setActiveView = useAsukaStore((state) => (state as any).setActiveView);

  const canMoveLayoutPanels = (permissions as any)?.canMovePanels !== false;
  const canCollapseLayoutPanels = (permissions as any)?.canCollapsePanels !== false;
  const isDragging = Boolean(draggedBlockId);
  const draggingBlockIdRef = useRef<string | null>(null);
  const shellRootRef = useRef<HTMLDivElement | null>(null);
  const uiConfig = useAsukaStore((state: any) => state.uiConfig);
  const responsiveConfig = useAsukaStore((state: any) => state.responsiveConfig);
  const responsiveBreakpoint = useAsukaStore((state: any) => state.responsiveBreakpoint ?? 'desktop');
  const setResponsiveMetrics = useAsukaStore((state: any) => state.setResponsiveMetrics);
  const responsiveEnabled = responsiveConfig?.enabled !== false;
  const isMobileLayout = responsiveEnabled && responsiveBreakpoint === 'mobile';
  const isTabletLayout = responsiveEnabled && responsiveBreakpoint === 'tablet';
  const [mainOverlayTarget, setMainOverlayTarget] = useState<HTMLElement | null>(null);
  const bindMainOverlayTarget = useCallback((node: HTMLElement | null) => {
    setMainOverlayTarget(node);
  }, []);

  const syncActiveModuleTriggerDom = useCallback((moduleIdInput?: string | null) => {
    const moduleId = String(moduleIdInput ?? '').trim();
    const root = shellRootRef.current;
    if (!root) return;
    const triggers = root.querySelectorAll<HTMLElement>('[data-asuka-module-trigger="true"]');
    triggers.forEach((trigger) => {
      const kind = String(trigger.dataset.asukaModuleKind ?? '').trim();
      const active = !!moduleId && kind === moduleId;
      trigger.classList.toggle('is-active', active);
      trigger.setAttribute('data-asuka-module-active', active ? 'true' : 'false');
      if (trigger.tagName === 'BUTTON') trigger.setAttribute('aria-pressed', active ? 'true' : 'false');
    });
  }, []);

  useEffect(() => {
    const node = shellRootRef.current;
    if (!node || typeof setResponsiveMetrics !== 'function') return;

    let raf = 0;
    const measure = () => {
      try {
        const rect = node.getBoundingClientRect?.();
        const width = Math.max(0, Math.floor(Number(rect?.width ?? node.clientWidth ?? 0) || 0));
        setResponsiveMetrics(width);
      } catch {}
    };
    const schedule = () => {
      if (raf) cancelAnimationFrame(raf);
      raf = requestAnimationFrame(measure);
    };

    schedule();

    let ro: ResizeObserver | null = null;
    try {
      ro = new ResizeObserver(schedule);
      ro.observe(node);
    } catch {
      try { window.addEventListener('resize', schedule); } catch {}
    }

    return () => {
      if (raf) {
        try { cancelAnimationFrame(raf); } catch {}
      }
      try { ro?.disconnect?.(); } catch {}
      try { window.removeEventListener('resize', schedule); } catch {}
    };
  }, [setResponsiveMetrics, responsiveConfig?.enabled, responsiveConfig?.mobileMaxWidth, responsiveConfig?.tabletMaxWidth]);

  const pointerMenuDragRef = useRef<any>(null);
  const [pointerMenuDrag, setPointerMenuDrag] = useState<any>(null);
  const [settlingInlineBlockIds, setSettlingInlineBlockIds] = useState<Record<string, true>>({});

  useEffect(() => {
    dndLog('state', {
      canMoveLayoutPanels,
      canMovePanelsPermission: (permissions as any)?.canMovePanels,
      canCollapsePanelsPermission: (permissions as any)?.canCollapsePanels,
      role: (useAsukaStore.getState() as any)?.role,
      draggedBlockId,
      blocksByZone: {
        top: topBlocks.map((b: any) => ({ id: b.id, kind: b.kind, order: b.order })),
        left: leftBlocks.map((b: any) => ({ id: b.id, kind: b.kind, order: b.order })),
        right: rightBlocks.map((b: any) => ({ id: b.id, kind: b.kind, order: b.order })),
        bottom: bottomBlocks.map((b: any) => ({ id: b.id, kind: b.kind, order: b.order })),
      },
    });
  }, [
    canMoveLayoutPanels,
    draggedBlockId,
    permissions,
    topBlocks,
    leftBlocks,
    rightBlocks,
    bottomBlocks,
  ]);

  // --- Listeners runtime toggle (F2) ---
  const [listenersTick, setListenersTick] = useState(0);
  useEffect(() => {
    return onAsukaEvent('listeners.changed', () => {
      setListenersTick((t) => t + 1);
    });
  }, []);

  const isBlockSupportedByActiveEngine = useCallback((block: any) => {
    // Capabilities can change when a lazy module is registered.
    void moduleRegistryVersion;
    const current = String(activeRenderEngineKind ?? 'fabric').trim().toLowerCase() === '3d' ? '3d' : 'fabric';
    const def = getModule(String(block?.kind ?? '') as any);
    const supported = Array.isArray(block?.supportedEngines) ? block.supportedEngines : null;
    if (supported && supported.length > 0) {
      const ok = supported.some((item: any) => String(item ?? '').trim().toLowerCase() === current);
      if (!ok) return false;
    }
    if (def && !isModuleCompatibleWithEngine(def, current as any)) return false;
    const required = normalizeModuleRequiredCapabilities((block as any)?.requiredCapabilities);
    if (required && required.length > 0) {
      const capabilities = getRenderEngineCapabilitiesForKind(current as any);
      if (!areRenderEngineCapabilitiesSatisfied(required, capabilities)) return false;
    }
    return true;
  }, [activeRenderEngineKind, moduleRegistryVersion]);

  const getBlockKind = useCallback((blockOrKind: any) => typeof blockOrKind === 'string' ? blockOrKind : String(blockOrKind?.kind ?? ''), []);

  const canOpenBlock = useCallback((blockOrKind: any) => {
    const kind = getBlockKind(blockOrKind);
    if (!kind) return false;
    const blockSupported = typeof blockOrKind === 'string' ? true : isBlockSupportedByActiveEngine(blockOrKind);
    const rendererReady = activeRenderEngineKind !== '3d' || kind === 'views' || external3dEngineReady;
    return rendererReady
      && blockSupported
      && canOpenModuleId(permissions as any, kind as any)
      && hasModule(kind as any)
      && !isModuleUnavailableWithoutView(kind);
  }, [activeRenderEngineKind, external3dEngineReady, getBlockKind, isBlockSupportedByActiveEngine, isModuleUnavailableWithoutView, permissions]);

  const canRenderBlock = useCallback((block: any) => {
    const kind = getBlockKind(block);
    return !!kind && isBlockSupportedByActiveEngine(block) && canOpenModuleId(permissions as any, kind as any) && hasModule(kind as any);
  }, [getBlockKind, isBlockSupportedByActiveEngine, permissions]);

  const isPagePortalBlock = useCallback((block: any) => (
    block?.renderMode === 'portal'
    && typeof block?.mountTarget === 'string'
    && block.mountTarget.trim().length > 0
  ), []);

  const shouldShowBlock = useCallback((block: any) => (
    !isPagePortalBlock(block) && canRenderBlock(block)
  ), [canRenderBlock, isPagePortalBlock]);

  const pagePortalBlocks = useMemo(
    () => [...topBlocks, ...rightBlocks, ...bottomBlocks, ...leftBlocks]
      .filter((block) => isPagePortalBlock(block)),
    [bottomBlocks, isPagePortalBlock, leftBlocks, rightBlocks, topBlocks],
  );
  const visiblePagePortalBlocks = useMemo(
    () => pagePortalBlocks.filter((block) => canRenderBlock(block)),
    [canRenderBlock, pagePortalBlocks],
  );
  const pagePortalDebug = useMemo(() => {
    if (typeof window === 'undefined' || !/[?&](?:asuka_portal_debug|apd_debug_template)=1(?:&|$)/.test(window.location.search)) {
      return undefined;
    }
    return JSON.stringify(pagePortalBlocks.map((block) => {
      const kind = getBlockKind(block);
      return {
        id: String(block?.id ?? ''),
        kind,
        mountTarget: String(block?.mountTarget ?? ''),
        registered: hasModule(kind as any),
        permitted: canOpenModuleId(permissions as any, kind as any),
        supported: isBlockSupportedByActiveEngine(block),
      };
    }));
  }, [getBlockKind, isBlockSupportedByActiveEngine, pagePortalBlocks, permissions]);
  const visibleTopBlocks = useMemo(() => topBlocks.filter((b) => shouldShowBlock(b)), [shouldShowBlock, topBlocks]);
  const visibleBottomBlocks = useMemo(() => bottomBlocks.filter((b) => shouldShowBlock(b)), [bottomBlocks, shouldShowBlock]);
  const visibleLeftBlocks = useMemo(() => leftBlocks.filter((b) => shouldShowBlock(b)), [leftBlocks, shouldShowBlock]);
  const visibleRightBlocks = useMemo(() => rightBlocks.filter((b) => shouldShowBlock(b)), [rightBlocks, shouldShowBlock]);

  const moveSideZonesToBottomNav = isMobileLayout && responsiveConfig?.moveSideZonesToBottomNav !== false;
  const renderSideZonesAsTabletRails = isTabletLayout && responsiveConfig?.drawerPanelsOnTablet !== false;
  const mobileBottomNavZones = Array.isArray(responsiveConfig?.bottomNavZones) && responsiveConfig.bottomNavZones.length > 0
    ? responsiveConfig.bottomNavZones
    : ['left', 'right'];
  const moveLeftZoneToBottomNav = moveSideZonesToBottomNav && mobileBottomNavZones.includes('left');
  const moveRightZoneToBottomNav = moveSideZonesToBottomNav && mobileBottomNavZones.includes('right');
  const visibleLeftBlocksForLayout = moveLeftZoneToBottomNav ? [] : visibleLeftBlocks;
  const visibleRightBlocksForLayout = moveRightZoneToBottomNav ? [] : visibleRightBlocks;
  const leftEmbeddedModuleLoadKey = JSON.stringify(
    !renderSideZonesAsTabletRails && !leftCollapsed && (isLeftPanels || isLeftInline)
      ? visibleLeftBlocksForLayout.map((block) => String(block.kind ?? '')).filter(Boolean)
      : [],
  );
  const rightEmbeddedModuleLoadKey = JSON.stringify(
    !renderSideZonesAsTabletRails && !rightCollapsed && (isRightPanels || isRightInline)
      ? visibleRightBlocksForLayout.map((block) => String(block.kind ?? '')).filter(Boolean)
      : [],
  );
  const pagePortalModuleLoadKey = JSON.stringify(
    visiblePagePortalBlocks.map((block) => String(block.kind ?? '')).filter(Boolean),
  );

  useEffect(() => {
    const moduleIds = new Set<ModuleId>();
    for (const key of [leftEmbeddedModuleLoadKey, rightEmbeddedModuleLoadKey, pagePortalModuleLoadKey]) {
      for (const moduleId of JSON.parse(key) as string[]) {
        if (moduleId) moduleIds.add(moduleId as ModuleId);
      }
    }
    moduleIds.forEach((moduleId) => {
      void ensureModuleAvailable(moduleId);
    });
  }, [leftEmbeddedModuleLoadKey, pagePortalModuleLoadKey, rightEmbeddedModuleLoadKey]);

  const mobileSideBlocks = (() => {
    if (!moveSideZonesToBottomNav) return [] as any[];
    const candidates = [
      ...(moveLeftZoneToBottomNav ? visibleLeftBlocks.map((block: any) => ({ ...block, __asukaSourceZone: 'left' as LayoutZoneId })) : []),
      ...(moveRightZoneToBottomNav ? visibleRightBlocks.map((block: any) => ({ ...block, __asukaSourceZone: 'right' as LayoutZoneId })) : []),
    ].map((block: any) => ({ ...block, __asukaMobilePriority: getMobileNavPriority(block) }));

    const byKind = new Map<string, any>();
    const shouldReplace = (next: any, prev: any) => {
      const priorityDelta = Number(next.__asukaMobilePriority ?? 999) - Number(prev.__asukaMobilePriority ?? 999);
      if (priorityDelta !== 0) return priorityDelta < 0;
      const orderDelta = Number(next.order ?? 0) - Number(prev.order ?? 0);
      if (orderDelta !== 0) return orderDelta < 0;
      const prevZone = String(prev.__asukaSourceZone ?? 'right');
      const nextZone = String(next.__asukaSourceZone ?? 'right');
      return nextZone.localeCompare(prevZone) < 0;
    };

    candidates.forEach((block: any) => {
      const key = String(block.kind ?? block.id ?? '');
      if (!key) return;
      const prev = byKind.get(key);
      if (!prev || shouldReplace(block, prev)) byKind.set(key, block);
    });

    return Array.from(byKind.values()).sort((a: any, b: any) => {
      const priorityDelta = Number(a.__asukaMobilePriority ?? 999) - Number(b.__asukaMobilePriority ?? 999);
      if (priorityDelta !== 0) return priorityDelta;
      const orderDelta = Number(a.order ?? 0) - Number(b.order ?? 0);
      if (orderDelta !== 0) return orderDelta;
      const zoneDelta = String(a.__asukaSourceZone ?? '').localeCompare(String(b.__asukaSourceZone ?? ''));
      if (zoneDelta !== 0) return zoneDelta;
      return String(a.id ?? '').localeCompare(String(b.id ?? ''));
    });
  })();
  const maxVisibleMobileItems = Math.max(1, Math.floor(Number(responsiveConfig?.maxVisibleMobileItems ?? 5) || 5));
  const mobilePrimaryCount = mobileSideBlocks.length > maxVisibleMobileItems
    ? Math.max(1, maxVisibleMobileItems - 1)
    : maxVisibleMobileItems;
  const mobilePrimaryBlocks = mobileSideBlocks.slice(0, mobilePrimaryCount);
  const mobileOverflowBlocks = mobileSideBlocks.slice(mobilePrimaryCount);
  const [mobileMoreOpen, setMobileMoreOpen] = useState(false);
  const mobileNavRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (!moveSideZonesToBottomNav) setMobileMoreOpen(false);
  }, [moveSideZonesToBottomNav]);

  useEffect(() => {
    setMobileMoreOpen(false);
  }, [responsiveBreakpoint]);

  useEffect(() => {
    activeModuleIdRef.current = ((useAsukaStore.getState() as any)?.activeModuleId ?? null) as ModuleId | null;
    syncActiveModuleTriggerDom(activeModuleIdRef.current);
    const unsubscribe = useAsukaStore.subscribe((nextState: any, previousState: any) => {
      const nextId = (nextState?.activeModuleId ?? null) as ModuleId | null;
      const previousId = (previousState?.activeModuleId ?? null) as ModuleId | null;
      if (String(nextId ?? '') === String(previousId ?? '')) return;
      activeModuleIdRef.current = nextId;
      syncActiveModuleTriggerDom(nextId);
      // This state update is a no-op on desktop in the common case because the
      // mobile overflow is already closed. It does not subscribe AsukaRoot to
      // every overlay change anymore.
      setMobileMoreOpen((open) => open ? false : open);
      logAsuka3DPerf('ui-module', 'active-dom-synced', {
        previousId: previousId ?? null,
        nextId: nextId ?? null,
      }, { console: false });
    });
    return () => unsubscribe?.();
  }, [syncActiveModuleTriggerDom]);

  useEffect(() => {
    if (!mobileMoreOpen) return;
    const closeOnOutsidePointer = (event: PointerEvent) => {
      const node = mobileNavRef.current;
      if (!node || node.contains(event.target as Node)) return;
      setMobileMoreOpen(false);
    };
    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === 'Escape') setMobileMoreOpen(false);
    };
    try { document.addEventListener('pointerdown', closeOnOutsidePointer, true); } catch {}
    try { document.addEventListener('keydown', closeOnEscape, true); } catch {}
    return () => {
      try { document.removeEventListener('pointerdown', closeOnOutsidePointer, true); } catch {}
      try { document.removeEventListener('keydown', closeOnEscape, true); } catch {}
    };
  }, [mobileMoreOpen]);

  // ALWAYS mount zones (prevents native drag cancellation)
  const showTopZone = true;
  const showLeftZone = !moveLeftZoneToBottomNav || isDragging;
  const showRightZone = !moveRightZoneToBottomNav || isDragging;
  const showBottomZone = true;

  // Prevent accidental click after a drag operation
  const suppressClickRef = useRef(false);
  const getBlockAccessLabel = (block: any) => {
    const kind = String(block?.kind ?? '');
    if (isModuleUnavailableWithoutView(kind)) return noViewModuleTooltip;
    if (activeRenderEngineKind === '3d' && kind !== 'views' && !external3dEngineReady) {
      return t('ui.loading3DRenderer', 'Loading 3D renderer…');
    }
    return getMenuLabel(block);
  };

  const handleBlockClick = (blockOrKind: any) => (e: any) => {
    const kind = getBlockKind(blockOrKind);
    const clickHandledAt = typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
    const eventTimestamp = Number(e?.timeStamp);
    const inputDelayMs = Number.isFinite(eventTimestamp) && eventTimestamp >= 0 && eventTimestamp <= clickHandledAt + 1000
      ? Math.max(0, clickHandledAt - eventTimestamp)
      : undefined;
    logAsuka3DPerf('ui-module', 'trigger-click', {
      id: kind,
      activeViewId: (useAsukaStore.getState() as any)?.activeViewId ?? null,
      engine: (useAsukaStore.getState() as any)?.activeRenderEngineKind ?? null,
    }, { durationMs: inputDelayMs, console: false });
    if (!canOpenBlock(blockOrKind)) {
      e.preventDefault?.();
      e.stopPropagation?.();
      return;
    }
    if (suppressClickRef.current) {
      e.preventDefault?.();
      e.stopPropagation?.();
      return;
    }
    toggleMenu(kind);
  };

  const getEmptyZoneClass = (zoneId: LayoutZoneId, blocksLen: number) => {
    if (blocksLen > 0) return '';

    // When idle and the sidebar is empty, fully hide it so it doesn't reserve space.
    // While dragging, we keep a dedicated overlay drop target (see below), so the
    // sidebar itself can remain collapsed/hidden without shifting the layout.
    if (!isDragging && (zoneId === 'left' || zoneId === 'right')) {
      return 'ask-hidden';
    }
    if (isDragging) {
      // IMPORTANT: do NOT expand left/right empty sidebars during drag (it shifts the whole layout).
      // We'll use an absolute overlay drop target instead (see overlays in the layout container).
      if (zoneId === 'left' || zoneId === 'right') {
        return 'ask-w-0 ask-min-w-0 ask-p-0 ask-border-0 ask-overflow-hidden';
      }
      // Minimal drop target size for top/bottom while dragging.
      return 'ask-min-h-[48px] ask-h-[48px]';
    }
    // Fully collapsed when idle & empty.
    return zoneId === 'top' || zoneId === 'bottom'
      ? 'ask-h-0 ask-min-h-0 ask-p-0 ask-border-0 ask-overflow-hidden'
      : 'ask-w-0 ask-min-w-0 ask-p-0 ask-border-0 ask-overflow-hidden';
  };

  const hideTopBottomCollapseBtn = (zoneId: LayoutZoneId, blocksLen: number) => {
    if (!canCollapseLayoutPanels) return true;
    if (!(zoneCollapsible as any)?.[zoneId]) return true;
    // When zone is empty and we're not dragging, hide the collapse button (more elegant).
    if (blocksLen > 0) return false;
    if (isDragging) return false;
    return zoneId === 'top' || zoneId === 'bottom';
  };

  const canvasHostRef = useRef<HTMLDivElement | null>(null);
  const canvasFrameRef = useRef<HTMLDivElement | null>(null);
  const canvasHoverTooltipRef = useRef<HTMLDivElement | null>(null);
  const canvasHover3DTooltipRef = useRef<HTMLDivElement | null>(null);
  const canvasHover3DTooltipLabelRef = useRef<HTMLDivElement | null>(null);
  const [canvasFrameSize, setCanvasFrameSize] = useState<{ w: number; h: number }>({ w: 0, h: 0 });
  const [stateRestoreRevision, setStateRestoreRevision] = useState(0);
  const stateRestoreRevisionRef = useRef(0);
  const mounted3dEngineRef = useRef<AsukaRenderEngine | null>(null);
  const mounted3dViewIdRef = useRef<string | null>(null);
  const lastLoaded3dViewIdRef = useRef<string | null>(null);
  const last3dSwitchPersistRef = useRef<{ viewId: string; at: number } | null>(null);
  const active3DLoadRequestRef = useRef(0);
  const active3DLoadQueueRef = useRef<Promise<void>>(Promise.resolve());
  const engineMountTokenRef = useRef(0);
  const mountedRenderEngineRef = useRef<AsukaRenderEngine | null>(null);
  const mountedRenderViewIdRef = useRef<string | null>(null);
  const historyBootBaselineCapturedRef = useRef<AsukaRenderEngine | null>(null);
  const [mountedEngineVersion, setMountedEngineVersion] = useState(0);
  const [placementStatus, setPlacementStatus] = useState<AsukaPlacementStatus | null>(null);

  useEffect(() => {
    const canvasFrame = canvasFrameRef.current;
    if (!canvasFrame || activeRenderEngineKind !== '3d') return;

    const preventHostScroll = (event: WheelEvent) => {
      if (event.cancelable) event.preventDefault();
    };

    canvasFrame.addEventListener('wheel', preventHostScroll, { passive: false, capture: true });
    return () => canvasFrame.removeEventListener('wheel', preventHostScroll, true);
  }, [activeRenderEngineKind]);

  useEffect(() => {
    setPlacementStatus(null);
    let clearTimer = 0;
    const engine = createLiveModuleRenderEngine(activeRenderEngineKind);
    const off = engine.events.subscribe((event) => {
      if (event.type !== 'placement') return;
      if (clearTimer) window.clearTimeout(clearTimer);
      const next = event.payload && typeof event.payload === 'object'
        ? event.payload as AsukaPlacementStatus
        : null;
      setPlacementStatus(next);
      if (!next || next.phase === 'committed' || next.phase === 'cancelled') {
        clearTimer = window.setTimeout(() => setPlacementStatus(null), 420);
      }
    });
    return () => {
      if (clearTimer) window.clearTimeout(clearTimer);
      off?.();
    };
  }, [activeRenderEngineKind, mountedEngineVersion]);

  useEffect(() => {
    const nextEngine = getActiveViewEngineKind(views as any, activeViewId, defaultRenderEngineKind as any);
    try { setActiveRenderEngineKind?.(nextEngine); } catch {}
  }, [views, activeViewId, defaultRenderEngineKind, setActiveRenderEngineKind]);


  // Keep a monotonic restore signal mounted for the whole application lifetime.
  // The state event can be emitted before a newly selected Babylon view has
  // mounted; storing the revision guarantees that the normal 3D load effect
  // replays the imported scene as soon as the replacement runtime is ready.
  useEffect(() => {
    const off = onAsukaEvent('after:state.restore', () => {
      const nextRevision = stateRestoreRevisionRef.current + 1;
      stateRestoreRevisionRef.current = nextRevision;
      // Invalidate any load that started before this imported store transaction.
      active3DLoadRequestRef.current += 1;
      setStateRestoreRevision(nextRevision);
    });
    return () => off?.();
  }, []);

  const viewThumbnails = useAsukaStore((s) => s.viewThumbnails);
  const setViewThumbnail = useAsukaStore((s) => s.setViewThumbnail);
  const bumpCanvasChangeTick = useAsukaStore((s) => (s as any).bumpCanvasChangeTick);
  const thumbnailsRefreshTick = useAsukaStore((s) => (s as any).thumbnailsRefreshTick);
  const thumbnailsRefreshViewIds = useAsukaStore((s) => (s as any).thumbnailsRefreshViewIds);

  const thumbnailPollRef = useRef<{ lastChange: number; timeoutId: number | null }>({
    lastChange: 0,
    timeoutId: null,
  });
  const thumbnailCaptureRef = useRef<{ inFlight: boolean; requestId: number }>({ inFlight: false, requestId: 0 });
  const last3DInteractionAtRef = useRef(0);

  const [selectionBox, setSelectionBox] = useState<{
    left: number;
    top: number;
    width: number;
    height: number;
  } | null>(null);
  const [canvasHover, setCanvasHover] = useState<{
    engine: 'fabric' | '3d';
    label: string;
    clientX: number;
    clientY: number;
  } | null>(null);
  const [isTransformingSelection, setIsTransformingSelection] = useState(false);

  const get3DSelectionAnchorBox = useCallback((payload?: any, selected?: any) => {
    const explicitX = Number(payload?.clientX ?? payload?.x ?? payload?.pointerX);
    const explicitY = Number(payload?.clientY ?? payload?.y ?? payload?.pointerY);
    if (Number.isFinite(explicitX) && Number.isFinite(explicitY)) {
      return { left: explicitX - 12, top: explicitY - 12, width: 24, height: 24 };
    }
    const screenBox = (selected as any)?.metadata?.screenBox ?? payload?.screenBox;
    const boxLeft = Number(screenBox?.left);
    const boxTop = Number(screenBox?.top);
    const boxWidth = Number(screenBox?.width);
    const boxHeight = Number(screenBox?.height);
    if (Number.isFinite(boxLeft) && Number.isFinite(boxTop)) {
      return {
        left: boxLeft,
        top: boxTop,
        width: Math.max(24, Number.isFinite(boxWidth) ? boxWidth : 24),
        height: Math.max(24, Number.isFinite(boxHeight) ? boxHeight : 24),
      };
    }
    const rect = canvasFrameRef.current?.getBoundingClientRect?.();
    if (rect) {
      return {
        left: rect.left + rect.width / 2 - 18,
        top: rect.top + rect.height / 2 - 18,
        width: 36,
        height: 36,
      };
    }
    return { left: window.innerWidth / 2 - 18, top: window.innerHeight / 2 - 18, width: 36, height: 36 };
  }, []);

  const open3DProductVariationPicker = useCallback((selected: any, options: { focusOnOpen?: boolean; source?: string } = {}) => {
    const startedAt = typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();
    try {
      const store = useAsukaStore.getState() as any;
      const selectedRuntimeId = getProduct3DCanvasSelectionInstanceId(selected);
      const selectedViewId = String(selected?.metadata?.viewId ?? store?.activeViewId ?? '').trim();
      const scene = selectedViewId ? store?.document3dScenesByView?.[selectedViewId] : null;
      const product = resolveProduct3DCanvasSelectionInstance(selected, scene);
      const keys = [
        product?.catalogId,
        product?.id,
        product?.name,
        selected?.metadata?.catalogId,
        selected?.metadata?.asukaProductId,
        selected?.metadata?.productId,
        selectedRuntimeId,
      ].map((value) => String(value ?? '').trim()).filter(Boolean);
      const catalog = Array.isArray(store?.elements) ? store.elements : [];
      const findNested = (items: any[]): any | null => {
        for (const element of items) {
          const elementKeys = [element?.id, element?.name].map((value) => String(value ?? '').trim()).filter(Boolean);
          if (elementKeys.some((key) => keys.includes(key))) return element;
          const child = findNested(Array.isArray(element?.children) ? element.children : []);
          if (child) return child;
        }
        return null;
      };
      const fullElement = keys.map((key) => {
        try { return store?.findElement?.(key) ?? null; } catch { return null; }
      }).find(Boolean) ?? findNested(catalog);
      const elementId = String(fullElement?.id ?? product?.catalogId ?? product?.id ?? '').trim();
      const elementName = String(fullElement?.name ?? product?.name ?? '').trim();
      if (!elementId && !elementName) {
        logAsuka3DPerf('ui-module', 'variation-picker-product-unresolved', {
          selectedId: selected?.id ?? null,
          keys,
          partId: selected?.metadata?.productPartId ?? selected?.metadata?.partId ?? null,
        }, { durationMs: (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now()) - startedAt, level: 'warn' });
        return false;
      }
      const objectId = String(selected?.id ?? `product:${elementId || elementName}`).trim() || `product:${elementId || elementName}`;
      let variationId = String(product?.variationId ?? selected?.metadata?.variationId ?? '').trim();
      const partId = String(selected?.metadata?.productPartId ?? selected?.metadata?.partId ?? '').trim();
      const partMeshId = String(selected?.metadata?.productPartMeshId ?? selected?.metadata?.meshId ?? '').trim();
      const partMaterialId = String(selected?.metadata?.productPartMaterialId ?? selected?.metadata?.materialId ?? '').trim();
      const partLabel = String(selected?.metadata?.productPartLabel ?? selected?.metadata?.partLabel ?? '').trim();
      const engine = getRuntimeRenderEngine('3d') as any;
      if (partId || partMeshId || partMaterialId) {
        // Babylon already selected the clicked part before emitting
        // product-part event. Selecting it again here emitted a full renderer
        // mutation, scene persistence and module refresh before
        // the picker could open. Read the active variation only.
        try {
          const activePartVariation = partId ? engine?.parts?.getActiveVariation?.(partId) ?? null : null;
          variationId = String(activePartVariation?.id ?? variationId ?? '').trim();
        } catch {}
        // Canvas clicks should open the variation picker without moving the camera.
        // Focus remains reserved for explicit variation/group navigation inside
        // ElementsModule and ElementVariationPickerModule.
      }
      store.openElementVariationPicker?.({
        objectId,
        elementId,
        elementName,
        variationId,
        objectSrc: '',
        partId: partId || undefined,
        partLabel: partLabel || undefined,
        partMeshId: partMeshId || undefined,
        partMaterialId: partMaterialId || undefined,
        focusOnOpen: options.focusOnOpen !== false,
        source: String(options.source ?? '').trim() || undefined,
      });
      logAsuka3DPerf('ui-module', 'variation-picker-requested', {
        objectId,
        elementId,
        elementName,
        partId: partId || null,
        variationId: variationId || null,
        source: options.source ?? null,
      }, { durationMs: (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now()) - startedAt, console: false });
      return true;
    } catch (error) {
      logAsuka3DPerf('ui-module', 'variation-picker-request-error', { error }, {
        durationMs: (typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now()) - startedAt,
        level: 'warn',
      });
      return false;
    }
  }, []);

  const infer3DSelectionMenuItems = useCallback((selected: any | null): ModuleId[] => {
    if (!selected) return [];
    const kind = String(selected?.kind ?? selected?.type ?? 'object').toLowerCase();
    const group = String(selected?.metadata?.itemGroup ?? '').toLowerCase();
    const source = selected?.source ?? selected?.metadata?.source ?? {};
    if (kind === 'product-3d' || group === 'product' || kind === 'mesh') return [];

    const items: ModuleId[] = [];
    const isText = kind === 'text'
      || group === 'text'
      || typeof selected?.pricing?.text === 'string'
      || typeof source?.text === 'string'
      || typeof source?.fontFamily === 'string';
    const isImage = kind === 'image' || group === 'images' || group === 'image';
    const isDesign = kind === 'design' || group === 'designs' || group === 'design';

    // The 3D layer menu reuses the same contextual toolbar as Fabric and only
    // exposes modules that work against the common renderer contract.
    if (isText && hasModule('font-family' as ModuleId)) items.push('font-family' as ModuleId);
    if (isText && hasModule('text-formatting' as ModuleId)) items.push('text-formatting' as ModuleId);
    if ((isText || isImage || isDesign || kind === 'object') && hasModule('colors' as ModuleId)) {
      items.push('colors' as ModuleId);
    }
    if (hasModule('transform' as ModuleId)) {
      items.push('transform' as ModuleId);
    }
    if ((isText || isImage || isDesign) && hasModule('studio' as ModuleId)) {
      items.push('studio' as ModuleId);
    }

    return Array.from(new Set(items));
  }, []);

  const open3DSelectionMenuForObject = useCallback((selected: any, payload?: any) => {
    if (!selected) {
      setSelectionBox(null);
      setSelectionMenuItems([] as any);
      setSelectionMenuAnchorBox?.(null);
      resetSelectionMenuNav();
      closeSelectionMenu?.();
      return;
    }

    const kind = String(selected?.kind ?? selected?.type ?? 'object').toLowerCase();
    if (kind === 'product-3d') {
      setSelectionBox(null);
      setSelectionMenuItems([] as any);
      setSelectionMenuAnchorBox?.(null);
      resetSelectionMenuNav();
      closeSelectionMenu?.();
      open3DProductVariationPicker(selected);
      return;
    }

    // Match Fabric's direct-canvas behavior: clicking a selectable 3D
    // decoration in the Babylon canvas opens the contextual SelectionToolbar.
    // Layers can still explicitly request the same menu, but they are no
    // longer the only trigger. Product clicks are handled above and keep
    // opening the variation picker instead of this menu.
    const anchor = get3DSelectionAnchorBox(payload, selected);
    const menuItems = infer3DSelectionMenuItems(selected) as any;
    setIsTransformingSelection(false);
    resetSelectionMenuNav();
    setSelectionBox(anchor);
    setSelectionMenuAnchorBox?.(anchor);
    setSelectionMenuItems(menuItems);
    openSelectionMenu();
  }, [closeSelectionMenu, get3DSelectionAnchorBox, infer3DSelectionMenuItems, open3DProductVariationPicker, openSelectionMenu, resetSelectionMenuNav, setSelectionMenuAnchorBox, setSelectionMenuItems]);

  useEffect(() => {
    // When there are no views, hide the rendering surface and release any
    // mounted engine. Engine-specific mount/unmount now goes through the
    // shared engine manager instead of being spread through the layout.
    if (!hasViews) {
      engineMountTokenRef.current += 1;
      const prev = mountedRenderEngineRef.current;
      if (prev) {
        try { prev.lifecycle.unmount(); } catch {}
      }
      mountedRenderEngineRef.current = null;
      mountedRenderViewIdRef.current = null;
      mounted3dEngineRef.current = null;
      mounted3dViewIdRef.current = null;
      active3DLoadRequestRef.current += 1;
      active3DLoadQueueRef.current = Promise.resolve();
      setExternal3dEngineReady(false);
      setExternal3dEngineError(null);
      setMountedEngineVersion((value) => value + 1);
      try { registerMountedRuntimeEngine('fabric', null); } catch {}
      try { registerMountedRuntimeEngine('3d', null); } catch {}
      return;
    }

    const frame = canvasFrameRef.current;
    if (!frame) return;

    const prev = mountedRenderEngineRef.current;
    if (prev) {
      try { prev.lifecycle.unmount(); } catch {}
      mountedRenderEngineRef.current = null;
      mountedRenderViewIdRef.current = null;
    }

    const engineKind = activeRenderEngineKind === '3d' ? '3d' : 'fabric';
    const mountToken = engineMountTokenRef.current + 1;
    engineMountTokenRef.current = mountToken;
    const isCurrentMount = () => engineMountTokenRef.current === mountToken;
    try {
      if (typeof frame.replaceChildren === 'function') frame.replaceChildren();
      else frame.innerHTML = '';
    } catch {}
    const engineOptions = getRenderEngineOptionsForKind(engineKind, renderEngineOptionsByKind as any) as any;
    let cancelled = false;
    let mountedEngine: AsukaRenderEngine | null = null;
    let fallbackFabricRuntime: { engine: AsukaRenderEngine; unmount: () => void } | null = null;

    const getActiveViewContext = () => {
      const latestState = useAsukaStore.getState() as any;
      const viewId = latestState?.activeViewId ?? null;
      const view = latestState?.views?.find?.((entry: any) => String(entry?.id ?? '') === String(viewId ?? '')) ?? null;
      return { viewId, view, kind: engineKind as AsukaRenderEngineKind };
    };

    const mountEngine = async () => {
      setExternal3dEngineReady(false);
      setExternal3dEngineError(null);
      if (engineKind === '3d') {
        mounted3dEngineRef.current = null;
        mounted3dViewIdRef.current = null;
      }

      let resolvedEngine: AsukaRenderEngine | null = null;
      let resolveErrorMessage: string | null = null;
      try {
        resolvedEngine = await loadRenderEngineFromOptions(engineKind, engineOptions, getActiveViewContext());
      } catch (error) {
        if (engineKind === '3d' && !cancelled && isCurrentMount()) {
          resolveErrorMessage = error instanceof Error && error.message
            ? error.message
            : 'The 3D renderer could not be loaded.';
          setExternal3dEngineError(resolveErrorMessage);
        }
        resolvedEngine = null;
      }

      if (cancelled || !isCurrentMount()) return;

      if (!resolvedEngine && engineKind === 'fabric') {
        fallbackFabricRuntime = await mountBuiltInFabricRenderEngine(frame, engineOptions ?? { backgroundColor: '#ffffff' });
        if (cancelled || !isCurrentMount()) {
          try { fallbackFabricRuntime.unmount(); } catch {}
          return;
        }
        resolvedEngine = fallbackFabricRuntime.engine;
      }

      if (!resolvedEngine) {
        if (engineKind === '3d' && !resolveErrorMessage) {
          setExternal3dEngineError('The 3D renderer is not configured.');
        }
        return;
      }

      mountedEngine = resolvedEngine;
      try {
        await mountedEngine.lifecycle.mount(frame);
      } catch (error) {
        if (engineKind !== 'fabric' || fallbackFabricRuntime) throw error;
        try { mountedEngine.lifecycle.unmount(); } catch {}
        fallbackFabricRuntime = await mountBuiltInFabricRenderEngine(frame, engineOptions ?? { backgroundColor: '#ffffff' });
        if (cancelled || !isCurrentMount()) {
          try { fallbackFabricRuntime.unmount(); } catch {}
          return;
        }
        mountedEngine = fallbackFabricRuntime.engine;
        await mountedEngine.lifecycle.mount(frame);
      }
      if (cancelled || !isCurrentMount()) {
        try { mountedEngine.lifecycle.unmount(); } catch {}
        return;
      }

      registerMountedRuntimeEngine(engineKind, mountedEngine);
      mountedRenderEngineRef.current = mountedEngine;
      setMountedEngineVersion((value) => value + 1);

      if (engineKind === '3d') {
        // A disposed Babylon engine must never keep the next engine waiting on
        // its old loading chain. Request ids still prevent detached work from
        // publishing stale state after the replacement engine mounts.
        active3DLoadRequestRef.current += 1;
        active3DLoadQueueRef.current = Promise.resolve();
        mounted3dEngineRef.current = mountedEngine;
        setExternal3dEngineError(null);
        setExternal3dEngineReady(true);
        return;
      }

      const mountedViewId = String(getActiveViewContext().viewId ?? '').trim();
      if (mountedViewId) {
        mountedRenderViewIdRef.current = mountedViewId;
        await Promise.resolve(mountedEngine.lifecycle.activateView(mountedViewId, { mode: 'mount' }));
      }
    };

    void mountEngine().catch((error) => {
      if (!cancelled && isCurrentMount() && engineKind === '3d') {
        setExternal3dEngineReady(false);
        setExternal3dEngineError(
          error instanceof Error && error.message
            ? error.message
            : 'The 3D renderer could not be loaded.',
        );
      }
    });

    return () => {
      cancelled = true;
      if (isCurrentMount()) setExternal3dEngineReady(false);
      if (engineKind === '3d') {
        active3DLoadRequestRef.current += 1;
        active3DLoadQueueRef.current = Promise.resolve();
      }
      const restoringState = isStateRestoringNow();
      const currentStoreViewId = (useAsukaStore.getState() as any)?.activeViewId;
      const viewId = String(mounted3dViewIdRef.current ?? lastLoaded3dViewIdRef.current ?? currentStoreViewId ?? '').trim();
      if (!restoringState && engineKind === '3d' && mountedEngine && viewId) {
        const lastPersist = last3dSwitchPersistRef.current;
        const now = Date.now();
        const alreadyPersistedForSwitch = Boolean(lastPersist && lastPersist.viewId === viewId && now - lastPersist.at < 2500);
        if (!alreadyPersistedForSwitch) {
          try {
            const engineAtUnmount = mountedEngine;
            const scene = engineAtUnmount.lifecycle.exportView(viewId);
            void Promise.resolve(scene).then((exportedScene) => {
              try { setDocument3dSceneForView?.(viewId, exportedScene as any); } catch {}
              try {
                const track = engineAtUnmount.history.exportTrack(viewId);
                if (track) setDocument3dHistoryForView?.(viewId, track);
                const info = engineAtUnmount.history.getInfo();
                if (info) setDocumentHistoryInfo?.({ ...(info as any), viewId, engine: '3d', scope: 'view' });
              } catch {}
            });
          } catch {}
        }
      }
      if (!restoringState && engineKind === 'fabric') {
        const renderedViewId = String(mountedRenderViewIdRef.current ?? currentStoreViewId ?? '').trim();
        if (renderedViewId) {
          try { void Promise.resolve(mountedEngine?.lifecycle.deactivateView(renderedViewId)); } catch {}
        }
      }
      try { mountedEngine?.lifecycle.unmount(); } catch {}
      try { fallbackFabricRuntime?.unmount?.(); } catch {}
      if (isCurrentMount()) {
        if (engineKind === '3d' && mounted3dEngineRef.current === mountedEngine) mounted3dEngineRef.current = null;
        mounted3dViewIdRef.current = null;
        if (mountedRenderEngineRef.current === mountedEngine) mountedRenderEngineRef.current = null;
        mountedRenderViewIdRef.current = null;
        setMountedEngineVersion((value) => value + 1);
        try { registerMountedRuntimeEngine(engineKind, null); } catch {}
      }
    };
  }, [hasViews, activeRenderEngineKind, threeDOptions, renderEngineOptionsByKind, renderEngineRetryVersion, setDocument3dSceneForView, setDocument3dHistoryForView, setDocumentHistoryInfo]);



  useEffect(() => {
    if (activeRenderEngineKind !== '3d' || !external3dEngineReady || !activeViewId) return;
    const engine = mounted3dEngineRef.current;
    const requestedViewId = String(activeViewId ?? '').trim();
    if (!engine || !requestedViewId) return;

    // Babylon model loading is asynchronous. A second state restore or view
    // switch can arrive while the previous load is still resolving. Serialize
    // the work and invalidate stale requests so an older scene can never finish
    // last and overwrite the state that was just imported.
    const requestId = ++active3DLoadRequestRef.current;
    const restoreRevisionAtLoad = stateRestoreRevisionRef.current;
    const queuedLoad = active3DLoadQueueRef.current
      .catch(() => {})
      .then(async () => {
        if (requestId !== active3DLoadRequestRef.current) return;
        if (mounted3dEngineRef.current !== engine) return;

        const latestState = useAsukaStore.getState() as any;
        const latestViewId = String(latestState?.activeViewId ?? '').trim();
        const latestEngineKind = String(latestState?.activeRenderEngineKind ?? '').trim().toLowerCase();
        if (latestEngineKind !== '3d' || latestViewId !== requestedViewId) return;

        const latestViews = Array.isArray(latestState?.views) ? latestState.views : [];
        const latestElements = Array.isArray(latestState?.elements) ? latestState.elements : [];
        try { engine.products?.syncCatalog(latestElements); } catch {}
        const latestScenes = latestState?.document3dScenesByView ?? {};
        const view = latestViews.find((entry: any) => String(entry?.id ?? '') === requestedViewId) ?? null;
        const sceneState = resolve3DSceneForView(view, latestScenes as any, threeDOptions);
        mounted3dViewIdRef.current = requestedViewId;
        lastLoaded3dViewIdRef.current = requestedViewId;

        await Promise.resolve(engine.lifecycle.loadView(requestedViewId, sceneState));
        if (requestId !== active3DLoadRequestRef.current) return;
        if (mounted3dEngineRef.current !== engine) return;

        const latestAfterLoad = useAsukaStore.getState() as any;
        if (String(latestAfterLoad?.activeViewId ?? '').trim() !== requestedViewId) return;
        if (String(latestAfterLoad?.activeRenderEngineKind ?? '').trim().toLowerCase() !== '3d') return;

        const storedTrack = latestAfterLoad?.document3dHistoryByView?.[requestedViewId] ?? null;
        const storedLength = Array.isArray(storedTrack?.snapshots) ? storedTrack.snapshots.length : 0;
        if (storedTrack && storedLength > 0) {
          // Import the track so Undo/Redo keeps its stack, but do not replay the
          // current snapshot on mount/switch. The persisted scene is already the
          // source of truth for the active view.
          await engine.history.importTrack(requestedViewId, storedTrack);
        }
        const nextInfo = engine.history.getInfo();
        if (nextInfo) setDocumentHistoryInfo?.({ ...(nextInfo as any), viewId: requestedViewId, engine: '3d', scope: 'view' });
        const nextTrack = engine.history.exportTrack(requestedViewId);
        if (nextTrack) setDocument3dHistoryForView?.(requestedViewId, nextTrack);
        try {
          const nextScene = await Promise.resolve(engine.lifecycle.exportView(requestedViewId));
          if (requestId === active3DLoadRequestRef.current && nextScene) {
            setDocument3dSceneForView?.(requestedViewId, nextScene as any);
          }
        } catch {}
        const readyState = useAsukaStore.getState() as any;
        if (
          requestId === active3DLoadRequestRef.current
          && mounted3dEngineRef.current === engine
          && String(readyState?.activeViewId ?? '').trim() === requestedViewId
          && String(readyState?.activeRenderEngineKind ?? '').trim().toLowerCase() === '3d'
        ) {
          emitRenderEngineEvent({
            type: 'ready',
            action: 'view.activate',
            engine: '3d',
            viewId: requestedViewId,
          });
        }
      });
    active3DLoadQueueRef.current = queuedLoad.catch((error) => {
      if (
        requestId === active3DLoadRequestRef.current
        && mounted3dEngineRef.current === engine
      ) {
        emitRenderEngineEvent({
          type: 'error',
          action: 'view.activate',
          engine: '3d',
          viewId: requestedViewId,
          payload: {
            message: error instanceof Error ? error.message : String(error ?? 'Unknown 3D loading error.'),
          },
        });
      }
    });

    return () => {
      if (active3DLoadRequestRef.current === requestId) active3DLoadRequestRef.current += 1;
      if (stateRestoreRevisionRef.current !== restoreRevisionAtLoad) return;
      if (isStateRestoringNow()) return;
      try {
        void Promise.resolve(engine.lifecycle.exportView(requestedViewId)).then((scene) => {
          try { setDocument3dSceneForView?.(requestedViewId, scene as any); } catch {}
          try {
            const track = engine.history.exportTrack(requestedViewId);
            if (track) setDocument3dHistoryForView?.(requestedViewId, track);
            const info = engine.history.getInfo();
            if (info) setDocumentHistoryInfo?.({ ...(info as any), viewId: requestedViewId, engine: '3d', scope: 'view' });
          } catch {}
        });
      } catch {}
    };
  }, [activeRenderEngineKind, external3dEngineReady, activeViewId, threeDOptions, stateRestoreRevision, setDocument3dSceneForView, setDocument3dHistoryForView, setDocumentHistoryInfo]);


  useEffect(() => {
    if (activeRenderEngineKind !== '3d' || !activeViewId) return;
    let sceneTimer: number | null = null;
    let historyTimer: number | null = null;
    let pendingScene: { viewId: string; payload?: any } | null = null;
    let pendingHistory: { viewId: string; payload?: any } | null = null;
    const clearSceneTimer = () => {
      if (sceneTimer == null) return;
      try { window.clearTimeout(sceneTimer); } catch {}
      sceneTimer = null;
    };
    const clearHistoryTimer = () => {
      if (historyTimer == null) return;
      try { window.clearTimeout(historyTimer); } catch {}
      historyTimer = null;
    };
    const getPayloadViewId = (payload?: any) => String(payload?.viewId ?? payload?.from ?? mounted3dViewIdRef.current ?? lastLoaded3dViewIdRef.current ?? activeViewId ?? '').trim();
    const persistHistoryInfo = (viewId: string, payload?: any) => {
      if (isStateRestoringNow()) return;
      const engine = mounted3dEngineRef.current ?? getRuntimeRenderEngine('3d');
      try {
        const info = payload?.info && typeof payload.info === 'object'
          ? payload.info
          : (engine ? engine.history.getInfo() : null);
        asukaHistoryDebug('root.3d.persistHistory.info', { viewId, info: summarizeHistoryInfo(info) });
        if (info) setDocumentHistoryInfo?.({ ...(info as any), viewId, engine: '3d', scope: 'view' });
      } catch (error) {
        asukaHistoryDebug('root.3d.persistHistory.info.error', { viewId, error: String((error as any)?.message ?? error) });
      }
    };
    const persistHistoryTrackNow = (viewId: string, payload?: any) => {
      if (isStateRestoringNow()) return;
      const engine = mounted3dEngineRef.current ?? getRuntimeRenderEngine('3d');
      asukaHistoryDebug('root.3d.persistHistoryTrackNow.start', {
        viewId,
        activeViewId,
        hasEngine: Boolean(engine),
        payloadReason: payload?.reason ?? null,
      });
      if (!viewId) return;
      try {
        const payloadTrack = payload?.track && typeof payload.track === 'object' ? payload.track : null;
        const exportedTrack = payloadTrack
          ? null
          : (engine ? engine.history.exportTrack(viewId) : null);
        const track = payloadTrack ?? exportedTrack;
        asukaHistoryDebug('root.3d.persistHistory.track', {
          viewId,
          usingPayloadTrack: Boolean(payloadTrack),
          exportedTrack: summarizeHistoryTrack(exportedTrack),
          savedTrack: summarizeHistoryTrack(track),
        });
        if (track) setDocument3dHistoryForView?.(viewId, track);
      } catch (error) {
        asukaHistoryDebug('root.3d.persistHistory.track.error', { viewId, error: String((error as any)?.message ?? error) });
      }
    };
    const scheduleHistoryTrackPersist = (viewId: string, payload?: any, delay = 320) => {
      if (isStateRestoringNow()) return;
      if (!viewId) return;
      pendingHistory = { viewId, payload };
      clearHistoryTimer();
      try {
        historyTimer = window.setTimeout(() => {
          historyTimer = null;
          const next = pendingHistory;
          pendingHistory = null;
          if (next?.viewId) persistHistoryTrackNow(next.viewId, next.payload);
        }, delay) as unknown as number;
      } catch {
        persistHistoryTrackNow(viewId, payload);
      }
    };
    const flushHistoryTrackPersist = () => {
      if (isStateRestoringNow()) {
        pendingHistory = null;
        clearHistoryTimer();
        return;
      }
      const next = pendingHistory;
      pendingHistory = null;
      clearHistoryTimer();
      if (next?.viewId) {
        persistHistoryTrackNow(next.viewId, next.payload);
      }
    };
    const persistHistory = (payload?: any, options: { immediateTrack?: boolean } = {}) => {
      if (isStateRestoringNow()) return;
      const viewId = getPayloadViewId(payload);
      asukaHistoryDebug('root.3d.persistHistory.start', {
        activeViewId,
        mounted3dViewId: mounted3dViewIdRef.current,
        lastLoaded3dViewId: lastLoaded3dViewIdRef.current,
        payloadViewId: viewId,
        payloadAction: payload?.action ?? null,
        payloadReason: payload?.reason ?? null,
        payloadTrack: summarizeHistoryTrack(payload?.track),
        payloadInfo: summarizeHistoryInfo(payload?.info),
        immediateTrack: options.immediateTrack === true,
      });
      if (!viewId) {
        asukaHistoryDebug('root.3d.persistHistory.abort.noViewId', { payload });
        return;
      }
      persistHistoryInfo(viewId, payload);
      if (options.immediateTrack === true) persistHistoryTrackNow(viewId, payload);
      else scheduleHistoryTrackPersist(viewId, payload);
    };
    const persistSceneNow = (viewId: string, payload?: any) => {
      if (isStateRestoringNow()) return;
      const engine = mounted3dEngineRef.current ?? getRuntimeRenderEngine('3d');
      const endPersistSpan = beginAsuka3DPerfSpan('scene-persist', 'total', {
        viewId,
        action: payload?.action ?? null,
        reason: payload?.reason ?? null,
        hasEngine: Boolean(engine),
      });
      asukaHistoryDebug('root.3d.persistSceneNow.start', {
        viewId,
        activeViewId,
        hasEngine: Boolean(engine),
        payloadAction: payload?.action ?? null,
        payloadReason: payload?.reason ?? null,
      });
      if (!engine || !viewId) {
        endPersistSpan({ ok: false, skipped: true });
        return;
      }
      try {
        const endExportSpan = beginAsuka3DPerfSpan('scene-persist', 'export', { viewId });
        void Promise.resolve(engine.lifecycle.exportView(viewId)).then((scene) => {
          endExportSpan({ ok: true, scene: summarize3DScene(scene) });
          asukaHistoryDebug('root.3d.persistSceneNow.exported', { viewId, scene: summarize3DScene(scene) });
          const endStoreSpan = beginAsuka3DPerfSpan('scene-persist', 'store', { viewId });
          try {
            setDocument3dSceneForView?.(viewId, scene as any);
            endStoreSpan({ ok: true });
            endPersistSpan({ ok: true });
          } catch (error) {
            endStoreSpan({ ok: false, error }, { level: 'warn' });
            endPersistSpan({ ok: false, error }, { level: 'warn' });
            asukaHistoryDebug('root.3d.persistSceneNow.storeScene.error', { viewId, error: String((error as any)?.message ?? error) });
          }
        }).catch((error) => {
          endExportSpan({ ok: false, error }, { level: 'warn' });
          endPersistSpan({ ok: false, error }, { level: 'warn' });
          asukaHistoryDebug('root.3d.persistSceneNow.export.error', { viewId, error: String((error as any)?.message ?? error) });
        });
      } catch (error) {
        endPersistSpan({ ok: false, error }, { level: 'warn' });
        asukaHistoryDebug('root.3d.persistSceneNow.error', { viewId, error: String((error as any)?.message ?? error) });
      }
    };
    const scheduleScenePersist = (viewId: string, payload?: any, delay = 180) => {
      if (isStateRestoringNow()) return;
      if (!viewId) return;
      pendingScene = { viewId, payload };
      clearSceneTimer();
      try {
        sceneTimer = window.setTimeout(() => {
          sceneTimer = null;
          const next = pendingScene;
          pendingScene = null;
          if (next?.viewId) persistSceneNow(next.viewId, next.payload);
        }, delay) as unknown as number;
      } catch {
        persistSceneNow(viewId, payload);
      }
    };
    const flushScenePersist = () => {
      if (isStateRestoringNow()) {
        pendingScene = null;
        clearSceneTimer();
        return;
      }
      const next = pendingScene;
      pendingScene = null;
      clearSceneTimer();
      if (next?.viewId) {
        persistSceneNow(next.viewId, next.payload);
      }
    };
    const persistScene = (payload?: any) => {
      if (isStateRestoringNow()) return;
      const action = String(payload?.action ?? '').trim();
      if (action === 'loadView' || action === '3d:loadView') return;
      // Selection/focus/camera updates are viewport/UI state. Persisting the full
      // 3D scene for those transient actions rewrites tens of MB after every
      // pointerdown, which is exactly what delayed the first camera movement
      // after repeated variation changes. Real document mutations still persist.
      if (/^(?:3d:)?(?:selectObject|selectPart|focusPart|hoverObject|camera|viewport)$/i.test(action)) return;
      const reason = String(payload?.reason ?? '').trim();
      if (/^(?:camera|viewport|focusPart|selectPart|selectObject)$/i.test(reason)) return;
      const viewId = getPayloadViewId(payload);
      if (!viewId) return;
      scheduleScenePersist(viewId, payload);
    };
    const off0 = onAsukaEvent('before:views.switch', (payload?: any) => {
      if (isStateRestoringNow()) return;
      const viewId = String(payload?.from ?? mounted3dViewIdRef.current ?? lastLoaded3dViewIdRef.current ?? activeViewId ?? '').trim();
      asukaHistoryDebug('root.event.beforeViewsSwitch', {
        payload,
        resolvedFromViewId: viewId,
        mounted3dViewId: mounted3dViewIdRef.current,
        lastLoaded3dViewId: lastLoaded3dViewIdRef.current,
        activeViewId,
      });
      if (!viewId) return;
      flushScenePersist();
      flushHistoryTrackPersist();
      persistSceneNow(viewId, { ...(payload ?? {}), viewId, action: 'before:views.switch' });
      persistHistory({ ...(payload ?? {}), viewId }, { immediateTrack: true });
      last3dSwitchPersistRef.current = { viewId, at: Date.now() };
    });
    const persistRestoredHistory = (payload?: any) => {
      const viewId = getPayloadViewId(payload);
      if (!viewId) return;
      // Undo/redo changes the mounted Babylon scene through loadView(). Persist
      // both the track and the restored scene centrally so every history entry
      // point (top toolbar, module, API) leaves the store aligned with the engine.
      persistHistory(payload, { immediateTrack: true });
      persistSceneNow(viewId, payload);
    };
    const offEngine = createLiveModuleRenderEngine('3d').events.subscribe((event) => {
      const payload: Record<string, any> = {
        ...((event.payload && typeof event.payload === 'object') ? event.payload as Record<string, unknown> : {}),
        ...event,
      };
      if (event.type === 'history') {
        if (/history\.(?:undo|redo|reset|goto)$/i.test(String(event.action ?? ''))) persistRestoredHistory(payload);
        else persistHistory(payload, { immediateTrack: event.action === 'history.sync' });
        return;
      }
      if (event.type === 'viewport' && payload.camera && typeof payload.camera === 'object') {
        const viewId = getPayloadViewId(payload);
        if (viewId) setDocument3dCameraForView?.(viewId, payload.camera);
        return;
      }
      if (event.type === 'changed') persistScene(payload);
    });
    persistScene();
    persistHistory();
    return () => {
      flushScenePersist();
      flushHistoryTrackPersist();
      off0?.();
      offEngine?.();
    };
  }, [activeRenderEngineKind, activeViewId, setDocument3dCameraForView, setDocument3dSceneForView, setDocument3dHistoryForView, setDocumentHistoryInfo]);

  useEffect(() => {
    if (activeRenderEngineKind !== 'fabric' || !activeViewId) return;
    const viewId = String(activeViewId ?? '').trim();
    if (!viewId) return;
    const fabricEngine = mountedRenderEngineRef.current?.kind === 'fabric'
      ? mountedRenderEngineRef.current
      : getRuntimeRenderEngine('fabric');
    if (!fabricEngine || fabricEngine.capabilities.canvas2d !== true) return;

    const exportAndStoreFabricTrack = (reason: string, targetViewId = viewId) => {
      if (isStateRestoringNow()) return;
      try {
        const track = fabricEngine.history.exportTrack(targetViewId);
        asukaHistoryDebug('root.fabricHistory.persist', { reason, viewId: targetViewId, track: summarizeHistoryTrack(track) });
        if (track) setDocumentFabricHistoryForView?.(targetViewId, track as any);
      } catch (error) {
        asukaHistoryDebug('root.fabricHistory.persist.error', { reason, viewId: targetViewId, error: String((error as any)?.message ?? error) });
      }
    };

    const importStoredFabricTrack = (reason: string, targetViewId = viewId) => {
      if (isStateRestoringNow()) return;
      try {
        const store = useAsukaStore.getState() as any;
        const track = store?.documentFabricHistoryByView?.[DOCUMENT_FABRIC_HISTORY_KEY] ?? store?.documentFabricHistoryByView?.[targetViewId] ?? null;
        const local: any = fabricEngine.history.exportTrack(targetViewId);
        const trackLength = Array.isArray(track?.snapshots) ? track.snapshots.length : 0;
        const localLength = Array.isArray(local?.snapshots) ? local.snapshots.length : 0;
        const trackIndex = Number(track?.index ?? -1);
        const localIndex = Number(local?.index ?? -1);
        const shouldImport = trackLength > 0 && (localLength === 0 || trackLength > localLength || trackIndex !== localIndex);
        asukaHistoryDebug('root.fabricHistory.importCompare', {
          reason,
          viewId: targetViewId,
          storeTrack: summarizeHistoryTrack(track),
          localTrack: summarizeHistoryTrack(local),
          shouldImport,
        });
        if (shouldImport) void fabricEngine.history.importTrack(targetViewId, track).catch((error) => {
          asukaHistoryDebug('root.fabricHistory.import.error', { reason, viewId: targetViewId, error: String((error as any)?.message ?? error) });
        });
      } catch (error) {
        asukaHistoryDebug('root.fabricHistory.importCompare.error', { reason, viewId: targetViewId, error: String((error as any)?.message ?? error) });
      }
    };

    importStoredFabricTrack('active-view-mounted', viewId);
    exportAndStoreFabricTrack('active-view-mounted', viewId);

    const offBefore = onAsukaEvent('before:views.switch', (payload?: any) => {
      const fromViewId = String(payload?.from ?? '').trim();
      if (!fromViewId) return;
      exportAndStoreFabricTrack('before:views.switch', fromViewId);
    });
    const offAfter = onAsukaEvent('after:views.switch', (payload?: any) => {
      const toViewId = String(payload?.to ?? '').trim();
      const engine = String(payload?.engine ?? (useAsukaStore.getState() as any)?.activeRenderEngineKind ?? 'fabric').toLowerCase();
      if (!toViewId || engine === '3d') return;
      importStoredFabricTrack('after:views.switch', toViewId);
      exportAndStoreFabricTrack('after:views.switch', toViewId);
    });
    const offEngine = fabricEngine.events.subscribe((event) => {
      if (event.type !== 'changed' && event.type !== 'history') return;
      const targetViewId = String(event.viewId ?? (useAsukaStore.getState() as any)?.activeViewId ?? viewId).trim() || viewId;
      requestAnimationFrame(() => exportAndStoreFabricTrack(String(event.action ?? event.type), targetViewId));
    });
    return () => {
      exportAndStoreFabricTrack('effect.cleanup', viewId);
      offBefore?.();
      offAfter?.();
      offEngine?.();
    };
  }, [activeRenderEngineKind, activeViewId, mountedEngineVersion, setDocumentFabricHistoryForView]);

  // Full/document imports can replace the active Fabric view without changing
  // its id. In that case the regular activeViewId effect does not run again.
  // Rebuild after the store transaction and Preact paint have both completed.
  useEffect(() => {
    let restoreRequest = 0;
    const restoreFabricView = (payload?: any) => {
      if (payload?.history === true || String(payload?.options?.source ?? '').toLowerCase() === 'history') return;
      const request = ++restoreRequest;
      let attempt = 0;
      const raf = typeof requestAnimationFrame === 'function'
        ? requestAnimationFrame
        : ((cb: FrameRequestCallback) => setTimeout(() => cb(Date.now()), 16) as any);
      const run = () => {
        if (request !== restoreRequest) return;
        const latest = useAsukaStore.getState() as any;
        const viewId = String(latest?.activeViewId ?? '').trim();
        const engine = String(latest?.activeRenderEngineKind ?? 'fabric').trim().toLowerCase();
        if (!viewId || engine === '3d') return;
        // A full import can switch from Babylon to Fabric. Wait until the new
        // Fabric runtime is actually mounted instead of rebuilding against the
        // disposed canvas that existed before the import.
        const fabricEngine = getRuntimeRenderEngine('fabric');
        if ((!fabricEngine || fabricEngine.capabilities.canvas2d !== true) && attempt < 90) {
          attempt += 1;
          raf(() => run());
          return;
        }
        try { void Promise.resolve(fabricEngine?.lifecycle.activateView(viewId, { mode: 'restore' })); } catch {}
      };
      raf(() => run());
    };
    const off = onAsukaEvent('after:state.restore', restoreFabricView);
    return () => {
      restoreRequest += 1;
      off?.();
    };
  }, []);

  // Sync the document size to the active view without recreating the canvas.
  // IMPORTANT: We must NOT dispose/recreate the Fabric canvas when views change
  // (e.g. during state import). Recreating the instance can wipe the canvas right
  // after a snapshot is loaded, which looks like "content appears briefly then disappears".
  useEffect(() => {
    if (activeRenderEngineKind !== 'fabric' || !hasViews) return;
    const engine = getRuntimeRenderEngine('fabric');
    if (!engine || engine.capabilities.canvas2d !== true) return;
    try {
      const v = views.find((x) => x.id === activeViewId) ?? views[0];
      const vw = Math.max(1, Math.floor((v as any)?.width ?? 800));
      const vh = Math.max(1, Math.floor((v as any)?.height ?? 600));
      engine.viewport.setDocumentSize?.(vw, vh);
      engine.viewport.resize?.();
    } catch {}
  }, [hasViews, views, activeViewId, activeRenderEngineKind, mountedEngineVersion]);


  useEffect(() => {
    if (activeRenderEngineKind !== 'fabric' || !hasViews || !activeViewId || !bootReady) return;
    const engine = getRuntimeRenderEngine('fabric');
    if (!engine || engine.capabilities.canvas2d !== true) return;
    if (historyBootBaselineCapturedRef.current === engine) return;

    let cancelled = false;
    const rafIds: number[] = [];
    const schedule = (cb: FrameRequestCallback) => {
      if (typeof requestAnimationFrame === 'function') {
        const id = requestAnimationFrame(cb);
        rafIds.push(id);
        return id;
      }
      const id = setTimeout(() => cb((globalThis as any)?.performance?.now?.() ?? Date.now()), 16) as any as number;
      rafIds.push(id);
      return id;
    };

    schedule(() => {
      schedule(() => {
        if (cancelled || historyBootBaselineCapturedRef.current === engine) return;
        try {
          const historyByView = (useAsukaStore.getState() as any)?.documentFabricHistoryByView ?? {};
          const existingTrack = historyByView[DOCUMENT_FABRIC_HISTORY_KEY] ?? historyByView[activeViewId] ?? null;
          const existingLength = Array.isArray(existingTrack?.snapshots) ? existingTrack.snapshots.length : 0;
          asukaHistoryDebug('root.bootBaseline.check', {
            activeViewId,
            existingTrack: summarizeHistoryTrack(existingTrack),
            willCapture: existingLength <= 1,
          });
          if (existingLength <= 1) {
            void engine.history.commit({ force: true, reset: true, reason: 'boot-ready' }).catch(() => {});
          }
          historyBootBaselineCapturedRef.current = engine;
        } catch {}
      });
    });

    return () => {
      cancelled = true;
      for (const id of rafIds) {
        try {
          if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(id);
          else clearTimeout(id as any);
        } catch {}
      }
    };
  }, [hasViews, activeViewId, bootReady, activeRenderEngineKind, mountedEngineVersion]);


  // Match Fabric's fit-to-host rule for every engine: shrink responsively when
  // needed, but never enlarge beyond the configured document dimensions.
  useEffect(() => {
    const host = canvasHostRef.current;
    if (!host) return;

    const calc = () => {
      const styles = getComputedStyle(host);
      const contentWidth = host.clientWidth
        - (Number.parseFloat(styles.paddingLeft) || 0)
        - (Number.parseFloat(styles.paddingRight) || 0);
      const contentHeight = host.clientHeight
        - (Number.parseFloat(styles.paddingTop) || 0)
        - (Number.parseFloat(styles.paddingBottom) || 0);
      const v = views.find((x) => x.id === activeViewId) ?? views[0];
      const { w, h } = resolveCanvasFrameSize({
        availableWidth: contentWidth,
        availableHeight: contentHeight,
        viewWidth: (v as any)?.width,
        viewHeight: (v as any)?.height,
      });

      setCanvasFrameSize((prev) => (prev.w === w && prev.h === h ? prev : { w, h }));
    };

    let raf = 0;
    const ro = new ResizeObserver(() => {
      if (raf) cancelAnimationFrame(raf);
      raf = requestAnimationFrame(calc);
    });
    try {
      ro.observe(host);
    } catch {}

    calc();

    return () => {
      try {
        ro.disconnect();
      } catch {}
      if (raf) {
        try {
          cancelAnimationFrame(raf);
        } catch {}
      }
    };
  }, [views, activeViewId]);


  const blobToThumbnailDataUrl = useCallback(async (blob: Blob, maxWidth = 360, maxHeight = 240): Promise<string> => {
    if (!blob || blob.size <= 0) return '';
    try {
      if (typeof createImageBitmap === 'function') {
        const bitmap = await createImageBitmap(blob);
        try {
          const sourceWidth = Math.max(1, Number(bitmap.width) || 1);
          const sourceHeight = Math.max(1, Number(bitmap.height) || 1);
          const ratio = Math.min(1, maxWidth / sourceWidth, maxHeight / sourceHeight);
          const canvas = document.createElement('canvas');
          canvas.width = Math.max(1, Math.round(sourceWidth * ratio));
          canvas.height = Math.max(1, Math.round(sourceHeight * ratio));
          const ctx = canvas.getContext('2d');
          if (!ctx) return '';
          ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
          return canvas.toDataURL('image/png');
        } finally {
          try { bitmap.close(); } catch {}
        }
      }
    } catch {}

    return await new Promise<string>((resolve) => {
      try {
        const reader = new FileReader();
        reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
        reader.onerror = () => resolve('');
        reader.readAsDataURL(blob);
      } catch {
        resolve('');
      }
    });
  }, []);

  const captureThumbnailForView = useCallback(async (viewIdInput: string, multiplier = 0.25): Promise<void> => {
    const viewId = String(viewIdInput ?? '').trim();
    if (!viewId) return;
    const store = useAsukaStore.getState() as any;
    const engineKind = getActiveViewEngineKind(store?.views ?? views, viewId, store?.defaultRenderEngineKind ?? defaultRenderEngineKind);

    if (engineKind === '3d') {
      // Babylon only renders the mounted view. Inactive 3D views keep their last
      // captured thumbnail until they are mounted again.
      if (String(store?.activeViewId ?? activeViewIdRef.current ?? '').trim() !== viewId) return;
      const engine = mounted3dEngineRef.current ?? getRuntimeRenderEngine('3d');
      if (!engine) return;
      if (thumbnailCaptureRef.current.inFlight) {
        logAsuka3DPerf('thumbnail', 'capture-skipped-in-flight', { viewId }, { console: false });
        return;
      }
      const interactionAgeMs = Date.now() - Number(last3DInteractionAtRef.current || 0);
      if (interactionAgeMs >= 0 && interactionAgeMs < 900) {
        logAsuka3DPerf('thumbnail', 'capture-skipped-interaction', { viewId, interactionAgeMs }, { console: false });
        return;
      }
      const requestId = thumbnailCaptureRef.current.requestId + 1;
      thumbnailCaptureRef.current = { inFlight: true, requestId };
      const endSpan = beginAsuka3DPerfSpan('thumbnail', 'capture', {
        viewId,
        multiplier,
        requestId,
        interactionAgeMs,
      });
      try {
        const blob = await engine.exports.png({ format: 'png', scale: Math.min(0.2, Math.max(0.08, multiplier)), quality: 0.82 });
        const dataUrl = await blobToThumbnailDataUrl(blob, 320, 220);
        const latestActiveViewId = String((useAsukaStore.getState() as any)?.activeViewId ?? '').trim();
        if (dataUrl && latestActiveViewId === viewId && thumbnailCaptureRef.current.requestId === requestId) {
          setViewThumbnail(viewId, dataUrl);
        }
        endSpan({ ok: true, blobBytes: blob.size, dataUrlLength: dataUrl.length });
      } catch (error) {
        endSpan({ ok: false, error }, { level: 'warn' });
        // A WebGL frame may not be ready yet while the model is loading. The
        // next structural engine change schedules another idle capture.
      } finally {
        if (thumbnailCaptureRef.current.requestId === requestId) {
          thumbnailCaptureRef.current = { inFlight: false, requestId };
        }
      }
      return;
    }

    const engine = mountedRenderEngineRef.current?.kind === 'fabric'
      ? mountedRenderEngineRef.current
      : getRuntimeRenderEngine('fabric');
    if (!engine || engine.capabilities.canvas2d !== true) return;
    try {
      const blob = await engine.exports.png({ format: 'png', viewId, scale: multiplier, quality: 0.85 });
      const dataUrl = await blobToThumbnailDataUrl(blob, 360, 240);
      if (dataUrl) setViewThumbnail(viewId, dataUrl);
    } catch {
      // ignore
    }
  }, [blobToThumbnailDataUrl, defaultRenderEngineKind, setViewThumbnail, views]);

  const captureActiveThumbnail = useCallback((multiplier = 0.25) => {
    const viewId = String(activeViewIdRef.current ?? activeViewId ?? '').trim();
    if (!viewId) return;
    void captureThumbnailForView(viewId, multiplier);
  }, [activeViewId, captureThumbnailForView]);

  // Keep the latest activeViewId in a ref so timeout callbacks never use a stale closure.
  const activeViewIdRef = useRef<string | null>(null);
  useEffect(() => {
    activeViewIdRef.current = activeViewId;

    // If a thumbnail polling timeout is running and the user switches view,
    // stop it so the previous view cannot receive the new view's frame.
    if (thumbnailPollRef.current.timeoutId != null) {
      try { window.clearTimeout(thumbnailPollRef.current.timeoutId); } catch {}
      thumbnailPollRef.current.timeoutId = null;
    }
  }, [activeViewId]);

  const bumpThumbnailPoll = useCallback(() => {
    thumbnailPollRef.current.lastChange = Date.now();
    if (thumbnailPollRef.current.timeoutId != null) {
      try { window.clearTimeout(thumbnailPollRef.current.timeoutId); } catch {}
      thumbnailPollRef.current.timeoutId = null;
    }

    // WebGL readback must never compete with pointer/camera interaction. Wait
    // until the 3D canvas has been quiet, and keep only one capture in flight.
    const schedule = (delay = 1200) => {
      thumbnailPollRef.current.timeoutId = window.setTimeout(() => {
        thumbnailPollRef.current.timeoutId = null;
        const viewId = String(activeViewIdRef.current ?? '').trim();
        if (!viewId) return;
        const state = useAsukaStore.getState() as any;
        const kind = getActiveViewEngineKind(state?.views ?? views, viewId, state?.defaultRenderEngineKind ?? defaultRenderEngineKind);
        if (kind === '3d') {
          const interactionAgeMs = Date.now() - Number(last3DInteractionAtRef.current || 0);
          if (interactionAgeMs >= 0 && interactionAgeMs < 1000) {
            schedule(Math.max(250, 1100 - interactionAgeMs));
            return;
          }
          if (thumbnailCaptureRef.current.inFlight) {
            schedule(450);
            return;
          }
        }
        void captureThumbnailForView(viewId, kind === '3d' ? 0.16 : 0.2);
      }, Math.max(120, delay));
    };
    schedule();
  }, [captureThumbnailForView, defaultRenderEngineKind, views]);

  useEffect(() => () => {
    if (thumbnailPollRef.current.timeoutId != null) {
      try { window.clearTimeout(thumbnailPollRef.current.timeoutId); } catch {}
      thumbnailPollRef.current.timeoutId = null;
    }
  }, []);

  // Refresh thumbnails for a subset of views. Each renderer owns the details of
  // rendering its active or stored view.
  const subsetThumbRefreshRef = useRef<number | null>(null);
  const refreshSomeViewThumbnails = async (viewIds: string[], multiplier = 0.25) => {
    const ids = Array.isArray(viewIds) ? [...new Set(viewIds.map((value) => String(value ?? '').trim()).filter(Boolean))] : [];
    if (ids.length === 0) return;

    const store = useAsukaStore.getState() as any;
    const activeId = String(store?.activeViewId ?? activeViewIdRef.current ?? '').trim();
    for (const viewId of ids) {
      const kind = getActiveViewEngineKind(store?.views ?? views, viewId, store?.defaultRenderEngineKind ?? defaultRenderEngineKind);
      if (kind === '3d') {
        if (viewId === activeId) await captureThumbnailForView(viewId, multiplier);
      } else {
        await captureThumbnailForView(viewId, multiplier);
      }
    }
  };

  // Debounced subset refresh trigger.
  useEffect(() => {
    if (!thumbnailsRefreshTick) return;

    if (subsetThumbRefreshRef.current != null) {
      try { window.clearTimeout(subsetThumbRefreshRef.current); } catch {}
      subsetThumbRefreshRef.current = null;
    }

    subsetThumbRefreshRef.current = window.setTimeout(() => {
      void refreshSomeViewThumbnails(thumbnailsRefreshViewIds ?? [], 0.25);
    }, 120);

    return () => {
      if (subsetThumbRefreshRef.current != null) {
        try { window.clearTimeout(subsetThumbRefreshRef.current); } catch {}
        subsetThumbRefreshRef.current = null;
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [thumbnailsRefreshTick, mountedEngineVersion, external3dEngineReady]);

  // Always refresh the active view shortly after its engine finishes mounting.
  useEffect(() => {
    if (!activeViewId) return;
    if (activeRenderEngineKind === 'fabric' && !getRuntimeRenderEngine('fabric')) return;
    if (activeRenderEngineKind === '3d' && !external3dEngineReady) return;
    const delay = activeRenderEngineKind === '3d' ? 1400 : 220;
    const timeoutId = window.setTimeout(() => captureActiveThumbnail(0.25), delay);
    return () => window.clearTimeout(timeoutId);
  }, [activeViewId, activeRenderEngineKind, captureActiveThumbnail, mountedEngineVersion, external3dEngineReady, listenersTick]);

  // Babylon thumbnails are updated after document mutations and history restores.
  // Camera motion is intentionally excluded: preview readback must never stall orbiting.
  useEffect(() => {
    if (activeRenderEngineKind !== '3d' || !external3dEngineReady) return;
    const schedule = (payload?: any) => {
      const payloadViewId = String(payload?.viewId ?? activeViewIdRef.current ?? '').trim();
      const currentViewId = String(activeViewIdRef.current ?? '').trim();
      if (!currentViewId || (payloadViewId && payloadViewId !== currentViewId)) return;
      bumpThumbnailPoll();
    };
    const offEngine = createLiveModuleRenderEngine('3d').events.subscribe((event) => {
      if (event.type !== 'changed' && event.type !== 'history') return;
      schedule({ ...((event.payload && typeof event.payload === 'object') ? event.payload as Record<string, unknown> : {}), ...event });
    });
    schedule({ viewId: activeViewId });
    return () => { try { offEngine?.(); } catch {} };
  }, [activeRenderEngineKind, external3dEngineReady, activeViewId, bumpThumbnailPoll]);
  useEffect(() => {
    if (activeRenderEngineKind !== '3d') return;

    const resolveSelected3DObject = (payload?: any) => {
      const engine = mounted3dEngineRef.current ?? getRuntimeRenderEngine('3d');
      const objects = engine?.objects.list() ?? [];
      const id = String(payload?.id ?? payload?.objectId ?? '').trim();
      const payloadObject = (payload?.object ?? payload?.selectedObject ?? payload?.selected ?? null) as any;

      if (payloadObject && typeof payloadObject === 'object') {
        const payloadId = String(payloadObject?.id ?? '').trim();
        if (!id || !payloadId || payloadId === id) return payloadObject;
      }

      if (id) {
        const exact = objects.find((obj: any) => String(obj?.id ?? '') === id) ?? null;
        if (exact) return exact;
        // Layers may fire the menu request immediately after selectObject(). If the
        // engine has not refreshed its summaries yet, still open the toolbar from
        // the layer payload instead of closing it as a missing selection.
        if (String(payload?.source ?? '').trim() === 'layers' || payload?.forceOpen === true) {
          return {
            id,
            kind: String(payload?.kind ?? payload?.type ?? 'object'),
            label: String(payload?.label ?? id),
            selected: true,
            locked: payload?.locked === true,
            visible: payload?.visible !== false,
            metadata: {
              ...((payload?.metadata && typeof payload.metadata === 'object') ? payload.metadata : {}),
              ...(payload?.screenBox ? { screenBox: payload.screenBox } : null),
            },
          };
        }
      }

      return objects.find((obj: any) => !!obj?.selected) ?? null;
    };

    const refresh3DSelectionAnchor = (payload?: any) => {
      const selected = resolveSelected3DObject(payload);
      if (!selected) return;
      const kind = String(selected?.kind ?? selected?.type ?? '').toLowerCase();
      if (kind === 'product-3d') return;
      const anchor = get3DSelectionAnchorBox(payload, selected);
      setSelectionBox(anchor);
      setSelectionMenuAnchorBox?.(anchor);
    };

    const handleEngineSelection = (payload?: any) => {
      const action = String(payload?.action ?? '').trim();
      if (action === 'selectObject' || action === '3d:selectObject') {
        const selected = resolveSelected3DObject(payload);
        open3DSelectionMenuForObject(selected, payload);
        return;
      }
      if (action === 'updateObject' || action === '3d:updateObject') {
        refresh3DSelectionAnchor(payload);
      }
    };

    const handleSelectionMenuRequest = (payload?: any) => {
      if (String(payload?.engine ?? '').trim() !== '3d') return;
      const selected = resolveSelected3DObject(payload);
      open3DSelectionMenuForObject(selected, payload);
    };

    const handleCameraChanged = (payload?: any) => {
      refresh3DSelectionAnchor(payload);
      if (String(payload?.action ?? '').trim() !== 'camera.limits') return;
      const viewId = String(payload?.viewId ?? activeViewId ?? '').trim();
      if (!viewId) return;
      const state = useAsukaStore.getState() as any;
      const previous = state?.document3dScenesByView?.[viewId] ?? { products: [] };
      try {
        setDocument3dSceneForView?.(viewId, {
          ...previous,
          ...(payload?.camera ? { camera: payload.camera } : null),
          ...(payload?.cameraLimits ? { cameraLimits: payload.cameraLimits } : null),
        });
      } catch {}
    };

    const handleProductPartClick = (payload?: any) => {
      if (!['3d', 'babylon'].includes(String(payload?.engine ?? '').trim())) return;
      if (payload?.viewId && String(payload.viewId) !== String(activeViewId)) return;
      setSelectionBox(null);
      setSelectionMenuItems([] as any);
      setSelectionMenuAnchorBox?.(null);
      resetSelectionMenuNav();
      closeSelectionMenu?.();
      open3DProductVariationPicker(payload?.selected ?? payload, { focusOnOpen: false, source: 'canvas-product-part-click' });
    };

    const off1 = createLiveModuleRenderEngine('3d').events.subscribe((event) => {
      const payload = {
        ...((event.payload && typeof event.payload === 'object') ? event.payload as Record<string, unknown> : {}),
        ...event,
      };
      if (event.type === 'viewport') handleCameraChanged(payload);
      else if (event.type === 'part-activated') handleProductPartClick(payload);
      else if (event.type === 'selection' || event.type === 'changed') handleEngineSelection(payload);
    });
    const off2 = onAsukaEvent('selection.request.menu', handleSelectionMenuRequest);
    return () => { off1?.(); off2?.(); };
  }, [activeRenderEngineKind, activeViewId, closeSelectionMenu, get3DSelectionAnchorBox, open3DProductVariationPicker, open3DSelectionMenuForObject, resetSelectionMenuNav, setDocument3dSceneForView, setSelectionMenuAnchorBox, setSelectionMenuItems]);

  useEffect(() => {
    if (activeRenderEngineKind !== '3d') return;
    const isTypingTarget = (target: EventTarget | null): boolean => {
      const el = target as HTMLElement | null;
      if (!el) return false;
      const tag = String(el.tagName ?? '').toLowerCase();
      if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
      if ((el as any).isContentEditable === true) return true;
      return !!el.closest?.('input, textarea, select, [contenteditable="true"]');
    };
    const getSelected3DObject = (engine: any): any | null => {
      const selected = engine?.selection.list()?.[0] ?? null;
      if (selected) return selected;
      const objects = engine?.objects.list() ?? [];
      return objects.find((object: any) => object?.selected) ?? null;
    };
    const onKeyDown = (event: KeyboardEvent) => {
      if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
      if (event.altKey || event.metaKey) return;
      if (isTypingTarget(event.target)) return;
      const state = useAsukaStore.getState() as any;
      const perms = state?.permissions ?? permissions ?? DEFAULT_PERMISSIONS;
      const canMoveElements = (perms as any)?.canMoveElements !== false && (perms as any)?.canEditElements !== false && (perms as any)?.canMoveObject !== false;
      if (!canMoveElements) return;
      const engine = mounted3dEngineRef.current ?? getRuntimeRenderEngine('3d');
      if (!engine) return;
      const selected = getSelected3DObject(engine as any);
      const selectedId = String(selected?.id ?? '').trim();
      const selectedKind = String(selected?.kind ?? selected?.type ?? '').toLowerCase();
      const isProduct = selectedId.startsWith('product:') || selectedKind === 'product-3d';
      if (!isProduct || !selectedId || selected?.locked === true) return;
      const baseStep = event.shiftKey ? 0.24 : 0.08;
      const patch: Record<string, number> = {};
      if (event.ctrlKey) {
        if (event.key === 'ArrowLeft') patch.deltaX = -baseStep;
        if (event.key === 'ArrowRight') patch.deltaX = baseStep;
        if (event.key === 'ArrowUp') patch.deltaY = baseStep;
        if (event.key === 'ArrowDown') patch.deltaY = -baseStep;
      } else {
        if (event.key === 'ArrowLeft') patch.deltaX = -baseStep;
        if (event.key === 'ArrowRight') patch.deltaX = baseStep;
        if (event.key === 'ArrowUp') patch.deltaZ = -baseStep;
        if (event.key === 'ArrowDown') patch.deltaZ = baseStep;
      }
      if (!Object.keys(patch).length) return;
      event.preventDefault();
      event.stopPropagation();
      pendingKeyboardMoveId = selectedId;
      void engine.objects.updateTransform(selectedId, patch, { source: 'keyboard.product-element-nudge', transient: true, commit: false, persist: false }).catch(() => {});
      scheduleKeyboardMoveCommit();
    };
    const commitKeyboardMove = () => {
      const id = String(pendingKeyboardMoveId ?? '').trim();
      pendingKeyboardMoveId = '';
      if (keyboardMoveTimer != null) {
        try { window.clearTimeout(keyboardMoveTimer); } catch {}
        keyboardMoveTimer = null;
      }
      if (!id) return;
      const engine = mounted3dEngineRef.current ?? getRuntimeRenderEngine('3d');
      void engine?.objects.updateTransform(id, {}, { source: 'keyboard.product-element-nudge.commit' }).catch(() => {});
    };
    function scheduleKeyboardMoveCommit() {
      if (keyboardMoveTimer != null) {
        try { window.clearTimeout(keyboardMoveTimer); } catch {}
      }
      keyboardMoveTimer = window.setTimeout(commitKeyboardMove, 260) as unknown as number;
    }
    const onKeyUp = (event: KeyboardEvent) => {
      if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return;
      commitKeyboardMove();
    };
    let pendingKeyboardMoveId = '';
    let keyboardMoveTimer: number | null = null;
    try { document.addEventListener('keydown', onKeyDown, true); } catch {}
    try { document.addEventListener('keyup', onKeyUp, true); } catch {}
    try { window.addEventListener('blur', commitKeyboardMove, true); } catch {}
    return () => {
      commitKeyboardMove();
      try { document.removeEventListener('keydown', onKeyDown, true); } catch {}
      try { document.removeEventListener('keyup', onKeyUp, true); } catch {}
      try { window.removeEventListener('blur', commitKeyboardMove, true); } catch {}
    };
  }, [activeRenderEngineKind, permissions]);


  useEffect(() => {
    setCanvasHover(null);
    const tooltip = canvasHover3DTooltipRef.current;
    const labelNode = canvasHover3DTooltipLabelRef.current;
    if (tooltip) {
      tooltip.style.display = 'none';
      tooltip.classList.remove('is-visible');
    }
    if (labelNode) labelNode.textContent = '';
  }, [activeRenderEngineKind, activeViewId]);

  useEffect(() => {
    if (activeRenderEngineKind !== '3d') return;
    let lastHoverSignature = '';

    const hideTooltip = () => {
      const tooltip = canvasHover3DTooltipRef.current;
      const labelNode = canvasHover3DTooltipLabelRef.current;
      if (tooltip) {
        tooltip.style.display = 'none';
        tooltip.classList.remove('is-visible');
        tooltip.setAttribute('aria-hidden', 'true');
      }
      if (labelNode) labelNode.textContent = '';
    };

    const positionTooltip = (payload?: any) => {
      const rawX = Number(payload?.clientX ?? payload?.x ?? payload?.pointerX);
      const rawY = Number(payload?.clientY ?? payload?.y ?? payload?.pointerY);
      if (!Number.isFinite(rawX) || !Number.isFinite(rawY)) return;
      last3DInteractionAtRef.current = Date.now();
      const tooltip = canvasHover3DTooltipRef.current;
      if (!tooltip) return;
      tooltip.style.left = `${rawX}px`;
      tooltip.style.top = `${Math.max(10, rawY - 14)}px`;
    };

    const off = onAsukaEvent('engine3d.hover.changed', (payload?: any) => {
      const receivedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
      last3DInteractionAtRef.current = Date.now();
      const active = payload?.active !== false && payload?.target != null;
      const label = String(payload?.label ?? payload?.target?.label ?? '').trim();
      const targetKey = String(payload?.target?.meshId ?? payload?.target?.productId ?? payload?.target?.productPartId ?? '').trim();
      const signature = `${label}|${targetKey}`;
      if (!active || !label) {
        logAsuka3DPerf('hover-ui', 'changed-received-clear', { previousSignature: lastHoverSignature, payload });
        lastHoverSignature = '';
        hideTooltip();
        return;
      }
      positionTooltip(payload);
      if (signature === lastHoverSignature) {
        logAsuka3DPerf('hover-ui', 'changed-duplicate', { signature, label, targetKey }, { console: false });
        return;
      }
      const previousSignature = lastHoverSignature;
      lastHoverSignature = signature;
      const frameRect = canvasFrameRef.current?.getBoundingClientRect?.() ?? null;
      const rawX = Number(payload?.clientX ?? payload?.x ?? payload?.pointerX);
      const rawY = Number(payload?.clientY ?? payload?.y ?? payload?.pointerY);
      const clientX = Number.isFinite(rawX) ? rawX : (frameRect ? frameRect.left + frameRect.width / 2 : window.innerWidth / 2);
      const clientY = Number.isFinite(rawY) ? rawY : (frameRect ? frameRect.top + frameRect.height / 2 : window.innerHeight / 2);
      const tooltip = canvasHover3DTooltipRef.current;
      const labelNode = canvasHover3DTooltipLabelRef.current;
      if (labelNode) labelNode.textContent = label;
      if (tooltip) {
        tooltip.style.left = `${clientX}px`;
        tooltip.style.top = `${Math.max(10, clientY - 14)}px`;
        tooltip.style.display = 'block';
        tooltip.classList.add('is-visible');
        tooltip.setAttribute('aria-hidden', 'false');
      }
      logAsuka3DPerf('hover-ui', 'dom-updated', {
        previousSignature,
        signature,
        label,
        targetKey,
        clientX,
        clientY,
        eventToDomMs: (typeof performance !== 'undefined' ? performance.now() : Date.now()) - receivedAt,
      }, { console: false });
    });
    const offMove = onAsukaEvent('engine3d.hover.move', (payload?: any) => {
      positionTooltip(payload);
    });
    return () => {
      off?.();
      offMove?.();
      hideTooltip();
    };
  }, [activeRenderEngineKind]);

  useEffect(() => {
    if (activeRenderEngineKind === '3d') return;
    const engine = getRuntimeRenderEngine('fabric');
    const canvas2d = engine?.canvas2d;
    if (!engine || !canvas2d || engine.capabilities.canvas2d !== true) {
      setSelectionBox(null);
      setIsTransformingSelection(false);
      return;
    }

    const DEFAULT_SELECTION_ITEMS: ModuleId[] = ['colors', 'layers', 'history'];
    const inferSelectionMenuItems = (active: AsukaObjectSummary | null): ModuleId[] => {
      if (!active) return DEFAULT_SELECTION_ITEMS;
      const metadata = active.metadata ?? {};
      const group = String(metadata.itemGroup ?? '').toLowerCase();
      const type = String(metadata.type ?? active.kind ?? '').toLowerCase();
      const isText = metadata.textLike === true
        || group === 'text'
        || ['text', 'textbox', 'i-text', 'itext'].includes(type)
        || typeof active.pricing?.text === 'string'
        || typeof (metadata as any).text === 'string'
        || typeof (metadata as any).fontFamily === 'string';
      const isImage = metadata.imageLike === true || group === 'image' || group === 'images' || type === 'image';
      const isSvgLike = type === 'group' || type === 'path-group' || type.includes('path');
      const isDesign = metadata.designLike === true || group === 'design' || group === 'designs' || isSvgLike;
      const items: ModuleId[] = [];

      if (isText) items.push('font-family' as ModuleId, 'text-formatting' as ModuleId, 'text-curve' as ModuleId, 'colors' as ModuleId);
      if (isImage) items.push('studio' as ModuleId);
      if (isDesign && !isText) items.push('colors' as ModuleId, 'studio' as ModuleId);
      if (!isText && !isImage && ['path', 'polygon', 'polyline', 'line', 'rect', 'circle', 'ellipse', 'triangle', 'group', 'path-group'].some((value) => type === value || type.includes('path'))) {
        items.push('colors' as ModuleId);
      }
      if (hasModule('transform' as ModuleId)) items.push('transform' as ModuleId);
      if (hasModule('position' as ModuleId)) items.push('position' as ModuleId);
      return Array.from(new Set(items));
    };

    const updateFromSelection = (activeInput?: AsukaObjectSummary | null, anchorInput?: any) => {
      const active = activeInput ?? engine.selection.list()[0] ?? null;
      if (!active) {
        setSelectionBox(null);
        return;
      }
      const printingMethodId = String(active.metadata?.printingMethodId ?? '').trim();
      if (printingMethodId) setActivePrintingMethodId(printingMethodId);
      const anchor = anchorInput ?? getRenderEngineSelectionAnchorBox(engine);
      if (!anchor) {
        setSelectionBox(null);
        return;
      }
      setSelectionBox({ left: anchor.left, top: anchor.top, width: anchor.width, height: anchor.height });
    };

    const handleInteraction = (event: AsukaCanvas2DInteractionEvent) => {
      if (event.type === 'hover') {
        if (event.phase === 'clear' || !event.object || !event.pointer) {
          setCanvasHover((current) => current?.engine === 'fabric' ? null : current);
          return;
        }
        const label = String(event.object.label ?? event.object.metadata?.elementName ?? event.object.id ?? 'Element').trim() || 'Element';
        setCanvasHover({
          engine: 'fabric',
          label,
          clientX: event.pointer.x,
          clientY: event.pointer.y,
        });
        return;
      }

      if (event.type === 'selection') {
        if (event.phase === 'clear' || !event.object) {
          setSelectionBox(null);
          setSelectionMenuItems([] as any);
          resetSelectionMenuNav();
          return;
        }
        setCanvasHover((current) => current?.engine === 'fabric' ? null : current);
        setIsTransformingSelection(false);
        setSelectionMenuItems(inferSelectionMenuItems(event.object));
        openSelectionMenu();
        updateFromSelection(event.object, event.anchor);
        return;
      }

      if (event.type === 'transform') {
        if (event.phase === 'update' && event.userInitiated) {
          setIsTransformingSelection(true);
          setSelectionBox(null);
          return;
        }
        if (event.phase === 'end') {
          setIsTransformingSelection(false);
          bumpCanvasChangeTick?.();
          bumpThumbnailPoll();
          updateFromSelection(event.object, event.anchor);
        }
        return;
      }

      if (event.type === 'object') {
        bumpCanvasChangeTick?.();
        bumpThumbnailPoll();
        return;
      }

      if (event.type === 'viewport' && event.object) updateFromSelection(event.object, event.anchor);
    };

    // Hover feedback lives in the DOM overlay. Mutating a Fabric object's
    // opacity here forces an extra canvas render and causes a visible blink.
    const unsubscribe = canvas2d.subscribeInteractions(handleInteraction, { dimHoveredObject: false });
    const handleSelectionMenuRequest = (payload?: any) => {
      if (payload?.engine && String(payload.engine) !== 'fabric') return;
      const active = engine.selection.list()[0] ?? null;
      if (!active) return;
      setSelectionMenuItems(inferSelectionMenuItems(active));
      resetSelectionMenuNav();
      openSelectionMenu();
      updateFromSelection(active);
    };
    const offSelectionMenuRequest = onAsukaEvent('selection.request.menu', handleSelectionMenuRequest);
    updateFromSelection();

    return () => {
      unsubscribe?.();
      offSelectionMenuRequest?.();
      setCanvasHover((current) => current?.engine === 'fabric' ? null : current);
    };
  }, [activeRenderEngineKind, bumpCanvasChangeTick, bumpThumbnailPoll, mountedEngineVersion, listenersTick, openSelectionMenu, resetSelectionMenuNav, setSelectionMenuItems]);


  // --- Magnifier overlay (action module) ---
  useEffect(() => {
    if (activeRenderEngineKind === '3d') return;
    const overlay = magnifierOverlayRef.current;
    const engine = getRuntimeRenderEngine('fabric');
    const canvas2d = engine?.canvas2d;
    if (!overlay || !engine || !canvas2d || engine.capabilities.canvas2d !== true) return;

    return canvas2d.attachMagnifier({
      overlay,
      enabled: Boolean(magnifierEnabled),
      size: Number(magnifierSize) || 140,
      zoom: Number(magnifierZoom) || 2,
      onStateChange: (state) => {
        setMagnifierActive?.(state.active);
        setMagnifierPosition?.(state.x, state.y);
      },
    });
  }, [
    activeRenderEngineKind,
    mountedEngineVersion,
    magnifierEnabled,
    magnifierSize,
    magnifierZoom,
    setMagnifierActive,
    setMagnifierPosition,
  ]);




  const canUseUndo = Boolean((permissions as any)?.canUndo);
  const canUseRedo = Boolean((permissions as any)?.canRedo);
  const canUseReset = Boolean((permissions as any)?.canReset);
  const live3DHistoryInfo = (() => {
    if (String(activeRenderEngineKind) !== '3d') return null;
    try {
      const engine = mounted3dEngineRef.current ?? getRuntimeRenderEngine('3d');
      const info = engine?.history.getInfo() ?? null;
      const infoViewId = String((info as any)?.viewId ?? '').trim();
      if (info && (!infoViewId || infoViewId === String(activeViewId ?? '').trim())) return info;
    } catch {}
    return null;
  })();
  const effectiveHistoryInfo = (() => {
    if (String(activeRenderEngineKind) === '3d') {
      if (live3DHistoryInfo) {
        const length = Math.max(0, Number((live3DHistoryInfo as any)?.length ?? 0) || 0);
        const index = length > 0 ? Math.max(0, Math.min(length - 1, Number((live3DHistoryInfo as any)?.index ?? length - 1) || 0)) : -1;
        return {
          ...(documentHistoryInfo as any ?? {}),
          ...(live3DHistoryInfo as any),
          engine: '3d',
          scope: 'view',
          viewId: activeViewId,
          index,
          length,
          canUndo: index > 0,
          canRedo: index >= 0 && index < length - 1,
          maxSnapshots: Number((live3DHistoryInfo as any)?.maxSnapshots ?? (documentHistoryInfo as any)?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
        };
      }
      if (active3dHistoryTrack && Array.isArray((active3dHistoryTrack as any).snapshots) && (active3dHistoryTrack as any).snapshots.length > 0) {
        const length = Number((active3dHistoryTrack as any).snapshots.length ?? 0) || 0;
        const index = Math.max(0, Math.min(length - 1, Number((active3dHistoryTrack as any).index ?? length - 1) || 0));
        return {
          ...(documentHistoryInfo as any ?? {}),
          engine: '3d',
          scope: 'view',
          viewId: activeViewId,
          index,
          length,
          canUndo: index > 0,
          canRedo: index < length - 1,
          maxSnapshots: Number((active3dHistoryTrack as any).maxSnapshots ?? (documentHistoryInfo as any)?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
        };
      }
      return {
        ...(documentHistoryInfo as any ?? {}),
        engine: '3d',
        scope: 'view',
        viewId: activeViewId,
        index: -1,
        length: 0,
        canUndo: false,
        canRedo: false,
        maxSnapshots: Number((documentHistoryInfo as any)?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
      };
    }
    if (activeFabricHistoryTrack && Array.isArray((activeFabricHistoryTrack as any).snapshots) && (activeFabricHistoryTrack as any).snapshots.length > 0) {
      const length = Number((activeFabricHistoryTrack as any).snapshots.length ?? 0) || 0;
      const index = Math.max(0, Math.min(length - 1, Number((activeFabricHistoryTrack as any).index ?? length - 1) || 0));
      return {
        ...(documentHistoryInfo as any ?? {}),
        engine: 'fabric',
        scope: String((activeFabricHistoryTrack as any)?.scope ?? '').trim() === 'document' ? 'document' : 'view',
        viewId: String((activeFabricHistoryTrack as any)?.scope ?? '').trim() === 'document' ? DOCUMENT_FABRIC_HISTORY_KEY : activeViewId,
        index,
        length,
        canUndo: index > 0,
        canRedo: index < length - 1,
        maxSnapshots: Number((activeFabricHistoryTrack as any).maxSnapshots ?? (documentHistoryInfo as any)?.maxSnapshots ?? DEFAULT_HISTORY_MAX_SNAPSHOTS) || DEFAULT_HISTORY_MAX_SNAPSHOTS,
      };
    }
    return documentHistoryInfo as any;
  })();
  const canUndoHistory = Boolean((effectiveHistoryInfo as any)?.canUndo);
  const canRedoHistory = Boolean((effectiveHistoryInfo as any)?.canRedo);
  const hasHistory = Number((effectiveHistoryInfo as any)?.length ?? 0) > 0;

  useEffect(() => {
    asukaHistoryDebug('root.effectiveHistoryInfo', {
      activeViewId,
      activeRenderEngineKind,
      documentHistoryInfo: summarizeHistoryInfo(documentHistoryInfo),
      active3dHistoryTrack: summarizeHistoryTrack(active3dHistoryTrack),
      activeFabricHistoryTrack: summarizeHistoryTrack(activeFabricHistoryTrack),
      effectiveHistoryInfo: summarizeHistoryInfo(effectiveHistoryInfo),
      hasHistory,
      canUndoHistory,
      canRedoHistory,
    });
  }, [activeViewId, activeRenderEngineKind, documentHistoryInfo, active3dHistoryTrack, activeFabricHistoryTrack, effectiveHistoryInfo, hasHistory, canUndoHistory, canRedoHistory]);

  const resolveActiveHistoryEngine = () => {
    const kind = String((useAsukaStore.getState() as any)?.activeRenderEngineKind ?? activeRenderEngineKind ?? 'fabric') === '3d' ? '3d' : 'fabric';
    if (kind === '3d') return mounted3dEngineRef.current ?? getRuntimeRenderEngine('3d');
    return getRuntimeRenderEngine('fabric') ?? null;
  };

  const persist3DHistoryAndSceneAfterHistoryAction = async (engine: any, viewId: string) => {
    asukaHistoryDebug('root.historyAction.persistAfter.start', { viewId, hasEngine: Boolean(engine) });
    if (!engine || !viewId) return;
    const store = useAsukaStore.getState() as any;
    try {
      const info = engine.history.getInfo();
      if (info) store?.setDocumentHistoryInfo?.({ ...(info as any), viewId, engine: '3d', scope: 'view' });
      const track = engine.history.exportTrack(viewId);
      if (track) store?.setDocument3dHistoryForView?.(viewId, track);
      const scene = await Promise.resolve(engine.lifecycle.exportView(viewId));
      if (scene) store?.setDocument3dSceneForView?.(viewId, scene);
      asukaHistoryDebug('root.historyAction.persistAfter.done', {
        viewId,
        info: summarizeHistoryInfo(info),
        track: summarizeHistoryTrack(track),
        scene: summarize3DScene(scene),
      });
    } catch (error) {
      asukaHistoryDebug('root.historyAction.persistAfter.error', { viewId, error: String((error as any)?.message ?? error) });
    }
  };

  const ensure3DHistoryTrackIsMounted = (engine: any, viewId: string) => {
    asukaHistoryDebug('root.ensure3DHistoryTrackIsMounted.start', { viewId, hasEngine: Boolean(engine) });
    if (!engine || !viewId) return;
    let engineInfo: any = null;
    let engineLength = 0;
    let engineIndex = -1;
    let engineMatchesView = false;
    try {
      engineInfo = engine.history.getInfo();
      engineLength = Number((engineInfo as any)?.length ?? 0) || 0;
      engineIndex = Number((engineInfo as any)?.index ?? -1);
      const infoViewId = String((engineInfo as any)?.viewId ?? '').trim();
      engineMatchesView = engineLength > 0 && (!infoViewId || infoViewId === viewId);
    } catch (error) {
      asukaHistoryDebug('root.ensure3DHistoryTrackIsMounted.engineInfo.error', { viewId, error: String((error as any)?.message ?? error) });
    }
    try {
      const track = (useAsukaStore.getState() as any)?.document3dHistoryByView?.[viewId] ?? null;
      const trackLength = Array.isArray((track as any)?.snapshots) ? Number((track as any).snapshots.length) || 0 : 0;
      const trackIndex = Number((track as any)?.index ?? -1);
      const shouldImportStoredTrack = trackLength > 0 && !engineMatchesView;
      asukaHistoryDebug('root.ensure3DHistoryTrackIsMounted.compare', {
        viewId,
        engineInfo: summarizeHistoryInfo(engineInfo),
        engineLength,
        engineIndex,
        engineMatchesView,
        trackIndex,
        storeTrack: summarizeHistoryTrack(track),
        shouldImportStoredTrack,
        canImport: true,
      });
      if (shouldImportStoredTrack) {
        void engine.history.importTrack(viewId, track).catch((error: unknown) => {
          asukaHistoryDebug('root.ensure3DHistoryTrackIsMounted.import.error', { viewId, error: String((error as any)?.message ?? error) });
        });
        asukaHistoryDebug('root.ensure3DHistoryTrackIsMounted.imported', { viewId, track: summarizeHistoryTrack(track) });
        return;
      }
    } catch (error) {
      asukaHistoryDebug('root.ensure3DHistoryTrackIsMounted.store.error', { viewId, error: String((error as any)?.message ?? error) });
    }
    if (engineMatchesView) return;
    asukaHistoryDebug('root.ensure3DHistoryTrackIsMounted.noTrackMounted', { viewId, engineInfo: summarizeHistoryInfo(engineInfo) });
  };

  const runHistoryAction = async (action: 'undo' | 'redo' | 'reset') => {
    const storeBefore = useAsukaStore.getState() as any;
    const kind = String(storeBefore?.activeRenderEngineKind ?? activeRenderEngineKind ?? 'fabric') === '3d' ? '3d' : 'fabric';
    asukaHistoryDebug('root.historyAction.start', {
      action,
      kind,
      activeViewId: storeBefore?.activeViewId ?? activeViewId,
      documentHistoryInfo: summarizeHistoryInfo(storeBefore?.documentHistoryInfo),
      store3dTrack: summarizeHistoryTrack(storeBefore?.document3dHistoryByView?.[String(storeBefore?.activeViewId ?? activeViewId ?? '').trim()]),
      mounted3dViewId: mounted3dViewIdRef.current,
      lastLoaded3dViewId: lastLoaded3dViewIdRef.current,
    });
    if (kind === '3d') {
      const engine = resolveActiveHistoryEngine() as any;
      const viewId = String((useAsukaStore.getState() as any)?.activeViewId ?? activeViewId ?? '').trim();
      let beforeEngineInfo: any = null;
      try { beforeEngineInfo = engine?.history.getInfo() ?? null; } catch {}
      asukaHistoryDebug('root.historyAction.3d.beforeEnsure', {
        action,
        viewId,
        hasEngine: Boolean(engine),
        canAction: Boolean(engine && typeof engine.history[action] === 'function'),
        engineInfo: summarizeHistoryInfo(beforeEngineInfo),
      });
      if (!engine || !viewId) {
        asukaHistoryDebug('root.historyAction.3d.abort', { action, viewId, hasEngine: Boolean(engine) });
        return;
      }
      ensure3DHistoryTrackIsMounted(engine, viewId);
      let afterEnsureInfo: any = null;
      try { afterEnsureInfo = engine?.history.getInfo() ?? null; } catch {}
      asukaHistoryDebug('root.historyAction.3d.afterEnsure', { action, viewId, engineInfo: summarizeHistoryInfo(afterEnsureInfo) });
      if (typeof engine.history[action] === 'function') {
        await Promise.resolve(engine.history[action]());
        let afterActionInfo: any = null;
        let afterActionTrack: any = null;
        try { afterActionInfo = engine.history.getInfo(); } catch {}
        try { afterActionTrack = isAsukaHistoryDebugEnabled() ? engine.history.exportTrack(viewId) : null; } catch {}
        asukaHistoryDebug('root.historyAction.3d.afterAction', {
          action,
          viewId,
          engineInfo: summarizeHistoryInfo(afterActionInfo),
          engineTrack: summarizeHistoryTrack(afterActionTrack),
        });
        await persist3DHistoryAndSceneAfterHistoryAction(engine, viewId);
      } else {
        asukaHistoryDebug('root.historyAction.3d.missingMethod', { action, viewId, keys: Object.keys(engine ?? {}).filter((k) => /history|undo|redo|reset/i.test(k)) });
      }
      return;
    }

    const engine = resolveActiveHistoryEngine() as any;
    asukaHistoryDebug('root.historyAction.fabric.start', {
      action,
      hasEngine: Boolean(engine),
      engineInfo: summarizeHistoryInfo(engine?.history.getInfo() ?? null),
    });
    if (engine && typeof engine.history[action] === 'function') {
      await Promise.resolve(engine.history[action]());
      asukaHistoryDebug('root.historyAction.fabric.engineDone', { action, engineInfo: summarizeHistoryInfo(engine.history.getInfo()) });
      return;
    }
    asukaHistoryDebug('root.historyAction.fabric.missingEngineMethod', { action });
  };

  const handleUndo = () => {
    if (!canUseUndo) return;
    void runHistoryAction('undo');
  };

  const handleRedo = () => {
    if (!canUseRedo) return;
    void runHistoryAction('redo');
  };

  const handleReset = () => {
    if (!canUseReset) return;
    void runHistoryAction('reset');
  };

  const getDndTransferDebug = (event: any) => {
    const transfer = event?.dataTransfer;
    const read = (type: string) => {
      try { return transfer?.getData?.(type) || ''; } catch { return '[blocked]'; }
    };
    return {
      types: (() => { try { return Array.from(transfer?.types ?? []); } catch { return []; } })(),
      items: (() => {
        try {
          return Array.from(transfer?.items ?? []).map((item: any) => ({
            kind: item?.kind,
            type: item?.type,
          }));
        } catch {
          return [];
        }
      })(),
      textAsukaBlockId: read('text/asuka-block-id'),
      appAsukaBlockId: read('application/x-asuka-block-id'),
      textPlain: read('text/plain'),
      effectAllowed: transfer?.effectAllowed,
      dropEffect: transfer?.dropEffect,
    };
  };

  const isInternalBlockDragEvent = (event: any) => {
    try {
      const types = Array.from(event?.dataTransfer?.types ?? []);
      if (types.includes('text/asuka-block-id') || types.includes('application/x-asuka-block-id')) {
        return true;
      }
    } catch {}
    try {
      if (draggingBlockIdRef.current || draggedBlockId || pointerMenuDragRef.current?.active) return true;
    } catch {}
    return false;
  };

  const isFileDragEvent = (event: any) => {
    if (isInternalBlockDragEvent(event)) return false;
    try {
      const types = Array.from(event?.dataTransfer?.types ?? []);
      if (types.includes('Files')) return true;
    } catch {}
    try {
      return Array.from(event?.dataTransfer?.items ?? []).some((item: any) => item?.kind === 'file');
    } catch {
      return false;
    }
  };

  const handleZoneDragOver = (event: any) => {
    const zone = (event?.currentTarget as any)?.getAttribute?.('data-zone') ?? null;

    if (isFileDragEvent(event)) {
      dndLog('dragover:file-ignored-by-layout', { zone, transfer: getDndTransferDebug(event) });
      // File uploads are handled by FileDropzone; still prevent the browser from opening the file.
      try { event.preventDefault?.(); } catch {}
      if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
      return;
    }

    if (!isInternalBlockDragEvent(event)) {
      return;
    }

    event.preventDefault();
    try { event.stopPropagation?.(); } catch {}
    dndLog('dragover', {
      zone,
      canMoveLayoutPanels,
      draggedBlockIdRef: draggingBlockIdRef.current,
      draggedBlockId,
      transfer: getDndTransferDebug(event),
    });
    if (event.dataTransfer) {
      event.dataTransfer.dropEffect = 'move';
    }
  };


  const normalizeLayoutZone = useCallback((zone: any): LayoutZoneId | null => {
    return zone === 'top' || zone === 'right' || zone === 'bottom' || zone === 'left' ? zone : null;
  }, []);

  const getZoneMenuElements = useCallback((zone: LayoutZoneId, containerEl?: HTMLElement | null): HTMLElement[] => {
    const root = containerEl ?? shellRootRef.current;
    if (!root) return [];

    const zoneRoot =
      normalizeLayoutZone(root.getAttribute?.('data-zone')) === zone
        ? root
        : (root.querySelector?.(`[data-zone="${zone}"]`) as HTMLElement | null);

    if (!zoneRoot) return [];

    const seen = new Set<string>();
    return Array.from(zoneRoot.querySelectorAll<HTMLElement>('[data-asuka-layout-block="true"][data-asuka-block-id]'))
      .filter((element) => {
        const id = String(element.dataset.asukaBlockId ?? '');
        if (!id || seen.has(id)) return false;
        // Keep only elements that actually belong to this zone. This avoids picking nested module content.
        const ownerZone = normalizeLayoutZone(element.closest?.('[data-zone]')?.getAttribute?.('data-zone'));
        if (ownerZone !== zone) return false;
        seen.add(id);
        return true;
      });
  }, [normalizeLayoutZone]);

  const isInlineDropZone = useCallback((zone: LayoutZoneId) => {
    if (zone === 'left') return isLeftInline;
    if (zone === 'right') return isRightInline;
    return false;
  }, [isLeftInline, isRightInline]);

  const findLayoutBlockById = useCallback((blockId: string) => {
    const id = String(blockId ?? '');
    if (!id) return null;
    return [...topBlocks, ...rightBlocks, ...bottomBlocks, ...leftBlocks].find((block: any) => String(block?.id ?? '') === id) ?? null;
  }, [bottomBlocks, leftBlocks, rightBlocks, topBlocks]);

  const getLayoutBlocksForZone = useCallback((zone: LayoutZoneId): any[] => {
    if (zone === 'top') return topBlocks;
    if (zone === 'bottom') return bottomBlocks;
    if (zone === 'left') return leftBlocks;
    if (zone === 'right') return rightBlocks;
    return [];
  }, [bottomBlocks, leftBlocks, rightBlocks, topBlocks]);

  const resolveAbsoluteDropIndex = useCallback((blockId: string, zone: LayoutZoneId, visibleTargetIndex?: number): number | undefined => {
    if (typeof visibleTargetIndex !== 'number' || !Number.isFinite(visibleTargetIndex)) return visibleTargetIndex;

    const id = String(blockId ?? '');
    const allBlocks = getLayoutBlocksForZone(zone).filter((block: any) => String(block?.id ?? '') !== id);
    const visibleBlocks = allBlocks.filter((block: any) => shouldShowBlock(block));
    const safeIndex = Math.max(0, Math.min(visibleBlocks.length, Math.round(visibleTargetIndex)));
    if (safeIndex >= visibleBlocks.length) return allBlocks.length;

    const referenceId = String(visibleBlocks[safeIndex]?.id ?? '');
    const absoluteIndex = allBlocks.findIndex((block: any) => String(block?.id ?? '') === referenceId);
    return absoluteIndex >= 0 ? absoluteIndex : allBlocks.length;
  }, [getLayoutBlocksForZone, shouldShowBlock]);

  const commitLayoutBlockMove = useCallback((blockId: string, zone: LayoutZoneId, targetIndex?: number) => {
    const id = String(blockId ?? '');
    if (!id) return;

    const block = findLayoutBlockById(id);
    const absoluteTargetIndex = resolveAbsoluteDropIndex(id, zone, targetIndex);
    const movingIntoInline = isInlineDropZone(zone);

    if (movingIntoInline) {
      // Inline zones mount the full module body. Doing that synchronously inside the
      // pointer/drop event can make complex modules re-enter layout while the drag is
      // still being cleaned up. Commit on the next frame and mount the body one frame
      // later so the drop stays responsive.
      setSettlingInlineBlockIds((current) => ({ ...(current ?? {}), [id]: true }));
      if (block?.kind && String(activeModuleIdRef.current ?? '') === String(block.kind ?? '')) {
        try { closeMenu?.(); } catch {}
      }
      requestAnimationFrame(() => {
        moveBlock(id, zone, absoluteTargetIndex);
        requestAnimationFrame(() => {
          setSettlingInlineBlockIds((current) => {
            if (!current?.[id]) return current ?? {};
            const next = { ...(current ?? {}) };
            delete next[id];
            return next;
          });
        });
      });
      return;
    }

    moveBlock(id, zone, absoluteTargetIndex);
  }, [closeMenu, findLayoutBlockById, isInlineDropZone, moveBlock, resolveAbsoluteDropIndex]);

  const getZoneDropReferenceElements = useCallback((zone: LayoutZoneId, containerEl?: HTMLElement | null): HTMLElement[] => {
    const root = containerEl ?? shellRootRef.current;
    if (!root) return [];

    const zoneRoot =
      normalizeLayoutZone(root.getAttribute?.('data-zone')) === zone
        ? root
        : (root.querySelector?.(`[data-zone="${zone}"]`) as HTMLElement | null);

    if (!zoneRoot) return [];

    if (!isInlineDropZone(zone)) {
      return getZoneMenuElements(zone, zoneRoot);
    }

    const seen = new Set<string>();
    return Array.from(zoneRoot.querySelectorAll<HTMLElement>('[data-asuka-inline-module-shell="true"][data-asuka-block-id]'))
      .filter((element) => {
        const id = String(element.dataset.asukaBlockId ?? '');
        if (!id || seen.has(id)) return false;
        const ownerZone = normalizeLayoutZone(element.closest?.('[data-zone]')?.getAttribute?.('data-zone'));
        if (ownerZone !== zone) return false;
        seen.add(id);
        return true;
      });
  }, [getZoneMenuElements, isInlineDropZone, normalizeLayoutZone]);

  const computeDropIndex = useCallback((zone: LayoutZoneId, containerEl: HTMLElement | null, pointerX: number, pointerY: number, draggedId?: string) => {
    const items = getZoneDropReferenceElements(zone, containerEl)
      .filter((el) => el.dataset.asukaBlockId && el.dataset.asukaBlockId !== draggedId);

    if (items.length === 0) return 0;

    const isHorizontal = zone === 'top' || zone === 'bottom';
    const sorted = items.slice().sort((a, b) => {
      const ar = a.getBoundingClientRect();
      const br = b.getBoundingClientRect();
      return isHorizontal ? ar.left - br.left : ar.top - br.top;
    });

    const pointer = isHorizontal ? pointerX : pointerY;

    for (let index = 0; index < sorted.length; index++) {
      const rect = sorted[index].getBoundingClientRect();
      const start = isHorizontal ? rect.left : rect.top;
      const size = isHorizontal ? rect.width : rect.height;
      const threshold = start + size / 2;
      if (pointer < threshold) return index;
    }

    return sorted.length;
  }, [getZoneDropReferenceElements]);

  const computeMenuDropIndicator = useCallback((zone: LayoutZoneId, containerEl: HTMLElement | null, pointerX: number, pointerY: number, draggedId?: string) => {
    const zoneEl =
      containerEl ??
      (shellRootRef.current?.querySelector?.(`[data-zone="${zone}"]`) as HTMLElement | null);

    if (!zoneEl) return null;

    const isHorizontal = zone === 'top' || zone === 'bottom';
    const targetIndex = computeDropIndex(zone, zoneEl, pointerX, pointerY, draggedId);
    const items = getZoneDropReferenceElements(zone, zoneEl)
      .filter((el) => el.dataset.asukaBlockId && el.dataset.asukaBlockId !== draggedId)
      .sort((a, b) => {
        const ar = a.getBoundingClientRect();
        const br = b.getBoundingClientRect();
        return isHorizontal ? ar.left - br.left : ar.top - br.top;
      });

    if (!items.length) {
      // Empty zones already display their own dedicated drop slot placeholder.
      // Returning null here avoids a second, oversized floating indicator.
      return null;
    }

    const before = items[Math.max(0, Math.min(items.length - 1, targetIndex))];
    const after = items[Math.max(0, Math.min(items.length - 1, targetIndex - 1))];

    if (isHorizontal) {
      const referenceRect =
        targetIndex <= 0
          ? before.getBoundingClientRect()
          : targetIndex >= items.length
            ? after.getBoundingClientRect()
            : before.getBoundingClientRect();
      const previousRect = targetIndex > 0 ? after.getBoundingClientRect() : null;
      const nextRect = targetIndex < items.length ? before.getBoundingClientRect() : null;
      const left =
        previousRect && nextRect
          ? previousRect.right + Math.max(6, (nextRect.left - previousRect.right) / 2) - 2
          : targetIndex <= 0
            ? referenceRect.left - 6
            : referenceRect.right + 6;
      const top = referenceRect.top - 4;
      return {
        zone,
        targetIndex,
        orientation: 'vertical',
        left,
        top,
        width: 4,
        height: Math.max(26, referenceRect.height + 8),
      };
    }

    const referenceRect =
      targetIndex <= 0
        ? before.getBoundingClientRect()
        : targetIndex >= items.length
          ? after.getBoundingClientRect()
          : before.getBoundingClientRect();
    const previousRect = targetIndex > 0 ? after.getBoundingClientRect() : null;
    const nextRect = targetIndex < items.length ? before.getBoundingClientRect() : null;
    const top =
      previousRect && nextRect
        ? previousRect.bottom + Math.max(6, (nextRect.top - previousRect.bottom) / 2) - 2
        : targetIndex <= 0
          ? referenceRect.top - 6
          : referenceRect.bottom + 6;

    const isInlineSidebar = isInlineDropZone(zone);
    return {
      zone,
      targetIndex,
      orientation: 'horizontal',
      left: referenceRect.left + (isInlineSidebar ? 8 : -4),
      top,
      width: Math.max(56, referenceRect.width + (isInlineSidebar ? -16 : 8)),
      height: 4,
    };
  }, [computeDropIndex, getZoneDropReferenceElements, isInlineDropZone]);


  const handleZoneDrop = (zone: LayoutZoneId) => (event: any) => {
    const isInternalLayoutDrop = isInternalBlockDragEvent(event);
    dndLog('drop:start', {
      zone,
      canMoveLayoutPanels,
      draggedBlockIdRef: draggingBlockIdRef.current,
      draggedBlockId,
      transfer: getDndTransferDebug(event),
      target: (event?.target as any)?.tagName,
      currentTarget: (event?.currentTarget as any)?.tagName,
    });

    // File uploads are handled by FileDropzone. Prevent the browser from opening dropped files,
    // but do not move panels or consume the file payload as a layout block id.
    if (isFileDragEvent(event)) {
      dndLog('drop:blocked-file-drag', { zone, transfer: getDndTransferDebug(event) });
      try { event.preventDefault?.(); } catch {}
      try { event.stopPropagation?.(); } catch {}
      return;
    }

    if (!isInternalLayoutDrop) {
      return;
    }

    if (!canMoveLayoutPanels) {
      dndLog('drop:blocked-permission', {
        zone,
        canMovePanelsPermission: (permissions as any)?.canMovePanels,
      canCollapsePanelsPermission: (permissions as any)?.canCollapsePanels,
        role: (useAsukaStore.getState() as any)?.role,
      });
      try { event.preventDefault?.(); } catch {}
      return;
    }
    event.preventDefault();
    try { event.stopPropagation?.(); } catch {}

    const id =
      event.dataTransfer?.getData('text/asuka-block-id') ||
      event.dataTransfer?.getData('application/x-asuka-block-id') ||
      event.dataTransfer?.getData('text/plain') ||
      draggingBlockIdRef.current ||
      draggedBlockId;

    if (id) {
      const container = event.currentTarget as HTMLElement | null;
      const draggedId = draggedBlockId ?? draggingBlockIdRef.current ?? id;
      const targetIndex =
        container && typeof event.clientX === 'number' && typeof event.clientY === 'number'
          ? computeDropIndex(zone, container, event.clientX, event.clientY, draggedId)
          : undefined;

      dndLog('drop:moveBlock-call', {
        id,
        zone,
        targetIndex,
        draggedId,
        beforeBlocksByZone: {
          top: topBlocks.map((b: any) => ({ id: b.id, kind: b.kind, order: b.order })),
          left: leftBlocks.map((b: any) => ({ id: b.id, kind: b.kind, order: b.order })),
          right: rightBlocks.map((b: any) => ({ id: b.id, kind: b.kind, order: b.order })),
          bottom: bottomBlocks.map((b: any) => ({ id: b.id, kind: b.kind, order: b.order })),
        },
      });
      commitLayoutBlockMove(id, zone, targetIndex);
      window.setTimeout(() => {
        const s: any = useAsukaStore.getState();
        dndLog('drop:after-store', {
          id,
          zone,
          targetIndex,
          blocksByZone: s.blocksByZone,
        });
      }, 0);
    } else {
      dndLog('drop:no-id-resolved', {
        zone,
        transfer: getDndTransferDebug(event),
        draggingBlockIdRef: draggingBlockIdRef.current,
        draggedBlockId,
      });
    }

    draggingBlockIdRef.current = null;
    endDraggingBlock();
  };

  const resolveMenuDropZoneFromPoint = useCallback((clientX: number, clientY: number, shell?: HTMLElement | null): LayoutZoneId | null => {
    try {
      const elements = typeof document !== 'undefined' && typeof document.elementsFromPoint === 'function'
        ? document.elementsFromPoint(clientX, clientY)
        : [];
      for (const element of elements) {
        const zone = normalizeLayoutZone((element as HTMLElement | null)?.closest?.('[data-zone]')?.getAttribute?.('data-zone'));
        if (zone) return zone;
      }
    } catch {}

    const root = shell ?? shellRootRef.current;
    if (!root) return null;
    const rect = root.getBoundingClientRect();
    if (!rect.width || !rect.height) return null;

    const x = clientX - rect.left;
    const y = clientY - rect.top;
    const leftRatio = x / rect.width;
    const topRatio = y / rect.height;

    if (topRatio <= 0.18) return 'top';
    if (topRatio >= 0.82) return 'bottom';
    if (leftRatio <= 0.30) return 'left';
    if (leftRatio >= 0.70) return 'right';
    return null;
  }, [normalizeLayoutZone]);

  const resolveMenuDropZoneFromShellEvent = useCallback((event: any): LayoutZoneId | null => {
    if (typeof event?.clientX !== 'number' || typeof event?.clientY !== 'number') return null;
    return resolveMenuDropZoneFromPoint(event.clientX, event.clientY, event?.currentTarget as HTMLElement | null);
  }, [resolveMenuDropZoneFromPoint]);

  const handleShellDragOverCapture = (event: any) => {
    if (!isInternalBlockDragEvent(event)) return;
    const zone = resolveMenuDropZoneFromShellEvent(event);
    dndLog('shell:dragover-capture', {
      zone,
      draggedBlockIdRef: draggingBlockIdRef.current,
      draggedBlockId,
      target: (event?.target as any)?.tagName,
      currentTarget: (event?.currentTarget as any)?.tagName,
      transfer: getDndTransferDebug(event),
    });
    if (!zone) return;
    try { event.preventDefault?.(); } catch {}
    try { event.stopPropagation?.(); } catch {}
    if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
  };

  const handleShellDropCapture = (event: any) => {
    if (!isInternalBlockDragEvent(event)) return;

    const zone = resolveMenuDropZoneFromShellEvent(event);
    const id =
      event.dataTransfer?.getData('text/asuka-block-id') ||
      event.dataTransfer?.getData('application/x-asuka-block-id') ||
      event.dataTransfer?.getData('text/plain') ||
      draggingBlockIdRef.current ||
      draggedBlockId;

    dndLog('shell:drop-capture:start', {
      zone,
      id,
      canMoveLayoutPanels,
      draggedBlockIdRef: draggingBlockIdRef.current,
      draggedBlockId,
      target: (event?.target as any)?.tagName,
      currentTarget: (event?.currentTarget as any)?.tagName,
      transfer: getDndTransferDebug(event),
    });

    if (!zone || !id) return;

    try { event.preventDefault?.(); } catch {}
    try { event.stopPropagation?.(); } catch {}

    if (!canMoveLayoutPanels) {
      dndLog('shell:drop-capture:blocked-permission', {
        zone,
        id,
        canMovePanelsPermission: (permissions as any)?.canMovePanels,
      canCollapsePanelsPermission: (permissions as any)?.canCollapsePanels,
        role: (useAsukaStore.getState() as any)?.role,
      });
      return;
    }

    let targetIndex: number | undefined = undefined;
    try {
      const zoneEl = (event?.currentTarget as HTMLElement | null)?.querySelector?.(`[data-zone="${zone}"]`) as HTMLElement | null;
      targetIndex =
        zoneEl && typeof event.clientX === 'number' && typeof event.clientY === 'number'
          ? computeDropIndex(zone, zoneEl, event.clientX, event.clientY, draggingBlockIdRef.current ?? draggedBlockId ?? id)
          : undefined;
    } catch {}

    dndLog('shell:drop-capture:moveBlock-call', { id, zone, targetIndex });
    commitLayoutBlockMove(id, zone, targetIndex);

    window.setTimeout(() => {
      const s: any = useAsukaStore.getState();
      dndLog('shell:drop-capture:after-store', { id, zone, targetIndex, blocksByZone: s.blocksByZone });
    }, 0);

    draggingBlockIdRef.current = null;
    endDraggingBlock();
  };

  const finishPointerMenuDrag = useCallback((event?: any) => {
    const drag = pointerMenuDragRef.current;
    if (!drag) return;
    const zone = drag.zone || (
      typeof event?.clientX === 'number' && typeof event?.clientY === 'number'
        ? resolveMenuDropZoneFromPoint(event.clientX, event.clientY, shellRootRef.current)
        : null
    );
    dndLog('pointer:finish', { blockId: drag.blockId, active: drag.active, zone, x: event?.clientX, y: event?.clientY, canMoveLayoutPanels });
    if (drag.active && zone && canMoveLayoutPanels) {
      let targetIndex: number | undefined = undefined;
      try {
        const zoneEl = shellRootRef.current?.querySelector?.(`[data-zone="${zone}"]`) as HTMLElement | null;
        targetIndex = zoneEl && typeof event?.clientX === 'number' && typeof event?.clientY === 'number'
          ? computeDropIndex(zone, zoneEl, event.clientX, event.clientY, drag.blockId)
          : (typeof drag.targetIndex === 'number' ? drag.targetIndex : undefined);

        // Same-zone fallback: if the pointer moved clearly but landed in an ambiguous slot,
        // still allow a one-step reorder. This makes intra-zone movement feel responsive.
        if (zone === drag.sourceZone && typeof targetIndex === 'number' && targetIndex === Number(drag.sourceOrder ?? 0)) {
          const isHorizontal = zone === 'top' || zone === 'bottom';
          const delta = isHorizontal ? Number(event?.clientX ?? drag.x) - Number(drag.startX) : Number(event?.clientY ?? drag.y) - Number(drag.startY);
          if (Math.abs(delta) > 28) {
            targetIndex = Math.max(0, targetIndex + (delta > 0 ? 1 : -1));
          }
        }
      } catch {}
      commitLayoutBlockMove(drag.blockId, zone, targetIndex);
      window.setTimeout(() => {
        const s: any = useAsukaStore.getState();
        dndLog('pointer:after-store', { id: drag.blockId, zone, targetIndex, blocksByZone: s.blocksByZone });
      }, 0);
    }
    pointerMenuDragRef.current = null;
    draggingBlockIdRef.current = null;
    setPointerMenuDrag(null);
    endDraggingBlock();
    window.setTimeout(() => { suppressClickRef.current = false; }, 0);
  }, [canMoveLayoutPanels, commitLayoutBlockMove, computeDropIndex, endDraggingBlock, resolveMenuDropZoneFromPoint]);

  useEffect(() => {
    const onPointerMove = (event: PointerEvent) => {
      const drag = pointerMenuDragRef.current;
      if (!drag) return;
      const dx = event.clientX - drag.startX;
      const dy = event.clientY - drag.startY;
      const distance = Math.sqrt(dx * dx + dy * dy);
      const active = drag.active || distance >= 4;
      const zone = resolveMenuDropZoneFromPoint(event.clientX, event.clientY, shellRootRef.current);
      let targetIndex: number | null = null;
      let indicator: any = null;
      if (zone) {
        try {
          const zoneEl = shellRootRef.current?.querySelector?.(`[data-zone="${zone}"]`) as HTMLElement | null;
          targetIndex = computeDropIndex(zone, zoneEl, event.clientX, event.clientY, drag.blockId);
          indicator = computeMenuDropIndicator(zone, zoneEl, event.clientX, event.clientY, drag.blockId);
        } catch {
          targetIndex = null;
          indicator = null;
        }
      }
      if (active) {
        try { event.preventDefault(); } catch {}
        if (!drag.active) {
          suppressClickRef.current = true;
          draggingBlockIdRef.current = drag.blockId;
          startDraggingBlock(drag.blockId);
        }
      }
      const next = { ...drag, active, x: event.clientX, y: event.clientY, zone, targetIndex, indicator };
      pointerMenuDragRef.current = next;
      setPointerMenuDrag(next);
    };
    const onPointerUp = (event: PointerEvent) => {
      if (!pointerMenuDragRef.current) return;
      try { event.preventDefault(); } catch {}
      finishPointerMenuDrag(event);
    };
    const onPointerCancel = (event: PointerEvent) => {
      if (!pointerMenuDragRef.current) return;
      dndLog('pointer:cancel', { blockId: pointerMenuDragRef.current?.blockId });
      finishPointerMenuDrag(event);
    };
    window.addEventListener('pointermove', onPointerMove, { capture: true });
    window.addEventListener('pointerup', onPointerUp, { capture: true });
    window.addEventListener('pointercancel', onPointerCancel, { capture: true });
    return () => {
      window.removeEventListener('pointermove', onPointerMove, { capture: true } as any);
      window.removeEventListener('pointerup', onPointerUp, { capture: true } as any);
      window.removeEventListener('pointercancel', onPointerCancel, { capture: true } as any);
    };
  }, [canMoveLayoutPanels, computeDropIndex, computeMenuDropIndicator, finishPointerMenuDrag, resolveMenuDropZoneFromPoint, startDraggingBlock]);

  const handleBlockPointerDown = (block: any, sourceZone?: LayoutZoneId | null) => (event: any) => {
    if (!canMoveLayoutPanels) return;
    if (typeof event?.button === 'number' && event.button !== 0) return;
    const blockId = String(block?.id ?? '');
    if (!blockId) return;
    const resolvedSourceZone = sourceZone ?? normalizeLayoutZone(event?.currentTarget?.closest?.('[data-zone]')?.getAttribute?.('data-zone'));
    const label = getMenuLabel(block);
    try { event.currentTarget?.setPointerCapture?.(event.pointerId); } catch {}
    pointerMenuDragRef.current = {
      blockId,
      label,
      icon: getMenuIcon(block),
      sourceZone: resolvedSourceZone,
      sourceOrder: Number(block?.order ?? 0),
      startX: event.clientX,
      startY: event.clientY,
      x: event.clientX,
      y: event.clientY,
      zone: resolvedSourceZone,
      active: false,
      targetIndex: null,
    };
  };

  const layoutDragDisabledTitle = t('ui.layout_drag_disabled', 'Menu drag and drop is disabled by permissions.');

  const activeViewName = hasViews
    ? views.find((v) => v.id === activeViewId)?.name ?? t('ui.front', 'Front')
    : null;

  const viewsPaginationConfig = uiConfig?.viewsPagination ?? {
    enabled: true,
    mode: 'auto',
    showLabels: false,
    enableGrabNavigation: true,
  };
  const viewsPaginationMode = (
    viewsPaginationConfig.mode === 'auto' || !viewsPaginationConfig.mode
      ? Object.keys(viewThumbnails ?? {}).length > 0 ? 'thumbnails' : 'dots'
      : viewsPaginationConfig.mode
  ) as CarouselPaginationMode;
  const shouldRenderViewsPagination = hasViews && viewsPaginationConfig.enabled !== false;
  const historyControlsConfig = uiConfig?.historyControls ?? {
    undo: true,
    redo: true,
    reset: true,
  };
  const shouldRenderUndo = canUseUndo && historyControlsConfig.undo !== false;
  const shouldRenderRedo = canUseRedo && historyControlsConfig.redo !== false;
  const shouldRenderReset = canUseReset && historyControlsConfig.reset !== false;

  const viewCarouselItems = views.map((view) => ({
    id: view.id,
    label: view.name,
    thumbnailDataUrl: viewThumbnails?.[view.id] ?? null,
  }));

  const activateViewThroughRenderer = (id: string) => {
    const targetId = String(id ?? '').trim();
    if (!targetId) return;
    const engine = mountedRenderEngineRef.current ?? getRuntimeRenderEngine(activeRenderEngineKind === '3d' ? '3d' : 'fabric');
    if (!engine) {
      setActiveView?.(targetId);
      return;
    }
    const targetKind = getActiveViewEngineKind(views as any, targetId, defaultRenderEngineKind as any);
    try {
      void Promise.resolve(engine.lifecycle.activateView(targetId, { mode: 'switch' })).then(() => {
        if (engine.kind === 'fabric' && targetKind === 'fabric') mountedRenderViewIdRef.current = targetId;
      });
    } catch {
      setActiveView?.(targetId);
    }
  };

  const handleViewCarouselSelect = (id: string) => {
    activateViewThroughRenderer(id);
  };

  const handleCreateFirstView = () => {
    if (!(permissions as any)?.canCreateView) return;
    // Reuse the existing Views module "Create view" form.
    try {
      useAsukaStore.getState?.().requestOpenCreateViewModal?.();
      useAsukaStore.getState?.().openModule?.('views' as any);
    } catch {
      // Fallback (should not happen): keep a safe direct create.
      const id = addView?.({ name: 'Front', width: 800, height: 600 });
      if (!id) return;
      activateViewThroughRenderer(id);
    }
  };


  const isActionBlockActive = (kind: string) => {
    if (kind === 'magnifier') return Boolean(magnifierEnabled);
    if (kind === 'ruler') return Boolean(rulerEnabled);
    return false;
  };

  const isBlockActive = (block: any) => {
    if (draggedBlockId === block.id) return true;
    if (!isDragging && activeModuleIdRef.current === block.kind) return true;
    return isActionBlockActive(String(block.kind));
  };

  const collapsedBlockButtonClass = (block: any) => `ask-theme-sidebar-item ask-theme-sidebar-item--collapsed ask-inline-flex ask-items-center ask-justify-center ask-border ask-p-0 ask-mx-auto ask-text-[11px] ask-transition-colors ${isBlockActive(block) ? 'is-active' : ''} ${!canOpenBlock(block) ? 'ask-opacity-45 ask-cursor-not-allowed' : ''} ${!canOpenBlock(block) ? 'ask-opacity-45 ask-cursor-not-allowed' : ''}`;

  const defaultBlockButtonClass = (block: any) => `ask-theme-sidebar-item ask-w-full ask-flex ask-flex-col ask-items-center ask-justify-center ask-gap-1.5 ask-px-2 ask-py-3 ask-text-[11px] ask-transition-colors ask-border ask-rounded-[20px] ${isBlockActive(block) ? 'is-active' : ''} ${!canOpenBlock(block) ? 'ask-opacity-45 ask-cursor-not-allowed' : ''} ${!canOpenBlock(block) ? 'ask-opacity-45 ask-cursor-not-allowed' : ''}`;

  const zoneToggleIconName = (collapsed: boolean) => (collapsed ? 'expand' : 'reduce');

  const renderZoneToggleButton = (zone: 'left' | 'right' | 'top' | 'bottom', collapsed: boolean, title: string, horizontal = false) => {
    if (!canCollapseLayoutPanels) return null;
    return (
      <div class={`ask-zone-toggle-wrapper ${horizontal ? 'ask-zone-toggle-wrapper--horizontal' : 'ask-zone-toggle-wrapper--vertical'}`}>
        <button
          type="button"
          class="ask-zone-toggle ask-inline-flex ask-items-center ask-justify-center ask-transition-colors"
          onClick={() => {
            if (!canCollapseLayoutPanels) return;
            toggleZoneCollapsed(zone);
          }}
          title={title}
          aria-label={title}
        >
          <AsukaIcon name={zoneToggleIconName(collapsed)} className="ask-pointer-events-none" size={12} />
        </button>
      </div>
    );
  };

  const renderCollapsedBlocks = (zone: 'left' | 'right', blocks: any[]) => (
    <div class={`ask-collapsed-navigation-list ask-flex-1 ask-flex ask-flex-col ask-items-center ${canMoveLayoutPanels && isDragging ? 'ask-dnd-zone-active' : ''}`}>
      {blocks.map((block) => (
        <Tooltip key={block.id} content={getBlockAccessLabel(block)} direction={zone === 'left' ? 'right' : 'left'}>
          <button
            type="button"
            draggable={false}
            data-asuka-block-id={block.id}
            data-asuka-layout-block="true"
            data-asuka-module-trigger="true"
            data-asuka-module-kind={String(block.kind ?? '')}
            data-asuka-module-active={isBlockActive(block) ? 'true' : 'false'}
            onPointerDown={handleBlockPointerDown(block, zone)}
            onClick={handleBlockClick(block)}
            class={collapsedBlockButtonClass(block)}
            title={getBlockAccessLabel(block)}
            aria-label={getMenuLabel(block)}
            aria-pressed={isBlockActive(block)}
            aria-disabled={!canOpenBlock(block)}
            disabled={!canOpenBlock(block)}
          >
            <AsukaIcon name={getMenuIcon(block)} className="ask-pointer-events-none" size={18} />
          </button>
        </Tooltip>
      ))}
    </div>
  );

  const renderInlineBlocks = (zone: LayoutZoneId, blocks: any[]) => (
    <ScrollArea
      className={`ask-flex-1 ask-min-h-0 ${canMoveLayoutPanels && isDragging ? 'ask-dnd-zone-active' : ''}`}
      contentClassName="ask-zone-inline"
    >
      {blocks.map((block) => {
        const def = getModule(block.kind);
        if (!def || typeof def.render !== 'function') return null;
        const ModuleComponent = def.render;
        const blockedWithoutView = isModuleUnavailableWithoutView(String(block.kind));
        return (
          <section
            key={block.id}
            class={`ask-zone-inline__module ask-zone-inline__module-shell ${draggedBlockId === block.id ? 'is-dragging' : ''}`}
            data-asuka-inline-module={String(block.kind)}
            data-asuka-inline-module-shell="true"
            data-asuka-block-id={block.id}
          >
            <div class="ask-zone-inline__module-header">
              <button
                type="button"
                class="ask-zone-inline__module-drag-handle"
                data-asuka-block-id={block.id}
                data-asuka-layout-block="true"
                onPointerDown={(event: any) => {
                  try { event.preventDefault?.(); } catch {}
                  try { event.stopPropagation?.(); } catch {}
                  handleBlockPointerDown(block, zone)(event);
                }}
                title={canMoveLayoutPanels ? getI18n().t('ui.drag_this_panel_to_move_it') : layoutDragDisabledTitle}
                aria-label={canMoveLayoutPanels ? getI18n().t('ui.drag_this_panel_to_move_it') : layoutDragDisabledTitle}
                draggable={false}
              >
                <span class="ask-zone-inline__module-grip ask-pointer-events-none">⠿</span>
                <AsukaIcon name={getMenuIcon(block)} className="ask-pointer-events-none" size={15} />
              </button>
              <button
                type="button"
                class="ask-zone-inline__module-popout"
                data-asuka-module-trigger="true"
                data-asuka-module-kind={String(block.kind ?? '')}
                data-asuka-module-active={isBlockActive(block) ? 'true' : 'false'}
                onClick={handleBlockClick(block)}
                title={getBlockAccessLabel(block)}
                aria-label={getBlockAccessLabel(block)}
                aria-pressed={isBlockActive(block)}
                disabled={!canOpenBlock(block)}
                draggable={false}
                onDragStart={(event: any) => event.preventDefault?.()}
              >
                {getI18n().t('overlay.popOut', 'Pop out')}
              </button>
            </div>
            <div class="ask-zone-inline__module-body" data-asuka-inline-module-body="true">
              {settlingInlineBlockIds?.[String(block.id)] ? (
                <div class="ask-inline-module-settling" aria-hidden="true" />
              ) : blockedWithoutView ? (
                <ModuleUnavailableState className="ask-module-unavailable-state--inline" />
              ) : (
                <ModuleComponent context={moduleContext} />
              )}
            </div>
          </section>
        );
      })}
    </ScrollArea>
  );

  const renderDefaultBlocks = (zone: LayoutZoneId, blocks: any[]) => (
    <ScrollArea
      className={`ask-flex-1 ask-min-h-0 ${canMoveLayoutPanels && isDragging ? 'ask-dnd-zone-active' : ''}`}
      viewportClassName="ask-p-2"
      contentClassName="ask-space-y-1"
    >
      {blocks.map((block) => (
        <button
          key={block.id}
          type="button"
          draggable={false}
          data-asuka-block-id={block.id}
          data-asuka-layout-block="true"
          data-asuka-module-trigger="true"
          data-asuka-module-kind={String(block.kind ?? '')}
          data-asuka-module-active={isBlockActive(block) ? 'true' : 'false'}
          onPointerDown={handleBlockPointerDown(block, zone)}
          onClick={handleBlockClick(block)}
          class={defaultBlockButtonClass(block)}
          title={getBlockAccessLabel(block)}
          aria-pressed={isBlockActive(block)}
          aria-disabled={!canOpenBlock(block)}
          disabled={!canOpenBlock(block)}
        >
          <span class="ask-flex ask-flex-col ask-items-center ask-justify-center ask-gap-1.5 ask-pointer-events-none ask-w-full">
            <AsukaIcon name={getMenuIcon(block)} className="ask-pointer-events-none" size={18} />
            <span class="ask-theme-sidebar-item__label ask-font-medium ask-pointer-events-none ask-text-center ask-leading-tight">{getMenuLabel(block)}</span>
          </span>
        </button>
      ))}
    </ScrollArea>
  );

  const renderPanelBlocks = (zone: LayoutZoneId, blocks: any[]) => (
    <ScrollArea
      className={`ask-flex-1 ask-min-h-0 ${canMoveLayoutPanels && isDragging ? 'ask-dnd-zone-active' : ''}`}
      viewportClassName="ask-p-3"
      contentClassName="ask-space-y-2"
    >
      {blocks.map((block) => {
        const def = getModule(block.kind);
        if (!def || typeof def.render !== 'function') return null;
        const ModuleComponent = def.render;
        if ((block as any).renderMode === 'inline') {
          const blockedWithoutView = isModuleUnavailableWithoutView(String(block.kind));
          return (
            <div key={block.id} class="ask-zone-inline__module ask-zone-inline__module--panel" data-asuka-inline-module={String(block.kind)}>
              {blockedWithoutView ? (
                <ModuleUnavailableState className="ask-module-unavailable-state--inline" />
              ) : (
                <ModuleComponent context={moduleContext} />
              )}
            </div>
          );
        }
        const isItemCollapsed = Boolean((block as any).collapsed);
        const canCollapseBlock = canCollapseLayoutPanels && (block as any).collapsible !== false;
        return (
          <section
            key={block.id}
            class={`ask-theme-panel-block ask-relative ask-rounded-xl ask-transition-colors ${draggedBlockId === block.id ? 'ask-ring-1 ask-ring-primary' : ''}`}
          >
            <div
              class={`ask-theme-panel-header ask-flex ask-items-center ask-justify-between ask-gap-2 ask-px-2.5 ask-py-1.5 ${isItemCollapsed ? 'ask-rounded-xl' : 'ask-rounded-t-xl ask-border-b'} ask-cursor-grab ask-select-none ask-touch-none`}
              draggable={false}
              data-asuka-block-id={block.id}
              data-asuka-layout-block="true"
              onPointerDown={handleBlockPointerDown(block, zone)}
              title={canMoveLayoutPanels ? getI18n().t('ui.drag_this_panel_to_move_it') : layoutDragDisabledTitle}
            >
              <div class="ask-flex ask-items-center ask-gap-2 ask-pointer-events-none">
                <span class="ask-text-[12px] ask-pointer-events-none">⠿</span>
                <AsukaIcon name={getMenuIcon(block)} className="ask-pointer-events-none" size={18} />
                <span class="ask-text-[11px] ask-font-semibold ask-pointer-events-none">{getMenuLabel(block)}</span>
              </div>
              <div class="ask-flex ask-items-center ask-gap-1.5">
                {canCollapseBlock ? (
                  <button
                    type="button"
                    class="ask-theme-btn ask-theme-btn-ghost ask-inline-flex ask-items-center ask-justify-center ask-w-5 ask-h-5 ask-rounded-md"
                    onClick={(e: any) => {
                      e.preventDefault?.();
                      e.stopPropagation?.();
                      if (!canCollapseBlock) return;
                      toggleBlockCollapsed?.(block.id);
                    }}
                    title={isItemCollapsed ? t('ui.expand_panel', 'Expand panel') : t('ui.collapse_panel', 'Collapse panel')}
                    draggable={false}
                    onDragStart={(e: any) => e.preventDefault?.()}
                  >
                    {isItemCollapsed ? '▸' : '▾'}
                  </button>
                ) : null}
                <button
                  type="button"
                  class="ask-text-[10px] ask-text-textMuted hover:ask-text-textMain ask-leading-none"
                  data-asuka-module-trigger="true"
                  data-asuka-module-kind={String(block.kind ?? '')}
                  data-asuka-module-active={isBlockActive(block) ? 'true' : 'false'}
                  onClick={handleBlockClick(block)}
                  title={getBlockAccessLabel(block)}
                  aria-pressed={isBlockActive(block)}
                  disabled={!canOpenBlock(block)}
                  draggable={false}
                  onDragStart={(e: any) => e.preventDefault?.()}
                >
                  {getI18n().t('overlay.popOut', 'Pop out')}
                </button>
              </div>
            </div>
            {!isItemCollapsed && (
              <div class="ask-p-2">
                {isModuleUnavailableWithoutView(String(block.kind)) ? (
                  <ModuleUnavailableState />
                ) : (
                  <ModuleComponent context={moduleContext} />
                )}
              </div>
            )}
          </section>
        );
      })}
    </ScrollArea>
  );

  const renderMobileNavButton = (block: any, fromOverflow = false) => (
    <button
      key={`${block.__asukaSourceZone ?? 'mobile'}-${block.id}`}
      type="button"
      class={`ask-responsive-bottom-nav__item ${isBlockActive(block) ? 'is-active' : ''} ${!canOpenBlock(block) ? 'is-disabled' : ''}`}
      onClick={(event: any) => {
        setMobileMoreOpen(false);
        handleBlockClick(block)(event);
      }}
      title={getBlockAccessLabel(block)}
      aria-label={getMenuLabel(block)}
      aria-disabled={!canOpenBlock(block)}
      disabled={!canOpenBlock(block)}
      role={fromOverflow ? 'menuitem' : undefined}
      data-asuka-block-id={block.id}
      data-asuka-mobile-source-zone={block.__asukaSourceZone ?? ''}
    >
      <AsukaIcon name={getMenuIcon(block)} className="ask-pointer-events-none" size={18} />
      <span class="ask-responsive-bottom-nav__label ask-pointer-events-none">{getMenuLabel(block)}</span>
    </button>
  );

  const renderSidebar = (zone: 'left' | 'right', blocks: any[], collapsed: boolean, mode: 'panels' | 'inline' | 'default' | 'rail') => {
    const isLeft = zone === 'left';
    const collapsible = canCollapseLayoutPanels && (isLeft ? zoneCollapsible.left : zoneCollapsible.right);
    const isEmpty = blocks.length === 0;
    const showDropPlaceholder = canMoveLayoutPanels && isDragging && isEmpty;
    const widthClass = showDropPlaceholder
      ? 'ask-w-[72px]'
      : (isEmpty ? '' : (collapsed ? 'ask-w-12' : mode === 'inline' ? 'ask-w-64' : mode === 'panels' ? 'ask-w-64' : ''));
    const borderClass = isLeft ? 'ask-border-r' : 'ask-border-l';
    const collapseTitle = collapsed
      ? isLeft ? t('ui.expand_left_sidebar', 'Expand left sidebar') : t('ui.expand_right_sidebar', 'Expand right sidebar')
      : isLeft ? t('ui.collapse_left_sidebar', 'Collapse left sidebar') : t('ui.collapse_right_sidebar', 'Collapse right sidebar');
    return (
      <aside
        class={`${getEmptyZoneClass(zone, blocks.length)} ask-theme-sidebar ${mode === 'inline' ? 'ask-theme-sidebar--inline' : ''} ${mode === 'panels' ? 'ask-theme-sidebar--panels' : ''} ${mode === 'rail' ? 'ask-theme-sidebar--responsive-rail' : ''} ${collapsed ? 'ask-theme-sidebar--collapsed' : 'ask-theme-sidebar--expanded'} ${borderClass} ask-border-borderSubtle ask-flex ask-flex-col ${widthClass}`}
        onDragOver={handleZoneDragOver}
        data-zone={zone}
        onDrop={handleZoneDrop(zone)}
      >
        {collapsible && !isEmpty && mode !== 'rail' ? (
          <div class="ask-theme-shell-header ask-zone-toggle-strip">
            {renderZoneToggleButton(zone, collapsed, collapseTitle)}
          </div>
        ) : null}
        {showDropPlaceholder ? (
          <div
            class="ask-zone-drop-slot"
            onDragOver={handleZoneDragOver}
            data-zone={zone}
            onDrop={handleZoneDrop(zone)}
          />
        ) : (collapsed ? renderCollapsedBlocks(zone, blocks) : mode === 'panels' ? renderPanelBlocks(zone, blocks) : mode === 'inline' ? renderInlineBlocks(zone, blocks) : renderDefaultBlocks(zone, blocks))}
      </aside>
    );
  };

  const topIdleEmpty = visibleTopBlocks.length === 0 && !isDragging;
  const bottomIdleEmpty = visibleBottomBlocks.length === 0 && !isDragging;
  const leftSidebarMode: 'panels' | 'inline' | 'default' | 'rail' = renderSideZonesAsTabletRails ? 'rail' : isLeftPanels ? 'panels' : isLeftInline ? 'inline' : 'default';
  const rightSidebarMode: 'panels' | 'inline' | 'default' | 'rail' = renderSideZonesAsTabletRails ? 'rail' : isRightPanels ? 'panels' : isRightInline ? 'inline' : 'default';

  useEffect(() => {
    const raf1 = window.requestAnimationFrame(() => {
      const resizeEvt = new Event('resize');
      window.dispatchEvent(resizeEvt);
      mountedRenderEngineRef.current?.viewport.resize?.();

      window.requestAnimationFrame(() => {
        window.dispatchEvent(new Event('resize'));
        mountedRenderEngineRef.current?.viewport.resize?.();
      });
    });

    return () => window.cancelAnimationFrame(raf1);
  }, [
    visibleTopBlocks,
    visibleBottomBlocks,
    visibleLeftBlocks,
    visibleRightBlocks,
    topCollapsed,
    bottomCollapsed,
    leftCollapsed,
    rightCollapsed,
  ]);

  const showBootOverlay = !bootReady;
  const placementFeedback = placementStatus && !['committed', 'cancelled'].includes(placementStatus.phase)
    ? {
        tone: placementStatus.phase === 'blocked'
          ? 'blocked'
          : (placementStatus.phase === 'constrained' ? 'constrained' : 'valid'),
        icon: placementStatus.phase === 'blocked' ? 'warning' : 'check',
        label: placementStatus.phase === 'blocked'
          ? t('placement.lastValid', 'Keeping the last valid position')
          : (placementStatus.phase === 'constrained'
              ? t('placement.constrained', 'Position constrained to the drawing area')
              : t('placement.valid', 'Valid position')),
      }
    : null;

  return (
    <div
      class={`ask-relative ask-h-full ask-w-full ask-flex ask-items-stretch ask-justify-center ask-responsive-shell ask-responsive-shell--${responsiveBreakpoint} ${isMobileLayout ? 'ask-responsive-shell--mobile' : ''} ${isTabletLayout ? 'ask-responsive-shell--tablet' : ''} ${className ?? ''
        }`}
      data-asuka-responsive-breakpoint={responsiveBreakpoint}
      data-asuka-responsive-enabled={responsiveEnabled ? 'true' : 'false'}
      data-asuka-responsive-safe-area={responsiveConfig?.safeArea !== false ? 'true' : 'false'}
      data-asuka-page-portal-candidates={pagePortalBlocks.length}
      data-asuka-page-portal-visible={visiblePagePortalBlocks.length}
      data-asuka-page-portal-debug={pagePortalDebug}
      ref={shellRootRef}
      onDragOverCapture={handleShellDragOverCapture}
      onDropCapture={handleShellDropCapture}
    >
      {pointerMenuDrag?.active && pointerMenuDrag?.indicator ? (
        <div
          class={`ask-dnd-insertion-line ${pointerMenuDrag.indicator.orientation === 'vertical' ? 'ask-dnd-insertion-line--vertical' : ''}`}
          style={{
            left: `${Number(pointerMenuDrag.indicator.left ?? 0)}px`,
            top: `${Number(pointerMenuDrag.indicator.top ?? 0)}px`,
            width: `${Number(pointerMenuDrag.indicator.width ?? 4)}px`,
            height: `${Number(pointerMenuDrag.indicator.height ?? 4)}px`,
          }}
        />
      ) : null}

      {pointerMenuDrag?.active ? (
        <div
          class="ask-dnd-ghost"
          style={{ left: `${Number(pointerMenuDrag.x ?? 0) + 14}px`, top: `${Number(pointerMenuDrag.y ?? 0) + 14}px` }}
        >
          <AsukaIcon name={pointerMenuDrag.icon || DEFAULT_MENU_ICON} size={14} />
          <span>{pointerMenuDrag.label}</span>
          {pointerMenuDrag.zone ? <span class="ask-dnd-ghost__hint">{getI18n().t('overlay.dropIn', 'Drop in')} {pointerMenuDrag.zone}</span> : <span class="ask-dnd-ghost__hint">{getI18n().t('common.move', 'Move')}</span>}
        </div>
      ) : null}

      <style>{`
        @keyframes ask-boot-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
        @keyframes ask-boot-pulse { 0%, 100% { opacity: .72; transform: scaleX(.35); } 50% { opacity: 1; transform: scaleX(1); } }
      `}</style>
      <div class="ask-shell-card ask-theme-shell ask-responsive-shell-card ask-relative ask-flex ask-flex-col ask-w-full ask-h-full" data-asuka-responsive-breakpoint={responsiveBreakpoint} style={{ width: width ?? undefined, height: height ?? undefined, maxWidth: maxWidth ?? undefined, maxHeight: maxHeight ?? undefined }}>
        {showBootOverlay ? (
          <div class="ask-absolute ask-inset-0 ask-z-[60] ask-flex ask-items-center ask-justify-center ask-bg-surfaceAlt/90 ask-backdrop-blur-md" aria-live="polite" aria-busy="true">
            <div class="ask-flex ask-w-[min(92%,420px)] ask-flex-col ask-items-center ask-gap-5 ask-rounded-[28px] ask-border ask-border-borderSubtle ask-bg-surfaceAlt/95 ask-px-8 ask-py-9 ask-shadow-[0_24px_80px_rgba(15,23,42,0.16)]">
              <div class="ask-relative ask-flex ask-h-16 ask-w-16 ask-items-center ask-justify-center">
                <div class="ask-absolute ask-inset-0 ask-rounded-full ask-border-2 ask-border-primary/15"></div>
                <div class="ask-absolute ask-inset-[6px] ask-rounded-full ask-border-2 ask-border-transparent" style={{ borderTopColor: 'var(--ask-color-primary, #2563eb)', animation: 'ask-boot-rotate 1s linear infinite' }}></div>
                <div class="ask-h-2.5 ask-w-2.5 ask-rounded-full ask-bg-primary"></div>
              </div>
              <div class="ask-space-y-1 ask-text-center">
                <div class="ask-text-[15px] ask-font-semibold ask-text-textMain">{getI18n().t('ui.loadingAsukaDesigner', 'Loading AsukaDesigner')}</div>
                <div class="ask-text-sm ask-text-textMuted">{bootStatus}</div>
              </div>
              <div class="ask-h-1.5 ask-w-full ask-overflow-hidden ask-rounded-full ask-bg-surface">
                <div class="ask-h-full ask-origin-left ask-rounded-full ask-bg-primary" style={{ animation: 'ask-boot-pulse 1.2s ease-in-out infinite' }}></div>
              </div>
            </div>
          </div>
        ) : null}

        {showTopZone && (
          <div
            class={`ask-theme-toolbar ask-shrink-0 ${getEmptyZoneClass('top', visibleTopBlocks.length)} ask-flex ask-items-center ${topIdleEmpty ? '' : 'ask-border-b ask-border-borderSubtle'} ${isDragging && visibleTopBlocks.length === 0 ? 'ask-py-1.5 ask-px-1.5' : ''} ${isDragging ? 'ask-dnd-zone-active' : ''}`}
            onDragOver={handleZoneDragOver}
            data-zone="top"
            data-asuka-zone-collapsed={topCollapsed ? 'true' : 'false'}
            data-asuka-zone-empty={topIdleEmpty ? 'true' : 'false'}
            onDrop={handleZoneDrop('top')}
          >
            {!hideTopBottomCollapseBtn('top', visibleTopBlocks.length) && renderZoneToggleButton('top', topCollapsed, topCollapsed ? t('ui.expand_top_bar', 'Expand top bar') : t('ui.collapse_top_bar', 'Collapse top bar'), true)}

            {isDragging && visibleTopBlocks.length === 0 ? (
              <div
                class="ask-zone-drop-slot ask-zone-drop-slot--horizontal"
                onDragOver={handleZoneDragOver}
                data-zone="top"
                onDrop={handleZoneDrop('top')}
              />
            ) : (
              <ScrollArea
                axis="horizontal"
                className="ask-zone-scroll-area ask-flex-1 ask-min-w-0"
                viewportClassName="ask-zone-scroll-area__viewport ask-w-full"
                contentClassName={`ask-zone-scroll-area__content ask-zone-scroll-area__content--horizontal ask-inline-flex ask-items-center ask-gap-2 ask-min-w-max ${topCollapsed ? "" : "ask-pl-1"}`}
              >
                {visibleTopBlocks.map((block) => {
                  const blockButton = (
                    <button
                      key={block.id}
                      type="button"
                      draggable={false}
                      data-asuka-block-id={block.id}
                      data-asuka-layout-block="true"
                      data-asuka-module-trigger="true"
                      data-asuka-module-kind={String(block.kind ?? '')}
                      data-asuka-module-active={isBlockActive(block) ? 'true' : 'false'}
                      onPointerDown={handleBlockPointerDown(block, 'top')}
                      onClick={handleBlockClick(block)}
                      title={getBlockAccessLabel(block)}
                      aria-label={getMenuLabel(block)}
                      aria-pressed={isBlockActive(block)}
                      aria-disabled={!canOpenBlock(block)}
                      disabled={!canOpenBlock(block)}
                      class={`ask-theme-sidebar-item ask-theme-sidebar-item--horizontal ${topCollapsed ? 'ask-w-10 ask-h-10 ask-p-0 ask-justify-center ask-rounded-[14px]' : 'ask-gap-1.5 ask-px-3 ask-py-1 ask-rounded-full'} ask-inline-flex ask-items-center ask-border ask-text-[11px] ask-transition-colors ${isBlockActive(block) ? 'is-active' : ''} ${!canOpenBlock(block) ? 'ask-opacity-45 ask-cursor-not-allowed' : ''}`}
                    >
                      <AsukaIcon name={getMenuIcon(block)} className="ask-pointer-events-none" size={18} />
                      {!topCollapsed && <span class="ask-theme-sidebar-item__label ask-pointer-events-none">{getMenuLabel(block)}</span>}
                    </button>
                  );

                  return topCollapsed ? (
                    <Tooltip key={block.id} content={getBlockAccessLabel(block)} direction="bottom">
                      {blockButton}
                    </Tooltip>
                  ) : blockButton;
                })}
              </ScrollArea>
            )}
          </div>
        )}

        <div class="ask-flex ask-flex-1 ask-min-h-0 ask-relative">

          {/* Side drop placeholders are rendered inside the sidebar flow itself when the zone is empty. */}
          {showLeftZone && renderSidebar('left', visibleLeftBlocksForLayout, leftCollapsed, leftSidebarMode)}

          <main
            ref={bindMainOverlayTarget}
            data-asuka-overlay-region="main"
            class="ask-flex-1 ask-flex ask-flex-col ask-min-w-0 ask-min-h-0 ask-relative ask-overflow-hidden"
          >
            <div class="ask-theme-shell-header ask-flex ask-items-center ask-justify-between ask-px-4 ask-py-2 ask-border-b ask-border-borderSubtle">
              <div class="ask-shell-view-meta ask-flex ask-min-w-0 ask-items-center ask-gap-2 ask-overflow-hidden ask-text-[11px] ask-text-textMuted ask-pointer-events-none">
                {hasViews ? (
                  <>
                    <span>Zoom: {zoomPct}%</span>
                    <span class="ask-text-borderSubtle">•</span>
                    <span>View: {activeViewName}</span>
                  </>
                ) : (
                  <span>{t('ui.noViewsTitle', 'No views yet')}</span>
                )}
              </div>
              <div class="ask-flex ask-items-center ask-gap-1.5">
                {!hasViews ? (
                  (permissions as any)?.canCreateView ? (
                    <button
                      type="button"
                      onClick={handleCreateFirstView}
                      class="ask-theme-btn ask-theme-btn-primary ask-text-[11px] ask-px-3 ask-py-1 ask-rounded-full ask-font-medium"
                    >{t('ui.createFirstView', 'Create a view')}</button>
                  ) : null
                ) : null}
              </div>
            </div>

            <div class="ask-theme-stage ask-flex-1 ask-min-h-0 ask-flex ask-items-center ask-justify-center ask-relative">
              <ModuleHost defaultDrawerMountTarget={mainOverlayTarget} pagePortalBlocks={visiblePagePortalBlocks} />
              {hasViews ? (
                <div
                  ref={canvasHostRef}
                  class="ask-canvas-host ask-relative ask-w-full ask-h-full ask-min-h-0 ask-flex ask-items-center ask-justify-center"
                >
                  {rulerEnabled && activeRenderEngineKind === 'fabric' ? <RulerOverlay hostRef={canvasHostRef} frameRef={canvasFrameRef} /> : null}

                  <div
                    class="ask-canvas-frame-shell ask-relative ask-flex-none ask-overflow-hidden"
                    style={{
                      width: canvasFrameSize.w ? `${canvasFrameSize.w}px` : '100%',
                      height: canvasFrameSize.h ? `${canvasFrameSize.h}px` : '100%',
                    }}
                  >
                    {/* Ratio-locked rendering container. Engines may replace all of its children. */}
                    <div
                      ref={canvasFrameRef}
                      class="ask-canvas-frame ask-relative ask-overflow-hidden ask-w-full ask-h-full"
                      data-asuka-engine={activeRenderEngineKind}
                    >
                      {activeRenderEngineKind === '3d' && !external3dEngineReady ? null : null}
                    </div>
                  </div>

                  {placementFeedback ? (
                    <div
                      class={`ask-placement-feedback ask-placement-feedback--${placementFeedback.tone}`}
                      role="status"
                      aria-live="polite"
                    >
                      <AsukaIcon name={placementFeedback.icon} size={13} />
                      <span>{placementFeedback.label}</span>
                    </div>
                  ) : null}
                  <div class="ask-powered-by-asuka" aria-label={getI18n().t('branding.poweredByAsukaDesigner', 'Powered by Asuka Designer')}>
                    <img class="ask-powered-by-asuka__mark" src={asukaDesignerMarkUrl} alt="" aria-hidden="true" />
                    <span class="ask-powered-by-asuka__prefix">{getI18n().t('branding.poweredBy', 'Powered by')}</span>
                    <span class="ask-powered-by-asuka__brand">{getI18n().t('branding.asukaDesigner', 'Asuka Designer')}</span>
                  </div>
                  {activeRenderEngineKind === '3d' && !external3dEngineReady ? (
                    <div class="ask-absolute ask-inset-0 ask-z-30 ask-flex ask-items-center ask-justify-center ask-bg-surface/90 ask-p-6">
                      <div class="ask-flex ask-max-w-md ask-items-center ask-gap-3 ask-text-left">
                        <AsukaIcon
                          name={external3dEngineError ? 'warning' : 'loader'}
                          size={22}
                          className={external3dEngineError ? 'ask-text-danger' : 'ask-animate-spin ask-text-primary'}
                        />
                        <div>
                          <div class="ask-text-sm ask-font-semibold ask-text-textMain">
                            {external3dEngineError
                              ? t('ui.3DRendererLoadFailed', 'The 3D renderer could not be loaded.')
                              : t('ui.loading3DRenderer', 'Loading 3D renderer…')}
                          </div>
                          {external3dEngineError ? (
                            <>
                              <div class="ask-mt-1 ask-text-xs ask-text-textMuted">{external3dEngineError}</div>
                              <button
                                type="button"
                                class="ask-theme-btn ask-theme-btn-secondary ask-mt-3 ask-inline-flex ask-items-center ask-justify-center ask-rounded-lg ask-px-3 ask-py-2 ask-text-xs ask-font-semibold"
                                onClick={() => {
                                  setExternal3dEngineError(null);
                                  setExternal3dEngineReady(false);
                                  setRenderEngineRetryVersion((value) => value + 1);
                                }}
                              >
                                {t('ui.retry3DRenderer', 'Retry 3D renderer')}
                              </button>
                            </>
                          ) : null}
                        </div>
                      </div>
                    </div>
                  ) : null}
                </div>
              ) : (
                <div class="ask-w-full ask-h-full ask-flex ask-items-center ask-justify-center">
                  <div class="ask-theme-panel-block ask-max-w-md ask-w-full ask-mx-4 ask-rounded-2xl ask-p-6 ask-text-center">
                    <div class="ask-text-sm ask-font-medium ask-text-textMain">{t('ui.noViewsTitle', 'No views yet')}</div>
                    <div class="ask-mt-1 ask-text-[11px] ask-text-textMuted">
                      {t('ui.noViewsBody', 'Create a view to start designing.')}
                    </div>
                    {(permissions as any)?.canCreateView ? (
                      <button
                        type="button"
                        onClick={handleCreateFirstView}
                        class="ask-theme-btn ask-theme-btn-primary ask-mt-4 ask-inline-flex ask-items-center ask-justify-center ask-gap-2 ask-px-4 ask-py-2 ask-rounded-xl ask-text-[12px] ask-font-medium"
                      >
                        {t('ui.createView', 'Create a view')}
                      </button>
                    ) : (
                      <div class="ask-mt-4 ask-text-[11px] ask-text-textMuted">
                        {t('ui.noViewsNoPermission', "You don't have permission to create views.")}
                      </div>
                    )}
                  </div>
                </div>
              )}

{/* Magnifier overlay canvas (action module). Positioned over Fabric canvas. */}
<canvas
  ref={magnifierOverlayRef}
  class="ask-absolute ask-pointer-events-none ask-z-40"
  style={{ display: activeRenderEngineKind === 'fabric' && magnifierEnabled && magnifierActive ? 'block' : 'none' }}
/>


              {(shouldRenderUndo || shouldRenderRedo || shouldRenderReset) ? (
                <div class="ask-absolute ask-top-3 ask-right-4 ask-flex ask-items-center ask-gap-1.5">
                  {shouldRenderUndo ? (
                    <Tooltip content={getI18n().t('common.undo')} direction="bottom">
                      <IconButton
                        onClick={handleUndo}
                        disabled={!hasHistory || !canUndoHistory || !canUseUndo}
                        title={getI18n().t('common.undo')}
                        aria-label={getI18n().t('common.undo')}
                        className="ask-theme-btn ask-theme-btn-secondary ask-h-9 ask-w-9 ask-rounded-full ask-border ask-shadow-sm"
                      >
                        <AsukaIcon name="undo" size={14} />
                      </IconButton>
                    </Tooltip>
                  ) : null}
                  {shouldRenderRedo ? (
                    <Tooltip content={getI18n().t('common.redo')} direction="bottom">
                      <IconButton
                        onClick={handleRedo}
                        disabled={!hasHistory || !canRedoHistory || !canUseRedo}
                        title={getI18n().t('common.redo')}
                        aria-label={getI18n().t('common.redo')}
                        className="ask-theme-btn ask-theme-btn-secondary ask-h-9 ask-w-9 ask-rounded-full ask-border ask-shadow-sm"
                      >
                        <AsukaIcon name="redo" size={14} />
                      </IconButton>
                    </Tooltip>
                  ) : null}
                  {shouldRenderReset ? (
                    <Tooltip content={getI18n().t('common.reset')} direction="bottom">
                      <IconButton
                        onClick={handleReset}
                        disabled={!hasHistory || !canUseReset}
                        title={getI18n().t('common.reset')}
                        aria-label={getI18n().t('common.reset')}
                        className="ask-theme-btn ask-theme-btn-secondary ask-h-9 ask-w-9 ask-rounded-full ask-border ask-shadow-sm"
                      >
                        <AsukaIcon name="reset" size={14} />
                      </IconButton>
                    </Tooltip>
                  ) : null}
                </div>
              ) : null}

              {canvasHover?.label ? (
                <div
                  ref={canvasHoverTooltipRef}
                  class="ask-tooltip ask-tooltip--top is-visible ask-canvas-hover-tooltip"
                  style={{
                    left: `${canvasHover.clientX}px`,
                    top: `${Math.max(10, canvasHover.clientY - 14)}px`,
                    transform: 'translate(-50%, -100%) scale(1)',
                  }}
                  role="tooltip"
                >
                  <div class="ask-tooltip__bubble">{canvasHover.label}</div>
                </div>
              ) : null}
              <div
                ref={canvasHover3DTooltipRef}
                class="ask-tooltip ask-tooltip--top ask-canvas-hover-tooltip"
                style={{
                  display: 'none',
                  left: '0px',
                  top: '0px',
                  transform: 'translate(-50%, -100%) scale(1)',
                  pointerEvents: 'none',
                }}
                role="tooltip"
                aria-hidden="true"
              >
                <div ref={canvasHover3DTooltipLabelRef} class="ask-tooltip__bubble" />
              </div>

              <SelectionToolbar
                box={selectionBox}
                visible={Boolean(selectionBox) && !isTransformingSelection}
                engine={createLiveModuleRenderEngine(activeRenderEngineKind as any)}
              />
            </div>

            {shouldRenderViewsPagination ? (
              <div class="ask-theme-toolbar ask-shrink-0 ask-border-t ask-border-borderSubtle">
                <Carousel
                  items={viewCarouselItems}
                  activeId={activeViewId}
                  onSelect={handleViewCarouselSelect}
                  label={getI18n().t('modules.views', 'Views')}
                  paginationMode={viewsPaginationMode}
                  showLabels={viewsPaginationConfig.showLabels === true}
                  enableGrabNavigation={viewsPaginationConfig.enableGrabNavigation !== false}
                />
              </div>
            ) : null}

            {showBottomZone && (
              <div
                key={`bottom-zone-${visibleBottomBlocks.map((block) => `${block.id}:${block.order}`).join('|')}-${bottomCollapsed ? 'collapsed' : 'expanded'}`}
                class={`ask-theme-toolbar ask-shrink-0 ${getEmptyZoneClass('bottom', visibleBottomBlocks.length)} ask-flex ask-items-center ${bottomIdleEmpty ? '' : 'ask-border-t ask-border-borderSubtle'} ${isDragging && visibleBottomBlocks.length === 0 ? 'ask-py-1.5 ask-px-1.5' : ''} ${isDragging ? 'ask-dnd-zone-active' : ''}`}
                onDragOver={handleZoneDragOver}
                data-zone="bottom"
                data-asuka-zone-collapsed={bottomCollapsed ? 'true' : 'false'}
                data-asuka-zone-empty={bottomIdleEmpty ? 'true' : 'false'}
                onDrop={handleZoneDrop('bottom')}
              >
                {!hideTopBottomCollapseBtn('bottom', visibleBottomBlocks.length) && renderZoneToggleButton('bottom', bottomCollapsed, bottomCollapsed ? t('ui.expand_bottom_bar', 'Expand bottom bar') : t('ui.collapse_bottom_bar', 'Collapse bottom bar'), true)}

                {isDragging && visibleBottomBlocks.length === 0 ? (
                  <div
                    class="ask-zone-drop-slot ask-zone-drop-slot--horizontal"
                    onDragOver={handleZoneDragOver}
                    data-zone="bottom"
                    onDrop={handleZoneDrop('bottom')}
                  />
                ) : (
                  <ScrollArea
                    axis="horizontal"
                    className="ask-zone-scroll-area ask-flex-1 ask-min-w-0"
                    viewportClassName="ask-zone-scroll-area__viewport ask-w-full"
                    contentClassName={`ask-inline-flex ask-items-center ask-gap-2 ask-min-w-max`}
                  >
                    {visibleBottomBlocks.map((block) => {
                      const blockButton = (
                        <button
                          key={block.id}
                          type="button"
                          draggable={false}
                          data-asuka-block-id={block.id}
                          data-asuka-layout-block="true"
                          data-asuka-module-trigger="true"
                          data-asuka-module-kind={String(block.kind ?? '')}
                          data-asuka-module-active={isBlockActive(block) ? 'true' : 'false'}
                          onPointerDown={handleBlockPointerDown(block, 'bottom')}
                          onClick={handleBlockClick(block)}
                          title={getBlockAccessLabel(block)}
                          aria-label={getMenuLabel(block)}
                          aria-pressed={isBlockActive(block)}
                          aria-disabled={!canOpenBlock(block)}
                          disabled={!canOpenBlock(block)}
                          class={`ask-theme-sidebar-item ask-theme-sidebar-item--horizontal ${bottomCollapsed ? 'ask-w-10 ask-h-10 ask-p-0 ask-justify-center ask-rounded-[14px]' : 'ask-gap-1.5 ask-px-3 ask-py-1 ask-rounded-full'} ask-inline-flex ask-items-center ask-border ask-text-[11px] ask-transition-colors ${isBlockActive(block) ? 'is-active' : ''} ${!canOpenBlock(block) ? 'ask-opacity-45 ask-cursor-not-allowed' : ''}`}
                        >
                          <AsukaIcon name={getMenuIcon(block)} className="ask-pointer-events-none" size={18} />
                          {!bottomCollapsed && <span class="ask-theme-sidebar-item__label ask-pointer-events-none">{getMenuLabel(block)}</span>}
                        </button>
                      );

                      return bottomCollapsed ? (
                        <Tooltip key={block.id} content={getBlockAccessLabel(block)} direction="top">
                          {blockButton}
                        </Tooltip>
                      ) : blockButton;
                    })}
                  </ScrollArea>
                )}
              </div>
            )}
          </main>

          {showRightZone && renderSidebar('right', visibleRightBlocksForLayout, rightCollapsed, rightSidebarMode)}

        </div>

        {moveSideZonesToBottomNav && mobileSideBlocks.length > 0 ? (
          <nav ref={mobileNavRef} class="ask-responsive-bottom-nav" aria-label={t('ui.mobile_navigation', 'Mobile navigation')}>
            <div class="ask-responsive-bottom-nav__items">
              {mobilePrimaryBlocks.map((block) => renderMobileNavButton(block))}
              {mobileOverflowBlocks.length > 0 ? (
                <div class="ask-responsive-bottom-nav__more-wrap">
                  <button
                    type="button"
                    class={`ask-responsive-bottom-nav__item ask-responsive-bottom-nav__more ${mobileMoreOpen ? 'is-active' : ''}`}
                    onClick={() => setMobileMoreOpen((value) => !value)}
                    aria-expanded={mobileMoreOpen ? 'true' : 'false'}
                    aria-haspopup="menu"
                  >
                    <AsukaIcon name="menu" className="ask-pointer-events-none" size={18} />
                    <span class="ask-responsive-bottom-nav__label ask-pointer-events-none">{t('common.more', 'More')}</span>
                  </button>
                  {mobileMoreOpen ? (
                    <div class="ask-responsive-bottom-nav__more-panel" role="menu" data-asuka-overflow-count={mobileOverflowBlocks.length}>
                      {mobileOverflowBlocks.map((block) => renderMobileNavButton(block, true))}
                    </div>
                  ) : null}
                </div>
              ) : null}
            </div>
          </nav>
        ) : null}

      </div>
    </div>
  );
};
