'use client';

import React, { useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, forwardRef } from 'react';
import { Add01Icon, ArrowRight01Icon, Copy01Icon, Delete02Icon, DragDropVerticalIcon, RepeatIcon } from '@zen/core/shared/icons';
import { BOX_CLASS, ITEM_CLASS, ITEM_DANGER_CLASS, SEPARATOR_CLASS } from './inline/menuStyles.js';

// Style « boîte » pour l'icône d'un type de bloc, repris du SlashMenu.
const TYPE_ICON_BOX_CLASS = 'w-8 h-8 flex items-center justify-center rounded-md border border-neutral-200 dark:border-neutral-700 text-xs font-medium text-neutral-700 dark:text-neutral-300 flex-shrink-0';

const DROPDOWN_MAX_HEIGHT = 360;
const DROPDOWN_MIN_HEIGHT = 160;
const DROPDOWN_VIEWPORT_MARGIN = 8;
const DROPDOWN_GAP = 6;

// Décide si un dropdown ancré au `triggerRef` doit s'ouvrir au-dessus ou en
// dessous, et calcule le maxHeight disponible. Recalculé chaque fois que le
// menu s'ouvre (anchor stable tant qu'il est ouvert). Quand il s'ouvre vers
// le haut, le contenu est ancré par `bottom: 100%` côté caller — comme ça
// le bas reste collé au bouton et le contenu rétrécit vers le haut.
function useDropdownPlacement(open, triggerRef) {
  const [placement, setPlacement] = useState({ side: 'below', maxHeight: DROPDOWN_MAX_HEIGHT });
  useLayoutEffect(() => {
    if (!open) return;
    const el = triggerRef.current;
    if (!el || typeof window === 'undefined') return;
    const rect = el.getBoundingClientRect();
    const vh = window.innerHeight;
    const spaceBelow = vh - rect.bottom - DROPDOWN_VIEWPORT_MARGIN - DROPDOWN_GAP;
    const spaceAbove = rect.top - DROPDOWN_VIEWPORT_MARGIN - DROPDOWN_GAP;
    if (spaceBelow >= Math.min(DROPDOWN_MAX_HEIGHT, DROPDOWN_MIN_HEIGHT) || spaceBelow >= spaceAbove) {
      setPlacement({ side: 'below', maxHeight: Math.max(DROPDOWN_MIN_HEIGHT, Math.min(DROPDOWN_MAX_HEIGHT, spaceBelow)) });
    } else {
      setPlacement({ side: 'above', maxHeight: Math.max(DROPDOWN_MIN_HEIGHT, Math.min(DROPDOWN_MAX_HEIGHT, spaceAbove)) });
    }
  }, [open, triggerRef]);
  return placement;
}
import { getBlockDef, listBlocks } from './blockRegistry.js';
import { inlineLength } from './inline/types.js';
import { inlineToDom, domToInline } from './inline/serialize.js';
import { htmlToBlocks, notionJsonToBlocks } from './inline/clipboard.js';
import {
  getCaretOffset,
  getCaretRange,
  setCaretOffset,
  setCaretRange,
  focusEnd,
  isCaretAtStart,
} from './utils/caret.js';

