<template>
  <div
    class="json-view"
    :class="[
      `json-view--${props.theme}`,
      {
        'json-view--suppress-menu-hover': suppressMenuHover,
        'json-view--wrap-lines': wrapsLines,
        'json-view--show-line': props.showLine,
      },
    ]"
  >
    <div
      ref="viewportRef"
      class="json-view__viewport"
      :style="viewportStyle"
      role="tree"
      aria-label="JSON viewer"
      @pointermove="handleViewportPointerMove"
      @pointerleave="clearMenuHoverSuppression"
      @scroll="handleScroll"
    >
      <div v-if="props.virtual" class="json-view__spacer" :style="{ height: `${virtualTotalHeight}px` }">
        <div
          v-for="row in renderedRows"
          :key="row.node.id"
          class="json-view__row"
          :style="getRowStyle(row)"
          role="treeitem"
          :aria-level="row.node.level + 1"
          :aria-setsize="row.node.setSize"
          :aria-posinset="row.node.posInSet"
          :aria-expanded="row.node.hasToggle ? row.node.isExpanded : undefined"
          tabindex="-1"
          @click="emit('nodeClick', row.node)"
          @mouseenter="emit('nodeMouseover', row.node)"
        >
          <div v-if="props.showLineNumber" class="json-view__line-number" :style="lineNumberStyle">
            {{ getLineNumber(row.node, row.index) }}
          </div>
          <div class="json-view__line" :style="getLineStyle(row.node)">
            <button
              v-if="props.showIcon && row.node.hasToggle"
              class="json-view__toggle"
              type="button"
              :class="{ 'is-expanded': row.node.isExpanded }"
              :aria-label="row.node.isExpanded ? 'Collapse node' : 'Expand node'"
              @click.stop="toggleNode(row.node, 'icon')"
            ></button>

            <span v-if="hasKey(row.node)" class="json-view__key">
              <KeyRenderer :node="row.node" />
              <span class="json-view__colon">{{ props.showKeyValueSpace ? ": " : ":" }}</span>
            </span>

            <span
              class="json-view__value"
              :class="getValueClass(row.node)"
              @click.stop="handleBracketClick(row.node)"
            >
              <ValueRenderer :node="row.node" />
            </span>

            <span v-if="props.showLength && shouldShowLength(row.node)" class="json-view__length">
              ({{ row.node.length }})
            </span>

            <ActionsRenderer v-if="hasActionsRenderer" :node="row.node" />

            <div v-if="showsMenu" class="json-view__menu-shell">
              <button
                class="json-view__menu-trigger"
                type="button"
                aria-haspopup="menu"
                :aria-expanded="isMenuOpen(row.node)"
                aria-label="Open node menu"
                @click.stop="openMenu(row.node, $event)"
              ></button>
            </div>
          </div>
        </div>
      </div>

      <div v-else class="json-view__list">
        <div
          v-for="row in renderedRows"
          :key="row.node.id"
          class="json-view__row"
          :style="getRowStyle(row)"
          role="treeitem"
          :aria-level="row.node.level + 1"
          :aria-setsize="row.node.setSize"
          :aria-posinset="row.node.posInSet"
          :aria-expanded="row.node.hasToggle ? row.node.isExpanded : undefined"
          tabindex="-1"
          @click="emit('nodeClick', row.node)"
          @mouseenter="emit('nodeMouseover', row.node)"
        >
          <div v-if="props.showLineNumber" class="json-view__line-number" :style="lineNumberStyle">
            {{ getLineNumber(row.node, row.index) }}
          </div>
          <div class="json-view__line" :style="getLineStyle(row.node)">
            <button
              v-if="props.showIcon && row.node.hasToggle"
              class="json-view__toggle"
              type="button"
              :class="{ 'is-expanded': row.node.isExpanded }"
              :aria-label="row.node.isExpanded ? 'Collapse node' : 'Expand node'"
              @click.stop="toggleNode(row.node, 'icon')"
            ></button>

            <span v-if="hasKey(row.node)" class="json-view__key">
              <KeyRenderer :node="row.node" />
              <span class="json-view__colon">{{ props.showKeyValueSpace ? ": " : ":" }}</span>
            </span>

            <span
              class="json-view__value"
              :class="getValueClass(row.node)"
              @click.stop="handleBracketClick(row.node)"
            >
              <ValueRenderer :node="row.node" />
            </span>

            <span v-if="props.showLength && shouldShowLength(row.node)" class="json-view__length">
              ({{ row.node.length }})
            </span>

            <ActionsRenderer v-if="hasActionsRenderer" :node="row.node" />

            <div v-if="showsMenu" class="json-view__menu-shell">
              <button
                class="json-view__menu-trigger"
                type="button"
                aria-haspopup="menu"
                :aria-expanded="isMenuOpen(row.node)"
                aria-label="Open node menu"
                @click.stop="openMenu(row.node, $event)"
              ></button>
            </div>
          </div>
        </div>
      </div>
    </div>

    <el-dropdown
      v-if="showsMenu && activeMenuTrigger"
      ref="sharedMenuRef"
      class="json-view__shared-menu"
      trigger="click"
      placement="bottom-end"
      popper-class="json-view__menu-dropdown"
      :popper-options="{ strategy: 'fixed' }"
      :virtual-ref="activeMenuTrigger"
      virtual-triggering
      @command="handleMenuCommand"
      @visible-change="handleMenuVisibleChange"
    >
      <span class="json-view__shared-menu-anchor" aria-hidden="true"></span>

      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item
            v-for="item in activeMenuItems"
            :key="item.key"
            :command="item.key"
            :disabled="item.disabled"
          >
            {{ item.label }}
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
</template>

