/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// #region Imports
import { animatePosition, animateToOriginalPosition, cancelAnimations } from '../Utilities/AnimationUtils';
import { addPoints, applyTransform, clearTransform, constrainPointToBoundary, constrainToAxis, getDelta, getDistance, getPointerPosition, getRectTopLeft, subtractPoints } from '../Utilities/PositionUtils';
import { getDragDropRegistry } from './DragDropRegistry';
import { DragStyleContainer } from './DragStyleContainer';
// #endregion
// #region DragRef
/**
 * Reference to a draggable element.
 * Core class for drag functionality - framework agnostic.
 *
 * @public
 */
export class DragRef {
    // #region Fields
    _element;
    _rootElement;
    _config;
    _state;
    _registry = getDragDropRegistry();
    _dropContainer;
    _boundaryElement;
    _listeners;
    _dragStartTimer;
    _data;
    _handles;
    _pointerDownListener;
    _nextDropListRef;
    _parentDragRef;
    _styleContainer;
    // #endregion
    // #region Ctor
    /**
     * @param element - Element to make draggable.
     * @param config - Configuration options.
     *
     * @public
     */
    constructor(element, config = {}) {
        this._element = element;
        this._rootElement = config.rootElement ?? element;
        this._config = this.applyConfigDefaults(config);
        this._dropContainer = null;
        this._boundaryElement = null;
        this._listeners = new Map();
        this._dragStartTimer = null;
        this._handles = [];
        this._pointerDownListener = null;
        this._nextDropListRef = null;
        this._parentDragRef = null;
        this._styleContainer = null;
        this._state = {
            isDragging: false,
            pointerPosition: {
                x: 0,
                y: 0
            },
            pickupOffset: {
                x: 0,
                y: 0
            },
            initialPosition: {
                x: 0,
                y: 0
            },
            currentPosition: {
                x: 0,
                y: 0
            },
            initialRect: new DOMRect(),
            currentRect: new DOMRect(),
            pointerDirection: {
                x: 0,
                y: 0
            },
            previousPointerPosition: {
                x: 0,
                y: 0
            },
            previewElement: null,
            placeholderElement: null,
            previewContainer: null,
            activeDropList: null,
            pointerId: null,
            isKeyboardDrag: false,
            savedStyles: null
        };
        this._registry.registerDragItem(this);
        this.attachPointerListeners();
        // Initialize boundary element from config
        if (this._config.boundaryElement) {
            this.withBoundaryElement(this._config.boundaryElement);
        }
    }
    // #endregion
    // #region Properties
    /**
     * The native element.
     *
     * @public
     */
    get element() {
        return this._element;
    }
    /**
     * Whether dragging is currently active.
     *
     * @public
     */
    get isDragging() {
        return this._state.isDragging;
    }
    /**
     * Whether dragging is disabled.
     *
     * @public
     */
    get disabled() {
        return this._config.disabled;
    }
    set disabled(value) {
        if (this._config.disabled !== value) {
            this._config.disabled = value;
            if (value && this._state.isDragging) {
                this.endDragSequence(false);
            }
        }
    }
    /**
     * Lock axis.
     *
     * @public
     */
    get lockAxis() {
        return this._config.lockAxis;
    }
    set lockAxis(value) {
        this._config.lockAxis = value;
    }
    /**
     * Arbitrary data attached to this drag item.
     *
     * @public
     */
    get data() {
        return this._data;
    }
    set data(value) {
        this._data = value;
    }
    /**
     * Drop container this item belongs to.
     *
     * @internal
     */
    get dropContainer() {
        return this._dropContainer;
    }
    /**
     * Scale factor.
     *
     * @public
     */
    get scale() {
        return this._config.scale;
    }
    set scale(value) {
        this._config.scale = value;
    }
    // #endregion
    // #region Methods
    /**
     * Reset drag item to its original position.
     *
     * @public
     */
    reset() {
        if (!this._state.isDragging) {
            this._state.currentPosition = {
                x: 0,
                y: 0
            };
            applyTransform(this._rootElement, this._state.currentPosition);
        }
    }
    /**
     * Reset drag item to boundary edge if outside boundary.
     *
     * @public
     */
    resetToBoundary() {
        if (this._boundaryElement && !this._state.isDragging) {
            const boundaries = this._boundaryElement.getBoundingClientRect();
            const elementRect = this._rootElement.getBoundingClientRect();
            const constrained = constrainPointToBoundary(this._state.currentPosition, boundaries, {
                width: elementRect.width,
                height: elementRect.height
            });
            this._state.currentPosition = constrained;
            applyTransform(this._rootElement, this._state.currentPosition);
        }
    }
    /**
     * Get free drag position (for standalone dragging).
     *
     * @returns Current position.
     *
     * @public
     */
    getFreeDragPosition() {
        return { ...this._state.currentPosition };
    }
    /**
     * Set free drag position programmatically.
     *
     * @param value - Position to set.
     *
     * @public
     */
    setFreeDragPosition(value) {
        this._state.currentPosition = { ...value };
        this._state.initialPosition = { ...value };
        applyTransform(this._rootElement, this._state.currentPosition);
    }
    /**
     * Get placeholder element.
     *
     * @returns Placeholder element or null.
     *
     * @public
     */
    getPlaceholderElement() {
        return this._state.placeholderElement;
    }
    /**
     * Get root element.
     *
     * @returns Root element.
     *
     * @public
     */
    getRootElement() {
        return this._rootElement;
    }
    /**
     * Get visible element (returns preview during drag, root element otherwise).
     *
     * @returns Currently visible element.
     *
     * @public
     */
    getVisibleElement() {
        return this._state.isDragging && this._state.previewElement
            ? this._state.previewElement
            : this._rootElement;
    }
    /**
     * Add a drag handle.
     *
     * @param handle - Handle element.
     *
     * @public
     */
    withHandles(handles) {
        this._handles = handles;
        this.reattachPointerListeners();
        return this;
    }
    /**
     * Update root element.
     *
     * @param element - New root element.
     *
     * @public
     */
    withRootElement(element) {
        if (element !== this._rootElement) {
            this._rootElement = element;
        }
        return this;
    }
    /**
     * Update boundary element.
     *
     * @param boundary - Boundary element or selector.
     *
     * @public
     */
    withBoundaryElement(boundary) {
        if (typeof boundary === 'string') {
            this._boundaryElement =
                document.querySelector(boundary) ??
                    this._element.closest(boundary);
        }
        else {
            this._boundaryElement = boundary;
        }
        return this;
    }
    /**
     * Set preview container element.
     *
     * @param container - Container for preview element.
     *
     * @public
     */
    withPreviewContainer(container) {
        this._state.previewContainer = container;
        return this;
    }
    /**
     * Disable a specific drag handle.
     *
     * @param handle - Handle element to disable.
     *
     * @public
     */
    disableHandle(handle) {
        const index = this._handles.indexOf(handle);
        if (index > -1 && !handle.hasAttribute('data-drag-disabled')) {
            handle.setAttribute('data-drag-disabled', 'true');
            handle.style.cursor = 'default';
        }
    }
    /**
     * Enable a specific drag handle.
     *
     * @param handle - Handle element to enable.
     *
     * @public
     */
    enableHandle(handle) {
        if (handle.hasAttribute('data-drag-disabled')) {
            handle.removeAttribute('data-drag-disabled');
            handle.style.cursor = '';
        }
    }
    /**
     * Set layout direction (ltr/rtl).
     *
     * @param direction - Layout direction.
     *
     * @public
     */
    withDirection(direction) {
        this._config.direction = direction;
        return this;
    }
    /**
     * Set parent DragRef (for nested draggables).
     *
     * @param parent - Parent DragRef.
     *
     * @public
     */
    withParent(parent) {
        this._parentDragRef = parent;
        return this;
    }
    /**
     * Set custom preview template.
     *
     * @param template - Preview template element or factory function.
     *
     * @public
     */
    withPreviewTemplate(template) {
        if (template !== null) {
            this._config.previewTemplate = template;
        }
        return this;
    }
    /**
     * Set custom placeholder template.
     *
     * @param template - Placeholder template element or factory function.
     *
     * @public
     */
    withPlaceholderTemplate(template) {
        if (template !== null) {
            this._config.placeholderTemplate = template;
        }
        return this;
    }
    /**
     * Associate this drag item with a drop container.
     *
     * @param container - Drop container.
     *
     * @internal
     */
    withDropContainer(container) {
        this._dropContainer = container;
        return this;
    }
    /**
     * Dispose of this drag ref.
     *
     * @public
     */
    dispose() {
        if (this._state.isDragging) {
            this.endDragSequence(false);
        }
        this.detachPointerListeners();
        this._registry.removeDragItem(this);
        this._listeners.clear();
    }
    // #endregion
    // #region Private Methods - Pointer Event Handling
    /**
     * Attach pointer event listeners.
     *
     * @private
     */
    attachPointerListeners() {
        this.detachPointerListeners();
        const elements = this._handles.length > 0 ? this._handles : [this._rootElement];
        this._pointerDownListener = this.onPointerDown.bind(this);
        elements.forEach((el) => {
            el.addEventListener('pointerdown', this._pointerDownListener);
        });
    }
    /**
     * Detach pointer event listeners.
     *
     * @private
     */
    detachPointerListeners() {
        if (this._pointerDownListener) {
            const elements = this._handles.length > 0 ? this._handles : [this._rootElement];
            elements.forEach((el) => {
                el.removeEventListener('pointerdown', this._pointerDownListener);
            });
            this._pointerDownListener = null;
        }
    }
    /**
     * Reattach pointer listeners (after handle changes).
     *
     * @private
     */
    reattachPointerListeners() {
        this.attachPointerListeners();
    }
    /**
     * Handle pointerdown event.
     *
     * @param event - Pointer event.
     *
     * @private
     */
    onPointerDown(event) {
        if (this._config.disabled || this._state.isDragging) {
            return;
        }
        // Only handle primary button (left click / first touch)
        if (event.button !== 0) {
            return;
        }
        // Check if pointer is on a disabled handle
        if (this._handles.length > 0) {
            const target = event.target;
            let isOnHandle = false;
            let isOnDisabledHandle = false;
            for (const handle of this._handles) {
                if (handle.contains(target)) {
                    isOnHandle = true;
                    if (handle.hasAttribute('data-drag-disabled')) {
                        isOnDisabledHandle = true;
                    }
                    break;
                }
            }
            // If we have handles, must be on a handle to drag
            if (!isOnHandle) {
                return;
            }
            // If on disabled handle, don't allow drag
            if (isOnDisabledHandle) {
                return;
            }
        }
        const delay = this.getStartDelay(event);
        const distance = this.getStartDistance(event);
        if (distance > 0) {
            // Distance-based drag start: wait until pointer moves far enough
            const startPosition = getPointerPosition(event);
            const cleanup = () => {
                document.removeEventListener('pointermove', onMove);
                document.removeEventListener('pointerup', onUp);
                if (this._dragStartTimer) {
                    clearTimeout(this._dragStartTimer);
                    this._dragStartTimer = null;
                }
            };
            const onUp = () => {
                cleanup();
            };
            const onMove = (moveEvent) => {
                if (getDistance(startPosition, getPointerPosition(moveEvent)) >= distance) {
                    cleanup();
                    this.initializeDragSequence(moveEvent);
                }
            };
            document.addEventListener('pointermove', onMove);
            document.addEventListener('pointerup', onUp, { once: true });
            // If both delay and distance are set, apply a timeout as upper bound
            if (delay > 0) {
                this._dragStartTimer = window.setTimeout(() => {
                    cleanup();
                }, delay);
            }
        }
        else if (delay > 0) {
            this._dragStartTimer = window.setTimeout(() => {
                this.initializeDragSequence(event);
            }, delay);
            // Attach temporary listeners to cancel timer
            const cancelTimer = () => {
                if (this._dragStartTimer) {
                    clearTimeout(this._dragStartTimer);
                    this._dragStartTimer = null;
                }
                document.removeEventListener('pointerup', cancelTimer);
                document.removeEventListener('pointermove', cancelOnMove);
            };
            const cancelOnMove = (moveEvent) => {
                const threshold = 5;
                if (getDistance(getPointerPosition(event), getPointerPosition(moveEvent)) > threshold) {
                    cancelTimer();
                }
            };
            document.addEventListener('pointerup', cancelTimer, { once: true });
            document.addEventListener('pointermove', cancelOnMove);
        }
        else {
            this.initializeDragSequence(event);
        }
    }
    /**
     * Initialize drag sequence.
     *
     * @param event - Trigger event.
     *
     * @private
     */
    initializeDragSequence(event) {
        const pointerPosition = getPointerPosition(event);
        // Save original inline styles that will be overwritten during drag
        this._state.savedStyles = {
            position: this._rootElement.style.position,
            top: this._rootElement.style.top,
            left: this._rootElement.style.left,
            visibility: this._rootElement.style.visibility,
            transform: this._rootElement.style.transform,
            width: this._rootElement.style.width,
            height: this._rootElement.style.height,
            zIndex: this._rootElement.style.zIndex,
            margin: this._rootElement.style.margin,
            boxSizing: this._rootElement.style.boxSizing,
            pointerEvents: this._rootElement.style.pointerEvents
        };
        // Store state
        this._state.pointerId = event.pointerId;
        this._state.pointerPosition = pointerPosition;
        this._state.previousPointerPosition = pointerPosition;
        this._state.initialRect = this._rootElement.getBoundingClientRect();
        this._state.currentRect = this._state.initialRect;
        this._state.pickupOffset = {
            x: pointerPosition.x - this._state.initialRect.left,
            y: pointerPosition.y - this._state.initialRect.top
        };
        // Emit start event (cancelable)
        const startEvent = this.createDragStartEvent(event);
        const dispatched = this._element.dispatchEvent(startEvent);
        if (!dispatched) {
            // Event was cancelled
            return;
        }
        // Begin drag
        this.startDragSequence(event);
    }
    /**
     * Start drag sequence (after start event).
     *
     * @param event - Trigger event.
     *
     * @private
     */
    startDragSequence(_event) {
        this._state.isDragging = true;
        this._registry.startDragging(this);
        // Set pointer capture
        try {
            this._rootElement.setPointerCapture(this._state.pointerId);
        }
        catch {
            // Ignore errors (might not be supported)
        }
        // Create preview and placeholder
        this.createPreviewElement();
        this.createPlaceholderElement();
        // Setup drag listeners
        this.attachDragListeners();
        // Notify drop container and set as active
        if (this._dropContainer) {
            this._dropContainer._startDragging?.(this);
            this._state.activeDropList = this._dropContainer;
        }
        // Add dragging class
        this._rootElement.classList.add('cdk-drag-dragging');
    }
    /**
     * Handle pointermove during drag.
     *
     * @param event - Pointer event.
     *
     * @private
     */
    onPointerMove(event) {
        if (!this._state.isDragging) {
            return;
        }
        event.preventDefault();
        const pointerPosition = getPointerPosition(event);
        // Log every 10th move to avoid spam
        if (Math.random() < 0.1) {
        }
        this.updatePointerDirection(pointerPosition);
        this._state.pointerPosition = pointerPosition;
        // Calculate new position
        let targetPosition = this.calculateTargetPosition(pointerPosition);
        // Apply constraints
        targetPosition = this.applyConstraints(targetPosition);
        // Update position
        this.updateDragPosition(targetPosition);
        // Emit move event
        const moveEvent = this.createDragMoveEvent(event);
        this._element.dispatchEvent(moveEvent);
        // Check drop list intersection
        this.checkDropListIntersection(pointerPosition);
        this._state.previousPointerPosition = pointerPosition;
    }
    /**
     * Handle pointerup (end drag).
     *
     * @param event - Pointer event.
     *
     * @private
     */
    onPointerUp(_event) {
        if (!this._state.isDragging) {
            return;
        }
        this.endDragSequence(true);
    }
    /**
     * Handle lost pointer capture.
     *
     * @private
     */
    onLostPointerCapture() {
        if (this._state.isDragging) {
            this.endDragSequence(false);
        }
    }
    /**
     * Handle keydown during drag.
     * Pressing Escape cancels the active drag operation.
     *
     * @param event - Keyboard event.
     *
     * @private
     */
    onKeyDown(event) {
        if (event.key === 'Escape' && this._state.isDragging) {
            event.preventDefault();
            this.endDragSequence(false);
        }
    }
    // #endregion
    // TO BE CONTINUED IN NEXT PART...
    // #region Private Methods - Position & Transform
    /**
     * Calculate target position based on pointer.
     *
     * @param pointerPosition - Current pointer position.
     * @returns Target position for element (transform delta from initial position).
     *
     * @private
     */
    calculateTargetPosition(pointerPosition) {
        // Calculate transform delta:
        // (pointer - pickupOffset) gives absolute target position
        // - initialRect gives delta from starting position (for transform)
        return {
            x: pointerPosition.x - this._state.pickupOffset.x - this._state.initialRect.left,
            y: pointerPosition.y - this._state.pickupOffset.y - this._state.initialRect.top
        };
    }
    /**
     * Apply all constraints to a position.
     *
     * @param position - Unconstrained position.
     * @returns Constrained position.
     *
     * @private
     */
    applyConstraints(position) {
        let constrained = position;
        // Axis lock
        if (this._config.lockAxis) {
            constrained = constrainToAxis(constrained, this._state.initialPosition, this._config.lockAxis);
        }
        // Boundary constraint
        if (this._boundaryElement) {
            const boundaryRect = this._boundaryElement.getBoundingClientRect();
            constrained = constrainPointToBoundary(addPoints(constrained, getRectTopLeft(this._state.initialRect)), boundaryRect, {
                width: this._state.initialRect.width,
                height: this._state.initialRect.height
            });
            constrained = subtractPoints(constrained, getRectTopLeft(this._state.initialRect));
        }
        // Custom constraint function (check typeof to avoid unnecessary conditional)
        if (this._config.constrainPosition) {
            const absolute = addPoints(constrained, getRectTopLeft(this._state.initialRect));
            const constrainedAbsolute = this._config.constrainPosition(absolute, this, this._state.currentRect, this._state.pickupOffset);
            constrained = subtractPoints(constrainedAbsolute, getRectTopLeft(this._state.initialRect));
        }
        return constrained;
    }
    /**
     * Update drag position (transform).
     *
     * @param position - New position.
     *
     * @private
     */
    updateDragPosition(position) {
        this._state.currentPosition = position;
        // Update preview element
        if (this._state.previewElement) {
            applyTransform(this._state.previewElement, position);
        }
        // Update current rect
        this._state.currentRect = new DOMRect(this._state.initialRect.left + position.x, this._state.initialRect.top + position.y, this._state.initialRect.width, this._state.initialRect.height);
    }
    /**
     * Update pointer direction.
     *
     * @param currentPosition - Current pointer position.
     *
     * @private
     */
    updatePointerDirection(currentPosition) {
        const delta = getDelta(this._state.previousPointerPosition, currentPosition);
        const threshold = this._config.pointerDirectionChangeThreshold;
        const direction = {
            x: 0,
            y: 0
        };
        if (Math.abs(delta.x) > threshold) {
            direction.x = delta.x > 0 ? 1 : -1;
        }
        if (Math.abs(delta.y) > threshold) {
            direction.y = delta.y > 0 ? 1 : -1;
        }
        this._state.pointerDirection = direction;
    }
    // #endregion
    // #region Private Methods - Preview & Placeholder
    /**
     * Create preview element (ghost).
     *
     * @private
     */
    createPreviewElement() {
        if (this._config.previewMode === 'element' && !this._config.previewTemplate) {
            // Element mode: use the actual element as the drag preview.
            // Apply position: fixed so it is lifted out of normal flow and
            // positioned relative to the viewport.
            this._rootElement.style.position = 'fixed';
            this._rootElement.style.top = `${this._state.initialRect.top}px`;
            this._rootElement.style.left = `${this._state.initialRect.left}px`;
            this._rootElement.style.width = `${this._state.initialRect.width}px`;
            this._rootElement.style.height = `${this._state.initialRect.height}px`;
            this._rootElement.style.zIndex = `${this._config.zIndex}`;
            this._rootElement.style.margin = '0';
            this._rootElement.style.boxSizing = 'border-box';
            this._rootElement.style.pointerEvents = 'none';
            this._rootElement.classList.add('drag-preview');
            this._state.currentPosition = { x: 0, y: 0 };
            this._state.previewElement = this._rootElement;
            this._state.previewContainer = null;
            return;
        }
        // Ghost mode (legacy): create a separate <mosaik-ghost> clone.
        const ghost = document.createElement('mosaik-ghost');
        ghost.classList.add('drag-preview');
        // Use inline styles for positioning to override theme styles
        // The ghost needs position: fixed for viewport-relative positioning
        ghost.style.cssText = `
            position: fixed !important;
            top: ${this._state.initialRect.top}px !important;
            left: ${this._state.initialRect.left}px !important;
            width: ${this._state.initialRect.width}px;
            height: ${this._state.initialRect.height}px;
            z-index: ${this._config.zIndex};
            pointer-events: none;
            margin: 0;
            box-sizing: border-box;
        `;
        // Also set the element properties for any internal use
        ghost.width = `${this._state.initialRect.width}px`;
        ghost.height = `${this._state.initialRect.height}px`;
        // Use custom preview template if provided, otherwise copy inner content
        if (this._config.previewTemplate) {
            if (typeof this._config.previewTemplate === 'function') {
                const customContent = this._config.previewTemplate();
                // Remove inline style that the dragPreview directive applies to hide the template
                customContent.removeAttribute('style');
                ghost.appendChild(customContent);
            }
            else {
                const customContent = this._config.previewTemplate.cloneNode(true);
                // Remove inline style that the dragPreview directive applies to hide the template
                customContent.removeAttribute('style');
                ghost.appendChild(customContent);
            }
        }
        else {
            ghost.innerHTML = this._rootElement.innerHTML;
        }
        // Initial transform should be 0,0 since we're using top/left for initial position
        // Transform will be applied on move relative to initial position
        this._state.currentPosition = { x: 0, y: 0 };
        // Get container
        const container = this.getPreviewContainer();
        container.appendChild(ghost);
        this._state.previewElement = ghost;
        this._state.previewContainer = container;
    }
    /**
     * Get container for preview element.
     *
     * @returns Container element.
     *
     * @private
     */
    getPreviewContainer() {
        const config = this._config.previewContainer;
        if (config === 'global') {
            return document.body;
        }
        else if (config === 'parent') {
            return this._rootElement.parentElement ?? document.body;
        }
        else {
            return config;
        }
    }
    /**
     * Create placeholder element.
     *
     * @private
     */
    createPlaceholderElement() {
        // Use PlaceholderElement as placeholder
        const placeholder = document.createElement('mosaik-placeholder');
        placeholder.classList.add('drag-placeholder');
        // No explicit positioning - let it stay in normal flow at original element's position
        placeholder.width = `${this._state.initialRect.width}px`;
        placeholder.height = `${this._state.initialRect.height}px`;
        placeholder.style.pointerEvents = 'none';
        placeholder.style.display = 'block';
        placeholder.style.boxSizing = 'border-box';
        // In element mode the placeholder is just a transparent spacer.
        // In ghost mode (or when a custom template is provided) it carries content.
        if (this._config.placeholderTemplate) {
            if (typeof this._config.placeholderTemplate === 'function') {
                const customContent = this._config.placeholderTemplate();
                placeholder.appendChild(customContent);
            }
            else {
                const customContent = this._config.placeholderTemplate.cloneNode(true);
                placeholder.appendChild(customContent);
            }
        }
        else if (this._config.previewMode === 'ghost') {
            // Ghost mode: copy inner content from root element
            placeholder.innerHTML = this._rootElement.innerHTML;
        }
        // Element mode default: placeholder stays empty (transparent spacer)
        // Insert before original element
        this._rootElement.parentElement?.insertBefore(placeholder, this._rootElement);
        this._state.placeholderElement = placeholder;
        // In element mode the root element IS the preview (already position: fixed).
        // Move it to the preview container (document.body by default) so that
        // ancestor transforms / overflow do not affect the fixed positioning.
        if (this._config.previewMode === 'element') {
            const sourceShadowRoot = this._rootElement.getRootNode();
            // When the element lives inside a Shadow DOM, moving it to document.body
            // would strip all scoped styles (e.g. [part='item'] rules). To preserve them
            // we create a DragStyleContainer that adopts the source Shadow Root's stylesheets.
            if (sourceShadowRoot instanceof ShadowRoot) {
                this._styleContainer = new DragStyleContainer(sourceShadowRoot);
                this._styleContainer.attach(this._rootElement);
            }
            else {
                const container = this.getPreviewContainer();
                container.appendChild(this._rootElement);
            }
        }
        else {
            // Ghost mode: hide original element completely (not just visibility).
            // Using position: absolute removes it from layout flow while keeping it in DOM.
            // This prevents layout shift while allowing us to restore it later.
            /* eslint-disable no-restricted-syntax */
            this._rootElement.style.position = 'absolute';
            this._rootElement.style.top = '-9999px';
            this._rootElement.style.left = '-9999px';
            this._rootElement.style.visibility = 'hidden';
            /* eslint-enable no-restricted-syntax */
        }
    }
    /**
     * Remove preview element.
     *
     * @private
     */
    removePreviewElement() {
        if (this._state.previewElement) {
            // In element mode the preview IS the root element — do not remove it
            // from the DOM. Just clear the reference and remove the preview class.
            if (this._state.previewElement !== this._rootElement) {
                this._state.previewElement.remove();
            }
            else {
                this._rootElement.classList.remove('drag-preview');
            }
            this._state.previewElement = null;
        }
        this._state.previewContainer = null;
    }
    /**
     * Remove placeholder element.
     *
     * @private
     */
    removePlaceholderElement() {
        if (this._state.placeholderElement) {
            // Move the root element to the placeholder's DOM position so
            // the visual sort order is preserved after the placeholder is removed.
            const parent = this._state.placeholderElement.parentNode;
            if (parent) {
                parent.insertBefore(this._rootElement, this._state.placeholderElement);
            }
            this._state.placeholderElement.remove();
            this._state.placeholderElement = null;
        }
        // Dispose the style container after the element has been moved back.
        if (this._styleContainer) {
            this._styleContainer.dispose();
            this._styleContainer = null;
        }
        // Restore original element's inline styles that were overwritten during drag.
        // In element mode additional properties (width, height, zIndex, margin, boxSizing)
        // were also altered by createPreviewElement, so restore them too.
        /* eslint-disable no-restricted-syntax */
        if (this._state.savedStyles) {
            this._rootElement.style.position = this._state.savedStyles.position;
            this._rootElement.style.top = this._state.savedStyles.top;
            this._rootElement.style.left = this._state.savedStyles.left;
            this._rootElement.style.visibility = this._state.savedStyles.visibility;
            this._rootElement.style.width = this._state.savedStyles.width;
            this._rootElement.style.height = this._state.savedStyles.height;
            this._rootElement.style.zIndex = this._state.savedStyles.zIndex;
            this._rootElement.style.margin = this._state.savedStyles.margin;
            this._rootElement.style.boxSizing = this._state.savedStyles.boxSizing;
            this._rootElement.style.pointerEvents = this._state.savedStyles.pointerEvents;
        }
        else {
            this._rootElement.style.position = '';
            this._rootElement.style.top = '';
            this._rootElement.style.left = '';
            this._rootElement.style.visibility = '';
            this._rootElement.style.width = '';
            this._rootElement.style.height = '';
            this._rootElement.style.zIndex = '';
            this._rootElement.style.margin = '';
            this._rootElement.style.boxSizing = '';
            this._rootElement.style.pointerEvents = '';
        }
        /* eslint-enable no-restricted-syntax */
    }
    // #endregion
    // #region Private Methods - Drop List Interaction
    /**
     * Check if pointer intersects with a drop list.
     *
     * @param pointerPosition - Current pointer position.
     *
     * @private
     */
    checkDropListIntersection(pointerPosition) {
        const dropList = this._registry.getDropContainerAtPoint(pointerPosition.x, pointerPosition.y);
        const currentListId = this._state.activeDropList ? this._state.activeDropList.id || 'unknown' : 'none';
        const newListId = dropList ? dropList.id || 'unknown' : 'none';
        if (dropList !== this._state.activeDropList) {
            // Exit previous
            if (this._state.activeDropList) {
                this.exitDropList(this._state.activeDropList);
            }
            // Enter new
            if (dropList) {
                this.enterDropList(dropList, pointerPosition);
            }
            this._state.activeDropList = dropList;
        }
        else if (dropList) {
            // Still in same list, notify of movement
            if (dropList._sortItem) {
                dropList._sortItem(this, pointerPosition);
            }
        }
    }
    /**
     * Enter a drop list.
     *
     * @param dropList - Drop list to enter.
     * @param pointerPosition - Pointer position.
     *
     * @private
     */
    enterDropList(dropList, pointerPosition) {
        if (dropList._canReceive) {
            const canEnter = dropList._canReceive(this, pointerPosition);
            if (!canEnter) {
                return;
            }
        }
        // Notify drop list
        if (dropList._enter) {
            dropList._enter(this, pointerPosition);
        }
        else {
        }
        // Emit enter event
        const enterEvent = this.createDragEnterEvent(dropList);
        this._element.dispatchEvent(enterEvent);
    }
    /**
     * Exit a drop list.
     *
     * @param dropList - Drop list to exit.
     *
     * @private
     */
    exitDropList(dropList) {
        // Notify drop list
        if (dropList._exit) {
            dropList._exit(this);
        }
        // Emit exit event
        const exitEvent = this.createDragExitEvent(dropList);
        this._element.dispatchEvent(exitEvent);
    }
    // #endregion
    // #region Private Methods - Drag Sequence
    /**
     * End drag sequence.
     *
     * @param dropped - Whether item was dropped (true) or cancelled (false).
     *
     * @private
     */
    endDragSequence(dropped) {
        if (!this._state.isDragging) {
            return;
        }
        // Emit release event
        const releaseEvent = this.createDragReleaseEvent();
        this._element.dispatchEvent(releaseEvent);
        // Notify drop list
        const finalDropList = this._state.activeDropList ?? this._dropContainer;
        if (finalDropList && dropped) {
            if (finalDropList._drop) {
                finalDropList._drop(this, this._state.pointerPosition);
            }
            else {
            }
        }
        // Exit any active drop list (only if drop did NOT happen — a successful drop already handles cleanup)
        if (this._state.activeDropList && !dropped) {
            this.exitDropList(this._state.activeDropList);
        }
        this._state.activeDropList = null;
        // Animate back to position
        this.animatePreviewToPlaceholder(() => {
            this.cleanupDragArtifacts();
            // Emit end event
            const endEvent = this.createDragEndEvent(dropped);
            this._element.dispatchEvent(endEvent);
            // Final cleanup
            this.finalizeDragSequence();
        });
    }
    /**
     * Animate preview back to placeholder position.
     *
     * @param callback - Callback when animation completes.
     *
     * @private
     */
    animatePreviewToPlaceholder(callback) {
        if (!this._state.previewElement) {
            callback();
            return;
        }
        // If there's a placeholder, animate to its position
        if (this._state.placeholderElement) {
            const placeholderRect = this._state.placeholderElement.getBoundingClientRect();
            const previewRect = this._state.previewElement.getBoundingClientRect();
            // Calculate current visual position (where preview appears on screen)
            const currentVisualPosition = {
                x: previewRect.left,
                y: previewRect.top
            };
            // Calculate target position (where placeholder is)
            const targetPosition = {
                x: placeholderRect.left,
                y: placeholderRect.top
            };
            // Calculate the delta needed to move from current to target
            const delta = {
                x: targetPosition.x - currentVisualPosition.x,
                y: targetPosition.y - currentVisualPosition.y
            };
            // Animate from current transform position to target delta
            const animation = animatePosition(this._state.previewElement, this._state.currentPosition, {
                x: this._state.currentPosition.x + delta.x,
                y: this._state.currentPosition.y + delta.y
            });
            animation.onfinish = () => callback();
        }
        else {
            // No placeholder, animate back to origin
            const animation = animateToOriginalPosition(this._state.previewElement, this._state.currentPosition);
            animation.onfinish = () => callback();
        }
    }
    /**
     * Cleanup drag artifacts.
     *
     * @private
     */
    cleanupDragArtifacts() {
        this.removePreviewElement();
        this.removePlaceholderElement();
        this._rootElement.classList.remove('cdk-drag-dragging');
        // Restore original transform instead of clearing it, so pre-existing
        // CSS transforms (e.g. translate(-50%, -50%) for centering) are preserved.
        if (this._state.savedStyles?.transform) {
            /* eslint-disable no-restricted-syntax */
            this._rootElement.style.transform = this._state.savedStyles.transform;
            /* eslint-enable no-restricted-syntax */
        }
        else {
            clearTransform(this._rootElement);
        }
        cancelAnimations(this._rootElement);
    }
    /**
     * Finalize drag sequence.
     *
     * @private
     */
    finalizeDragSequence() {
        this.detachDragListeners();
        this._registry.stopDragging(this);
        this._state.isDragging = false;
        this._state.pointerId = null;
    }
    /**
     * Attach drag event listeners.
     *
     * @private
     */
    attachDragListeners() {
        const moveListener = this.onPointerMove.bind(this);
        const upListener = this.onPointerUp.bind(this);
        const lostCaptureListener = this.onLostPointerCapture.bind(this);
        const keydownListener = this.onKeyDown.bind(this);
        document.addEventListener('pointermove', moveListener);
        document.addEventListener('pointerup', upListener);
        document.addEventListener('keydown', keydownListener);
        this._rootElement.addEventListener('lostpointercapture', lostCaptureListener);
        this._listeners.set('pointermove', moveListener);
        this._listeners.set('pointerup', upListener);
        this._listeners.set('keydown', keydownListener);
        this._listeners.set('lostpointercapture', lostCaptureListener);
    }
    /**
     * Detach drag event listeners.
     *
     * @private
     */
    detachDragListeners() {
        const moveListener = this._listeners.get('pointermove');
        const upListener = this._listeners.get('pointerup');
        const keydownListener = this._listeners.get('keydown');
        const lostCaptureListener = this._listeners.get('lostpointercapture');
        if (moveListener) {
            document.removeEventListener('pointermove', moveListener);
        }
        if (upListener) {
            document.removeEventListener('pointerup', upListener);
        }
        if (keydownListener) {
            document.removeEventListener('keydown', keydownListener);
        }
        if (lostCaptureListener) {
            this._rootElement.removeEventListener('lostpointercapture', lostCaptureListener);
        }
        this._listeners.clear();
    }
    // #endregion
    // #region Private Methods - Event Creation
    /**
     * Create base event detail.
     *
     * @param event - Native event (optional).
     * @returns Base event detail.
     *
     * @private
     */
    createBaseEventDetail(event) {
        const delta = getDelta(this._state.initialPosition, this._state.currentPosition);
        const distance = {
            x: Math.abs(delta.x),
            y: Math.abs(delta.y)
        };
        return {
            source: this,
            pointerPosition: this._state.pointerPosition,
            delta,
            distance,
            event
        };
    }
    /**
     * Create drag start event.
     *
     * @param event - Native event.
     * @returns Drag start event.
     *
     * @private
     */
    createDragStartEvent(event) {
        const detail = {
            ...this.createBaseEventDetail(event),
            type: 'dragStart'
        };
        return new CustomEvent('dragStart', {
            bubbles: true,
            composed: true,
            cancelable: true,
            detail
        });
    }
    /**
     * Create drag move event.
     *
     * @param event - Native event.
     * @returns Drag move event.
     *
     * @private
     */
    createDragMoveEvent(event) {
        const detail = {
            ...this.createBaseEventDetail(event),
            type: 'dragMove'
        };
        return new CustomEvent('dragMove', {
            bubbles: true,
            composed: true,
            cancelable: false,
            detail
        });
    }
    /**
     * Create drag release event.
     *
     * @returns Drag release event.
     *
     * @private
     */
    createDragReleaseEvent() {
        const detail = {
            ...this.createBaseEventDetail(),
            type: 'dragRelease'
        };
        return new CustomEvent('dragRelease', {
            bubbles: true,
            composed: true,
            cancelable: false,
            detail
        });
    }
    /**
     * Create drag end event.
     *
     * @param dropped - Whether item was dropped.
     * @returns Drag end event.
     *
     * @private
     */
    createDragEndEvent(dropped) {
        const detail = {
            ...this.createBaseEventDetail(),
            type: 'dragEnd',
            dropped
        };
        return new CustomEvent('dragEnd', {
            bubbles: true,
            composed: true,
            cancelable: false,
            detail
        });
    }
    /**
     * Create drag enter event.
     *
     * @param container - Container being entered.
     * @returns Drag enter event.
     *
     * @private
     */
    createDragEnterEvent(container) {
        const detail = {
            type: 'dragEnter',
            item: this,
            container,
            currentIndex: container._getItemIndex ? container._getItemIndex(this) : -1
        };
        return new CustomEvent('dragEnter', {
            bubbles: true,
            composed: true,
            cancelable: false,
            detail
        });
    }
    /**
     * Create drag exit event.
     *
     * @param container - Container being exited.
     * @returns Drag exit event.
     *
     * @private
     */
    createDragExitEvent(container) {
        const detail = {
            type: 'dragExit',
            item: this,
            container
        };
        return new CustomEvent('dragExit', {
            bubbles: true,
            composed: true,
            cancelable: false,
            detail
        });
    }
    // #endregion
    // #region Private Methods - Config & Utilities
    /**
     * Apply default configuration values.
     *
     * @param config - User config.
     * @returns Full config with defaults.
     *
     * @private
     */
    applyConfigDefaults(config) {
        return {
            disabled: config.disabled ?? false,
            lockAxis: config.lockAxis ?? null,
            rootElement: config.rootElement ?? this._element,
            previewContainer: config.previewContainer ?? 'global',
            previewClass: config.previewClass ?? '',
            dragStartDelay: config.dragStartDelay ?? 0,
            dragStartDistance: config.dragStartDistance ?? 0,
            boundaryElement: config.boundaryElement ?? null,
            constrainPosition: config.constrainPosition ?? undefined,
            scale: config.scale ?? 1,
            zIndex: config.zIndex ?? 1000,
            pointerDirectionChangeThreshold: config.pointerDirectionChangeThreshold ?? 5,
            direction: config.direction ?? 'ltr',
            parentDragRef: config.parentDragRef,
            previewTemplate: config.previewTemplate,
            placeholderTemplate: config.placeholderTemplate,
            previewMode: config.previewMode ?? 'element'
        };
    }
    /**
     * Get start delay based on pointer type.
     *
     * @param event - Pointer event.
     * @returns Delay in ms.
     *
     * @private
     */
    getStartDelay(event) {
        const delay = this._config.dragStartDelay;
        if (typeof delay === 'number') {
            return delay;
        }
        return event.pointerType === 'touch' ? delay.touch : delay.mouse;
    }
    /**
     * Get start distance based on pointer type.
     *
     * @param event - Pointer event.
     * @returns Distance in pixels.
     *
     * @private
     */
    getStartDistance(event) {
        const distance = this._config.dragStartDistance;
        if (typeof distance === 'number') {
            return distance;
        }
        return event.pointerType === 'touch' ? distance.touch : distance.mouse;
    }
}
// #endregion
// #region Factory
/**
 * Create a new DragRef instance.
 *
 * @param element - Element to make draggable.
 * @param config - Configuration options.
 * @returns DragRef instance.
 *
 * @public
 */
export function createDragRef(element, config) {
    return new DragRef(element, config);
}
// #endregion
//# sourceMappingURL=DragRef.js.map