random script i found
Key RequiredUniversal

random script i found

Universal Scriptaswqtr876aswqtr876 45 views 284 executesUpdated Aug 4, 2026
Unrated
00
Lua Script•3003 lines (133541 chars)
--[[
    ╔══════════════════════════════════════════════════════╗
    ║         FNF ANIMATION INJECTOR v6.1                  ║
    ║  Arrow HUD • Dynamic Face • Anim Maker • Leg Lock    ║
    ║  + Idle BPM Speed Control                             ║
    ║  + Live Preview • Favorites • Playlist • Themes       ║
    ║  + Hold-to-Loop + Weighted Alt Animations (per bind)  ║
    ╚══════════════════════════════════════════════════════╝
    • Z/X/C/V  → play directional anims
    • Arrow HUD reacts on screen when you hit keys
    • Face decal swaps per direction (if your character has one)
    • Animation Sequence Maker (chain anims with delays)
    • Optional Legs-on-Ground lock (no walk / no jump)
    • Idle animation speed scales with BPM
    • Live character viewport preview
    • Equalizer / waveform / beat-sync visualizers
    • Favorites, recents, presets, playlist manager
    • Pink / Cyan / Purple / RGB theme selector
    • NEW: per-direction HOLD-LOOP toggle — hold the key to keep the
      animation looping instead of snap-freezing on the pose
    • NEW: per-direction weighted ALT ANIMATIONS — each key press can
      roll into an alt animation instead of the primary one, at
      whatever % chance you configure
]]

-- ============================================================
--  SERVICES & GLOBALS
-- ============================================================
local Players          = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local TweenService     = game:GetService("TweenService")
local RunService       = game:GetService("RunService")
local LocalPlayer      = Players.LocalPlayer
local PlayerGui        = LocalPlayer:WaitForChild("PlayerGui")

-- ============================================================
--  DEFAULT BINDINGS
-- ============================================================
local DEFAULT_BINDINGS = {
    { dir = "LEFT",  key = "Z", id = "243827693" },
    { dir = "DOWN",  key = "X", id = "182724289" },
    { dir = "UP",    key = "C", id = "183412246" },
    { dir = "RIGHT", key = "V", id = "243656287" },
}
local CurrentBindings = {}
for _, v in ipairs(DEFAULT_BINDINGS) do
    -- holdLoop: false = original "snap to pose and freeze" behavior,
    --           true  = animation actually loops for as long as the key is held
    -- alts: { {id = "123", chance = 25}, ... } — weighted alt anim IDs,
    --       rolled against on every key press; uncovered % falls back to `id`
    table.insert(CurrentBindings, { dir = v.dir, key = v.key, id = v.id, speedMult = 1, holdLoop = false, alts = {} })
end

-- Placeholder quick-preset library (edit IDs freely; these are just slots)
local PRESETS = {
    { name = "Default Set",  ids = { LEFT="204062532", DOWN="204292303", UP="183412246", RIGHT="218504594" } },
    { name = "Preset Slot A",ids = { LEFT="0", DOWN="0", UP="0", RIGHT="0" } },
    { name = "Preset Slot B",ids = { LEFT="0", DOWN="0", UP="0", RIGHT="0" } },
}

-- ============================================================
--  STATE
-- ============================================================
local animTracks   = {}
local idleTrack    = nil
local connections  = {}
local animator     = nil
local character    = nil
local rebindingRow = nil
local guiVisible   = true
local idleLooping  = false

-- RP Tug-of-War State
local rpModeEnabled   = false
local currentTarget   = nil
local rpMoveStep      = 1.5      -- Base speed/distance per note
local rpMinDistance   = 3.5      -- How close before stopping
local rpPushbackStep  = 1.0      -- How far you get pushed back when the target hits a note
local targetAnimConn  = nil      -- Stores the listener for the target's animations
-- Legs on ground
local legsLocked     = false
local savedWalkSpeed = 16
local savedJumpPower = 50

-- Face system
local faceDecal     = nil
local defaultFaceId = nil
local faceIds = { LEFT = "", DOWN = "", UP = "", RIGHT = "" }

-- Sequence maker
local sequence     = {}
local seqPlaying   = false
local seqTrack     = nil
local seqRowFrames = {}

-- Arrow HUD handles
local arrowHudFrames = {}  -- [dir] = { bg, lbl }

-- Idle BPM control
local idleBPM  = 100  -- the tempo you want the idle to play at
local BASE_BPM = 58   -- the tempo the idle animation was originally made/synced for

-- Status / playback tracking (drives the status panel + visualizers)
local StatusState = {
    currentAnimName = "—",
    currentAnimId   = "",
    speedMult       = 1,
    bpm             = idleBPM,
    playing         = false,
}

-- Favorites / recents / playlist
-- Seeded with Roblox's own default emote IDs — these are built into every
-- avatar by Roblox itself, so they're free to use and always available.
local favorites = {
    { name = "Default: Dance",  id = "507771019" },
    { name = "Default: Dance2", id = "507776043" },
    { name = "Default: Dance3", id = "507777268" },
    { name = "Default: Laugh",  id = "507770818" },
    { name = "Default: Cheer",  id = "507770677" },
    { name = "Default: Point",  id = "507770453" },
    { name = "Default: Sit",    id = "2506281703" },
}
local recents       = {}   -- { id, id, ... } most-recent-first, capped
local RECENTS_CAP   = 8
local playlist      = {}   -- { {name=, id=, delay=} }
local playlistIndex = 1
local playlistPlaying = false

-- Preview viewport
local previewClone     = nil
local previewAnimator  = nil
local previewCam       = nil
local previewAngle     = 0

-- Theme
local THEMES = {
    Pink   = { accent = Color3.fromRGB(255, 80, 180), accent2 = Color3.fromRGB(0, 200, 255) },
    Cyan   = { accent = Color3.fromRGB(0, 200, 255),  accent2 = Color3.fromRGB(255, 80, 180) },
    Purple = { accent = Color3.fromRGB(165, 85, 255), accent2 = Color3.fromRGB(255, 80, 180) },
    RGB    = { accent = Color3.fromRGB(255, 0, 0),    accent2 = Color3.fromRGB(0, 255, 255) },
}
local currentThemeName = "Pink"
local themedStrokes = {}   -- UIStroke objects to recolor with accent
local themedBgs     = {}   -- Frame/TextButton objects to recolor with accent
local themedTexts   = {}   -- TextLabel objects to recolor with accent
local rgbCycleOn    = false

-- ============================================================
--  VINYL RECORD WIDGET
-- ============================================================
local vinylImage       = nil   -- ImageLabel, set your decal via vinylImage.Image or the Settings box
local VINYL_DECAL_ID   = ""    -- paste an asset id here, or set it live from the FX tab
local vinylRotation    = 0
local vinylBaseRPM     = 33.3  -- classic vinyl RPM at speedMult == 1 / idle BPM baseline

-- ============================================================
--  CRT MONITOR MODE
-- ============================================================
local crtEnabled  = false
local crtOverlay  = nil        -- Frame that holds scanlines + tint, toggled visible
local crtFlickerConn = nil

-- ============================================================
--  SPECTROGRAM (MIC) VISUALIZER
-- ============================================================
-- Uses Roblox's newer Audio API (AudioDeviceInput + AudioAnalyzer). This is
-- still a fairly recent, evolving part of the engine, needs the experience
-- to have voice/microphone permission granted by the player, and isn't
-- guaranteed to exist on every client — everything below is wrapped in
-- pcall and falls back to an animation-synced "fake" spectrogram if real
-- mic analysis isn't available.
local micDeviceInput = nil
local micAnalyzer    = nil
local micWire        = nil
local micAvailable    = false
local specBars        = {}
local SPEC_BAR_COUNT   = 20

-- ============================================================
--  ANIMATION BPM DETECTOR
-- ============================================================
-- Two detection modes:
--  1) TAP MODE: every directional key press timestamp is recorded; the
--     rolling average gap between presses is converted straight to BPM.
--  2) IDLE-LENGTH MODE: BPM is derived from the currently loaded idle
--     animation's length and a user-supplied "beats per loop" count.
local bpmDetectTaps     = {}   -- recent os.clock() timestamps of direction presses
local BPM_TAP_WINDOW    = 8    -- how many taps to average over
local detectedBPM       = nil
local beatsPerIdleLoop  = 4

-- ============================================================
--  SHARED MUSIC PLAYER (no VC — plays an uploaded audio asset for
--  every player in the server via a server-replicated Sound, instead
--  of routing anything through voice chat)
-- ============================================================
-- Requires a small companion SERVER script (provided separately) that
-- creates the "FNFMusicRemote" RemoteEvent and the actual Sound
-- instance — a LocalScript alone cannot make a sound audible to other
-- players, only the server can do that.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SoundService      = game:GetService("SoundService")
local musicRemote       = nil
local MUSIC_SOUND_NAME  = "FNFMusicPlayer"
local musicStatusLbl    = nil -- assigned once the FX tab is built
local musicRequestTick  = 0

-- ============================================================
--  CONSTANTS
-- ============================================================
local PRIORITY     = Enum.AnimationPriority.Action
local FRAME_TARGET = 10
local BLEND_IN     = 0
local BLEND_OUT    = 0.05

-- FNF arrow colours
local ARROW_COLORS = {
    LEFT  = { idle = Color3.fromRGB(150, 50, 130), hit = Color3.fromRGB(255, 110, 230) },
    DOWN  = { idle = Color3.fromRGB(0,  130, 170), hit = Color3.fromRGB(0,   240, 255) },
    UP    = { idle = Color3.fromRGB(15, 130,  10), hit = Color3.fromRGB(20,  250,   5) },
    RIGHT = { idle = Color3.fromRGB(160, 35,  35), hit = Color3.fromRGB(255,  90,  90) },
}
local ARROW_SYMBOLS = { LEFT = "◄", DOWN = "▼", UP = "▲", RIGHT = "►" }

-- Arrow HUD layout
local ARROW_SIZE     = 64
local ARROW_HIT_SIZE = 74
local ARROW_GAP      = 14
local ARROW_ORDER    = { "LEFT", "DOWN", "UP", "RIGHT" }

-- ============================================================
--  LOGGING
-- ============================================================
local function log(m) print("[FNF] "..tostring(m)) end

-- ============================================================
--  ANIMATOR HELPERS
-- ============================================================
local function getAnimator(char)
    local hum = char:FindFirstChildOfClass("Humanoid")
    if not hum then return nil end
    local a = hum:FindFirstChildOfClass("Animator")
    if not a then a = Instance.new("Animator"); a.Parent = hum end
    return a
end

local function destroyTrack(key)
    local t = animTracks[key]
    if t then pcall(function() t:Stop(0) end) pcall(function() t:Destroy() end) animTracks[key] = nil end
end

local function destroyIdleTrack()
    if idleTrack then pcall(function() idleTrack:Stop(0) end) pcall(function() idleTrack:Destroy() end) idleTrack = nil end
    idleLooping = false
end

local function destroySeqTrack()
    if seqTrack then pcall(function() seqTrack:Stop(0) end) pcall(function() seqTrack:Destroy() end) seqTrack = nil end
end

local function cleanupAll()
    idleLooping = false; seqPlaying = false
    destroyIdleTrack(); destroySeqTrack()
    for k in pairs(animTracks) do destroyTrack(k) end
    for _, c in ipairs(connections) do pcall(function() c:Disconnect() end) end
    connections = {}; animator = nil; character = nil
    faceDecal = nil; defaultFaceId = nil
end

-- ============================================================
--  NOTIFICATION TOASTS  (forward-declared, container built in GUI section)
-- ============================================================
local notifyContainer = nil
local function notify(text, kind)
    if not notifyContainer then log("[toast] "..text); return end
    kind = kind or "info"
    local colors = {
        info    = Color3.fromRGB(0, 200, 255),
        success = Color3.fromRGB(80, 220, 120),
        warn    = Color3.fromRGB(255, 180, 60),
        error   = Color3.fromRGB(255, 80, 80),
    }
    local toast = Instance.new("Frame")
    toast.Size = UDim2.new(1, 0, 0, 40)
    toast.BackgroundColor3 = Color3.fromRGB(20, 20, 30)
    toast.BackgroundTransparency = 0.05
    toast.BorderSizePixel = 0
    toast.ClipsDescendants = true
    toast.Parent = notifyContainer
    local corner = Instance.new("UICorner"); corner.CornerRadius = UDim.new(0,8); corner.Parent = toast
    local stroke = Instance.new("UIStroke"); stroke.Color = colors[kind] or colors.info; stroke.Thickness = 1.5; stroke.Parent = toast

    local bar = Instance.new("Frame")
    bar.Size = UDim2.new(0, 4, 1, 0)
    bar.BackgroundColor3 = colors[kind] or colors.info
    bar.BorderSizePixel = 0
    bar.Parent = toast

    local lbl = Instance.new("TextLabel")
    lbl.Size = UDim2.new(1, -16, 1, 0); lbl.Position = UDim2.new(0, 12, 0, 0)
    lbl.BackgroundTransparency = 1; lbl.Text = text
    lbl.TextColor3 = Color3.fromRGB(230,230,240); lbl.Font = Enum.Font.GothamBold
    lbl.TextSize = 12; lbl.TextXAlignment = Enum.TextXAlignment.Left
    lbl.TextWrapped = true; lbl.Parent = toast

    toast.Position = UDim2.new(1, 20, 0, 0)
    TweenService:Create(toast, TweenInfo.new(0.25, Enum.EasingStyle.Quint, Enum.EasingDirection.Out), { Position = UDim2.new(0,0,0,0) }):Play()

    task.delay(2.6, function()
        if not toast or not toast.Parent then return end
        local t = TweenService:Create(toast, TweenInfo.new(0.25, Enum.EasingStyle.Quint, Enum.EasingDirection.In), { BackgroundTransparency = 1 })
        TweenService:Create(lbl, TweenInfo.new(0.2), { TextTransparency = 1 }):Play()
        TweenService:Create(stroke, TweenInfo.new(0.2), { Transparency = 1 }):Play()
        t:Play()
        t.Completed:Connect(function() if toast then toast:Destroy() end end)
    end)
end

-- ============================================================
--  THEME SYSTEM
-- ============================================================
local function regStroke(obj) table.insert(themedStrokes, obj); obj.Color = THEMES[currentThemeName].accent end
local function regBg(obj)     table.insert(themedBgs, obj);     obj.BackgroundColor3 = THEMES[currentThemeName].accent end
local function regText(obj)   table.insert(themedTexts, obj);   obj.TextColor3 = THEMES[currentThemeName].accent end

local function applyThemeColors(accent)
    for _, o in ipairs(themedStrokes) do pcall(function() o.Color = accent end) end
    for _, o in ipairs(themedBgs)     do pcall(function() o.BackgroundColor3 = accent end) end
    for _, o in ipairs(themedTexts)   do pcall(function() o.TextColor3 = accent end) end
end

local function setTheme(name)
    if not THEMES[name] then return end
    currentThemeName = name
    rgbCycleOn = (name == "RGB")
    if not rgbCycleOn then
        applyThemeColors(THEMES[name].accent)
    end
    notify("Theme set to "..name, "info")
end

-- RGB cycling loop (only active while theme == RGB)
task.spawn(function()
    local hue = 0
    while true do
        task.wait(0.05)
        if rgbCycleOn then
            hue = (hue + 0.006) % 1
            applyThemeColors(Color3.fromHSV(hue, 0.85, 1))
        end
    end
end)

-- ============================================================
--  FACE SYSTEM
-- ============================================================
local function initFace(char)
    local head = char:FindFirstChild("Head")
    if not head then return end
    local d = head:FindFirstChild("face")
    if not d or d.ClassName ~= "Decal" then return end
    faceDecal = d
    defaultFaceId = d.Texture
    log("Face found: "..defaultFaceId)
end

local function swapFace(dir)
    if not faceDecal then return end
    local id = faceIds[dir]
    if id and id ~= "" then
        faceDecal.Texture = "rbxassetid://"..id
    end
end

local function restoreFace()
    if faceDecal and defaultFaceId then
        faceDecal.Texture = defaultFaceId
    end
end

-- ============================================================
--  LEGS ON GROUND
-- ============================================================
local function applyLegsLock(locked)
    if not character then return end
    local hum = character:FindFirstChildOfClass("Humanoid")
    if not hum then return end
    if locked then
        savedWalkSpeed = hum.WalkSpeed
        savedJumpPower = hum.JumpPower
        hum.WalkSpeed = 0
        hum.JumpPower = 0
    else
        hum.WalkSpeed = savedWalkSpeed
        hum.JumpPower = savedJumpPower
    end
end

-- ============================================================
--  RECENTS
-- ============================================================
local function pushRecent(id, label)
    if not id or id == "" then return end
    for i = #recents, 1, -1 do
        if recents[i].id == id then table.remove(recents, i) end
    end
    table.insert(recents, 1, { id = id, label = label or id })
    while #recents > RECENTS_CAP do table.remove(recents) end
    if refreshRecentsUI then refreshRecentsUI() end
end

-- ============================================================
--  LIVE PREVIEW HELPERS (forward declared, viewport built in GUI section)
-- ============================================================
local function updatePreviewAnimation(assetId, label)
    StatusState.currentAnimName = label or assetId
    StatusState.currentAnimId = assetId
    if refreshStatusUI then refreshStatusUI() end
    if not previewAnimator or not assetId or assetId == "" then return end
    local ok, result = pcall(function()
        local anim = Instance.new("Animation")
        anim.AnimationId = "rbxassetid://"..assetId
        local t = previewAnimator:LoadAnimation(anim)
        t.Priority = Enum.AnimationPriority.Action
        return t
    end)
    if ok and result then
        for _, t in ipairs(previewAnimator:GetPlayingAnimationTracks()) do t:Stop(0) end
        result:Play(0.05)
    end