<script lang="tsx" setup>
import { computed, nextTick, onMounted, onUnmounted, ref, useSlots, type CSSProperties, type Slot } from "vue";
import type { DropdownInstance } from "element-plus";
import { measureShrinkWrapWidth, measureTextHeight } from "../list-table-v2/measureText";
import { useVirtualRows } from "../list-table-v2/useVirtualRows";
import { buildVisibleJsonRows } from "./flattenJson";
import type { JsonViewMenuActionContext, JsonViewMenuItem, JsonViewNode, JsonViewProps } from "./types";

const DEFAULT_FONT = "13px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace";
const DEFAULT_LINE_HEIGHT = 20;
const ROW_VERTICAL_PADDING = 0;
const LINE_NUMBER_PADDING_START = 6;
const LINE_NUMBER_PADDING_END = 6;
const MIN_LINE_NUMBER_WIDTH = 24;
const TOGGLE_WIDTH = 20;
const ROW_GAP = 4;
const MENU_TRIGGER_WIDTH = 18;
const EXTRA_GUTTER = 32;

interface RenderedRow {
  index: number;
  node: JsonViewNode;
  height: number;
  offsetY?: number;
}

const slots = useSlots() as {
  renderNodeKey?: Slot;
  renderNodeValue?: Slot;
  renderNodeActions?: Slot;
};

const props = withDefaults(defineProps<JsonViewProps>(), {
  data: undefined,
  rootPath: "root",
  indent: 2,
  collapsedNodeLength: undefined,
  deep: undefined,
  showLength: false,
  showLine: true,
  showLineNumber: false,
  showIcon: false,
  showDoubleQuotes: true,
  showKeyValueSpace: true,
  virtual: false,
  height: 400,
  itemHeight: 20,
  dynamicHeight: true,
  collapsedOnClickBrackets: true,
  theme: "light",
  renderNodeKey: undefined,
  renderNodeValue: undefined,
  renderNodeActions: undefined,
  showMenu: true,
  menuItems: undefined,
});

