Ariver
2026-06-12 63128a870cbedb841cb19cdfbf2bf54e3dfbc25c
C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -55,9 +55,12 @@
    private var focusedSpaceID: UInt64?
    private var debugHoveredAppGroupIndex: Int?
    private var waterfallColumns: [WaterfallColumnLayers] = []
    private var waterfallViewMode: QuickSwitchWaterfallViewMode = .verticalColumns
    private var waterfallContentWidth: CGFloat = 0
    private var waterfallScrollOffset: CGFloat = 0
    private var waterfallColumnScrollOffsets: [Int: CGFloat] = [:]
    private var horizontalMasonryContentHeight: CGFloat = 0
    private var horizontalMasonryScrollOffset: CGFloat = 0
    private var currentSelection: QuickSwitchSelection?
    private var columnSelectionHistory: [Int: Int] = [:]
    private var keyboardCommandsApplied: [String] = []
@@ -78,8 +81,16 @@
    private var keyboardResponseLatencyMilliseconds: [Double] = []
    private var mouseResponseLatencyMilliseconds: [Double] = []
    private var hoverResponseLatencyMilliseconds: [Double] = []
    private var lastProjectionTransitionMovedLayerCount = 0
    private var lastProjectionTransitionFadeInLayerCount = 0
    private var lastProjectionTransitionFadeOutLayerCount = 0
    private var lastProjectionTransitionDurationMilliseconds: Double = 0
    private var suppressEventInput = false
    private var suppressEventBackgroundClicks = false
    var onEscape: (() -> Void)?
    var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
    var onSpaceLaneClick: ((UInt64, Int) -> Void)?
    var onBackgroundClick: (() -> Bool)?
    var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
    var onCloseConfirmationDisabled: (() -> Void)?
@@ -258,6 +269,22 @@
        let closeButton: CloseButtonLayers
    }
    private struct ProjectionLayerSnapshot {
        let layer: CALayer
        let frame: CGRect
    }
    private struct ProjectionColumnSnapshot {
        let layer: CALayer
        let frame: CGRect
        let cardsByWindowID: [UInt32: ProjectionLayerSnapshot]
    }
    private struct ProjectionTransitionSnapshot {
        let appShelfItemsByAppGroupIndex: [Int: ProjectionLayerSnapshot]
        let waterfallColumnsByAppGroupIndex: [Int: ProjectionColumnSnapshot]
    }
    private enum AppShelfMetrics {
        static let normalIconSize: CGFloat = 56
        static let hoverIconSize: CGFloat = 68
@@ -290,6 +317,18 @@
        static let cardGap: CGFloat = 12
        static let revealPadding: CGFloat = 24
        static let horizontalRevealPadding: CGFloat = 0
    }
    private enum HorizontalMasonryMetrics {
        static let horizontalPadding: CGFloat = 12
        static let verticalPadding: CGFloat = 0
        static let cardGap: CGFloat = 16
        static let cardMinWidth: CGFloat = 220
        static let cardMaxWidth: CGFloat = 320
        static let titleBarHeight: CGFloat = 26
        static let titleGap: CGFloat = 6
        static let minThumbnailHeight: CGFloat = 92
        static let maxThumbnailHeightFraction: CGFloat = 0.40
    }
    private enum WaterfallHorizontalIntent {
@@ -358,6 +397,13 @@
    }
    override func keyDown(with event: NSEvent) {
        if suppressEventInput {
            DevelopmentDiagnostics.log("quickSwitch.view.keyDown.ignoredForDebugSequence", [
                "keyCode": event.keyCode
            ])
            return
        }
        switch event.keyCode {
        case TriggerKeyCode.escape where Self.shouldDismissOnEscape(event):
            onEscape?()
@@ -400,6 +446,15 @@
        }
        if shouldScrollWaterfall(for: event) {
            if isHorizontalMasonryMode {
                let currentOffset = horizontalMasonryScrollOffset
                horizontalMasonryScrollOffset = clampedHorizontalMasonryOffset(
                    currentOffset - event.scrollingDeltaY
                )
                needsLayout = true
                return
            }
            if abs(event.scrollingDeltaX) >= abs(event.scrollingDeltaY), waterfallAlignmentScrollable {
                waterfallScrollOffset = clampedWaterfallAlignmentOffset(waterfallScrollOffset - event.scrollingDeltaX)
                needsLayout = true
@@ -441,6 +496,15 @@
    }
    override func mouseDown(with event: NSEvent) {
        if suppressEventInput {
            DevelopmentDiagnostics.log("quickSwitch.view.mouseDown.ignoredForDebugSequence", [
                "x": event.locationInWindow.x,
                "y": event.locationInWindow.y,
                "bounds": NSStringFromRect(bounds)
            ])
            return
        }
        if handleCloseConfirmationMouseDown(at: event.locationInWindow) {
            return
        }
@@ -456,7 +520,7 @@
        }
        if let segment = spaceLaneSegment(at: event.locationInWindow) {
            clickSpaceLaneSegment(segment)
            clickSpaceLaneSegment(segment, clickCount: max(1, event.clickCount))
            return
        }
@@ -470,6 +534,9 @@
            "y": event.locationInWindow.y,
            "bounds": NSStringFromRect(bounds)
        ])
        if !suppressEventBackgroundClicks, onBackgroundClick?() == true {
            return
        }
        super.mouseDown(with: event)
    }
@@ -513,6 +580,15 @@
        }
    }
    func setSuppressEventBackgroundClicks(_ suppressed: Bool) {
        suppressEventBackgroundClicks = suppressed
    }
    func setSuppressEventInput(_ suppressed: Bool) {
        suppressEventInput = suppressed
        suppressEventBackgroundClicks = suppressed
    }
    func beginPerformanceFirstFrameMeasurement() {
        firstFrameMeasurementStart = CACurrentMediaTime()
    }
@@ -543,6 +619,17 @@
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
        }
        needsLayout = true
    }
    func setWaterfallViewMode(_ mode: QuickSwitchWaterfallViewMode) {
        guard waterfallViewMode != mode else { return }
        waterfallViewMode = mode
        waterfallScrollOffset = 0
        waterfallColumnScrollOffsets = [:]
        horizontalMasonryScrollOffset = 0
        horizontalMasonryContentHeight = 0
        needsLayout = true
    }
