local addonName, ns = ...
local L = ns.L or {}

local ASSET_ROOT = "Interface\\AddOns\\" .. tostring(addonName or "Vaultloom") .. "\\Assets\\"
local TRAVEL_BAR_COLORS = {
    frameBackdrop = { 0.01, 0.01, 0.02, 0.92 },
    frameBorder = { 0.40, 0.29, 0.12, 0.78 },
    frameInset = { 0.04, 0.04, 0.05, 0.90 },
    accentGold = { 0.94, 0.78, 0.26, 0.92 },
    accentGoldSoft = { 0.86, 0.70, 0.24, 0.44 },
    accentRed = { 0.74, 0.16, 0.18, 0.88 },
    buttonBackdrop = { 0.05, 0.05, 0.06, 0.94 },
    buttonBorder = { 0.22, 0.22, 0.24, 0.88 },
    inactiveRing = { 0.58, 0.56, 0.52, 0.98 },
    activeRing = { 0.96, 0.80, 0.30, 0.98 },
    pendingRing = { 0.88, 0.24, 0.24, 0.96 },
    hoverRing = { 1.00, 0.92, 0.70, 0.72 },
    menuText = { 0.94, 0.90, 0.84, 0.98 },
    menuTextMuted = { 0.74, 0.74, 0.78, 0.88 },
}
local TRAVEL_BAR_RING_TEXTURE = ASSET_ROOT .. "dock_ring_clean.tga"

local function registerTravelBarFeature()
local TRAVEL_BAR_SCALE_MIN = 0.70
local TRAVEL_BAR_SCALE_MAX = 1.45
local TRAVEL_BAR_SCALE_STEP = 0.05
local TRAVEL_BAR_POSITION_KEY = "travel_bar_position"
local TRAVEL_BAR_SCALE_KEY = "travel_bar_scale"
local TRAVEL_BAR_DEFAULT_POSITION = {
    point = "TOP",
    relativePoint = "TOP",
    x = 0,
    y = -262,
}
local TRAVEL_BAR_BUTTON_SIZE = 36
local TRAVEL_BAR_BUTTON_GAP = 6
local TRAVEL_BAR_FRAME_PADDING_X = 7
local TRAVEL_BAR_FRAME_PADDING_Y = 7
local TRAVEL_BAR_COOLDOWN_REFRESH_SECONDS = 1.00
local TRAVEL_BAR_RANDOM_HEARTH_TOY_IDS = {
    184353,
    183716,
    180290,
    182773,
    54452,
    64488,
    93672,
    142542,
    162973,
    163045,
    165669,
    165670,
    165802,
    166746,
    166747,
    168907,
    172179,
    193588,
    188952,
    200630,
    190237,
    190196,
    209035,
    208704,
    206195,
    212337,
    210455,
    228940,
    235016,
    236687,
    245970,
    246565,
    263489,
    257736,
    265100,
    263933,
}
local TRAVEL_BAR_SPECIAL_TRAVEL_ITEM_IDS = {
    132119,
    132120,
    95567,
    95568,
    112059,
    132517,
    48933,
    87215,
    151652,
    168807,
    168808,
    172924,
    198156,
    221965,
    221966,
    248485,
}
local TRAVEL_BAR_CLASS_SPELLS = {
    DRUID = 193753,
    MONK = 126892,
    DEATHKNIGHT = 50977,
    SHAMAN = 556,
    MAGE = 193759,
}
local TRAVEL_BAR_COVENANT_REQUIREMENTS = {
    [184353] = 1,
    [183716] = 2,
    [180290] = 3,
    [182773] = 4,
}
local TRAVEL_BAR_HOUSING_ICON = "Interface\\AddOns\\Vaultloom\\Assets\\travel_bar_housing_icon.tga"
local TravelBarRuntime = {
    enabled = false,
    buttons = {},
    pendingRefresh = false,
    refreshQueued = false,
    currentHearthKey = nil,
    currentHearthAction = nil,
    state = nil,
}

local function getTravelBarBagItemCount(itemID)
    if not itemID or type(GetItemCount) ~= "function" then
        return 0
    end

    local ok, count = pcall(GetItemCount, itemID, false, false)
    if not ok then
        return 0
    end

    return tonumber(count) or 0
end

local function isTravelBarHearthToyAllowed(itemID)
    if not itemID or type(PlayerHasToy) ~= "function" or not PlayerHasToy(itemID) then
        return false
    end

    local covenantID = TRAVEL_BAR_COVENANT_REQUIREMENTS[itemID]
    if covenantID then
        local activeCovenantID = C_Covenants and type(C_Covenants.GetActiveCovenantID) == "function" and C_Covenants.GetActiveCovenantID() or nil
        if activeCovenantID ~= covenantID then
            local covenantMastered = type(GetAchievementInfo) == "function" and select(4, GetAchievementInfo(15241)) == true or false
            if not covenantMastered then
                return false
            end
        end
    end

    if itemID == 210455 then
        local _, _, raceID = UnitRace("player")
        if not (raceID == 11 or raceID == 30) then
            return false
        end
    end

    return true
end

local function getTravelBarToyInfo(itemID)
    if C_ToyBox and type(C_ToyBox.GetToyInfo) == "function" then
        local name, icon = C_ToyBox.GetToyInfo(itemID)
        if type(name) == "string" and name ~= "" then
            return name, icon
        end
    end

    local name, _, _, _, _, _, _, _, _, icon = GetItemInfo(itemID)
    return name, icon
end

local function getTravelBarItemInfo(itemID)
    local name, link, _, _, _, _, _, _, _, icon = GetItemInfo(itemID)
    if type(name) == "string" and name ~= "" then
        return name, icon, link
    end

    local fallbackIcon = nil
    if C_Item and type(C_Item.GetItemIconByID) == "function" then
        fallbackIcon = C_Item.GetItemIconByID(itemID)
    elseif type(GetItemIcon) == "function" then
        fallbackIcon = GetItemIcon(itemID)
    end

    return nil, fallbackIcon, nil
end

local function getTravelBarSpellInfo(spellID)
    if C_Spell and type(C_Spell.GetSpellInfo) == "function" then
        local spellInfo = C_Spell.GetSpellInfo(spellID)
        if type(spellInfo) == "table" and type(spellInfo.name) == "string" and spellInfo.name ~= "" then
            return spellInfo.name, spellInfo.iconID
        end
    end

    if type(GetSpellInfo) == "function" then
        local name, _, icon = GetSpellInfo(spellID)
        return name, icon
    end

    return nil, nil
end

local function cleanTravelBarTooltipText(text)
    if type(text) ~= "string" or text == "" then
        return nil
    end

    local cleanText = text
        :gsub("|A:[^|]+|a", "")
        :gsub("|c%x%x%x%x%x%x%x%x", "")
        :gsub("|r", "")
        :gsub("%s+", " ")
        :gsub("^%s+", "")
        :gsub("%s+$", "")

    if cleanText == "" then
        return nil
    end

    return cleanText
end

local function getTravelBarTooltipDataForAction(action)
    if type(action) ~= "table" or type(C_TooltipInfo) ~= "table" then
        return nil
    end

    local tooltipData = nil
    if action.kind == "spell" and action.spellID and type(C_TooltipInfo.GetSpellByID) == "function" then
        local ok, result = pcall(C_TooltipInfo.GetSpellByID, action.spellID)
        tooltipData = ok and result or nil
    elseif action.itemID and type(C_TooltipInfo.GetItemByID) == "function" then
        local ok, result = pcall(C_TooltipInfo.GetItemByID, action.itemID)
        tooltipData = ok and result or nil
    end

    if tooltipData and type(TooltipUtil) == "table" and type(TooltipUtil.SurfaceArgs) == "function" then
        TooltipUtil.SurfaceArgs(tooltipData)
    end

    return tooltipData
end

local function getTravelBarTooltipDescriptionFromLines(lines, action)
    if type(lines) ~= "table" then
        return nil
    end

    local titleText = cleanTravelBarTooltipText(action and action.label or nil)
    local fallbackText = nil
    for _, lineData in ipairs(lines) do
        local text = cleanTravelBarTooltipText(lineData and lineData.leftText or nil)
        if text and text ~= titleText then
            local lowerText = text:lower()
            if lowerText:find("teleport")
                or lowerText:find("portal")
                or lowerText:find("return")
                or lowerText:find("hearth")
                or lowerText:find("returns you")
                or lowerText:find("ruhestein")
                or lowerText:find("teleportiert")
                or lowerText:find("bringt dich")
                or lowerText:find("zurück")
            then
                return text
            end

            if not lowerText:find("^cooldown")
                and not lowerText:find("^abklingzeit")
                and not lowerText:find("^cast time")
                and not lowerText:find("^zauberzeit")
                and not lowerText:find("^range")
                and not lowerText:find("^reichweite")
                and not lowerText:find("^toy")
                and not lowerText:find("^spielzeug")
                and not lowerText:find("^requires")
                and not lowerText:find("^benötigt")
            then
                fallbackText = fallbackText or text
            end
        end
    end

    return fallbackText
end

local function getTravelBarHomeDescription()
    local bindLocation = type(GetBindLocation) == "function" and GetBindLocation() or nil
    if type(bindLocation) == "string" and bindLocation ~= "" then
        return string.format(
            L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_HOME or "Teleports to your hearth location: %s",
            bindLocation
        )
    end

    return L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_HOME_GENERIC or "Teleports to your hearth location."
end

local function getTravelBarActionDescription(action)
    if type(action) ~= "table" then
        return nil
    end

    if type(action.tooltipDescription) == "string" and action.tooltipDescription ~= "" then
        return action.tooltipDescription
    end

    local tooltipData = getTravelBarTooltipDataForAction(action)
    local description = getTravelBarTooltipDescriptionFromLines(tooltipData and tooltipData.lines or nil, action)
    if description then
        return description
    end

    if action.key == "hearthstone" or (type(action.key) == "string" and action.key:find("^hearth_")) then
        return getTravelBarHomeDescription()
    end

    return nil
end

local function createTravelBarToyAction(itemID, key)
    local name, icon = getTravelBarToyInfo(itemID)
    if itemID == nil then
        return nil
    end

    return {
        key = key or ("toy_" .. tostring(itemID)),
        kind = "toy",
        itemID = itemID,
        token = name or tostring(itemID),
        name = name or ("item:" .. tostring(itemID)),
        label = name or ("item:" .. tostring(itemID)),
        icon = icon or 134414,
    }
end

local function createTravelBarItemAction(itemID, key)
    if itemID == nil or getTravelBarBagItemCount(itemID) <= 0 then
        return nil
    end

    local name, icon, link = getTravelBarItemInfo(itemID)
    return {
        key = key or ("item_" .. tostring(itemID)),
        kind = "item",
        itemID = itemID,
        token = link or ("item:" .. tostring(itemID)),
        name = name or ("item:" .. tostring(itemID)),
        label = name or ("item:" .. tostring(itemID)),
        icon = icon or 134414,
    }
end

local function createTravelBarItemOrToyAction(itemID, key)
    if itemID == nil then
        return nil
    end

    if type(PlayerHasToy) == "function" and PlayerHasToy(itemID) then
        return createTravelBarToyAction(itemID, key)
    end

    return createTravelBarItemAction(itemID, key)
end

local function createTravelBarSpellAction(spellID, key)
    if spellID == nil then
        return nil
    end

    local isKnown = false
    if type(IsPlayerSpell) == "function" then
        isKnown = IsPlayerSpell(spellID) == true
    end
    if not isKnown and type(IsSpellKnown) == "function" then
        isKnown = IsSpellKnown(spellID) == true
    end
    if not isKnown then
        return nil
    end

    local name, icon = getTravelBarSpellInfo(spellID)
    if type(name) ~= "string" or name == "" then
        return nil
    end

    return {
        key = key or ("spell_" .. tostring(spellID)),
        kind = "spell",
        spellID = spellID,
        token = name,
        name = name,
        label = name,
        icon = icon or 134414,
    }
end

local function createTravelBarMacroAction(macrotext, label, icon, key, cooldownKind, tooltipDescription)
    if type(macrotext) ~= "string" or macrotext == "" then
        return nil
    end

    label = (type(label) == "string" and label ~= "") and label or "Travel"

    return {
        key = key or "macro",
        kind = "macro",
        macrotext = macrotext,
        token = label,
        name = label,
        label = label,
        icon = icon or 134414,
        cooldownKind = cooldownKind,
        tooltipDescription = tooltipDescription,
    }
end

local function getTravelBarClassAction()
    local _, classTag = UnitClass("player")
    local spellID = classTag and TRAVEL_BAR_CLASS_SPELLS[classTag] or nil
    if not spellID then
        return nil
    end

    return createTravelBarSpellAction(spellID, "class_travel")
end

local function ensureTravelBarHousingTeleportButton()
    if TravelBarRuntime.housingTeleportButton then
        return TravelBarRuntime.housingTeleportButton
    end

    local button = CreateFrame("Button", "VaultloomTravelBarHousingTeleportButton", UIParent, "SecureActionButtonTemplate")
    button:SetSize(1, 1)
    button:SetPoint("BOTTOMRIGHT", UIParent, "TOPLEFT", -1, -1)
    button:RegisterForClicks("AnyDown", "AnyUp")
    button:SetAttribute("useOnKeyDown", false)
    TravelBarRuntime.housingTeleportButton = button
    return button
end

