import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import React, { useEffect, useMemo, useCallback, memo, useRef, useState, } from "react";
import { Loader2 } from "lucide-react";
import { ChevronRight } from "lucide-react";
import { setMultiDragImage } from "../ui/DragPreview";
/**
 * Usage example with keyboard search:
 *
 * ```tsx
 * import { PerfectTree } from './PerfectTree';
 *
 * <PerfectTree
 *   nodes={treeNodes}
 *   enableKeyboardSearch={true}
 *   renderNode={(node) => (
 *     <div>
 *       {node.icon}
 *       {node.label}
 *     </div>
 *   )}
 *   // ... other props
 * />
 * ```
 *
 * Note: Text highlighting is applied automatically when enableKeyboardSearch is true.
 * The highlightText helper function is still available for manual highlighting if needed.
 */
// Local DropZone component to handle drag-over state.
const DropZone = memo(({ parent, index, isDragging, onDragOverZone, onDrop, onDragEnd, isLast, isValidDropZone, }) => {
    const [isDragOver, setIsDragOver] = React.useState(false);
    const [isValidDrop, setIsValidDrop] = React.useState(true);
    useEffect(() => {
        if (isDragging) {
            if (isValidDropZone) {
                const isValid = isValidDropZone(parent, index);
                setIsValidDrop(isValid);
            }
            else {
                setIsValidDrop(true);
            }
        }
    }, [isValidDropZone, parent, index, isDragging]);
    const handleDragEnter = useCallback((e) => {
        e.preventDefault();
        e.stopPropagation();
        if (onDragOverZone) {
            const allowed = onDragOverZone(parent, index, e);
            setIsDragOver(allowed);
            //   e.dataTransfer.dropEffect = allowed ? "move" : "none";
        }
    }, [onDragOverZone, parent, index]);
    const handleDragLeave = useCallback((e) => {
        e.preventDefault();
        e.stopPropagation();
        setIsDragOver(false);
    }, []);
    const handleDragOver = useCallback((e) => {
        e.preventDefault();
        e.stopPropagation();
        if (onDragOverZone) {
            const allowed = onDragOverZone(parent, index, e);
            setIsDragOver(allowed);
        }
    }, [onDragOverZone, parent, index]);
    const handleDrop = useCallback((e) => {
        e.preventDefault();
        e.stopPropagation();
        setIsDragOver(false);
        if (onDrop) {
            onDrop(parent, index, e);
        }
    }, [onDrop, parent, index]);
    if (!isDragging || !isValidDrop)
        return null;
    return (_jsx("div", { className: `relative ${isLast ? "h-2" : ""}`, children: _jsx("div", { className: `drop-zone absolute top-[-5px] right-0 left-[45px] z-1000 h-2 rounded-md transition-all duration-100 ${isDragOver
                ? "border-component-blue bg-component-blue/20 border-2 border-dashed"
                : "border-2 border-transparent"}`, "data-drag-over": isDragOver, onDragEnter: handleDragEnter, onDragOver: handleDragOver, onDrop: handleDrop, onDragLeave: handleDragLeave }) }));
});
// Helper function to highlight matching text
export const highlightText = (text, searchTerm) => {
    if (!searchTerm.trim())
        return text;
    const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
    const parts = text.split(regex);
    return parts.map((part, index) => {
        if (regex.test(part)) {
            return (_jsx("span", { className: "bg-yellow-200 underline", children: part }, index));
        }
        return part;
    });
};
// Helper function to highlight text within React elements
const highlightReactElement = (element, searchTerm, nodeLabel) => {
    try {
        // If searchTerm doesn't match the node label, return as-is
        if (!nodeLabel.toLowerCase().includes(searchTerm.toLowerCase())) {
            return element;
        }
        const processChildren = (children) => {
            return React.Children.map(children, (child) => {
                if (typeof child === "string") {
                    // If it's a string and contains the search term, highlight it
                    if (child.toLowerCase().includes(searchTerm.toLowerCase())) {
                        return highlightText(child, searchTerm);
                    }
                    return child;
                }
                if (React.isValidElement(child)) {
                    // Recursively process React elements
                    const childProps = child.props;
                    if (childProps && childProps.children) {
                        return React.cloneElement(child, {
                            ...childProps,
                            children: processChildren(childProps.children),
                        });
                    }
                }
                return child;
            });
        };
        // Clone the element with processed children
        const elementProps = element.props;
        if (elementProps && elementProps.children) {
            return React.cloneElement(element, {
                ...elementProps,
                children: processChildren(elementProps.children),
            });
        }
    }
    catch (error) {
        // If any error occurs, return the original element
        console.warn("Error highlighting React element:", error);
    }
    return element;
};
// Helper function to check if a node matches the search term
const nodeMatchesSearch = (node, searchTerm) => {
    if (!searchTerm.trim())
        return true;
    return node.label.toLowerCase().includes(searchTerm.toLowerCase());
};
// Helper function to filter tree nodes based on search term
const filterTreeNodes = (nodes, searchTerm, expandedKeys) => {
    if (!searchTerm.trim())
        return nodes;
    const filterNode = (node) => {
        const nodeMatches = nodeMatchesSearch(node, searchTerm);
        const isExpanded = expandedKeys.includes(node.key);
        // Process children only if the node is expanded
        let filteredChildren = [];
        let hasMatchingChildren = false;
        if (isExpanded && node.children && Array.isArray(node.children)) {
            filteredChildren = node.children
                .map((child) => filterNode(child))
                .filter((child) => child !== null);
            hasMatchingChildren = filteredChildren.length > 0;
        }
        // Include node if it matches or has matching children
        if (nodeMatches || hasMatchingChildren) {
            return {
                ...node,
                children: isExpanded ? filteredChildren : node.children,
            };
        }
        return null;
    };
    return nodes
        .map((node) => filterNode(node))
        .filter((node) => node !== null);
};
// Helper function to flatten tree nodes into a depth-first list of visible nodes
const flattenVisibleNodes = (nodes, expandedKeys, parent = null, depth = 0) => {
    const result = [];
    for (const node of nodes) {
        // Add current node
        result.push({ node, parent, depth });
        // If expanded and has children, recursively add children
        const isExpanded = expandedKeys.includes(node.key);
        if (isExpanded && node.children && Array.isArray(node.children)) {
            result.push(...flattenVisibleNodes(node.children, expandedKeys, node, depth + 1));
        }
    }
    return result;
};
// NodeContent component extracted and memoized
const NodeContent = memo(({ node, isExpanded, isSelected, onSelect, onToggleNode, onStartDrag, onDragEnd, onDragOverZone, onDrop, onDoubleClick, renderNode, renderNodePrefix, depth, onContextMenu, enableDragAndDrop = false, selectedKeys, isDragging, searchTerm = "", multiDragNoun, }) => {
    const [isDragOver, setIsDragOver] = React.useState(false);
    useEffect(() => {
        if (!isDragging) {
            setIsDragOver(false);
        }
    }, [isDragging]);
    const handleDragStart = useCallback((event) => {
        const isMultiSelect = isSelected && selectedKeys && selectedKeys.length > 1;
        // Set drag preview for multiple items if applicable
        if (isMultiSelect)
            setMultiDragImage(event, selectedKeys.length, multiDragNoun || "items");
        if (onStartDrag) {
            onStartDrag({ node, event, isMultiSelect: isMultiSelect ?? false });
        }
    }, [node, onStartDrag, isSelected, selectedKeys, multiDragNoun]);
    const handleDragLeave = useCallback((event) => {
        event.preventDefault();
        setIsDragOver(false);
    }, []);
    const handleDragEnter = useCallback((event) => {
        event.preventDefault();
        // Only set drag-over state if we're tracking an internal drag
        if (isDragging && onDragOverZone) {
            const allowed = onDragOverZone(node, -1, event);
            setIsDragOver(allowed);
        }
    }, [node, onDragOverZone, isDragging]);
    const handleDragOver = useCallback((event) => {
        event.preventDefault();
        // Only set drag-over state if we're tracking an internal drag
        if (isDragging && onDragOverZone) {
            const allowed = onDragOverZone(node, -1, event);
            setIsDragOver(allowed);
            if (!allowed) {
                event.dataTransfer.dropEffect = "none";
            }
        }
    }, [node, onDragOverZone, isDragging]);
    const handleDrop = useCallback((e) => {
        e.preventDefault();
        e.stopPropagation();
        if (onDrop) {
            onDrop(node, -1, e);
        }
    }, [node, onDrop]);
    const handleDoubleClick = useCallback((e) => {
        e.stopPropagation();
        onDoubleClick?.(node);
    }, [node, onDoubleClick]);
    const handleSelect = useCallback((e) => {
        e.stopPropagation();
        onSelect(node.key, e);
    }, [node.key, onSelect]);
    // Middle-click (aux click) should behave like a normal click for consumers that want
    // "open in new slot/tab" semantics, but we only forward true middle-clicks.
    const handleAuxClick = useCallback((e) => {
        if (e.button !== 1)
            return;
        e.stopPropagation();
        // Prevent browser autoscroll / default middle-click behavior
        e.preventDefault();
        onSelect(node.key, e);
    }, [node.key, onSelect]);
    const handleToggle = useCallback((e) => {
        e.stopPropagation();
        onToggleNode(node);
    }, [node, onToggleNode]);
    const handleToggleDoubleClick = useCallback((e) => {
        e.stopPropagation();
        e.preventDefault();
    }, []);
    const renderToggle = () => {
        if (node.hasChildren && node.children === null) {
            return (_jsx("div", { className: "flex h-[23px] w-[24px] items-center justify-center", children: _jsx(Loader2, { className: "h-4 w-4 animate-spin text-gray-500" }) }));
        }
        return (_jsx("div", { className: "flex h-[23px] w-[24px] items-center justify-center", children: _jsx("span", { onMouseDown: handleToggle, onClick: (e) => e.stopPropagation(), onDoubleClick: handleToggleDoubleClick, className: `toggle inline-block transform cursor-pointer text-gray-500 transition duration-150 select-none ${isExpanded ? "rotate-90" : "rotate-0"}`, children: _jsx(ChevronRight, { strokeWidth: 1, className: "h-5.5 w-5.5" }) }) }));
    };
    const handleContextMenu = useCallback((e) => {
        e.stopPropagation();
        e.preventDefault();
        onContextMenu?.(node, e);
    }, [node, onContextMenu]);
    return (_jsxs("div", { className: `tree-node ${node?.type ? `tree-node-${node.type}` : ""} relative mb-0.5 flex cursor-pointer items-center`, draggable: enableDragAndDrop && !!node.isDraggable, onClick: handleSelect, onAuxClick: handleAuxClick, onDragStart: (event) => handleDragStart(event), onDragEnd: onDragEnd, onDragLeave: handleDragLeave, onDragEnter: handleDragEnter, onDragOver: handleDragOver, onDrop: handleDrop, onDoubleClick: handleDoubleClick, onContextMenu: handleContextMenu, "data-node-key": node.key, "data-selected": isSelected, "data-node-type": node?.type, children: [renderNodePrefix?.(node, depth), node.hasChildren || node.children?.length ? (renderToggle()) : (_jsx("div", { className: "w-[24px]" })), _jsx("div", { className: `flex-1 rounded-md border border-transparent p-0.5 pr-1.5 hover:border-gray-300 ${isDragOver ? "bg-sky-200" : isSelected ? "bg-blue-100" : ""}`, onClick: handleSelect, onAuxClick: handleAuxClick, children: renderNode(node, searchTerm) })] }));
});
export const PerfectTree = ({ nodes, selectedKeys = [], expandedKeys = [], renderNode, renderNodePrefix, onToggleExpand, onSelect, onDragOverZone, onDrop, isDragging = false, onStartDrag, onDragEnd, onLazyLoad, onDoubleClick, onContextMenu, enableDragAndDrop = false, isValidDropZone, scrollToSelected = false, enableKeyboardSearch = false, searchClearDelay = 1500, disableAutoSelectOnExpand = false, multiDragNoun = "items", enableKeyboardNavigation = true, onKeyboardNavigationChange, }) => {
    const [searchTerm, setSearchTerm] = useState("");
    const [isFocused, setIsFocused] = useState(false);
    const [keyboardNavPosition, setKeyboardNavPosition] = useState(null);
    const searchTimeoutRef = useRef(null);
    // When toggling a node, notify parent and trigger external lazy load if needed.
    const handleToggle = useCallback((node) => {
        const isCurrentlyExpanded = expandedKeys.includes(node.key);
        // If the node is being expanded (not collapsed) and there's an active search filter
        if (!isCurrentlyExpanded) {
            // Only select the node for quick navigation if there's an active search filter
            // and auto-selection is not disabled
            if (searchTerm && onSelect && !disableAutoSelectOnExpand) {
                onSelect(node.key, {});
            }
            // Clear the search filter
            setSearchTerm("");
            // Clear any pending search timeout
            if (searchTimeoutRef.current) {
                clearTimeout(searchTimeoutRef.current);
                searchTimeoutRef.current = null;
            }
        }
        if (onToggleExpand) {
            onToggleExpand(node.key);
        }
        // If the node is expandable and its children haven't been loaded,
        // call onLazyLoad (external async loading should update the node to `null` while loading)
        if (node.hasChildren && node.children === undefined && onLazyLoad) {
            onLazyLoad(node);
        }
    }, [
        onToggleExpand,
        onLazyLoad,
        expandedKeys,
        onSelect,
        searchTerm,
        disableAutoSelectOnExpand,
    ]);
    const handleSelect = useCallback((nodeKey, event) => {
        if (onSelect) {
            onSelect(nodeKey, event);
        }
    }, [onSelect]);
    // Global drag end handler.
    const isDraggingRef = React.useRef(false);
    const dragTimeoutRef = React.useRef(undefined);
    useEffect(() => {
        const handleGlobalDragEnd = (event) => {
            if (isDraggingRef.current && onDragEnd) {
                onDragEnd(event);
                isDraggingRef.current = false;
            }
            // Clear timeout when drag ends properly
            if (dragTimeoutRef.current !== undefined) {
                clearTimeout(dragTimeoutRef.current);
                dragTimeoutRef.current = undefined;
            }
        };
        const handleEscapeKey = (event) => {
            if (event.key === "Escape" && isDraggingRef.current && onDragEnd) {
                onDragEnd(null);
                isDraggingRef.current = false;
                if (dragTimeoutRef.current !== undefined) {
                    clearTimeout(dragTimeoutRef.current);
                    dragTimeoutRef.current = undefined;
                }
            }
        };
        const handleMouseLeave = (event) => {
            // Reset drag state if mouse leaves the window during a drag
            if (isDraggingRef.current && onDragEnd) {
                onDragEnd(null);
                isDraggingRef.current = false;
                if (dragTimeoutRef.current !== undefined) {
                    clearTimeout(dragTimeoutRef.current);
                    dragTimeoutRef.current = undefined;
                }
            }
        };
        document.addEventListener("dragend", handleGlobalDragEnd);
        document.addEventListener("keydown", handleEscapeKey);
        window.addEventListener("mouseleave", handleMouseLeave);
        return () => {
            document.removeEventListener("dragend", handleGlobalDragEnd);
            document.removeEventListener("keydown", handleEscapeKey);
            window.removeEventListener("mouseleave", handleMouseLeave);
        };
    }, [onDragEnd]);
    // Scroll to selected node when scrollToSelected is enabled and selectedKeys change
    const treeRef = useRef(null);
    // Track pending scroll request with a ref so it persists across effect cleanup
    const pendingScrollRef = useRef(null);
    // Helper function to perform the scroll with retry logic for when node isn't in DOM yet
    const performScroll = useCallback((keysToScroll, maxRetries = 5, retryDelay = 100) => {
        const attemptScroll = (retryCount) => {
            const treeContainer = treeRef.current;
            if (!treeContainer) {
                // Tree not mounted yet, retry
                if (retryCount < maxRetries) {
                    pendingScrollRef.current = {
                        keys: keysToScroll,
                        timeoutId: setTimeout(() => attemptScroll(retryCount + 1), retryDelay),
                        retryCount: retryCount + 1,
                    };
                }
                return;
            }
            // Try multiple selection strategies
            let selectedNode = null;
            const nodeKey = keysToScroll[0];
            // Strategy 1: Use data-node-key attribute (most reliable)
            if (nodeKey) {
                try {
                    const escapedKey = CSS.escape(nodeKey);
                    selectedNode = treeContainer.querySelector(`[data-node-key="${escapedKey}"]`);
                }
                catch (e) {
                    // Fallback without escape
                    selectedNode = treeContainer.querySelector(`[data-node-key="${nodeKey}"]`);
                }
            }
            // Strategy 2: Use data-selected attribute
            if (!selectedNode) {
                selectedNode = treeContainer.querySelector('[data-selected="true"]');
            }
            // Strategy 3: Use bg-blue-100 class
            if (!selectedNode) {
                selectedNode = treeContainer.querySelector(".bg-blue-100");
            }
            if (!selectedNode) {
                // Node not in DOM yet (tree may still be expanding), retry
                if (retryCount < maxRetries) {
                    pendingScrollRef.current = {
                        keys: keysToScroll,
                        timeoutId: setTimeout(() => attemptScroll(retryCount + 1), retryDelay),
                        retryCount: retryCount + 1,
                    };
                }
                return;
            }
            // Clear pending scroll since we found the node
            pendingScrollRef.current = null;
            // Find the scrollable parent container
            let scrollContainer = selectedNode.closest(".overflow-auto");
            if (!scrollContainer) {
                scrollContainer = selectedNode.closest('[style*="overflow"]');
            }
            if (!scrollContainer) {
                scrollContainer = treeContainer.parentElement;
            }
            if (scrollContainer) {
                const containerRect = scrollContainer.getBoundingClientRect();
                const nodeRect = selectedNode.getBoundingClientRect();
                // Check if the node is fully visible within the scroll container
                // NO padding - if ANY part of the node is visible, don't scroll
                const isFullyVisible = nodeRect.top >= containerRect.top &&
                    nodeRect.bottom <= containerRect.bottom;
                // Only scroll if the node is NOT fully visible
                if (!isFullyVisible) {
                    const scrollTop = scrollContainer.scrollTop;
                    const containerTop = containerRect.top;
                    const nodeTop = nodeRect.top;
                    const offset = nodeTop - containerTop;
                    const newScrollTop = scrollTop +
                        offset -
                        containerRect.height / 2 +
                        nodeRect.height / 2;
                    scrollContainer.scrollTo({
                        top: newScrollTop,
                        behavior: "smooth",
                    });
                }
            }
            // Removed fallback scrollIntoView - only scroll if we have a proper container
            // and the item is not visible
        };
        // Cancel any pending scroll first
        if (pendingScrollRef.current?.timeoutId) {
            clearTimeout(pendingScrollRef.current.timeoutId);
        }
        // Start the scroll attempt with a small initial delay to let React render
        pendingScrollRef.current = {
            keys: keysToScroll,
            timeoutId: setTimeout(() => attemptScroll(0), 50),
            retryCount: 0,
        };
    }, []);
    // Track the last scroll request to avoid duplicate scrolls
    const lastScrollRequestRef = useRef(null);
    useEffect(() => {
        if (scrollToSelected && selectedKeys.length > 0) {
            // Create a unique key for this scroll request
            const scrollRequestKey = selectedKeys.join(",");
            // Only scroll if this is a new request (avoid re-scrolling on unrelated re-renders)
            if (scrollRequestKey !== lastScrollRequestRef.current) {
                lastScrollRequestRef.current = scrollRequestKey;
                performScroll(selectedKeys);
            }
        }
        // Cleanup: cancel pending scroll on unmount only (not on deps change)
        // We intentionally don't cancel when scrollToSelected becomes false
        // to allow the scroll to complete even if the flag is reset quickly
    }, [scrollToSelected, selectedKeys, performScroll]);
    // Cleanup pending scroll on unmount
    useEffect(() => {
        return () => {
            if (pendingScrollRef.current?.timeoutId) {
                clearTimeout(pendingScrollRef.current.timeoutId);
                pendingScrollRef.current = null;
            }
        };
    }, []);
    // Enhanced renderNode function that handles highlighting
    const enhancedRenderNode = useCallback((node, searchTermForNode = searchTerm) => {
        // Get the original content from renderNode
        const originalContent = renderNode(node, searchTermForNode);
        // If there's no search term, return original content
        if (!searchTermForNode?.trim()) {
            return originalContent;
        }
        // Auto-highlight if the content is simple text
        if (typeof originalContent === "string") {
            return highlightText(originalContent, searchTermForNode);
        }
        // For React elements, try to find and highlight text content
        if (React.isValidElement(originalContent)) {
            return highlightReactElement(originalContent, searchTermForNode, node.label);
        }
        // Fallback: if we can't process the content, return original
        return originalContent;
    }, [renderNode, searchTerm]);
    // Filter nodes based on search term
    const filteredNodes = useMemo(() => {
        return filterTreeNodes(nodes, searchTerm, expandedKeys);
    }, [nodes, searchTerm, expandedKeys]);
    // Wrapper for onStartDrag that has access to the refs
    const handleStartDragWithTimeout = useCallback((data) => {
        // Mark dragging as active
        isDraggingRef.current = true;
        // Failsafe timeout to recover if the browser fails to emit dragend
        dragTimeoutRef.current = setTimeout(() => {
            if (isDraggingRef.current && onDragEnd) {
                onDragEnd(null);
                isDraggingRef.current = false;
            }
        }, 30000); // 30s safety timeout
        // Delegate to consumer
        if (onStartDrag) {
            onStartDrag(data);
        }
    }, [onStartDrag, onDragEnd]);
    const handleDragEnd = useCallback((event) => {
        isDraggingRef.current = false;
        // Clear timeout when drag ends normally
        if (dragTimeoutRef.current !== undefined) {
            clearTimeout(dragTimeoutRef.current);
            dragTimeoutRef.current = undefined;
        }
        if (onDragEnd) {
            onDragEnd(event);
        }
    }, [onDragEnd]);
    // Recursive function to render tree nodes along with drop zones.
    const renderTreeList = useCallback((nodes, depth, parent = null) => {
        return (_jsxs("div", { className: "tree-container flex flex-col", children: [nodes.map((node, index) => {
                    const children = node.children;
                    const isExpanded = expandedKeys.includes(node.key);
                    const isSelected = selectedKeys.includes(node.key);
                    return (_jsxs(React.Fragment, { children: [_jsx(DropZone, { parent: parent, index: index, isDragging: isDragging, onDragOverZone: onDragOverZone, onDrop: onDrop, onDragEnd: onDragEnd, isValidDropZone: isValidDropZone }), _jsxs("div", { style: {
                                    marginLeft: depth > 0 ? "24px" : undefined,
                                }, className: "flex flex-col", children: [_jsx(NodeContent, { node: node, isExpanded: isExpanded, isSelected: isSelected, onSelect: handleSelect, onToggleNode: handleToggle, onStartDrag: handleStartDragWithTimeout, onDragEnd: handleDragEnd, onDragOverZone: onDragOverZone, onDrop: onDrop, onDoubleClick: onDoubleClick, onContextMenu: onContextMenu, renderNode: enhancedRenderNode, renderNodePrefix: renderNodePrefix, depth: depth, enableDragAndDrop: enableDragAndDrop, selectedKeys: selectedKeys, isDragging: isDragging, searchTerm: searchTerm, multiDragNoun: multiDragNoun }), isExpanded && (_jsx(_Fragment, { children: children && children.length > 0 ? (_jsx("div", { children: renderTreeList(children, depth + 1, node) })) : null }))] })] }, node.key));
                }), _jsx(DropZone, { parent: parent, index: nodes.length, isDragging: isDragging, onDragOverZone: onDragOverZone, onDrop: onDrop, onDragEnd: onDragEnd, isLast: true, isValidDropZone: isValidDropZone })] }));
    }, [
        expandedKeys,
        selectedKeys,
        isDragging,
        onDragOverZone,
        onDrop,
        onDragEnd,
        handleStartDragWithTimeout,
        onDoubleClick,
        onContextMenu,
        handleSelect,
        handleToggle,
        enhancedRenderNode,
        searchTerm,
        enableDragAndDrop,
        multiDragNoun,
        isValidDropZone,
    ]);
    // Memoize the tree structure
    const treeContent = useMemo(() => renderTreeList(filteredNodes, 0), [filteredNodes, renderTreeList]);
    // Initialize keyboard navigation position when tree gains focus
    useEffect(() => {
        if (isFocused && !keyboardNavPosition) {
            // Set initial position to first selected node, or first node
            if (selectedKeys.length > 0 && selectedKeys[0]) {
                setKeyboardNavPosition(selectedKeys[0]);
            }
            else if (filteredNodes.length > 0 && filteredNodes[0]) {
                setKeyboardNavPosition(filteredNodes[0].key);
            }
        }
    }, [isFocused, keyboardNavPosition, selectedKeys, filteredNodes]);
    // Update keyboard navigation position when selection changes externally (e.g., via click)
    useEffect(() => {
        if (isFocused && selectedKeys.length > 0 && selectedKeys[0]) {
            // Only update if the current nav position is not in the selected keys
            // This preserves nav position during shift-select
            if (!keyboardNavPosition || !selectedKeys.includes(keyboardNavPosition)) {
                setKeyboardNavPosition(selectedKeys[0]);
            }
        }
    }, [selectedKeys, isFocused, keyboardNavPosition]);
    // Helper function to scroll a node into view
    const scrollNodeIntoView = useCallback((nodeKey) => {
        if (!treeRef.current)
            return;
        setTimeout(() => {
            const treeContainer = treeRef.current;
            if (!treeContainer)
                return;
            const targetNode = treeContainer.querySelector(`[data-node-key="${CSS.escape(nodeKey)}"]`);
            if (targetNode) {
                let scrollContainer = targetNode.closest(".overflow-auto");
                if (!scrollContainer) {
                    scrollContainer = targetNode.closest('[style*="overflow"]');
                }
                if (!scrollContainer) {
                    scrollContainer = treeContainer.parentElement;
                }
                if (scrollContainer) {
                    const containerRect = scrollContainer.getBoundingClientRect();
                    const nodeRect = targetNode.getBoundingClientRect();
                    const isVisible = nodeRect.top >= containerRect.top &&
                        nodeRect.bottom <= containerRect.bottom;
                    if (!isVisible) {
                        const scrollTop = scrollContainer.scrollTop;
                        const containerTop = containerRect.top;
                        const nodeTop = nodeRect.top;
                        const offset = nodeTop - containerTop;
                        const newScrollTop = scrollTop +
                            offset -
                            containerRect.height / 2 +
                            nodeRect.height / 2;
                        scrollContainer.scrollTo({
                            top: newScrollTop,
                            behavior: "smooth",
                        });
                    }
                }
                else {
                    targetNode.scrollIntoView({
                        behavior: "smooth",
                        block: "nearest",
                    });
                }
            }
        }, 0);
    }, []);
    // Keyboard navigation functionality
    useEffect(() => {
        if (!enableKeyboardNavigation || !isFocused)
            return;
        const handleKeyDown = (event) => {
            // Ignore if user is typing in an input, textarea, or contenteditable element
            const target = event.target;
            if (target.tagName === "INPUT" ||
                target.tagName === "TEXTAREA" ||
                target.contentEditable === "true" ||
                target.isContentEditable) {
                return;
            }
            // Get flattened list of visible nodes
            const visibleNodes = flattenVisibleNodes(filteredNodes, expandedKeys);
            if (visibleNodes.length === 0)
                return;
            // Use keyboard navigation position as the current position
            const currentIndex = keyboardNavPosition
                ? visibleNodes.findIndex((n) => n.node.key === keyboardNavPosition)
                : -1;
            let newSelectedKey = null;
            let shouldUseShift = false;
            switch (event.key) {
                case "ArrowDown": {
                    event.preventDefault();
                    shouldUseShift = event.shiftKey;
                    if (currentIndex < visibleNodes.length - 1) {
                        const nextNode = visibleNodes[currentIndex + 1];
                        if (nextNode) {
                            newSelectedKey = nextNode.node.key;
                        }
                    }
                    else if (currentIndex === -1 &&
                        visibleNodes.length > 0 &&
                        visibleNodes[0]) {
                        // No selection, select first node
                        newSelectedKey = visibleNodes[0].node.key;
                    }
                    break;
                }
                case "ArrowUp": {
                    event.preventDefault();
                    shouldUseShift = event.shiftKey;
                    if (currentIndex > 0) {
                        const prevNode = visibleNodes[currentIndex - 1];
                        if (prevNode) {
                            newSelectedKey = prevNode.node.key;
                        }
                    }
                    else if (currentIndex === -1 &&
                        visibleNodes.length > 0 &&
                        visibleNodes[0]) {
                        // No selection, select first node
                        newSelectedKey = visibleNodes[0].node.key;
                    }
                    break;
                }
                case "ArrowRight": {
                    event.preventDefault();
                    const currentNode = currentIndex >= 0 ? visibleNodes[currentIndex] : undefined;
                    if (currentNode) {
                        const node = currentNode.node;
                        const isExpanded = expandedKeys.includes(node.key);
                        if (node.hasChildren || node.children?.length) {
                            if (!isExpanded) {
                                // Expand the node
                                if (onToggleExpand) {
                                    onToggleExpand(node.key);
                                }
                                if (node.hasChildren &&
                                    node.children === undefined &&
                                    onLazyLoad) {
                                    onLazyLoad(node);
                                }
                            }
                            else if (node.children &&
                                node.children.length > 0 &&
                                node.children[0]) {
                                // Move to first child
                                newSelectedKey = node.children[0].key;
                            }
                        }
                    }
                    break;
                }
                case "ArrowLeft": {
                    event.preventDefault();
                    const currentNode = currentIndex >= 0 ? visibleNodes[currentIndex] : undefined;
                    if (currentNode) {
                        const node = currentNode.node;
                        const isExpanded = expandedKeys.includes(node.key);
                        if (isExpanded && (node.hasChildren || node.children?.length)) {
                            // Collapse the node
                            if (onToggleExpand) {
                                onToggleExpand(node.key);
                            }
                        }
                        else if (currentNode.parent) {
                            // Move to parent node
                            newSelectedKey = currentNode.parent.key;
                        }
                    }
                    break;
                }
                case "Enter": {
                    event.preventDefault();
                    if (keyboardNavPosition) {
                        // Toggle expand/collapse if it has children
                        const currentNode = currentIndex >= 0 ? visibleNodes[currentIndex] : undefined;
                        if (currentNode) {
                            const node = currentNode.node;
                            if (node.hasChildren || node.children?.length) {
                                if (onToggleExpand) {
                                    onToggleExpand(node.key);
                                }
                                if (node.hasChildren &&
                                    node.children === undefined &&
                                    onLazyLoad) {
                                    onLazyLoad(node);
                                }
                            }
                        }
                    }
                    break;
                }
                case "Home": {
                    event.preventDefault();
                    if (visibleNodes.length > 0) {
                        const firstNode = visibleNodes[0];
                        if (firstNode) {
                            newSelectedKey = firstNode.node.key;
                        }
                    }
                    break;
                }
                case "End": {
                    event.preventDefault();
                    if (visibleNodes.length > 0) {
                        const lastNode = visibleNodes[visibleNodes.length - 1];
                        if (lastNode) {
                            newSelectedKey = lastNode.node.key;
                        }
                    }
                    break;
                }
            }
            if (newSelectedKey && newSelectedKey !== keyboardNavPosition) {
                // Update keyboard navigation position
                setKeyboardNavPosition(newSelectedKey);
                if (onSelect) {
                    // Create a synthetic mouse event with shiftKey set for range selection
                    const syntheticEvent = {
                        shiftKey: shouldUseShift,
                        ctrlKey: false,
                        metaKey: false,
                        button: 0,
                    };
                    onSelect(newSelectedKey, syntheticEvent);
                }
                scrollNodeIntoView(newSelectedKey);
                if (onKeyboardNavigationChange) {
                    onKeyboardNavigationChange(newSelectedKey);
                }
            }
        };
        document.addEventListener("keydown", handleKeyDown);
        return () => {
            document.removeEventListener("keydown", handleKeyDown);
        };
    }, [
        enableKeyboardNavigation,
        isFocused,
        keyboardNavPosition,
        filteredNodes,
        expandedKeys,
        onToggleExpand,
        onSelect,
        onLazyLoad,
        scrollNodeIntoView,
        onKeyboardNavigationChange,
    ]);
    // Keyboard search functionality
    useEffect(() => {
        if (!enableKeyboardSearch || !isFocused)
            return;
        const handleKeyDown = (event) => {
            // Ignore if user is typing in an input, textarea, or contenteditable element
            const target = event.target;
            if (target.tagName === "INPUT" ||
                target.tagName === "TEXTAREA" ||
                target.contentEditable === "true" ||
                target.isContentEditable) {
                return;
            }
            // Ignore navigation keys for search
            if ([
                "ArrowUp",
                "ArrowDown",
                "ArrowLeft",
                "ArrowRight",
                "Enter",
                "Home",
                "End",
            ].includes(event.key)) {
                return;
            }
            // Handle different key types
            if (event.key === "Escape") {
                // Clear search on Escape
                setSearchTerm("");
                if (searchTimeoutRef.current) {
                    clearTimeout(searchTimeoutRef.current);
                    searchTimeoutRef.current = null;
                }
                event.preventDefault();
            }
            else if (event.key === "Backspace") {
                // Remove last character on Backspace
                if (searchTerm.length > 0) {
                    setSearchTerm((prev) => prev.slice(0, -1));
                    resetSearchTimeout();
                    event.preventDefault();
                }
            }
            else if (event.key.length === 1 &&
                !event.ctrlKey &&
                !event.metaKey &&
                !event.altKey) {
                // Add character for printable keys (letters, numbers, symbols)
                setSearchTerm((prev) => prev + event.key);
                resetSearchTimeout();
                event.preventDefault();
            }
        };
        const resetSearchTimeout = () => {
            if (searchTimeoutRef.current) {
                clearTimeout(searchTimeoutRef.current);
            }
            searchTimeoutRef.current = setTimeout(() => {
                setSearchTerm("");
                searchTimeoutRef.current = null;
            }, searchClearDelay);
        };
        // Add event listener to document
        document.addEventListener("keydown", handleKeyDown);
        return () => {
            document.removeEventListener("keydown", handleKeyDown);
            if (searchTimeoutRef.current) {
                clearTimeout(searchTimeoutRef.current);
            }
        };
    }, [enableKeyboardSearch, isFocused, searchTerm, searchClearDelay]);
    return (_jsxs("div", { ref: treeRef, className: "perfect-tree focus:outline-none", tabIndex: 0, onFocus: () => setIsFocused(true), onBlur: () => {
            setIsFocused(false);
            setSearchTerm("");
            setKeyboardNavPosition(null);
            if (searchTimeoutRef.current) {
                clearTimeout(searchTimeoutRef.current);
                searchTimeoutRef.current = null;
            }
        }, children: [enableKeyboardSearch && searchTerm && (_jsxs("div", { className: "mb-2 flex items-center px-2 py-1 text-xs", children: [_jsx("span", { className: "text-gray-2", children: "Filter:" }), _jsx("span", { className: "ml-1", children: searchTerm }), _jsx("span", { className: "text-gray-2 ml-1", children: "(ESC to clear)" })] })), treeContent] }));
};
// Custom comparison function for memo to prevent unnecessary re-renders
// when callback props change but actual data hasn't
const arePropsEqual = (prevProps, nextProps) => {
    // Compare primitive and array props
    if (prevProps.nodes !== nextProps.nodes ||
        prevProps.isDragging !== nextProps.isDragging ||
        prevProps.enableDragAndDrop !== nextProps.enableDragAndDrop ||
        prevProps.scrollToSelected !== nextProps.scrollToSelected ||
        prevProps.enableKeyboardSearch !== nextProps.enableKeyboardSearch ||
        prevProps.searchClearDelay !== nextProps.searchClearDelay ||
        prevProps.disableAutoSelectOnExpand !==
            nextProps.disableAutoSelectOnExpand ||
        prevProps.multiDragNoun !== nextProps.multiDragNoun ||
        prevProps.enableKeyboardNavigation !== nextProps.enableKeyboardNavigation) {
        return false;
    }
    // Compare arrays with shallow equality
    if (prevProps.selectedKeys?.length !== nextProps.selectedKeys?.length ||
        prevProps.selectedKeys?.some((key, i) => key !== nextProps.selectedKeys?.[i])) {
        return false;
    }
    if (prevProps.expandedKeys?.length !== nextProps.expandedKeys?.length ||
        prevProps.expandedKeys?.some((key, i) => key !== nextProps.expandedKeys?.[i])) {
        return false;
    }
    // For callback props, we assume they are stable if memoized in parent
    // If they change on every render, the parent should memoize them with useCallback
    return true;
};
export default memo(PerfectTree, arePropsEqual);
//# sourceMappingURL=PerfectTree.js.map