@@ -608,35 +695,77 @@
        }
    }
    func effectiveSelectionForProjection() -> QuickSwitchSelection? {
        effectiveSelection
    }
    func apply(viewModel: QuickSwitchViewModel?) {
        apply(viewModel: viewModel, preservingInteractionState: false, animatedProjection: false)
    }
    func applyProjected(viewModel: QuickSwitchViewModel?) {
        apply(viewModel: viewModel, preservingInteractionState: true, animatedProjection: true)
    }
    private func apply(
        viewModel: QuickSwitchViewModel?,
        preservingInteractionState: Bool,
        animatedProjection: Bool
    ) {
        let transitionSnapshot = animatedProjection ? captureProjectionTransitionSnapshot() : nil
        if !animatedProjection {
            resetProjectionTransitionReport()
        }
        currentViewModel = viewModel
        screenshotSourcesByWindowID = [:]
        screenshotSkippedReasonsByWindowID = [:]
        currentSelection = viewModel?.initialSelection
        columnSelectionHistory = [:]
        keyboardCommandsApplied = []
        lastKeyboardCommand = nil
        tabIgnoredCount = 0
        lastCommittedSelection = nil
        lastCommitSource = nil
        mouseCommandsApplied = []
        lastMouseCommand = nil
        lastClickedAppGroupIndex = nil
        lastAppShelfClickChangedSelection = nil
        lastClickedWindowID = nil
        lastSpaceFocusedAppGroupIndex = nil
        lastSpaceLaneClickSpaceID = nil
        lastAppShelfScrollSyncedAppGroupIndex = nil
        focusedSpaceID = nil
        hoveredCloseTarget = nil
        pendingCloseTarget = nil
        pendingCloseRequest = nil
        pendingCloseOptOut = false
        closeConfirmationLayers.containerLayer.isHidden = true
        closeConfirmationArrowEdge = nil
        clearCloseFeedback()
        setHoverTarget(.none)
        hoveredAppGroupIndex = debugHoveredAppGroupIndex
        if preservingInteractionState {
            currentSelection = validSelection(currentSelection, in: viewModel) ?? viewModel?.initialSelection
            columnSelectionHistory = columnSelectionHistory.filter { appGroupIndex, _ in
                viewModel?.waterfallColumns.contains { $0.appGroupIndex == appGroupIndex } == true
            }
            if let currentSelection {
                columnSelectionHistory[currentSelection.appGroupIndex] = currentSelection.windowIndex
            }
            hoveredCloseTarget = nil
            pendingCloseTarget = nil
            pendingCloseRequest = nil
            pendingCloseOptOut = false
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
        } else {
            screenshotSourcesByWindowID = [:]
            screenshotSkippedReasonsByWindowID = [:]
            currentSelection = viewModel?.initialSelection
            columnSelectionHistory = [:]
            keyboardCommandsApplied = []
            lastKeyboardCommand = nil
            tabIgnoredCount = 0
            lastCommittedSelection = nil
            lastCommitSource = nil
            mouseCommandsApplied = []
            lastMouseCommand = nil
            lastClickedAppGroupIndex = nil
            lastAppShelfClickChangedSelection = nil
            lastClickedWindowID = nil
            lastSpaceFocusedAppGroupIndex = nil
            lastSpaceLaneClickSpaceID = nil
            lastAppShelfScrollSyncedAppGroupIndex = nil
            waterfallScrollOffset = 0
            waterfallColumnScrollOffsets = [:]
            horizontalMasonryScrollOffset = 0
            horizontalMasonryContentHeight = 0
            focusedSpaceID = nil
            hoveredCloseTarget = nil
            pendingCloseTarget = nil
            pendingCloseRequest = nil
            pendingCloseOptOut = false
            closeConfirmationLayers.containerLayer.isHidden = true
            closeConfirmationArrowEdge = nil
            clearCloseFeedback()
            setHoverTarget(.none)
            hoveredAppGroupIndex = debugHoveredAppGroupIndex
        }
        if let currentSelection {
            columnSelectionHistory[currentSelection.appGroupIndex] = currentSelection.windowIndex
        }
@@ -646,6 +775,250 @@
        needsLayout = true
        ensureCurrentSelectionVisible(horizontalIntent: .alignWithAppShelf(animated: false))
        needsLayout = true
        if animatedProjection, let transitionSnapshot {
            layoutSubtreeIfNeeded()
            applyProjectionTransitionAnimations(from: transitionSnapshot)
        }
    }
    private func validSelection(
        _ selection: QuickSwitchSelection?,
        in viewModel: QuickSwitchViewModel?
    ) -> QuickSwitchSelection? {
        guard let selection, let viewModel else { return nil }
        for column in viewModel.waterfallColumns {
            if let card = column.windows.first(where: { $0.window.id == selection.windowID }) {
                return QuickSwitchSelection(
                    appGroupIndex: card.appGroupIndex,
                    windowIndex: card.windowIndex,
                    windowID: card.window.id
                )
            }
        }
        return nil
    }
    private func captureProjectionTransitionSnapshot() -> ProjectionTransitionSnapshot {
        ProjectionTransitionSnapshot(
            appShelfItemsByAppGroupIndex: Dictionary(
                uniqueKeysWithValues: appShelfItems.map { item in
                    (
                        item.item.appGroupIndex,
                        ProjectionLayerSnapshot(
                            layer: item.containerLayer,
                            frame: item.containerLayer.frame
                        )
                    )
                }
            ),
            waterfallColumnsByAppGroupIndex: Dictionary(
                uniqueKeysWithValues: waterfallColumns.map { column in
                    (
                        column.column.appGroupIndex,
                        ProjectionColumnSnapshot(
                            layer: column.containerLayer,
                            frame: column.containerLayer.frame,
                            cardsByWindowID: Dictionary(
                                uniqueKeysWithValues: column.cards.map { card in
                                    (
                                        card.item.window.id,
                                        ProjectionLayerSnapshot(
                                            layer: card.containerLayer,
                                            frame: card.containerLayer.frame
                                        )
                                    )
                                }
                            )
                        )
                    )
                }
            )
        )
    }
    private func applyProjectionTransitionAnimations(from snapshot: ProjectionTransitionSnapshot) {
        let reduceMotion = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion
        let duration: CFTimeInterval = reduceMotion ? 0.12 : 0.21
        let timing = CAMediaTimingFunction(name: reduceMotion ? .linear : .easeOut)
        var movedLayerCount = 0
        var fadeInLayerCount = 0
        var fadeOutLayerCount = 0
        for item in appShelfItems {
            guard let oldSnapshot = snapshot.appShelfItemsByAppGroupIndex[item.item.appGroupIndex] else {
                if addFadeInAnimation(to: item.containerLayer, duration: duration, timing: timing) {
                    fadeInLayerCount += 1
                }
                continue
            }
            if addPositionAnimation(to: item.containerLayer, from: oldSnapshot.frame, duration: duration, timing: timing) {
                movedLayerCount += 1
            }
        }
        let visibleAppGroupIndexes = Set(appShelfItems.map(\.item.appGroupIndex))
        for (appGroupIndex, oldSnapshot) in snapshot.appShelfItemsByAppGroupIndex
            where !visibleAppGroupIndexes.contains(appGroupIndex) {
            if addFadeOutSnapshot(
                oldSnapshot,
                to: appShelfContentLayer,
                frame: oldSnapshot.frame,
                duration: duration,
                timing: timing
            ) {
                fadeOutLayerCount += 1
            }
        }
        for column in waterfallColumns {
            guard let oldSnapshot = snapshot.waterfallColumnsByAppGroupIndex[column.column.appGroupIndex] else {
                if addFadeInAnimation(to: column.containerLayer, duration: duration, timing: timing) {
                    fadeInLayerCount += 1
                }
                continue
            }
            if addPositionAnimation(to: column.containerLayer, from: oldSnapshot.frame, duration: duration, timing: timing) {
                movedLayerCount += 1
            }
            for card in column.cards {
                guard let oldCardSnapshot = oldSnapshot.cardsByWindowID[card.item.window.id] else {
                    if addFadeInAnimation(to: card.containerLayer, duration: duration, timing: timing) {
                        fadeInLayerCount += 1
                    }
                    continue
                }
                if addPositionAnimation(to: card.containerLayer, from: oldCardSnapshot.frame, duration: duration, timing: timing) {
                    movedLayerCount += 1
                }
            }
        }
        let visibleColumnsByAppGroupIndex = Dictionary(
            uniqueKeysWithValues: waterfallColumns.map { ($0.column.appGroupIndex, $0) }
        )
        for (appGroupIndex, oldSnapshot) in snapshot.waterfallColumnsByAppGroupIndex {
            guard let visibleColumn = visibleColumnsByAppGroupIndex[appGroupIndex] else {
                if addFadeOutSnapshot(
                    ProjectionLayerSnapshot(layer: oldSnapshot.layer, frame: oldSnapshot.frame),
                    to: waterfallContentLayer,
                    frame: oldSnapshot.frame,
                    duration: duration,
                    timing: timing
                ) {
                    fadeOutLayerCount += 1
                }
                continue
            }
            let visibleWindowIDs = Set(visibleColumn.cards.map(\.item.window.id))
            for (windowID, oldCardSnapshot) in oldSnapshot.cardsByWindowID
                where !visibleWindowIDs.contains(windowID) {
                let absoluteFrame = oldCardSnapshot.frame.offsetBy(
                    dx: oldSnapshot.frame.minX,
                    dy: oldSnapshot.frame.minY
                )
                if addFadeOutSnapshot(
                    oldCardSnapshot,
                    to: waterfallContentLayer,
                    frame: absoluteFrame,
                    duration: duration,
                    timing: timing
                ) {
                    fadeOutLayerCount += 1
                }
            }
        }
        lastProjectionTransitionMovedLayerCount = movedLayerCount
        lastProjectionTransitionFadeInLayerCount = fadeInLayerCount
        lastProjectionTransitionFadeOutLayerCount = fadeOutLayerCount
        lastProjectionTransitionDurationMilliseconds = duration * 1_000
    }
    private func resetProjectionTransitionReport() {
        lastProjectionTransitionMovedLayerCount = 0
        lastProjectionTransitionFadeInLayerCount = 0
        lastProjectionTransitionFadeOutLayerCount = 0
        lastProjectionTransitionDurationMilliseconds = 0
    }
    @discardableResult
    private func addPositionAnimation(
        to layer: CALayer,
        from oldFrame: CGRect,
        duration: CFTimeInterval,
        timing: CAMediaTimingFunction
    ) -> Bool {
        let oldPosition = CGPoint(x: oldFrame.midX, y: oldFrame.midY)
        let newPosition = layer.position
        guard abs(oldPosition.x - newPosition.x) > 0.5 || abs(oldPosition.y - newPosition.y) > 0.5 else {
            return false
        }
        let animation = CABasicAnimation(keyPath: "position")
        animation.fromValue = oldPosition
        animation.toValue = newPosition
        animation.duration = duration
        animation.timingFunction = timing
        animation.isRemovedOnCompletion = true
        layer.add(animation, forKey: "quickSwitchProjectionPosition")
        return true
    }
    @discardableResult
    private func addFadeInAnimation(
        to layer: CALayer,
        duration: CFTimeInterval,
        timing: CAMediaTimingFunction
    ) -> Bool {
        let animation = CABasicAnimation(keyPath: "opacity")
        animation.fromValue = 0
        animation.toValue = layer.opacity
        animation.duration = duration
        animation.timingFunction = timing
        animation.isRemovedOnCompletion = true
        layer.add(animation, forKey: "quickSwitchProjectionFadeIn")
        return true
    }
    @discardableResult
    private func addFadeOutSnapshot(
        _ snapshot: ProjectionLayerSnapshot,
        to parentLayer: CALayer,
        frame: CGRect,
        duration: CFTimeInterval,
        timing: CAMediaTimingFunction
    ) -> Bool {
        let layer = snapshot.layer
        layer.removeAllAnimations()
        layer.removeFromSuperlayer()
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        layer.frame = frame
        layer.opacity = max(layer.opacity, 0.01)
        layer.zPosition = max(layer.zPosition, 240)
        parentLayer.addSublayer(layer)
        CATransaction.commit()
        let animation = CABasicAnimation(keyPath: "opacity")
        animation.fromValue = layer.opacity
        animation.toValue = 0
        animation.duration = duration
        animation.timingFunction = timing
        animation.isRemovedOnCompletion = true
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        layer.opacity = 0
        CATransaction.commit()
        layer.add(animation, forKey: "quickSwitchProjectionFadeOut")
        DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.05) { [weak layer] in
            layer?.removeFromSuperlayer()
        }
        return true
    }
    func reportDictionary() -> [String: Any] {
@@ -685,6 +1058,8 @@
            "spaceLaneHoveredSpaceID": hoveredSpaceLaneID ?? NSNull(),
            "spaceLaneFocusedSpaceID": focusedSpaceID ?? NSNull(),
            "activeSpaceFocusID": activeSpaceFocusID ?? NSNull(),
            "spaceFilterLockedSpaceID": currentViewModel?.lockedSpaceID ?? NSNull(),
            "spaceFilterActive": currentViewModel?.lockedSpaceID != nil,
            "spaceFocusAppGroupIndexes": spaceFocusedAppGroupIndexes(),
            "appHoverAssociatedSpaceIDs": appHoverAssociatedSpaceIDs.sorted(),
            "appHoverAssociatedSpaceLabels": appHoverAssociatedSpaceLabels,
@@ -716,13 +1091,19 @@
            "appShelfMinIconSize": Double(AppShelfMetrics.minIconSize),
            "appShelfItems": appShelfItemReports(),
            "waterfallFrame": dictionary(from: waterfallLayer.frame),
            "waterfallViewMode": waterfallViewMode.rawValue,
            "waterfallContentWidth": Double(waterfallContentWidth),
            "waterfallVisibleWidth": Double(waterfallLayer.bounds.width),
            "waterfallScrollOffset": Double(waterfallScrollOffset),
            "waterfallMaxScrollOffset": Double(waterfallMaxScrollOffset),
            "horizontalMasonryContentHeight": Double(horizontalMasonryContentHeight),
            "horizontalMasonryScrollOffset": Double(horizontalMasonryScrollOffset),
            "horizontalMasonryMaxScrollOffset": Double(horizontalMasonryMaxScrollOffset),
            "horizontalMasonryScrollable": horizontalMasonryMaxScrollOffset > 0,
            "waterfallAlignmentMinScrollOffset": Double(waterfallAlignmentScrollRange.lowerBound),
            "waterfallAlignmentMaxScrollOffset": Double(waterfallAlignmentScrollRange.upperBound),
            "waterfallScrollable": waterfallAlignmentScrollable,
            "waterfallScrollable": waterfallMaxScrollOffset > 0,
            "waterfallAlignmentScrollable": waterfallAlignmentScrollable,
            "waterfallColumnCount": waterfallColumns.count,
            "waterfallColumnNames": waterfallColumns.map(\.column.app.name),
            "waterfallClipsToBounds": waterfallLayer.masksToBounds,
@@ -731,6 +1112,10 @@
            "waterfallAlignmentTolerance": Double(WaterfallAlignmentMetrics.tolerance),
            "waterfallAlignmentAnimationDurationMilliseconds": Double(WaterfallAlignmentMetrics.animationDuration * 1_000),
            "waterfallAlignmentAnimationTimingName": "easeOut",
            "projectionTransitionMovedLayerCount": lastProjectionTransitionMovedLayerCount,
            "projectionTransitionFadeInLayerCount": lastProjectionTransitionFadeInLayerCount,
            "projectionTransitionFadeOutLayerCount": lastProjectionTransitionFadeOutLayerCount,
            "projectionTransitionDurationMilliseconds": lastProjectionTransitionDurationMilliseconds,
            "reduceMotionEffective": NSWorkspace.shared.accessibilityDisplayShouldReduceMotion,
            "selectedAppGroupIndex": effectiveSelection?.appGroupIndex ?? NSNull(),
            "selectedWindowIndex": effectiveSelection?.windowIndex ?? NSNull(),
@@ -826,8 +1211,13 @@
            recordKeyboardCommand("enter")
            commitCurrentSelection()
        case "tab":
            tabIgnoredCount += 1
            recordKeyboardCommand("tabIgnored")
            if isHorizontalMasonryMode {
                recordKeyboardCommand("tab")
                moveSelection(.right)
            } else {
                tabIgnoredCount += 1
                recordKeyboardCommand("tabIgnored")
            }
        default:
            recordKeyboardCommand("unknown:\(command)")
        }