local function ensureTravelBarMenuProxyButton()
    if TravelBarRuntime.menuProxyButton then
        return TravelBarRuntime.menuProxyButton
    end

    local button = CreateFrame("Button", "VaultloomTravelBarMenuProxyButton", UIParent, "SecureActionButtonTemplate")
    button:SetSize(1, 1)
    button:SetPoint("BOTTOMRIGHT", UIParent, "TOPLEFT", -1, -1)
    button:SetAlpha(0)
    button:RegisterForClicks("AnyUp", "AnyDown")
    button:SetAttribute("useOnKeyDown", false)
    button:SetScript("OnClick", function()
        local sourceButton = TravelBarRuntime.menuRequestButton
        TravelBarRuntime.menuRequestButton = nil
        if sourceButton then
            TravelBarRuntime:ToggleMenuForButton(sourceButton)
        end
    end)
    TravelBarRuntime.menuProxyButton = button
    return button
end

local getStoredTravelBarHousingTarget
local storeTravelBarHousingTarget

local function getTravelBarHousingAction()
    if type(C_Housing) ~= "table" then
        return nil
    end

    local secureButton = ensureTravelBarHousingTeleportButton()
    if not secureButton then
        return nil
    end

    local canReturnHome = type(C_HousingNeighborhood) == "table"
        and type(C_HousingNeighborhood.CanReturnAfterVisitingHouse) == "function"
        and C_HousingNeighborhood.CanReturnAfterVisitingHouse() == true

    if canReturnHome then
        secureButton:SetAttribute("type", "returnhome")
        secureButton:SetAttribute("house-neighborhood-guid", nil)
        secureButton:SetAttribute("house-guid", nil)
        secureButton:SetAttribute("house-plot-id", nil)
        return createTravelBarMacroAction(
            "/click " .. secureButton:GetName(),
            L.HOUSING_SECTION_HOME or L.MAIN_TAB_HOUSING or "Housing",
            TRAVEL_BAR_HOUSING_ICON,
            "housing",
            "housing",
            L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_HOUSING_RETURN or "Returns you from a visited house."
        )
    end

    local houseInfoList = ns.GetHousingHouseList and ns:GetHousingHouseList() or {}
    if #houseInfoList == 0 and type(C_Housing.GetPlayerOwnedHouses) == "function" then
        local ok, liveHouseInfoList = pcall(C_Housing.GetPlayerOwnedHouses)
        if ok and type(liveHouseInfoList) == "table" then
            houseInfoList = liveHouseInfoList
            if ns.RememberHousingHouseList then
                ns:RememberHousingHouseList(liveHouseInfoList)
            end
        end
    end

    local activeNeighborhoodGUID = nil
    if type(C_NeighborhoodInitiative) == "table" and type(C_NeighborhoodInitiative.GetActiveNeighborhood) == "function" then
        local ok, neighborhoodGUID = pcall(C_NeighborhoodInitiative.GetActiveNeighborhood)
        if ok then
            activeNeighborhoodGUID = neighborhoodGUID
        end
    end

    local selectedHouse = nil
    for _, houseInfo in ipairs(houseInfoList or {}) do
        if houseInfo and houseInfo.neighborhoodGUID and houseInfo.neighborhoodGUID == activeNeighborhoodGUID then
            selectedHouse = houseInfo
            break
        end
    end
    if not selectedHouse then
        selectedHouse = houseInfoList and houseInfoList[1] or nil
    end

    if selectedHouse then
        storeTravelBarHousingTarget(selectedHouse)
    else
        local storedTarget = getStoredTravelBarHousingTarget()
        if storedTarget then
            secureButton:SetAttribute("type", "teleporthome")
            secureButton:SetAttribute("house-neighborhood-guid", storedTarget.neighborhoodGUID)
            secureButton:SetAttribute("house-guid", storedTarget.houseGUID)
            secureButton:SetAttribute("house-plot-id", storedTarget.plotID)

            return createTravelBarMacroAction(
                "/click " .. secureButton:GetName(),
                storedTarget.houseName or storedTarget.neighborhoodName or L.HOUSING_SECTION_HOME or L.MAIN_TAB_HOUSING or "Housing",
                TRAVEL_BAR_HOUSING_ICON,
                "housing",
                "housing",
                (storedTarget.houseName or storedTarget.neighborhoodName) and string.format(
                    L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_DESTINATION or "Teleports to: %s",
                    storedTarget.houseName or storedTarget.neighborhoodName
                ) or (L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_HOUSING or "Teleports to your active house.")
            )
        end
    end

    if not selectedHouse
        or not selectedHouse.neighborhoodGUID
        or not selectedHouse.houseGUID
        or selectedHouse.plotID == nil
    then
        secureButton:SetAttribute("type", nil)
        secureButton:SetAttribute("house-neighborhood-guid", nil)
        secureButton:SetAttribute("house-guid", nil)
        secureButton:SetAttribute("house-plot-id", nil)
        return nil
    end

    secureButton:SetAttribute("type", "teleporthome")
    secureButton:SetAttribute("house-neighborhood-guid", selectedHouse.neighborhoodGUID)
    secureButton:SetAttribute("house-guid", selectedHouse.houseGUID)
    secureButton:SetAttribute("house-plot-id", selectedHouse.plotID)

    return createTravelBarMacroAction(
        "/click " .. secureButton:GetName(),
        selectedHouse.houseName or L.HOUSING_SECTION_HOME or L.MAIN_TAB_HOUSING or "Housing",
        TRAVEL_BAR_HOUSING_ICON,
        "housing",
        "housing",
        selectedHouse.houseName and string.format(
            L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_DESTINATION or "Teleports to: %s",
            selectedHouse.houseName
        ) or (L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_HOUSING or "Teleports to your active house.")
    )
end

local function findTravelBarActionByKey(actions, actionKey)
    if type(actions) ~= "table" or type(actionKey) ~= "string" or actionKey == "" then
        return nil
    end

    for _, action in ipairs(actions) do
        if type(action) == "table" and action.key == actionKey then
            return action
        end
    end

    return nil
end