const emit = defineEmits<{
  (e: "nodeClick", node: JsonViewNode): void;
  (e: "nodeMouseover", node: JsonViewNode): void;
  (e: "bracketsClick", collapsed: boolean, node: JsonViewNode): void;
  (e: "iconClick", collapsed: boolean, node: JsonViewNode): void;
}>();

const viewportRef = ref<HTMLElement | null>(null);
const viewportWidth = ref(0);
let resizeObserver: ResizeObserver | null = null;
const sharedMenuRef = ref<DropdownInstance>();

const expandedState = ref(new Map<string, boolean>());
const activeMenuNode = ref<JsonViewNode | null>(null);
const activeMenuTrigger = ref<HTMLElement | null>(null);
const suppressMenuHover = ref(false);

const indentSize = computed(() => Math.max(props.indent, 1) * 8);

const visibleNodes = computed(() =>
  buildVisibleJsonRows(props.data, {
    rootPath: props.rootPath,
    deep: props.deep,
    collapsedNodeLength: props.collapsedNodeLength,
    expandedState: expandedState.value,
  })
);

const hasActionsRenderer = computed(() => Boolean(props.renderNodeActions || slots.renderNodeActions));
const hasCustomRenderers = computed(() =>
  Boolean(
    props.renderNodeKey ||
      props.renderNodeValue ||
      props.renderNodeActions ||
      slots.renderNodeKey ||
      slots.renderNodeValue ||
      slots.renderNodeActions
  )
);
const wrapsLines = computed(() => props.dynamicHeight && !hasCustomRenderers.value);
const showsMenu = computed(() => props.showMenu);
const maxVisibleLineNumber = computed(() =>
  visibleNodes.value.reduce((maxLineNumber, node) => Math.max(maxLineNumber, node.lineNumber), 1)
);
const lineNumberWidth = computed(() =>
  Math.max(
    MIN_LINE_NUMBER_WIDTH,
    Math.ceil(
      measureShrinkWrapWidth(String(maxVisibleLineNumber.value), DEFAULT_FONT) +
        LINE_NUMBER_PADDING_START +
        LINE_NUMBER_PADDING_END
    )
  )
);
const lineNumberStyle = computed<CSSProperties>(() => ({
  flex: `0 0 ${lineNumberWidth.value}px`,
}));
const activeMenuItems = computed(() => (activeMenuNode.value ? getMenuItems(activeMenuNode.value) : []));

function hasKey(node: JsonViewNode) {
  return node.key !== undefined || node.displayKey !== undefined || node.index !== undefined;
}

function shouldShowLength(node: JsonViewNode) {
  return node.type === "objectCollapsed" || node.type === "arrayCollapsed";
}

function getDefaultKeyText(node: JsonViewNode) {
  if (node.displayKey !== undefined) {
    return node.displayKey;
  }
  if (node.index !== undefined) {
    return String(node.index);
  }
  if (node.key === undefined) {
    return "";
  }
  return props.showDoubleQuotes ? JSON.stringify(node.key) : node.key;
}

function getDefaultValueText(node: JsonViewNode) {
  switch (node.type) {
    case "objectEnd":
    case "objectCollapsed":
    case "arrayEnd":
    case "arrayCollapsed":
    case "content":
      return node.showComma ? `${node.content},` : node.content;
    case "objectStart":
    case "arrayStart":
    default:
      return node.content;
  }
}

function getMeasurementText(node: JsonViewNode) {
  const key = getDefaultKeyText(node);
  const value = getDefaultValueText(node);
  const prefix = key ? `${key}${props.showKeyValueSpace ? ": " : ":"}` : "";
  const suffix = props.showLength && shouldShowLength(node) ? ` (${node.length})` : "";

  return {
    prefix,
    value,
    suffix,
  };
}