@@ -897,8 +1287,21 @@
            }
            recordMouseCommand("click-space:\(spaceID)")
            if let segment = spaceLaneSegment(spaceID: spaceID) {
                clickSpaceLaneSegment(segment)
                clickSpaceLaneSegment(segment, clickCount: 1)
            }
        case "double-click-space" where parts.count == 2:
            guard let spaceID = UInt64(parts[1]) else {
                recordMouseCommand("invalid:\(command)")
                return
            }
            recordMouseCommand("double-click-space:\(spaceID)")
            if let segment = spaceLaneSegment(spaceID: spaceID) {
                clickSpaceLaneSegment(segment, clickCount: 1)
                clickSpaceLaneSegment(segment, clickCount: 2)
            }
        case "click-background":
            recordMouseCommand("click-background")
            _ = onBackgroundClick?()
        case "hover-card" where parts.count == 3:
            guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                recordMouseCommand("invalid:\(command)")
@@ -1014,22 +1417,15 @@
        focusSpace(segment.space.id, persistent: false, source: source)
    }
    private func clickSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers) {
    private func clickSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers, clickCount: Int = 1) {
        DevelopmentDiagnostics.log("quickSwitch.view.clickSpaceLane", [
            "spaceID": segment.space.id,
            "label": segment.space.label
            "label": segment.space.label,
            "clickCount": clickCount
        ])
        lastSpaceLaneClickSpaceID = segment.space.id
        guard let firstCard = firstWaterfallCard(inSpaceID: segment.space.id) else {
            DevelopmentDiagnostics.log("quickSwitch.view.clickSpaceLane.emptySpaceUnsupported", [
                "spaceID": segment.space.id,
                "label": segment.space.label
            ])
            return
        }
        commitWindowCard(firstCard, trigger: "spaceLane")
        onSpaceLaneClick?(segment.space.id, clickCount)
        lastSpaceLaneClickSpaceID = segment.space.id
    }
    private func focusSpace(_ spaceID: UInt64, persistent: Bool, source: HoverTargetSource = .event) {
@@ -1227,6 +1623,11 @@
    }
    private func moveSelection(_ direction: QuickSwitchKeyboardDirection) {
        if isHorizontalMasonryMode {
            moveHorizontalMasonrySelection(direction)
            return
        }
        guard let currentViewModel,
              let snapshot = snapshotForFocus(from: currentViewModel),
              let current = currentSelection ?? currentViewModel.initialSelection,
@@ -1256,6 +1657,113 @@
            "windowID": next.windowID
        ])
        needsLayout = true
    }
    private func moveHorizontalMasonrySelection(_ direction: QuickSwitchKeyboardDirection) {
        guard let current = currentSelection ?? currentViewModel?.initialSelection,
              let next = nextHorizontalMasonrySelection(from: current, direction: direction)
        else {
            DevelopmentDiagnostics.log("quickSwitch.view.moveHorizontalMasonrySelection.blocked", [
                "direction": String(describing: direction),
                "hasSelection": (currentSelection ?? currentViewModel?.initialSelection) != nil
            ])
            return
        }
        columnSelectionHistory[current.appGroupIndex] = current.windowIndex
        currentSelection = next
        columnSelectionHistory[next.appGroupIndex] = next.windowIndex
        ensureCurrentSelectionVisible(horizontalIntent: .alignWithAppShelf(animated: true))
        DevelopmentDiagnostics.log("quickSwitch.view.moveHorizontalMasonrySelection", [
            "direction": String(describing: direction),
            "appGroupIndex": next.appGroupIndex,
            "windowIndex": next.windowIndex,
            "windowID": next.windowID
        ])
        needsLayout = true
    }
    private func nextHorizontalMasonrySelection(
        from current: QuickSwitchSelection,
        direction: QuickSwitchKeyboardDirection
    ) -> QuickSwitchSelection? {
        layoutSubtreeIfNeeded()
        let cards = horizontalMasonryCardsInPlacementOrder()
        guard let currentIndex = cards.firstIndex(where: { $0.item.window.id == current.windowID }) else {
            return cards.first.map(selectionForCard)
        }
        switch direction {
        case .right:
            return selectionForCard(cards[(currentIndex + 1) % cards.count])
        case .left:
            return selectionForCard(cards[(currentIndex - 1 + cards.count) % cards.count])
        case .up:
            return nearestHorizontalMasonrySelection(from: cards[currentIndex], direction: .up)
                ?? selectionForCard(cards.first ?? cards[currentIndex])
        case .down:
            return nearestHorizontalMasonrySelection(from: cards[currentIndex], direction: .down)
                ?? selectionForCard(cards.last ?? cards[currentIndex])
        }
    }
    private func nearestHorizontalMasonrySelection(
        from currentCard: WaterfallCardLayers,
        direction: QuickSwitchKeyboardDirection
    ) -> QuickSwitchSelection? {
        let currentFrame = currentCard.containerLayer.frame
        let candidates = horizontalMasonryCardsInPlacementOrder().filter { card in
            guard card.item.window.id != currentCard.item.window.id else { return false }
            switch direction {
            case .up:
                return card.containerLayer.frame.minY >= currentFrame.maxY - 0.5
            case .down:
                return card.containerLayer.frame.maxY <= currentFrame.minY + 0.5
            case .left, .right:
                return false
            }
        }
        let best = candidates.min { lhs, rhs in
            let lhsFrame = lhs.containerLayer.frame
            let rhsFrame = rhs.containerLayer.frame
            let lhsPrimaryDistance = direction == .up
                ? lhsFrame.minY - currentFrame.maxY
                : currentFrame.minY - lhsFrame.maxY
            let rhsPrimaryDistance = direction == .up
                ? rhsFrame.minY - currentFrame.maxY
                : currentFrame.minY - rhsFrame.maxY
            if abs(lhsPrimaryDistance - rhsPrimaryDistance) > 0.5 {
                return lhsPrimaryDistance < rhsPrimaryDistance
            }
            let lhsHorizontalDistance = abs(lhsFrame.midX - currentFrame.midX)
            let rhsHorizontalDistance = abs(rhsFrame.midX - currentFrame.midX)
            if abs(lhsHorizontalDistance - rhsHorizontalDistance) > 0.5 {
                return lhsHorizontalDistance < rhsHorizontalDistance
            }
            return lhs.item.globalIndex < rhs.item.globalIndex
        }
        return best.map(selectionForCard)
    }
    private func horizontalMasonryCardsInPlacementOrder() -> [WaterfallCardLayers] {
        waterfallColumns
            .sorted { $0.column.appGroupIndex < $1.column.appGroupIndex }
            .flatMap { column in
                column.cards.sorted { $0.item.windowIndex < $1.item.windowIndex }
            }
    }
    private func selectionForCard(_ card: WaterfallCardLayers) -> QuickSwitchSelection {
        QuickSwitchSelection(
            appGroupIndex: card.item.appGroupIndex,
            windowIndex: card.item.windowIndex,
            windowID: card.item.window.id
        )
    }
    private func commitCurrentSelection() {
@@ -1315,9 +1823,24 @@
        _ selection: QuickSwitchSelection,
        horizontalIntent: WaterfallHorizontalIntent = .reveal
    ) {
        if isHorizontalMasonryMode {
            switch horizontalIntent {
            case .reveal:
                ensureHorizontalMasonrySelectionVisible(selection, animated: false)
            case .alignWithAppShelf(let animated):
                ensureHorizontalMasonrySelectionVisible(selection, animated: animated)
            }
            return
        }
        guard
            let column = waterfallColumns.first(where: { $0.column.appGroupIndex == selection.appGroupIndex })
        else {
            return
        }
        if waterfallShouldKeepFilteredColumnsCentered {
            ensureWaterfallCardVisibleVertically(selection)
            return
        }
@@ -1331,6 +1854,11 @@
    }
    private func ensureWaterfallCardVisibleVertically(_ selection: QuickSwitchSelection) {
        guard !isHorizontalMasonryMode else {
            ensureHorizontalMasonrySelectionVisible(selection, animated: false)
            return
        }
        let visibleHeight = max(
            1,
            waterfallLayer.bounds.height
@@ -1358,6 +1886,11 @@
    }
    private func alignWaterfallColumnWithAppShelfIcon(appGroupIndex: Int, animated: Bool) {
        if isHorizontalMasonryMode {
            ensureHorizontalMasonryAppVisible(appGroupIndex, animated: animated)
            return
        }
        guard
            let item = appShelfItems.first(where: { $0.item.appGroupIndex == appGroupIndex }),
            let column = waterfallColumns.first(where: { $0.column.appGroupIndex == appGroupIndex })
@@ -1384,6 +1917,11 @@
    }
    private func ensureWaterfallColumnVisible(_ appGroupIndex: Int) {
        if isHorizontalMasonryMode {
            ensureHorizontalMasonryAppVisible(appGroupIndex, animated: false)
            return
        }
        guard let column = waterfallColumns.first(where: { $0.column.appGroupIndex == appGroupIndex }) else {
            return
        }
@@ -1392,6 +1930,11 @@
    }
    private func ensureWaterfallColumnVisible(_ column: WaterfallColumnLayers) {
        if isHorizontalMasonryMode {
            ensureHorizontalMasonryAppVisible(column.column.appGroupIndex, animated: false)
            return
        }
        waterfallScrollOffset = clampedWaterfallOffset(
            scrollOffset(
                makingVisible: column.containerLayer.frame,
@@ -1404,6 +1947,14 @@
    }
    private var waterfallAlignmentScrollRange: ClosedRange<CGFloat> {
        if isHorizontalMasonryMode {
            return 0...0
        }
        if waterfallShouldKeepFilteredColumnsCentered {
            return 0...0
        }
        var minimumOffset: CGFloat = 0
        var maximumOffset = waterfallMaxScrollOffset
@@ -1428,6 +1979,10 @@
    private func clampedWaterfallAlignmentOffset(_ offset: CGFloat) -> CGFloat {
        let range = waterfallAlignmentScrollRange
        return min(max(range.lowerBound, offset), range.upperBound)
    }
    private var waterfallShouldKeepFilteredColumnsCentered: Bool {
        currentViewModel?.lockedSpaceID != nil && waterfallMaxScrollOffset <= 0.5
    }
    private func setWaterfallScrollOffset(_ offset: CGFloat, animated: Bool) {
@@ -1462,6 +2017,80 @@
        animation.timingFunction = CAMediaTimingFunction(name: WaterfallAlignmentMetrics.animationTimingName)
        animation.isRemovedOnCompletion = true
        waterfallContentLayer.add(animation, forKey: "waterfallColumnAlignment")
    }
    private func ensureHorizontalMasonrySelectionVisible(
        _ selection: QuickSwitchSelection,
        animated: Bool
    ) {
        guard let card = waterfallCard(
            appGroupIndex: selection.appGroupIndex,
            windowIndex: selection.windowIndex
        ) else {
            return
        }
        let visibleFrame = card.containerLayer.frame.offsetBy(
            dx: 0,
            dy: waterfallContentLayer.frame.minY
        )
        let visibleHeight = waterfallLayer.bounds.height
        let padding = WaterfallMetrics.revealPadding
        var nextOffset = horizontalMasonryScrollOffset
        if visibleFrame.maxY > visibleHeight - padding {
            nextOffset -= visibleFrame.maxY - (visibleHeight - padding)
        } else if visibleFrame.minY < padding {
            nextOffset += padding - visibleFrame.minY
        }
        setHorizontalMasonryScrollOffset(nextOffset, animated: animated)
    }
    private func ensureHorizontalMasonryAppVisible(_ appGroupIndex: Int, animated: Bool) {
        guard let firstCard = waterfallColumns
            .first(where: { $0.column.appGroupIndex == appGroupIndex })?
            .cards
            .sorted(by: { $0.item.windowIndex < $1.item.windowIndex })
            .first
        else {
            return
        }
        let cardTopDistance = horizontalMasonryContentHeight - firstCard.containerLayer.frame.maxY
        setHorizontalMasonryScrollOffset(
            cardTopDistance - WaterfallMetrics.revealPadding,
            animated: animated
        )
    }
    private func setHorizontalMasonryScrollOffset(_ offset: CGFloat, animated: Bool) {
        let nextOffset = clampedHorizontalMasonryOffset(offset)
        let previousPositionY = waterfallContentLayer.presentation()?.position.y
            ?? waterfallContentLayer.position.y
        horizontalMasonryScrollOffset = nextOffset
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        waterfallContentLayer.frame = horizontalMasonryContentFrame()
        CATransaction.commit()
        let nextPositionY = waterfallContentLayer.position.y
        guard animated,
              !NSWorkspace.shared.accessibilityDisplayShouldReduceMotion,
              abs(previousPositionY - nextPositionY) > 0.5
        else {
            waterfallContentLayer.removeAnimation(forKey: "horizontalMasonryAlignment")
            return
        }
        let animation = CABasicAnimation(keyPath: "position.y")
        animation.fromValue = previousPositionY
        animation.toValue = nextPositionY
        animation.duration = WaterfallAlignmentMetrics.animationDuration
        animation.timingFunction = CAMediaTimingFunction(name: WaterfallAlignmentMetrics.animationTimingName)
        animation.isRemovedOnCompletion = true
        waterfallContentLayer.add(animation, forKey: "horizontalMasonryAlignment")
    }
    private func scrollOffset(
@@ -2117,6 +2746,7 @@
            segment.space.id == activeSpaceFocusID
                || appHoverAssociatedSpaceIDs.contains(segment.space.id)
                || segment.space.id == windowHoverAssociatedSpaceID
                || segment.space.id == currentViewModel?.lockedSpaceID
        }
        let hasCurrentSegment = group.display.spaces.contains { $0.isCurrent }
        group.containerLayer.backgroundColor = NSColor.windowBackgroundColor.cgColor
@@ -2132,16 +2762,19 @@
    private func applySpaceLaneVisualState(_ segment: SpaceLaneSegmentLayers) {
        let isHovered = segment.space.id == activeSpaceFocusID
        let isFocused = segment.space.id == activeSpaceFocusID
        let isLocked = segment.space.id == currentViewModel?.lockedSpaceID
        let isCurrent = segment.space.isCurrent
        let hasWindows = segment.space.windowCount > 0
        let isAppAssociated = appHoverAssociatedSpaceIDs.contains(segment.space.id)
        let isWindowAssociated = segment.space.id == windowHoverAssociatedSpaceID
        let isActive = isHovered || isFocused
        let isActive = isHovered || isFocused || isLocked
        let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive
        segment.containerLayer.borderWidth = (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1
        segment.containerLayer.borderWidth = isLocked
            ? 6
            : (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1
        segment.containerLayer.borderColor = (isActive
            ? spaceLaneOccupiedColor.withAlphaComponent(0.62)
            ? spaceLaneOccupiedColor.withAlphaComponent(isLocked ? 0.92 : 0.62)
            : isAssociated
                ? spaceLaneOccupiedColor.withAlphaComponent(0.50)
                : hasWindows
@@ -2150,7 +2783,9 @@
                    ? NSColor.labelColor.withAlphaComponent(0.34)
                : NSColor.separatorColor.withAlphaComponent(0.34)
        ).cgColor
        if isActive {
        if !hasWindows {
            applySpaceLaneSolidFill(to: segment.containerLayer, color: .clear)
        } else if isActive {
            applySpaceLaneSolidFill(
                to: segment.containerLayer,
                color: accentTintedWindowBackground(fraction: 0.10, alpha: 0.94)
@@ -2165,25 +2800,32 @@
                to: segment.containerLayer,
                colors: spaceLaneIdleOccupiedGradientColors()
            )
        } else {
            applySpaceLaneSolidFill(to: segment.containerLayer, color: .clear)
        }
        segment.containerLayer.shadowOpacity = isActive ? 0.26 : isAssociated ? 0.18 : isCurrent ? 0.14 : 0
        segment.containerLayer.shadowRadius = isActive ? 24 : isAssociated ? 18 : isCurrent ? 12 : 0
        segment.containerLayer.shadowColor = isLocked
            ? NSColor.white.withAlphaComponent(0.98).cgColor
            : NSColor.black.cgColor
        segment.containerLayer.shadowOpacity = (hasWindows || isLocked)
            ? (isLocked ? 0.72 : isActive ? 0.26 : isAssociated ? 0.18 : isCurrent ? 0.14 : 0)
            : 0
        segment.containerLayer.shadowRadius = (hasWindows || isLocked)
            ? (isLocked ? 30 : isActive ? 24 : isAssociated ? 18 : isCurrent ? 12 : 0)
            : 0
        segment.containerLayer.shadowOffset = CGSize(
            width: 0,
            height: isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0
            height: (hasWindows || isLocked) ? (isLocked ? 0 : isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0) : 0
        )
        segment.containerLayer.zPosition = isActive ? 30 : isAssociated ? 18 : isCurrent ? 10 : 0
        segment.containerLayer.zPosition = isLocked ? 42 : isActive ? 30 : isAssociated ? 18 : isCurrent ? 10 : 0
        segment.containerLayer.transform = (isActive || isAssociated)
            ? CATransform3DMakeScale(isActive ? 1.03 : 1.015, isActive ? 1.03 : 1.015, 1)
            : CATransform3DIdentity
        segment.containerLayer.shadowPath = CGPath(
            roundedRect: segment.containerLayer.bounds,
            cornerWidth: segment.containerLayer.cornerRadius,
            cornerHeight: segment.containerLayer.cornerRadius,
            transform: nil
        )
        segment.containerLayer.shadowPath = hasWindows
            ? CGPath(
                roundedRect: segment.containerLayer.bounds,
                cornerWidth: segment.containerLayer.cornerRadius,
                cornerHeight: segment.containerLayer.cornerRadius,
                transform: nil
            )
            : nil
    }
    private func shouldScrollSpaceLane(for event: NSEvent) -> Bool {
@@ -2569,6 +3211,10 @@
            return "none"
        }
        guard !isHorizontalMasonryMode else {
            return "visible"
        }
        let visibleMinX = waterfallScrollOffset
        let visibleMaxX = waterfallScrollOffset + waterfallLayer.bounds.width
        if column.containerLayer.frame.maxX < visibleMinX + WaterfallMetrics.gap {
@@ -2651,17 +3297,24 @@
    }
    private func layoutWaterfallColumns() {
        if isHorizontalMasonryMode {
            layoutHorizontalMasonryWaterfall()
            return
        }
        guard !waterfallColumns.isEmpty else {
            waterfallContentWidth = waterfallLayer.bounds.width
            waterfallScrollOffset = 0
            horizontalMasonryContentHeight = waterfallLayer.bounds.height
            horizontalMasonryScrollOffset = 0
            waterfallContentLayer.frame = waterfallLayer.bounds
            return
        }
        let columnWidth = waterfallColumnWidth()
        let totalWidth = WaterfallMetrics.contentHorizontalPadding * 2
            + CGFloat(waterfallColumns.count) * columnWidth
        let columnsWidth = CGFloat(waterfallColumns.count) * columnWidth
            + CGFloat(max(0, waterfallColumns.count - 1)) * WaterfallMetrics.gap
        let totalWidth = WaterfallMetrics.contentHorizontalPadding * 2 + columnsWidth
        waterfallContentWidth = max(waterfallLayer.bounds.width, totalWidth)
        waterfallScrollOffset = clampedWaterfallAlignmentOffset(waterfallScrollOffset)
        waterfallContentLayer.frame = CGRect(
@@ -2671,7 +3324,7 @@
            height: waterfallLayer.bounds.height
        )
        var x = WaterfallMetrics.contentHorizontalPadding
        var x = waterfallColumnStartX(columnsWidth: columnsWidth)
        for column in waterfallColumns {
            column.containerLayer.frame = CGRect(
                x: x,
@@ -2684,7 +3337,215 @@
        }
    }
    private func layoutHorizontalMasonryWaterfall() {
        guard !waterfallColumns.isEmpty else {
            waterfallContentWidth = waterfallLayer.bounds.width
            waterfallScrollOffset = 0
            horizontalMasonryContentHeight = waterfallLayer.bounds.height
            horizontalMasonryScrollOffset = 0
            waterfallContentLayer.frame = waterfallLayer.bounds
            return
        }
        let layout = horizontalMasonryLayout()
        waterfallContentWidth = waterfallLayer.bounds.width
        waterfallScrollOffset = 0
        horizontalMasonryContentHeight = layout.contentHeight
        horizontalMasonryScrollOffset = clampedHorizontalMasonryOffset(horizontalMasonryScrollOffset)
        waterfallContentLayer.frame = horizontalMasonryContentFrame()
        for column in waterfallColumns {
            column.containerLayer.frame = CGRect(
                x: 0,
                y: 0,
                width: waterfallContentWidth,
                height: horizontalMasonryContentHeight
            )
            column.containerLayer.masksToBounds = false
            column.containerLayer.backgroundColor = NSColor.clear.cgColor
            column.containerLayer.borderColor = NSColor.clear.cgColor
            column.containerLayer.borderWidth = 0
            column.containerLayer.shadowOpacity = 0
            column.containerLayer.shadowRadius = 0
            column.containerLayer.shadowOffset = .zero
            column.headerLayer.isHidden = true
            column.appNameLayer.isHidden = true
            column.countLayer.isHidden = true
            for card in column.cards {
                if let frame = layout.cardFramesByWindowID[card.item.window.id] {
                    card.containerLayer.frame = frame
                } else {
                    card.containerLayer.frame = .zero
                }
                layoutWaterfallCard(card)
            }
        }
    }
    private struct HorizontalMasonryLayout {
        let contentHeight: CGFloat
        let cardFramesByWindowID: [UInt32: CGRect]
    }
    private func horizontalMasonryLayout() -> HorizontalMasonryLayout {
        let cards = horizontalMasonryCardsInPlacementOrder()
        guard !cards.isEmpty else {
            return HorizontalMasonryLayout(
                contentHeight: waterfallLayer.bounds.height,
                cardFramesByWindowID: [:]
            )
        }
        let viewportWidth = max(1, waterfallLayer.bounds.width)
        let availableWidth = max(
            HorizontalMasonryMetrics.cardMinWidth,
            viewportWidth - HorizontalMasonryMetrics.horizontalPadding * 2
        )
        let maximumLaneCount = max(
            1,
            Int(
                floor(
                    (availableWidth + HorizontalMasonryMetrics.cardGap)
                        / (HorizontalMasonryMetrics.cardMinWidth + HorizontalMasonryMetrics.cardGap)
                )
            )
        )
        let laneCount = min(maximumLaneCount, max(1, cards.count))
        let fittingWidth = (
            availableWidth - CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.cardGap
        ) / CGFloat(laneCount)
        let cardWidth = floor(
            min(
                HorizontalMasonryMetrics.cardMaxWidth,
                max(HorizontalMasonryMetrics.cardMinWidth, fittingWidth)
            )
        )
        let usedWidth = CGFloat(laneCount) * cardWidth
            + CGFloat(max(0, laneCount - 1)) * HorizontalMasonryMetrics.cardGap
        let startX = max(
            HorizontalMasonryMetrics.horizontalPadding,
            (viewportWidth - usedWidth) / 2
        )
        var laneHeights = Array(repeating: CGFloat(0), count: laneCount)
        var plannedFrames: [(windowID: UInt32, x: CGFloat, top: CGFloat, width: CGFloat, height: CGFloat)] = []
        for card in cards {
            let laneIndex = laneHeights.enumerated().min { lhs, rhs in
                if abs(lhs.element - rhs.element) > 0.5 {
                    return lhs.element < rhs.element
                }
                return lhs.offset < rhs.offset
            }?.offset ?? 0
            let cardHeight = horizontalMasonryCardHeight(
                for: card,
                width: cardWidth,
                viewportHeight: waterfallLayer.bounds.height
            )
            let x = startX + CGFloat(laneIndex) * (cardWidth + HorizontalMasonryMetrics.cardGap)
            let top = HorizontalMasonryMetrics.verticalPadding + laneHeights[laneIndex]
            plannedFrames.append((
                windowID: card.item.window.id,
                x: x,
                top: top,
                width: cardWidth,
                height: cardHeight
            ))
            laneHeights[laneIndex] += cardHeight + HorizontalMasonryMetrics.cardGap
        }
        let tallestLane = laneHeights.max().map {
            max(0, $0 - HorizontalMasonryMetrics.cardGap)
        } ?? 0
        let contentHeight = max(
            waterfallLayer.bounds.height,
            HorizontalMasonryMetrics.verticalPadding * 2 + tallestLane
        )
        let frames = Dictionary(
            uniqueKeysWithValues: plannedFrames.map { planned in
                (
                    planned.windowID,
                    CGRect(
                        x: planned.x,
                        y: contentHeight - planned.top - planned.height,
                        width: planned.width,
                        height: planned.height
                    )
                )
            }
        )
        return HorizontalMasonryLayout(contentHeight: contentHeight, cardFramesByWindowID: frames)
    }
    private func horizontalMasonryCardHeight(
        for card: WaterfallCardLayers,
        width: CGFloat,
        viewportHeight: CGFloat
    ) -> CGFloat {
        let thumbnailHeight = min(
            max(
                HorizontalMasonryMetrics.minThumbnailHeight,
                width / horizontalMasonryThumbnailAspectRatio(for: card)
            ),
            max(1, viewportHeight * HorizontalMasonryMetrics.maxThumbnailHeightFraction)
        )
        return HorizontalMasonryMetrics.titleBarHeight
            + HorizontalMasonryMetrics.titleGap
            + thumbnailHeight
    }
    private func horizontalMasonryThumbnailAspectRatio(for card: WaterfallCardLayers) -> CGFloat {
        if card.item.window.isFullscreen {
            return 16.0 / 9.0
        }
        if card.item.window.isMinimized {
            return 4.0 / 3.0
        }
        switch Int(card.item.window.id % 4) {
        case 0:
            return 16.0 / 9.0
        case 1:
            return 3.0 / 2.0
        case 2:
            return 4.0 / 3.0
        default:
            return 5.0 / 4.0
        }
    }
    private func horizontalMasonryContentFrame() -> CGRect {
        let contentHeight = max(waterfallLayer.bounds.height, horizontalMasonryContentHeight)
        return CGRect(
            x: 0,
            y: waterfallLayer.bounds.height - contentHeight + horizontalMasonryScrollOffset,
            width: waterfallLayer.bounds.width,
            height: contentHeight
        )
    }
    private func waterfallColumnStartX(columnsWidth: CGFloat) -> CGFloat {
        guard currentViewModel?.lockedSpaceID != nil else {
            return WaterfallMetrics.contentHorizontalPadding
        }
        let centeredStart = (waterfallContentWidth - columnsWidth) / 2
        return max(WaterfallMetrics.contentHorizontalPadding, centeredStart)
    }
    private func layoutWaterfallColumn(_ column: WaterfallColumnLayers) {
        column.containerLayer.cornerRadius = 14
        column.containerLayer.masksToBounds = true
        column.containerLayer.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.56).cgColor
        column.containerLayer.shadowOpacity = 0
        column.containerLayer.shadowRadius = 0
        column.containerLayer.shadowOffset = .zero
        column.headerLayer.isHidden = false
        column.appNameLayer.isHidden = false
        column.countLayer.isHidden = false
        let bounds = column.containerLayer.bounds
        let isSpaceFocused = spaceFocusWindowCount(for: column.column.appGroupIndex) > 0
        let isHovered = column.column.appGroupIndex == effectiveHoveredAppGroupIndex
@@ -3405,15 +4266,30 @@
        )
    }
    private var isHorizontalMasonryMode: Bool {
        waterfallViewMode == .horizontalMasonry
    }
    private var waterfallMaxScrollOffset: CGFloat {
        max(0, waterfallContentWidth - waterfallLayer.bounds.width)
        guard !isHorizontalMasonryMode else { return 0 }
        return max(0, waterfallContentWidth - waterfallLayer.bounds.width)
    }
    private func clampedWaterfallOffset(_ offset: CGFloat) -> CGFloat {
        min(max(0, offset), waterfallMaxScrollOffset)
    }
    private var horizontalMasonryMaxScrollOffset: CGFloat {
        max(0, horizontalMasonryContentHeight - waterfallLayer.bounds.height)
    }
    private func clampedHorizontalMasonryOffset(_ offset: CGFloat) -> CGFloat {
        min(max(0, offset), horizontalMasonryMaxScrollOffset)
    }
    private func waterfallMaxVerticalScrollOffset(for appGroupIndex: Int) -> CGFloat {
        guard !isHorizontalMasonryMode else { return 0 }
        guard
            let column = waterfallColumns.first(where: { $0.column.appGroupIndex == appGroupIndex })
        else {
@@ -4116,8 +4992,13 @@
                    "gradientColors": gradientColorDictionaries(from: segment.containerLayer),
                    "borderColor": colorDictionary(from: segment.containerLayer.borderColor),
                    "borderWidth": Double(segment.containerLayer.borderWidth),
                    "shadowColor": colorDictionary(from: segment.containerLayer.shadowColor),
                    "shadowOpacity": Double(segment.containerLayer.shadowOpacity),
                    "shadowRadius": Double(segment.containerLayer.shadowRadius),
                    "shadowOffset": [
                        "width": Double(segment.containerLayer.shadowOffset.width),
                        "height": Double(segment.containerLayer.shadowOffset.height)
                    ],
                    "appFrame": dictionary(from: segment.appLayer.frame),
                    "frame": dictionary(from: segment.containerLayer.frame),
                    "visibleFrame": dictionary(from: visibleFrame),
@@ -4157,6 +5038,9 @@
        if space.id == activeSpaceFocusID {
            states.append("focused")
        }
        if space.id == currentViewModel?.lockedSpaceID {
            states.append("locked")
        }
        if space.type == .fullscreen {
            states.append("fullscreen")
        }