"use client";

import { useRef, useCallback, useEffect, useState, forwardRef, type ReactNode } from "react";
import { Plus, Save, X } from "lucide-react";
import { useDashboardStore } from "@/stores/dashboardStore";
import { useControlMode } from "@/components/control/hooks/useControlMode";
import { deleteDashboardCard } from "@/lib/api/dashMenu";
import { toast } from "sonner";
import { DashboardCard } from "./DashboardCard";
import { EmptyDashboard } from "@/components/layout/EmptyDashboard";

/**
 * AnimatedFab — enter/exit 애니메이션을 가진 플로팅 액션 바.
 * show=false 전환 시 closing 클래스를 주고 320ms 후 unmount.
 * Ref: INVYONE Design System / ui_kits/app/dashboard-user.jsx (AnimatedFab).
 */
function AnimatedFab({ show, children }: { show: boolean; children: ReactNode }) {
  const [rendered, setRendered] = useState(show);
  const [closing, setClosing] = useState(false);
  useEffect(() => {
    if (show) {
      setRendered(true);
      setClosing(false);
    } else if (rendered) {
      setClosing(true);
      const t = window.setTimeout(() => {
        setRendered(false);
        setClosing(false);
      }, 320);
      return () => window.clearTimeout(t);
    }
  }, [show, rendered]);
  if (!rendered) return null;
  return <div className={`ud-fab${closing ? "closing" : ""}`}>{children}</div>;
}

interface DashboardCanvasProps {
  dashboardName: string;
  onOpenLibrary: () => void;
  onOpenSettings?: (cardId: string) => void;
  controlMode?: boolean;
}

// ═══ 자유 배치 + 반응형을 위한 상수/유틸 ═══
// 저장 단위: % (0~100). 기존 px 데이터는 로드 시 자동 변환됨.
const SNAP_THRESHOLD = 8; // px — 스냅 걸리는 임계거리
const MIN_CARD_W_PX = 220;
const MIN_CARD_H_PX = 140;
const INTERACTION_THRESHOLD = 4;

type Dir = "" | "n" | "s" | "e" | "w" | "ne" | "nw" | "se" | "sw";

// 값이 px 로 저장된 건지 판별 — % 는 0~100, px 는 150+ 가 일반
function isValuePx(val: number): boolean {
  return val > 100;
}

function pxToPct(px: number, container: number): number {
  if (!container || container <= 0) return 0;
  return (px / container) * 100;
}

function pctToPx(pct: number, container: number): number {
  return (pct / 100) * container;
}

function round3(n: number): number {
  return Math.round(n * 1000) / 1000;
}

// 카드 좌표를 항상 px 로 해석 (% 면 container 로 환산)
function readCardPx(card: Record<string, unknown>, cw: number, ch: number) {
  const x = Number(card.position_x ?? card.POSITION_X ?? 0);
  const y = Number(card.position_y ?? card.POSITION_Y ?? 0);
  const w = Number(card.width ?? card.WIDTH ?? 0);
  const h = Number(card.height ?? card.HEIGHT ?? 0);
  return {
    x: isValuePx(x) ? x : pctToPx(x, cw),
    y: isValuePx(y) ? y : pctToPx(y, ch),
    w: isValuePx(w) ? w : pctToPx(w, cw),
    h: isValuePx(h) ? h : pctToPx(h, ch),
  };
}