function getAvailableWidth(node: JsonViewNode) {
  const currentLineNumberWidth = props.showLineNumber ? lineNumberWidth.value : 0;
  const toggleWidth = props.showIcon ? TOGGLE_WIDTH + ROW_GAP : 0;
  const menuWidth = showsMenu.value ? MENU_TRIGGER_WIDTH + ROW_GAP : 0;
  const indentWidth = node.level * indentSize.value;
  return Math.max(
    80,
    viewportWidth.value - currentLineNumberWidth - toggleWidth - menuWidth - indentWidth - EXTRA_GUTTER
  );
}

function getLineNumber(node: JsonViewNode, fallbackIndex: number) {
  return node.lineNumber || fallbackIndex + 1;
}

function getMeasurementHeight(node: JsonViewNode) {
  const availableWidth = getAvailableWidth(node);
  const { prefix, value, suffix } = getMeasurementText(node);
  const suffixWithGap = suffix ? `${props.showLength ? " " : ""}${suffix.trimStart()}` : "";
  const reservedWidth =
    (prefix ? measureShrinkWrapWidth(prefix, DEFAULT_FONT) : 0) +
    (suffixWithGap ? measureShrinkWrapWidth(suffixWithGap, DEFAULT_FONT) : 0);
  const valueWidth = Math.max(80, availableWidth - reservedWidth);
  const measurementText = `${value}${suffixWithGap}`;

  return (
    measureTextHeight(measurementText, DEFAULT_FONT, valueWidth, DEFAULT_LINE_HEIGHT) + ROW_VERTICAL_PADDING * 2
  );
}

const rowHeights = computed(() => {
  if (!wrapsLines.value || viewportWidth.value <= 0) {
    return visibleNodes.value.map(() => props.itemHeight);
  }

  return visibleNodes.value.map((node) =>
    Math.max(props.itemHeight, getMeasurementHeight(node))
  );
});

const viewportHeight = computed(() => props.height);
const virtualizer = useVirtualRows(rowHeights, viewportHeight, { overscan: 8 });
const { visibleRows: virtualRows, totalHeight: virtualTotalHeight, onScroll } = virtualizer;

const renderedRows = computed<RenderedRow[]>(() => {
  if (props.virtual) {
    const rows: RenderedRow[] = [];

    for (const row of virtualRows.value) {
      const node = visibleNodes.value[row.index];
      if (!node) continue;
      rows.push({
        index: row.index,
        node,
        height: row.height,
        offsetY: row.offsetY,
      });
    }

    return rows;
  }

  return visibleNodes.value.map((node, index) => ({
    index,
    node,
    height: rowHeights.value[index] ?? props.itemHeight,
  }));
});

const viewportStyle = computed<CSSProperties>(() => ({
  height: props.virtual ? `${props.height}px` : undefined,
  maxHeight: !props.virtual ? `${props.height}px` : undefined,
}));

function getLineStyle(node: JsonViewNode): CSSProperties {
  const indentWidth = Math.max(0, node.level * indentSize.value);
  const spacerWidth = props.showIcon && !node.hasToggle ? TOGGLE_WIDTH + ROW_GAP : 0;

  return {
    "--json-indent-size": `${indentSize.value}px`,
    "--json-guides-width": `${indentWidth}px`,
    "--json-leading-width": `${indentWidth + spacerWidth}px`,
    paddingInlineStart: `${indentWidth + spacerWidth}px`,
  } as CSSProperties;
}

function getRowStyle(row: RenderedRow): CSSProperties {
  const style: CSSProperties = {
    height: `${row.height}px`,
  };

  if (props.virtual) {
    style.position = "absolute";
    style.left = "0";
    style.right = "0";
    style.transform = `translateY(${row.offsetY ?? 0}px)`;
  }

  return style;
}

function updateExpandedState(path: string, nextExpanded: boolean) {
  const next = new Map(expandedState.value);
  next.set(path, nextExpanded);
  expandedState.value = next;
}