local function pickRandomTravelBarAction(actions, avoidKey)
    if type(actions) ~= "table" or #actions == 0 then
        return nil
    end

    if #actions == 1 then
        return actions[1]
    end

    local candidates = {}
    for _, action in ipairs(actions) do
        if type(action) == "table" and action.key ~= avoidKey then
            candidates[#candidates + 1] = action
        end
    end

    if #candidates == 0 then
        candidates = actions
    end

    return candidates[math.random(1, #candidates)]
end

local getStoredTravelBarHiddenActions
local isTravelBarActionHidden
local setTravelBarActionHidden

local function buildTravelBarState()
    local state = {
        hearths = {},
        hearthsAll = {},
        specials = {},
        specialsAll = {},
        direct = {},
        extras = {},
        main = nil,
        more = nil,
    }
    local seenKeys = {}

    local function addAction(list, action)
        if type(action) ~= "table" or type(action.key) ~= "string" or action.key == "" then
            return false
        end
        if seenKeys[action.key] then
            return false
        end

        seenKeys[action.key] = true
        list[#list + 1] = action
        return true
    end

    local function addHearthAction(action)
        if not addAction(state.hearthsAll, action) then
            return
        end

        if not isTravelBarActionHidden(action.key) then
            state.hearths[#state.hearths + 1] = action
        end
    end

    local function addSpecialAction(action)
        if not addAction(state.specialsAll, action) then
            return
        end

        if isTravelBarActionHidden(action.key) then
            return
        end

        state.specials[#state.specials + 1] = action
        if action.key == "dalaran"
            or action.key == "garrison"
            or action.key == "housing"
            or action.key == "class_travel"
        then
            state.direct[#state.direct + 1] = action
        else
            state.extras[#state.extras + 1] = action
        end
    end

    addHearthAction(createTravelBarItemAction(6948, "hearthstone"))
    for _, itemID in ipairs(TRAVEL_BAR_RANDOM_HEARTH_TOY_IDS) do
        if isTravelBarHearthToyAllowed(itemID) then
            addHearthAction(createTravelBarToyAction(itemID, "hearth_" .. tostring(itemID)))
        end
    end

    addSpecialAction(createTravelBarItemOrToyAction(140192, "dalaran"))
    addSpecialAction(createTravelBarItemOrToyAction(110560, "garrison"))
    addSpecialAction(getTravelBarHousingAction())
    addSpecialAction(getTravelBarClassAction())
    for _, itemID in ipairs(TRAVEL_BAR_SPECIAL_TRAVEL_ITEM_IDS) do
        addSpecialAction(createTravelBarItemOrToyAction(itemID, "travel_" .. tostring(itemID)))
    end

    return state
end

local function getStoredTravelBarSelectedExtraKey()
    local db = ns:EnsureFeatureDB()
    local selectedKey = type(db) == "table" and db.travel_bar_selected_extra_key or nil
    if type(selectedKey) ~= "string" or selectedKey == "" then
        return nil
    end

    return selectedKey
end

getStoredTravelBarHiddenActions = function()
    local db = ns:EnsureFeatureDB()
    if type(db) ~= "table" then
        return {}
    end

    db.travel_bar_hidden_actions = type(db.travel_bar_hidden_actions) == "table" and db.travel_bar_hidden_actions or {}
    return db.travel_bar_hidden_actions
end

isTravelBarActionHidden = function(actionKey)
    if type(actionKey) ~= "string" or actionKey == "" then
        return false
    end

    return getStoredTravelBarHiddenActions()[actionKey] == true
end

setTravelBarActionHidden = function(actionKey, hidden)
    if type(actionKey) ~= "string" or actionKey == "" then
        return
    end

    local db = ns:EnsureFeatureDB()
    if type(db) ~= "table" then
        return
    end

    db.travel_bar_hidden_actions = type(db.travel_bar_hidden_actions) == "table" and db.travel_bar_hidden_actions or {}
    if hidden then
        db.travel_bar_hidden_actions[actionKey] = true
    else
        db.travel_bar_hidden_actions[actionKey] = nil
        if next(db.travel_bar_hidden_actions) == nil then
            db.travel_bar_hidden_actions = nil
        end
    end
end

getStoredTravelBarHousingTarget = function()
    local db = ns:EnsureFeatureDB()
    local target = type(db) == "table" and type(db.travel_bar_housing_target) == "table" and db.travel_bar_housing_target or nil
    if not target then
        return nil
    end

    local plotID = tonumber(target.plotID)
    if not target.neighborhoodGUID or not target.houseGUID or plotID == nil then
        return nil
    end

    return {
        neighborhoodGUID = target.neighborhoodGUID,
        houseGUID = target.houseGUID,
        plotID = plotID,
        houseName = type(target.houseName) == "string" and target.houseName or nil,
        neighborhoodName = type(target.neighborhoodName) == "string" and target.neighborhoodName or nil,
    }
end

storeTravelBarHousingTarget = function(houseInfo)
    if type(houseInfo) ~= "table" then
        return
    end

    local plotID = tonumber(houseInfo.plotID)
    if not houseInfo.neighborhoodGUID or not houseInfo.houseGUID or plotID == nil then
        return
    end

    local db = ns:EnsureFeatureDB()
    if type(db) ~= "table" then
        return
    end

    db.travel_bar_housing_target = {
        neighborhoodGUID = houseInfo.neighborhoodGUID,
        houseGUID = houseInfo.houseGUID,
        plotID = plotID,
        houseName = houseInfo.houseName,
        neighborhoodName = houseInfo.neighborhoodName,
    }
end

local function storeTravelBarSelectedExtraKey(actionKey)
    if type(actionKey) ~= "string" or actionKey == "" then
        return
    end

    local db = ns:EnsureFeatureDB()
    if type(db) ~= "table" then
        return
    end

    db.travel_bar_selected_extra_key = actionKey
end

local function getTravelBarOrientation()
    local orientation = ns.GetFeatureOptionValue and ns:GetFeatureOptionValue("travel_bar", "orientation") or "horizontal"
    if orientation == "vertical" then
        return "vertical"
    end
    return "horizontal"
end

local function getTravelBarFrameStyle()
    local frameStyle = ns.GetFeatureOptionValue and ns:GetFeatureOptionValue("travel_bar", "frame_style") or "shown"
    if frameStyle == "hidden" then
        return "hidden"
    end
    return "shown"
end

local function getTravelBarActionCooldown(action)
    if type(action) ~= "table" then
        return 0, 0, 0, 1
    end

    if action.kind == "spell" then
        if C_Spell and type(C_Spell.GetSpellCooldown) == "function" then
            local cooldownInfo = C_Spell.GetSpellCooldown(action.spellID)
            if type(cooldownInfo) == "table" then
                return cooldownInfo.startTime or 0, cooldownInfo.duration or 0, cooldownInfo.isEnabled and 1 or 0, cooldownInfo.modRate or 1
            end
        end
        if type(GetSpellCooldown) == "function" then
            local startTime, duration, isEnabled, modRate = GetSpellCooldown(action.spellID)
            return startTime or 0, duration or 0, isEnabled or 0, modRate or 1
        end
    elseif action.cooldownKind == "housing" then
        if type(C_Housing) == "table" and type(C_Housing.GetVisitCooldownInfo) == "function" then
            local cooldownInfo = C_Housing.GetVisitCooldownInfo()
            if type(cooldownInfo) == "table" then
                return cooldownInfo.startTime or 0, cooldownInfo.duration or 0, cooldownInfo.isEnabled and 1 or 0, cooldownInfo.modRate or 1
            end
        end
    elseif action.itemID then
        if C_Container and type(C_Container.GetItemCooldown) == "function" then
            local startTime, duration, isEnabled, modRate = C_Container.GetItemCooldown(action.itemID)
            if (tonumber(isEnabled) or 0) == 1 and (tonumber(startTime) or 0) > 0 and (tonumber(duration) or 0) > 0 then
                return startTime or 0, duration or 0, isEnabled or 0, modRate or 1
            end
        end
        if C_Item and type(C_Item.GetItemCooldown) == "function" then
            local cooldownInfo = C_Item.GetItemCooldown(action.itemID)
            if type(cooldownInfo) == "table" then
                return cooldownInfo.startTime or 0, cooldownInfo.duration or 0, cooldownInfo.isEnabled and 1 or 0, cooldownInfo.modRate or 1
            end
        end
        if type(GetItemCooldown) == "function" then
            local startTime, duration, isEnabled, modRate = GetItemCooldown(action.itemID)
            return startTime or 0, duration or 0, isEnabled or 0, modRate or 1
        end
    end

    return 0, 0, 0, 1
end

local function formatTravelBarCooldownText(remaining)
    remaining = tonumber(remaining) or 0
    if remaining <= 0 then
        return nil
    end

    if remaining >= 3600 then
        return string.format("%dh", math.ceil(remaining / 3600))
    end
    if remaining >= 60 then
        return string.format("%dm", math.ceil(remaining / 60))
    end
    if remaining >= 10 then
        return tostring(math.ceil(remaining))
    end

    local text = string.format("%.1f", remaining)
    text = text:gsub("%.?0+$", "")
    return text
end

local function setTravelBarCooldownVisual(button, startTime, duration, isEnabled, modRate)
    if not button or not button.cooldown then
        return
    end

    local enabled = tonumber(isEnabled) or 0
    local durationValue = tonumber(duration) or 0
    local startValue = tonumber(startTime) or 0
    local rateValue = tonumber(modRate) or 1
    local hasCooldown = enabled > 0 and durationValue > 1.5 and startValue > 0

    if not hasCooldown then
        if type(button.cooldown.SetCooldown) == "function" then
            local ok = pcall(button.cooldown.SetCooldown, button.cooldown, 0, 0, 1)
            if not ok then
                button.cooldown:SetCooldown(0, 0)
            end
        end
        if type(button.cooldown.Hide) == "function" then
            button.cooldown:Hide()
        end
        if button.cooldownText then
            button.cooldownText:SetText("")
            button.cooldownText:Hide()
        end
        return
    end

    if type(button.cooldown.Show) == "function" then
        button.cooldown:Show()
    end

    if type(CooldownFrame_Set) == "function" then
        CooldownFrame_Set(button.cooldown, startValue, durationValue, true, false, rateValue)
    else
        local ok = pcall(button.cooldown.SetCooldown, button.cooldown, startValue, durationValue, rateValue)
        if not ok then
            button.cooldown:SetCooldown(startValue, durationValue)
        end
    end

    if button.cooldownText and button.useCustomCooldownText == true then
        local now = type(GetTime) == "function" and GetTime() or 0
        local remaining = (startValue + durationValue) - now
        if rateValue > 0 and rateValue ~= 1 then
            remaining = remaining / rateValue
        end
        local text = formatTravelBarCooldownText(remaining)
        if text and text ~= "" then
            button.cooldownText:SetText(text)
            button.cooldownText:Show()
        else
            button.cooldownText:SetText("")
            button.cooldownText:Hide()
        end
    elseif button.cooldownText then
        button.cooldownText:SetText("")
        button.cooldownText:Hide()
    end
end

local function applyTravelBarButtonAction(button, action)
    if not button then
        return
    end

    button.travelAction = action
    button:SetAttribute("type", nil)
    button:SetAttribute("item", nil)
    button:SetAttribute("toy", nil)
    button:SetAttribute("spell", nil)
    button:SetAttribute("macrotext", nil)
    button:SetAttribute("type1", nil)
    button:SetAttribute("item1", nil)
    button:SetAttribute("toy1", nil)
    button:SetAttribute("spell1", nil)
    button:SetAttribute("macrotext1", nil)
    button:SetAttribute("type2", nil)
    button:SetAttribute("item2", nil)
    button:SetAttribute("toy2", nil)
    button:SetAttribute("spell2", nil)
    button:SetAttribute("macrotext2", nil)

    if button.icon then
        button.icon:SetTexture(type(action) == "table" and action.icon or 134414)
    end

    if type(action) ~= "table" then
        button:SetAlpha(0.42)
        return
    end

    local menuProxyButton = ensureTravelBarMenuProxyButton()
    if menuProxyButton and menuProxyButton.GetName then
        button:SetAttribute("type2", "macro")
        button:SetAttribute("macrotext2", "/click " .. menuProxyButton:GetName())
    end

    if action.kind == "toy" then
        button:SetAttribute("type1", "toy")
        button:SetAttribute("toy1", action.token)
    elseif action.kind == "spell" then
        button:SetAttribute("type1", "spell")
        button:SetAttribute("spell1", action.token)
    elseif action.kind == "macro" then
        button:SetAttribute("type1", "macro")
        button:SetAttribute("macrotext1", action.macrotext)
    else
        button:SetAttribute("type1", "item")
        button:SetAttribute("item1", action.token)
    end

    button:SetAlpha(1)
end

local function refreshTravelBarCooldownForButton(button)
    if not button or not button.cooldown then
        return
    end

    if button.menuEditing then
        setTravelBarCooldownVisual(button, 0, 0, 0, 1)
        return
    end

    local action = button.travelAction
    if type(action) ~= "table" then
        setTravelBarCooldownVisual(button, 0, 0, 0, 1)
        return
    end

    local startTime, duration, isEnabled, modRate = getTravelBarActionCooldown(action)
    if (tonumber(isEnabled) or 0) == 0 or (tonumber(duration) or 0) <= 1.5 then
        setTravelBarCooldownVisual(button, 0, 0, 0, 1)
        if button.icon and type(button.icon.SetDesaturated) == "function" then
            button.icon:SetDesaturated(false)
        end
        return
    end

    setTravelBarCooldownVisual(button, startTime, duration, isEnabled, modRate)
    if button.icon and type(button.icon.SetDesaturated) == "function" then
        button.icon:SetDesaturated(true)
    end
end

local function ensureTravelBarFrame()
    if TravelBarRuntime.frame then
        return TravelBarRuntime.frame
    end

    local frame = CreateFrame("Frame", "VaultloomTravelBar", UIParent, BACKDROP_TEMPLATE)
    frame:SetSize(TRAVEL_BAR_BUTTON_SIZE + TRAVEL_BAR_FRAME_PADDING_X * 2, TRAVEL_BAR_BUTTON_SIZE + TRAVEL_BAR_FRAME_PADDING_Y * 2)
    frame:SetFrameStrata("MEDIUM")
    frame:SetFrameLevel(10)
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:EnableMouseWheel(true)
    frame:RegisterForDrag("LeftButton")
    frame:Hide()
    ns:ApplyFeaturePosition(frame, TRAVEL_BAR_POSITION_KEY, TRAVEL_BAR_DEFAULT_POSITION)
    ns:ApplyFeatureScale(frame, TRAVEL_BAR_SCALE_KEY, TRAVEL_BAR_SCALE_MIN, TRAVEL_BAR_SCALE_MAX)

    if frame.SetBackdrop then
        frame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        frame:SetBackdropColor(
            TRAVEL_BAR_COLORS.frameBackdrop[1],
            TRAVEL_BAR_COLORS.frameBackdrop[2],
            TRAVEL_BAR_COLORS.frameBackdrop[3],
            TRAVEL_BAR_COLORS.frameBackdrop[4]
        )
        frame:SetBackdropBorderColor(
            TRAVEL_BAR_COLORS.frameBorder[1],
            TRAVEL_BAR_COLORS.frameBorder[2],
            TRAVEL_BAR_COLORS.frameBorder[3],
            TRAVEL_BAR_COLORS.frameBorder[4]
        )
    end

    frame.bg = frame:CreateTexture(nil, "BACKGROUND")
    frame.bg:SetAllPoints()
    frame.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.bg:SetColorTexture(
        TRAVEL_BAR_COLORS.frameBackdrop[1],
        TRAVEL_BAR_COLORS.frameBackdrop[2],
        TRAVEL_BAR_COLORS.frameBackdrop[3],
        0.96
    )

    frame.inner = frame:CreateTexture(nil, "BACKGROUND", nil, 1)
    frame.inner:SetPoint("TOPLEFT", 1, -1)
    frame.inner:SetPoint("BOTTOMRIGHT", -1, 1)
    frame.inner:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.inner:SetColorTexture(
        TRAVEL_BAR_COLORS.frameInset[1],
        TRAVEL_BAR_COLORS.frameInset[2],
        TRAVEL_BAR_COLORS.frameInset[3],
        TRAVEL_BAR_COLORS.frameInset[4]
    )

    frame.topAccent = frame:CreateTexture(nil, "ARTWORK")
    frame.topAccent:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.topAccent:SetColorTexture(
        TRAVEL_BAR_COLORS.accentGold[1],
        TRAVEL_BAR_COLORS.accentGold[2],
        TRAVEL_BAR_COLORS.accentGold[3],
        0.88
    )

    frame.bottomAccent = frame:CreateTexture(nil, "ARTWORK")
    frame.bottomAccent:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.bottomAccent:SetColorTexture(
        TRAVEL_BAR_COLORS.accentGold[1],
        TRAVEL_BAR_COLORS.accentGold[2],
        TRAVEL_BAR_COLORS.accentGold[3],
        0.88
    )

    frame.dragHandles = {}
    for _, handleKey in ipairs({ "top", "bottom", "left", "right" }) do
        local dragHandle = CreateFrame("Frame", nil, UIParent)
        dragHandle:EnableMouse(true)
        dragHandle:RegisterForDrag("LeftButton")
        dragHandle:SetFrameStrata("HIGH")
        dragHandle:SetFrameLevel(90)
        dragHandle.ownerFrame = frame
        dragHandle:Hide()
        dragHandle.texture = dragHandle:CreateTexture(nil, "BACKGROUND")
        dragHandle.texture:SetAllPoints()
        dragHandle.texture:SetTexture("Interface\\Buttons\\WHITE8X8")
        dragHandle.texture:SetColorTexture(1, 1, 1, 0.01)
        dragHandle:SetScript("OnDragStart", function(selfHandle)
            if not selfHandle.ownerFrame or not selfHandle.ownerFrame:IsShown() then
                return
            end
            TravelBarRuntime:HideMenu()
            selfHandle.ownerFrame:StartMoving()
        end)
        dragHandle:SetScript("OnDragStop", function(selfHandle)
            if not selfHandle.ownerFrame then
                return
            end
            selfHandle.ownerFrame:StopMovingOrSizing()
            ns:StoreFeaturePosition(selfHandle.ownerFrame, TRAVEL_BAR_POSITION_KEY)
        end)
        frame.dragHandles[handleKey] = dragHandle
    end

    frame.menuButton = CreateFrame("Button", "VaultloomTravelBarMenuButton", UIParent, BACKDROP_TEMPLATE)
    frame.menuButton:SetSize(18, 18)
    frame.menuButton:SetFrameLevel(60)
    frame.menuButton:SetFrameStrata("HIGH")
    frame.menuButton:RegisterForClicks("AnyUp")
    frame.menuButton:EnableMouse(true)
    frame.menuButton:SetHitRectInsets(-4, -4, -4, -4)
    if type(frame.menuButton.SetMouseClickEnabled) == "function" then
        frame.menuButton:SetMouseClickEnabled(true)
    end
    if frame.menuButton.SetBackdrop then
        frame.menuButton:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        frame.menuButton:SetBackdropColor(0.05, 0.05, 0.06, 0.96)
        frame.menuButton:SetBackdropBorderColor(0.24, 0.24, 0.26, 0.88)
    end
    frame.menuButton.dotTop = frame.menuButton:CreateTexture(nil, "ARTWORK")
    frame.menuButton.dotTop:SetSize(3, 3)
    frame.menuButton.dotTop:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.menuButton.dotTop:SetColorTexture(0.94, 0.82, 0.32, 0.98)
    frame.menuButton.dotTop:SetPoint("CENTER", frame.menuButton, "CENTER", 0, 5)
    frame.menuButton.dotMiddle = frame.menuButton:CreateTexture(nil, "ARTWORK")
    frame.menuButton.dotMiddle:SetSize(3, 3)
    frame.menuButton.dotMiddle:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.menuButton.dotMiddle:SetColorTexture(0.94, 0.82, 0.32, 0.98)
    frame.menuButton.dotMiddle:SetPoint("CENTER", frame.menuButton, "CENTER", 0, 0)
    frame.menuButton.dotBottom = frame.menuButton:CreateTexture(nil, "ARTWORK")
    frame.menuButton.dotBottom:SetSize(3, 3)
    frame.menuButton.dotBottom:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.menuButton.dotBottom:SetColorTexture(0.94, 0.82, 0.32, 0.98)
    frame.menuButton.dotBottom:SetPoint("CENTER", frame.menuButton, "CENTER", 0, -5)
    frame.menuButton:SetScript("OnClick", function(selfButton, mouseButton)
        local mode = (mouseButton == "RightButton" or mouseButton == "2" or (type(IsShiftKeyDown) == "function" and IsShiftKeyDown()))
            and "hearths"
            or "extras"
        TravelBarRuntime:OpenMenu(mode, selfButton)
    end)
    frame.menuButton:SetScript("OnEnter", function(selfButton)
        GameTooltip:SetOwner(selfButton, "ANCHOR_RIGHT")
        GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_TITLE or "Travel Bar", 1, 0.82, 0.22, true)
        GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_MENU_TRAVEL or "Left click: open travel menu", 1.00, 1.00, 1.00, true)
        GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_MENU_HEARTH or "Right click: open hearth menu", 0.65, 0.85, 1.00, true)
        GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_MENU_SHIFT_HEARTH or "Shift + left click: hearth menu", 0.84, 0.84, 0.82, true)
        GameTooltip:Show()
    end)
    frame.menuButton:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    frame.menuButton:Hide()

    frame:SetScript("OnDragStart", function(selfFrame)
        TravelBarRuntime:HideMenu()
        selfFrame:StartMoving()
    end)
    frame:SetScript("OnDragStop", function(selfFrame)
        selfFrame:StopMovingOrSizing()
        ns:StoreFeaturePosition(selfFrame, TRAVEL_BAR_POSITION_KEY)
    end)
    frame:SetScript("OnEnter", function(selfFrame)
        GameTooltip:SetOwner(selfFrame, "ANCHOR_TOP")
        GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_TITLE or "Travel Bar", 1, 0.82, 0.22, true)
        GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_DRAG or "Left drag: move", 0.84, 0.84, 0.82, true)
        GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_WHEEL or "Shift + mouse wheel: size", 0.84, 0.84, 0.82, true)
        GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_BUTTON_MENU or "Right click a button: open menu", 0.84, 0.84, 0.82, true)
        GameTooltip:Show()
    end)
    frame:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    frame:SetScript("OnMouseWheel", function(selfFrame, delta)
        if type(IsShiftKeyDown) == "function" and not IsShiftKeyDown() then
            return
        end

        local nextScale = ns:GetFeatureScale(TRAVEL_BAR_SCALE_KEY, TRAVEL_BAR_SCALE_MIN, TRAVEL_BAR_SCALE_MAX)
        if delta > 0 then
            nextScale = nextScale + TRAVEL_BAR_SCALE_STEP
        elseif delta < 0 then
            nextScale = nextScale - TRAVEL_BAR_SCALE_STEP
        end

        nextScale = ns:ClampNumber(nextScale, TRAVEL_BAR_SCALE_MIN, TRAVEL_BAR_SCALE_MAX)
        selfFrame:SetScale(nextScale)
        ns:StoreFeatureScale(TRAVEL_BAR_SCALE_KEY, nextScale, TRAVEL_BAR_SCALE_MIN, TRAVEL_BAR_SCALE_MAX)
    end)
    TravelBarRuntime.frame = frame
    return frame
