Element Picker

Reusable element picker library for userscripts.

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greasyfork.org/scripts/589922/1893812/Element%20Picker.js을(를) 사용하여 포함하는 라이브러리입니다.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name           Element Picker
// @namespace      https://greasyfork.org/users/821661
// @version        0.0.3
// @description    Reusable element picker library for userscripts with premium UI. Includes DOM traversal, metrics, unique CSS selectors, and theme customization.
// @author         hdyzen
// @grant          none
// ==/UserScript==

(() => {
    function elementPicker(globalConfig = {}) {
        const state = {
            enabled: false,
            isLocked: false,
            hoveredElement: null,
            selectedElement: null,
            filter: null,
            exclude: null,
        };

        const selectCallbacks = new Set();
        const customActions = new Set();

        const xrayHiddenElements = new Set();
        let lastMouseX = 0;
        let lastMouseY = 0;

        const icons = {
            grip: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/></svg>`,
            mouse: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/><path d="M13 13l6 6"/></svg>`,
            copy: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
            check: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
            x: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
            lock: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
        };

        const host = document.createElement("div");
        host.id = "element-picker-host";
        host.style.cssText = "position: fixed; inset: 0; z-index: 2147483647; pointer-events: none;";
        const shadow = host.attachShadow({ mode: "closed" });
        (document.body || document.documentElement).appendChild(host);

        const style = document.createElement("style");
        style.textContent = `
            :host { 
                /* Theme Variables - Dark Mode Default */
                --ep-primary: #0070F3;
                --ep-bg: #111111;
                --ep-text: #EDEDED;
                --ep-text-muted: rgba(255, 255, 255, 0.7);
                --ep-border: rgba(255, 255, 255, 0.08);
                --ep-divider: rgba(255, 255, 255, 0.15);
                --ep-pill-bg: rgba(255, 255, 255, 0.04);
                
                --ep-highlight-border: rgba(128, 128, 128, 0.5);
                --ep-badge-bg: #111111;
                --ep-badge-text: #ffffff;
                
                --ep-btn-bg: #ffffff;
                --ep-btn-text: #000000;

                font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; 
            }

            .ep-highlight { position: fixed; pointer-events: none; transition: all 0.1s cubic-bezier(0.2, 0, 0, 1); z-index: 9998; }
            .ep-highlight-border { position: absolute; inset: 0; border: 1.5px solid; transition: border-color 0.15s, opacity 0.15s; }
            .ep-highlight-badge { position: absolute; top: -28px; left: 0; padding: 4px 8px; display: flex; align-items: center; gap: 6px; border-radius: 4px; font-size: 11px; font-family: ui-monospace, monospace; font-weight: 500; box-shadow: 0 1px 2px rgba(0,0,0,0.05); transition: background-color 0.15s, color 0.15s; white-space: nowrap; max-width: 90vw;}
            
            .ep-badge-divider { width: 1px; height: 12px; background: currentColor; opacity: 0.25; margin: 0 2px; }
            .ep-badge-metrics { opacity: 0.9; display: flex; align-items: center; gap: 2px; font-size: 10.5px; }
            .ep-badge-times { opacity: 0.5; font-size: 9px; margin-top: 1px; }
            .ep-badge-tag { color: var(--ep-primary); font-weight: 700; text-transform: lowercase; }
            .ep-badge-extra { color: currentColor; opacity: 0.7; font-style: italic; overflow: hidden; text-overflow: ellipsis; max-width: 150px; display: inline-block; vertical-align: bottom; }

            .ep-ui-bar { position: fixed; bottom: 32px; left: 50%; transform: translateX(-50%); background-color: var(--ep-bg); color: var(--ep-text); border: 1px solid var(--ep-border); box-shadow: 0 12px 24px -4px rgba(0,0,0,0.2); border-radius: 9999px; height: 40px; padding: 0 6px; display: flex; align-items: center; animation: ep-slide-up 0.2s ease-out forwards; pointer-events: auto; z-index: 10000; box-sizing: border-box; max-width: 95vw; }
            
            .ep-drag-handle { display: flex; align-items: center; padding: 0 4px; opacity: 0.4; cursor: grab; }
            .ep-drag-handle:active { cursor: grabbing; opacity: 0.8; }
            
            .ep-path-pill { display: flex; align-items: center; gap: 10px; padding: 0 16px 0 12px; height: 28px; background: var(--ep-pill-bg); border: 1px solid var(--ep-pill-bg); border-radius: 9999px; margin-left: 4px; overflow: hidden; flex: 1; }
            
            .ep-ping-container { position: relative; display: flex; width: 6px; height: 6px; flex-shrink: 0;}
            .ep-ping-dot { position: relative; display: inline-flex; border-radius: 50%; width: 6px; height: 6px; transition: opacity 0.2s, background-color 0.2s; }
            .ep-ping-anim { position: absolute; display: inline-flex; width: 100%; height: 100%; border-radius: 50%; background-color: var(--ep-primary); opacity: 0.6; animation: ep-ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite; }
            
            .ep-breadcrumbs { display: flex; align-items: center; overflow-x: auto; font-size: 12px; font-family: ui-monospace, monospace; color: var(--ep-text-muted); scrollbar-width: none; white-space: nowrap; mask-image: linear-gradient(to right, transparent, black 10px, black 95%, transparent); padding: 0 10px; }
            .ep-breadcrumbs::-webkit-scrollbar { display: none; }
            .ep-crumb { cursor: pointer; padding: 2px 4px; border-radius: 4px; transition: background 0.15s, color 0.15s; }
            .ep-crumb:hover { background: var(--ep-divider); color: var(--ep-text); }
            .ep-crumb.active { color: var(--ep-text); font-weight: 600; }
            .ep-crumb-sep { opacity: 0.3; margin: 0 2px; user-select: none; }
            
            .ep-nav-divider { width: 1px; height: 14px; background: var(--ep-text); opacity: 0.1; margin: 0 2px 0 4px; flex-shrink: 0; }
            .ep-nav-hint { display: flex; align-items: center; gap: 4px; opacity: 0.4; user-select: none; flex-shrink: 0; }
            .ep-nav-text { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 9px; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
            .ep-nav-arrows { display: flex; align-items: center; gap: 2px; font-size: 11px; margin-top: 1px; }

            .ep-actions { display: flex; align-items: center; gap: 2px; margin-left: 4px; flex-shrink: 0; }
            .ep-divider { width: 1px; height: 14px; background: var(--ep-divider); margin: 0 6px; }
            
            .ep-btn-icon { width: 28px; height: 28px; background: none; border: none; padding: 0; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--ep-text-muted); transition: all 0.15s; flex-shrink: 0; }
            .ep-btn-icon:hover:not(:disabled) { background: var(--ep-pill-bg); color: var(--ep-text); }
            .ep-btn-icon:disabled { opacity: 0.3; cursor: not-allowed; }
            
            .ep-btn-text { height: 28px; background: var(--ep-btn-bg); color: var(--ep-btn-text); border: none; padding: 0 12px; font-size: 12px; font-weight: 500; border-radius: 9999px; cursor: pointer; transition: filter 0.15s; margin: 0 2px; display: flex; align-items: center; white-space: nowrap; flex-shrink: 0; }
            .ep-btn-text:hover { filter: brightness(0.9); }
            
            .ep-btn-primary { background: var(--ep-primary); color: #fff; }
            
            @keyframes ep-ping { 75%, 100% { transform: scale(2.5); opacity: 0; } }
            @keyframes ep-slide-up { from { opacity: 0; transform: translate(-50%, 16px); } to { opacity: 1; transform: translate(-50%, 0); } }
        `;

        const highlightLayer = document.createElement("div");
        highlightLayer.className = "ep-highlight";
        highlightLayer.style.display = "none";
        const highlightBorder = document.createElement("div");
        highlightBorder.className = "ep-highlight-border";
        const highlightBadge = document.createElement("div");
        highlightBadge.className = "ep-highlight-badge";
        highlightLayer.append(highlightBorder, highlightBadge);

        const uiContainer = document.createElement("div");
        uiContainer.className = "ep-ui-bar";
        uiContainer.style.display = "none";

        shadow.append(style, highlightLayer, uiContainer);

        let rafMouse = null;
        let rafScroll = null;

        let isDragging = false;
        let startX = 0,
            startY = 0;
        let currentX = 0,
            currentY = 0;

        uiContainer.addEventListener("mousedown", (e) => {
            if (e.target.closest("button, .ep-breadcrumbs, .ep-crumb")) return;
            isDragging = true;
            const rect = uiContainer.getBoundingClientRect();
            uiContainer.style.animation = "none";
            uiContainer.style.transform = "none";
            uiContainer.style.bottom = "auto";
            uiContainer.style.left = `${rect.left}px`;
            uiContainer.style.top = `${rect.top}px`;
            startX = e.clientX;
            startY = e.clientY;
            currentX = rect.left;
            currentY = rect.top;
            document.addEventListener("mousemove", onDragMove);
            document.addEventListener("mouseup", onDragEnd);
        });

        const onDragMove = (e) => {
            if (!isDragging) return;
            const rect = uiContainer.getBoundingClientRect();
            const maxX = window.innerWidth - rect.width;
            const maxY = window.innerHeight - rect.height;
            const newX = Math.max(0, Math.min(currentX + (e.clientX - startX), maxX));
            const newY = Math.max(0, Math.min(currentY + (e.clientY - startY), maxY));
            currentX = newX;
            currentY = newY;
            uiContainer.style.left = `${currentX}px`;
            uiContainer.style.top = `${currentY}px`;
            startX = e.clientX;
            startY = e.clientY;
        };

        const onDragEnd = () => {
            isDragging = false;
            document.removeEventListener("mousemove", onDragMove);
            document.removeEventListener("mouseup", onDragEnd);
        };

        const generateUniqueSelector = (el) => {
            if (!el || el.nodeType !== Node.ELEMENT_NODE) return "";
            const isUnique = (selector) => {
                try {
                    const matches = document.querySelectorAll(selector);
                    return matches.length === 1 && matches[0] === el;
                } catch (e) {
                    return false;
                }
            };

            if (el.id) {
                const idSelector = `#${CSS.escape(el.id)}`;
                if (isUnique(idSelector)) return idSelector;
            }

            for (const attr of ["data-testid", "data-cy", "data-test"]) {
                if (el.hasAttribute(attr)) {
                    const dataSelector = `[${attr}="${CSS.escape(el.getAttribute(attr))}"]`;
                    if (isUnique(dataSelector)) return dataSelector;
                }
            }

            if (el.hasAttribute("name")) {
                const nameSelector = `${el.tagName.toLowerCase()}[name="${CSS.escape(el.getAttribute("name"))}"]`;
                if (isUnique(nameSelector)) return nameSelector;
            }

            const path = [];
            let current = el;

            while (current && current.nodeType === Node.ELEMENT_NODE) {
                let selectorPart = current.tagName.toLowerCase();

                if (current.id) {
                    selectorPart = `#${CSS.escape(current.id)}`;
                    path.unshift(selectorPart);
                    if (isUnique(path.join(" > "))) return path.join(" > ");
                    break;
                }

                const classes = Array.from(current.classList).filter((c) => !c.startsWith("hover:") && !c.startsWith("focus:"));
                if (classes.length > 0) selectorPart += `.${classes.map(CSS.escape).join(".")}`;

                let needsNthChild = false;
                if (current.parentElement) {
                    const siblings = Array.from(current.parentElement.children);
                    const similarSiblings = siblings.filter((s) => {
                        if (s.tagName.toLowerCase() !== current.tagName.toLowerCase()) return false;
                        const sClasses = Array.from(s.classList).filter((c) => !c.startsWith("hover:") && !c.startsWith("focus:"));
                        return classes.join(".") === sClasses.join(".");
                    });
                    if (similarSiblings.length > 1) needsNthChild = true;
                }

                if (needsNthChild) {
                    const index = Array.from(current.parentNode.children).indexOf(current) + 1;
                    selectorPart += `:nth-child(${index})`;
                }

                path.unshift(selectorPart);
                if (isUnique(path.join(" > "))) return path.join(" > ");
                current = current.parentElement;
            }
            return path.join(" > ");
        };

        const renderUI = () => {
            const target = state.isLocked ? state.selectedElement : state.hoveredElement;
            const selectorStr = target ? generateUniqueSelector(target) : "";

            const actionsHTML = Array.from(customActions)
                .map(
                    (action) => `
                <button class="ep-btn-text ${action.primary ? "ep-btn-primary" : ""}" id="${action.id}">
                    ${action.label}
                </button>
                `,
                )
                .join("");

            uiContainer.innerHTML = `
                <div class="ep-drag-handle" title="Drag to move">${icons.grip}</div>
                <div class="ep-path-pill">
                    <div class="ep-ping-container">
                        ${!state.isLocked ? '<span class="ep-ping-anim"></span>' : ""}
                        <span class="ep-ping-dot" style="background-color: var(--ep-primary); opacity: ${state.isLocked ? "1" : "0.8"};"></span>
                    </div>
                    <div class="ep-breadcrumbs" id="ep-breadcrumbs-container"></div>
                    ${
                        state.isLocked
                            ? `
                        <div class="ep-nav-divider"></div>
                        <div class="ep-nav-hint"><span class="ep-nav-text">Nav</span><span class="ep-nav-arrows"><span>↑</span><span>↓</span></span></div>
                    `
                            : ""
                    }
                </div>
                <div class="ep-actions">
                    <button class="ep-btn-icon" id="ep-btn-copy" title="Copy selector" ${!target ? "disabled" : ""}>
                        ${icons.copy}
                    </button>
                    <div class="ep-divider"></div>
                    ${
                        state.isLocked
                            ? `
                        ${actionsHTML}
                        <button class="ep-btn-icon" id="ep-btn-deselect" title="Unlock selection">${icons.x}</button>
                    `
                            : `
                        <button class="ep-btn-icon" id="ep-btn-close" title="Close Picker (Esc)">${icons.x}</button>
                    `
                    }
                </div>
            `;

            const breadcrumbsContainer = uiContainer.querySelector("#ep-breadcrumbs-container");
            if (target && breadcrumbsContainer) {
                const path = [];
                let curr = target;
                while (curr && path.length < 5) {
                    path.unshift(curr);
                    if (curr.tagName.toLowerCase() === "html") break;
                    curr = curr.parentElement;
                }

                path.forEach((el, index) => {
                    const isLast = index === path.length - 1;
                    const crumb = document.createElement("span");
                    crumb.className = `ep-crumb ${isLast ? "active" : ""}`;
                    crumb.textContent = el.tagName.toLowerCase() + (el.id ? `#${el.id}` : el.classList.length ? `.${el.classList[0]}` : "");
                    crumb.title = generateUniqueSelector(el);

                    crumb.addEventListener("click", () => setSelectedElement(el));
                    breadcrumbsContainer.appendChild(crumb);

                    if (!isLast) {
                        const sep = document.createElement("span");
                        sep.className = "ep-crumb-sep";
                        sep.textContent = "›";
                        breadcrumbsContainer.appendChild(sep);
                    }
                });

                requestAnimationFrame(() => {
                    breadcrumbsContainer.scrollLeft = breadcrumbsContainer.scrollWidth;
                });
            } else if (!target && breadcrumbsContainer) {
                breadcrumbsContainer.textContent = "Hover an element...";
            }

            const copyBtn = uiContainer.querySelector("#ep-btn-copy");
            if (copyBtn)
                copyBtn.addEventListener("click", () => {
                    if (target) {
                        navigator.clipboard.writeText(selectorStr);
                        copyBtn.innerHTML = icons.check;
                        setTimeout(() => (copyBtn.innerHTML = icons.copy), 2000);
                    }
                });

            const closeBtn = uiContainer.querySelector("#ep-btn-close");
            if (closeBtn) closeBtn.addEventListener("click", disable);

            const deselectBtn = uiContainer.querySelector("#ep-btn-deselect");
            if (deselectBtn)
                deselectBtn.addEventListener("click", () => {
                    state.isLocked = false;
                    state.selectedElement = null;
                    renderUI();
                    updateLayer(state.hoveredElement);
                });

            if (state.isLocked) {
                for (const action of customActions) {
                    const btn = uiContainer.querySelector(`#${action.id}`);
                    if (btn && typeof action.onClick === "function") {
                        btn.addEventListener("click", () => {
                            if (target) action.onClick(target);
                        });
                    }
                }
            }
        };

        const updateLayer = (element) => {
            if (!element?.isConnected) {
                highlightLayer.style.display = "none";
                return;
            }

            const rect = element.getBoundingClientRect();
            Object.assign(highlightLayer.style, {
                display: "block",
                left: `${rect.left}px`,
                top: `${rect.top}px`,
                width: `${rect.width}px`,
                height: `${rect.height}px`,
            });

            highlightBorder.style.borderColor = state.isLocked ? "var(--ep-primary)" : "var(--ep-highlight-border)";
            highlightBorder.style.opacity = state.isLocked ? "1" : "0.4";
            highlightBadge.style.backgroundColor = state.isLocked ? "var(--ep-primary)" : "var(--ep-badge-bg)";
            highlightBadge.style.color = state.isLocked ? "#fff" : "var(--ep-badge-text)";

            const metricsHTML =
                Math.round(rect.width) > 0
                    ? `
                <div class="ep-badge-divider"></div>
                <span class="ep-badge-metrics">${Math.round(rect.width)}<span class="ep-badge-times">×</span>${Math.round(rect.height)}</span>
            `
                    : "";

            let extraInfo = "";
            if (element.tagName === "A" && element.href) extraInfo = `<span class="ep-badge-extra" title="${element.href}">${element.getAttribute("href")}</span>`;
            if (element.tagName === "IMG" && element.src) extraInfo = `<span class="ep-badge-extra" title="${element.src}">${element.getAttribute("src")}</span>`;

            highlightBadge.innerHTML = `
                ${state.isLocked ? icons.lock : ""}
                <span style="display: flex; align-items: center; gap: 4px;">
                    <span class="ep-badge-tag">${element.tagName.toLowerCase()}</span>
                    <span style="overflow: hidden; text-overflow: ellipsis;">${element.id ? `#${element.id}` : element.className && typeof element.className === "string" ? `.${element.className.split(" ").join(".")}` : ""}</span>
                    ${extraInfo}
                </span>
                ${metricsHTML}
            `;

            renderUI();
        };

        const handleGlobalMouseMove = (ev) => {
            lastMouseX = ev.clientX;
            lastMouseY = ev.clientY;
        };

        const handleMouseover = (ev) => {
            if (!state.enabled || state.isLocked) return;
            if (ev.composedPath().includes(host)) return;

            const element = ev.composedPath()[0];
            if (!(element instanceof Element)) return;

            if (state.filter && !element.matches(state.filter)) return;
            if (state.exclude && element.matches(state.exclude)) return;

            if (rafMouse) cancelAnimationFrame(rafMouse);
            rafMouse = requestAnimationFrame(() => {
                state.hoveredElement = element;
                updateLayer(element);
            });
        };

        const handleClick = (ev) => {
            if (!state.enabled) return;
            if (ev.composedPath().includes(host)) return;

            ev.preventDefault();
            ev.stopPropagation();

            const element = ev.composedPath()[0];
            if (!(element instanceof Element)) return;

            if (state.filter && !element.matches(state.filter)) return;
            if (state.exclude && element.matches(state.exclude)) return;

            if (state.selectedElement === element) {
                state.isLocked = false;
                state.selectedElement = null;
            } else {
                state.selectedElement = element;
                state.isLocked = true;
            }

            updateLayer(element);
            if (state.isLocked) {
                for (const callback of selectCallbacks) callback(element);
            }
        };

        const handleScroll = () => {
            if (!state.enabled) return;
            if (rafScroll) cancelAnimationFrame(rafScroll);
            rafScroll = requestAnimationFrame(() => {
                updateLayer(state.isLocked ? state.selectedElement : state.hoveredElement);
            });
        };

        const handleKeydown = (ev) => {
            if (globalConfig.toggleHotkey) {
                const keys = globalConfig.toggleHotkey.toLowerCase().split("+");
                const isMatch = keys.every((key) => {
                    if (key === "ctrl") return ev.ctrlKey;
                    if (key === "shift") return ev.shiftKey;
                    if (key === "alt") return ev.altKey;
                    return ev.key.toLowerCase() === key;
                });
                if (isMatch) {
                    ev.preventDefault();
                    toggle();
                    return;
                }
            }

            if (!state.enabled) return;

            if (ev.key === "Alt" && !state.isLocked) {
                ev.preventDefault();
                const hoverEl = state.hoveredElement;
                if (hoverEl) {
                    hoverEl.style.setProperty("pointer-events", "none", "important");
                    xrayHiddenElements.add(hoverEl);

                    const elementBeneath = document.elementFromPoint(lastMouseX, lastMouseY);
                    if (elementBeneath && elementBeneath !== hoverEl) {
                        state.hoveredElement = elementBeneath;
                        updateLayer(elementBeneath);
                    }
                }
            }

            if (ev.key === "Escape") {
                ev.preventDefault();
                disable();
                return;
            }

            if (state.isLocked && state.selectedElement) {
                const keys = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];
                if (keys.includes(ev.key)) {
                    ev.preventDefault();
                    let nextEl = null;
                    const current = state.selectedElement;

                    if (ev.key === "ArrowUp") {
                        nextEl = current.parentElement;
                        if (nextEl === document.documentElement || nextEl === document.body) nextEl = null;
                    } else if (ev.key === "ArrowDown") {
                        nextEl = current.firstElementChild;
                    } else if (ev.key === "ArrowRight") {
                        nextEl = current.nextElementSibling;
                    } else if (ev.key === "ArrowLeft") {
                        nextEl = current.previousElementSibling;
                    }

                    if (nextEl && !ev.composedPath().includes(host)) {
                        setSelectedElement(nextEl);
                        nextEl.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
                    }
                }
            }
        };

        const handleKeyup = (ev) => {
            if (ev.key === "Alt" && xrayHiddenElements.size > 0) {
                xrayHiddenElements.forEach((el) => el.style.removeProperty("pointer-events"));
                xrayHiddenElements.clear();

                const elementTop = document.elementFromPoint(lastMouseX, lastMouseY);
                if (elementTop && !state.isLocked) {
                    state.hoveredElement = elementTop;
                    updateLayer(elementTop);
                }
            }
        };

        const enable = (options = {}) => {
            if (state.enabled) return;
            state.enabled = true;
            state.isLocked = false;

            state.filter = options.filter || null;
            state.exclude = options.exclude || null;

            uiContainer.style.animation = "";
            uiContainer.style.transform = "translateX(-50%)";
            uiContainer.style.bottom = "32px";
            uiContainer.style.left = "50%";
            uiContainer.style.top = "auto";
            uiContainer.style.display = "flex";

            renderUI();

            document.addEventListener("mousemove", handleGlobalMouseMove, true);
            document.addEventListener("mouseover", handleMouseover, true);
            document.addEventListener("click", handleClick, true);
            document.addEventListener("scroll", handleScroll, true);
            window.addEventListener("resize", handleScroll, true);
        };

        const disable = () => {
            if (!state.enabled) return;
            state.enabled = false;
            state.isLocked = false;
            state.hoveredElement = null;
            state.selectedElement = null;

            xrayHiddenElements.forEach((el) => el.style.removeProperty("pointer-events"));
            xrayHiddenElements.clear();

            document.removeEventListener("mousemove", handleGlobalMouseMove, true);
            document.removeEventListener("mouseover", handleMouseover, true);
            document.removeEventListener("click", handleClick, true);
            document.removeEventListener("scroll", handleScroll, true);
            window.removeEventListener("resize", handleScroll, true);

            if (rafMouse) cancelAnimationFrame(rafMouse);
            if (rafScroll) cancelAnimationFrame(rafScroll);

            highlightLayer.style.display = "none";
            uiContainer.style.display = "none";
        };

        const toggle = (options) => (state.enabled ? disable() : enable(options));

        const onSelect = (callback) => {
            selectCallbacks.add(callback);
            return () => selectCallbacks.delete(callback);
        };

        const pickOnce = (options = {}) => {
            return new Promise((resolve) => {
                enable(options);
                const unsubscribe = onSelect((element) => {
                    unsubscribe();
                    disable();
                    resolve(element);
                });
            });
        };

        const addAction = ({ label, primary = false, onClick }) => {
            const id = `ep-action-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
            const action = { id, label, primary, onClick };
            customActions.add(action);
            if (state.enabled && state.isLocked) renderUI();
            return id;
        };

        const removeAction = (id) => {
            for (const action of customActions) {
                if (action.id === id) {
                    customActions.delete(action);
                    break;
                }
            }
            if (state.enabled && state.isLocked) renderUI();
        };

        const removeAllActions = () => {
            customActions.clear();
            if (state.enabled && state.isLocked) renderUI();
        };

        const setTheme = (colors = {}) => {
            const cssVarMapping = {
                primary: "--ep-primary",
                background: "--ep-bg",
                text: "--ep-text",
                textMuted: "--ep-text-muted",
                border: "--ep-border",
                divider: "--ep-divider",
                pillBackground: "--ep-pill-bg",
                highlightBorder: "--ep-highlight-border",
                badgeBackground: "--ep-badge-bg",
                badgeText: "--ep-badge-text",
                buttonBackground: "--ep-btn-bg",
                buttonText: "--ep-btn-text",
            };
            for (const [key, value] of Object.entries(colors)) {
                if (cssVarMapping[key] && value) host.style.setProperty(cssVarMapping[key], value);
            }
        };

        const getHoveredElement = () => state.hoveredElement;
        const getSelectedElement = () => state.selectedElement;
        const isEnabled = () => state.enabled;

        const setSelectedElement = (element) => {
            if (!(element instanceof Element)) return;
            state.selectedElement = element;
            state.isLocked = true;
            updateLayer(element);
            for (const callback of selectCallbacks) callback(element);
        };

        const refresh = () => updateLayer(state.isLocked ? state.selectedElement : state.hoveredElement);

        const destroy = () => {
            disable();
            document.removeEventListener("keydown", handleKeydown, true);
            document.removeEventListener("keyup", handleKeyup, true);
            selectCallbacks.clear();
            customActions.clear();
            host.remove();
        };

        document.addEventListener("keydown", handleKeydown, true);
        document.addEventListener("keyup", handleKeyup, true);

        return {
            enable,
            disable,
            toggle,
            pickOnce,
            isEnabled,
            getHoveredElement,
            getSelectedElement,
            setSelectedElement,
            onSelect,
            addAction,
            removeAction,
            removeAllActions,
            setTheme,
            refresh,
            destroy,
        };
    }

    Object.defineProperty(globalThis, "elementPicker", {
        value: elementPicker,
        writable: false,
    });
})();