function toggleNode(node: JsonViewNode, source: "icon" | "brackets") {
  if (!node.hasToggle) return;
  const nextExpanded = !node.isExpanded;
  updateExpandedState(node.path, nextExpanded);
  const collapsed = !nextExpanded;

  if (source === "icon") {
    emit("iconClick", collapsed, node);
  } else {
    emit("bracketsClick", collapsed, node);
  }
}

function handleBracketClick(node: JsonViewNode) {
  if (!props.collapsedOnClickBrackets || !node.hasToggle) {
    return;
  }
  toggleNode(node, "brackets");
}

function handleScroll(event: Event) {
  closeMenu();
  if (!props.virtual) return;
  onScroll((event.target as HTMLElement).scrollTop);
}

function getValueClass(node: JsonViewNode) {
  if (node.type !== "content") {
    return "json-view__value--container";
  }

  if (typeof node.value === "string") return "json-view__value--string";
  if (typeof node.value === "number") return "json-view__value--number";
  if (typeof node.value === "boolean") return "json-view__value--boolean";
  if (node.value === null) return "json-view__value--null";
  if (node.value === undefined) return "json-view__value--undefined";
  return "json-view__value--container";
}

const KeyRenderer = ({ node }: { node: JsonViewNode }) => {
  const defaultKey = getDefaultKeyText(node);
  if (slots.renderNodeKey) {
    return slots.renderNodeKey({ node, defaultKey });
  }
  if (props.renderNodeKey) {
    return props.renderNodeKey({ node, defaultKey });
  }
  return defaultKey;
};

const ValueRenderer = ({ node }: { node: JsonViewNode }) => {
  const defaultValue = getDefaultValueText(node);
  if (slots.renderNodeValue) {
    return slots.renderNodeValue({ node, defaultValue });
  }
  if (props.renderNodeValue) {
    return props.renderNodeValue({ node, defaultValue });
  }
  return defaultValue;
};

const ActionsRenderer = ({ node }: { node: JsonViewNode }) => {
  const defaultActions = null;
  if (slots.renderNodeActions) {
    return slots.renderNodeActions({ node, defaultActions });
  }
  if (props.renderNodeActions) {
    return props.renderNodeActions({ node, defaultActions });
  }
  return null;
};

function toSerializableValue(value: unknown, ancestors = new WeakSet<object>()): unknown {
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
    return value;
  }

  if (typeof value === "bigint") {
    return `${String(value)}n`;
  }

  if (value === undefined) {
    return "undefined";
  }

  if (typeof value === "symbol") {
    return value.toString();
  }

  if (typeof value === "function") {
    return value.name ? `[Function ${value.name}]` : "[Function]";
  }

  if (typeof value !== "object") {
    return String(value);
  }

  if (ancestors.has(value)) {
    return "[Circular]";
  }

  ancestors.add(value);

  if (Array.isArray(value)) {
    const serialized = value.map((entry) => toSerializableValue(entry, ancestors));
    ancestors.delete(value);
    return serialized;
  }

  if (value instanceof Set) {
    const serialized = { $set: Array.from(value, (entry) => toSerializableValue(entry, ancestors)) };
    ancestors.delete(value);
    return serialized;
  }

  if (value instanceof Map) {
    const serialized = {
      $map: Array.from(value, ([key, entry]) => [
        toSerializableValue(key, ancestors),
        toSerializableValue(entry, ancestors),
      ]),
    };
    ancestors.delete(value);
    return serialized;
  }

  const serialized = Object.fromEntries(
    Object.entries(value).map(([key, entry]) => [key, toSerializableValue(entry, ancestors)])
  );
  ancestors.delete(value);
  return serialized;
}

function serializeNodeValue(value: unknown): string {
  if (typeof value === "string") {
    return value;
  }
  if (value === undefined) {
    return "undefined";
  }
  try {
    const json = JSON.stringify(toSerializableValue(value), null, 2);
    if (json !== undefined) {
      return json;
    }
  } catch {
    // Fall through to String() for circular or non-JSON-safe values.
  }
  return String(value);
}