end

local function ensureTravelBarButton(index)
    local frame = ensureTravelBarFrame()
    local button = TravelBarRuntime.buttons[index]
    if button then
        return button
    end

    button = CreateFrame("Button", nil, frame, "SecureActionButtonTemplate")
    button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    button:RegisterForDrag("LeftButton")
    button:SetAttribute("useOnKeyDown", false)

    button.backplate = button:CreateTexture(nil, "BACKGROUND")
    button.backplate:SetAllPoints()
    button.backplate:SetTexture("Interface\\Buttons\\WHITE8X8")
    button.backplate:SetColorTexture(0.06, 0.06, 0.07, 0.94)

    button.iconShadow = button:CreateTexture(nil, "BACKGROUND", nil, 1)
    button.iconShadow:SetPoint("TOPLEFT", 5, -5)
    button.iconShadow:SetPoint("BOTTOMRIGHT", -5, 5)
    button.iconShadow:SetTexture("Interface\\Buttons\\WHITE8X8")
    button.iconShadow:SetColorTexture(0, 0, 0, 0.56)

    button.activeGlow = button:CreateTexture(nil, "BACKGROUND", nil, 1)
    button.activeGlow:SetPoint("TOPLEFT", -2, 2)
    button.activeGlow:SetPoint("BOTTOMRIGHT", 2, -2)
    button.activeGlow:SetTexture(TRAVEL_BAR_RING_TEXTURE)
    button.activeGlow:SetBlendMode("ADD")
    button.activeGlow:SetAlpha(0)

    button.icon = button:CreateTexture(nil, "ARTWORK")
    button.icon:SetPoint("TOPLEFT", 4, -4)
    button.icon:SetPoint("BOTTOMRIGHT", -4, 4)
    button.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    button.iconMask = button:CreateMaskTexture(nil, "ARTWORK")
    button.iconMask:SetTexture("Interface\\CHARACTERFRAME\\TempPortraitAlphaMask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
    button.iconMask:SetPoint("TOPLEFT", button.icon, "TOPLEFT", 0, 0)
    button.iconMask:SetPoint("BOTTOMRIGHT", button.icon, "BOTTOMRIGHT", 0, 0)
    button.icon:AddMaskTexture(button.iconMask)

    button.iconShadowMask = button:CreateMaskTexture(nil, "ARTWORK")
    button.iconShadowMask:SetTexture("Interface\\CHARACTERFRAME\\TempPortraitAlphaMask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")
    button.iconShadowMask:SetPoint("TOPLEFT", button.iconShadow, "TOPLEFT", 0, 0)
    button.iconShadowMask:SetPoint("BOTTOMRIGHT", button.iconShadow, "BOTTOMRIGHT", 0, 0)
    button.iconShadow:AddMaskTexture(button.iconShadowMask)

    button.highlight = button:CreateTexture(nil, "HIGHLIGHT")
    button.highlight:SetPoint("TOPLEFT", -1, 1)
    button.highlight:SetPoint("BOTTOMRIGHT", 1, -1)
    button.highlight:SetTexture(TRAVEL_BAR_RING_TEXTURE)
    button.highlight:SetVertexColor(
        TRAVEL_BAR_COLORS.hoverRing[1],
        TRAVEL_BAR_COLORS.hoverRing[2],
        TRAVEL_BAR_COLORS.hoverRing[3],
        TRAVEL_BAR_COLORS.hoverRing[4]
    )
    button.highlight:SetAlpha(0)

    button.ring = button:CreateTexture(nil, "OVERLAY")
    button.ring:SetPoint("TOPLEFT", -1, 1)
    button.ring:SetPoint("BOTTOMRIGHT", 1, -1)
    button.ring:SetTexture(TRAVEL_BAR_RING_TEXTURE)
    button.ring:SetVertexColor(
        TRAVEL_BAR_COLORS.inactiveRing[1],
        TRAVEL_BAR_COLORS.inactiveRing[2],
        TRAVEL_BAR_COLORS.inactiveRing[3],
        TRAVEL_BAR_COLORS.inactiveRing[4]
    )
    button.ring:SetAlpha(0.94)

    button.stateBar = button:CreateTexture(nil, "OVERLAY")
    button.stateBar:SetPoint("BOTTOMLEFT", 11, 3)
    button.stateBar:SetPoint("BOTTOMRIGHT", -11, 3)
    button.stateBar:SetHeight(1)
    button.stateBar:SetTexture("Interface\\Buttons\\WHITE8X8")
    button.stateBar:SetColorTexture(0.56, 0.58, 0.62, 0.50)

    button.cooldown = CreateFrame("Cooldown", nil, button, "CooldownFrameTemplate")
    button.cooldown:SetAllPoints(button.icon)
    button.cooldown:SetFrameLevel(button:GetFrameLevel() + 6)
    if type(button.cooldown.EnableMouse) == "function" then
        button.cooldown:EnableMouse(false)
    end
    if type(button.cooldown.SetReverse) == "function" then
        button.cooldown:SetReverse(false)
    end
    if type(button.cooldown.SetDrawBling) == "function" then
        button.cooldown:SetDrawBling(false)
    end
    if type(button.cooldown.SetDrawEdge) == "function" then
        button.cooldown:SetDrawEdge(false)
    end
    if type(button.cooldown.SetDrawSwipe) == "function" then
        button.cooldown:SetDrawSwipe(false)
    end
    if type(button.cooldown.SetSwipeColor) == "function" then
        button.cooldown:SetSwipeColor(0, 0, 0, 0)
    end
    if type(button.cooldown.SetHideCountdownNumbers) == "function" then
        button.cooldown:SetHideCountdownNumbers(true)
    end
    button.cooldown:Hide()

    button.cooldownText = button:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
    button.cooldownText:SetPoint("CENTER", button, "CENTER", 0, 0)
    button.cooldownText:SetTextColor(1, 0.96, 0.82, 0.98)
    button.cooldownText:SetShadowOffset(1, -1)
    button.cooldownText:SetShadowColor(0, 0, 0, 1)
    button.cooldownText:SetJustifyH("CENTER")
    button.cooldownText:SetJustifyV("MIDDLE")
    if type(button.cooldownText.SetFont) == "function" then
        button.cooldownText:SetFont(STANDARD_TEXT_FONT, 14, "OUTLINE")
    end
    button.cooldownText:Hide()
    button.useCustomCooldownText = true

    button:SetScript("PreClick", function(selfButton, mouseButton)
        if mouseButton == "RightButton" or mouseButton == "2" or mouseButton == "Button2" then
            TravelBarRuntime.menuRequestButton = selfButton
        else
            TravelBarRuntime.menuRequestButton = nil
        end
    end)
    button:SetScript("PostClick", function(selfButton, mouseButton)
        if mouseButton ~= "LeftButton" and mouseButton ~= "1" then
            return
        end

        if selfButton.travelRole == "main" then
            TravelBarRuntime:AdvanceRandomHearth(selfButton.travelAction and selfButton.travelAction.key or nil)
        elseif selfButton.travelRole == "more" and selfButton.travelAction and selfButton.travelAction.key then
            storeTravelBarSelectedExtraKey(selfButton.travelAction.key)
            TravelBarRuntime:QueueRefresh(0.06)
        end
    end)
    button:SetScript("OnDragStart", function()
        TravelBarRuntime:HideMenu()
        frame:StartMoving()
    end)
    button:SetScript("OnDragStop", function()
        frame:StopMovingOrSizing()
        ns:StoreFeaturePosition(frame, TRAVEL_BAR_POSITION_KEY)
    end)
    button:SetScript("OnEnter", function(selfButton)
        if selfButton.highlight then
            selfButton.highlight:SetAlpha(0.58)
        end
        local action = selfButton.travelAction
        local description = getTravelBarActionDescription(action)
        GameTooltip:SetOwner(selfButton, "ANCHOR_RIGHT")
        GameTooltip:AddLine((type(action) == "table" and action.label) or (L.FEATURE_TRAVEL_BAR_TITLE or "Travel Bar"), 1, 0.82, 0.22, true)
        if description then
            GameTooltip:AddLine(description, 0.84, 0.84, 0.82, true)
        end

        if selfButton.travelRole == "main" then
            GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_MAIN_USE or "Left click: use a random hearthstone", 1.00, 1.00, 1.00, true)
            GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_MAIN_MENU or "Right click: open hearth menu", 0.65, 0.85, 1.00, true)
        elseif selfButton.travelRole == "more" then
            GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_MORE_USE or "Left click: use the selected travel option", 1.00, 1.00, 1.00, true)
            GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_MORE_MENU or "Right click: open travel menu", 0.65, 0.85, 1.00, true)
        else
            GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_DIRECT_USE or "Left click: use this travel option", 1.00, 1.00, 1.00, true)
            GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_DIRECT_MENU or "Right click: open travel menu", 0.65, 0.85, 1.00, true)
        end

        if InCombatLockdown() and (selfButton.travelRole == "main" or selfButton.travelRole == "more" or selfButton.travelRole == "direct") then
            GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_ERROR_COMBAT or "Vaultloom: travel menus cannot open in combat.", 1.00, 0.30, 0.30, true)
        end

        GameTooltip:Show()
    end)
    button:SetScript("OnLeave", function(selfButton)
        if selfButton.highlight then
            selfButton.highlight:SetAlpha(0)
        end
        GameTooltip:Hide()
    end)

    TravelBarRuntime.buttons[index] = button
    return button
end

function TravelBarRuntime:HideMenu()
    if self.menu then
        self.menu:Hide()
        self.menu.anchorButton = nil
        self.menu.mode = nil
        self.menu.editing = false
    end
end

function TravelBarRuntime:HideConfigPanel()
    if self.configPanel then
        self.configPanel:Hide()
    end
end

function TravelBarRuntime:ShowConfigPanel(forceRecenter)
    local panel = self:EnsureConfigPanel()
    if forceRecenter == true or not panel.vlUserPlaced then
        panel:ClearAllPoints()
        local addonFrame = _G.VaultloomFrame
        if addonFrame and addonFrame.IsShown and addonFrame:IsShown() then
            panel:SetPoint("TOPLEFT", addonFrame, "TOPRIGHT", 12, -16)
        else
            panel:SetPoint("CENTER", UIParent, "CENTER", 360, 0)
        end
    end

    self:HideMenu()
    self:RefreshConfigPanel()
    panel:Show()
    if panel.Raise then
        panel:Raise()
    end
    return panel
end