export const DashboardCanvas = forwardRef<HTMLDivElement, DashboardCanvasProps>(function DashboardCanvas(
  { dashboardName, onOpenLibrary, onOpenSettings, controlMode: controlActive },
  externalRef,
) {
  const cards = useDashboardStore((s) => s.cards);
  const editMode = useDashboardStore((s) => s.editMode);
  const setEditMode = useDashboardStore((s) => s.setEditMode);
  const updateCard = useDashboardStore((s) => s.updateCard);
  const removeCard = useDashboardStore((s) => s.removeCard);
  const setCards = useDashboardStore((s) => s.setCards);
  const activeDashboardId = useDashboardStore((s) => s.activeDashboardId);
  const toggleControlMode = useControlMode((s) => s.toggleControlMode);
  const internalRef = useRef<HTMLDivElement>(null);
  const canvasRef = (externalRef as React.RefObject<HTMLDivElement | null>) ?? internalRef;
  const [canvasSize, setCanvasSize] = useState({ cw: 0, ch: 0 });

  // 스냅 가이드 라인 (시각 피드백)
  const [guides, setGuides] = useState<{ v: number[]; h: number[] }>({ v: [], h: [] });

  const dragRef = useRef<{
    cardId: string;
    startX: number;
    startY: number;
    origLeft: number;
    origTop: number; // px
    origW: number;
    origH: number; // px
    liveLeft: number;
    liveTop: number;
    liveW: number;
    liveH: number;
    hasMoved: boolean;
    activated: boolean;
    mode: "drag" | "resize";
    dir: Dir;
    el: HTMLElement;
  } | null>(null);

  const getCanvasSize = useCallback(() => {
    const cv = canvasRef.current;
    const rect = cv?.getBoundingClientRect();
    return {
      cw: rect?.width || cv?.clientWidth || canvasSize.cw || 1000,
      ch: rect?.height || cv?.clientHeight || canvasSize.ch || 600,
    };
  }, [canvasRef, canvasSize.ch, canvasSize.cw]);

  useEffect(() => {
    const cv = canvasRef.current;
    if (!cv) return;

    const syncSize = () => {
      const rect = cv.getBoundingClientRect();
      setCanvasSize({
        cw: rect.width || cv.clientWidth,
        ch: rect.height || cv.clientHeight,
      });
    };

    syncSize();

    const ro = new ResizeObserver(() => syncSize());
    ro.observe(cv);
    window.addEventListener("resize", syncSize);
    return () => {
      ro.disconnect();
      window.removeEventListener("resize", syncSize);
    };
  }, [canvasRef]);

  // 카드 값 정규화 — px → %, 범위 clamp (5~100, x+w≤100, y+h≤100)
  // px 데이터 마이그레이션과 오버플로우/NaN 보정을 동시에 처리.
  // changed=false 면 setCards 안 부르므로 무한루프 방지.
  useEffect(() => {
    if (cards.length === 0) return;
    const { cw, ch } = canvasSize;
    if (cw <= 0 || ch <= 0) return;

    let changed = false;
    const migrated = cards.map((c) => {
      const x = Number(c.position_x ?? c.POSITION_X ?? 5);
      const y = Number(c.position_y ?? c.POSITION_Y ?? 5);
      const w = Number(c.width ?? c.WIDTH ?? 40);
      const h = Number(c.height ?? c.HEIGHT ?? 35);

      // 1) px → %  (NaN 이면 기본값)
      let nx = Number.isFinite(x) ? (isValuePx(x) ? pxToPct(x, cw) : x) : 5;
      let ny = Number.isFinite(y) ? (isValuePx(y) ? pxToPct(y, ch) : y) : 5;
      let nw = Number.isFinite(w) ? (isValuePx(w) ? pxToPct(w, cw) : w) : 40;
      let nh = Number.isFinite(h) ? (isValuePx(h) ? pxToPct(h, ch) : h) : 35;

      // 2) 범위 clamp — 카드가 항상 캔버스 안에 들어오도록
      nw = Math.max(5, Math.min(nw, 100));
      nh = Math.max(5, Math.min(nh, 100));
      nx = Math.max(0, Math.min(nx, 100 - nw));
      ny = Math.max(0, Math.min(ny, 100 - nh));

      const changedThis =
        !Number.isFinite(x) ||
        !Number.isFinite(y) ||
        !Number.isFinite(w) ||
        !Number.isFinite(h) ||
        Math.abs(nx - x) > 0.01 ||
        Math.abs(ny - y) > 0.01 ||
        Math.abs(nw - w) > 0.01 ||
        Math.abs(nh - h) > 0.01;
      if (changedThis) {
        changed = true;
        return { ...c, position_x: nx, position_y: ny, width: nw, height: nh };
      }
      return c;
    });
    if (changed) setCards(migrated);
  }, [cards, canvasSize, setCards]);

  // 드래그 clamp + 스냅 — 리턴은 px
  const applyDragConstraints = useCallback(
    (cardId: string, newLeft: number, newTop: number, w: number, h: number) => {
      const { cw, ch } = getCanvasSize();
      // 카드가 캔버스보다 크면(마이그레이션 미동작 등 edge) 왼쪽/위 경계로 강제 이동 금지 — origin 유지
      const maxL = Math.max(0, cw - w);
      const maxT = Math.max(0, ch - h);
      let l = w > cw ? newLeft : Math.max(0, Math.min(newLeft, maxL));
      let t = h > ch ? newTop : Math.max(0, Math.min(newTop, maxT));

      const vGuides: number[] = [];
      const hGuides: number[] = [];

      // 1) 다른 카드 가장자리 스냅
      for (const o of cards) {
        const oid = o.card_id ?? o.CARD_ID;
        if (oid === cardId) continue;
        const { x: oL, y: oT, w: oW, h: oH } = readCardPx(o, cw, ch);
        const oR = oL + oW;
        const oB = oT + oH;

        // X: left↔otherRight, right↔otherLeft, left↔otherLeft, right↔otherRight
        if (Math.abs(l - oR) < SNAP_THRESHOLD) {
          l = oR;
          vGuides.push(l);
        } else if (Math.abs(l + w - oL) < SNAP_THRESHOLD) {
          l = oL - w;
          vGuides.push(oL);
        } else if (Math.abs(l - oL) < SNAP_THRESHOLD) {
          l = oL;
          vGuides.push(l);
        } else if (Math.abs(l + w - oR) < SNAP_THRESHOLD) {
          l = oR - w;
          vGuides.push(oR);
        }

        // Y
        if (Math.abs(t - oB) < SNAP_THRESHOLD) {
          t = oB;
          hGuides.push(t);
        } else if (Math.abs(t + h - oT) < SNAP_THRESHOLD) {
          t = oT - h;
          hGuides.push(oT);
        } else if (Math.abs(t - oT) < SNAP_THRESHOLD) {
          t = oT;
          hGuides.push(t);
        } else if (Math.abs(t + h - oB) < SNAP_THRESHOLD) {
          t = oB - h;
          hGuides.push(oB);
        }
      }

      // 2) 캔버스 경계 스냅 (0 / cw / ch)
      if (Math.abs(l) < SNAP_THRESHOLD) {
        l = 0;
        vGuides.push(0);
      }
      if (Math.abs(l + w - cw) < SNAP_THRESHOLD) {
        l = cw - w;
        vGuides.push(cw);
      }
      if (Math.abs(t) < SNAP_THRESHOLD) {
        t = 0;
        hGuides.push(0);
      }
      if (Math.abs(t + h - ch) < SNAP_THRESHOLD) {
        t = ch - h;
        hGuides.push(ch);
      }

      // 3) 최종 clamp — 카드가 캔버스에 맞는 경우만 경계로 clamp, 오버플로우 카드는 그대로 두기
      if (w <= cw) l = Math.max(0, Math.min(l, cw - w));
      if (h <= ch) t = Math.max(0, Math.min(t, ch - h));

      return { l, t, vGuides, hGuides };
    },
    [cards, getCanvasSize],
  );

  // 리사이즈 clamp (방향별) — 캔버스 경계를 벗어나지 않게, 최소 크기 보장
  // 오버플로우 카드 edge: 이미 카드가 캔버스보다 크면 추가 확장은 금지, 축소는 자유롭게.
  const applyResizeConstraints = useCallback(
    (cardId: string, origL: number, origT: number, origW: number, origH: number, dx: number, dy: number, dir: Dir) => {
      const { cw, ch } = getCanvasSize();
      let l = origL,
        t = origT,
        w = origW,
        h = origH;
      const vGuides: number[] = [];
      const hGuides: number[] = [];

      if (dir.includes("e")) {
        // 확장 한계: 캔버스 오른쪽 경계. 이미 넘치고 있으면 현재 origW 를 한계로 (확장 금지, 축소 허용)
        const maxW = Math.max(origW, cw - origL);
        w = Math.max(MIN_CARD_W_PX, Math.min(origW + dx, maxW));
      }
      if (dir.includes("s")) {
        const maxH = Math.max(origH, ch - origT);
        h = Math.max(MIN_CARD_H_PX, Math.min(origH + dy, maxH));
      }
      if (dir.includes("w")) {
        // w 핸들: dx 만큼 left 증가 + width 감소. 왼쪽 경계(0) 허용 범위 내에서.
        const minDx = origL > 0 ? -origL : 0; // 캔버스 안에 있을 때만 왼쪽으로 확장 허용
        const maxShrink = origW - MIN_CARD_W_PX;
        const clampedDx = Math.max(minDx, Math.min(dx, maxShrink));
        l = origL + clampedDx;
        w = origW - clampedDx;
      }
      if (dir.includes("n")) {
        const minDy = origT > 0 ? -origT : 0;
        const maxShrink = origH - MIN_CARD_H_PX;
        const clampedDy = Math.max(minDy, Math.min(dy, maxShrink));
        t = origT + clampedDy;
        h = origH - clampedDy;
      }

      let left = l;
      let top = t;
      let right = l + w;
      let bottom = t + h;

      const snapValue = (value: number, targets: number[]) => {
        let snapped = value;
        let matched = false;
        for (const target of targets) {
          if (Math.abs(value - target) < SNAP_THRESHOLD) {
            snapped = target;
            matched = true;
            break;
          }
        }
        return { snapped, matched };
      };

      const xTargets = [0, cw];
      const yTargets = [0, ch];
      for (const o of cards) {
        const oid = o.card_id ?? o.CARD_ID;
        if (oid === cardId) continue;
        const { x: oL, y: oT, w: oW, h: oH } = readCardPx(o, cw, ch);
        xTargets.push(oL, oL + oW);
        yTargets.push(oT, oT + oH);
      }

      if (dir.includes("e")) {
        const { snapped, matched } = snapValue(right, xTargets);
        if (matched && snapped - left >= MIN_CARD_W_PX) {
          right = snapped;
          vGuides.push(snapped);
        }
      }
      if (dir.includes("w")) {
        const { snapped, matched } = snapValue(left, xTargets);
        if (matched && right - snapped >= MIN_CARD_W_PX) {
          left = snapped;
          vGuides.push(snapped);
        }
      }
      if (dir.includes("s")) {
        const { snapped, matched } = snapValue(bottom, yTargets);
        if (matched && snapped - top >= MIN_CARD_H_PX) {
          bottom = snapped;
          hGuides.push(snapped);
        }
      }
      if (dir.includes("n")) {
        const { snapped, matched } = snapValue(top, yTargets);
        if (matched && bottom - snapped >= MIN_CARD_H_PX) {
          top = snapped;
          hGuides.push(snapped);
        }
      }

      l = left;
      t = top;
      w = right - left;
      h = bottom - top;

      return { l, t, w, h, vGuides, hGuides };
    },
    [cards, getCanvasSize],
  );

  const handleMouseDown = useCallback(
    (e: React.MouseEvent) => {
      if (!editMode) return;
      const target = e.target as HTMLElement;

      const resizeEl = target.closest("[data-resize]") as HTMLElement | null;
      const isResize = !!resizeEl;
      if (!isResize) {
        if (
          target.closest("button") ||
          target.closest("input") ||
          target.closest("select") ||
          target.closest("textarea")
        )
          return;
        if (!target.closest(".dash-card")) return;
      }

      const wrapperEl = target.closest("[data-card-id]") as HTMLElement;
      if (!wrapperEl) return;

      const cardId = wrapperEl.dataset.cardId;
      if (!cardId) return;

      e.preventDefault();

      const dir = (resizeEl?.dataset.resize ?? "") as Dir;
      const canvasRect = canvasRef.current?.getBoundingClientRect();
      const wrapperRect = wrapperEl.getBoundingClientRect();
      const box =
        canvasRect && wrapperRect.width > 0 && wrapperRect.height > 0
          ? {
              x: wrapperRect.left - canvasRect.left,
              y: wrapperRect.top - canvasRect.top,
              w: wrapperRect.width,
              h: wrapperRect.height,
            }
          : {
              x: wrapperEl.offsetLeft,
              y: wrapperEl.offsetTop,
              w: wrapperEl.offsetWidth,
              h: wrapperEl.offsetHeight,
            };

      dragRef.current = {
        cardId,
        startX: e.clientX,
        startY: e.clientY,
        origLeft: box.x,
        origTop: box.y,
        origW: box.w,
        origH: box.h,
        liveLeft: box.x,
        liveTop: box.y,
        liveW: box.w,
        liveH: box.h,
        hasMoved: false,
        activated: false,
        mode: isResize ? "resize" : "drag",
        dir: isResize ? dir : "",
        el: wrapperEl,
      };
    },
    [canvasRef, editMode],
  );

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      const d = dragRef.current;
      if (!d) return;

      const dx = e.clientX - d.startX;
      const dy = e.clientY - d.startY;
      if (!d.hasMoved && Math.max(Math.abs(dx), Math.abs(dy)) < INTERACTION_THRESHOLD) return;
      d.hasMoved = true;
      if (!d.activated) {
        d.activated = true;
        const cardEl = d.el.querySelector(".dash-card") as HTMLElement | null;
        if (cardEl) cardEl.classList.add(d.mode === "resize" ? "resizing" : "dragging");
        d.el.style.left = d.origLeft + "px";
        d.el.style.top = d.origTop + "px";
        d.el.style.width = d.origW + "px";
        d.el.style.height = d.origH + "px";

        const cursorMap: Record<string, string> = {
          n: "ns-resize",
          s: "ns-resize",
          e: "ew-resize",
          w: "ew-resize",
          ne: "nesw-resize",
          sw: "nesw-resize",
          nw: "nwse-resize",
          se: "nwse-resize",
        };
        document.body.style.cursor = d.mode === "resize" ? (cursorMap[d.dir] ?? "nwse-resize") : "grabbing";
        document.body.style.userSelect = "none";
      }

      if (d.mode === "drag") {
        const { l, t, vGuides, hGuides } = applyDragConstraints(
          d.cardId,
          d.origLeft + dx,
          d.origTop + dy,
          d.origW,
          d.origH,
        );
        d.liveLeft = l;
        d.liveTop = t;
        d.liveW = d.origW;
        d.liveH = d.origH;
        d.el.style.left = l + "px";
        d.el.style.top = t + "px";
        setGuides({ v: vGuides, h: hGuides });
      } else {
        const { l, t, w, h, vGuides, hGuides } = applyResizeConstraints(
          d.cardId,
          d.origLeft,
          d.origTop,
          d.origW,
          d.origH,
          dx,
          dy,
          d.dir,
        );
        d.liveLeft = l;
        d.liveTop = t;
        d.liveW = w;
        d.liveH = h;
        d.el.style.left = l + "px";
        d.el.style.top = t + "px";
        d.el.style.width = w + "px";
        d.el.style.height = h + "px";
        setGuides({ v: vGuides, h: hGuides });
      }
    };

    const handleMouseUp = () => {
      const d = dragRef.current;
      if (!d) return;

      document.body.style.cursor = "";
      document.body.style.userSelect = "";

      if (!d.hasMoved) {
        dragRef.current = null;
        setGuides({ v: [], h: [] });
        return;
      }

      const cardEl = d.el.querySelector(".dash-card") as HTMLElement | null;
      if (cardEl) cardEl.classList.remove("dragging", "resizing");

      // px → % 변환해서 store 에 저장 (다음 render 에서 % 로 다시 그려짐)
      const { cw, ch } = getCanvasSize();
      if (d.mode === "drag") {
        const finalW = d.el.getBoundingClientRect().width || d.origW;
        const finalH = d.el.getBoundingClientRect().height || d.origH;
        updateCard(d.cardId, {
          position_x: round3(pxToPct(d.liveLeft, cw)),
          position_y: round3(pxToPct(d.liveTop, ch)),
          width: round3(pxToPct(finalW, cw)),
          height: round3(pxToPct(finalH, ch)),
        });
      } else {
        updateCard(d.cardId, {
          position_x: round3(pxToPct(d.liveLeft, cw)),
          position_y: round3(pxToPct(d.liveTop, ch)),
          width: round3(pxToPct(d.liveW, cw)),
          height: round3(pxToPct(d.liveH, ch)),
        });
      }

      dragRef.current = null;
      setGuides({ v: [], h: [] });
    };

    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);
    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
  }, [applyDragConstraints, applyResizeConstraints, getCanvasSize, updateCard]);

  const handleRemove = useCallback(
    async (cardId: string) => {
      if (!confirm("이 카드를 삭제하시겠습니까?")) return;
      if (!activeDashboardId) {
        toast.error("활성 대시보드가 없습니다");
        return;
      }
      removeCard(cardId);
      try {
        await deleteDashboardCard(activeDashboardId, cardId);
        toast.success("카드 삭제됨");
      } catch (err) {
        console.error("[Dashboard] 카드 삭제 실패", err);
        toast.error("카드 삭제 실패 — 새로고침 후 다시 시도해주세요");
      }
    },
    [activeDashboardId, removeCard],
  );

  const handleRequestSave = useCallback(() => {
    window.dispatchEvent(new CustomEvent("dash:save"));
  }, []);

  const canvasClassName = ["dash-canvas", editMode && "edit-mode", controlActive && "control-mode"]
    .filter(Boolean)
    .join(" ");

  return (
    <div ref={canvasRef} className={canvasClassName} onMouseDown={controlActive ? undefined : handleMouseDown}>
      {cards.length === 0 ? (
        <EmptyDashboard />
      ) : (
        cards.map((card) => {
          const id = card.card_id ?? card.CARD_ID;
          const xRaw = Number(card.position_x ?? card.POSITION_X ?? 5);
          const yRaw = Number(card.position_y ?? card.POSITION_Y ?? 5);
          const wRaw = Number(card.width ?? card.WIDTH ?? 40);
          const hRaw = Number(card.height ?? card.HEIGHT ?? 35);

          // 마이그레이션 전 px 데이터는 px 로 (곧 useEffect 가 % 로 전환), 그 외는 % 로 렌더
          const asPx = isValuePx(xRaw) || isValuePx(yRaw) || isValuePx(wRaw) || isValuePx(hRaw);
          const unit = asPx ? "px" : "%";

          // % 렌더 시 안전망 clamp (NaN / 오버플로우 방어)
          let x = xRaw,
            y = yRaw,
            w = wRaw,
            h = hRaw;
          if (!asPx) {
            if (!Number.isFinite(x)) x = 5;
            if (!Number.isFinite(y)) y = 5;
            if (!Number.isFinite(w)) w = 40;
            if (!Number.isFinite(h)) h = 35;
            w = Math.max(5, Math.min(w, 100));
            h = Math.max(5, Math.min(h, 100));
            x = Math.max(0, Math.min(x, 100 - w));
            y = Math.max(0, Math.min(y, 100 - h));
          }

          return (
            <div
              key={id}
              data-card-id={id}
              style={{
                position: "absolute",
                left: x + unit,
                top: y + unit,
                width: w + unit,
                height: h + unit,
                zIndex: 10,
              }}
            >
              <DashboardCard
                card={card}
                editMode={editMode}
                onRemove={handleRemove}
                onOpenSettings={onOpenSettings}
              />
            </div>
          );
        })
      )}

      {/* 스냅 가이드 — 드래그 중 달라붙는 지점 시각 표시 */}
      {guides.v.map((x, i) => (
        <div key={`gv-${i}`} className="dash-snap-guide v" style={{ left: x + "px", top: 0, bottom: 0 }} />
      ))}
      {guides.h.map((y, i) => (
        <div key={`gh-${i}`} className="dash-snap-guide h" style={{ top: y + "px", left: 0, right: 0 }} />
      ))}

      {/* 편집 모드 FAB — 캔버스 우하단 */}
      <AnimatedFab show={editMode && !controlActive}>
        <span className="ud-fab-badge">
          <span className="dot" />
          편집 모드 · 카드 {cards.length}개
        </span>
        <span className="ud-fab-sep" />
        <button className="ud-fab-btn" onClick={onOpenLibrary} title="템플릿 추가">
          <Plus size={12} />
          <span>템플릿</span>
        </button>
        <button className="ud-fab-btn primary" onClick={handleRequestSave} title="레이아웃 저장">
          <Save size={12} />
          <span>저장</span>
        </button>
        <button className="ud-fab-btn ghost" onClick={() => setEditMode(false)} title="편집 종료">
          <X size={12} />
          <span>종료</span>
        </button>
      </AnimatedFab>

      {/* 제어 모드 FAB */}
      <AnimatedFab show={!!controlActive}>
        <span className="ud-fab-badge ctrl">
          <span className="dot" />
          제어 모드 · 실시간 데이터
        </span>
        <span className="ud-fab-sep" />
        <button className="ud-fab-btn ghost" onClick={() => toggleControlMode()} title="제어 종료">
          <X size={12} />
          <span>종료</span>
        </button>
      </AnimatedFab>
    </div>
  );
});