async function copyText(text: string) {
  try {
    if (navigator.clipboard?.writeText) {
      await navigator.clipboard.writeText(text);
      return true;
    }
  } catch {
    // Fall back to execCommand below.
  }

  const textarea = document.createElement("textarea");
  textarea.value = text;
  textarea.setAttribute("readonly", "true");
  textarea.style.position = "fixed";
  textarea.style.opacity = "0";
  document.body.appendChild(textarea);
  textarea.select();

  try {
    return document.execCommand("copy");
  } finally {
    document.body.removeChild(textarea);
  }
}

function createMenuContext(node: JsonViewNode): JsonViewMenuActionContext {
  return {
    node,
    path: getDisplayPath(node.path),
    value: node.value,
    copy: copyText,
    closeMenu,
  };
}

function getDisplayPath(path: string) {
  if (path === props.rootPath) {
    return "$";
  }

  if (path.startsWith(`${props.rootPath}.`)) {
    return `$${path.slice(props.rootPath.length)}`;
  }

  if (path.startsWith(`${props.rootPath}[`)) {
    return `$${path.slice(props.rootPath.length)}`;
  }

  return path;
}

function getMenuItems(node: JsonViewNode): JsonViewMenuItem[] {
  const context = createMenuContext(node);
  const defaultItems: JsonViewMenuItem[] = [
    {
      key: "copy-value",
      label: "Copy Value",
      onSelect: async ({ value, copy }) => {
        await copy(serializeNodeValue(value));
      },
    },
    {
      key: "copy-path",
      label: "Copy Path",
      onSelect: async ({ path, copy }) => {
        await copy(path);
      },
    },
  ];

  if (!props.menuItems) {
    return defaultItems;
  }

  const customItems = Array.isArray(props.menuItems) ? props.menuItems : props.menuItems(context);
  return [...defaultItems, ...customItems];
}

async function handleMenuItemClick(item: JsonViewMenuItem, node: JsonViewNode) {
  if (item.disabled) return;
  await item.onSelect?.(createMenuContext(node));
  closeMenu();
}

function isMenuOpen(node: JsonViewNode) {
  return activeMenuNode.value?.id === node.id;
}

function closeMenu() {
  sharedMenuRef.value?.handleClose();
  suppressMenuHover.value = true;
  activeMenuTrigger.value?.blur();
  activeMenuNode.value = null;
  activeMenuTrigger.value = null;
}

async function openMenu(node: JsonViewNode, event: MouseEvent) {
  const trigger = event.currentTarget;
  if (!(trigger instanceof HTMLElement)) return;

  if (isMenuOpen(node)) {
    closeMenu();
    return;
  }

  activeMenuNode.value = node;
  activeMenuTrigger.value = trigger;
  suppressMenuHover.value = false;
  await nextTick();
  sharedMenuRef.value?.handleOpen();
}

function handleMenuVisibleChange(visible: boolean) {
  if (!visible) {
    suppressMenuHover.value = true;
    activeMenuTrigger.value?.blur();
    activeMenuNode.value = null;
    activeMenuTrigger.value = null;
  }
}

function handleMenuCommand(command: string | number | object) {
  if (!activeMenuNode.value) return;
  const item = activeMenuItems.value.find((entry) => entry.key === command);
  if (!item) return;
  void handleMenuItemClick(item, activeMenuNode.value);
}

function handleViewportPointerMove() {
  if (suppressMenuHover.value) {
    suppressMenuHover.value = false;
  }
}

function clearMenuHoverSuppression() {
  suppressMenuHover.value = false;
}

onMounted(() => {
  if (viewportRef.value) {
    viewportWidth.value = viewportRef.value.getBoundingClientRect().width;
  }

  resizeObserver = new ResizeObserver((entries) => {
    for (const entry of entries) {
      viewportWidth.value = entry.contentRect.width;
    }
  });

  if (viewportRef.value) {
    resizeObserver.observe(viewportRef.value);
  }

});