function TravelBarRuntime:EnsureConfigPanel()
    if self.configPanel then
        return self.configPanel
    end

    local panel = CreateFrame("Frame", "VaultloomTravelBarConfigPanel", UIParent, BACKDROP_TEMPLATE)
    panel:SetFrameStrata("FULLSCREEN_DIALOG")
    panel:SetFrameLevel(120)
    panel:SetToplevel(true)
    panel:SetClampedToScreen(true)
    panel:SetSize(336, 364)
    panel:Hide()
    panel:EnableMouse(true)
    panel:SetMovable(true)
    panel:SetPoint("CENTER", UIParent, "CENTER", 360, 0)

    if panel.SetBackdrop then
        panel:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        panel:SetBackdropColor(0.02, 0.02, 0.03, 0.97)
        panel:SetBackdropBorderColor(
            TRAVEL_BAR_COLORS.frameBorder[1],
            TRAVEL_BAR_COLORS.frameBorder[2],
            TRAVEL_BAR_COLORS.frameBorder[3],
            0.92
        )
    end
    panel:SetBackdropColor(0.02, 0.02, 0.03, 0.94)

    panel.bg = panel:CreateTexture(nil, "BACKGROUND")
    panel.bg:SetAllPoints()
    panel.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
    panel.bg:SetColorTexture(0.02, 0.02, 0.03, 0.92)

    panel.inner = panel:CreateTexture(nil, "BACKGROUND", nil, 1)
    panel.inner:SetPoint("TOPLEFT", 1, -1)
    panel.inner:SetPoint("BOTTOMRIGHT", -1, 1)
    panel.inner:SetTexture("Interface\\Buttons\\WHITE8X8")
    panel.inner:SetColorTexture(0.05, 0.05, 0.06, 0.88)

    panel.header = CreateFrame("Frame", nil, panel, BACKDROP_TEMPLATE)
    panel.header:SetPoint("TOPLEFT", 1, -1)
    panel.header:SetPoint("TOPRIGHT", -1, -1)
    panel.header:SetHeight(34)
    if panel.header.SetBackdrop then
        panel.header:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 0, right = 0, top = 0, bottom = 0 },
        })
        panel.header:SetBackdropColor(0.08, 0.08, 0.10, 0.92)
        panel.header:SetBackdropBorderColor(0, 0, 0, 0)
    end

    panel.headerAccent = panel.header:CreateTexture(nil, "ARTWORK")
    panel.headerAccent:SetPoint("BOTTOMLEFT", 10, 0)
    panel.headerAccent:SetPoint("BOTTOMRIGHT", -10, 0)
    panel.headerAccent:SetHeight(1)
    panel.headerAccent:SetTexture("Interface\\Buttons\\WHITE8X8")
    panel.headerAccent:SetColorTexture(
        TRAVEL_BAR_COLORS.accentGold[1],
        TRAVEL_BAR_COLORS.accentGold[2],
        TRAVEL_BAR_COLORS.accentGold[3],
        0.58
    )

    panel.dragHandle = CreateFrame("Frame", nil, panel.header)
    panel.dragHandle:SetAllPoints()
    panel.dragHandle:EnableMouse(true)
    panel.dragHandle:RegisterForDrag("LeftButton")
    panel.dragHandle:SetScript("OnDragStart", function()
        panel:StartMoving()
    end)
    panel.dragHandle:SetScript("OnDragStop", function()
        panel:StopMovingOrSizing()
        panel.vlUserPlaced = true
    end)

    panel.title = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    panel.title:SetPoint("LEFT", panel.header, "LEFT", 14, 0)
    panel.title:SetPoint("RIGHT", panel.header, "RIGHT", -44, 0)
    panel.title:SetJustifyH("LEFT")
    panel.title:SetTextColor(
        TRAVEL_BAR_COLORS.accentGold[1],
        TRAVEL_BAR_COLORS.accentGold[2],
        TRAVEL_BAR_COLORS.accentGold[3],
        1
    )

    panel.subtitle = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    panel.subtitle:SetPoint("TOPLEFT", panel.header, "BOTTOMLEFT", 14, -12)
    panel.subtitle:SetPoint("TOPRIGHT", -14, 0)
    panel.subtitle:SetJustifyH("LEFT")
    panel.subtitle:SetTextColor(0.84, 0.84, 0.82, 0.96)

    panel.closeButton = CreateFrame("Button", nil, panel, BACKDROP_TEMPLATE)
    panel.closeButton:SetSize(24, 20)
    panel.closeButton:SetPoint("RIGHT", panel.header, "RIGHT", -10, 0)
    if panel.closeButton.SetBackdrop then
        panel.closeButton:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        panel.closeButton:SetBackdropColor(0.08, 0.08, 0.10, 0.96)
        panel.closeButton:SetBackdropBorderColor(0.22, 0.22, 0.24, 0.88)
    end
    panel.closeButton.label = panel.closeButton:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    panel.closeButton.label:SetPoint("CENTER")
    panel.closeButton.label:SetText("X")
    panel.closeButton:SetScript("OnClick", function()
        TravelBarRuntime:HideConfigPanel()
    end)

    panel.frameStyleButton = CreateFrame("Button", nil, panel, BACKDROP_TEMPLATE)
    panel.frameStyleButton:SetSize(108, 22)
    panel.frameStyleButton:SetPoint("TOPLEFT", panel.subtitle, "BOTTOMLEFT", 0, -12)
    if panel.frameStyleButton.SetBackdrop then
        panel.frameStyleButton:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        panel.frameStyleButton:SetBackdropColor(0.08, 0.08, 0.10, 0.96)
        panel.frameStyleButton:SetBackdropBorderColor(0.22, 0.22, 0.24, 0.88)
    end
    panel.frameStyleButton.label = panel.frameStyleButton:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    panel.frameStyleButton.label:SetPoint("CENTER")
    panel.frameStyleButton:SetScript("OnClick", function()
        local nextValue = getTravelBarFrameStyle() == "hidden" and "shown" or "hidden"
        if ns.SetFeatureOptionValue then
            ns:SetFeatureOptionValue("travel_bar", "frame_style", nextValue)
        end
        TravelBarRuntime:RefreshConfigPanel()
    end)
    panel.frameStyleButton:SetScript("OnEnter", function(selfButton)
        if selfButton.SetBackdropBorderColor then
            selfButton:SetBackdropBorderColor(
                TRAVEL_BAR_COLORS.accentGold[1],
                TRAVEL_BAR_COLORS.accentGold[2],
                TRAVEL_BAR_COLORS.accentGold[3],
                0.90
            )
        end
    end)
    panel.frameStyleButton:SetScript("OnLeave", function(selfButton)
        if selfButton.SetBackdropBorderColor then
            selfButton:SetBackdropBorderColor(0.22, 0.22, 0.24, 0.88)
        end
    end)

    panel.hearthHeader = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    panel.hearthHeader:SetPoint("TOPLEFT", panel.frameStyleButton, "BOTTOMLEFT", 0, -16)
    panel.hearthHeader:SetTextColor(
        TRAVEL_BAR_COLORS.accentGold[1],
        TRAVEL_BAR_COLORS.accentGold[2],
        TRAVEL_BAR_COLORS.accentGold[3],
        1
    )

    panel.travelHeader = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    panel.travelHeader:SetTextColor(
        TRAVEL_BAR_COLORS.accentGold[1],
        TRAVEL_BAR_COLORS.accentGold[2],
        TRAVEL_BAR_COLORS.accentGold[3],
        1
    )

    panel.rows = {}
    self.configPanel = panel
    return panel
end

function TravelBarRuntime:EnsureConfigRow(index)
    local panel = self:EnsureConfigPanel()
    local row = panel.rows[index]
    if row then
        return row
    end

    row = CreateFrame("Button", nil, panel, BACKDROP_TEMPLATE)
    row:SetSize(292, 24)
    row:EnableMouse(true)
    if row.SetBackdrop then
        row:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        row:SetBackdropColor(0.06, 0.06, 0.07, 0.96)
        row:SetBackdropBorderColor(0.18, 0.18, 0.20, 0.80)
    end

    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetSize(18, 18)
    row.icon:SetPoint("LEFT", 7, 0)
    row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    row.label = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.label:SetPoint("LEFT", row.icon, "RIGHT", 8, 0)
    row.label:SetPoint("RIGHT", -56, 0)
    row.label:SetJustifyH("LEFT")

    row.state = row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    row.state:SetPoint("RIGHT", -10, 0)
    row.state:SetJustifyH("RIGHT")

    row:SetScript("OnClick", function(selfRow)
        if not selfRow.actionKey then
            return
        end
        TravelBarRuntime:ToggleActionVisibility(selfRow.actionKey, selfRow.rowMode)
        TravelBarRuntime:RefreshConfigPanel()
    end)

    panel.rows[index] = row
    return row
end