// Dropdown manuel pour le bouton « + » : liste tous les types de blocs
// disponibles (filtrés par `enabledBlocks`) et insère le type choisi juste
// après le bloc courant. Même mécanique de fermeture que BlockActionsMenu :
// clic extérieur + Escape.
function BlockInsertMenu({
  open,
  setOpen,
  disabled,
  insertOptions,
  onSelectBlock,
  onInsert,
}) {
  const containerRef = useRef(null);
  const triggerRef = useRef(null);
  const { side, maxHeight } = useDropdownPlacement(open, triggerRef);

  useEffect(() => {
    if (!open) return;
    function handleDocMouseDown(e) {
      if (containerRef.current && !containerRef.current.contains(e.target)) {
        setOpen(false);
      }
    }
    function handleKeyDown(e) {
      if (e.key === 'Escape') setOpen(false);
    }
    document.addEventListener('mousedown', handleDocMouseDown);
    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('mousedown', handleDocMouseDown);
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [open, setOpen]);

  function handleButtonClick() {
    onSelectBlock?.();
    setOpen(!open);
  }

  function selectAndClose(type) {
    return () => {
      onInsert?.(type);
      setOpen(false);
    };
  }

  const panelPositionClass = side === 'above' ? 'bottom-full mb-1' : 'top-full mt-1';

  return (
    <div ref={containerRef} className="relative">
      <button
        ref={triggerRef}
        type="button"
        tabIndex={-1}
        title="Insérer un bloc"
        onClick={handleButtonClick}
        disabled={disabled}
        className="w-5 h-5 flex items-center justify-center rounded text-neutral-500 hover:bg-neutral-200 dark:hover:bg-neutral-700/60 hover:text-neutral-900 dark:hover:text-white text-sm leading-none outline-none"
      >
        <Add01Icon width={14} height={14} />
      </button>

      {open && (
        <div
          className={`absolute left-0 ${panelPositionClass} w-64 ${BOX_CLASS} z-50 flex flex-col`}
          style={{ maxHeight }}
        >
          <div className="p-1.5 flex flex-col gap-0.5 overflow-y-auto">
            <div className="px-2 pt-1 pb-1 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-neutral-500">
              Insérer un bloc
            </div>
            {insertOptions.map((d) => (
              <button
                key={d.type}
                type="button"
                onClick={selectAndClose(d.type)}
                className="cursor-pointer w-full flex items-center gap-3 px-2 py-1.5 rounded-lg text-[13px] leading-none text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-white/5 hover:text-neutral-900 dark:hover:text-white transition-colors duration-[120ms] ease-out text-left"
              >
                <span className={TYPE_ICON_BOX_CLASS}>
                  {d.icon}
                </span>
                {d.label}
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// Dropdown manuel pour le menu d'actions du bloc. Headless UI Menu ouvrait
// le panneau dès le pointerdown, ce qui empêchait de démarrer un drag avec
// un clic-maintenu. Ici on n'ouvre que sur le `click` (= mouseup sans drag),
// après consultation du drapeau `getJustDragged()` exposé par le parent.
function BlockActionsMenu({
  open,
  setOpen,
  disabled,
  currentLabel,
  transformOptions,
  onMouseDownHandle,
  getJustDragged,
  onSelectBlock,
  onTransform,
  onDuplicate,
  onDelete,
}) {
  const containerRef = useRef(null);
  const triggerRef = useRef(null);
  const { side } = useDropdownPlacement(open, triggerRef);
  const [submenuOpen, setSubmenuOpen] = useState(false);
  const submenuTriggerRef = useRef(null);
  const submenuPanelRef = useRef(null);
  const [submenuSide, setSubmenuSide] = useState('below');

  useLayoutEffect(() => {
    if (!submenuOpen) return;
    const trigger = submenuTriggerRef.current;
    const panel = submenuPanelRef.current;
    if (!trigger || !panel || typeof window === 'undefined') return;
    const triggerRect = trigger.getBoundingClientRect();
    const panelHeight = panel.getBoundingClientRect().height;
    const vh = window.innerHeight;
    const spaceBelow = vh - triggerRect.top - DROPDOWN_VIEWPORT_MARGIN;
    const spaceAbove = triggerRect.bottom - DROPDOWN_VIEWPORT_MARGIN;
    if (panelHeight > spaceBelow && spaceAbove > spaceBelow) {
      setSubmenuSide('above');
    } else {
      setSubmenuSide('below');
    }
  }, [submenuOpen, transformOptions]);

  // Fermeture sur clic extérieur ou Escape.
  useEffect(() => {
    if (!open) return;
    function handleDocMouseDown(e) {
      if (containerRef.current && !containerRef.current.contains(e.target)) {
        setOpen(false);
        setSubmenuOpen(false);
      }
    }
    function handleKeyDown(e) {
      if (e.key === 'Escape') {
        setOpen(false);
        setSubmenuOpen(false);
      }
    }
    document.addEventListener('mousedown', handleDocMouseDown);
    document.addEventListener('keydown', handleKeyDown);
    return () => {
      document.removeEventListener('mousedown', handleDocMouseDown);
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [open, setOpen]);

  function handleButtonClick() {
    if (getJustDragged()) return; // un drag vient de se terminer → pas de menu
    onSelectBlock?.();
    setOpen(!open);
  }

  function selectAndClose(fn) {
    return () => {
      fn?.();
      setOpen(false);
      setSubmenuOpen(false);
    };
  }

  const panelPositionClass = side === 'above' ? 'bottom-full mb-1' : 'top-full mt-1';

  return (
    <div ref={containerRef} className="relative">
      <button
        ref={triggerRef}
        type="button"
        tabIndex={-1}
        title="Glisser pour réordonner ou cliquer pour les actions"
        onMouseDown={onMouseDownHandle}
        onClick={handleButtonClick}
        disabled={disabled}
        className="w-5 h-5 flex items-center justify-center rounded text-neutral-500 hover:bg-neutral-200 dark:hover:bg-neutral-700/60 hover:text-neutral-900 dark:hover:text-white text-xs cursor-grab active:cursor-grabbing leading-none outline-none"
      >
        <DragDropVerticalIcon width={14} height={14} />
      </button>

      {open && (
        <div
          className={`absolute left-0 ${panelPositionClass} w-56 ${BOX_CLASS} z-50 flex flex-col`}
        >
          <div className="p-1.5 flex flex-col gap-0.5">
            {currentLabel && (
              <div className="px-2 pt-1 pb-1 text-[10px] font-medium uppercase tracking-wider text-neutral-400 dark:text-neutral-500">
                {currentLabel}
              </div>
            )}
            {transformOptions.length > 0 && (
              <div ref={submenuTriggerRef} className="relative">
                <div
                  role="button"
                  tabIndex={0}
                  onClick={() => setSubmenuOpen(prev => !prev)}
                  className={`${ITEM_CLASS} ${submenuOpen ? 'bg-neutral-100 dark:bg-white/5 text-neutral-900 dark:text-white' : ''}`}
                >
                  <RepeatIcon className="w-4 h-4 shrink-0" />
                  <span className="flex-1">Transformer</span>
                  <ArrowRight01Icon className="w-3.5 h-3.5 shrink-0" />
                </div>
                {submenuOpen && (
                  <div
                    ref={submenuPanelRef}
                    className={`absolute left-full ${submenuSide === 'above' ? 'bottom-0' : 'top-0'} ml-1 w-56 ${BOX_CLASS} p-1.5 flex flex-col gap-0.5 z-50`}
                  >
                    {transformOptions.map((d) => (
                      <button
                        key={d.type}
                        type="button"
                        onClick={selectAndClose(() => onTransform?.(d.type))}
                        className="cursor-pointer w-full flex items-center gap-3 px-2 py-1.5 rounded-lg text-[13px] leading-none text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-white/5 hover:text-neutral-900 dark:hover:text-white transition-colors duration-[120ms] ease-out text-left"
                      >
                        <span className={TYPE_ICON_BOX_CLASS}>
                          {d.icon}
                        </span>
                        {d.label}
                      </button>
                    ))}
                  </div>
                )}
              </div>
            )}

            <button
              type="button"
              onClick={selectAndClose(onDuplicate)}
              className={ITEM_CLASS}
            >
              <Copy01Icon className="w-4 h-4 shrink-0" />
              Dupliquer
            </button>

            <div className={SEPARATOR_CLASS} />

            <button
              type="button"
              onClick={selectAndClose(onDelete)}
              className={ITEM_DANGER_CLASS}
            >
              <Delete02Icon className="w-4 h-4 shrink-0" />
              Supprimer
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// Wrapper d'un bloc unique. Gère :
// - le contentEditable pour les blocs texte (sync uncontrolled ↔ value)
// - les handles à gauche (drag, +)
// - les évènements clavier (Enter, Backspace, /, shortcuts markdown)
// - le drop zone pour le drag-and-drop natif
//
// L'orchestrateur (BlockEditor) reçoit des évènements via les callbacks et
// mute la liste des blocs en conséquence. Le contenu inline est un
// `InlineNode[]` (cf. inline/types.js) — sérialisé vers le DOM via
// `inlineToDom` / `domToInline`.

const Block = forwardRef(function Block(
  {
    block,
    index,
    numberedIndex,
    disabled,
    isDragOverTop,
    isDragOverBottom,
    isSelected,
    onContentChange,
    onBlockPatch,
    onEnter,
    onBackspaceAtStart,
    onSlashOpen,
    onSlashClose,
    onSelectAllBlocks,
    onSelectBlock,
    onShortcutMatch,
    onFocus,
    onPasteInline,
    onPasteBlocks,
    onDragStart,
    onDragEnd,
    onDragOver,
    onDragLeave,
    onDrop,
    onTransformBlock,
    onDuplicateBlock,
    onDeleteBlock,
    onInsertBlock,
    enabledBlocks,
  },
  ref,
) {
  const def = getBlockDef(block.type);
  const transformOptions = useMemo(() => {
    const all = listBlocks();
    const allowed = enabledBlocks ? all.filter(b => enabledBlocks.includes(b.type)) : all;
    return allowed.filter(d => d.isText && d.type !== block.type);
  }, [enabledBlocks, block.type]);
  const insertOptions = useMemo(() => {
    const all = listBlocks();
    return enabledBlocks ? all.filter(b => enabledBlocks.includes(b.type)) : all;
  }, [enabledBlocks]);
  const editableRef = useRef(null);
  const [hovered, setHovered] = useState(false);
  const [draggable, setDraggable] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);
  const [insertOpen, setInsertOpen] = useState(false);
  // Drapeau de drag réel : passe à true au premier dragstart, consommé par
  // le onClick du MenuButton pour ne pas ouvrir le menu après un drag.
  const justDraggedRef = useRef(false);
  // Référence vers le dernier contenu qu'on a écrit dans le DOM. Permet de
  // détecter si un changement de `block.content` provient d'un évènement
  // externe (undo, transform, toolbar) plutôt que de la frappe utilisateur.
  // Si la nouvelle valeur === lastWritten, on ne touche pas au DOM (sinon
  // on perdrait le caret).
  const lastWrittenRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: (offset) => {
      if (!def?.isText) return;
      if (typeof offset === 'number') setCaretOffset(editableRef.current, offset);
      else focusEnd(editableRef.current);
    },
    getElement: () => editableRef.current,
    getCaretRange: () => getCaretRange(editableRef.current),
    setCaretRange: (start, end) => setCaretRange(editableRef.current, start, end),
  }), [def]);

  // Synchronisation contrôlée → DOM. On compare par référence à
  // `lastWrittenRef.current` : si égal, c'est que la valeur reçue est celle
  // qu'on vient d'émettre (frappe), donc le DOM est déjà à jour. Sinon on
  // réécrit le sous-arbre et on restaure le caret quand l'élément a le focus.
  useEffect(() => {
    if (!def?.isText) return;
    const el = editableRef.current;
    if (!el) return;
    if (lastWrittenRef.current === block.content) return;
    const hadFocus = document.activeElement === el;
    const range = hadFocus ? getCaretRange(el) : null;
    el.replaceChildren(inlineToDom(block.content ?? []));
    lastWrittenRef.current = block.content;
    if (hadFocus && range) {
      const total = inlineLength(block.content ?? []);
      setCaretRange(el, Math.min(range.start, total), Math.min(range.end, total));
    }
  }, [block.content, def?.isText]);

  if (!def) {
    return (
      <div className="text-xs text-red-500 px-2 py-1">
        Type de bloc inconnu : {block.type}
      </div>
    );
  }

  function emitContent(content) {
    lastWrittenRef.current = content;
    onContentChange?.(block.id, content);
  }

  function handleInput() {
    const el = editableRef.current;
    if (!el) return;
    const text = el.textContent ?? '';

    // Détection slash menu : "/" au tout début ou après un espace
    if (!disabled && text === '/' && getCaretOffset(el) === 1) {
      onSlashOpen?.({ blockId: block.id, query: '' });
    } else if (text.startsWith('/') && !text.includes(' ', 1)) {
      onSlashOpen?.({ blockId: block.id, query: text.slice(1) });
    } else {
      onSlashClose?.();
    }

    // Markdown shortcut : si le contenu commence par un préfixe connu suivi
    // d'un espace, on demande à l'orchestrateur de transformer le bloc.
    if (!def.disableShortcuts) {
      const match = text.match(/^(#{1,6} |- |1\. |> |```\s|---|\[ ?\] )/);
      if (match) {
        const prefix = match[1];
        onShortcutMatch?.({ blockId: block.id, prefix, rest: text.slice(prefix.length) });
        return;
      }
    }

    emitContent(domToInline(el));
  }

  function handleKeyDown(e) {
    // Si le slash menu est ouvert (texte commence par "/" sans espace),
    // ne pas intercepter Enter / Arrow / Escape — l'orchestrateur les gère.
    const el = editableRef.current;
    const text = el?.textContent ?? '';
    const slashOpen = text.startsWith('/') && !text.slice(1).includes(' ');
    if (slashOpen && ['Enter', 'ArrowUp', 'ArrowDown', 'Escape'].includes(e.key)) {
      return;
    }

    if (e.key === '/') {
      // L'ouverture se fait dans handleInput après que le caractère soit écrit.
      return;
    }

    // Ctrl/Cmd+A : 1er appui → sélectionne le contenu du bloc courant.
    // 2e appui (le contenu est déjà entièrement sélectionné) → bascule en
    // sélection multi-blocs (tous les blocs).
    if ((e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'A')) {
      if (el) {
        e.preventDefault();
        const sel = window.getSelection();
        const txt = el.textContent ?? '';
        if (txt.length === 0) {
          onSelectAllBlocks?.();
        } else {
          const fullySelected =
            !!sel && sel.rangeCount > 0 && !sel.isCollapsed &&
            el.contains(sel.anchorNode) && el.contains(sel.focusNode) &&
            sel.toString() === txt;
          if (fullySelected) {
            onSelectAllBlocks?.();
          } else {
            const range = document.createRange();
            range.selectNodeContents(el);
            sel?.removeAllRanges();
            sel?.addRange(range);
          }
        }
      }
      return;
    }

    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      const el = editableRef.current;
      const offset = el ? getCaretOffset(el) : 0;
      const content = el ? domToInline(el) : [];
      // On synchronise lastWritten : la valeur que `BlockEditor` va écrire
      // dans le bloc courant (le before) sera dérivée de ce contenu.
      lastWrittenRef.current = null;
      onEnter?.({ blockId: block.id, offset, content });
      return;
    }

    if (e.key === 'Backspace') {
      const el = editableRef.current;
      const sel = typeof window !== 'undefined' ? window.getSelection() : null;
      // Si une sélection non-vide existe (ex. après Ctrl+A), laisser le
      // navigateur supprimer le texte sélectionné — ne pas déclencher le
      // merge avec le bloc précédent.
      if (el && sel?.isCollapsed !== false && isCaretAtStart(el)) {
        e.preventDefault();
        const content = domToInline(el);
        lastWrittenRef.current = null;
        onBackspaceAtStart?.({ blockId: block.id, content });
      }
    }
  }

  function handleFocus() {
    if (def?.isText && !disabled) {
      const el = editableRef.current;
      const text = el?.textContent ?? '';
      if (text.startsWith('/') && !text.slice(1).includes(' ')) {
        onSlashOpen?.({ blockId: block.id, query: text.slice(1) });
      }
    }
    onFocus?.(block.id);
  }

  function handlePaste(e) {
    e.preventDefault();
    // Notion expose ses blocs natifs via sa MIME propriétaire en plus du HTML.
    // Le HTML aplatit `to_do` en `<ul><li>`, donc on prend le JSON quand il est
    // là — sinon on retombe sur le HTML standard.
    const notionJson = e.clipboardData.getData('text/_notion-blocks-v3-production');
    const html = e.clipboardData.getData('text/html');
    const text = e.clipboardData.getData('text/plain');

    if (notionJson) {
      const pasted = notionJsonToBlocks(notionJson);
      if (pasted.length > 0) {
        if (pasted.length === 1) {
          const only = pasted[0];
          const onlyDef = getBlockDef(only.type);
          if (only.type === 'paragraph' && onlyDef?.isText) {
            onPasteInline?.({ blockId: block.id, inline: only.content ?? [] });
            return;
          }
        }
        onPasteBlocks?.({ blockId: block.id, blocks: pasted });
        return;
      }
    }

    // Si du HTML est disponible, on tente le parsing rich. Sinon fallback
    // sur le texte brut inséré au caret.
    if (html) {
      const pasted = htmlToBlocks(html);
      if (pasted.length === 1) {
        const only = pasted[0];
        const onlyDef = getBlockDef(only.type);
        // Un seul paragraph collé → on splice son contenu inline au caret
        // pour conserver l'UX d'un paste « simple » (pas de découpe du bloc).
        if (only.type === 'paragraph' && onlyDef?.isText) {
          onPasteInline?.({ blockId: block.id, inline: only.content ?? [] });
          return;
        }
      }
      if (pasted.length > 0) {
        onPasteBlocks?.({ blockId: block.id, blocks: pasted });
        return;
      }
      // pasted vide (HTML sans contenu interprétable) → on retombe sur text
    }

    if (!text) return;
    // execCommand reste la voie la plus simple pour une insertion au caret
    // qui s'intègre dans la pile undo native du contentEditable. Sa
    // dépréciation n'a pas de remplacement standard à ce jour.
    document.execCommand('insertText', false, text);
  }

  function handleHandleMouseDown() {
    setDraggable(true);
  }

  function handleDragStart(e) {
    if (!draggable) {
      e.preventDefault();
      return;
    }
    justDraggedRef.current = true;
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/plain', block.id);
    onDragStart?.(block.id);
  }

  function handleDragEnd() {
    setDraggable(false);
    onDragEnd?.();
  }

  function handleDragOver(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
    const rect = e.currentTarget.getBoundingClientRect();
    const isTop = e.clientY < rect.top + rect.height / 2;
    onDragOver?.(block.id, isTop ? 'top' : 'bottom');
  }

  function handleDrop(e) {
    e.preventDefault();
    const sourceId = e.dataTransfer.getData('text/plain');
    if (sourceId && sourceId !== block.id) {
      onDrop?.(sourceId, block.id);
    }
  }

  const isEmpty = def.isText && inlineLength(block.content ?? []) === 0;
  const checklistChecked = block.type === 'checklist' && !!block.checked;
  const checklistClasses = checklistChecked ? ' line-through opacity-60' : '';

  const dropIndicator = (
    <>
      {isDragOverTop && (
        <div className="absolute left-0 right-0 -top-px h-0.5 bg-blue-500 dark:bg-blue-400 pointer-events-none" />
      )}
      {isDragOverBottom && (
        <div className="absolute left-0 right-0 -bottom-px h-0.5 bg-blue-500 dark:bg-blue-400 pointer-events-none" />
      )}
    </>
  );

  return (
    <div
      className="relative group flex items-start gap-1 px-1 py-1.5"
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      draggable={draggable}
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}
      onDragOver={handleDragOver}
      onDragLeave={onDragLeave}
      onDrop={handleDrop}
      data-block-id={block.id}
    >
      {dropIndicator}
      {isSelected && (
        <div
          className="absolute inset-0 rounded-md bg-blue-500/20 dark:bg-blue-400/20 pointer-events-none"
          aria-hidden="true"
        />
      )}

      <div
        className={`flex items-center gap-0.5 pt-1 transition-opacity ${hovered || menuOpen || insertOpen ? 'opacity-100' : 'opacity-0'} ${disabled ? 'pointer-events-none' : ''}`}
        aria-hidden={!(hovered || menuOpen || insertOpen)}
      >
        <BlockInsertMenu
          open={insertOpen}
          setOpen={setInsertOpen}
          disabled={disabled}
          insertOptions={insertOptions}
          onSelectBlock={() => onSelectBlock?.(block.id)}
          onInsert={(type) => onInsertBlock?.(block.id, type)}
        />
        <BlockActionsMenu
          open={menuOpen}
          setOpen={setMenuOpen}
          disabled={disabled}
          currentLabel={def?.label}
          transformOptions={transformOptions}
          onMouseDownHandle={handleHandleMouseDown}
          getJustDragged={() => {
            const v = justDraggedRef.current;
            justDraggedRef.current = false;
            return v;
          }}
          onSelectBlock={() => onSelectBlock?.(block.id)}
          onTransform={(type) => onTransformBlock?.(block.id, type)}
          onDuplicate={() => onDuplicateBlock?.(block.id)}
          onDelete={() => onDeleteBlock?.(block.id)}
        />
      </div>

      <div className="flex-1 min-w-0">
        {def.isText ? (
          def.renderPrefix ? (
            <div className="flex items-start">
              {def.renderPrefix({ block, index, numberedIndex, disabled, onPatch: (patch) => onBlockPatch?.(block.id, patch) })}
              <TextEditable
                ref={editableRef}
                className={`flex-1 outline-none ${def.textClassName || ''}${checklistClasses} ${isEmpty ? 'block-empty' : ''}`}
                placeholder={def.placeholder}
                disabled={disabled}
                onInput={handleInput}
                onKeyDown={handleKeyDown}
                onPaste={handlePaste}
                onFocus={handleFocus}
              />
            </div>
          ) : (
            <TextEditable
              ref={editableRef}
              className={`outline-none ${def.textClassName || ''}${checklistClasses} ${isEmpty ? 'block-empty' : ''}`}
              placeholder={def.placeholder}
              disabled={disabled}
              onInput={handleInput}
              onKeyDown={handleKeyDown}
              onPaste={handlePaste}
              onFocus={handleFocus}
            />
          )
        ) : def.Component ? (
          <def.Component
            block={block}
            disabled={disabled}
            onChange={(patch) => onBlockPatch?.(block.id, patch)}
          />
        ) : (
          <div className="text-xs text-red-500">Bloc {block.type} sans rendu.</div>
        )}
      </div>
    </div>
  );
});

const TextEditable = forwardRef(function TextEditable(
  { className, placeholder, disabled, onInput, onKeyDown, onPaste, onFocus },
  ref,
) {
  return (
    <div
      ref={ref}
      contentEditable={!disabled}
      suppressContentEditableWarning
      role="textbox"
      aria-multiline="true"
      data-placeholder={placeholder}
      className={className}
      onInput={onInput}
      onKeyDown={onKeyDown}
      onPaste={onPaste}
      onFocus={onFocus}
      spellCheck="true"
    />
  );
});

export default Block;