onUnmounted(() => {
  if (resizeObserver) {
    resizeObserver.disconnect();
    resizeObserver = null;
  }
});
</script>

<style lang="scss" scoped>
.json-view {
  --json-bg: #fff;
  --json-border: #dfe4ea;
  --json-color: #1f2937;
  --json-muted: #6b7280;
  --json-key: #7c3aed;
  --json-string: #0f766e;
  --json-number: #c2410c;
  --json-boolean: #2563eb;
  --json-null: #9333ea;
  --json-guide: rgba(148, 163, 184, 0.26);
  --json-hover: rgba(51, 103, 209, 0.08);
  --json-toggle: #64748b;
  --json-menu-bg: rgba(255, 255, 255, 0.96);
  --json-menu-border: rgba(203, 213, 225, 0.9);
  --json-menu-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
  --json-row-gap: 4px;
  background: var(--json-bg);
  border: 1px solid var(--json-border);
  border-radius: 10px;
  color: var(--json-color);
  font: 13px/20px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace;
}

.json-view--dark {
  --json-bg: #0f172a;
  --json-border: #243041;
  --json-color: #e2e8f0;
  --json-muted: #94a3b8;
  --json-key: #c084fc;
  --json-string: #34d399;
  --json-number: #fb923c;
  --json-boolean: #60a5fa;
  --json-null: #d8b4fe;
  --json-guide: rgba(148, 163, 184, 0.18);
  --json-hover: rgba(96, 165, 250, 0.12);
  --json-toggle: #94a3b8;
  --json-menu-bg: rgba(15, 23, 42, 0.96);
  --json-menu-border: rgba(71, 85, 105, 0.9);
  --json-menu-shadow: 0 12px 32px rgba(2, 6, 23, 0.45);
}

.json-view__viewport {
  overflow-y: auto;
  overflow-x: auto;
  position: relative;
  min-height: 80px;
}

.json-view--wrap-lines .json-view__viewport {
  overflow-x: hidden;
}

.json-view__spacer {
  position: relative;
}

.json-view__list {
  position: relative;
}

.json-view__row {
  display: flex;
  align-items: stretch;
  min-width: max-content;
}

.json-view--wrap-lines .json-view__row {
  min-width: 100%;
}

.json-view__row:hover {
  background: var(--json-hover);
}

.json-view__line-number {
  box-sizing: border-box;
  color: var(--json-muted);
  padding: 0 6px 0 6px;
  text-align: right;
  user-select: none;
}

.json-view__line {
  align-items: flex-start;
  box-sizing: border-box;
  display: flex;
  flex: 1;
  gap: var(--json-row-gap);
  min-height: 100%;
  min-width: 0;
  padding: 0 12px 0 8px;
  position: relative;
  white-space: nowrap;
}

.json-view--wrap-lines .json-view__line {
  width: 100%;
}

.json-view--show-line .json-view__line::before {
  background-image: repeating-linear-gradient(
    to right,
    transparent 0 calc(var(--json-indent-size) - 1px),
    var(--json-guide) calc(var(--json-indent-size) - 1px) var(--json-indent-size)
  );
  background-position: left top;
  background-repeat: no-repeat;
  background-size: var(--json-guides-width) 100%;
  content: "";
  inset: 0 auto 0 0;
  pointer-events: none;
  position: absolute;
  width: var(--json-leading-width);
}

.json-view__toggle {
  align-self: flex-start;
  flex: 0 0 20px;
  height: 20px;
  position: relative;
  width: 20px;
}

.json-view__toggle {
  background: transparent;
  border: none;
  border-radius: 6px;
  color: var(--json-toggle);
  cursor: pointer;
  padding: 0;
}

.json-view__toggle:hover {
  background: rgba(148, 163, 184, 0.1);
}