function TravelBarRuntime:RefreshConfigPanel()
    local panel = self:EnsureConfigPanel()
    self.state = buildTravelBarState()

    panel.title:SetText(L.FEATURE_TRAVEL_BAR_CONFIG_TITLE or "Travel Bar Buttons")
    panel.subtitle:SetText(L.FEATURE_TRAVEL_BAR_CONFIG_NOTE or "Wähle, welche Hearthstones und Reiseziele auf der Leiste sichtbar sind.")
    panel.frameStyleButton.label:SetText(
        (L.FEATURE_TRAVEL_BAR_FRAME_LABEL or "Frame") .. ": "
            .. ((getTravelBarFrameStyle() == "hidden" and (L.FEATURE_TRAVEL_BAR_FRAME_HIDDEN or "Clean")) or (L.FEATURE_TRAVEL_BAR_FRAME_SHOWN or "Frame"))
    )
    panel.hearthHeader:SetText(L.FEATURE_TRAVEL_BAR_CONFIG_HEARTHS or "Hearthstones")
    panel.travelHeader:SetText(L.FEATURE_TRAVEL_BAR_CONFIG_TRAVEL or "Travel")

    local rows = {}
    for _, action in ipairs(self.state.hearthsAll or {}) do
        rows[#rows + 1] = { action = action, rowMode = "hearths" }
    end
    for _, action in ipairs(self.state.specialsAll or {}) do
        rows[#rows + 1] = { action = action, rowMode = "specials" }
    end

    local startY = -112
    local rowHeight = 28
    local travelHeaderOffset = startY + math.max(0, #self.state.hearthsAll) * rowHeight + 12
    panel.hearthHeader:ClearAllPoints()
    panel.hearthHeader:SetPoint("TOPLEFT", 14, startY)
    panel.travelHeader:ClearAllPoints()
    panel.travelHeader:SetPoint("TOPLEFT", 14, travelHeaderOffset)

    local index = 0
    local currentY = startY - 22
    for _, entry in ipairs(rows) do
        if entry.rowMode == "specials" and entry.action == (self.state.specialsAll or {})[1] then
            currentY = travelHeaderOffset - 22
        end

        index = index + 1
        local row = self:EnsureConfigRow(index)
        local action = entry.action
        local isHidden = isTravelBarActionHidden(action.key)
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", 14, currentY)
        row.icon:SetTexture(action.icon or 134414)
        row.label:SetText(action.label or action.key or "")
        row.state:SetText(isHidden and (L.FEATURE_TRAVEL_BAR_MENU_STATE_HIDDEN or "Off") or (L.FEATURE_TRAVEL_BAR_MENU_STATE_SHOWN or "On"))
        row.state:SetTextColor(
            isHidden and TRAVEL_BAR_COLORS.menuTextMuted[1] or TRAVEL_BAR_COLORS.accentGold[1],
            isHidden and TRAVEL_BAR_COLORS.menuTextMuted[2] or TRAVEL_BAR_COLORS.accentGold[2],
            isHidden and TRAVEL_BAR_COLORS.menuTextMuted[3] or TRAVEL_BAR_COLORS.accentGold[3],
            isHidden and TRAVEL_BAR_COLORS.menuTextMuted[4] or 1
        )
        row.label:SetTextColor(
            isHidden and TRAVEL_BAR_COLORS.menuTextMuted[1] or TRAVEL_BAR_COLORS.menuText[1],
            isHidden and TRAVEL_BAR_COLORS.menuTextMuted[2] or TRAVEL_BAR_COLORS.menuText[2],
            isHidden and TRAVEL_BAR_COLORS.menuTextMuted[3] or TRAVEL_BAR_COLORS.menuText[3],
            isHidden and TRAVEL_BAR_COLORS.menuTextMuted[4] or TRAVEL_BAR_COLORS.menuText[4]
        )
        row.actionKey = action.key
        row.rowMode = entry.rowMode
        row:Show()
        currentY = currentY - rowHeight
    end

    for hiddenIndex = index + 1, #panel.rows do
        panel.rows[hiddenIndex]:Hide()
    end

    panel:SetHeight(math.max(260, 148 + index * rowHeight))
end

function TravelBarRuntime:ToggleConfigPanel()
    local panel = self:EnsureConfigPanel()
    if panel:IsShown() then
        self:HideConfigPanel()
        return
    end
    self:ShowConfigPanel(false)
end

function TravelBarRuntime:EnsureMenu()
    if self.menu then
        return self.menu
    end

    local menu = CreateFrame("Frame", "VaultloomTravelBarMenu", UIParent, BACKDROP_TEMPLATE)
    menu:SetFrameStrata("FULLSCREEN_DIALOG")
    menu:SetFrameLevel(100)
    menu:SetToplevel(true)
    menu:SetClampedToScreen(true)
    menu:Hide()

    if menu.SetBackdrop then
        menu:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        menu:SetBackdropColor(
            TRAVEL_BAR_COLORS.frameBackdrop[1],
            TRAVEL_BAR_COLORS.frameBackdrop[2],
            TRAVEL_BAR_COLORS.frameBackdrop[3],
            0.97
        )
        menu:SetBackdropBorderColor(
            TRAVEL_BAR_COLORS.frameBorder[1],
            TRAVEL_BAR_COLORS.frameBorder[2],
            TRAVEL_BAR_COLORS.frameBorder[3],
            0.90
        )
    end

    menu.bg = menu:CreateTexture(nil, "BACKGROUND")
    menu.bg:SetAllPoints()
    menu.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
    menu.bg:SetColorTexture(0.03, 0.03, 0.04, 0.96)

    menu.overlay = menu:CreateTexture(nil, "BACKGROUND", nil, 1)
    menu.overlay:SetPoint("TOPLEFT", 1, -1)
    menu.overlay:SetPoint("BOTTOMRIGHT", -1, 1)
    menu.overlay:SetTexture("Interface\\Buttons\\WHITE8X8")
    menu.overlay:SetColorTexture(0.05, 0.05, 0.06, 0.94)

    menu.title = menu:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    menu.title:SetPoint("TOPLEFT", 12, -10)
    menu.title:SetPoint("TOPRIGHT", -12, -10)
    menu.title:SetJustifyH("LEFT")
    menu.title:SetTextColor(
        TRAVEL_BAR_COLORS.accentGold[1],
        TRAVEL_BAR_COLORS.accentGold[2],
        TRAVEL_BAR_COLORS.accentGold[3],
        1
    )

    menu.divider = menu:CreateTexture(nil, "ARTWORK")
    menu.divider:SetTexture("Interface\\Buttons\\WHITE8X8")
    menu.divider:SetPoint("TOPLEFT", menu.title, "BOTTOMLEFT", 0, -6)
    menu.divider:SetPoint("TOPRIGHT", -12, 0)
    menu.divider:SetHeight(1)
    menu.divider:SetColorTexture(
        TRAVEL_BAR_COLORS.accentGoldSoft[1],
        TRAVEL_BAR_COLORS.accentGoldSoft[2],
        TRAVEL_BAR_COLORS.accentGoldSoft[3],
        0.44
    )

    menu.editButton = CreateFrame("Button", nil, menu, BACKDROP_TEMPLATE)
    menu.editButton:SetSize(218, 20)
    if menu.editButton.SetBackdrop then
        menu.editButton:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        menu.editButton:SetBackdropColor(0.07, 0.07, 0.08, 0.98)
        menu.editButton:SetBackdropBorderColor(0.20, 0.20, 0.22, 0.88)
    end
    menu.editButton.text = menu.editButton:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    menu.editButton.text:SetPoint("CENTER", menu.editButton, "CENTER", 0, 0)
    menu.editButton.text:SetTextColor(
        TRAVEL_BAR_COLORS.menuText[1],
        TRAVEL_BAR_COLORS.menuText[2],
        TRAVEL_BAR_COLORS.menuText[3],
        TRAVEL_BAR_COLORS.menuText[4]
    )
    menu.editButton:SetScript("OnClick", function()
        TravelBarRuntime:ToggleMenuEdit()
    end)

    menu.rows = {}
    self.menu = menu
    return menu
end

function TravelBarRuntime:EnsureMenuRow(index)
    local menu = self:EnsureMenu()
    local row = menu.rows[index]
    if row then
        return row
    end

    row = CreateFrame("Button", nil, menu, BACKDROP_TEMPLATE)
    row:RegisterForClicks("LeftButtonUp")
    row:SetSize(218, 26)
    row:EnableMouse(true)

    row.bg = row:CreateTexture(nil, "BACKGROUND")
    row.bg:SetAllPoints()
    row.bg:SetTexture("Interface\\Buttons\\WHITE8X8")
    row.bg:SetColorTexture(0.06, 0.06, 0.07, 0.98)

    row.borderTop = row:CreateTexture(nil, "ARTWORK")
    row.borderTop:SetPoint("TOPLEFT", 0, 0)
    row.borderTop:SetPoint("TOPRIGHT", 0, 0)
    row.borderTop:SetHeight(1)
    row.borderTop:SetTexture("Interface\\Buttons\\WHITE8X8")
    row.borderTop:SetColorTexture(0.18, 0.18, 0.20, 0.90)

    row.borderBottom = row:CreateTexture(nil, "ARTWORK")
    row.borderBottom:SetPoint("BOTTOMLEFT", 0, 0)
    row.borderBottom:SetPoint("BOTTOMRIGHT", 0, 0)
    row.borderBottom:SetHeight(1)
    row.borderBottom:SetTexture("Interface\\Buttons\\WHITE8X8")
    row.borderBottom:SetColorTexture(0.18, 0.18, 0.20, 0.90)

    row.accent = row:CreateTexture(nil, "ARTWORK")
    row.accent:SetPoint("TOPLEFT", 0, 0)
    row.accent:SetPoint("BOTTOMLEFT", 0, 0)
    row.accent:SetWidth(2)
    row.accent:SetTexture("Interface\\Buttons\\WHITE8X8")
    row.accent:SetColorTexture(
        TRAVEL_BAR_COLORS.accentGoldSoft[1],
        TRAVEL_BAR_COLORS.accentGoldSoft[2],
        TRAVEL_BAR_COLORS.accentGoldSoft[3],
        0.24
    )

    row.highlight = row:CreateTexture(nil, "HIGHLIGHT")
    row.highlight:SetAllPoints()
    row.highlight:SetTexture("Interface\\Buttons\\WHITE8X8")
    row.highlight:SetColorTexture(1, 1, 1, 0.05)

    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetPoint("LEFT", 8, 0)
    row.icon:SetSize(18, 18)
    row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    row.toggleBox = CreateFrame("Frame", nil, row, BACKDROP_TEMPLATE)
    row.toggleBox:SetSize(14, 14)
    row.toggleBox:SetPoint("RIGHT", -10, 0)
    if row.toggleBox.SetBackdrop then
        row.toggleBox:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        row.toggleBox:SetBackdropColor(0.05, 0.05, 0.06, 0.98)
        row.toggleBox:SetBackdropBorderColor(0.28, 0.28, 0.32, 0.92)
    end
    row.toggleBox:Hide()

    row.toggleMark = row.toggleBox:CreateTexture(nil, "OVERLAY")
    row.toggleMark:SetPoint("TOPLEFT", 2, -2)
    row.toggleMark:SetPoint("BOTTOMRIGHT", -2, 2)
    row.toggleMark:SetTexture("Interface\\RaidFrame\\ReadyCheck-Ready")
    row.toggleMark:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    row.toggleMark:Hide()

    row.cooldown = CreateFrame("Cooldown", nil, row, "CooldownFrameTemplate")
    row.cooldown:SetAllPoints(row.icon)
    row.cooldown:SetFrameLevel(row:GetFrameLevel() + 4)
    if type(row.cooldown.SetDrawBling) == "function" then
        row.cooldown:SetDrawBling(false)
    end
    if type(row.cooldown.SetDrawEdge) == "function" then
        row.cooldown:SetDrawEdge(false)
    end
    if type(row.cooldown.SetDrawSwipe) == "function" then
        row.cooldown:SetDrawSwipe(true)
    end
    if type(row.cooldown.SetSwipeColor) == "function" then
        row.cooldown:SetSwipeColor(0, 0, 0, 0.82)
    end
    if type(row.cooldown.SetHideCountdownNumbers) == "function" then
        row.cooldown:SetHideCountdownNumbers(true)
    end
    row.cooldown:Hide()

    row.cooldownText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.cooldownText:SetPoint("CENTER", row.icon, "CENTER", 0, 0)
    row.cooldownText:SetTextColor(1, 0.96, 0.82, 0.98)
    row.cooldownText:SetShadowOffset(1, -1)
    row.cooldownText:SetShadowColor(0, 0, 0, 1)
    row.cooldownText:Hide()
    row.useCustomCooldownText = true

    row.label = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.label:SetPoint("LEFT", row.icon, "RIGHT", 10, 0)
    row.label:SetPoint("RIGHT", -92, 0)
    row.label:SetJustifyH("LEFT")

    row.stateLabel = row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    row.stateLabel:SetPoint("RIGHT", row.toggleBox, "LEFT", -8, 0)
    row.stateLabel:SetJustifyH("RIGHT")
    row.stateLabel:Hide()

    row:SetScript("OnClick", function(selfRow)
        TravelBarRuntime:OnMenuRowUsed(selfRow)
    end)
    row:SetScript("OnEnter", function(selfRow)
        if type(selfRow.travelAction) ~= "table" then
            return
        end

        local description = getTravelBarActionDescription(selfRow.travelAction)
        GameTooltip:SetOwner(selfRow, "ANCHOR_RIGHT")
        GameTooltip:AddLine(selfRow.travelAction.label or (L.UNKNOWN or "Unknown"), 1, 0.82, 0.22, true)
        if description then
            GameTooltip:AddLine(description, 0.84, 0.84, 0.82, true)
        end
        if selfRow.menuEditing then
            GameTooltip:AddLine(
                selfRow.menuHidden
                    and (L.FEATURE_TRAVEL_BAR_MENU_EDIT_SHOW or "Left click: show on bar")
                    or (L.FEATURE_TRAVEL_BAR_MENU_EDIT_HIDE or "Left click: hide from bar"),
                1.00,
                1.00,
                1.00,
                true
            )
        else
            GameTooltip:AddLine(L.FEATURE_TRAVEL_BAR_RUNTIME_TOOLTIP_DIRECT_USE or "Left click: use this travel option", 1.00, 1.00, 1.00, true)
        end
        GameTooltip:Show()
    end)
    row:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)

    menu.rows[index] = row
    return row
end

function TravelBarRuntime:ToggleMenuEdit()
    local menu = self:EnsureMenu()
    if not menu:IsShown() then
        return
    end

    if menu.editing == true then
        self:HideMenu()
        return
    end

    menu.editing = not menu.editing
    self:RefreshMenu()
end

function TravelBarRuntime:ToggleActionVisibility(actionKey, menuMode)
    if type(actionKey) ~= "string" or actionKey == "" then
        return
    end

    local isHidden = isTravelBarActionHidden(actionKey)
    if not isHidden and type(self.state) == "table" then
        local visibleCount = menuMode == "hearths" and #(self.state.hearths or {}) or #(self.state.specials or {})
        if visibleCount <= 1 then
            ns:ShowFeatureError(
                menuMode == "hearths"
                    and (L.FEATURE_TRAVEL_BAR_ERROR_KEEP_ONE_HEARTH or "Vaultloom: keep at least one hearthstone visible.")
                    or (L.FEATURE_TRAVEL_BAR_ERROR_KEEP_ONE_TRAVEL or "Vaultloom: keep at least one travel option visible.")
            )
            return
        end
    end

    setTravelBarActionHidden(actionKey, not isHidden)
    self:RefreshAll()
end

function TravelBarRuntime:OnMenuRowUsed(row)
    if not row or type(row.travelAction) ~= "table" then
        return
    end

    if row.menuEditing then
        self:ToggleActionVisibility(row.travelAction.key, row.menuMode)
        return
    end

    if row.menuMode == "extras" and row.travelAction.key then
        storeTravelBarSelectedExtraKey(row.travelAction.key)
    end

    self:HideMenu()

    if row.menuMode == "hearths" then
        self:AdvanceRandomHearth(row.travelAction.key)
    else
        self:QueueRefresh(0.06)
    end
end

function TravelBarRuntime:RefreshMenu()
    local menu = self:EnsureMenu()
    if not menu:IsShown() or type(self.state) ~= "table" then
        return
    end

    local isEditing = menu.editing == true
    local entries
    if menu.mode == "hearths" then
        entries = isEditing and self.state.hearthsAll or self.state.hearths
    else
        entries = isEditing and self.state.specialsAll or self.state.extras
    end
    local width = 218
    local controlHeight = 20
    local rowHeight = 26
    local padding = 10

    if isEditing then
        menu.title:SetText(
            menu.mode == "hearths"
                and (L.FEATURE_TRAVEL_BAR_MENU_HEARTHS_EDIT or "Visible Hearthstones")
                or (L.FEATURE_TRAVEL_BAR_MENU_TRAVEL_EDIT or "Visible Travel")
        )
    else
        menu.title:SetText(
            menu.mode == "hearths"
                and (L.FEATURE_TRAVEL_BAR_MENU_HEARTHS or "Hearthstones")
                or (L.FEATURE_TRAVEL_BAR_MENU_TRAVEL or "Travel")
        )
    end

    menu.editButton:ClearAllPoints()
    menu.editButton:SetPoint("TOPLEFT", menu.divider, "BOTTOMLEFT", 0, -8)
    menu.editButton:SetPoint("TOPRIGHT", menu.divider, "BOTTOMRIGHT", 0, -8)
    menu.editButton.text:SetText(
        isEditing
            and (L.FEATURE_TRAVEL_BAR_MENU_EDIT_DONE or "Done")
            or (L.FEATURE_TRAVEL_BAR_MENU_EDIT or "Edit visible buttons")
    )
    menu.editButton.text:SetTextColor(
        isEditing and TRAVEL_BAR_COLORS.accentGold[1] or TRAVEL_BAR_COLORS.menuText[1],
        isEditing and TRAVEL_BAR_COLORS.accentGold[2] or TRAVEL_BAR_COLORS.menuText[2],
        isEditing and TRAVEL_BAR_COLORS.accentGold[3] or TRAVEL_BAR_COLORS.menuText[3],
        isEditing and 1 or TRAVEL_BAR_COLORS.menuText[4]
    )
    if menu.editButton.SetBackdropBorderColor then
        menu.editButton:SetBackdropBorderColor(
            isEditing and TRAVEL_BAR_COLORS.accentGoldSoft[1] or 0.20,
            isEditing and TRAVEL_BAR_COLORS.accentGoldSoft[2] or 0.20,
            isEditing and TRAVEL_BAR_COLORS.accentGoldSoft[3] or 0.22,
            isEditing and 0.86 or 0.88
        )
    end

    if type(entries) ~= "table" or #entries == 0 then
        entries = {
            {
                key = "empty",
                label = L.FEATURE_TRAVEL_BAR_MENU_EMPTY or "Nothing available right now.",
                icon = 134414,
                empty = true,
            },
        }
    end

    for index, action in ipairs(entries) do
        local row = self:EnsureMenuRow(index)
        row:Show()
        row:ClearAllPoints()
        if index == 1 then
            row:SetPoint("TOPLEFT", menu.editButton, "BOTTOMLEFT", 0, -6)
        else
            row:SetPoint("TOPLEFT", menu.rows[index - 1], "BOTTOMLEFT", 0, -4)
        end

        row.menuMode = menu.mode
        row.menuEditing = isEditing
        row.travelAction = action.empty and nil or action
        row.menuHidden = false
        row.icon:SetTexture(action.icon or 134414)
        row.icon:SetAlpha(1)
        if row.icon.SetDesaturated then
            row.icon:SetDesaturated(false)
        end
        row.label:SetText(action.label or ("Travel " .. tostring(index)))
        row:SetSize(width, rowHeight)
        row.stateLabel:Hide()
        row.toggleBox:Hide()
        row.toggleMark:Hide()

        if action.empty then
            row.label:SetTextColor(
                TRAVEL_BAR_COLORS.menuTextMuted[1],
                TRAVEL_BAR_COLORS.menuTextMuted[2],
                TRAVEL_BAR_COLORS.menuTextMuted[3],
                TRAVEL_BAR_COLORS.menuTextMuted[4]
            )
            row.accent:SetColorTexture(0.30, 0.30, 0.34, 0.28)
            applyTravelBarButtonAction(row, nil)
        elseif isEditing then
            local isHidden = isTravelBarActionHidden(action.key)
            row.menuHidden = isHidden
            row.stateLabel:SetShown(true)
            row.toggleBox:Show()
            row.toggleMark:SetShown(not isHidden)
            if row.toggleBox.SetBackdropBorderColor then
                row.toggleBox:SetBackdropBorderColor(
                    isHidden and 0.28 or TRAVEL_BAR_COLORS.accentGoldSoft[1],
                    isHidden and 0.28 or TRAVEL_BAR_COLORS.accentGoldSoft[2],
                    isHidden and 0.32 or TRAVEL_BAR_COLORS.accentGoldSoft[3],
                    0.92
                )
            end
            row.stateLabel:SetText(isHidden and (L.FEATURE_TRAVEL_BAR_MENU_STATE_HIDDEN or "Off") or (L.FEATURE_TRAVEL_BAR_MENU_STATE_SHOWN or "On"))
            row.stateLabel:SetTextColor(
                isHidden and TRAVEL_BAR_COLORS.menuTextMuted[1] or TRAVEL_BAR_COLORS.accentGold[1],
                isHidden and TRAVEL_BAR_COLORS.menuTextMuted[2] or TRAVEL_BAR_COLORS.accentGold[2],
                isHidden and TRAVEL_BAR_COLORS.menuTextMuted[3] or TRAVEL_BAR_COLORS.accentGold[3],
                isHidden and TRAVEL_BAR_COLORS.menuTextMuted[4] or 1
            )
            row.label:SetTextColor(
                isHidden and TRAVEL_BAR_COLORS.menuTextMuted[1] or TRAVEL_BAR_COLORS.menuText[1],
                isHidden and TRAVEL_BAR_COLORS.menuTextMuted[2] or TRAVEL_BAR_COLORS.menuText[2],
                isHidden and TRAVEL_BAR_COLORS.menuTextMuted[3] or TRAVEL_BAR_COLORS.menuText[3],
                isHidden and TRAVEL_BAR_COLORS.menuTextMuted[4] or TRAVEL_BAR_COLORS.menuText[4]
            )
            row.accent:SetColorTexture(
                isHidden and 0.34 or TRAVEL_BAR_COLORS.accentGoldSoft[1],
                isHidden and 0.20 or TRAVEL_BAR_COLORS.accentGoldSoft[2],
                isHidden and 0.20 or TRAVEL_BAR_COLORS.accentGoldSoft[3],
                isHidden and 0.48 or 0.62
            )
            applyTravelBarButtonAction(row, nil)
            row.travelAction = action
            row:SetAlpha(1)
            if row.icon.SetDesaturated then
                row.icon:SetDesaturated(isHidden)
            end
            row.icon:SetAlpha(isHidden and 0.52 or 1)
        else
            local isSelected = menu.mode == "extras" and self.state.more and self.state.more.key == action.key
            row.label:SetTextColor(
                isSelected and TRAVEL_BAR_COLORS.accentGold[1] or TRAVEL_BAR_COLORS.menuText[1],
                isSelected and TRAVEL_BAR_COLORS.accentGold[2] or TRAVEL_BAR_COLORS.menuText[2],
                isSelected and TRAVEL_BAR_COLORS.accentGold[3] or TRAVEL_BAR_COLORS.menuText[3],
                isSelected and 1 or TRAVEL_BAR_COLORS.menuText[4]
            )
            row.accent:SetColorTexture(
                isSelected and TRAVEL_BAR_COLORS.accentGold[1] or TRAVEL_BAR_COLORS.accentGoldSoft[1],
                isSelected and TRAVEL_BAR_COLORS.accentGold[2] or TRAVEL_BAR_COLORS.accentGoldSoft[2],
                isSelected and TRAVEL_BAR_COLORS.accentGold[3] or TRAVEL_BAR_COLORS.accentGoldSoft[3],
                isSelected and 0.94 or 0.26
            )
            applyTravelBarButtonAction(row, action)
            row.toggleBox:Hide()
            row.toggleMark:Hide()
        end

        refreshTravelBarCooldownForButton(row)
    end

    for index = #entries + 1, #menu.rows do
        menu.rows[index]:Hide()
    end

    menu:SetSize(width + padding * 2, 42 + controlHeight + 6 + #entries * rowHeight + math.max(0, #entries - 1) * 4 + padding)

    if menu.anchorButton and menu.anchorButton:IsShown() then
        menu:ClearAllPoints()
        local openUpward = ns:ShouldOpenMenuUpward(menu, menu.anchorButton)
        if getTravelBarOrientation() == "vertical" then
            if openUpward then
                menu:SetPoint("BOTTOMLEFT", menu.anchorButton, "BOTTOMRIGHT", 8, 0)
            else
                menu:SetPoint("TOPLEFT", menu.anchorButton, "TOPRIGHT", 8, 0)
            end
        else
            if openUpward then
                menu:SetPoint("BOTTOMLEFT", menu.anchorButton, "TOPLEFT", 0, 8)
            else
                menu:SetPoint("TOPLEFT", menu.anchorButton, "BOTTOMLEFT", 0, -8)
            end
        end
    end
end

function TravelBarRuntime:OpenMenu(mode, anchorButton, startEditing)
    if mode ~= "hearths" and mode ~= "extras" then
        return
    end

    if InCombatLockdown() then
        ns:ShowFeatureError(L.FEATURE_TRAVEL_BAR_ERROR_COMBAT or "Vaultloom: travel menus cannot open in combat.")
        return
    end

    self.state = buildTravelBarState()

    local menu = self:EnsureMenu()
    local resolvedAnchor = anchorButton
    if not resolvedAnchor or not resolvedAnchor.IsShown or not resolvedAnchor:IsShown() then
        resolvedAnchor = self.frame and self.frame.menuButton or nil
    end
    if not resolvedAnchor or not resolvedAnchor.IsShown or not resolvedAnchor:IsShown() then
        resolvedAnchor = self.frame
    end

    if menu:IsShown() and menu.anchorButton == resolvedAnchor and menu.mode == mode then
        self:HideMenu()
        return
    end

    menu.anchorButton = resolvedAnchor
    menu.mode = mode
    menu.editing = startEditing == true
    menu:Show()
    self:RefreshMenu()
    self:StartCooldownTicker()
    if menu.Raise then
        menu:Raise()
    end
end

function TravelBarRuntime:ToggleMenuForButton(button)
    if not button or (button.travelRole ~= "main" and button.travelRole ~= "more" and button.travelRole ~= "direct") then
        return
    end

    self:HideConfigPanel()
    local mode = button.travelRole == "main" and "hearths" or "extras"
    self:OpenMenu(mode, button, true)
end

function ns:ToggleTravelBarConfigPanel()
    TravelBarRuntime:ToggleConfigPanel()
end

function ns:OpenTravelBarConfigPanel()
    TravelBarRuntime:ShowConfigPanel(true)
end

function TravelBarRuntime:PrepareMainAction()
    if type(self.state) ~= "table" then
        self.state = buildTravelBarState()
    end

    local current = self.currentHearthKey and findTravelBarActionByKey(self.state.hearths, self.currentHearthKey) or nil
    if not current then
        current = pickRandomTravelBarAction(self.state.hearths, nil)
    end

    self.currentHearthAction = current
    self.currentHearthKey = current and current.key or nil
    self.state.main = current
end

function TravelBarRuntime:AdvanceRandomHearth(avoidKey)
    if type(self.state) ~= "table" then
        self.state = buildTravelBarState()
    end

    local nextAction = pickRandomTravelBarAction(self.state.hearths, avoidKey or self.currentHearthKey)
    self.currentHearthAction = nextAction
    self.currentHearthKey = nextAction and nextAction.key or nil
    self.state.main = nextAction
    self:QueueRefresh(0.06)
end

function TravelBarRuntime:StopCooldownTicker()
    self.cooldownTickerActive = false
    self.cooldownTickerToken = (tonumber(self.cooldownTickerToken) or 0) + 1
end

function TravelBarRuntime:StartCooldownTicker()
    if self.enabled ~= true or self.cooldownTickerActive == true then
        return
    end
    if not (C_Timer and type(C_Timer.After) == "function") then
        return
    end

    local frameVisible = self.frame and self.frame.IsShown and self.frame:IsShown()
    local menuVisible = self.menu and self.menu.IsShown and self.menu:IsShown()
    if not frameVisible and not menuVisible then
        return
    end

    self.cooldownTickerActive = true
    self.cooldownTickerToken = (tonumber(self.cooldownTickerToken) or 0) + 1
    local token = self.cooldownTickerToken

    local function tickCooldowns()
        if TravelBarRuntime.enabled ~= true or token ~= TravelBarRuntime.cooldownTickerToken then
            TravelBarRuntime.cooldownTickerActive = false
            return
        end

        local currentFrameVisible = TravelBarRuntime.frame
            and TravelBarRuntime.frame.IsShown
            and TravelBarRuntime.frame:IsShown()
        local currentMenuVisible = TravelBarRuntime.menu
            and TravelBarRuntime.menu.IsShown
            and TravelBarRuntime.menu:IsShown()
        if not currentFrameVisible and not currentMenuVisible then
            TravelBarRuntime.cooldownTickerActive = false
            return
        end

        TravelBarRuntime:RefreshCooldowns()
        C_Timer.After(TRAVEL_BAR_COOLDOWN_REFRESH_SECONDS, tickCooldowns)
    end

    C_Timer.After(TRAVEL_BAR_COOLDOWN_REFRESH_SECONDS, tickCooldowns)
end

function TravelBarRuntime:RefreshCooldowns()
    if self.enabled ~= true then
        return
    end

    for _, button in ipairs(self.buttons) do
        if button:IsShown() then
            refreshTravelBarCooldownForButton(button)
        end
    end

    if self.menu and self.menu:IsShown() then
        for _, row in ipairs(self.menu.rows or {}) do
            if row:IsShown() then
                refreshTravelBarCooldownForButton(row)
            end
        end
    end
end

function TravelBarRuntime:RefreshButtons()
    local frame = ensureTravelBarFrame()
    local orientation = getTravelBarOrientation()
    local frameStyle = getTravelBarFrameStyle()
    local showFrame = frameStyle ~= "hidden"
    local paddingX = showFrame and TRAVEL_BAR_FRAME_PADDING_X or 0
    local paddingY = showFrame and TRAVEL_BAR_FRAME_PADDING_Y or 0

    self.state = buildTravelBarState()
    self:PrepareMainAction()

    if #self.state.extras > 0 then
        self.state.more = findTravelBarActionByKey(self.state.extras, getStoredTravelBarSelectedExtraKey()) or self.state.extras[1]
        if self.state.more and self.state.more.key then
            storeTravelBarSelectedExtraKey(self.state.more.key)
        end
    else
        self.state.more = nil
    end

    local visibleButtons = {}
    if self.state.main then
        visibleButtons[#visibleButtons + 1] = {
            role = "main",
            action = self.state.main,
        }
    end
    for _, action in ipairs(self.state.direct) do
        visibleButtons[#visibleButtons + 1] = {
            role = "direct",
            action = action,
        }
    end
    if self.state.more then
        visibleButtons[#visibleButtons + 1] = {
            role = "more",
            action = self.state.more,
        }
    end

    if #visibleButtons == 0 then
        self:HideMenu()
        self:HideConfigPanel()
        self:StopCooldownTicker()
        if frame.menuButton then
            frame.menuButton:Hide()
        end
        for _, dragHandle in pairs(frame.dragHandles or {}) do
            dragHandle:Hide()
        end
        frame:Hide()
        return
    end

    local buttonAreaWidth = orientation == "horizontal"
        and (#visibleButtons * TRAVEL_BAR_BUTTON_SIZE + math.max(0, #visibleButtons - 1) * TRAVEL_BAR_BUTTON_GAP)
        or TRAVEL_BAR_BUTTON_SIZE
    local buttonAreaHeight = orientation == "horizontal"
        and TRAVEL_BAR_BUTTON_SIZE
        or (#visibleButtons * TRAVEL_BAR_BUTTON_SIZE + math.max(0, #visibleButtons - 1) * TRAVEL_BAR_BUTTON_GAP)
    local frameWidth = showFrame and (buttonAreaWidth + paddingX * 2) or buttonAreaWidth
    local frameHeight = showFrame and (buttonAreaHeight + paddingY * 2) or buttonAreaHeight

    frame:SetSize(frameWidth, frameHeight)

    if frame.SetBackdrop then
        if showFrame then
            frame:SetBackdropColor(
                TRAVEL_BAR_COLORS.frameBackdrop[1],
                TRAVEL_BAR_COLORS.frameBackdrop[2],
                TRAVEL_BAR_COLORS.frameBackdrop[3],
                TRAVEL_BAR_COLORS.frameBackdrop[4]
            )
            frame:SetBackdropBorderColor(
                TRAVEL_BAR_COLORS.frameBorder[1],
                TRAVEL_BAR_COLORS.frameBorder[2],
                TRAVEL_BAR_COLORS.frameBorder[3],
                TRAVEL_BAR_COLORS.frameBorder[4]
            )
        else
            frame:SetBackdropColor(0, 0, 0, 0)
            frame:SetBackdropBorderColor(0, 0, 0, 0)
        end
    end

    if frame.bg then
        frame.bg:SetShown(showFrame)
    end
    if frame.inner then
        frame.inner:SetShown(showFrame)
    end
    frame.topAccent:SetShown(showFrame)
    frame.bottomAccent:SetShown(showFrame)

    frame.topAccent:ClearAllPoints()
    frame.bottomAccent:ClearAllPoints()
    if orientation == "horizontal" then
        frame.topAccent:SetPoint("TOPLEFT", frame, "TOPLEFT", 6, -2)
        frame.topAccent:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -2)
        frame.topAccent:SetHeight(1)
        frame.bottomAccent:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 6, 2)
        frame.bottomAccent:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -6, 2)
        frame.bottomAccent:SetHeight(1)
    else
        frame.topAccent:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -6)
        frame.topAccent:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 2, 6)
        frame.topAccent:SetWidth(1)
        frame.bottomAccent:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -2, -6)
        frame.bottomAccent:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -2, 6)
        frame.bottomAccent:SetWidth(1)
    end
    frame.topAccent:SetColorTexture(
        TRAVEL_BAR_COLORS.accentGold[1],
        TRAVEL_BAR_COLORS.accentGold[2],
        TRAVEL_BAR_COLORS.accentGold[3],
        0.88
    )
    frame.bottomAccent:SetColorTexture(
        TRAVEL_BAR_COLORS.accentGold[1],
        TRAVEL_BAR_COLORS.accentGold[2],
        TRAVEL_BAR_COLORS.accentGold[3],
        0.88
    )

    local dragHandles = frame.dragHandles or {}
    for _, dragHandle in pairs(dragHandles) do
        dragHandle:Hide()
    end

    for index, buttonData in ipairs(visibleButtons) do
        local button = ensureTravelBarButton(index)
        button:SetSize(TRAVEL_BAR_BUTTON_SIZE, TRAVEL_BAR_BUTTON_SIZE)
        button:ClearAllPoints()
        if orientation == "horizontal" then
            local xOffset = paddingX + (index - 1) * (TRAVEL_BAR_BUTTON_SIZE + TRAVEL_BAR_BUTTON_GAP)
            button:SetPoint("TOPLEFT", frame, "TOPLEFT", xOffset, -paddingY)
        else
            local xOffset = math.floor((frameWidth - TRAVEL_BAR_BUTTON_SIZE) * 0.5 + 0.5)
            local yOffset = paddingY + (index - 1) * (TRAVEL_BAR_BUTTON_SIZE + TRAVEL_BAR_BUTTON_GAP)
            button:SetPoint("TOPLEFT", frame, "TOPLEFT", xOffset, -yOffset)
        end

        button.travelRole = buttonData.role
        button.backplate:Hide()
        button.iconShadow:Hide()
        button.highlight:SetAlpha(0)
        button.activeGlow:SetAlpha(0)
        button.ring:SetVertexColor(
            TRAVEL_BAR_COLORS.inactiveRing[1],
            TRAVEL_BAR_COLORS.inactiveRing[2],
            TRAVEL_BAR_COLORS.inactiveRing[3],
            TRAVEL_BAR_COLORS.inactiveRing[4]
        )
        button.ring:SetAlpha(0.92)
        button.stateBar:SetColorTexture(
            TRAVEL_BAR_COLORS.accentGoldSoft[1],
            TRAVEL_BAR_COLORS.accentGoldSoft[2],
            TRAVEL_BAR_COLORS.accentGoldSoft[3],
            0.48
        )
        button.stateBar:Hide()
        applyTravelBarButtonAction(button, buttonData.action)
        refreshTravelBarCooldownForButton(button)
        button:Show()
    end

    if frame.menuButton then
        frame.menuButton:Hide()
    end

    for index = #visibleButtons + 1, #self.buttons do
        self.buttons[index]:Hide()
    end