end
-- ============================================================
--  RP TUG-OF-WAR STATE & CONFIG
-- ============================================================
local rpModeEnabled       = true
local currentTarget       = nil

-- Forward Movement (YOUR Note Hits)
local rpMoveStep          = 1.5   -- Distance you slide forward per note
local rpMinDistance       = 3.5   -- Closest you can get to target

-- Backward Movement (TARGET'S Note Hits / Health Drain)
local rpRetreatStep       = 1.2   -- Distance you get pulled back when target hits a note
local rpMaxDistance       = 25.0  -- Maximum pushback distance limit

-- Event Connections
local targetAnimConn      = nil   -- Stores AnimationPlayed listener
local targetCharAddedConn = nil   -- Stores target respawn listener


-- ============================================================
--  TUG-OF-WAR LOGIC
-- ============================================================

-- Disconnects active listeners to prevent duplicate triggers or memory leaks
local function stopWatchingTargetAnims()
    if targetAnimConn then
        targetAnimConn:Disconnect()
        targetAnimConn = nil
    end
    if targetCharAddedConn then
        targetCharAddedConn:Disconnect()
        targetCharAddedConn = nil
    end
end

-- Pushes YOUR character BACKWARD when the TARGET plays an animation
local function triggerRetreat()
    if not rpModeEnabled or not currentTarget then return end
    if not character or not currentTarget.Character then return end

    local myRoot = character:FindFirstChild("HumanoidRootPart")
    local targetRoot = currentTarget.Character:FindFirstChild("HumanoidRootPart")
    if not myRoot or not targetRoot then return end

    -- Vector pointing directly AWAY from the target
    local awayVector = (myRoot.Position - targetRoot.Position)
    local distance = awayVector.Magnitude

    if distance <= 0.001 then return end -- Avoid divide-by-zero if overlapping

    -- Push backward only if within max distance
    if distance < rpMaxDistance then
        local actualStep = math.min(rpRetreatStep, rpMaxDistance - distance)
        myRoot.CFrame = myRoot.CFrame + (awayVector.Unit * actualStep)
    end
end

-- Hooks into the target's Animator to detect any incoming notes/anims
local function startWatchingTargetAnims()
    stopWatchingTargetAnims()
    if not currentTarget then return end

    local function bindAnimator(targetChar)
        if not targetChar then return end
        
        local hum = targetChar:WaitForChild("Humanoid", 5)
        if not hum then return end
        
        local anr = hum:WaitForChild("Animator", 5) or hum:FindFirstChildOfClass("Animator")
        if not anr then return end

        -- Triggers retreat every time target plays ANY animation track
        targetAnimConn = anr.AnimationPlayed:Connect(function(track)
            if rpModeEnabled then
                triggerRetreat()
            end
        end)
    end

    -- Bind immediately if character exists
    if currentTarget.Character then
        bindAnimator(currentTarget.Character)
    end

    -- Re-bind automatically if target dies/respawns
    targetCharAddedConn = currentTarget.CharacterAdded:Connect(function(newChar)
        stopWatchingTargetAnims()
        bindAnimator(newChar)
    end)
end

-- Target Selection Function
local function setTargetPlayer(partialName)
    if not partialName or partialName == "" then
        currentTarget = nil
        stopWatchingTargetAnims()
        if notify then notify("RP Target cleared.", "info") end
        return nil
    end

    local lowerName = partialName:lower()
    for _, player in ipairs(Players:GetPlayers()) do
        if player ~= LocalPlayer and player.Name:lower():sub(1, #lowerName) == lowerName then
            currentTarget = player
            startWatchingTargetAnims()
            if notify then notify("Target locked: " .. player.Name, "success") end
            return player
        end
    end

    if notify then notify("Player not found.", "warn") end
    return nil
end

-- Pushes YOUR character FORWARD when YOU hit a note key
local function triggerRPMovement()
    if not rpModeEnabled or not currentTarget then return end
    if not character or not currentTarget.Character then return end

    local myRoot = character:FindFirstChild("HumanoidRootPart")
    local targetRoot = currentTarget.Character:FindFirstChild("HumanoidRootPart")
    if not myRoot or not targetRoot then return end

    local direction = (targetRoot.Position - myRoot.Position)
    local distance = direction.Magnitude

    -- Slide forward towards target
    if distance > rpMinDistance then
        local actualStep = math.min(rpMoveStep, distance - rpMinDistance)
        myRoot.CFrame = myRoot.CFrame + (direction.Unit * actualStep)
    end
end
 
-- ============================================================
--  DIRECTIONAL ANIMATION PLAYBACK
-- ============================================================
-- holdLoop: false (default) reproduces the original behavior — quickly
-- play to the target frame, then freeze there until the key is released.
-- holdLoop: true instead keeps the animation genuinely looping at
-- speedMult for as long as the key stays down, and lets stopAnim's
-- normal blend-out handle the release.
local function playAnim(key, assetId, speedMult, holdLoop)
    if not animator then return end
    speedMult = speedMult or 1
    if speedMult <= 0 then speedMult = 0.01 end
    local wantLooped = holdLoop and true or false

    local track = animTracks[key]
    if not track or not track.Animation
        or track.Animation.AnimationId ~= "rbxassetid://"..assetId
        or track.Looped ~= wantLooped then
        destroyTrack(key)
        local ok, result = pcall(function()
            local anim = Instance.new("Animation")
            anim.AnimationId = "rbxassetid://"..assetId
            local t = animator:LoadAnimation(anim)
            t.Priority = PRIORITY; t.Looped = wantLooped
            return t
        end)
        if not ok then warn("[FNF] Load fail: "..assetId); return end
        track = result; animTracks[key] = track
    end

    if wantLooped then
        -- HOLD-TO-SUSTAIN MODE: just keep it looping at speedMult; release
        -- is handled by stopAnim()'s normal blend-out below.
        track:Play(BLEND_IN)
        track:AdjustSpeed(speedMult)
    else
        -- ORIGINAL "SNAP TO POSE" MODE: quickly play to the target frame
        -- then freeze there until release.
        local baseReach = FRAME_TARGET / 60
        local reach = baseReach / speedMult
        track:Play(BLEND_IN)
        track:AdjustSpeed(track.Length > 0 and (track.Length / reach) or 10)
        task.delay(reach, function()
            if track and track.IsPlaying then
                track:AdjustSpeed(0)
                track.TimePosition = math.max(0, track.Length - 0.01)
            end
        end)
    end

    StatusState.currentAnimName = key
    StatusState.currentAnimId   = assetId
    StatusState.speedMult       = speedMult
    StatusState.playing         = true
    if refreshStatusUI then refreshStatusUI() end
    updatePreviewAnimation(assetId, key)
    pushRecent(assetId, key)
end

local function stopAnim(key)
    local t = animTracks[key]
    if t and t.IsPlaying then t:Stop(BLEND_OUT) end
    StatusState.playing = false
    if refreshStatusUI then refreshStatusUI() end
end

-- Rolls which animation id to actually use for a given binding's press.
-- Alt slots are checked in order, each claiming its configured % slice of
-- a 0-100 roll; anything left uncovered (including "no alts configured
-- at all") falls back to the binding's primary animation id.
local function rollBindingAnimId(row)
    if row.alts and #row.alts > 0 then
        local roll = math.random() * 100
        local acc = 0
        for _, alt in ipairs(row.alts) do
            acc += alt.chance
            if roll <= acc and alt.id ~= "" then
                return alt.id
            end
        end
    end
    return row.id
end

-- ============================================================
--  ARROW HUD REACTIONS
-- ============================================================
local function hitArrow(dir)
    local f = arrowHudFrames[dir]
    if not f then return end
    TweenService:Create(f.bg, TweenInfo.new(0.05, Enum.EasingStyle.Linear), {
        BackgroundColor3 = ARROW_COLORS[dir].hit,
        Size = UDim2.new(0, ARROW_HIT_SIZE, 0, ARROW_HIT_SIZE),
    }):Play()
    if f.stroke then
        TweenService:Create(f.stroke, TweenInfo.new(0.05, Enum.EasingStyle.Linear), { Thickness = 4 }):Play()
    end
    if f.glow then
        f.glow.Visible = true
        TweenService:Create(f.glow, TweenInfo.new(0.05), { BackgroundTransparency = 0.35 }):Play()
    end
end

local function releaseArrow(dir)
    local f = arrowHudFrames[dir]
    if not f then return end
    TweenService:Create(f.bg, TweenInfo.new(0.1, Enum.EasingStyle.Linear), {
        BackgroundColor3 = ARROW_COLORS[dir].idle,
        Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE),
    }):Play()
    if f.stroke then
        TweenService:Create(f.stroke, TweenInfo.new(0.1, Enum.EasingStyle.Linear), { Thickness = 2 }):Play()
    end
    if f.glow then
        local tw = TweenService:Create(f.glow, TweenInfo.new(0.15), { BackgroundTransparency = 1 })
        tw:Play()
        tw.Completed:Connect(function() if f.glow then f.glow.Visible = false end end)
    end
end

-- ============================================================
--  AFTERIMAGE HELPER
-- ============================================================
local function spawnAfterimage(char)
    if not char then return end
    
    -- Briefly allow cloning in case the game disabled it
    local oldArch = char.Archivable
    char.Archivable = true
    local ok, clone = pcall(function() return char:Clone() end)
    char.Archivable = oldArch
    
    if not ok or not clone then return end
    
    -- Strip out logic and configure parts for the ghost effect
    for _, v in ipairs(clone:GetDescendants()) do
        if v:IsA("Script") or v:IsA("LocalScript") then
            v:Destroy()
        elseif v:IsA("BasePart") then
            v.CanCollide = false
            v.Anchored = true
            v.Massless = true
            v.Material = Enum.Material.ForceField -- Gives a slick, ghostly energy vibe
            
            -- Fade out parts smoothly
            TweenService:Create(v, TweenInfo.new(0.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Transparency = 1}):Play()
        elseif v:IsA("Decal") or v:IsA("Texture") or v:IsA("SurfaceAppearance") then
            TweenService:Create(v, TweenInfo.new(0.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Transparency = 1}):Play()
        end
    end
    
    -- Make sure the clone stays frozen exactly where it spawned
    local root = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
    if root then
        root.Anchored = true
    end
    
    clone.Parent = workspace
    
    -- Delete the clone entirely once the fade tween is done
    task.delay(0.45, function()
        if clone then clone:Destroy() end
    end)
end
-- ============================================================
--  IDLE ANIMATION
-- ============================================================
local function idleSpeedFromBPM()
    if not BASE_BPM or BASE_BPM <= 0 then return 1 end
    return idleBPM / BASE_BPM
end

local function loadIdleTrack(assetId, shouldLoop)
    destroyIdleTrack()
    if not animator then return nil end
    local ok, result = pcall(function()
        local anim = Instance.new("Animation")
        anim.AnimationId = "rbxassetid://"..assetId
        local t = animator:LoadAnimation(anim)
        t.Priority = Enum.AnimationPriority.Idle; t.Looped = shouldLoop
        return t
    end)
    if not ok then return nil end
    return result
end

local function playIdleOnce(assetId, cb)
    local t = loadIdleTrack(assetId, false)
    if not t then return end
    idleTrack = t; t:Play(0.1)
    t:AdjustSpeed(idleSpeedFromBPM())
    updatePreviewAnimation(assetId, "IDLE (once)")
    t.Stopped:Connect(function() if cb then cb() end end)
end

local function playIdleLoop(assetId)
    local t = loadIdleTrack(assetId, true)
    if not t then return end
    idleTrack = t; idleLooping = true; t:Play(0.1)
    t:AdjustSpeed(idleSpeedFromBPM())
    updatePreviewAnimation(assetId, "IDLE (loop)")
    StatusState.playing = true
    if refreshStatusUI then refreshStatusUI() end
end

local function stopIdle()
    idleLooping = false
    if idleTrack and idleTrack.IsPlaying then idleTrack:Stop(0.2) end
    StatusState.playing = false
    if refreshStatusUI then refreshStatusUI() end
end

local function setIdleBPM(bpm)
    if bpm and bpm > 0 then idleBPM = bpm end
    StatusState.bpm = idleBPM
    if idleTrack and idleTrack.IsPlaying then
        idleTrack:AdjustSpeed(idleSpeedFromBPM())
    end
    if refreshStatusUI then refreshStatusUI() end
end

local function setIdleBaseBPM(bpm)
    if bpm and bpm > 0 then BASE_BPM = bpm end
    if idleTrack and idleTrack.IsPlaying then
        idleTrack:AdjustSpeed(idleSpeedFromBPM())
    end
end

-- ============================================================
--  ANIMATION BPM DETECTOR
-- ============================================================
-- Tap mode: call this every time a directional anim is triggered by the
-- player. Keeps a rolling window of press timestamps and converts the
-- average gap between them into BPM.
local function registerBpmTap()
    local now = os.clock()
    table.insert(bpmDetectTaps, now)
    while #bpmDetectTaps > BPM_TAP_WINDOW do table.remove(bpmDetectTaps, 1) end
    if #bpmDetectTaps < 3 then return nil end

    local gaps = {}
    for i = 2, #bpmDetectTaps do
        local gap = bpmDetectTaps[i] - bpmDetectTaps[i-1]
        if gap > 0.08 and gap < 3 then table.insert(gaps, gap) end
    end
    if #gaps == 0 then return nil end

    -- discard outliers more than 2x away from the median gap, then average
    table.sort(gaps)
    local median = gaps[math.ceil(#gaps/2)]
    local sum, count = 0, 0
    for _, g in ipairs(gaps) do
        if g < median * 2 and g > median * 0.5 then sum = sum + g; count = count + 1 end
    end
    if count == 0 then return nil end
    local avgGap = sum / count
    detectedBPM = math.floor((60 / avgGap) + 0.5)
    return detectedBPM
end

-- Idle-length mode: BPM = (beats per loop * 60) / animation length in seconds.
-- Reads whatever idle track is currently loaded (call after playIdleLoop/Once).
local function detectBpmFromIdleLength(beatsPerLoop)
    beatsPerLoop = beatsPerLoop or beatsPerIdleLoop
    if not idleTrack or not idleTrack.Length or idleTrack.Length <= 0 then return nil end
    -- Length reflects the un-adjusted animation, AdjustSpeed doesn't change it,
    -- so this is the authored duration regardless of current playback speed.
    local bpm = (beatsPerLoop * 60) / idleTrack.Length
    detectedBPM = math.floor(bpm + 0.5)
    return detectedBPM
end

-- ============================================================
--  SHARED MUSIC PLAYER (no VC)
-- ============================================================
-- Waits for the companion server script's RemoteEvent. If that server
-- script hasn't been added to the place yet, this just times out and
-- the music section reports "server script missing" instead of erroring.
local function setupMusicRemote()
    local ok, remote = pcall(function()
        return ReplicatedStorage:WaitForChild("FNFMusicRemote", 8)
    end)
    if ok and remote then
        musicRemote = remote
    end
    return musicRemote ~= nil
end

local function setMusicStatus(text, color)
    if musicStatusLbl then
        musicStatusLbl.Text = text
        if color then musicStatusLbl.TextColor3 = color end
    end
end

-- Watches the actual replicated Sound object (created server-side) to
-- report real load/playback state — this reflects reality for every
-- player, not just an optimistic guess on the requesting client.
local function watchMusicStatus(expectedId)
    task.spawn(function()
        local myTick = musicRequestTick
        local deadline = os.clock() + 8
        local snd = nil
        while os.clock() < deadline and myTick == musicRequestTick do
            snd = SoundService:FindFirstChild(MUSIC_SOUND_NAME)
            if snd and snd.SoundId:find(expectedId, 1, true) then break end
            task.wait(0.2)
        end
        if myTick ~= musicRequestTick then return end -- a newer request superseded this one

        if not snd or not snd.SoundId:find(expectedId, 1, true) then
            setMusicStatus("✖ NO SERVER SCRIPT FOUND", Color3.fromRGB(255,80,80))
            return
        end

        -- give it a moment to load, then check IsLoaded
        local loadDeadline = os.clock() + 6
        while os.clock() < loadDeadline and myTick == musicRequestTick do
            if snd.IsLoaded then break end
            task.wait(0.15)
        end
        if myTick ~= musicRequestTick then return end

        if snd.IsLoaded and snd.IsPlaying then
            setMusicStatus("✔ PLAYING — allowed in this game", Color3.fromRGB(80,220,120))
        elseif snd.IsLoaded then
            setMusicStatus("● loaded, not playing", Color3.fromRGB(255,180,60))
        else
            setMusicStatus("✖ NOT PLAYABLE HERE (blocked/private asset)", Color3.fromRGB(255,80,80))
        end
    end)
end

local function playMusicTrack(id)
    if not musicRemote then
        setMusicStatus("✖ NO SERVER SCRIPT FOUND", Color3.fromRGB(255,80,80))
        return
    end
    musicRequestTick = musicRequestTick + 1
    setMusicStatus("⏳ requesting...", Color3.fromRGB(0,200,255))
    musicRemote:FireServer("play", id)
    watchMusicStatus(id)
end

local function stopMusicTrack()
    if not musicRemote then return end
    musicRequestTick = musicRequestTick + 1
    musicRemote:FireServer("stop")
    setMusicStatus("● stopped", Color3.fromRGB(160,80,80))
end

-- ============================================================
--  ANIMATION SEQUENCE MAKER
-- ============================================================
local function playSequence()
    if seqPlaying or #sequence == 0 or not animator then return end
    seqPlaying = true
    task.spawn(function()
        for _, step in ipairs(sequence) do
            if not seqPlaying then break end
            if step.id and step.id ~= "" then
                destroySeqTrack()
                local ok, result = pcall(function()
                    local anim = Instance.new("Animation")
                    anim.AnimationId = "rbxassetid://"..step.id
                    local t = animator:LoadAnimation(anim)
                    t.Priority = PRIORITY; t.Looped = false
                    return t
                end)
                if ok then
                    seqTrack = result; seqTrack:Play(0)
                    updatePreviewAnimation(step.id, "SEQUENCE")
                end
            end
            task.wait(math.max(0.01, step.delay or 0.5))
        end
        destroySeqTrack()
        seqPlaying = false
    end)
end

local function stopSequence()
    seqPlaying = false
    destroySeqTrack()
end

-- ============================================================
--  PLAYLIST MANAGER
-- ============================================================
local function playPlaylist()
    if playlistPlaying or #playlist == 0 or not animator then return end
    playlistPlaying = true
    task.spawn(function()
        while playlistPlaying do
            local step = playlist[playlistIndex]
            if step and step.id and step.id ~= "" then
                destroySeqTrack()
                local ok, result = pcall(function()
                    local anim = Instance.new("Animation")
                    anim.AnimationId = "rbxassetid://"..step.id
                    local t = animator:LoadAnimation(anim)
                    t.Priority = PRIORITY; t.Looped = false
                    return t
                end)
                if ok then
                    seqTrack = result; seqTrack:Play(0)
                    updatePreviewAnimation(step.id, step.name or ("Playlist #"..playlistIndex))
                end
            end
            task.wait(math.max(0.05, (step and step.delay) or 1))
            if not playlistPlaying then break end
            playlistIndex = playlistIndex + 1
            if playlistIndex > #playlist then playlistIndex = 1 end
            if refreshPlaylistUI then refreshPlaylistUI() end
        end
        destroySeqTrack()
    end)
end

local function stopPlaylist()
    playlistPlaying = false
    destroySeqTrack()
end

local function nextPlaylistTrack()
    if #playlist == 0 then return end
    playlistIndex = playlistIndex + 1
    if playlistIndex > #playlist then playlistIndex = 1 end
    if refreshPlaylistUI then refreshPlaylistUI() end
end

local function prevPlaylistTrack()
    if #playlist == 0 then return end
    playlistIndex = playlistIndex - 1
    if playlistIndex < 1 then playlistIndex = #playlist end
    if refreshPlaylistUI then refreshPlaylistUI() end
end

-- ============================================================
--  INPUT HANDLER
-- ============================================================
local lastNoteTime = 0
local DOUBLE_NOTE_WINDOW = 0.08 -- 80 milliseconds to count as a "double note"

local function onInput(input, _gp)
    if rebindingRow then
        local kn = input.KeyCode and input.KeyCode.Name or "Unknown"
        if input.UserInputState == Enum.UserInputState.Begin
            and kn ~= "Unknown" and not kn:match("Mouse") then
            rebindingRow.key = kn
            rebindingRow.keyBtn.Text = kn
            rebindingRow.keyBtn.BackgroundColor3 = Color3.fromRGB(30, 20, 45)
            rebindingRow = nil
        end
        return
    end
    
    for _, row in ipairs(CurrentBindings) do
        if input.KeyCode and input.KeyCode.Name == row.key then
            if input.UserInputState == Enum.UserInputState.Begin then
                
                -- Check for double notes
                local now = os.clock()
                local isDouble = (now - lastNoteTime) <= DOUBLE_NOTE_WINDOW
                lastNoteTime = now

                -- INTERRUPT: Instantly stop ALL other active directional animations
                for k, track in pairs(animTracks) do
                    if track.IsPlaying then
                        track:Stop(0) -- 0 second blend-out = instant snap
                    end
                end
                
                -- TRIGGER AFTERIMAGE: If we hit a double note, spawn the ghost
                if isDouble then
                    spawnAfterimage(character)
                end

                -- Play the new animation
                local chosenId = rollBindingAnimId(row)
                playAnim(row.key, chosenId, row.speedMult, row.holdLoop)
                
                hitArrow(row.dir)
                swapFace(row.dir)
                triggerRPMovement()
                registerBpmTap()
                if refreshBpmDetectorUI then refreshBpmDetectorUI() end
                
            elseif input.UserInputState == Enum.UserInputState.End then
                stopAnim(row.key)
                releaseArrow(row.dir)
                restoreFace()
            end
        end
    end
end
-- ============================================================
--  CHARACTER SETUP
-- ============================================================
local function buildPreviewClone(char)
    if not previewCloneContainer then return end
    if previewClone then previewClone:Destroy(); previewClone = nil end

    -- Archivable must be true for Clone() to work; some games/anti-cheat set it false.
    local wasArchivable = char.Archivable
    if not wasArchivable then char.Archivable = true end
    local ok, clone = pcall(function() return char:Clone() end)
    if not wasArchivable then char.Archivable = wasArchivable end

    if not ok or not clone then
        notify("Preview failed: character isn't clonable right now", "error")
        return
    end

    -- strip scripts so the clone doesn't run game logic
    for _, d in ipairs(clone:GetDescendants()) do
        if d:IsA("Script") or d:IsA("LocalScript") then d:Destroy() end
    end
    clone.Name = "PreviewClone"
    clone.Parent = previewCloneContainer

    local hum = clone:FindFirstChildOfClass("Humanoid")
    if hum then
        hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
        previewAnimator = hum:FindFirstChildOfClass("Animator") or Instance.new("Animator", hum)
    else
        notify("Preview clone has no Humanoid — can't animate it", "warn")
    end

    -- Anchor ONLY the root part. ViewportFrame contents aren't part of the
    -- main physics simulation, so unanchored limbs won't fall — but they DO
    -- need to stay unanchored for the Animator's Motor6D joints to pose them.
    -- Anchoring every part (as an earlier version of this script did) freezes
    -- each limb independently and blocks animations from moving anything.
    local root = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
    if root then root.Anchored = true end

    local okPivot = pcall(function()
        clone:PivotTo(CFrame.new(0, 3, 0))
    end)
    if not okPivot then
        local hrp = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
        if hrp then clone:SetPrimaryPartCFrame(CFrame.new(0, 3, 0)) end
    end

    previewClone = clone
end

local function setupCharacter(char)
    cleanupAll(); character = char
    pcall(function() character:WaitForChild("HumanoidRootPart", 10) end)
    animator = getAnimator(char)
    if not animator then return end
    initFace(char)
    if legsLocked then applyLegsLock(true) end
    table.insert(connections, UserInputService.InputBegan:Connect(onInput))
    table.insert(connections, UserInputService.InputEnded:Connect(onInput))
    local hum = char:FindFirstChildOfClass("Humanoid")
    if hum then table.insert(connections, hum.Died:Connect(cleanupAll)) end
    task.defer(buildPreviewClone, char)
    log("Character ready.")
    notify("Character loaded — ready to inject", "success")
end

if LocalPlayer.Character then task.spawn(setupCharacter, LocalPlayer.Character) end
LocalPlayer.CharacterAdded:Connect(function(c) task.spawn(setupCharacter, c) end)

-- ============================================================
--  NEARBY PLAYER ANIM SCANNER
--  Reads whatever animation IDs are currently playing on other
--  players' characters. This only reads data Roblox already
--  replicates to every client (the same way you see other
--  players' emotes/moves play out in real time) — it can't see
--  anything that isn't actively playing right now.
-- ============================================================
local function scanPlayingAnimations()
    local found = {}
    for _, plr in ipairs(Players:GetPlayers()) do
        if plr ~= LocalPlayer and plr.Character then
            local hum = plr.Character:FindFirstChildOfClass("Humanoid")
            local anr = hum and hum:FindFirstChildOfClass("Animator")
            if anr then
                for _, track in ipairs(anr:GetPlayingAnimationTracks()) do
                    local animObj = track.Animation
                    if animObj and animObj.AnimationId ~= "" then
                        local id = animObj.AnimationId:match("%d+") or ""
                        if id ~= "" then
                            table.insert(found, { player = plr.Name, id = id })
                        end
                    end
                end
            end
        end
    end
    return found
end
local function uiCorner(p, r)
    local c = Instance.new("UICorner"); c.CornerRadius = r or UDim.new(0,8); c.Parent = p
end
local function uiStroke(p, col, th)
    local s = Instance.new("UIStroke"); s.Color = col; s.Thickness = th or 1.5; s.Parent = p; return s
end
local function makeLabel(parent, text, pos, size, color, xAlign, ts)
    local l = Instance.new("TextLabel")
    l.Size = size; l.Position = pos; l.BackgroundTransparency = 1
    l.Text = text; l.TextColor3 = color or Color3.fromRGB(180,180,180)
    l.TextSize = ts or 11; l.Font = Enum.Font.GothamBold
    l.TextXAlignment = xAlign or Enum.TextXAlignment.Center
    l.Parent = parent; return l
end
local function makeBtn(parent, text, pos, size, bg, tc, ts)
    local b = Instance.new("TextButton")
    b.Size = size; b.Position = pos; b.BackgroundColor3 = bg
    b.Text = text; b.TextColor3 = tc or Color3.fromRGB(255,255,255)
    b.Font = Enum.Font.GothamBold; b.TextSize = ts or 12
    b.BorderSizePixel = 0; b.Parent = parent; b.AutoButtonColor = false
    uiCorner(b)

    -- hover / glow behaviour
    local baseSize = size
    local hoverGlow = uiStroke(b, Color3.fromRGB(255,255,255), 0)
    hoverGlow.Transparency = 1
    b.MouseEnter:Connect(function()
        TweenService:Create(b, TweenInfo.new(0.12, Enum.EasingStyle.Quad), {
            Size = UDim2.new(baseSize.X.Scale, baseSize.X.Offset + 4, baseSize.Y.Scale, baseSize.Y.Offset + 2)
        }):Play()
        TweenService:Create(hoverGlow, TweenInfo.new(0.12), { Thickness = 2, Transparency = 0.2 }):Play()
    end)
    b.MouseLeave:Connect(function()
        TweenService:Create(b, TweenInfo.new(0.15, Enum.EasingStyle.Quad), { Size = baseSize }):Play()
        TweenService:Create(hoverGlow, TweenInfo.new(0.15), { Thickness = 0, Transparency = 1 }):Play()
    end)
    b.MouseButton1Down:Connect(function()
        TweenService:Create(b, TweenInfo.new(0.06), { Size = UDim2.new(baseSize.X.Scale, baseSize.X.Offset - 3, baseSize.Y.Scale, baseSize.Y.Offset - 3) }):Play()
    end)
    b.MouseButton1Up:Connect(function()
        TweenService:Create(b, TweenInfo.new(0.08), { Size = baseSize }):Play()
    end)

    return b
end
local function makeDivider(parent, yPos, color)
    local d = Instance.new("Frame"); d.Size = UDim2.new(1,-16,0,1)
    d.Position = UDim2.new(0,8,0,yPos)
    d.BackgroundColor3 = color or Color3.fromRGB(0,200,255)
    d.BorderSizePixel = 0; d.Parent = parent
    return d
end
local function makeBox(parent, pos, size, placeholder, default)
    local b = Instance.new("TextBox")
    b.Size = size; b.Position = pos
    b.BackgroundColor3 = Color3.fromRGB(22,22,32)
    b.PlaceholderText = placeholder or ""; b.PlaceholderColor3 = Color3.fromRGB(90,90,110)
    b.Text = default or ""; b.TextColor3 = Color3.fromRGB(255,255,255)
    b.ClearTextOnFocus = false; b.Font = Enum.Font.Code; b.TextSize = 11
    b.Parent = parent; uiCorner(b); return b
end

-- Draggable slider: track + fill + handle. Works with mouse and touch.
local function makeSlider(parent, pos, size, minVal, maxVal, default, onChange)
    local track = Instance.new("Frame")
    track.Size = size; track.Position = pos
    track.BackgroundColor3 = Color3.fromRGB(28,28,42)
    track.BorderSizePixel = 0; track.Active = true
    track.Parent = parent; uiCorner(track, UDim.new(0,6))

    local fill = Instance.new("Frame")
    fill.BackgroundColor3 = Color3.fromRGB(255,80,180)
    fill.BorderSizePixel = 0; fill.Size = UDim2.new(0,0,1,0)
    fill.Parent = track; uiCorner(fill, UDim.new(0,6))
    regBg(fill)

    local handle = Instance.new("Frame")
    handle.Size = UDim2.new(0,14,0,14)
    handle.AnchorPoint = Vector2.new(0.5,0.5)
    handle.Position = UDim2.new(0,0,0.5,0)
    handle.BackgroundColor3 = Color3.fromRGB(255,255,255)
    handle.BorderSizePixel = 0; handle.ZIndex = 2
    handle.Parent = track; uiCorner(handle, UDim.new(1,0))
    local hStroke = uiStroke(handle, Color3.fromRGB(255,80,180), 2)
    regStroke(hStroke)

    local value = default
    local dragging = false

    local function setValue(v, fire)
        v = math.clamp(v, minVal, maxVal)
        value = v
        local pct = (maxVal > minVal) and (v - minVal) / (maxVal - minVal) or 0
        fill.Size = UDim2.new(pct, 0, 1, 0)
        handle.Position = UDim2.new(pct, 0, 0.5, 0)
        if fire and onChange then onChange(v) end
    end

    local function updateFromX(x)
        local abs = track.AbsolutePosition.X
        local w   = track.AbsoluteSize.X
        if w <= 0 then return end
        local pct = math.clamp((x - abs) / w, 0, 1)
        setValue(minVal + pct * (maxVal - minVal), true)
    end

    local function tryBegin(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1
            or input.UserInputType == Enum.UserInputType.Touch then
            dragging = true
            updateFromX(input.Position.X)
        end
    end

    track.InputBegan:Connect(tryBegin)
    handle.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1
            or input.UserInputType == Enum.UserInputType.Touch then
            dragging = true
        end
    end)
    UserInputService.InputChanged:Connect(function(input)
        if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement
            or input.UserInputType == Enum.UserInputType.Touch) then
            updateFromX(input.Position.X)
        end
    end)
    UserInputService.InputEnded:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1
            or input.UserInputType == Enum.UserInputType.Touch then
            dragging = false
        end
    end)

    setValue(default, false)

    return { track = track, fill = fill, handle = handle, setValue = setValue, getValue = function() return value end }
end

-- ============================================================
--  PER-BINDING ALT-ANIMATION LIST UI HELPER
-- ============================================================
-- Renders the current alt-animation entries for one binding row into a
-- given ScrollingFrame, with a remove button per entry and a running
-- total % footer. Self-referencing so removal can re-render in place.
local function refreshRowAltList(row, listFrame)
    for _, c in ipairs(listFrame:GetChildren()) do
        if c:IsA("Frame") or c:IsA("TextLabel") then c:Destroy() end
    end
    for j, alt in ipairs(row.alts) do
        local entry = Instance.new("Frame")
        entry.Size = UDim2.new(1,-4,0,20)
        entry.BackgroundColor3 = Color3.fromRGB(24,24,36)
        entry.BorderSizePixel = 0
        entry.LayoutOrder = j
        entry.Parent = listFrame
        uiCorner(entry, UDim.new(0,4))
        makeLabel(entry, string.format("⭐ %s — %d%%", alt.id, alt.chance),
            UDim2.new(0,6,0,0), UDim2.new(1,-28,1,0), Color3.fromRGB(200,200,215), Enum.TextXAlignment.Left, 9)
        local rem = makeBtn(entry, "✕", UDim2.new(1,-22,0,0), UDim2.new(0,20,0,20), Color3.fromRGB(170,35,35), nil, 9)
        rem.MouseButton1Click:Connect(function()
            table.remove(row.alts, j)
            refreshRowAltList(row, listFrame)
        end)
    end
    local total = 0
    for _, a in ipairs(row.alts) do total += a.chance end
    local footer = makeLabel(listFrame, string.format("Total: %d%% (remainder = primary anim)", total),
        UDim2.new(1,-4,0,14), UDim2.new(1,-4,0,14),
        (total > 100) and Color3.fromRGB(255,80,80) or Color3.fromRGB(140,140,160),
        Enum.TextXAlignment.Right, 9)
    footer.LayoutOrder = 9999
end

-- ============================================================
--  ARROW HUD BUILD
-- ============================================================
local function buildArrowHud()
    local old = PlayerGui:FindFirstChild("FNFArrowHud")
    if old then old:Destroy() end

    local sg = Instance.new("ScreenGui")
    sg.Name = "FNFArrowHud"; sg.ResetOnSpawn = false
    sg.ZIndexBehavior = Enum.ZIndexBehavior.Sibling; sg.Parent = PlayerGui

    local totalW = #ARROW_ORDER * ARROW_SIZE + (#ARROW_ORDER - 1) * ARROW_GAP
    local bottomY = 50  -- px from bottom

    for i, dir in ipairs(ARROW_ORDER) do
        local colors = ARROW_COLORS[dir]
        local cx = -totalW/2 + (i-1)*(ARROW_SIZE + ARROW_GAP) + ARROW_SIZE/2
        local cy = -(ARROW_SIZE/2 + bottomY)

        local bg = Instance.new("Frame")
        bg.AnchorPoint = Vector2.new(0.5, 0.5)
        bg.Size = UDim2.new(0, ARROW_SIZE, 0, ARROW_SIZE)
        bg.Position = UDim2.new(0.5, cx, 1, cy)
        bg.BackgroundColor3 = colors.idle
        bg.BorderSizePixel = 0; bg.Parent = sg
        uiCorner(bg, UDim.new(0, 10))
        local stroke = uiStroke(bg, Color3.fromRGB(255,255,255), 2)

        local glow = Instance.new("Frame")
        glow.Size = UDim2.new(1,0,1,0); glow.BackgroundTransparency = 0.7
        glow.BackgroundColor3 = colors.hit; glow.BorderSizePixel = 0; glow.Parent = bg
        uiCorner(glow, UDim.new(0,10))
        glow.Visible = false

        local lbl = Instance.new("TextLabel")
        lbl.Size = UDim2.new(1,0,1,0); lbl.BackgroundTransparency = 1
        lbl.Text = ARROW_SYMBOLS[dir]
        lbl.TextColor3 = Color3.fromRGB(255,255,255)
        lbl.TextSize = 26; lbl.Font = Enum.Font.GothamBold; lbl.Parent = bg

        arrowHudFrames[dir] = { bg = bg, lbl = lbl, stroke = stroke, glow = glow }
    end
end

-- ============================================================
--  MIC SPECTROGRAM SETUP
-- ============================================================
-- Tries to wire up Roblox's Audio API (AudioDeviceInput -> AudioAnalyzer)
-- so the spectrogram can react to real microphone input. This part of the
-- engine is newer and can vary by client/game settings, so every step is
-- pcall-guarded. If anything fails, micAvailable stays false and the
-- spectrogram loop below falls back to reacting to the animation/BPM state
-- instead (same as the equalizer), so the widget still looks alive.
local function setupMicSpectrogram()
    micAvailable = false
    local ok = pcall(function()
        micDeviceInput = Instance.new("AudioDeviceInput")
        micDeviceInput.Parent = LocalPlayer

        micAnalyzer = Instance.new("AudioAnalyzer")
        micAnalyzer.WindowSize = 1024
        micAnalyzer.Parent = LocalPlayer

        micWire = Instance.new("Wire")
        micWire.SourceInstance = micDeviceInput
        micWire.TargetInstance = micAnalyzer
        micWire.Parent = micAnalyzer

        micDeviceInput.Active = true
    end)
    micAvailable = ok and micDeviceInput ~= nil and micAnalyzer ~= nil
    if not micAvailable then
        log("Mic spectrogram unavailable on this client/game — using animation-synced fallback")
    end
    return micAvailable
end

-- ============================================================
--  BEAT / VISUALIZER LOOPS (equalizer, waveform, floating notes, beat pulse, particles)
-- ============================================================
local eqBars, waveBars, beatDot, notesLayer, particleLayer = nil, nil, nil, nil, nil

local function startVisualizerLoops()
    -- Equalizer bars: random-ish heights, faster while playing
    task.spawn(function()
        while true do
            local speed = StatusState.playing and 0.08 or 0.22
            task.wait(speed)
            if eqBars then
                for _, bar in ipairs(eqBars) do
                    local h = StatusState.playing and (0.25 + math.random() * 0.75) or (0.08 + math.random() * 0.2)
                    TweenService:Create(bar, TweenInfo.new(speed, Enum.EasingStyle.Sine), {
                        Size = UDim2.new(1, 0, h, 0)
                    }):Play()
                end
            end
        end
    end)

    -- BPM waveform: sine ripple scaled to bpm
    task.spawn(function()
        local t = 0
        while true do
            RunService.Heartbeat:Wait()
            t = t + (StatusState.bpm / 60) * 0.03
            if waveBars then
                for i, bar in ipairs(waveBars) do
                    local h = 0.15 + 0.35 * (0.5 + 0.5 * math.sin(t + i * 0.6))
                    bar.Size = UDim2.new(1, 0, h, 0)
                end
            end
        end
    end)

    -- Beat sync pulse: fires exactly on the beat interval
    task.spawn(function()
        while true do
            local interval = 60 / math.max(1, StatusState.bpm)
            task.wait(interval)
            if beatDot then
                beatDot.Size = UDim2.new(0, 22, 0, 22)
                TweenService:Create(beatDot, TweenInfo.new(interval * 0.85, Enum.EasingStyle.Quad), {
                    Size = UDim2.new(0, 10, 0, 10)
                }):Play()
            end
        end
    end)

    -- Floating music notes
    task.spawn(function()
        local symbols = { "♪", "♫", "♬", "♩" }
        while true do
            task.wait(0.7 + math.random() * 0.8)
            if notesLayer then
                local n = Instance.new("TextLabel")
                n.Size = UDim2.new(0, 20, 0, 20)
                n.Position = UDim2.new(math.random(), 0, 1, 0)
                n.BackgroundTransparency = 1
                n.Text = symbols[math.random(1, #symbols)]
                n.TextColor3 = THEMES[currentThemeName].accent
                n.TextTransparency = 0.2
                n.Font = Enum.Font.GothamBold
                n.TextSize = 14 + math.random(0, 10)
                n.Parent = notesLayer
                regText(n)
                local tw = TweenService:Create(n, TweenInfo.new(3 + math.random()*2, Enum.EasingStyle.Linear), {
                    Position = UDim2.new(n.Position.X.Scale, 0, -0.15, 0),
                    TextTransparency = 1
                })
                tw:Play()
                tw.Completed:Connect(function() n:Destroy() end)
            end
        end
    end)

    -- Ambient particles
    task.spawn(function()
        while true do
            task.wait(0.4 + math.random() * 0.5)
            if particleLayer then
                local p = Instance.new("Frame")
                local sz = math.random(2, 4)
                p.Size = UDim2.new(0, sz, 0, sz)
                p.Position = UDim2.new(math.random(), 0, 1, 0)
                p.BackgroundColor3 = THEMES[currentThemeName].accent2
                p.BackgroundTransparency = 0.3
                p.BorderSizePixel = 0
                p.Parent = particleLayer
                uiCorner(p, UDim.new(1,0))
                local tw = TweenService:Create(p, TweenInfo.new(1.5 + math.random(), Enum.EasingStyle.Sine), {
                    Position = UDim2.new(p.Position.X.Scale + (math.random()-0.5)*0.1, 0, -0.05, 0),
                    BackgroundTransparency = 1
                })
                tw:Play()
                tw.Completed:Connect(function() p:Destroy() end)
            end
        end
    end)

    -- Vinyl record spin: RPM scales with whatever's currently playing —
    -- directional anim speedMult if one is active, otherwise idle BPM ratio.
    task.spawn(function()
        while true do
            RunService.Heartbeat:Wait()
            if vinylImage then
                local rpm
                if StatusState.playing and StatusState.speedMult then
                    rpm = vinylBaseRPM * StatusState.speedMult
                else
                    rpm = vinylBaseRPM * idleSpeedFromBPM()
                end
                vinylRotation = (vinylRotation + (rpm / 60) * 360 * (1/60)) % 360
                vinylImage.Rotation = vinylRotation
            end
        end
    end)

    -- Spectrogram: real mic spectrum if available, otherwise an
    -- animation/BPM-synced fallback so the bars are never just dead.
    task.spawn(function()
        while true do
            task.wait(0.05)
            if #specBars > 0 then
                if micAvailable and micAnalyzer then
                    local ok, spectrum = pcall(function() return micAnalyzer:GetSpectrum() end)
                    if ok and spectrum then
                        local n = #spectrum
                        for i, bar in ipairs(specBars) do
                            local idx = math.max(1, math.floor((i / #specBars) * n))
                            local mag = spectrum[idx] or 0
                            local h = math.clamp(mag * 2, 0.05, 1)
                            TweenService:Create(bar, TweenInfo.new(0.05), { Size = UDim2.new(1,0,h,0) }):Play()
                        end
                    else
                        micAvailable = false -- analyzer stopped working mid-session, fall back
                    end
                else
                    for i, bar in ipairs(specBars) do
                        local base = StatusState.playing and 0.3 or 0.08
                        local h = base + math.random() * (StatusState.playing and 0.6 or 0.15)
                        TweenService:Create(bar, TweenInfo.new(0.05), { Size = UDim2.new(1,0,h,0) }):Play()
                    end
                end
            end
        end
    end)

    -- CRT flicker: tiny random transparency jitter on the scanline overlay
    task.spawn(function()
        while true do
            task.wait(0.08 + math.random() * 0.15)
            if crtEnabled and crtOverlay then
                crtOverlay.BackgroundTransparency = 0.82 + math.random() * 0.08
            end
        end
    end)

    -- (preview camera orbit loop lives in buildGUI, started once previewCam exists)
end

-- ============================================================
--  MAIN GUI BUILD
-- ============================================================
local function buildGUI()
    local old = PlayerGui:FindFirstChild("FNFInjectorGui")
    if old then old:Destroy() end

    local sg = Instance.new("ScreenGui")
    sg.Name = "FNFInjectorGui"
    sg.ResetOnSpawn = false
    sg.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
    sg.Parent = PlayerGui

    -- Notification stack (top-right of screen, independent of main panel)
    notifyContainer = Instance.new("Frame")
    notifyContainer.Size = UDim2.new(0, 260, 1, -20)
    notifyContainer.Position = UDim2.new(1, -270, 0, 10)
    notifyContainer.BackgroundTransparency = 1
    notifyContainer.Parent = sg
    local notifyLayout = Instance.new("UIListLayout")
    notifyLayout.SortOrder = Enum.SortOrder.LayoutOrder
    notifyLayout.Padding = UDim.new(0, 6)
    notifyLayout.Parent = notifyContainer

    local MAIN_W, MAIN_H = 430, 600
    local main = Instance.new("Frame")
    main.Name = "Main"; main.Size = UDim2.new(0,MAIN_W,0,MAIN_H)
    main.Position = UDim2.new(0,60,0,60)
    main.BackgroundColor3 = Color3.fromRGB(13,13,18)
    main.BorderSizePixel = 0; main.Active = true; main.Draggable = true
    main.Parent = sg; uiCorner(main, UDim.new(0,10))
    local mainStroke = uiStroke(main, Color3.fromRGB(255,80,180), 2)
    regStroke(mainStroke)

    -- decorative background particle / notes layers, clipped to main panel
    particleLayer = Instance.new("Frame")
    particleLayer.Size = UDim2.new(1,0,1,0); particleLayer.BackgroundTransparency = 1
    particleLayer.ClipsDescendants = true; particleLayer.ZIndex = 0; particleLayer.Parent = main
    notesLayer = Instance.new("Frame")
    notesLayer.Size = UDim2.new(1,0,1,0); notesLayer.BackgroundTransparency = 1
    notesLayer.ClipsDescendants = true; notesLayer.ZIndex = 0; notesLayer.Parent = main

    -- CRT overlay: green tint + scanlines, sits above everything else in
    -- the panel but ignores input so it never blocks clicks. Hidden by
    -- default; toggled from the FX tab.
    crtOverlay = Instance.new("Frame")
    crtOverlay.Size = UDim2.new(1,0,1,0); crtOverlay.BackgroundColor3 = Color3.fromRGB(20,255,120)
    crtOverlay.BackgroundTransparency = 0.88
    crtOverlay.BorderSizePixel = 0; crtOverlay.ZIndex = 50
    crtOverlay.Visible = false
    crtOverlay.Parent = main
    local crtOverlayCorner = Instance.new("UICorner"); crtOverlayCorner.CornerRadius = UDim.new(0,10); crtOverlayCorner.Parent = crtOverlay

    local scanlineHolder = Instance.new("Frame")
    scanlineHolder.Size = UDim2.new(1,0,1,0); scanlineHolder.BackgroundTransparency = 1
    scanlineHolder.ClipsDescendants = true; scanlineHolder.ZIndex = 51
    scanlineHolder.Parent = crtOverlay
    local SCANLINE_GAP = 4
    for y = 0, 700, SCANLINE_GAP do
        local line = Instance.new("Frame")
        line.Size = UDim2.new(1,0,0,1)
        line.Position = UDim2.new(0,0,0,y)
        line.BackgroundColor3 = Color3.fromRGB(0,0,0)
        line.BackgroundTransparency = 0.55
        line.BorderSizePixel = 0
        line.ZIndex = 51
        line.Parent = scanlineHolder
    end

    -- Title bar
    local titleBar = Instance.new("Frame")
    titleBar.Size = UDim2.new(1,0,0,36); titleBar.BackgroundColor3 = Color3.fromRGB(22,8,32)
    titleBar.BorderSizePixel = 0; titleBar.Parent = main; uiCorner(titleBar, UDim.new(0,10))

    local titleLbl = makeLabel(titleBar, "🎵  FNF ANIM INJECTOR  v6.1",
        UDim2.new(0,12,0,0), UDim2.new(1,-80,1,0),
        Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 13)
    regText(titleLbl)

    local toggleBtn = makeBtn(titleBar, "HIDE",
        UDim2.new(1,-68,0,5), UDim2.new(0,60,0,26), Color3.fromRGB(255,80,180))
    regBg(toggleBtn)

    -- Tab bar
    local TAB_H = 32
    local tabBar = Instance.new("Frame")
    tabBar.Size = UDim2.new(1,0,0,TAB_H); tabBar.Position = UDim2.new(0,0,0,36)
    tabBar.BackgroundColor3 = Color3.fromRGB(18,8,28)
    tabBar.BorderSizePixel = 0; tabBar.Parent = main

    -- Content area
    local contentArea = Instance.new("Frame")
    contentArea.Size = UDim2.new(1,0,1,-(36+TAB_H))
    contentArea.Position = UDim2.new(0,0,0,36+TAB_H)
    contentArea.BackgroundTransparency = 1; contentArea.Parent = main

    -- ── Tabs ──
    local tabNames   = { "📋 BINDS", "🎬 MAKER", "👁 PREVIEW", "❤ FAVES", "🎶 LIST", "🎛 FX", "⚙ SETTINGS" }
    local pages      = {}
    local tabButtons = {}
    local tabW       = math.floor(MAIN_W / #tabNames)

    for i, name in ipairs(tabNames) do
        local page = Instance.new("ScrollingFrame")
        page.Size = UDim2.new(1,0,1,0); page.BackgroundTransparency = 1
        page.ScrollBarThickness = 4
        page.CanvasSize = UDim2.new(0,0,0,0)
        page.AutomaticCanvasSize = Enum.AutomaticSize.Y
        page.Visible = (i == 1); page.Parent = contentArea
        pages[name] = page

        local tb = makeBtn(tabBar, name,
            UDim2.new(0,(i-1)*tabW,0,0), UDim2.new(0,tabW,1,0),
            i == 1 and Color3.fromRGB(255,80,180) or Color3.fromRGB(28,12,42), nil, 9)
        tabButtons[name] = tb
        if i == 1 then regBg(tb) end
        tb.MouseButton1Click:Connect(function()
            for n, p in pairs(pages) do
                p.Visible = (n == name)
            end
            for n, btn in pairs(tabButtons) do
                local isActive = (n == name)
                btn.BackgroundColor3 = isActive and THEMES[currentThemeName].accent or Color3.fromRGB(28,12,42)
                -- swap registration so theme/RGB cycling keeps following the active tab
                for j = #themedBgs, 1, -1 do if themedBgs[j] == btn then table.remove(themedBgs, j) end end
                if isActive then regBg(btn) end
            end
        end)
    end

    -- ============================================================
    -- TAB 1 : BINDS + IDLE + BPM
    -- ============================================================
    local pg1 = pages["📋 BINDS"]

    makeLabel(pg1, "DIR",     UDim2.new(0,8,0,6),   UDim2.new(0,36,0,20), Color3.fromRGB(255,80,180))
    makeLabel(pg1, "KEY",     UDim2.new(0,48,0,6),  UDim2.new(0,62,0,20), Color3.fromRGB(255,80,180))
    makeLabel(pg1, "ANIM ID", UDim2.new(0,116,0,6), UDim2.new(0,180,0,20),Color3.fromRGB(255,80,180))
    makeLabel(pg1, "TEST",    UDim2.new(0,308,0,6), UDim2.new(0,90,0,20), Color3.fromRGB(255,80,180))
    makeDivider(pg1, 28, Color3.fromRGB(0,200,255))

    -- ROW_H covers: dir/key/id/test line, speed slider line, hold-loop +
    -- alt-input line, and a small scrollable alt list — per binding row.
    local ROW_H  = 150
    local startY = 36
    for i, row in ipairs(CurrentBindings) do
        local y = startY + (i-1)*ROW_H
        local dirColor = ARROW_COLORS[row.dir] and ARROW_COLORS[row.dir].hit or Color3.fromRGB(255,255,255)
        makeLabel(pg1, row.dir, UDim2.new(0,8,0,y), UDim2.new(0,36,0,28), dirColor)

        local keyBtn = makeBtn(pg1, row.key, UDim2.new(0,48,0,y), UDim2.new(0,60,0,28), Color3.fromRGB(25,35,48))
        row.keyBtn = keyBtn
        keyBtn.MouseButton1Click:Connect(function()
            rebindingRow = row; keyBtn.Text = "..."
            keyBtn.BackgroundColor3 = Color3.fromRGB(0,200,255)
        end)

        local idBox = makeBox(pg1, UDim2.new(0,116,0,y), UDim2.new(0,180,0,28), "anim id", row.id)
        idBox.FocusLost:Connect(function()
            local n = idBox.Text:gsub("%D","")
            if n ~= "" then row.id = n; destroyTrack(row.key) else idBox.Text = row.id end
        end)

        local testBtn = makeBtn(pg1, "▶ TEST", UDim2.new(0,306,0,y), UDim2.new(0,90,0,28), Color3.fromRGB(0,180,230), nil, 11)
        testBtn.MouseButton1Down:Connect(function()
            local chosenId = rollBindingAnimId(row)
            playAnim(row.key, chosenId, row.speedMult, row.holdLoop); hitArrow(row.dir); swapFace(row.dir)
        end)
        testBtn.MouseButton1Up:Connect(function()   stopAnim(row.key); releaseArrow(row.dir); restoreFace() end)

        local sy = y + 30
        makeLabel(pg1, "SPD", UDim2.new(0,8,0,sy), UDim2.new(0,32,0,22), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
        local spdValLbl = makeLabel(pg1, string.format("x%.2f", row.speedMult),
            UDim2.new(0,280,0,sy), UDim2.new(0,112,0,22), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 10)
        regText(spdValLbl)
        makeSlider(pg1, UDim2.new(0,42,0,sy+2), UDim2.new(0,232,0,14), 0.25, 3, row.speedMult, function(v)
            row.speedMult = v
            spdValLbl.Text = string.format("x%.2f", v)
            if StatusState.currentAnimName == row.key then
                StatusState.speedMult = v
                if refreshStatusUI then refreshStatusUI() end
            end
        end)

        -- ── HOLD-LOOP toggle + alt-animation entry inputs ──
        local sy2 = y + 58
        local holdBtn = makeBtn(pg1, "🔁 HOLD-LOOP: OFF", UDim2.new(0,8,0,sy2), UDim2.new(0,140,0,24), Color3.fromRGB(35,35,55), nil, 9)
        holdBtn.MouseButton1Click:Connect(function()
            row.holdLoop = not row.holdLoop
            holdBtn.Text = row.holdLoop and "🔁 HOLD-LOOP: ON" or "🔁 HOLD-LOOP: OFF"
            holdBtn.BackgroundColor3 = row.holdLoop and Color3.fromRGB(0,180,230) or Color3.fromRGB(35,35,55)
        end)

        local altIdBox   = makeBox(pg1, UDim2.new(0,154,0,sy2), UDim2.new(0,142,0,24), "alt anim id")
        local altPctBox  = makeBox(pg1, UDim2.new(0,300,0,sy2), UDim2.new(0,52,0,24), "%")
        local addAltBtn  = makeBtn(pg1, "+", UDim2.new(0,356,0,sy2), UDim2.new(0,40,0,24), Color3.fromRGB(60,190,120), nil, 12)

        -- ── mini scrolling list of this row's configured alts ──
        local sy3 = y + 86
        local altListFrame = Instance.new("ScrollingFrame")
        altListFrame.Size = UDim2.new(1,-16,0,56)
        altListFrame.Position = UDim2.new(0,8,0,sy3)
        altListFrame.BackgroundColor3 = Color3.fromRGB(18,18,26)
        altListFrame.BorderSizePixel = 0
        altListFrame.ScrollBarThickness = 3
        altListFrame.CanvasSize = UDim2.new(0,0,0,0)
        altListFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
        altListFrame.Parent = pg1
        uiCorner(altListFrame, UDim.new(0,6))
        local altListLayout = Instance.new("UIListLayout")
        altListLayout.Padding = UDim.new(0,2)
        altListLayout.Parent = altListFrame
        local altListPad = Instance.new("UIPadding")
        altListPad.PaddingTop = UDim.new(0,2); altListPad.PaddingLeft = UDim.new(0,2); altListPad.PaddingRight = UDim.new(0,2)
        altListPad.Parent = altListFrame

        addAltBtn.MouseButton1Click:Connect(function()
            local id = altIdBox.Text:gsub("%D","")
            local chance = tonumber(altPctBox.Text)
            if id == "" or not chance then notify("Enter an alt anim ID + % chance", "warn"); return end
            table.insert(row.alts, { id = id, chance = math.clamp(chance, 0, 100) })
            altIdBox.Text = ""; altPctBox.Text = ""
            refreshRowAltList(row, altListFrame)
        end)

        refreshRowAltList(row, altListFrame)
    end

    -- Idle section
    local idleY = startY + #CurrentBindings * ROW_H + 6
    makeDivider(pg1, idleY, Color3.fromRGB(255,80,180))
    local idleTitleLbl = makeLabel(pg1, "🎵 IDLE ANIMATION",
        UDim2.new(0,8,0,idleY+6), UDim2.new(0,200,0,20),
        Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left)
    regText(idleTitleLbl)
    local statusLbl = makeLabel(pg1, "● OFF",
        UDim2.new(0,200,0,idleY+6), UDim2.new(0,196,0,20),
        Color3.fromRGB(160,80,80), Enum.TextXAlignment.Right)

    local idleIdBox = makeBox(pg1, UDim2.new(0,8,0,idleY+30), UDim2.new(1,-16,0,28), "Paste idle anim ID...")
    local loopBtn  = makeBtn(pg1, "⟳ LOOP",  UDim2.new(0,8,0,idleY+64), UDim2.new(0,120,0,28), Color3.fromRGB(70,120,255))
    local onceBtn  = makeBtn(pg1, "▶ ONCE",  UDim2.new(0,136,0,idleY+64), UDim2.new(0,120,0,28), Color3.fromRGB(60,190,120))
    local idleStop = makeBtn(pg1, "■ STOP",  UDim2.new(0,264,0,idleY+64), UDim2.new(0,130,0,28), Color3.fromRGB(190,50,50))

    local function getIdleId() return idleIdBox.Text:gsub("%D","") end
    loopBtn.MouseButton1Click:Connect(function()
        local id = getIdleId(); if id == "" then notify("Enter an idle anim ID first", "warn"); return end
        playIdleLoop(id); statusLbl.Text = "● LOOPING"; statusLbl.TextColor3 = Color3.fromRGB(80,220,120)
        notify("Idle looping started", "success")
    end)
    onceBtn.MouseButton1Click:Connect(function()
        local id = getIdleId(); if id == "" then notify("Enter an idle anim ID first", "warn"); return end
        playIdleOnce(id, function()
            if not idleLooping then statusLbl.Text = "● OFF"; statusLbl.TextColor3 = Color3.fromRGB(160,80,80) end
        end)
        statusLbl.Text = "▶ PLAYING"; statusLbl.TextColor3 = Color3.fromRGB(0,200,255)
    end)
    idleStop.MouseButton1Click:Connect(function()
        stopIdle(); statusLbl.Text = "● OFF"; statusLbl.TextColor3 = Color3.fromRGB(160,80,80)
    end)

    -- BPM controls
    local bpmY = idleY + 98
    makeLabel(pg1, "BPM", UDim2.new(0,8,0,bpmY), UDim2.new(0,38,0,28),
        Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
    local bpmBox = makeBox(pg1, UDim2.new(0,44,0,bpmY), UDim2.new(0,68,0,28), "100", tostring(idleBPM))

    makeLabel(pg1, "BASE", UDim2.new(0,120,0,bpmY), UDim2.new(0,44,0,28),
        Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
    local baseBox = makeBox(pg1, UDim2.new(0,164,0,bpmY), UDim2.new(0,68,0,28), "100", tostring(BASE_BPM))

    local applyBpmBtn = makeBtn(pg1, "✓ APPLY", UDim2.new(0,240,0,bpmY), UDim2.new(0,154,0,28), Color3.fromRGB(70,120,255), nil, 11)

    local bpmSliderY = bpmY + 36
    makeLabel(pg1, "DRAG", UDim2.new(0,8,0,bpmSliderY), UDim2.new(0,50,0,22), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
    local bpmValLbl = makeLabel(pg1, string.format("%d BPM", idleBPM),
        UDim2.new(0,300,0,bpmSliderY), UDim2.new(0,94,0,22), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 11)
    regText(bpmValLbl)
    local bpmSlider = makeSlider(pg1, UDim2.new(0,62,0,bpmSliderY+2), UDim2.new(0,232,0,14), 40, 300, idleBPM, function(v)
        v = math.floor(v + 0.5)
        setIdleBPM(v)
        bpmBox.Text = tostring(v)
        bpmValLbl.Text = string.format("%d BPM", v)
    end)

    bpmBox.FocusLost:Connect(function()
        local n = tonumber(bpmBox.Text)
        if n and n > 0 then
            idleBPM = n
            bpmSlider.setValue(n, false)
            bpmValLbl.Text = string.format("%d BPM", n)
            setIdleBPM(n)
        else
            bpmBox.Text = tostring(idleBPM)
        end
    end)
    baseBox.FocusLost:Connect(function()
        local n = tonumber(baseBox.Text)
        if n and n > 0 then BASE_BPM = n else baseBox.Text = tostring(BASE_BPM) end
        setIdleBPM(idleBPM)
    end)
    applyBpmBtn.MouseButton1Click:Connect(function()
        local n = tonumber(bpmBox.Text)
        local b = tonumber(baseBox.Text)
        if n and n > 0 then idleBPM = n; bpmSlider.setValue(n, false); bpmValLbl.Text = string.format("%d BPM", n) end
        if b and b > 0 then BASE_BPM = b end
        setIdleBPM(idleBPM)
        notify("BPM settings applied", "info")
    end)

    -- Beat-sync indicator + mini equalizer, docked at bottom of binds tab
    local vizY = bpmSliderY + 34
    makeDivider(pg1, vizY, Color3.fromRGB(0,200,255))
    makeLabel(pg1, "BEAT SYNC", UDim2.new(0,8,0,vizY+6), UDim2.new(0,90,0,18), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
    beatDot = Instance.new("Frame")
    beatDot.AnchorPoint = Vector2.new(0.5,0.5)
    beatDot.Size = UDim2.new(0,10,0,10)
    beatDot.Position = UDim2.new(0,100,0,vizY+15)
    beatDot.BackgroundColor3 = Color3.fromRGB(255,80,180)
    beatDot.BorderSizePixel = 0; beatDot.Parent = pg1
    uiCorner(beatDot, UDim.new(1,0))
    regBg(beatDot)

    makeLabel(pg1, "EQUALIZER", UDim2.new(0,140,0,vizY+6), UDim2.new(0,100,0,18), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
    local eqHolder = Instance.new("Frame")
    eqHolder.Size = UDim2.new(0,120,0,26); eqHolder.Position = UDim2.new(0,140,0,vizY+22)
    eqHolder.BackgroundTransparency = 1; eqHolder.Parent = pg1
    local eqLayout = Instance.new("UIListLayout")
    eqLayout.FillDirection = Enum.FillDirection.Horizontal
    eqLayout.Padding = UDim.new(0,3)
    eqLayout.VerticalAlignment = Enum.VerticalAlignment.Bottom
    eqLayout.Parent = eqHolder
    eqBars = {}
    for i = 1, 8 do
        local bar = Instance.new("Frame")
        bar.Size = UDim2.new(0, 10, 0.2, 0)
        bar.BackgroundColor3 = THEMES[currentThemeName].accent
        bar.BorderSizePixel = 0
        bar.Parent = eqHolder
        uiCorner(bar, UDim.new(0,2))
        regBg(bar)
        table.insert(eqBars, bar)
    end

    makeLabel(pg1, "WAVEFORM", UDim2.new(0,270,0,vizY+6), UDim2.new(0,120,0,18), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
    local waveHolder = Instance.new("Frame")
    waveHolder.Size = UDim2.new(0,130,0,26); waveHolder.Position = UDim2.new(0,270,0,vizY+22)
    waveHolder.BackgroundTransparency = 1; waveHolder.Parent = pg1
    local waveLayout = Instance.new("UIListLayout")
    waveLayout.FillDirection = Enum.FillDirection.Horizontal
    waveLayout.Padding = UDim.new(0,2)
    waveLayout.VerticalAlignment = Enum.VerticalAlignment.Center
    waveLayout.Parent = waveHolder
    waveBars = {}
    for i = 1, 16 do
        local bar = Instance.new("Frame")
        bar.Size = UDim2.new(0, 5, 0.3, 0)
        bar.BackgroundColor3 = THEMES[currentThemeName].accent2
        bar.BorderSizePixel = 0
        bar.Parent = waveHolder
        uiCorner(bar, UDim.new(0,2))
        table.insert(waveBars, bar)
    end

    -- ============================================================
    -- STATUS PANEL (bottom-fixed strip inside binds tab; also mirrored on preview tab)
    -- ============================================================
    local statusY = vizY + 60
    makeDivider(pg1, statusY, Color3.fromRGB(255,80,180))
    local statusTitle = makeLabel(pg1, "📊 STATUS", UDim2.new(0,8,0,statusY+6), UDim2.new(0,120,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
    regText(statusTitle)

    local statLbl1 = makeLabel(pg1, "Anim: —", UDim2.new(0,8,0,statusY+26), UDim2.new(1,-16,0,16), Color3.fromRGB(210,210,220), Enum.TextXAlignment.Left, 10)
    local statLbl2 = makeLabel(pg1, "Speed: x1.00   BPM: 100   State: STOPPED", UDim2.new(0,8,0,statusY+44), UDim2.new(1,-16,0,16), Color3.fromRGB(160,160,180), Enum.TextXAlignment.Left, 10)

    function refreshStatusUI()
        statLbl1.Text = string.format("Anim: %s (%s)", StatusState.currentAnimName, StatusState.currentAnimId ~= "" and StatusState.currentAnimId or "—")
        statLbl2.Text = string.format("Speed: x%.2f   BPM: %d   State: %s",
            StatusState.speedMult, StatusState.bpm, StatusState.playing and "PLAYING" or "STOPPED")
    end
    refreshStatusUI()

    -- ============================================================
    -- TAB 2 : ANIMATION SEQUENCE MAKER
    -- ============================================================
    local pg2 = pages["🎬 MAKER"]

    local makerTitle = makeLabel(pg2, "🎬 ANIMATION SEQUENCE MAKER",
        UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20),
        Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(makerTitle)
    makeDivider(pg2, 28, Color3.fromRGB(0,200,255))
    makeLabel(pg2, "ANIM ID",   UDim2.new(0,8,0,34),   UDim2.new(0,220,0,16), Color3.fromRGB(160,160,160))
    makeLabel(pg2, "DELAY (s)", UDim2.new(0,232,0,34), UDim2.new(0,80,0,16),  Color3.fromRGB(160,160,160))

    local seqScroll = Instance.new("ScrollingFrame")
    seqScroll.Size = UDim2.new(1,-16,0,182); seqScroll.Position = UDim2.new(0,8,0,52)
    seqScroll.BackgroundColor3 = Color3.fromRGB(18,18,26); seqScroll.BorderSizePixel = 0
    seqScroll.ScrollBarThickness = 4; seqScroll.CanvasSize = UDim2.new(0,0,0,0)
    seqScroll.Parent = pg2; uiCorner(seqScroll)

    local seqLayout = Instance.new("UIListLayout")
    seqLayout.SortOrder = Enum.SortOrder.LayoutOrder; seqLayout.Padding = UDim.new(0,4)
    seqLayout.Parent = seqScroll
    local seqPad = Instance.new("UIPadding")
    seqPad.PaddingTop = UDim.new(0,4); seqPad.PaddingLeft = UDim.new(0,4); seqPad.PaddingRight = UDim.new(0,4)
    seqPad.Parent = seqScroll

    local function updateCanvas()
        seqScroll.CanvasSize = UDim2.new(0,0,0, seqLayout.AbsoluteContentSize.Y + 10)
    end
    seqLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(updateCanvas)

    local seqStatusLbl = makeLabel(pg2, "● IDLE",
        UDim2.new(0,8,0,310), UDim2.new(1,-16,0,18), Color3.fromRGB(160,80,80), Enum.TextXAlignment.Left)

    local function addSeqRow(id, delay)
        local step = { id = id or "", delay = delay or 0.5 }
        table.insert(sequence, step)

        local row = Instance.new("Frame")
        row.Size = UDim2.new(1,-8,0,30); row.BackgroundColor3 = Color3.fromRGB(24,24,36)
        row.BorderSizePixel = 0; row.LayoutOrder = #sequence; row.Parent = seqScroll
        uiCorner(row, UDim.new(0,6))

        local idIn = makeBox(row, UDim2.new(0,4,0,3), UDim2.new(0,206,0,24), "Animation ID", step.id)
        idIn.FocusLost:Connect(function() step.id = idIn.Text:gsub("%D","") end)

        local dlIn = makeBox(row, UDim2.new(0,216,0,3), UDim2.new(0,64,0,24), "0.5", tostring(step.delay))
        dlIn.FocusLost:Connect(function() step.delay = tonumber(dlIn.Text) or 0.5 end)

        local rem = makeBtn(row, "✕", UDim2.new(0,286,0,3), UDim2.new(0,28,0,24), Color3.fromRGB(170,35,35), nil, 12)
        rem.MouseButton1Click:Connect(function()
            for j, s in ipairs(sequence) do if s == step then table.remove(sequence, j); break end end
            for j, r in ipairs(seqRowFrames) do if r == row then table.remove(seqRowFrames, j); break end end
            row:Destroy(); updateCanvas()
        end)

        table.insert(seqRowFrames, row); updateCanvas()
    end

    makeBtn(pg2, "+ ADD STEP", UDim2.new(0,8,0,240), UDim2.new(1,-16,0,26), Color3.fromRGB(35,35,55))
        .MouseButton1Click:Connect(function() addSeqRow("", 0.5) end)

    local seqPlay  = makeBtn(pg2, "▶ PLAY SEQUENCE", UDim2.new(0,8,0,272),   UDim2.new(0,176,0,30), Color3.fromRGB(60,190,120))
    local seqStop  = makeBtn(pg2, "■ STOP",          UDim2.new(0,192,0,272), UDim2.new(0,90,0,30),  Color3.fromRGB(190,50,50))
    local seqClear = makeBtn(pg2, "🗑 CLEAR",         UDim2.new(0,290,0,272), UDim2.new(0,106,0,30), Color3.fromRGB(70,25,70))

    seqPlay.MouseButton1Click:Connect(function()
        if #sequence == 0 then seqStatusLbl.Text = "⚠ Add steps first"; notify("Add a step first", "warn"); return end
        playSequence()
        seqStatusLbl.Text = "▶ RUNNING"; seqStatusLbl.TextColor3 = Color3.fromRGB(80,220,120)
        notify("Sequence started", "info")
        task.spawn(function()
            while seqPlaying do task.wait(0.1) end
            seqStatusLbl.Text = "● IDLE"; seqStatusLbl.TextColor3 = Color3.fromRGB(160,80,80)
        end)
    end)
    seqStop.MouseButton1Click:Connect(function()
        stopSequence(); seqStatusLbl.Text = "● IDLE"; seqStatusLbl.TextColor3 = Color3.fromRGB(160,80,80)
    end)
    seqClear.MouseButton1Click:Connect(function()
        stopSequence(); sequence = {}
        for _, r in ipairs(seqRowFrames) do pcall(function() r:Destroy() end) end
        seqRowFrames = {}; updateCanvas()
        seqStatusLbl.Text = "● IDLE"; seqStatusLbl.TextColor3 = Color3.fromRGB(160,80,80)
    end)

    -- Quick preset loader
    makeDivider(pg2, 330, Color3.fromRGB(0,200,255))
    local presetTitle = makeLabel(pg2, "⚡ QUICK PRESETS", UDim2.new(0,8,0,336), UDim2.new(1,-16,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
    regText(presetTitle)
    for i, preset in ipairs(PRESETS) do
        local py = 356 + (i-1)*32
        local pbtn = makeBtn(pg2, "📥 "..preset.name, UDim2.new(0,8,0,py), UDim2.new(1,-16,0,26), Color3.fromRGB(28,12,42), nil, 11)
        pbtn.MouseButton1Click:Connect(function()
            for _, row in ipairs(CurrentBindings) do
                local newId = preset.ids[row.dir]
                if newId then
                    row.id = newId
                    if row.keyBtn then destroyTrack(row.key) end
                end
            end
            notify("Loaded preset: "..preset.name, "success")
        end)
    end

    -- ============================================================
    -- TAB 3 : LIVE PREVIEW
    -- ============================================================
    local pg3v = pages["👁 PREVIEW"]
    local pvTitle = makeLabel(pg3v, "👁 LIVE CHARACTER PREVIEW", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(pvTitle)
    makeDivider(pg3v, 28, Color3.fromRGB(0,200,255))

    local viewport = Instance.new("ViewportFrame")
    viewport.Size = UDim2.new(1,-16,0,260); viewport.Position = UDim2.new(0,8,0,36)
    viewport.BackgroundColor3 = Color3.fromRGB(10,10,16)
    -- ViewportFrames render pure black without an explicit light source —
    -- these three properties act as a simple fill light for the clone.
    viewport.Ambient = Color3.fromRGB(140,140,150)
    viewport.LightColor = Color3.fromRGB(255,255,255)
    viewport.LightDirection = Vector3.new(-0.6, -1, -0.4)
    viewport.Parent = pg3v
    uiCorner(viewport)
    local vpStroke = uiStroke(viewport, Color3.fromRGB(255,80,180), 2)
    regStroke(vpStroke)

    previewCloneContainer = Instance.new("WorldModel")
    previewCloneContainer.Parent = viewport

    previewCam = Instance.new("Camera")
    previewCam.FieldOfView = 60
    previewCam.CFrame = CFrame.new(Vector3.new(0, 3, 6), Vector3.new(0, 3, 0))
    viewport.CurrentCamera = previewCam
    previewCam.Parent = viewport

    -- fix the earlier placeholder orbit loop now that previewCam/container exist
    task.spawn(function()
        while true do
            RunService.Heartbeat:Wait()
            if previewCam then
                previewAngle = previewAngle + 0.3
                local rad = math.rad(previewAngle)
                local dist = 6
                local pos = Vector3.new(math.sin(rad) * dist, 4, math.cos(rad) * dist)
                previewCam.CFrame = CFrame.new(pos, Vector3.new(0, 3, 0))
            end
        end
    end)

    local pvHint = makeLabel(pg3v, "Rotates automatically. Plays whatever anim you last triggered.",
        UDim2.new(0,8,0,300), UDim2.new(1,-16,0,18), Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)

    local pvRefreshBtn = makeBtn(pg3v, "🔄 REFRESH CLONE", UDim2.new(0,8,0,324), UDim2.new(1,-16,0,28), Color3.fromRGB(70,120,255))
    pvRefreshBtn.MouseButton1Click:Connect(function()
        if character then buildPreviewClone(character); notify("Preview clone refreshed", "info") end
    end)

    -- Status mirror on preview tab
    makeDivider(pg3v, 362, Color3.fromRGB(255,80,180))
    local pvStat1 = makeLabel(pg3v, "Anim: —", UDim2.new(0,8,0,370), UDim2.new(1,-16,0,16), Color3.fromRGB(210,210,220), Enum.TextXAlignment.Left, 10)
    local pvStat2 = makeLabel(pg3v, "Speed: x1.00   BPM: 100   State: STOPPED", UDim2.new(0,8,0,388), UDim2.new(1,-16,0,16), Color3.fromRGB(160,160,180), Enum.TextXAlignment.Left, 10)
    local oldRefresh = refreshStatusUI
    refreshStatusUI = function()
        oldRefresh()
        pvStat1.Text = string.format("Anim: %s (%s)", StatusState.currentAnimName, StatusState.currentAnimId ~= "" and StatusState.currentAnimId or "—")
        pvStat2.Text = string.format("Speed: x%.2f   BPM: %d   State: %s",
            StatusState.speedMult, StatusState.bpm, StatusState.playing and "PLAYING" or "STOPPED")
    end

    -- Recents list
    makeDivider(pg3v, 414, Color3.fromRGB(0,200,255))
    local recentsTitle = makeLabel(pg3v, "🕐 RECENT ANIMATIONS", UDim2.new(0,8,0,420), UDim2.new(1,-16,0,18), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 11)
    regText(recentsTitle)
    local recentsScroll = Instance.new("ScrollingFrame")
    recentsScroll.Size = UDim2.new(1,-16,0,120); recentsScroll.Position = UDim2.new(0,8,0,442)
    recentsScroll.BackgroundColor3 = Color3.fromRGB(18,18,26); recentsScroll.BorderSizePixel = 0
    recentsScroll.ScrollBarThickness = 4; recentsScroll.CanvasSize = UDim2.new(0,0,0,0)
    recentsScroll.Parent = pg3v; uiCorner(recentsScroll)
    local recentsLayout = Instance.new("UIListLayout")
    recentsLayout.Padding = UDim.new(0,4); recentsLayout.Parent = recentsScroll

    function refreshRecentsUI()
        for _, c in ipairs(recentsScroll:GetChildren()) do
            if c:IsA("Frame") then c:Destroy() end
        end
        for i, r in ipairs(recents) do
            local row = Instance.new("Frame")
            row.Size = UDim2.new(1,-8,0,26); row.BackgroundColor3 = Color3.fromRGB(24,24,36)
            row.BorderSizePixel = 0; row.LayoutOrder = i; row.Parent = recentsScroll
            uiCorner(row, UDim.new(0,6))
            makeLabel(row, r.label.." — "..r.id, UDim2.new(0,8,0,0), UDim2.new(1,-90,1,0), Color3.fromRGB(200,200,215), Enum.TextXAlignment.Left, 10)
            local playBtn = makeBtn(row, "▶", UDim2.new(1,-40,0,2), UDim2.new(0,32,0,22), Color3.fromRGB(60,190,120), nil, 11)
            playBtn.MouseButton1Click:Connect(function()
                updatePreviewAnimation(r.id, r.label)
                notify("Previewing "..r.label, "info")
            end)
        end
    end
    refreshRecentsUI()

    -- ============================================================
    -- TAB 4 : FAVORITES
    -- ============================================================
    local pg4 = pages["❤ FAVES"]
    local favTitle = makeLabel(pg4, "❤ FAVORITE ANIMATIONS", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(favTitle)
    makeDivider(pg4, 28, Color3.fromRGB(0,200,255))

    local favNameBox = makeBox(pg4, UDim2.new(0,8,0,36), UDim2.new(0,180,0,28), "Name")
    local favIdBox   = makeBox(pg4, UDim2.new(0,196,0,36), UDim2.new(0,138,0,28), "Anim ID")
    local favAddBtn  = makeBtn(pg4, "+ SAVE", UDim2.new(0,340,0,36), UDim2.new(0,74,0,28), Color3.fromRGB(60,190,120), nil, 11)

    local favScroll = Instance.new("ScrollingFrame")
    favScroll.Size = UDim2.new(1,-16,0,300); favScroll.Position = UDim2.new(0,8,0,72)
    favScroll.BackgroundColor3 = Color3.fromRGB(18,18,26); favScroll.BorderSizePixel = 0
    favScroll.ScrollBarThickness = 4; favScroll.CanvasSize = UDim2.new(0,0,0,0)
    favScroll.Parent = pg4; uiCorner(favScroll)
    local favLayout = Instance.new("UIListLayout")
    favLayout.Padding = UDim.new(0,4); favLayout.Parent = favScroll
    local favPad = Instance.new("UIPadding")
    favPad.PaddingTop = UDim.new(0,4); favPad.PaddingLeft = UDim.new(0,4); favPad.PaddingRight = UDim.new(0,4)
    favPad.Parent = favScroll

    local function refreshFavUI()
        for _, c in ipairs(favScroll:GetChildren()) do
            if c:IsA("Frame") then c:Destroy() end
        end
        for i, f in ipairs(favorites) do
            local row = Instance.new("Frame")
            row.Size = UDim2.new(1,-8,0,32); row.BackgroundColor3 = Color3.fromRGB(24,24,36)
            row.BorderSizePixel = 0; row.LayoutOrder = i; row.Parent = favScroll
            uiCorner(row, UDim.new(0,6))
            makeLabel(row, f.name.." — "..f.id, UDim2.new(0,8,0,0), UDim2.new(1,-116,1,0), Color3.fromRGB(200,200,215), Enum.TextXAlignment.Left, 10)
            local playBtn = makeBtn(row, "▶", UDim2.new(1,-146,0,3), UDim2.new(0,34,0,26), Color3.fromRGB(0,180,230), nil, 11)
            local addPlBtn = makeBtn(row, "➕LIST", UDim2.new(1,-108,0,3), UDim2.new(0,58,0,26), Color3.fromRGB(70,120,255), nil, 9)
            local delBtn = makeBtn(row, "✕", UDim2.new(1,-42,0,3), UDim2.new(0,34,0,26), Color3.fromRGB(170,35,35), nil, 11)

            playBtn.MouseButton1Click:Connect(function()
                updatePreviewAnimation(f.id, f.name)
                pushRecent(f.id, f.name)
                notify("Previewing favorite: "..f.name, "info")
            end)
            addPlBtn.MouseButton1Click:Connect(function()
                table.insert(playlist, { name = f.name, id = f.id, delay = 1 })
                if refreshPlaylistUI then refreshPlaylistUI() end
                notify(f.name.." added to playlist", "success")
            end)
            delBtn.MouseButton1Click:Connect(function()
                table.remove(favorites, i)
                refreshFavUI()
            end)
        end
    end

    favAddBtn.MouseButton1Click:Connect(function()
        local id = favIdBox.Text:gsub("%D","")
        local nm = favNameBox.Text ~= "" and favNameBox.Text or ("Fav "..(#favorites+1))
        if id == "" then notify("Enter an anim ID to save", "warn"); return end
        table.insert(favorites, { name = nm, id = id })
        favNameBox.Text = ""; favIdBox.Text = ""
        refreshFavUI()
        notify("Saved to favorites: "..nm, "success")
    end)
    refreshFavUI()

    -- ============================================================
    -- TAB 5 : PLAYLIST MANAGER
    -- ============================================================
    local pg5 = pages["🎶 LIST"]
    local plTitle = makeLabel(pg5, "🎶 PLAYLIST MANAGER", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(plTitle)
    makeDivider(pg5, 28, Color3.fromRGB(0,200,255))

    local plScroll = Instance.new("ScrollingFrame")
    plScroll.Size = UDim2.new(1,-16,0,260); plScroll.Position = UDim2.new(0,8,0,36)
    plScroll.BackgroundColor3 = Color3.fromRGB(18,18,26); plScroll.BorderSizePixel = 0
    plScroll.ScrollBarThickness = 4; plScroll.CanvasSize = UDim2.new(0,0,0,0)
    plScroll.Parent = pg5; uiCorner(plScroll)
    local plLayout = Instance.new("UIListLayout")
    plLayout.Padding = UDim.new(0,4); plLayout.Parent = plScroll
    local plPad = Instance.new("UIPadding")
    plPad.PaddingTop = UDim.new(0,4); plPad.PaddingLeft = UDim.new(0,4); plPad.PaddingRight = UDim.new(0,4)
    plPad.Parent = plScroll

    function refreshPlaylistUI()
        for _, c in ipairs(plScroll:GetChildren()) do
            if c:IsA("Frame") then c:Destroy() end
        end
        for i, item in ipairs(playlist) do
            local row = Instance.new("Frame")
            row.Size = UDim2.new(1,-8,0,30)
            row.BackgroundColor3 = (i == playlistIndex and playlistPlaying) and Color3.fromRGB(50,30,60) or Color3.fromRGB(24,24,36)
            row.BorderSizePixel = 0; row.LayoutOrder = i; row.Parent = plScroll
            uiCorner(row, UDim.new(0,6))
            makeLabel(row, string.format("%d. %s — %s", i, item.name or item.id, item.id),
                UDim2.new(0,8,0,0), UDim2.new(1,-40,1,0), Color3.fromRGB(200,200,215), Enum.TextXAlignment.Left, 10)
            local rem = makeBtn(row, "✕", UDim2.new(1,-34,0,3), UDim2.new(0,26,0,24), Color3.fromRGB(170,35,35), nil, 11)
            rem.MouseButton1Click:Connect(function()
                table.remove(playlist, i)
                refreshPlaylistUI()
            end)
        end
    end
    refreshPlaylistUI()

    local plAddBox = makeBox(pg5, UDim2.new(0,8,0,300), UDim2.new(0,220,0,26), "Animation ID to add")
    local plAddBtn = makeBtn(pg5, "+ ADD", UDim2.new(0,236,0,300), UDim2.new(0,80,0,26), Color3.fromRGB(35,35,55))
    plAddBtn.MouseButton1Click:Connect(function()
        local id = plAddBox.Text:gsub("%D","")
        if id == "" then notify("Enter an anim ID", "warn"); return end
        table.insert(playlist, { name = "Track "..(#playlist+1), id = id, delay = 1 })
        plAddBox.Text = ""
        refreshPlaylistUI()
    end)

    local plPlay = makeBtn(pg5, "▶ PLAY", UDim2.new(0,8,0,332), UDim2.new(0,96,0,28), Color3.fromRGB(60,190,120), nil, 11)
    local plPrev = makeBtn(pg5, "⏮ PREV", UDim2.new(0,110,0,332), UDim2.new(0,86,0,28), Color3.fromRGB(70,120,255), nil, 11)
    local plNext = makeBtn(pg5, "NEXT ⏭", UDim2.new(0,202,0,332), UDim2.new(0,86,0,28), Color3.fromRGB(70,120,255), nil, 11)
    local plStop = makeBtn(pg5, "■ STOP", UDim2.new(0,294,0,332), UDim2.new(0,100,0,28), Color3.fromRGB(190,50,50), nil, 11)

    plPlay.MouseButton1Click:Connect(function()
        if #playlist == 0 then notify("Playlist is empty", "warn"); return end
        playPlaylist(); notify("Playlist started", "info")
    end)
    plPrev.MouseButton1Click:Connect(prevPlaylistTrack)
    plNext.MouseButton1Click:Connect(nextPlaylistTrack)
    plStop.MouseButton1Click:Connect(function() stopPlaylist(); refreshPlaylistUI() end)

    -- ============================================================
    -- TAB : FX  (vinyl record / CRT mode / spectrogram / BPM detector)
    -- ============================================================
    local pgFx = pages["🎛 FX"]

    -- ── Vinyl record widget ──
    local vinylTitle = makeLabel(pgFx, "💿 VINYL RECORD", UDim2.new(0,8,0,6), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(vinylTitle)
    makeDivider(pgFx, 28, Color3.fromRGB(0,200,255))
    makeLabel(pgFx, "Spins faster the higher the current speed / BPM is.",
        UDim2.new(0,8,0,34), UDim2.new(1,-16,0,18), Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)

    local vinylHolder = Instance.new("Frame")
    vinylHolder.Size = UDim2.new(0,120,0,120); vinylHolder.Position = UDim2.new(0,8,0,56)
    vinylHolder.BackgroundColor3 = Color3.fromRGB(8,8,10); vinylHolder.BorderSizePixel = 0
    vinylHolder.Parent = pgFx; uiCorner(vinylHolder, UDim.new(1,0))
    local vinylHolderStroke = uiStroke(vinylHolder, Color3.fromRGB(255,80,180), 2)
    regStroke(vinylHolderStroke)

    vinylImage = Instance.new("ImageLabel")
    vinylImage.Size = UDim2.new(1,-8,1,-8); vinylImage.Position = UDim2.new(0,4,0,4)
    vinylImage.BackgroundTransparency = 1
    vinylImage.Image = VINYL_DECAL_ID ~= "" and ("rbxassetid://"..VINYL_DECAL_ID) or ""
    vinylImage.ScaleType = Enum.ScaleType.Fit
    vinylImage.Parent = vinylHolder
    -- fallback ring so it still reads as a record before a decal is set
    local vinylRing = Instance.new("Frame")
    vinylRing.Size = UDim2.new(1,0,1,0); vinylRing.BackgroundTransparency = 1
    vinylRing.Parent = vinylHolder
    uiCorner(vinylRing, UDim.new(1,0))
    uiStroke(vinylRing, Color3.fromRGB(60,60,65), 1)
    local vinylCenter = Instance.new("Frame")
    vinylCenter.Size = UDim2.new(0,14,0,14); vinylCenter.AnchorPoint = Vector2.new(0.5,0.5)
    vinylCenter.Position = UDim2.new(0.5,0,0.5,0); vinylCenter.BackgroundColor3 = Color3.fromRGB(255,80,180)
    vinylCenter.BorderSizePixel = 0; vinylCenter.Parent = vinylHolder
    uiCorner(vinylCenter, UDim.new(1,0)); regBg(vinylCenter)

    local vinylIdBox = makeBox(pgFx, UDim2.new(0,140,0,56), UDim2.new(1,-148,0,28), "Vinyl decal asset ID")
    vinylIdBox.FocusLost:Connect(function()
        local id = vinylIdBox.Text:gsub("%D","")
        VINYL_DECAL_ID = id
        vinylImage.Image = id ~= "" and ("rbxassetid://"..id) or ""
    end)
    makeLabel(pgFx, "Paste your record-label decal ID above — art rotates with the disc.",
        UDim2.new(0,140,0,88), UDim2.new(1,-148,0,32), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)

    makeLabel(pgFx, "BASE RPM", UDim2.new(0,140,0,124), UDim2.new(0,70,0,24), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
    local vinylRpmBox = makeBox(pgFx, UDim2.new(0,214,0,124), UDim2.new(0,70,0,24), "33.3", tostring(vinylBaseRPM))
    vinylRpmBox.FocusLost:Connect(function()
        local n = tonumber(vinylRpmBox.Text)
        if n and n > 0 then vinylBaseRPM = n else vinylRpmBox.Text = tostring(vinylBaseRPM) end
    end)

    -- ── CRT monitor mode ──
    makeDivider(pgFx, 190, Color3.fromRGB(255,80,180))
    local crtTitle = makeLabel(pgFx, "📺 CRT MONITOR MODE", UDim2.new(0,8,0,196), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(crtTitle)
    makeLabel(pgFx, "Adds scanlines + a green-tinted flicker over the whole panel.",
        UDim2.new(0,8,0,218), UDim2.new(1,-16,0,18), Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)

    local crtBtn = makeBtn(pgFx, "📴  CRT MODE OFF", UDim2.new(0,8,0,240), UDim2.new(1,-16,0,32), Color3.fromRGB(35,35,55), nil, 11)
    crtBtn.MouseButton1Click:Connect(function()
        crtEnabled = not crtEnabled
        if crtOverlay then crtOverlay.Visible = crtEnabled end
        if crtEnabled then
            crtBtn.Text = "📺  CRT MODE ON"
            crtBtn.BackgroundColor3 = THEMES[currentThemeName].accent
        else
            crtBtn.Text = "📴  CRT MODE OFF"
            crtBtn.BackgroundColor3 = Color3.fromRGB(35,35,55)
        end
        notify(crtEnabled and "CRT mode enabled" or "CRT mode disabled", "info")
    end)

    -- ── Mic spectrogram ──
    makeDivider(pgFx, 284, Color3.fromRGB(0,200,255))
    local specTitle = makeLabel(pgFx, "🎙 SPECTROGRAM (MIC)", UDim2.new(0,8,0,290), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(specTitle)
    local specStatusLbl = makeLabel(pgFx, "● not connected", UDim2.new(0,200,0,290), UDim2.new(1,-208,0,20), Color3.fromRGB(160,80,80), Enum.TextXAlignment.Right, 10)

    makeLabel(pgFx, "Uses Roblox's mic Audio API if this client/game supports it; otherwise the bars react to animation state instead.",
        UDim2.new(0,8,0,312), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)

    local specHolder = Instance.new("Frame")
    specHolder.Size = UDim2.new(1,-16,0,60); specHolder.Position = UDim2.new(0,8,0,346)
    specHolder.BackgroundColor3 = Color3.fromRGB(18,18,26); specHolder.BorderSizePixel = 0
    specHolder.Parent = pgFx; uiCorner(specHolder)
    local specLayout = Instance.new("UIListLayout")
    specLayout.FillDirection = Enum.FillDirection.Horizontal
    specLayout.Padding = UDim.new(0,3)
    specLayout.VerticalAlignment = Enum.VerticalAlignment.Bottom
    specLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
    specLayout.Parent = specHolder
    specBars = {}
    for i = 1, SPEC_BAR_COUNT do
        local bar = Instance.new("Frame")
        bar.Size = UDim2.new(0, 6, 0.15, 0)
        bar.BackgroundColor3 = THEMES[currentThemeName].accent2
        bar.BorderSizePixel = 0
        bar.Parent = specHolder
        uiCorner(bar, UDim.new(0,2))
        table.insert(specBars, bar)
    end

    local micBtn = makeBtn(pgFx, "🎙 REQUEST MIC ACCESS", UDim2.new(0,8,0,414), UDim2.new(1,-16,0,30), Color3.fromRGB(70,120,255), nil, 11)
    micBtn.MouseButton1Click:Connect(function()
        local success = setupMicSpectrogram()
        if success then
            specStatusLbl.Text = "● mic connected"; specStatusLbl.TextColor3 = Color3.fromRGB(80,220,120)
            notify("Mic spectrogram connected", "success")
        else
            specStatusLbl.Text = "● using fallback"; specStatusLbl.TextColor3 = Color3.fromRGB(255,180,60)
            notify("Mic unavailable here — using animation-synced fallback", "warn")
        end
    end)

    -- ── Animation BPM detector ──
    makeDivider(pgFx, 456, Color3.fromRGB(255,80,180))
    local bpmDetectTitle = makeLabel(pgFx, "🎯 BPM DETECTOR", UDim2.new(0,8,0,462), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(bpmDetectTitle)
    makeLabel(pgFx, "Tap mode reads the rhythm of your Z/X/C/V presses. Idle mode reads the loaded idle clip's length.",
        UDim2.new(0,8,0,484), UDim2.new(1,-16,0,28), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)

    local detectedLbl = makeLabel(pgFx, "Detected: — BPM", UDim2.new(0,8,0,516), UDim2.new(1,-16,0,22), Color3.fromRGB(0,200,255), Enum.TextXAlignment.Left, 13)

    function refreshBpmDetectorUI()
        if detectedBPM then
            detectedLbl.Text = string.format("Detected: %d BPM  (from %d taps)", detectedBPM, #bpmDetectTaps)
        end
    end

    makeLabel(pgFx, "BEATS/LOOP", UDim2.new(0,8,0,542), UDim2.new(0,80,0,26), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
    local beatsBox = makeBox(pgFx, UDim2.new(0,90,0,542), UDim2.new(0,50,0,26), "4", tostring(beatsPerIdleLoop))
    beatsBox.FocusLost:Connect(function()
        local n = tonumber(beatsBox.Text)
        if n and n > 0 then beatsPerIdleLoop = n else beatsBox.Text = tostring(beatsPerIdleLoop) end
    end)

    local idleDetectBtn = makeBtn(pgFx, "📐 FROM IDLE LENGTH", UDim2.new(0,148,0,542), UDim2.new(0,150,0,26), Color3.fromRGB(70,120,255), nil, 10)
    idleDetectBtn.MouseButton1Click:Connect(function()
        local bpm = detectBpmFromIdleLength(beatsPerIdleLoop)
        if bpm then
            refreshBpmDetectorUI()
            notify("Detected "..bpm.." BPM from idle length", "success")
        else
            notify("Load an idle animation first", "warn")
        end
    end)

    local resetTapBtn = makeBtn(pgFx, "↺ RESET TAPS", UDim2.new(0,306,0,542), UDim2.new(0,116,0,26), Color3.fromRGB(170,35,35), nil, 10)
    resetTapBtn.MouseButton1Click:Connect(function()
        bpmDetectTaps = {}; detectedBPM = nil
        detectedLbl.Text = "Detected: — BPM"
        notify("Tap history cleared", "info")
    end)

    local applyDetectedBtn = makeBtn(pgFx, "✓ APPLY DETECTED BPM TO IDLE", UDim2.new(0,8,0,574), UDim2.new(1,-16,0,28), Color3.fromRGB(60,190,120), nil, 11)
    applyDetectedBtn.MouseButton1Click:Connect(function()
        if detectedBPM then
            setIdleBPM(detectedBPM)
            notify("Idle BPM set to "..detectedBPM, "success")
        else
            notify("No BPM detected yet — tap some keys or use idle length", "warn")
        end
    end)

    -- ── Shared music player (no VC) ──
    makeDivider(pgFx, 616, Color3.fromRGB(0,200,255))
    local musicTitle = makeLabel(pgFx, "🎵 MUSIC PLAYER (NO VC)", UDim2.new(0,8,0,622), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(musicTitle)
    makeLabel(pgFx, "Plays an uploaded audio asset for the whole server directly — no voice chat involved, so no mic compression/suppression.",
        UDim2.new(0,8,0,644), UDim2.new(1,-16,0,30), Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)
    makeLabel(pgFx, "Needs the companion server script installed once in ServerScriptService (see notes).",
        UDim2.new(0,8,0,676), UDim2.new(1,-16,0,18), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 9)

    local musicIdBox = makeBox(pgFx, UDim2.new(0,8,0,698), UDim2.new(1,-16,0,28), "Audio asset ID (e.g. 1234567890)")

    local musicPlayBtn = makeBtn(pgFx, "▶ PLAY FOR EVERYONE", UDim2.new(0,8,0,730), UDim2.new(0,220,0,30), Color3.fromRGB(60,190,120), nil, 11)
    local musicStopBtn = makeBtn(pgFx, "■ STOP", UDim2.new(0,236,0,730), UDim2.new(0,160,0,30), Color3.fromRGB(190,50,50), nil, 11)

    musicStatusLbl = makeLabel(pgFx, "● idle", UDim2.new(0,8,0,766), UDim2.new(1,-16,0,20), Color3.fromRGB(160,80,80), Enum.TextXAlignment.Left, 11)

    musicPlayBtn.MouseButton1Click:Connect(function()
        local id = musicIdBox.Text:gsub("%D","")
        if id == "" then notify("Enter an audio asset ID first", "warn"); return end
        playMusicTrack(id)
        notify("Requested track "..id.." for the whole server", "info")
    end)
    musicStopBtn.MouseButton1Click:Connect(function()
        stopMusicTrack()
        notify("Stopped shared music", "info")
    end)

    -- ============================================================
    -- TAB 6 : SETTINGS  (legs / face / theme)
    -- ============================================================
    local pg6 = pages["⚙ SETTINGS"]

    local legsTitle = makeLabel(pg6, "🦵 LEGS ON GROUND (Optional)",
        UDim2.new(0,8,0,8), UDim2.new(1,-16,0,20),
        Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(legsTitle)
    makeDivider(pg6, 30, Color3.fromRGB(0,200,255))
    makeLabel(pg6, "Prevents walking and jumping — character stays planted.",
        UDim2.new(0,8,0,36), UDim2.new(1,-16,0,28),
        Color3.fromRGB(140,140,160), Enum.TextXAlignment.Left, 10)

    local legsBtn = makeBtn(pg6, "🔓  LEGS UNLOCKED  (OFF)",
        UDim2.new(0,8,0,68), UDim2.new(1,-16,0,32), Color3.fromRGB(35,35,55), nil, 11)
    legsBtn.MouseButton1Click:Connect(function()
        legsLocked = not legsLocked
        applyLegsLock(legsLocked)
        if legsLocked then
            legsBtn.Text = "🔒  LEGS LOCKED  (ON)"
            legsBtn.BackgroundColor3 = THEMES[currentThemeName].accent
        else
            legsBtn.Text = "🔓  LEGS UNLOCKED  (OFF)"
            legsBtn.BackgroundColor3 = Color3.fromRGB(35,35,55)
        end
        notify(legsLocked and "Legs locked" or "Legs unlocked", "info")
    end)

    -- Dynamic face
    makeDivider(pg6, 114, Color3.fromRGB(255,80,180))
    local faceTitle = makeLabel(pg6, "😊 DYNAMIC FACE IDs  (per arrow direction)",
        UDim2.new(0,8,0,120), UDim2.new(1,-16,0,20),
        Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(faceTitle)
    makeLabel(pg6, "Only works if your character has a face decal on its head.",
        UDim2.new(0,8,0,142), UDim2.new(1,-16,0,18),
        Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)

    local faceOrder = { "LEFT", "DOWN", "UP", "RIGHT" }
    for i, dir in ipairs(faceOrder) do
        local y = 164 + (i-1)*38
        local col = ARROW_COLORS[dir] and ARROW_COLORS[dir].hit or Color3.fromRGB(255,255,255)
        makeLabel(pg6, dir, UDim2.new(0,8,0,y), UDim2.new(0,52,0,28), col, Enum.TextXAlignment.Left)
        local fb = makeBox(pg6, UDim2.new(0,66,0,y), UDim2.new(1,-74,0,28), "Face decal ID for "..dir, faceIds[dir])
        local capturedDir = dir
        fb.FocusLost:Connect(function()
            faceIds[capturedDir] = fb.Text:gsub("%D","")
        end)
    end

    -- Theme selector
    makeDivider(pg6, 320, Color3.fromRGB(0,200,255))
    local themeTitle = makeLabel(pg6, "🎨 THEME", UDim2.new(0,8,0,326), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(themeTitle)

    local themeNames = { "Pink", "Cyan", "Purple", "RGB" }
    local themeSwatch = { Pink = Color3.fromRGB(255,80,180), Cyan = Color3.fromRGB(0,200,255),
                           Purple = Color3.fromRGB(165,85,255), RGB = Color3.fromRGB(255,255,255) }
    for i, tn in ipairs(themeNames) do
        local tx = 8 + (i-1) * 104
        local tbtn = makeBtn(pg6, tn, UDim2.new(0, tx, 0, 356), UDim2.new(0, 96, 0, 32), themeSwatch[tn], Color3.fromRGB(20,20,20), 11)
        tbtn.MouseButton1Click:Connect(function() setTheme(tn) end)
    end
    -- ============================================================
    -- RP TUG-OF-WAR UI
    -- ============================================================
    makeDivider(pg6, 400, Color3.fromRGB(0,200,255))
    local rpTitle = makeLabel(pg6, "⚔ RP TUG-OF-WAR (PROXIMITY)", UDim2.new(0,8,0,406), UDim2.new(1,-16,0,20), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Left, 12)
    regText(rpTitle)
    makeLabel(pg6, "Moves you toward the target on every note press.", UDim2.new(0,8,0,428), UDim2.new(1,-16,0,18), Color3.fromRGB(130,130,150), Enum.TextXAlignment.Left, 10)

    -- Toggle Button
    local rpBtn = makeBtn(pg6, "📴  RP MODE OFF", UDim2.new(0,8,0,450), UDim2.new(0,160,0,32), Color3.fromRGB(35,35,55), nil, 11)
    rpBtn.MouseButton1Click:Connect(function()
        rpModeEnabled = not rpModeEnabled
        if rpModeEnabled then
            rpBtn.Text = "⚔  RP MODE ON"
            rpBtn.BackgroundColor3 = THEMES[currentThemeName].accent
        else
            rpBtn.Text = "📴  RP MODE OFF"
            rpBtn.BackgroundColor3 = Color3.fromRGB(35,35,55)
        end
    end)

    -- Player Search Box
    local targetBox = makeBox(pg6, UDim2.new(0,176,0,450), UDim2.new(1,-184,0,32), "Target Player Name...")
    targetBox.FocusLost:Connect(function()
        setTargetPlayer(targetBox.Text)
    end)

    -- Speed Slider
    makeLabel(pg6, "SPEED", UDim2.new(0,8,0,490), UDim2.new(0,40,0,22), Color3.fromRGB(160,160,160), Enum.TextXAlignment.Left, 10)
    local rpValLbl = makeLabel(pg6, string.format("%.1f studs", rpMoveStep), UDim2.new(0,300,0,490), UDim2.new(0,94,0,22), Color3.fromRGB(255,80,180), Enum.TextXAlignment.Right, 11)
    regText(rpValLbl)
    
    makeSlider(pg6, UDim2.new(0,50,0,494), UDim2.new(0,250,0,14), 0.5, 5, rpMoveStep, function(v)
        rpMoveStep = v
        rpValLbl.Text = string.format("%.1f studs", v)
    end)

    -- Hide / show toggle
    toggleBtn.MouseButton1Click:Connect(function()
        guiVisible = not guiVisible
        contentArea.Visible = guiVisible
        tabBar.Visible = guiVisible
        toggleBtn.Text = guiVisible and "HIDE" or "SHOW"
    end)

    startVisualizerLoops()
    notify("FNF Injector v6.1 loaded", "success")

    return sg
end

-- ============================================================
--  LAUNCH
-- ============================================================
task.spawn(setupMusicRemote)
buildArrowHud()
buildGUI()
--[[
    ╔══════════════════════════════════════════════════════╗
    ║      FNF CINEMATIC STANDOFF INTRO — v1.0              ║
    ║  Back-to-back walk-off → turn → local VS split-cam     ║
    ╚══════════════════════════════════════════════════════╝
    HOW IT WORKS:
    • Run this SAME script on both accounts (yours + your alt).
    • Give each one a "role": "A" or "B", and the other account's
      username. Role decides which spot / which walk direction.
    • Movement (walking to spots, turning to face each other) is
      done by tweening YOUR OWN HumanoidRootPart CFrame. Since you
      have network ownership of your own character, this actually
      replicates to everyone else in the server — they see two
      players walk to their marks and square up, nothing more.
    • The VS split-screen face-cam overlay is a local ScreenGui —
      built from cloned heads in a ViewportFrame, never touches
      the real character, so only the client running this sees it.
    • True frame-perfect sync between two separate accounts needs
      a server RemoteEvent (one is auto-detected if present, see
      SYNC section). Without one, it falls back to a 3-2-1 on-screen
      countdown so you just start both scripts and count together.
]]

local Players           = game:GetService("Players")
local RunService         = game:GetService("RunService")
local TweenService       = game:GetService("TweenService")
local UserInputService   = game:GetService("UserInputService")
local ReplicatedStorage  = game:GetService("ReplicatedStorage")
local LocalPlayer        = Players.LocalPlayer
local PlayerGui          = LocalPlayer:WaitForChild("PlayerGui")
local camera              = workspace.CurrentCamera

-- ============================================================
--  CONFIG — tweak freely
-- ============================================================
local Config = {
    TriggerKey       = Enum.KeyCode.B,   -- press this to arm/start
    AltUsername       = "mentransmigrasilo",-- who the other rig is

    -- world anchor: standoff happens relative to wherever LocalPlayer
    -- is standing the moment you press the trigger key
    SpotDistance      = 14,      -- how far apart the final marks are
    CenterGap         = 3,       -- how close together they start, back to back
    WalkDuration      = 2.4,
    TurnDuration      = 0.55,
    HoldBeforeSplit   = 0.35,
    SplitCamDuration  = 2.6,     -- how long the VS face-cam stays up
    CountdownEnabled  = true,    -- 3-2-1 fallback sync

    WalkAnimId        = "180426354",  -- placeholder walk-forward loop
    IdleStanceAnimId  = "507766388",  -- placeholder FNF-ready idle stance

    -- role placement — "A" ends up on the right (Boyfriend side),
    -- "B" ends up on the left (Opponent side), classic FNF stage layout
    Role              = "A",     -- "A" or "B", set per-account
}

-- ============================================================
--  OPTIONAL SERVER SYNC (auto-detected, not required)
-- ============================================================
-- If a companion server script drops a "StandoffSyncRemote" RemoteEvent
-- into ReplicatedStorage, we use it to get a synced go-time. If it's not
-- there, we just fall back to the local 3-2-1 countdown.
local syncRemote = nil
task.spawn(function()
    pcall(function()
        syncRemote = ReplicatedStorage:WaitForChild("StandoffSyncRemote", 3)
    end)
end)

-- ============================================================
--  STATE
-- ============================================================
local standoffActive = false
local rigConns = {}

local function cleanupConns()
    for _, c in ipairs(rigConns) do pcall(function() c:Disconnect() end) end
    rigConns = {}
end

-- ============================================================
--  CHARACTER / ANIMATOR HELPERS
-- ============================================================
local function getHumanoidBits(char)
    if not char then return nil end
    local hum  = char:FindFirstChildOfClass("Humanoid")
    local root = char:FindFirstChild("HumanoidRootPart")
    if not hum or not root then return nil end
    local animator = hum:FindFirstChildOfClass("Animator") or Instance.new("Animator", hum)
    return { hum = hum, root = root, animator = animator }
end

local function loadAnim(animator, id, looped, priority)
    local ok, track = pcall(function()
        local anim = Instance.new("Animation")
        anim.AnimationId = "rbxassetid://"..id
        local t = animator:LoadAnimation(anim)
        t.Looped = looped
        t.Priority = priority or Enum.AnimationPriority.Action
        return t
    end)
    if ok then return track end
    return nil
end

-- Smoothly tween a HumanoidRootPart's CFrame. Root gets anchored for the
-- duration so physics doesn't fight the tween, then restored after.
local function tweenRoot(root, targetCFrame, duration, style, direction)
    local wasAnchored = root.Anchored
    root.Anchored = true
    local tw = TweenService:Create(root, TweenInfo.new(duration, style or Enum.EasingStyle.Sine, direction or Enum.EasingDirection.InOut), {
        CFrame = targetCFrame
    })
    tw:Play()
    tw.Completed:Wait()
    root.Anchored = wasAnchored
    return true
end

-- ============================================================
--  ON-SCREEN 3-2-1 SYNC COUNTDOWN (local only, helps line up two clients)
-- ============================================================
local function showCountdown(cb)
    local sg = Instance.new("ScreenGui")
    sg.Name = "StandoffCountdown"; sg.ResetOnSpawn = false; sg.Parent = PlayerGui
    local lbl = Instance.new("TextLabel")
    lbl.Size = UDim2.new(0,300,0,150)
    lbl.AnchorPoint = Vector2.new(0.5,0.5)
    lbl.Position = UDim2.new(0.5,0,0.5,0)
    lbl.BackgroundTransparency = 1
    lbl.Font = Enum.Font.GothamBlack
    lbl.TextSize = 90
    lbl.TextColor3 = Color3.fromRGB(255,80,180)
    lbl.TextStrokeTransparency = 0
    lbl.TextStrokeColor3 = Color3.new(0,0,0)
    lbl.Parent = sg

    task.spawn(function()
        for _, n in ipairs({"3","2","1","GO"}) do
            lbl.Text = n
            lbl.Size = UDim2.new(0,420,0,210)
            TweenService:Create(lbl, TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out), {
                Size = UDim2.new(0,300,0,150)
            }):Play()
            task.wait(0.7)
        end
        sg:Destroy()
        if cb then cb() end
    end)
end

-- ============================================================
--  CINEMATIC CAMERA (local only)
-- ============================================================
local savedCamType, savedCamCFrame
local function lockCinematicCamera(startCFrame)
    savedCamType  = camera.CameraType
    savedCamCFrame = camera.CFrame
    camera.CameraType = Enum.CameraType.Scriptable
    camera.CFrame = startCFrame
end

local function restoreCamera()
    camera.CameraType = savedCamType or Enum.CameraType.Custom
end

-- Wide establishing dolly: pulls back slowly as both walk to their marks
local function dollyCamera(fromCFrame, toCFrame, duration)
    local tw = TweenService:Create(camera, TweenInfo.new(duration, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut), {
        CFrame = toCFrame
    })
    camera.CFrame = fromCFrame
    tw:Play()
    return tw
end

-- Subtle handheld shake, used during the VS face-cam hold
local shakeConn = nil
local function startCameraShake(intensity)
    if shakeConn then shakeConn:Disconnect() end
    shakeConn = RunService.RenderStepped:Connect(function()
        if camera.CameraType == Enum.CameraType.Scriptable then
            local ox = (math.random() - 0.5) * intensity
            local oy = (math.random() - 0.5) * intensity
            camera.CFrame = camera.CFrame * CFrame.new(ox, oy, 0)
        end
    end)
end
local function stopCameraShake()
    if shakeConn then shakeConn:Disconnect(); shakeConn = nil end
end

-- ============================================================
--  VS FACE-CAM SPLIT SCREEN (local only — WorldModel clone trick,
--  same pattern as a standard viewport character preview)
-- ============================================================
local function buildFaceViewport(char, parent, position, size)
    local vp = Instance.new("ViewportFrame")
    vp.Size = size; vp.Position = position
    vp.BackgroundColor3 = Color3.fromRGB(6,6,10)
    vp.Ambient = Color3.fromRGB(160,160,170)
    vp.LightColor = Color3.fromRGB(255,255,255)
    vp.LightDirection = Vector3.new(-0.4,-0.6,-0.6)
    vp.Parent = parent

    local world = Instance.new("WorldModel")
    world.Parent = vp

    local wasArchivable = char.Archivable
    if not wasArchivable then char.Archivable = true end
    local ok, clone = pcall(function() return char:Clone() end)
    if not wasArchivable then char.Archivable = wasArchivable end
    if not ok or not clone then return vp end

    for _, d in ipairs(clone:GetDescendants()) do
        if d:IsA("Script") or d:IsA("LocalScript") then d:Destroy() end
    end
    clone.Parent = world

    local head = clone:FindFirstChild("Head")
    local hrp  = clone:FindFirstChild("HumanoidRootPart") or clone.PrimaryPart
    if hrp then hrp.Anchored = true end

    local cam = Instance.new("Camera")
    if head then
        cam.CFrame = CFrame.new(head.Position + Vector3.new(0,0,2.2), head.Position)
    else
        cam.CFrame = CFrame.new(Vector3.new(0,3,4), Vector3.new(0,3,0))
    end
    cam.FieldOfView = 45
    cam.Parent = vp
    vp.CurrentCamera = cam

    return vp
end

local function playVSFaceCam(myChar, altChar, holdTime)
    local sg = Instance.new("ScreenGui")
    sg.Name = "StandoffVS"; sg.ResetOnSpawn = false; sg.IgnoreGuiInset = true
    sg.Parent = PlayerGui

    local root = Instance.new("Frame")
    root.Size = UDim2.new(1,0,1,0); root.BackgroundColor3 = Color3.new(0,0,0)
    root.BackgroundTransparency = 1; root.ClipsDescendants = true; root.Parent = sg

    -- LEFT half (opponent / role B viewport by convention)
    local leftHalf = Instance.new("Frame")
    leftHalf.Size = UDim2.new(0.5,0,1,0); leftHalf.Position = UDim2.new(-0.5,0,0,0)
    leftHalf.BackgroundTransparency = 1; leftHalf.ClipsDescendants = true; leftHalf.Parent = root

    -- RIGHT half (player / role A viewport by convention)
    local rightHalf = Instance.new("Frame")
    rightHalf.Size = UDim2.new(0.5,0,1,0); rightHalf.Position = UDim2.new(1,0,0,0)
    rightHalf.BackgroundTransparency = 1; rightHalf.ClipsDescendants = true; rightHalf.Parent = root

    local isRoleA = (Config.Role == "A")
    local myHalf, altHalf = (isRoleA and rightHalf or leftHalf), (isRoleA and leftHalf or rightHalf)

    buildFaceViewport(altChar, altHalf, UDim2.new(0,0,0,0), UDim2.new(1,0,1,0))
    buildFaceViewport(myChar,  myHalf,  UDim2.new(0,0,0,0), UDim2.new(1,0,1,0))

    -- diagonal seam divider
    local seam = Instance.new("Frame")
    seam.Size = UDim2.new(0,10,2.4,0)
    seam.AnchorPoint = Vector2.new(0.5,0.5)
    seam.Position = UDim2.new(0.5,0,0.5,0)
    seam.Rotation = 18
    seam.BackgroundColor3 = Color3.fromRGB(255,80,180)
    seam.BorderSizePixel = 0; seam.ZIndex = 5; seam.Parent = root

    -- VS badge, pops in with a punchy scale tween
    local vsLbl = Instance.new("TextLabel")
    vsLbl.Size = UDim2.new(0,140,0,70)
    vsLbl.AnchorPoint = Vector2.new(0.5,0.5)
    vsLbl.Position = UDim2.new(0.5,0,0.5,0)
    vsLbl.BackgroundTransparency = 1
    vsLbl.Text = "VS"; vsLbl.Font = Enum.Font.GothamBlack; vsLbl.TextSize = 0
    vsLbl.TextColor3 = Color3.fromRGB(255,255,255)
    vsLbl.TextStrokeTransparency = 0; vsLbl.TextStrokeColor3 = Color3.fromRGB(255,80,180)
    vsLbl.ZIndex = 6; vsLbl.Parent = root

    -- slide both halves in
    TweenService:Create(leftHalf,  TweenInfo.new(0.45, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Position = UDim2.new(0,0,0,0) }):Play()
    local rightTw = TweenService:Create(rightHalf, TweenInfo.new(0.45, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { Position = UDim2.new(0.5,0,0,0) })
    rightTw:Play()
    task.wait(0.25)
    TweenService:Create(vsLbl, TweenInfo.new(0.35, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { TextSize = 64 }):Play()

    startCameraShake(0.02)
    task.wait(holdTime)
    stopCameraShake()

    -- fade out
    local fadeTime = 0.4
    TweenService:Create(leftHalf,  TweenInfo.new(fadeTime, Enum.EasingStyle.Quad), { Position = UDim2.new(-0.5,0,0,0) }):Play()
    TweenService:Create(rightHalf, TweenInfo.new(fadeTime, Enum.EasingStyle.Quad), { Position = UDim2.new(1,0,0,0) }):Play()
    TweenService:Create(vsLbl, TweenInfo.new(fadeTime, Enum.EasingStyle.Quad), { TextSize = 0 }):Play()
    task.wait(fadeTime + 0.1)
    sg:Destroy()
end

-- ============================================================
--  MAIN SEQUENCE
-- ============================================================
local function runStandoff()
    if standoffActive then return end
    standoffActive = true

    local altPlayer = Players:FindFirstChild(Config.AltUsername)
    if not altPlayer or not altPlayer.Character then
        warn("[Standoff] Alt player not found or has no character.")
        standoffActive = false
        return
    end

    local myChar  = LocalPlayer.Character
    local altChar = altPlayer.Character
    local myBits  = getHumanoidBits(myChar)
    local altBits = getHumanoidBits(altChar)
    if not myBits or not altBits then
        warn("[Standoff] Missing Humanoid/HumanoidRootPart on one of the rigs.")
        standoffActive = false
        return
    end

    local function begin()
        -- anchor point = my current position, facing my current LookVector
        local originCFrame = CFrame.new(myBits.root.Position, myBits.root.Position + myBits.root.CFrame.LookVector)
        local isRoleA = (Config.Role == "A")

        -- back-to-back start: both on the same line, facing opposite ways,
        -- separated only by CenterGap
        local halfGap = Config.CenterGap / 2
        local myStart  = originCFrame * CFrame.new(isRoleA and halfGap or -halfGap, 0, 0)
                            * (isRoleA and CFrame.Angles(0, math.rad(90), 0) or CFrame.Angles(0, math.rad(-90), 0))
        tweenRoot(myBits.root, myStart, 0.01, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut)

        -- cinematic camera: wide shot centered on both, looking down the line
        local camStart = originCFrame * CFrame.new(0, 5, 10)
        local camEnd   = originCFrame * CFrame.new(0, 4, Config.SpotDistance * 0.9)
        lockCinematicCamera(camStart)
        local camTw = dollyCamera(camStart, camEnd, Config.WalkDuration + Config.TurnDuration)

        -- walk animation while marching to spot
        local walkTrack = loadAnim(myBits.animator, Config.WalkAnimId, true, Enum.AnimationPriority.Movement)
        if walkTrack then walkTrack:Play(0.15) end

        -- walk out to final mark, still facing away from each other
        local mySpot = originCFrame * CFrame.new(isRoleA and Config.SpotDistance/2 or -Config.SpotDistance/2, 0, 0)
                            * (isRoleA and CFrame.Angles(0, math.rad(90), 0) or CFrame.Angles(0, math.rad(-90), 0))
        tweenRoot(myBits.root, mySpot, Config.WalkDuration, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

        if walkTrack then walkTrack:Stop(0.15) end

        -- turn to face each other — role A faces -X (toward B), role B faces +X (toward A)
        local turnCFrame = mySpot * (isRoleA and CFrame.Angles(0, math.rad(-90), 0) or CFrame.Angles(0, math.rad(90), 0))
        -- correct: rebuild from position only, then rotate to actually look at the other spot
        local lookTarget = originCFrame.Position + Vector3.new(isRoleA and -1 or 1, 0, 0)
        turnCFrame = CFrame.new(mySpot.Position, Vector3.new(lookTarget.X, mySpot.Position.Y, mySpot.Position.Z))
        tweenRoot(myBits.root, turnCFrame, Config.TurnDuration, Enum.EasingStyle.Back, Enum.EasingDirection.Out)

        -- FNF-ready idle stance
        local idleTrack = loadAnim(myBits.animator, Config.IdleStanceAnimId, true, Enum.AnimationPriority.Idle)
        if idleTrack then idleTrack:Play(0.2) end

        task.wait(Config.HoldBeforeSplit)

        -- local-only face-cam VS split
        playVSFaceCam(myChar, altChar, Config.SplitCamDuration)

        restoreCamera()
        standoffActive = false
    end

    if Config.CountdownEnabled and not syncRemote then
        showCountdown(begin)
    elseif syncRemote then
        -- server-synced start: fire our readiness, wait for GO from server
        syncRemote:FireServer("ready")
        local conn
        conn = syncRemote.OnClientEvent:Connect(function(cmd)
            if cmd == "go" then
                conn:Disconnect()
                task.spawn(begin)
            end
        end)
        table.insert(rigConns, conn)
    else
        task.spawn(begin)
    end
end

-- ============================================================
--  TRIGGER
-- ============================================================
table.insert(rigConns, UserInputService.InputBegan:Connect(function(input, gp)
    if gp then return end
    if input.KeyCode == Config.TriggerKey then
        runStandoff()
    end
end))

print("[Standoff] Loaded. Role: "..Config.Role.." — press "..Config.TriggerKey.Name.." to start, boss man.")

Features & Information

you can rap battle with your friends

Community Comments (0)

No comments yet for this script.

More Scripts for Universal Scripts