.json-view__toggle::before {
  border-bottom: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  content: "";
  height: 6px;
  inset: 6px auto auto 6px;
  position: absolute;
  transform: rotate(-45deg);
  transition: transform 0.12s ease;
  width: 6px;
}

.json-view__toggle.is-expanded::before {
  transform: rotate(45deg);
}

.json-view__key {
  color: var(--json-key);
  flex: 0 1 auto;
}

.json-view__colon {
  color: var(--json-muted);
}

.json-view__value {
  color: var(--json-color);
  flex: 1 1 auto;
  min-width: 0;
}

.json-view--wrap-lines .json-view__value {
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  word-break: break-word;
}

.json-view--wrap-lines .json-view__key,
.json-view--wrap-lines .json-view__length {
  white-space: nowrap;
}

.json-view:not(.json-view--wrap-lines) .json-view__value {
  overflow: hidden;
  text-overflow: ellipsis;
}

.json-view__value--string {
  color: var(--json-string);
}

.json-view__value--number {
  color: var(--json-number);
}

.json-view__value--boolean {
  color: var(--json-boolean);
}

.json-view__value--null,
.json-view__value--undefined {
  color: var(--json-null);
}

.json-view__value--container {
  color: var(--json-color);
}

.json-view__length {
  color: var(--json-muted);
}

.json-view__menu-shell {
  margin-left: auto;
  position: relative;
}

.json-view__menu-trigger {
  background: transparent;
  border: none;
  border-radius: 6px;
  color: var(--json-toggle);
  cursor: pointer;
  height: 18px;
  opacity: 0;
  padding: 0;
  pointer-events: none;
  position: relative;
  transition:
    opacity 0.12s ease,
    background-color 0.12s ease;
  width: 18px;
}

.json-view__menu-trigger::before {
  background: currentColor;
  border-radius: 50%;
  box-shadow:
    0 -4px 0 currentColor,
    0 4px 0 currentColor;
  content: "";
  height: 3px;
  inset: 7px auto auto 7px;
  position: absolute;
  width: 3px;
}

.json-view:not(.json-view--suppress-menu-hover) .json-view__row:hover .json-view__menu-trigger,
.json-view__row:focus-within .json-view__menu-trigger,
.json-view__menu-trigger[aria-expanded="true"] {
  opacity: 1;
  pointer-events: auto;
}

.json-view__menu-trigger:hover,
.json-view__menu-trigger:focus-visible {
  background: rgba(148, 163, 184, 0.1);
  outline: none;
}

:deep(.json-view__menu-dropdown.el-popper) {
  background: var(--json-menu-bg);
  border: 1px solid var(--json-menu-border);
  border-radius: 10px;
  box-shadow: var(--json-menu-shadow);
  padding: 6px;
}

:deep(.json-view__menu-dropdown .el-popper__arrow) {
  display: none;
}

:deep(.json-view__menu-dropdown .el-dropdown-menu) {
  background: transparent;
  border: none;
  box-shadow: none;
  min-width: 136px;
  padding: 0;
}

:deep(.json-view__menu-dropdown .el-dropdown-menu__item) {
  border-radius: 8px;
  color: inherit;
  font: inherit;
  line-height: 1.25;
  padding: 7px 10px;
}

:deep(.json-view__menu-dropdown .el-dropdown-menu__item:not(.is-disabled):hover),
:deep(.json-view__menu-dropdown .el-dropdown-menu__item:focus-visible) {
  background: rgba(148, 163, 184, 0.12);
  color: inherit;
}

:deep(.json-view__menu-dropdown .el-dropdown-menu__item.is-disabled) {
  opacity: 0.5;
}

.json-view__shared-menu {
  height: 0;
  left: 0;
  overflow: hidden;
  pointer-events: none;
  position: absolute;
  top: 0;
  width: 0;
}

.json-view__shared-menu-anchor {
  display: block;
  height: 0;
  width: 0;
}
</style>