end

function TravelBarRuntime:RefreshAll()
    if self.enabled ~= true then
        return
    end

    if InCombatLockdown() then
        self.pendingRefresh = true
        return
    end

    self.pendingRefresh = false
    local frame = ensureTravelBarFrame()
    ns:ApplyFeaturePosition(frame, TRAVEL_BAR_POSITION_KEY, TRAVEL_BAR_DEFAULT_POSITION)
    ns:ApplyFeatureScale(frame, TRAVEL_BAR_SCALE_KEY, TRAVEL_BAR_SCALE_MIN, TRAVEL_BAR_SCALE_MAX)
    self:RefreshButtons()

    if frame:IsShown() or (type(self.state) == "table" and (self.state.main or #self.state.direct > 0 or #self.state.extras > 0)) then
        frame:Show()
    end

    if self.menu and self.menu:IsShown() then
        self:RefreshMenu()
    end
    self:StartCooldownTicker()
end

function TravelBarRuntime:QueueRefresh(delay)
    if self.enabled ~= true then
        return
    end

    if InCombatLockdown() then
        self.pendingRefresh = true
        return
    end

    if self.refreshQueued then
        return
    end

    self.refreshQueued = true
    if C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(delay or 0.08, function()
            TravelBarRuntime.refreshQueued = false
            if TravelBarRuntime.enabled ~= true then
                return
            end
            TravelBarRuntime:RefreshAll()
        end)
    else
        self.refreshQueued = false
        self:RefreshAll()
    end
end

function TravelBarRuntime:SetEnabled(enabled)
    enabled = enabled == true
    self.enabled = enabled

    if not self.eventFrame then
        self.eventFrame = CreateFrame("Frame")
        self.eventFrame:SetScript("OnEvent", function(_, event)
            if event == "PLAYER_REGEN_DISABLED" then
                TravelBarRuntime:HideMenu()
                return
            end

            if event == "PLAYER_REGEN_ENABLED" then
                if TravelBarRuntime.pendingRefresh then
                    TravelBarRuntime:RefreshAll()
                end
                return
            end

            if event == "BAG_UPDATE_COOLDOWN" or event == "SPELL_UPDATE_COOLDOWN" then
                if type(TravelBarRuntime.state) == "table" then
                    TravelBarRuntime:RefreshCooldowns()
                    TravelBarRuntime:StartCooldownTicker()
                else
                    TravelBarRuntime:QueueRefresh(0.20)
                end
                return
            end

            if event == "PLAYER_ENTERING_WORLD" then
                if ns.UI and ns.UI.CaptureCurrentHousingSnapshot then
                    pcall(function()
                        ns.UI:CaptureCurrentHousingSnapshot(true)
                    end)
                end
                if type(C_Housing) == "table"
                    and type(C_Housing.GetPlayerOwnedHouses) == "function"
                    and ns.RememberHousingHouseList
                then
                    local ok, houseInfoList = pcall(C_Housing.GetPlayerOwnedHouses)
                    if ok and type(houseInfoList) == "table" then
                        ns:RememberHousingHouseList(houseInfoList)
                    end
                end
                TravelBarRuntime:QueueRefresh(0.18)
                if C_Timer and type(C_Timer.After) == "function" then
                    C_Timer.After(1.0, function()
                        if TravelBarRuntime.enabled == true then
                            if ns.UI and ns.UI.CaptureCurrentHousingSnapshot then
                                pcall(function()
                                    ns.UI:CaptureCurrentHousingSnapshot(true)
                                end)
                            end
                            TravelBarRuntime:RefreshAll()
                        end
                    end)
                    C_Timer.After(2.5, function()
                        if TravelBarRuntime.enabled == true then
                            if ns.UI and ns.UI.CaptureCurrentHousingSnapshot then
                                pcall(function()
                                    ns.UI:CaptureCurrentHousingSnapshot(true)
                                end)
                            end
                            TravelBarRuntime:RefreshAll()
                        end
                    end)
                end
                return
            end

            TravelBarRuntime:QueueRefresh(0.06)
        end)
    end

    self.eventFrame:UnregisterAllEvents()
    self.pendingRefresh = false
    self.refreshQueued = false
    self.state = nil

    if not enabled then
        self:StopCooldownTicker()
        self:HideMenu()
        if self.frame then
            for _, dragHandle in pairs(self.frame.dragHandles or {}) do
                dragHandle:Hide()
            end
            self.frame:Hide()
        end
        return
    end

    self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    self.eventFrame:RegisterEvent("BAG_UPDATE_DELAYED")
    self.eventFrame:RegisterEvent("BAG_UPDATE_COOLDOWN")
    self.eventFrame:RegisterEvent("SPELLS_CHANGED")
    self.eventFrame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
    self.eventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
    self.eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    self.eventFrame:RegisterEvent("NEW_TOY_ADDED")
    self.eventFrame:RegisterEvent("TOYS_UPDATED")
    self.eventFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
    self.eventFrame:RegisterEvent("PLAYER_HOUSE_LIST_UPDATED")

    self:RefreshAll()
    if C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(1.0, function()
            if TravelBarRuntime.enabled == true then
                if ns.UI and ns.UI.CaptureCurrentHousingSnapshot then
                    pcall(function()
                        ns.UI:CaptureCurrentHousingSnapshot(true)
                    end)
                end
                TravelBarRuntime:RefreshAll()
            end
        end)
    end
end

ns:RegisterFeatureDefinition("travel_bar", {
    id = "travel_bar",
    order = 21,
    category = "quick_tools",
    title = function() return L.FEATURE_TRAVEL_BAR_TITLE or "Travel Bar" end,
    description = function() return L.FEATURE_TRAVEL_BAR_DESCRIPTION or "Shows a movable hearthstone and travel bar with smart teleport shortcuts." end,
    note = function() return L.FEATURE_TRAVEL_BAR_NOTE or "Main button uses a random hearthstone. Other buttons use direct travel options like Dalaran, Garrison, class travel, and extra teleports." end,
    previewLabel = function() return L.FEATURE_TRAVEL_BAR_PREVIEW_LABEL or "Travel Dock" end,
    previewNote = function() return L.FEATURE_TRAVEL_BAR_PREVIEW_NOTE or "Compact hearthstone and teleport icons with drag support, menus, and Shift + mouse wheel scaling." end,
    optionToggle = {
        key = "orientation",
        label = function() return L.FEATURE_TRAVEL_BAR_ORIENTATION_LABEL or "Layout" end,
        values = { "horizontal", "vertical" },
        defaultValue = "horizontal",
        valueLabels = {
            horizontal = function() return L.FEATURE_TRAVEL_BAR_ORIENTATION_HORIZONTAL or "Horizontal" end,
            vertical = function() return L.FEATURE_TRAVEL_BAR_ORIENTATION_VERTICAL or "Vertical" end,
        },
    },
    secondaryOptionToggle = {
        key = "frame_style",
        label = function() return L.FEATURE_TRAVEL_BAR_FRAME_LABEL or "Frame" end,
        values = { "shown", "hidden" },
        defaultValue = "shown",
        valueLabels = {
            shown = function() return L.FEATURE_TRAVEL_BAR_FRAME_SHOWN or "Frame" end,
            hidden = function() return L.FEATURE_TRAVEL_BAR_FRAME_HIDDEN or "Clean" end,
        },
    },
    requiresReload = false,
    apply = function(isEnabled)
        TravelBarRuntime:SetEnabled(isEnabled == true)
    end,
    onOptionChanged = function()
        if TravelBarRuntime.enabled == true then
            TravelBarRuntime:RefreshAll()
        end
    end,
})
end

registerTravelBarFeature()
