Skip to content
RoRoScripts
Animation studio preview
UnknownUniversal

Animation studio

Script
Views
335
Likes
2
Dislikes
0
Added
Script
local function dbg(msg) pcall(function() print("[UGC Anim Studio] " .. tostring(msg)) end) end

local plrs = game:GetService("Players")
local as = game:GetService("AssetService")
local aes = game:GetService("AvatarEditorService")
local rs = game:GetService("RunService")
local hs = game:GetService("HttpService")
local cp = game:GetService("ContentProvider")
local uis = game:GetService("UserInputService")
local mps = game:GetService("MarketplaceService")
local lp = plrs.LocalPlayer

local env = (typeof(getgenv) == "function" and getgenv()) or _G
if type(env.UgcAnimStudioCleanup) == "function" then pcall(env.UgcAnimStudioCleanup) end

local connections = {}
local function track(c) connections[#connections + 1] = c; return c end

local toast
local onMetaChanged
local onHistoryChanged

local camera = workspace.CurrentCamera or workspace:WaitForChild("Camera", 10)
local vpSize = camera and camera.ViewportSize or Vector2.new(1280, 720)
local W0, H0 = 700, 450
local baseScale = math.clamp(math.min(vpSize.X / 1920, vpSize.Y / 1080), 0.55, 3) * 1.35
local scale = math.min(baseScale, (vpSize.X * 0.97) / W0, (vpSize.Y * 0.95) / H0)
if scale <= 0 or scale ~= scale then scale = 1 end
local function s(n) return math.round(n * scale) end

local C = {
	bg = Color3.fromRGB(38, 38, 40),
	panel = Color3.fromRGB(46, 46, 48),
	surface = Color3.fromRGB(58, 58, 60),
	surfaceHi = Color3.fromRGB(74, 74, 76),
	stroke = Color3.fromRGB(96, 96, 98),
	accent = Color3.fromRGB(150, 150, 152),
	accent2 = Color3.fromRGB(180, 180, 182),
	text = Color3.fromRGB(232, 232, 234),
	textDim = Color3.fromRGB(178, 178, 180),
	textMuted = Color3.fromRGB(130, 130, 132),
	overlay = Color3.fromRGB(24, 24, 26),
	green = Color3.fromRGB(150, 150, 152),
	red = Color3.fromRGB(190, 120, 120),
	gold = Color3.fromRGB(210, 190, 140),
	orange = Color3.fromRGB(190, 160, 120),
	pillBg = Color3.fromRGB(72, 72, 76),
	pillTx = Color3.fromRGB(210, 210, 212),
	badBg = Color3.fromRGB(90, 60, 60),
	badTx = Color3.fromRGB(220, 170, 170),
	wearBg = Color3.fromRGB(88, 88, 92),
}
local WHITE = Color3.new(1, 1, 1)

local function new(class, props, parent)
	local o = Instance.new(class)
	for k, v in pairs(props or {}) do o[k] = v end
	if parent then o.Parent = parent end
	return o
end

local EASE = {
	Linear = function(x) return x end,
	Quad = function(x) return 1 - (1 - x) * (1 - x) end,
	Quint = function(x) local y = 1 - x; return 1 - y * y * y * y * y end,
	Back = function(x)
		local c1 = 1.70158
		local y = x - 1
		return 1 + (c1 + 1) * y * y * y + c1 * y * y
	end,
}
local function lerpValue(a, b, e)
	local ty = typeof(a)
	if ty == "number" then return a + (b - a) * e end
	return a:Lerp(b, e)
end

local activeTweens, tweenSlots = {}, setmetatable({}, {__mode = "k"})
local tweenConn
local TweenObj = {}
TweenObj.__index = TweenObj
function TweenObj:Cancel() self.cancelled = true end
TweenObj.Destroy = TweenObj.Cancel

local function releaseTween(rec)
	local slots = tweenSlots[rec.obj]
	if slots then
		for prop in pairs(rec.p) do
			if slots[prop] == rec then slots[prop] = nil end
		end
	end
end

local function tweenStep()
	local now = os.clock()
	local n = #activeTweens
	local i = 1
	while i <= n do
		local rec = activeTweens[i]
		local o = rec.obj
		local done = rec.cancelled or o.Parent == nil or next(rec.p) == nil
		if not done then
			local x = (now - rec.t0) / rec.dur
			if rec.loop then
				x = x - math.floor(x)
			elseif x >= 1 then
				x = 1
				done = true
			end
			local e = rec.ease(x)
			for prop, pr in pairs(rec.p) do
				if done and not rec.loop then
					o[prop] = pr[2]
				else
					o[prop] = lerpValue(pr[1], pr[2], e)
				end
			end
		end
		if done then
			releaseTween(rec)
			activeTweens[i] = activeTweens[n]
			activeTweens[n] = nil
			n -= 1
		else
			i += 1
		end
	end
	if n == 0 and tweenConn then
		tweenConn:Disconnect()
		tweenConn = nil
	end
end

local function tw(o, time, props, style, dir, loop)
	local slots = tweenSlots[o]
	if not slots then slots = {}; tweenSlots[o] = slots end
	local rec = setmetatable({
		obj = o, p = {}, t0 = os.clock(), dur = math.max(time, 0.001), loop = loop and true or false,
		ease = EASE[style and style.Name or "Quad"] or EASE.Quad,
	}, TweenObj)
	if dir == Enum.EasingDirection.In and rec.loop == false and style and style.Name ~= "Linear" then
		local out = rec.ease
		rec.ease = function(x) return 1 - out(1 - x) end
	end
	for prop, goal in pairs(props) do
		local old = slots[prop]
		if old then old.p[prop] = nil end
		local ok, from = pcall(function() return o[prop] end)
		if ok and from ~= nil then
			rec.p[prop] = {from, goal}
			slots[prop] = rec
		end
	end
	if next(rec.p) == nil then return rec end
	activeTweens[#activeTweens + 1] = rec
	if not tweenConn then tweenConn = rs.RenderStepped:Connect(tweenStep) end
	return rec
end
local function tweenShutdown()
	if tweenConn then tweenConn:Disconnect(); tweenConn = nil end
	table.clear(activeTweens)
end

local function collectFade(root)
	local list = {}
	local function collect(obj)
		local isText = obj:IsA("TextLabel") or obj:IsA("TextButton") or obj:IsA("TextBox")
		list[#list + 1] = {obj = obj, bgT = obj.BackgroundTransparency, txtT = isText and obj.TextTransparency or nil}
		for _, c in ipairs(obj:GetChildren()) do
			if c:IsA("GuiObject") then collect(c) end
		end
	end
	collect(root)
	return list
end

local function corner(p, r) return new("UICorner", {CornerRadius = UDim.new(0, r or s(8))}, p) end
local function round(p) return new("UICorner", {CornerRadius = UDim.new(1, 0)}, p) end
local function stroke(p, color, th, tr)
	return new("UIStroke", {Color = color or C.stroke, Thickness = th or 1, Transparency = tr or 0, ApplyStrokeMode = Enum.ApplyStrokeMode.Border}, p)
end
local function pad(p, l, r, t, b)
	return new("UIPadding", {PaddingLeft = UDim.new(0, l or 0), PaddingRight = UDim.new(0, r or 0), PaddingTop = UDim.new(0, t or 0), PaddingBottom = UDim.new(0, b or 0)}, p)
end

local shimmers = {}
local function applyShimmer(obj, c1, c2, speed)
	if shimmers[obj] then return end
	local grad = new("UIGradient", {
		Name = "ShimmerGrad", Rotation = 25, Offset = Vector2.new(-1, 0),
		Color = ColorSequence.new({ColorSequenceKeypoint.new(0, c1), ColorSequenceKeypoint.new(0.5, c2), ColorSequenceKeypoint.new(1, c1)}),
	}, obj)
	local tween = tw(grad, speed or 1.3, {Offset = Vector2.new(1, 0)}, Enum.EasingStyle.Linear, Enum.EasingDirection.In, true)
	shimmers[obj] = {grad, tween}
end
local function stopShimmer(obj)
	local d = shimmers[obj]
	if d then
		pcall(function() d[2]:Cancel() end)
		pcall(function() d[1]:Destroy() end)
		shimmers[obj] = nil
	end
end

local btnState = setmetatable({}, {__mode = "k"})
local function bindHover(b)
	b.MouseEnter:Connect(function()
		local st = btnState[b]; if not st then return end
		st.hover = true
		tw(b, 0.12, {BackgroundColor3 = st.bg:Lerp(WHITE, 0.1)})
	end)
	b.MouseLeave:Connect(function()
		local st = btnState[b]; if not st then return end
		st.hover = false
		tw(b, 0.15, {BackgroundColor3 = st.bg})
	end)
	b.MouseButton1Down:Connect(function()
		local st = btnState[b]; if not st then return end
		tw(b, 0.08, {BackgroundColor3 = st.bg:Lerp(Color3.new(0, 0, 0), 0.15)})
	end)
	b.MouseButton1Up:Connect(function()
		local st = btnState[b]; if not st then return end
		tw(b, 0.12, {BackgroundColor3 = st.hover and st.bg:Lerp(WHITE, 0.1) or st.bg})
	end)
end
local function mkBtn(parent, props, bg, bgT, txt)
	props = props or {}
	props.AutoButtonColor = false
	props.BorderSizePixel = 0
	props.Font = props.Font or Enum.Font.GothamBold
	props.TextSize = props.TextSize or s(15)
	props.BackgroundColor3 = bg
	props.BackgroundTransparency = bgT or 0
	props.TextColor3 = txt or C.text
	local b = new("TextButton", props, parent)
	btnState[b] = {bg = bg, hover = false}
	bindHover(b)
	return b
end
local function setBtn(b, bg, bgT, txt)
	local st = btnState[b]; if not st then return end
	st.bg = bg
	tw(b, 0.15, {
		BackgroundColor3 = st.hover and bg:Lerp(WHITE, 0.1) or bg,
		BackgroundTransparency = bgT or 0,
		TextColor3 = txt or b.TextColor3,
	})
end

local function commas(n)
	local str = tostring(math.floor(tonumber(n) or 0))
	local out = str:reverse():gsub("(%d%d%d)", "%1,"):reverse()
	if out:sub(1, 1) == "," then out = out:sub(2) end
	return out
end

local function enumItem(enumName, itemName)
	local ok, v = pcall(function() return Enum[enumName][itemName] end)
	if ok and typeof(v) == "EnumItem" then return v end
	return nil
end
local function setParam(p, name, value) return (pcall(function() p[name] = value end)) end

local m = {
	["runanimation"] = "run", ["climbanimation"] = "climb", ["jumpanimation"] = "jump", ["fallanimation"] = "fall",
	["idleanimation"] = "idle", ["swimidleanimation"] = "swimidle", ["swimanimation"] = "swim", ["walkanimation"] = "walk",
}
local reverseM = {
	["run"] = "runanimation", ["climb"] = "climbanimation", ["jump"] = "jumpanimation", ["fall"] = "fallanimation",
	["idle"] = "idleanimation", ["swimidle"] = "swimidleanimation", ["swim"] = "swimanimation", ["walk"] = "walkanimation",
}
local shortNames = {
	["idleanimation"] = "Idle", ["walkanimation"] = "Walk", ["runanimation"] = "Run", ["jumpanimation"] = "Jump",
	["fallanimation"] = "Fall", ["climbanimation"] = "Climb", ["swimidleanimation"] = "S.Idle", ["swimanimation"] = "Swim",
}
local buttonOrder = {"idleanimation", "walkanimation", "runanimation", "jumpanimation", "fallanimation", "climbanimation", "swimidleanimation", "swimanimation"}
local CARD_ORDER = {"idleanimation", "walkanimation", "runanimation", "jumpanimation", "fallanimation", "climbanimation", "swimanimation"}
local DESC_FOLDERS = {
	IdleAnimation = {"idle"}, WalkAnimation = {"walk"}, RunAnimation = {"run"}, JumpAnimation = {"jump"},
	FallAnimation = {"fall"}, ClimbAnimation = {"climb"}, SwimAnimation = {"swim", "swimidle"},
}
local ASSET_TYPE_IDS = {
	[48] = "climbanimation", [50] = "fallanimation", [51] = "idleanimation", [52] = "jumpanimation",
	[53] = "runanimation", [54] = "swimanimation", [55] = "walkanimation",
}

local CACHE_FILE_NAME = "animation_bundle_data_cache.json"
local ANIM_CACHE_URL = "https://raw.githubusercontent.com/TribalFootball/TuffTeto/main/animation_bundle_data_cache.json"
local EQUIPPED_FILE = "FeUgcAnim.json"
local SAVED_BUNDLES_FILE = "FeUgcAnim_Bookmarks.json"
local HISTORY_FILE = "FeUgcAnim_History.json"
local HISTORY_MAX = 30
local PRESETS_FILE = "FeUgcAnim_Presets.json"
local SETTINGS_FILE = "FeUgcAnim_Settings.json"
local LAST_PACK_FILE = "FeUgcAnim_LastPack.json"

local function loadJSON(file)
	local ok, content = pcall(function()
		if isfile and not isfile(file) then return nil end
		return readfile(file)
	end)
	if ok and type(content) == "string" and content ~= "" then
		local suc, decoded = pcall(function() return hs:JSONDecode(content) end)
		if suc and type(decoded) == "table" then return decoded end
	end
	return {}
end
local function saveJSON(file, data)
	pcall(function() writefile(file, hs:JSONEncode(data)) end)
end
local queued = {}
local function queueSave(file, data)
	if queued[file] then return end
	queued[file] = true
	task.delay(1.5, function()
		queued[file] = nil
		saveJSON(file, data)
	end)
end

local assetCache, inflight, detailsCache, metaCache = {}, {}, {}, {}
local fileCache = loadJSON(CACHE_FILE_NAME)
local equippedAnims = loadJSON(EQUIPPED_FILE)
local savedBookmarks = loadJSON(SAVED_BUNDLES_FILE)
local historyList = loadJSON(HISTORY_FILE)
local presetList = loadJSON(PRESETS_FILE)
local scriptSettings = loadJSON(SETTINGS_FILE)
local lastPackData = loadJSON(LAST_PACK_FILE)

if next(presetList) ~= nil and presetList[1] == nil then presetList = {} end
if next(historyList) ~= nil and historyList[1] == nil then historyList = {} end

if not scriptSettings.globalSpeed then scriptSettings.globalSpeed = 1 end
if not scriptSettings.customSpeeds then scriptSettings.customSpeeds = {} end
if scriptSettings.advanceSpeed == nil then scriptSettings.advanceSpeed = false end
if scriptSettings.restoreLastPack == nil then scriptSettings.restoreLastPack = false end
if scriptSettings.forceAnimation == nil then scriptSettings.forceAnimation = false end

local function saveSettings() saveJSON(SETTINGS_FILE, scriptSettings) end

task.spawn(function()
	local ok, body = pcall(function() return game:HttpGet(ANIM_CACHE_URL) end)
	if ok and type(body) == "string" then
		local ok2, data = pcall(function() return hs:JSONDecode(body) end)
		if ok2 and type(data) == "table" then
			for k, v in pairs(data) do if not fileCache[k] then fileCache[k] = v end end
			queueSave(CACHE_FILE_NAME, fileCache)
		end
	end
end)

local function normType(t) return (string.lower(tostring(t or "")):gsub("[%s_%-]", "")) end

local function classifyItem(item)
	local n = tonumber(item.AssetType)
	if n and ASSET_TYPE_IDS[n] then return ASSET_TYPE_IDS[n] end
	local lt = normType(item.AssetType)
	if lt == "swimanimation" or shortNames[lt] then return lt end
	local nm = string.lower(tostring(item.Name or ""))
	if nm:find("swim") then return "swimanimation" end
	if nm:find("idle") then return "idleanimation" end
	if nm:find("walk") then return "walkanimation" end
	if nm:find("run") then return "runanimation" end
	if nm:find("jump") then return "jumpanimation" end
	if nm:find("fall") then return "fallanimation" end
	if nm:find("climb") then return "climbanimation" end
	return nil
end

local function mapBundleItems(items)
	local map = {}
	for _, item in ipairs(items or {}) do
		local slot = classifyItem(item)
		if slot then
			local payload = {Id = item.Id, Name = item.Name, AssetType = slot}
			if slot == "swimanimation" then
				map.swimanimation = payload
				map.swimidleanimation = payload
			else
				map[slot] = payload
			end
		end
	end
	return map
end

local function slotToDescProp(assetType)
	local lt = normType(assetType)
	if lt:find("idle") and not lt:find("swim") then return "IdleAnimation"
	elseif lt:find("walk") then return "WalkAnimation"
	elseif lt:find("run") then return "RunAnimation"
	elseif lt:find("jump") then return "JumpAnimation"
	elseif lt:find("fall") then return "FallAnimation"
	elseif lt:find("climb") then return "ClimbAnimation"
	elseif lt:find("swim") then return "SwimAnimation"
	end
	return "IdleAnimation"
end

local function fetchAnimations(id, bundleId, assetType)
	local t, ser = {}, {}
	local descProp = slotToDescProp(assetType)
	pcall(function()
		local desc = Instance.new("HumanoidDescription")
		desc[descProp] = tonumber(id)
		local dummy = plrs:CreateHumanoidModelFromDescription(desc, Enum.HumanoidRigType.R15)
		local animate = dummy:FindFirstChild("Animate")
		if animate then
			for _, folderName in ipairs(DESC_FOLDERS[descProp] or {}) do
				local folder = animate:FindFirstChild(folderName)
				if folder then
					for _, child in ipairs(folder:GetChildren()) do
						if child:IsA("Animation") and child.AnimationId ~= "" then
							local clone = child:Clone()
							t[#t + 1] = clone
							ser[#ser + 1] = {Name = clone.Name, AnimationId = clone.AnimationId, BundleId = tostring(bundleId)}
						end
					end
				end
			end
		end
		dummy:Destroy()
	end)
	if #t == 0 then
		pcall(function()
			local objs = game:GetObjects("rbxassetid://" .. tostring(id))
			if objs and #objs > 0 then
				local function processItem(item)
					if item:IsA("Animation") then
						t[#t + 1] = item:Clone()
						ser[#ser + 1] = {Name = item.Name, AnimationId = item.AnimationId, BundleId = tostring(bundleId)}
					end
				end
				processItem(objs[1])
				for _, c in ipairs(objs[1]:GetDescendants()) do processItem(c) end
			end
		end)
	end
	return t, ser
end

local function get(id, bundleId, assetType)
	local sid = tostring(id)
	if assetCache[sid] then return assetCache[sid] end
	local cached = fileCache[sid]
	if type(cached) == "table" and #cached > 0 then
		local rebuilt = {}
		for _, d in ipairs(cached) do
			if d.AnimationId and d.AnimationId ~= "" then
				local a = Instance.new("Animation")
				a.Name = d.Name or "Anim"
				a.AnimationId = d.AnimationId
				rebuilt[#rebuilt + 1] = a
			end
		end
		if #rebuilt > 0 then assetCache[sid] = rebuilt; return rebuilt end
	end
	if inflight[sid] then
		local t0 = os.clock()
		while inflight[sid] and os.clock() - t0 < 20 do task.wait(0.05) end
		return assetCache[sid] or {}
	end
	inflight[sid] = true
	local ok, t, ser = pcall(fetchAnimations, id, bundleId, assetType)
	inflight[sid] = nil
	if not ok or type(t) ~= "table" then return {} end
	if #t > 0 then
		assetCache[sid] = t
		fileCache[sid] = ser
		queueSave(CACHE_FILE_NAME, fileCache)
	end
	return t
end

local function clearAssetCache(id)
	local sid = tostring(id)
	assetCache[sid], fileCache[sid] = nil, nil
	queueSave(CACHE_FILE_NAME, fileCache)
end

local function getSpecificTrack(fetchedTracks, targetType)
	if #fetchedTracks == 0 then return nil end
	if targetType == "swimidleanimation" then
		for _, tr in ipairs(fetchedTracks) do
			if string.lower(tr.Name):find("idle") then return tr end
		end
		return fetchedTracks[2] or fetchedTracks[1]
	elseif targetType == "swimanimation" then
		for _, tr in ipairs(fetchedTracks) do
			if string.lower(tr.Name) == "swim" then return tr end
		end
	end
	return fetchedTracks[1]
end

local function getTracksFor(payload, slot)
	if payload.Direct then
		if not payload.Tracks then
			local out = {}
			for _, d in ipairs(payload.Direct) do
				if d.AnimationId and d.AnimationId ~= "" then
					local a = Instance.new("Animation")
					a.Name = d.Name or "Anim"
					a.AnimationId = d.AnimationId
					out[#out + 1] = a
				end
			end
			payload.Tracks = out
		end
		return payload.Tracks
	end
	local list = get(payload.Id, nil, payload.AssetType)
	if #list == 0 then return {} end
	if slot == "idleanimation" then return list end
	local spec = getSpecificTrack(list, slot)
	return spec and {spec} or {}
end

local function preloadAnimations(list)
	if #list > 0 then pcall(function() cp:PreloadAsync(list) end) end
end

local function findStoredEntry(key)
	local e = savedBookmarks[key]
	if type(e) == "table" and e.Items then return e end
	for _, h in ipairs(historyList) do
		if tostring(h.Id) == key and h.Items then return h end
	end
	return nil
end

local function getDetails(id)
	local key = tostring(id)
	if detailsCache[key] then return detailsCache[key] end
	local stored = findStoredEntry(key)
	if stored then
		local res = {Items = stored.Items}
		detailsCache[key] = res
		return res
	end
	local ok, res = pcall(function() return as:GetBundleDetailsAsync(id) end)
	if ok and type(res) == "table" and res.Items then
		detailsCache[key] = res
		return res
	end
	return nil
end

local function normalizeAnimationId(value)
	local raw = tostring(value or "")
	local digits = raw:match("(%d+)")
	return digits or raw
end

local function getTargetSpeed(slot)
	if scriptSettings.advanceSpeed and slot then
		return tonumber(scriptSettings.customSpeeds[slot]) or 1
	end
	return tonumber(scriptSettings.globalSpeed) or 1
end

local function applySpeed(track, slot)
	if not track then return end
	local speed = getTargetSpeed(slot)
	local currentSpeed
	pcall(function() currentSpeed = track.Speed end)
	if type(currentSpeed) ~= "number" or math.abs(currentSpeed - speed) > 0.001 then
		pcall(function() track:AdjustSpeed(speed) end)
	end
end

local function getTrackSlot(tr)
	local animation = tr and tr.Animation
	if not animation then return nil end
	local character = lp.Character
	local animate = character and character:FindFirstChild("Animate")
	local node = animation
	while node and node ~= animate do
		local slot = reverseM[string.lower(node.Name)]
		if slot then return slot end
		node = node.Parent
	end
	local targetId = normalizeAnimationId(animation.AnimationId)
	if animate and targetId ~= "" then
		for folderName, slot in pairs(reverseM) do
			local folder = animate:FindFirstChild(folderName)
			if folder then
				for _, item in ipairs(folder:GetDescendants()) do
					if item:IsA("Animation") and normalizeAnimationId(item.AnimationId) == targetId then
						return slot
					end
				end
			end
		end
	end
	for sName, list in pairs(equippedAnims) do
		for _, anim in ipairs(list) do
			if anim.Name == animation.Name or normalizeAnimationId(anim.AnimationId) == targetId then
				return sName
			end
		end
	end
	return nil
end

local function applyTrackSpeed(tr)
	local animation = tr and tr.Animation
	if not animation then return end
	local pName = animation.Parent and string.lower(animation.Parent.Name) or ""
	local animationName = string.lower(animation.Name or "")
	local isTool = pName == "tool" or animationName:find("tool", 1, true) ~= nil
	if scriptSettings.forceAnimation and isTool then
		tr:Stop(0)
		return
	end
	applySpeed(tr, getTrackSlot(tr))
end

local function applyCurrentCharacterSpeeds()
	local character = lp.Character
	local human = character and character:FindFirstChildOfClass("Humanoid")
	local animator = human and human:FindFirstChildOfClass("Animator")
	if not animator then return end
	for _, tr in ipairs(animator:GetPlayingAnimationTracks()) do
		applyTrackSpeed(tr)
	end
end

local hookedAnimators = setmetatable({}, {__mode = "k"})
local function hookAnimator(character)
	if not character then return end
	task.spawn(function()
		local human = character:FindFirstChildOfClass("Humanoid") or character:WaitForChild("Humanoid", 10)
		if not human or character ~= lp.Character then return end
		local animator = human:FindFirstChildOfClass("Animator") or human:WaitForChild("Animator", 5)
		if not animator then
			animator = Instance.new("Animator")
			animator.Parent = human
		end
		if hookedAnimators[animator] then return end
		hookedAnimators[animator] = true
		track(animator.AnimationPlayed:Connect(function(animationTrack)
			task.defer(function()
				if animationTrack and animationTrack.IsPlaying then applyTrackSpeed(animationTrack) end
			end)
		end))
		for _, tr in ipairs(animator:GetPlayingAnimationTracks()) do
			applyTrackSpeed(tr)
		end
	end)
end

track(lp.CharacterAdded:Connect(hookAnimator))
if lp.Character then hookAnimator(lp.Character) end
track(rs.Heartbeat:Connect(applyCurrentCharacterSpeeds))

local function equipMap(character, slotMap, persist)
	if not character then return 0 end
	local animate = character:FindFirstChild("Animate") or character:WaitForChild("Animate", 5)
	local human = character:FindFirstChildOfClass("Humanoid")
	if not animate or not human then return 0 end
	
	if human.RigType == Enum.HumanoidRigType.R6 then
		if toast then toast("R6 detected. Resetting to default.", "bad") end
		pcall(function() human:ApplyDescription(human:GetAppliedDescription()) end)
		return 0
	end

	for _, tr in ipairs(human:GetPlayingAnimationTracks()) do
		pcall(function() tr:AdjustWeight(0, 0); tr:Stop(0) end)
	end
	animate.Disabled = true
	local count = 0
	for slot, anims in pairs(slotMap) do
		local folderName = m[slot]
		local folder = folderName and animate:FindFirstChild(folderName)
		if folder and #anims > 0 then
			for _, o in ipairs(folder:GetChildren()) do
				if o:IsA("Animation") then o:Destroy() end
			end
			local saved = {}
			for _, a in ipairs(anims) do
				local na = Instance.new("Animation")
				na.Name = a.Name
				na.AnimationId = a.AnimationId
				na.Parent = folder
				saved[#saved + 1] = {Name = a.Name, AnimationId = a.AnimationId}
			end
			if persist then equippedAnims[slot] = saved end
			count = count + 1
		end
	end
	if persist then saveJSON(EQUIPPED_FILE, equippedAnims) end
	task.wait(0.05)
	animate.Disabled = false
	return count
end

local function applyBundleItemsToCharacter(items)
	local char = lp.Character
	if not char then return 0 end
	local slotMap, toLoad = {}, {}
	for _, slot in ipairs(buttonOrder) do
		local payload = items[slot]
		if payload then
			local list = getTracksFor(payload, slot)
			if #list > 0 then
				slotMap[slot] = list
				for _, a in ipairs(list) do toLoad[#toLoad + 1] = a end
			end
		end
	end
	preloadAnimations(toLoad)
	return equipMap(char, slotMap, true)
end

local capturing = {}
local function captureBundleData(id)
	local key = tostring(id)
	if capturing[key] then return end
	capturing[key] = true
	task.spawn(function()
		local ok = pcall(function()
			local details = getDetails(id)
			if not (details and details.Items) then return end
			local items = {}
			for _, it in ipairs(details.Items) do
				items[#items + 1] = {Id = it.Id, Name = it.Name, AssetType = it.AssetType, Type = it.Type}
			end
			local anims, seen = {}, {}
			for slot, payload in pairs(mapBundleItems(details.Items)) do
				local pid = tostring(payload.Id)
				if not seen[pid] then
					seen[pid] = true
					get(payload.Id, id, payload.AssetType)
				end
				local list = fileCache[pid]
				if type(list) == "table" and #list > 0 then
					local copy = {}
					for _, a in ipairs(list) do copy[#copy + 1] = {Name = a.Name, AnimationId = a.AnimationId} end
					anims[slot] = {Id = payload.Id, List = copy}
				end
			end
			local function store(e)
				if type(e) == "table" then e.Items, e.Anims = items, anims end
			end
			store(savedBookmarks[key])
			for _, h in ipairs(historyList) do
				if tostring(h.Id) == key then store(h) end
			end
			saveJSON(SAVED_BUNDLES_FILE, savedBookmarks)
			saveJSON(HISTORY_FILE, historyList)
		end)
		capturing[key] = nil
	end)
end

local function recordHistory(id, name)
	if not id then return end
	local key = tostring(id)
	local oldItems, oldAnims
	for i = #historyList, 1, -1 do
		if tostring(historyList[i].Id) == key then
			oldItems, oldAnims = historyList[i].Items, historyList[i].Anims
			table.remove(historyList, i)
		end
	end
	local sv = savedBookmarks[key]
	if not oldItems and type(sv) == "table" then oldItems, oldAnims = sv.Items, sv.Anims end
	local mt = metaCache[key] or {}
	table.insert(historyList, 1, {
		Id = id, Name = name or "Unknown", Time = os.time(), Kind = mt.kind, Amount = mt.amount, Creator = mt.creator,
		Items = oldItems, Anims = oldAnims,
	})
	while #historyList > HISTORY_MAX do table.remove(historyList) end
	saveJSON(HISTORY_FILE, historyList)
	if not oldAnims then captureBundleData(id) end
	if onHistoryChanged then onHistoryChanged() end
end

local function wearBundle(bundleId, bundleName, onDone)
	task.spawn(function()
		local details = getDetails(bundleId)
		if details and lp.Character then
			local n = applyBundleItemsToCharacter(mapBundleItems(details.Items))
			if n > 0 then 
				recordHistory(bundleId, bundleName) 
				lastPackData = {Id = bundleId, Name = bundleName}
				saveJSON(LAST_PACK_FILE, lastPackData)
			end
			if toast then toast(n > 0 and ("Equipped " .. n .. " animations") or "Nothing could be equipped", n > 0 and "ok" or "bad") end
		elseif toast then
			toast("Couldn't load that bundle", "bad")
		end
		if onDone then onDone() end
	end)
end

local function initAutoEquip(char)
	if not char then return end
	task.spawn(function()
		local human = char:WaitForChild("Humanoid", 10)
		local animate = char:WaitForChild("Animate", 10)
		if not human or not animate then return end
		if human.RigType == Enum.HumanoidRigType.R6 then
			if toast then toast("R6 detected. Custom animations are not supported.", "bad") end
			return
		end
		local animator = human:WaitForChild("Animator", 5)
		if not animator then
			animator = Instance.new("Animator")
			animator.Parent = human
		end
		local idleFolder = animate:FindFirstChild("idle")
		if idleFolder then
			local idleAnimation = idleFolder:FindFirstChild("Animation1") or idleFolder:FindFirstChildWhichIsA("Animation")
			if not idleAnimation then
				idleFolder:WaitForChild("Animation1", 5)
			end
		end
		task.wait(0.75)
		if char ~= lp.Character or human.Health <= 0 then return end
		if next(equippedAnims) ~= nil then
			equipMap(char, equippedAnims, false)
		end
	end)
end
if lp.Character then initAutoEquip(lp.Character) end
track(lp.CharacterAdded:Connect(initAutoEquip))

local badgeRefs, fetchingMeta = {}, {}

local function priceFromInfo(info)
	local price = tonumber(info.PriceInRobux)
	if price == 0 then return "free", 0 end
	if price then return "paid", price end
	if info.IsForSale == false then return "off", nil end
	if info.IsForSale == true and info.IsLimited ~= true and info.IsLimitedUnique ~= true then return "free", 0 end
	return nil, nil
end

local function seedMeta(entry)
	if type(entry) ~= "table" or not entry.Id then return nil end
	local key = tostring(entry.Id)
	local mt = metaCache[key] or {}
	if mt.kind == nil then
		local rawStatus = entry.PriceStatus or entry.ItemStatus or ""
		local status = string.lower(tostring(rawStatus))
		local price = tonumber(entry.Price)
		if price == nil then price = tonumber(entry.LowestPrice) end
		if status:find("free") or price == 0 then
			mt.kind, mt.amount = "free", 0
		elseif status:find("off") or status:find("reseller") then
			mt.kind = "off"
		elseif price then
			mt.kind, mt.amount = "paid", price
		end
	end
	if entry.CreatorName and not mt.creator then mt.creator = tostring(entry.CreatorName) end
	metaCache[key] = mt
	return mt
end

local function ensureMeta(id, needCreator)
	local key = tostring(id)
	local mt = metaCache[key]
	if mt and (mt.fetched or (mt.kind and (not needCreator or mt.creator))) then return end
	if fetchingMeta[key] then return end
	fetchingMeta[key] = true
	task.spawn(function()
		local ok, info = pcall(function() return mps:GetProductInfo(tonumber(id), Enum.InfoType.Bundle) end)
		fetchingMeta[key] = nil
		local mm = metaCache[key] or {}
		mm.fetched = true
		if ok and type(info) == "table" then
			if mm.kind == nil or mm.kind == "unknown" then
				local k, a = priceFromInfo(info)
				mm.kind, mm.amount = k or "unknown", a
			end
			if not mm.creator and type(info.Creator) == "table" and info.Creator.Name then
				mm.creator = tostring(info.Creator.Name)
			end
		elseif mm.kind == nil then
			mm.kind = "unknown"
		end
		metaCache[key] = mm
		if onMetaChanged then onMetaChanged(key) end
	end)
end

local function applyBadge(label, meta)
	local kind = meta and meta.kind
	local text, bg, fg
	if kind == "paid" then text, bg, fg = "R$ " .. commas(meta.amount), C.green, Color3.fromRGB(20, 20, 22)
	elseif kind == "free" then text, bg, fg = "FREE", C.accent2, Color3.fromRGB(20, 20, 22)
	elseif kind == "off" then text, bg, fg = "OFFSALE", C.surfaceHi, C.textDim
	elseif kind == "unknown" then text, bg, fg = "N/A", C.surfaceHi, C.textDim
	else text, bg, fg = "…", C.surfaceHi, C.textMuted end
	label.Text = text
	tw(label, 0.2, {BackgroundColor3 = bg, TextColor3 = fg})
end

local function bindBadge(label, id)
	local key = tostring(id)
	badgeRefs[key] = badgeRefs[key] or {}
	table.insert(badgeRefs[key], label)
	applyBadge(label, metaCache[key])
end

local function mkBadge(parent, props)
	props = props or {}
	props.BackgroundColor3 = C.surfaceHi
	props.AutomaticSize = Enum.AutomaticSize.X
	props.Size = UDim2.fromOffset(0, s(22))
	props.Font = Enum.Font.GothamBlack
	props.TextSize = s(13)
	props.TextColor3 = C.textMuted
	props.Text = "…"
	props.BorderSizePixel = 0
	local b = new("TextLabel", props, parent)
	round(b); pad(b, s(10), s(10))
	return b
end

local KEY_ALIASES = {
	cat = "category", cats = "category",
	min = "price_min", max = "price_max",
	pricemin = "price_min", pricemax = "price_max",
	minprice = "price_min", maxprice = "price_max",
}
local VALID_KEYS = {sort = true, price_min = true, price_max = true, price = true, creator = true, sale = true, category = true, sales = true, period = true, limit = true, is = true}
local SORT_ALIASES = {
	mostfavorited = "MostFavorited", pricelowtohigh = "PriceLowToHigh", pricehightolow = "PriceHighToLow", recentlycreated = "RecentlyCreated",
	rel = "Relevance", relevance = "Relevance", relevant = "Relevance",
	new = "RecentlyCreated", newest = "RecentlyCreated", recent = "RecentlyCreated", latest = "RecentlyCreated",
	fav = "MostFavorited", favorite = "MostFavorited", favorited = "MostFavorited", popular = "MostFavorited", top = "MostFavorited",
	best = "Bestselling", bestselling = "Bestselling", sales = "Bestselling", seller = "Bestselling",
	low = "PriceLowToHigh", asc = "PriceLowToHigh", cheap = "PriceLowToHigh", cheapest = "PriceLowToHigh",
	high = "PriceHighToLow", desc = "PriceHighToLow", expensive = "PriceHighToLow",
}
local SAVED_SORT = {
	new = "new", newest = "new", recent = "new", latest = "new", old = "old", oldest = "old",
	name = "name", az = "name", alpha = "name", recentlycreated = "new", pricelowtohigh = "low", pricehightolow = "high", mostfavorited = "fav",
	low = "low", asc = "low", cheap = "low", cheapest = "low", high = "high", desc = "high", expensive = "high",
	fav = "fav", favorite = "fav", favorites = "fav",
}
local CAT_ALIASES = {
	none = "None", all = "None", featured = "Featured", collectibles = "Collectibles", collectible = "Collectibles",
	community = "CommunityCreations", communitycreations = "CommunityCreations", ugc = "CommunityCreations", premium = "Premium", recommended = "Recommended",
}
local SALES_ALIASES = {all = "All", collectibles = "Collectibles", collectible = "Collectibles", premium = "Premium"}
local PERIOD_ALIASES = {
	["12h"] = "Past12Hours", day = "PastDay", ["1d"] = "PastDay", ["24h"] = "PastDay", ["3d"] = "Past3Days",
	week = "PastWeek", ["7d"] = "PastWeek", month = "PastMonth", ["30d"] = "PastMonth", all = "AllTime", alltime = "AllTime",
}
local LIMITS = {10, 28, 30, 60, 120}

local function parseQuery(text)
	text = text or ""
	local tags, words = {}, {}
	local function addTag(k, v)
		k = string.lower(k)
		k = KEY_ALIASES[k] or k
		if not VALID_KEYS[k] then return false end
		for i = #tags, 1, -1 do
			if tags[i].key == k then table.remove(tags, i) end
		end
		tags[#tags + 1] = {key = k, val = v}
		return true
	end
	local rest = text:gsub('([%a_]+):"([^"]*)"', function(k, v)
		if addTag(k, v) then return " " end
		return nil
	end)
	for word in rest:gmatch("%S+") do
		local k, v = word:match("^([%a_]+):(.+)$")
		if k and addTag(k, v) then
		else
			words[#words + 1] = word
		end
	end
	return {keyword = table.concat(words, " "), tags = tags}
end

local function buildQuery(q)
	local parts = {}
	if q.keyword ~= "" then parts[#parts + 1] = q.keyword end
	for _, tg in ipairs(q.tags) do
		local v = tg.val
		if v:find("%s") then v = '"' .. v .. '"' end
		parts[#parts + 1] = tg.key .. ":" .. v
	end
	return table.concat(parts, " ")
end

local function interpretTag(tab, key, val)
	key = KEY_ALIASES[key] or key
	local v = (string.lower(val):gsub("_", ""))
	local saved = (tab ~= "Discover")
	if tab == "Presets" then
		if key == "sort" then
			local x = ({new = "new", newest = "new", recent = "new", old = "old", oldest = "old", name = "name", az = "name", alpha = "name"})[v]
			return x ~= nil, x
		end
		return false
	end
	if key == "sort" then
		local x = (saved and SAVED_SORT or SORT_ALIASES)[v]
		return x ~= nil, x
	elseif key == "price_min" or key == "price_max" then
		local n = tonumber(v)
		if n and n >= 0 then return true, math.floor(n) end
		return false
	elseif key == "price" then
		if v == "free" then return true, {min = 0, max = 0} end
		local a, b = v:match("^(%d+)%-(%d+)$")
		if a then
			a, b = tonumber(a), tonumber(b)
			if a > b then a, b = b, a end
			return true, {min = a, max = b}
		end
		local n = v:match("^(%d+)$")
		if n then n = tonumber(n); return true, {min = n, max = n} end
		return false
	elseif key == "creator" then
		return val ~= "", val
	elseif key == "is" then
		if tab == "Saved" and (v == "fav" or v == "favorite" or v == "favorites" or v == "starred") then return true, "fav" end
		if saved and (v == "free" or v == "paid" or v == "offsale" or v == "off") then return true, (v == "off" and "offsale" or v) end
		return false
	elseif saved then
		return false
	elseif key == "sale" then
		if v == "on" or v == "only" then return true, "on" end
		if v == "all" or v == "any" then return true, "all" end
		return false
	elseif key == "category" then
		local x = CAT_ALIASES[v]; return x ~= nil, x
	elseif key == "sales" then
		local x = SALES_ALIASES[v]; return x ~= nil, x
	elseif key == "period" then
		local x = PERIOD_ALIASES[v]; return x ~= nil, x
	elseif key == "limit" then
		local n = tonumber(v)
		if not n then return false end
		local best = LIMITS[1]
		for _, l in ipairs(LIMITS) do
			if math.abs(l - n) < math.abs(best - n) then best = l end
		end
		return true, best
	end
	return false
end

local function compileQuery(tab, q)
	local spec = {keyword = q.keyword, issues = {}}
	for _, tg in ipairs(q.tags) do
		local ok, v = interpretTag(tab, tg.key, tg.val)
		if not ok then
			spec.issues[#spec.issues + 1] = tg.key .. ":" .. tg.val
		elseif tg.key == "sort" then spec.sort = v
		elseif tg.key == "price_min" then spec.min = v
		elseif tg.key == "price_max" then spec.max = v
		elseif tg.key == "price" then spec.min, spec.max = v.min, v.max
		elseif tg.key == "creator" then spec.creator = v
		elseif tg.key == "sale" then spec.saleOnly = (v == "on")
		elseif tg.key == "category" then spec.cat = v
		elseif tg.key == "sales" then spec.sales = v
		elseif tg.key == "period" then spec.period = v
		elseif tg.key == "limit" then spec.limit = v
		elseif tg.key == "is" then spec.is = v
		end
	end
	return spec
end

local function buildParams(spec)
	local p = CatalogSearchParams.new()
	setParam(p, "SearchKeyword", spec.keyword or "")
	setParam(p, "BundleTypes", {Enum.BundleType.Animations})
	local priceFiltered = spec.min ~= nil or spec.max ~= nil
	setParam(p, "IncludeOffSale", (not spec.saleOnly) and not priceFiltered)
	setParam(p, "Limit", spec.limit or 120)
	local freeOnly = spec.max ~= nil and spec.max == 0
	local sortName = spec.sort or (freeOnly and "PriceLowToHigh") or (((spec.keyword or "") == "") and "RecentlyCreated" or "Relevance")
	local e = enumItem("CatalogSortType", sortName)
	if e then setParam(p, "SortType", e) end
	if spec.min and spec.min > 0 then setParam(p, "MinPrice", spec.min) end
	if spec.max and spec.max > 0 then setParam(p, "MaxPrice", spec.max) end
	if spec.creator then setParam(p, "CreatorName", spec.creator) end
	if spec.cat then
		local ce = enumItem("CatalogCategoryFilter", spec.cat)
		if ce then setParam(p, "CategoryFilter", ce) end
	end
	local se = enumItem("SalesTypeFilter", spec.sales or "All")
	if se then setParam(p, "SalesTypeFilter", se) end
	if spec.period then
		local pe = enumItem("CatalogSortAggregation", spec.period)
		if pe then setParam(p, "SortAggregation", pe) end
	end
	return p
end

local priceTried = {}
local function resolveUnknownPrices(items)
	local queue = {}
	for _, entry in ipairs(items) do
		if type(entry) == "table" and entry.Id then
			local key = tostring(entry.Id)
			local mt = metaCache[key]
			if not mt and entry.Kind then
				mt = {kind = entry.Kind, amount = entry.Amount, creator = entry.Creator}
				metaCache[key] = mt
			end
			if not (mt and mt.kind) or mt.kind == "unknown" then mt = seedMeta(entry) end
			if (not (mt and mt.kind) or mt.kind == "unknown") and not priceTried[key] then
				queue[#queue + 1] = key
			end
		end
	end
	if #queue == 0 then return end
	local nextI, running = 1, 0
	local function worker()
		while true do
			local key = queue[nextI]
			if not key then break end
			nextI += 1
			local info
			for attempt = 1, 2 do
				local ok, res = pcall(function() return mps:GetProductInfo(tonumber(key), Enum.InfoType.Bundle) end)
				if ok and type(res) == "table" then info = res; break end
				task.wait(0.4)
			end
			if info then
				priceTried[key] = true
				local mm = metaCache[key] or {}
				local k, a = priceFromInfo(info)
				mm.kind, mm.amount = k or "unknown", a
				if not mm.creator and type(info.Creator) == "table" and info.Creator.Name then
					mm.creator = tostring(info.Creator.Name)
				end
				metaCache[key] = mm
			end
		end
		running -= 1
	end
	for _ = 1, math.min(12, #queue) do
		running += 1
		task.spawn(worker)
	end
	local t0 = os.clock()
	while running > 0 and os.clock() - t0 < 15 do task.wait(0.05) end
end

local function passesClientFilter(entry, spec)
	if type(entry) ~= "table" or not entry.Id then return false end
	local mt = seedMeta(entry)
	local wantPrice = spec and (spec.min ~= nil or spec.max ~= nil)
	if wantPrice then
		local kind = mt and mt.kind
		local amt
		if kind == "free" then amt = 0
		elseif kind == "paid" then amt = mt.amount end
		if amt == nil then return false end
		if spec.min and amt < spec.min then return false end
		if spec.max and amt > spec.max then return false end
	end
	return true
end

local function savedAmount(v)
	local mt = metaCache[tostring(v.Id)]
	local kind = (mt and mt.kind) or v.Kind
	local amount = (mt and mt.amount) or v.Amount
	if kind == "free" then return 0 end
	if kind == "paid" then return amount end
	return nil
end

local function extraFilter(v, spec)
	if spec.is == "fav" and not v.Fav then return false end
	if spec.is == "free" or spec.is == "paid" or spec.is == "offsale" then
		local mt = metaCache[tostring(v.Id)]
		local kind = (mt and mt.kind) or v.Kind
		local want = spec.is == "offsale" and "off" or spec.is
		if kind ~= want then return false end
	end
	return true
end

local function buildSavedList(spec)
	local list = {}
	local kw = string.lower(spec.keyword or "")
	local cr = spec.creator and string.lower(spec.creator) or nil
	for key, v in pairs(savedBookmarks) do
		if type(v) == "table" and v.Id then
			if not metaCache[key] and v.Kind then
				metaCache[key] = {kind = v.Kind, amount = v.Amount, creator = v.Creator}
			end
			local ok = true
			if kw ~= "" and not string.find(string.lower(v.Name or ""), kw, 1, true) then ok = false end
			if ok and cr then
				local mt = metaCache[key]
				local c = string.lower((mt and mt.creator) or v.Creator or "")
				if not string.find(c, cr, 1, true) then ok = false end
			end
			if ok and not extraFilter(v, spec) then ok = false end
			if ok and (spec.min ~= nil or spec.max ~= nil) then
				local a = savedAmount(v)
				if a == nil then ok = false end
				if ok and spec.min and a < spec.min then ok = false end
				if ok and spec.max and a > spec.max then ok = false end
			end
			if ok then list[#list + 1] = v end
		end
	end
	local function cmpTime(a, b)
		local ta, tb = a.Time or 0, b.Time or 0
		if ta ~= tb then return ta > tb end
		return tostring(a.Id) < tostring(b.Id)
	end
	local mode = spec.sort or "fav"
	table.sort(list, function(a, b)
		if mode == "new" then return cmpTime(a, b)
		elseif mode == "old" then
			local ta, tb = a.Time or 0, b.Time or 0
			if ta ~= tb then return ta < tb end
			return tostring(a.Id) < tostring(b.Id)
		elseif mode == "name" then
			local na, nb = string.lower(a.Name or ""), string.lower(b.Name or "")
			if na ~= nb then return na < nb end
			return cmpTime(a, b)
		elseif mode == "low" or mode == "high" then
			local x, y = savedAmount(a), savedAmount(b)
			if x == y then return cmpTime(a, b) end
			if x == nil then return false end
			if y == nil then return true end
			if mode == "low" then return x < y end
			return x > y
		end
		local fa, fb = a.Fav and true or false, b.Fav and true or false
		if fa ~= fb then return fa end
		return cmpTime(a, b)
	end)
	return list
end

local function buildHistoryList(spec)
	local list = {}
	local kw = string.lower(spec.keyword or "")
	local cr = spec.creator and string.lower(spec.creator) or nil
	for _, v in ipairs(historyList) do
		local key = tostring(v.Id)
		if not metaCache[key] and v.Kind then
			metaCache[key] = {kind = v.Kind, amount = v.Amount, creator = v.Creator}
		end
		local ok = true
		if kw ~= "" and not string.find(string.lower(v.Name or ""), kw, 1, true) then ok = false end
		if ok and cr then
			local mt = metaCache[key]
			local c = string.lower((mt and mt.creator) or v.Creator or "")
			if not string.find(c, cr, 1, true) then ok = false end
		end
		if ok and (spec.min ~= nil or spec.max ~= nil) then
			local a = savedAmount(v)
			if a == nil then ok = false end
			if ok and spec.min and a < spec.min then ok = false end
			if ok and spec.max and a > spec.max then ok = false end
		end
		if ok and not extraFilter(v, spec) then ok = false end
		if ok then list[#list + 1] = v end
	end
	if spec.sort == "old" then
		local rev = {}
		for i = #list, 1, -1 do rev[#rev + 1] = list[i] end
		list = rev
	elseif spec.sort == "name" then
		table.sort(list, function(a, b) return string.lower(a.Name or "") < string.lower(b.Name or "") end)
	elseif spec.sort == "low" or spec.sort == "high" then
		local asc = spec.sort == "low"
		table.sort(list, function(a, b)
			local pa, pb = savedAmount(a), savedAmount(b)
			if pa == nil and pb == nil then return false end
			if pa == nil then return false end
			if pb == nil then return true end
			if pa == pb then return false end
			if asc then return pa < pb else return pa > pb end
		end)
	end
	return list
end

local uiOk, uiErr = pcall(function()

local PAD, TOPH = s(12), s(50)
local W, H = s(W0), s(H0)
local BODY_H = H - TOPH - PAD
local LW, GAP = s(420), s(12)
local RW = W - PAD * 2 - LW - GAP
local CHIP_Y, CHIP_H, TAG_Y, TAG_H, GRID_Y, FOOT_H = s(42), s(28), s(76), s(44), s(126), s(34)
local GRID_H = BODY_H - GRID_Y - FOOT_H - s(4)
local COLS, ROWS = 3, 1
local PER_PAGE = 3

local CPAD = s(8)
local CW = math.floor((LW - CPAD * (COLS + 1)) / COLS)
local GRID_AVAIL_H = GRID_H - CPAD * (ROWS + 1)
local CH = math.min(math.floor(GRID_AVAIL_H / ROWS), s(200))
local AH = s(28)

local playerGui = lp:WaitForChild("PlayerGui", 10) or lp:FindFirstChildOfClass("PlayerGui")
local parentCandidates = {}
if playerGui then parentCandidates[#parentCandidates + 1] = playerGui end
pcall(function()
	if typeof(gethui) == "function" then
		local h = gethui()
		if h then parentCandidates[#parentCandidates + 1] = h end
	end
end)
pcall(function() parentCandidates[#parentCandidates + 1] = game:GetService("CoreGui") end)

pcall(function()
	if playerGui and playerGui:FindFirstChild("StudioAnimStudio") then playerGui.StudioAnimStudio:Destroy() end
	local cg = game:GetService("CoreGui")
	if cg and cg:FindFirstChild("StudioAnimStudio") then cg.StudioAnimStudio:Destroy() end
end)

local g = Instance.new("ScreenGui")
g.Name = "StudioAnimStudio"
g.ResetOnSpawn = false
g.IgnoreGuiInset = true
g.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
g.DisplayOrder = 999

local parented = false
for _, p in ipairs(parentCandidates) do
	local okP = pcall(function() g.Parent = p end)
	if okP and g.Parent == p then parented = true; break end
end
if not parented then
	pcall(function() g.Parent = lp:WaitForChild("PlayerGui") end)
end

local mf = new("Frame", {
	Name = "Main",
	Size = UDim2.fromOffset(W, H),
	AnchorPoint = Vector2.new(0.5, 0.5),
	Position = UDim2.fromScale(0.5, 0.5),
	BackgroundColor3 = C.bg,
	BackgroundTransparency = 0.15,
	BorderSizePixel = 0,
	ClipsDescendants = true,
	Active = true,
	Visible = true,
}, g)
corner(mf, s(16))
stroke(mf, C.stroke, 1, 0.35)

local topLine = new("Frame", {Size = UDim2.new(1, 0, 0, s(2)), BackgroundColor3 = C.accent2, BackgroundTransparency = 0.3, BorderSizePixel = 0, ZIndex = 3}, mf)

local top = new("Frame", {Name = "Top", Size = UDim2.new(1, 0, 0, TOPH), BackgroundTransparency = 1, Active = true}, mf)
local logo = new("Frame", {Size = UDim2.fromOffset(s(30), s(30)), Position = UDim2.new(0, PAD, 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), BackgroundColor3 = C.surfaceHi, BackgroundTransparency = 0.1, BorderSizePixel = 0}, top)
corner(logo, s(9))
new("TextLabel", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Text = "A", Font = Enum.Font.GothamBlack, TextSize = s(18), TextColor3 = C.text}, logo)
new("TextLabel", {Position = UDim2.fromOffset(PAD + s(42), s(8)), Size = UDim2.fromOffset(s(170), s(20)), BackgroundTransparency = 1, Text = "Animation Studio", Font = Enum.Font.GothamBlack, TextSize = s(18), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left}, top)
new("TextLabel", {Position = UDim2.fromOffset(PAD + s(42), s(28)), Size = UDim2.fromOffset(s(170), s(16)), BackgroundTransparency = 1, Text = "FE UGC bundles", Font = Enum.Font.Gotham, TextSize = s(13), TextColor3 = C.textMuted, TextXAlignment = Enum.TextXAlignment.Left}, top)

local seg = new("Frame", {Position = UDim2.fromOffset(PAD + s(212), math.floor((TOPH - s(34)) / 2)), Size = UDim2.fromOffset(s(300), s(34)), BackgroundColor3 = C.surface, BackgroundTransparency = 0.2, BorderSizePixel = 0}, top)
corner(seg, s(10))
local tabIndicator = new("Frame", {Position = UDim2.new(0, s(2), 0, s(2)), Size = UDim2.new(1 / 4, -s(3), 1, -s(4)), BackgroundColor3 = C.surfaceHi, BackgroundTransparency = 0.05, BorderSizePixel = 0, ZIndex = 1}, seg)
corner(tabIndicator, s(8))
local tabDiscover = mkBtn(seg, {Text = "Discover", Size = UDim2.fromScale(1 / 4, 1), ZIndex = 2, TextSize = s(13)}, C.surface, 1, C.text)
local tabSaved = mkBtn(seg, {Text = "Saved", Size = UDim2.fromScale(1 / 4, 1), Position = UDim2.fromScale(1 / 4, 0), ZIndex = 2, TextSize = s(13)}, C.surface, 1, C.textMuted)
local tabRecent = mkBtn(seg, {Text = "Recent", Size = UDim2.fromScale(1 / 4, 1), Position = UDim2.fromScale(2 / 4, 0), ZIndex = 2, TextSize = s(13)}, C.surface, 1, C.textMuted)
local tabPresets = mkBtn(seg, {Text = "Presets", Size = UDim2.fromScale(1 / 4, 1), Position = UDim2.fromScale(3 / 4, 0), ZIndex = 2, TextSize = s(13)}, C.surface, 1, C.textMuted)

local closeBtn = mkBtn(top, {Text = "×", Size = UDim2.fromOffset(s(30), s(30)), Position = UDim2.new(1, -PAD - s(30), 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), TextSize = s(20)}, C.surface, 0.2, C.textDim)
corner(closeBtn, s(8))
local minBtn = mkBtn(top, {Text = "–", Size = UDim2.fromOffset(s(30), s(30)), Position = UDim2.new(1, -PAD - s(66), 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), TextSize = s(18)}, C.surface, 0.2, C.textDim)
corner(minBtn, s(8))

local left = new("Frame", {Position = UDim2.fromOffset(PAD, TOPH), Size = UDim2.fromOffset(LW, BODY_H), BackgroundTransparency = 1}, mf)

local helpW, searchW, gap4 = s(38), s(86), s(6)
local sbW = LW - helpW - searchW - gap4 * 2
local sbBox = new("Frame", {Size = UDim2.fromOffset(sbW, s(38)), BackgroundColor3 = C.surface, BackgroundTransparency = 0.25, BorderSizePixel = 0}, left)
corner(sbBox, s(10))
stroke(sbBox, C.stroke, 1, 0.55)
local sb = new("TextBox", {
	Size = UDim2.new(1, -s(44), 1, 0), Position = UDim2.fromOffset(s(14), 0), BackgroundTransparency = 1, Text = "",
	PlaceholderText = "Search…  try sort:price_high_to_low price_max:500",
	ClearTextOnFocus = false, Font = Enum.Font.Gotham, TextSize = s(15),
	TextColor3 = C.text, PlaceholderColor3 = C.textMuted, TextXAlignment = Enum.TextXAlignment.Left, ClipsDescendants = true,
}, sbBox)
local clearBtn = mkBtn(sbBox, {Text = "×", Size = UDim2.fromOffset(s(26), s(26)), Position = UDim2.new(1, -s(30), 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), TextSize = s(17), Visible = false}, C.surfaceHi, 0.35, C.textDim)
round(clearBtn)
local searchBtn = mkBtn(left, {Text = "Search", Size = UDim2.fromOffset(searchW, s(38)), Position = UDim2.fromOffset(sbW + gap4, 0), TextSize = s(15)}, C.surfaceHi, 0.15, C.text)
corner(searchBtn, s(10))
local helpBtn = mkBtn(left, {Text = "?", Size = UDim2.fromOffset(helpW, s(38)), Position = UDim2.fromOffset(sbW + gap4 * 2 + searchW, 0), TextSize = s(16)}, C.surface, 0.25, C.textDim)
corner(helpBtn, s(10))

local settingsBtn = mkBtn(top, {Text = "⚙", Size = UDim2.fromOffset(s(30), s(30)), Position = UDim2.new(1, -PAD - s(102), 0.5, 0), AnchorPoint = Vector2.new(0, 0.5), TextSize = s(18)}, C.surface, 0.2, C.textDim)
corner(settingsBtn, s(8))

local function mkRow(y, h)
	local row = new("ScrollingFrame", {
		Position = UDim2.fromOffset(0, y), Size = UDim2.fromOffset(LW, h), BackgroundTransparency = 1, BorderSizePixel = 0,
		ScrollBarThickness = 0, CanvasSize = UDim2.new(0, 0, 0, 0), AutomaticCanvasSize = Enum.AutomaticSize.X,
		ScrollingDirection = Enum.ScrollingDirection.X, ZIndex = 2,
	}, left)
	new("UIListLayout", {FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, s(6)), SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Center}, row)
	return row
end
local chipsRow = mkRow(CHIP_Y, CHIP_H)
local tagsRow = mkRow(TAG_Y, TAG_H)

local tagHint = new("TextLabel", {
	Name = "TagHint",
	Position = UDim2.fromOffset(0, TAG_Y),
	Size = UDim2.fromOffset(LW, TAG_H),
	BackgroundTransparency = 1,
	BorderSizePixel = 0,
	Text = "No filters  ·  press  ?  to see all search tags",
	Font = Enum.Font.GothamBold,
	TextScaled = true,
	TextColor3 = C.textDim,
	TextXAlignment = Enum.TextXAlignment.Center,
	TextYAlignment = Enum.TextYAlignment.Center,
	TextWrapped = true,
	ZIndex = 6,
}, left)
new("UITextSizeConstraint", {MaxTextSize = s(28), MinTextSize = s(14)}, tagHint)

local gridHolder = new("Frame", {Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundColor3 = C.panel, BackgroundTransparency = 0.55, BorderSizePixel = 0, ClipsDescendants = true}, left)
corner(gridHolder, s(12)); stroke(gridHolder, C.stroke, 1, 0.7)
new("UIGridLayout", {
	CellSize = UDim2.fromOffset(CW, CH),
	CellPadding = UDim2.fromOffset(CPAD, CPAD),
	SortOrder = Enum.SortOrder.LayoutOrder,
	FillDirectionMaxCells = COLS,
	HorizontalAlignment = Enum.HorizontalAlignment.Center,
	VerticalAlignment = Enum.VerticalAlignment.Center,
}, gridHolder)
local gridMsg = new("TextLabel", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Text = "", Font = Enum.Font.GothamSemibold, TextSize = s(15), TextColor3 = C.textMuted, Visible = false, ZIndex = 2}, left)
gridMsg.Position = UDim2.fromOffset(0, GRID_Y)
gridMsg.Size = UDim2.fromOffset(LW, GRID_H)

local helpPanel = new("Frame", {Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundColor3 = C.overlay, BackgroundTransparency = 1, BorderSizePixel = 0, Visible = false, ClipsDescendants = true, ZIndex = 20}, left)
corner(helpPanel, s(12))
local helpScroll = new("ScrollingFrame", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = s(3), ScrollBarImageColor3 = C.surfaceHi, CanvasSize = UDim2.new(0, 0, 0, 0), AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 21}, helpPanel)
local HELP_TEXT = table.concat({
	'<b>Search tags</b>  <font color="#9AA1B8">type, press Enter</font>',
	'',
	'<font color="#B0B0B2">sort:</font>  relevance  newest  mostfavorited  bestselling  price_low_to_high  price_high_to_low',
	'<font color="#B0B0B2">price_min:</font> 50   <font color="#B0B0B2">price_max:</font> 500   <font color="#B0B0B2">price:</font> 100-500  or  free',
	'<font color="#B0B0B2">creator:</font> Roblox   or   creator:"Group Name"',
	'<font color="#B0B0B2">sale:</font> on (hide off-sale)   all',
	'<font color="#B0B0B2">category:</font> featured  collectibles  community  premium  recommended',
	'<font color="#B0B0B2">sales:</font> collectibles  premium  all',
	'<font color="#B0B0B2">period:</font> day  week  month  alltime',
	'<font color="#B0B0B2">limit:</font> 10  28  30  60  120',
	'',
	'<i>Underscores work as spaces in tag values:</i>  sort:price_high_to_low',
	'',
	'<b>Saved tab</b>',
	'<font color="#B0B0B2">sort:</font> newest  oldest  name  price_low_to_high  price_high_to_low  favorites',
	'<font color="#B0B0B2">is:</font> favorite  free  paid  offsale',
	'<font color="#B0B0B2">Recent tab:</font> same filters except is:favorite',
	'',
	'<b>Presets tab</b>',
	'<font color="#B0B0B2">sort:</font> newest  oldest  name   (search filters by name)',
	'',
	'<font color="#B0B0B2">Tab</font>  autocomplete the highlighted suggestion',
	'Example:  <i>zombie sort:price_high_to_low price_max:300</i>',
}, "\n")
local helpText = new("TextLabel", {
	Position = UDim2.fromOffset(s(14), s(12)), Size = UDim2.new(1, -s(28), 0, 0), AutomaticSize = Enum.AutomaticSize.Y, BackgroundTransparency = 1,
	RichText = true, Text = HELP_TEXT, TextWrapped = true, Font = Enum.Font.Gotham, TextSize = s(15), TextColor3 = C.text, TextTransparency = 1,
	TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, ZIndex = 22,
}, helpScroll)

local settingsPanel = new("Frame", {Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundColor3 = C.overlay, BackgroundTransparency = 1, BorderSizePixel = 0, Visible = false, ClipsDescendants = true, ZIndex = 20}, left)
corner(settingsPanel, s(12))
stroke(settingsPanel, C.stroke, 1, 0.2)
local setScroll = new("ScrollingFrame", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = s(3), ScrollBarImageColor3 = C.surfaceHi, CanvasSize = UDim2.new(0, 0, 0, 0), AutomaticCanvasSize = Enum.AutomaticSize.Y, ZIndex = 21}, settingsPanel)
pad(setScroll, s(14), s(14), s(10), s(10))

local function addSettingLabel(text, y)
    local lbl = new("TextLabel", {Position = UDim2.fromOffset(0, y), Size = UDim2.new(1, 0, 0, s(20)), BackgroundTransparency = 1, Text = text, Font = Enum.Font.GothamBold, TextSize = s(15), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 22}, setScroll)
    return lbl
end

local function createSlider(parent, label, minVal, maxVal, currentVal, yPos, callback)
    local row = new("Frame", {Position = UDim2.fromOffset(0, yPos), Size = UDim2.new(1, 0, 0, s(40)), BackgroundTransparency = 1, ZIndex = 22}, parent)
    if label ~= "" then
        new("TextLabel", {Size = UDim2.new(1, 0, 0, s(20)), BackgroundTransparency = 1, Text = label, Font = Enum.Font.Gotham, TextSize = s(14), TextColor3 = C.textDim, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 22}, row)
    end
    local bg = new("Frame", {Position = UDim2.fromOffset(0, label ~= "" and s(22) or s(5)), Size = UDim2.new(0.7, 0, 0, s(10)), BackgroundColor3 = C.surfaceHi, ZIndex = 22}, row)
    round(bg)
    local pct = (currentVal - minVal) / (maxVal - minVal)
    local fill = new("Frame", {Size = UDim2.new(pct, 0, 1, 0), BackgroundColor3 = C.accent2, ZIndex = 23}, bg)
    round(fill)
    local knob = new("Frame", {Size = UDim2.fromOffset(s(16), s(16)), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(pct, 0, 0.5, 0), BackgroundColor3 = WHITE, ZIndex = 24}, bg)
    round(knob)
    
    local valLbl = new("TextLabel", {Position = UDim2.new(0.72, 0, 0, label ~= "" and s(17) or s(0)), Size = UDim2.new(0.28, 0, 0, s(20)), BackgroundTransparency = 1, Text = string.format("x%.2f", currentVal), Font = Enum.Font.GothamBold, TextSize = s(13), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 22}, row)
    
    local dragging = false
    knob.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then dragging = true end
    end)
    track(uis.InputEnded:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then dragging = false end
    end))
    track(uis.InputChanged:Connect(function(input)
        if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
            local posX = math.clamp(input.Position.X - bg.AbsolutePosition.X, 0, bg.AbsoluteSize.X)
            local p = posX / bg.AbsoluteSize.X
            local val = minVal + p * (maxVal - minVal)
            fill.Size = UDim2.new(p, 0, 1, 0)
            knob.Position = UDim2.new(p, 0, 0.5, 0)
            valLbl.Text = string.format("x%.2f", val)
            callback(val)
        end
    end))
    return row
end

addSettingLabel("Restore Last Pack on Launch", s(10))
local restoreToggle = mkBtn(setScroll, {Text = scriptSettings.restoreLastPack and "ON" or "OFF", Size = UDim2.fromOffset(s(50), s(25)), Position = UDim2.fromOffset(0, s(40)), TextSize = s(14), ZIndex = 22}, scriptSettings.restoreLastPack and C.accent2 or C.surface, 0, scriptSettings.restoreLastPack and Color3.new(0,0,0) or C.textDim)
corner(restoreToggle, s(6))
restoreToggle.MouseButton1Click:Connect(function()
    scriptSettings.restoreLastPack = not scriptSettings.restoreLastPack
    restoreToggle.Text = scriptSettings.restoreLastPack and "ON" or "OFF"
    setBtn(restoreToggle, scriptSettings.restoreLastPack and C.accent2 or C.surface, 0, scriptSettings.restoreLastPack and Color3.new(0,0,0) or C.textDim)
    saveSettings()
end)

addSettingLabel("Force Animation (Ignore Tools)", s(85))
local forceToggle = mkBtn(setScroll, {Text = scriptSettings.forceAnimation and "ON" or "OFF", Size = UDim2.fromOffset(s(50), s(25)), Position = UDim2.fromOffset(0, s(115)), TextSize = s(14), ZIndex = 22}, scriptSettings.forceAnimation and C.accent2 or C.surface, 0, scriptSettings.forceAnimation and Color3.new(0,0,0) or C.textDim)
corner(forceToggle, s(6))
forceToggle.MouseButton1Click:Connect(function()
    scriptSettings.forceAnimation = not scriptSettings.forceAnimation
    forceToggle.Text = scriptSettings.forceAnimation and "ON" or "OFF"
    setBtn(forceToggle, scriptSettings.forceAnimation and C.accent2 or C.surface, 0, scriptSettings.forceAnimation and Color3.new(0,0,0) or C.textDim)
    saveSettings()
end)

addSettingLabel("Global Animation Speed", s(160))
createSlider(setScroll, "", 0.25, 5, scriptSettings.globalSpeed, s(180), function(val)
    scriptSettings.globalSpeed = val
    saveSettings()
    applyCurrentCharacterSpeeds()
end)

addSettingLabel("Advance Speed Animation", s(230))
local advToggle = mkBtn(setScroll, {Text = scriptSettings.advanceSpeed and "ON" or "OFF", Size = UDim2.fromOffset(s(50), s(25)), Position = UDim2.fromOffset(0, s(255)), TextSize = s(14), ZIndex = 22}, scriptSettings.advanceSpeed and C.accent2 or C.surface, 0, scriptSettings.advanceSpeed and Color3.new(0,0,0) or C.textDim)
corner(advToggle, s(6))

local customSpeedRows = {}
local customSpeedsY = s(290)
local function createCustomSliders()
    for _, row in ipairs(customSpeedRows) do row:Destroy() end
    table.clear(customSpeedRows)
    customSpeedsY = s(290)
    if scriptSettings.advanceSpeed then
        for _, slot in ipairs(buttonOrder) do
            local currentVal = scriptSettings.customSpeeds[slot] or 1
            local row = createSlider(setScroll, shortNames[slot] .. " Speed", 0.25, 5, currentVal, customSpeedsY, function(val)
                scriptSettings.customSpeeds[slot] = val
                saveSettings()
                applyCurrentCharacterSpeeds()
            end)
            customSpeedRows[#customSpeedRows + 1] = row
            customSpeedsY += s(45)
        end
    end
end
createCustomSliders()

advToggle.MouseButton1Click:Connect(function()
    scriptSettings.advanceSpeed = not scriptSettings.advanceSpeed
    advToggle.Text = scriptSettings.advanceSpeed and "ON" or "OFF"
    setBtn(advToggle, scriptSettings.advanceSpeed and C.accent2 or C.surface, 0, scriptSettings.advanceSpeed and Color3.new(0,0,0) or C.textDim)
    saveSettings()
    createCustomSliders()
    applyCurrentCharacterSpeeds()
end)

local footer = new("Frame", {Position = UDim2.fromOffset(0, BODY_H - FOOT_H), Size = UDim2.fromOffset(LW, FOOT_H), BackgroundTransparency = 1}, left)
local prevBtn = mkBtn(footer, {Text = "← Back", Size = UDim2.fromOffset(s(90), FOOT_H), TextSize = s(14)}, C.surfaceHi, 0.6, C.textMuted)
corner(prevBtn, s(8))
local nextBtn = mkBtn(footer, {Text = "Next →", Size = UDim2.fromOffset(s(90), FOOT_H), Position = UDim2.new(1, -s(90), 0, 0), TextSize = s(14)}, C.surfaceHi, 0.6, C.textMuted)
corner(nextBtn, s(8))
local pageLbl = new("TextLabel", {Position = UDim2.fromOffset(s(94), 0), Size = UDim2.new(1, -s(188), 1, 0), BackgroundTransparency = 1, Text = "Loading…", Font = Enum.Font.GothamSemibold, TextSize = s(14), TextColor3 = C.textDim, TextXAlignment = Enum.TextXAlignment.Center}, footer)

local rightP = new("Frame", {Position = UDim2.fromOffset(PAD + LW + GAP, TOPH), Size = UDim2.fromOffset(RW, BODY_H), BackgroundColor3 = C.panel, BackgroundTransparency = 0.5, BorderSizePixel = 0}, mf)
corner(rightP, s(14)); stroke(rightP, C.stroke, 1, 0.6)
local IP = s(10)
local innerW = RW - IP * 2
local VPH = s(140)

local masterViewport = new("ViewportFrame", {
	Position = UDim2.fromOffset(IP, IP), Size = UDim2.fromOffset(innerW, VPH), BackgroundColor3 = C.overlay, BackgroundTransparency = 0.15, BorderSizePixel = 0,
	Ambient = Color3.fromRGB(160, 160, 165), LightColor = WHITE, LightDirection = Vector3.new(-1, -1.5, -1), ClipsDescendants = true,
}, rightP)
corner(masterViewport, s(12)); stroke(masterViewport, C.stroke, 1, 0.5)
local hintLabel = new("TextLabel", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Text = "Select a bundle\nto preview it here", Font = Enum.Font.GothamSemibold, TextSize = s(15), TextColor3 = C.textMuted, ZIndex = 3}, masterViewport)
local previewShimmer = new("Frame", {Size = UDim2.fromScale(1, 1), BackgroundColor3 = C.surfaceHi, BackgroundTransparency = 0.5, BorderSizePixel = 0, Visible = false, ZIndex = 5}, masterViewport)
corner(previewShimmer, s(12)); applyShimmer(previewShimmer, C.overlay, C.surface)
local masterBadge = mkBadge(masterViewport, {Position = UDim2.fromOffset(s(10), s(10)), ZIndex = 6, Visible = false})
local statusChip = new("TextLabel", {
	Position = UDim2.fromOffset(s(10), VPH - s(34)), Size = UDim2.fromOffset(0, s(24)), AutomaticSize = Enum.AutomaticSize.X,
	BackgroundColor3 = C.overlay, BackgroundTransparency = 0.3, BorderSizePixel = 0, Text = "NO BUNDLE", Font = Enum.Font.GothamBold, TextSize = s(13), TextColor3 = C.textMuted, ZIndex = 6,
}, masterViewport)
round(statusChip); pad(statusChip, s(12), s(12))

local yInfo = IP + VPH + s(6)
local nameLabel = new("TextLabel", {Position = UDim2.fromOffset(IP, yInfo), Size = UDim2.fromOffset(innerW - s(42), s(22)), BackgroundTransparency = 1, Text = "No bundle selected", Font = Enum.Font.GothamBold, TextSize = s(17), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd}, rightP)
local creatorLabel = new("TextLabel", {Position = UDim2.fromOffset(IP, yInfo + s(24)), Size = UDim2.fromOffset(innerW - s(42), s(17)), BackgroundTransparency = 1, Text = "Pick one from the list", Font = Enum.Font.Gotham, TextSize = s(13), TextColor3 = C.textDim, TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd}, rightP)
local starBtn = mkBtn(rightP, {Text = "☆", Size = UDim2.fromOffset(s(38), s(38)), Position = UDim2.fromOffset(IP + innerW - s(38), yInfo), TextSize = s(22), Visible = false}, C.surface, 0.2, C.textDim)
corner(starBtn, s(9))

local ySlots = yInfo + s(44) + s(4)
local slotGrid = new("Frame", {Position = UDim2.fromOffset(IP, ySlots), Size = UDim2.fromOffset(innerW, s(60)), BackgroundTransparency = 1}, rightP)
local cellW = math.floor((innerW - s(6) * 3) / 4)
new("UIGridLayout", {CellSize = UDim2.fromOffset(cellW, s(28)), CellPadding = UDim2.fromOffset(s(6), s(6)), SortOrder = Enum.SortOrder.LayoutOrder}, slotGrid)
local slotButtons = {}
for i, slot in ipairs(buttonOrder) do
	local b = mkBtn(slotGrid, {Text = shortNames[slot], LayoutOrder = i, TextSize = s(14)}, C.surface, 0.5, C.textMuted)
	corner(b, s(8))
	slotButtons[slot] = b
end

local yAct = ySlots + s(60) + s(8)
local halfW = math.floor((innerW - s(10)) / 2)
local wearSelBtn = mkBtn(rightP, {Text = "Wear Selected", Size = UDim2.fromOffset(halfW, s(34)), Position = UDim2.fromOffset(IP, yAct), TextSize = s(14)}, C.surfaceHi, 0.55, C.textMuted)
corner(wearSelBtn, s(9))
local wearAllBtn = mkBtn(rightP, {Text = "Wear All", Size = UDim2.fromOffset(halfW, s(34)), Position = UDim2.fromOffset(IP + halfW + s(10), yAct), TextSize = s(14)}, C.surfaceHi, 0.55, C.textMuted)
corner(wearAllBtn, s(9))
local resetBtn = mkBtn(rightP, {Text = "RESET TO DEFAULT", Size = UDim2.fromOffset(innerW, s(34)), Position = UDim2.fromOffset(IP, yAct + s(34) + s(8)), TextSize = s(14), Font = Enum.Font.GothamBold}, Color3.fromRGB(160, 80, 80), 0.1, WHITE)
corner(resetBtn, s(8))

local toastLbl = new("TextLabel", {
	AnchorPoint = Vector2.new(0.5, 1), Position = UDim2.new(0.5, 0, 1, -s(18)), Size = UDim2.fromOffset(0, s(34)), AutomaticSize = Enum.AutomaticSize.X,
	BackgroundColor3 = C.panel, BackgroundTransparency = 1, BorderSizePixel = 0, Text = "", Font = Enum.Font.GothamSemibold, TextSize = s(15),
	TextColor3 = C.text, TextTransparency = 1, Visible = false, ZIndex = 100,
}, mf)
round(toastLbl); pad(toastLbl, s(18), s(18))
local toastStroke = stroke(toastLbl, C.accent2, 1, 1)

local pill = mkBtn(g, {Text = "UGC", Size = UDim2.fromOffset(s(58), s(58)), AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(1, -s(50), 0, s(76)), TextSize = s(17), Font = Enum.Font.GothamBlack, Visible = false, Active = true}, C.surfaceHi, 0.1, C.text)
round(pill); stroke(pill, C.accent2, 1, 0.5)

local currentTab = "Discover"
local searchResults, savedTabList = {}, {}
local catalogCursor
local currentPageIndex = 1
local activeSpec
local activeBundle
local currentBundleItems = {}
local selectedSlot = "idleanimation"
local searchGen, masterGen, inspectGen = 0, 0, 0
local masterTrack, masterRig, masterAnimator
local cards, skeletons = {}, {}
local chipButtons, tagPills = {}, {}
local pagingBusy, wearBusy = false, false

local inspectBundle, playMaster, runSearch, refreshTags, drawGridPage, refreshSavedView

local toastGen = 0
toast = function(text, kind)
	toastGen = toastGen + 1
	local myGen = toastGen
	local col = (kind == "bad" and C.red) or (kind == "ok" and C.accent2) or C.accent2
	toastLbl.Text = text
	toastStroke.Color = col
	toastLbl.Visible = true
	toastLbl.Position = UDim2.new(0.5, 0, 1, -s(2))
	toastLbl.BackgroundTransparency, toastLbl.TextTransparency, toastStroke.Transparency = 1, 1, 1
	tw(toastLbl, 0.28, {Position = UDim2.new(0.5, 0, 1, -s(18)), BackgroundTransparency = 0.15, TextTransparency = 0}, Enum.EasingStyle.Back)
	tw(toastStroke, 0.28, {Transparency = 0.3})
	task.delay(2.4, function()
		if myGen ~= toastGen then return end
		tw(toastLbl, 0.3, {BackgroundTransparency = 1, TextTransparency = 1})
		tw(toastStroke, 0.3, {Transparency = 1})
		task.delay(0.32, function()
			if myGen == toastGen then toastLbl.Visible = false end
		end)
	end)
end

local spinning = {}
local spinAngle = 0
local spinConn
spinConn = rs.RenderStepped:Connect(function(dt)
	if not g.Parent then spinConn:Disconnect(); return end
	spinAngle = (spinAngle + math.rad(28) * dt) % (math.pi * 2)
	local cf = CFrame.Angles(0, spinAngle, 0)
	for model in pairs(spinning) do
		if model.Parent then model:PivotTo(cf) else spinning[model] = nil end
	end
end)
track(spinConn)

local function buildRig(vpFrame)
	local wm = vpFrame:FindFirstChildOfClass("WorldModel")
	if wm then wm:ClearAllChildren() else wm = new("WorldModel", {}, vpFrame) end
	local char = lp.Character or lp.CharacterAdded:Wait()
	local wasArch = char.Archivable
	char.Archivable = true
	local clone = char:Clone()
	char.Archivable = wasArch
	if not clone then return nil end
	for _, d in ipairs(clone:GetDescendants()) do
		if d:IsA("LuaSourceContainer") or d:IsA("Tool") or d:IsA("Sound") then d:Destroy() end
	end
	local root = clone:FindFirstChild("HumanoidRootPart")
	local human = clone:FindFirstChildOfClass("Humanoid")
	if not root or not human then clone:Destroy(); return nil end
	root.Anchored = true
	pcall(function()
		human.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
		human.HealthDisplayType = Enum.HumanoidHealthDisplayType.AlwaysOff
	end)
	local animator = human:FindFirstChildOfClass("Animator") or new("Animator", {}, human)
	clone.Parent = wm
	clone:PivotTo(CFrame.new(0, 0, 0))
	local cam = vpFrame.CurrentCamera
	if not cam then
		cam = new("Camera", {}, vpFrame)
		vpFrame.CurrentCamera = cam
	end
	cam.FieldOfView = 50
	cam.CFrame = CFrame.lookAt(Vector3.new(0, 0.6, 8), Vector3.new(0, -0.2, 0))
	spinning[clone] = true
	return clone, animator
end

local function tryPlayTrack(animator, animation, looped)
	local ok, pt = pcall(function()
		local tr = animator:LoadAnimation(animation)
		tr.Looped = looped
		tr:Play()
		return tr
	end)
	if not ok or not pt then return false, nil end
	task.wait(0.15)
	if not pt.IsPlaying then
		pcall(function() pt:Stop() end)
		return false, nil
	end
	return true, pt
end

local function setPreviewState(state, slot, n)
	local label = (slot and shortNames[slot]) and string.upper(shortNames[slot]) or ""
	if state == "empty" then
		statusChip.Text, statusChip.TextColor3 = "NO BUNDLE", C.textMuted
		previewShimmer.Visible = false
		hintLabel.Visible = true
	elseif state == "loading" then
		statusChip.Text, statusChip.TextColor3 = label .. "  ·  LOADING", C.textDim
		previewShimmer.Visible = true
	elseif state == "retry" then
		statusChip.Text, statusChip.TextColor3 = label .. "  ·  RETRY " .. tostring(n or 1), C.orange
		previewShimmer.Visible = true
	elseif state == "playing" then
		statusChip.Text, statusChip.TextColor3 = "•  " .. label, C.accent2
		previewShimmer.Visible = false
	elseif state == "missing" then
		statusChip.Text, statusChip.TextColor3 = "NO ANIMATIONS", C.orange
		previewShimmer.Visible = false
	else
		statusChip.Text, statusChip.TextColor3 = label .. "  ·  FAILED", C.red
		previewShimmer.Visible = false
	end
end

local function refreshSlotButtons()
	for slot, b in pairs(slotButtons) do
		local has = currentBundleItems[slot] ~= nil
		if has and slot == selectedSlot then setBtn(b, C.surfaceHi, 0, C.text)
		elseif has then setBtn(b, C.surface, 0.2, C.text)
		else setBtn(b, C.surface, 0.5, C.textMuted) end
	end
end

local function setActionsEnabled(on)
	if on then
		setBtn(wearSelBtn, C.surfaceHi, 0.15, C.text)
		setBtn(wearAllBtn, C.surfaceHi, 0.15, C.text)
	else
		setBtn(wearSelBtn, C.surfaceHi, 0.55, C.textMuted)
		setBtn(wearAllBtn, C.surfaceHi, 0.55, C.textMuted)
	end
end

local function refreshActiveInfo()
	if not activeBundle then
		nameLabel.Text = "No bundle selected"
		creatorLabel.Text = "Pick one from the list"
		masterBadge.Visible = false
		starBtn.Visible = false
		return
	end
	if activeBundle.IsPreset then
		nameLabel.Text = activeBundle.Name
		creatorLabel.Text = "Preset  ·  " .. tostring(activeBundle.Count or 0) .. ((activeBundle.Count or 0) == 1 and " slot" or " slots")
		masterBadge.Visible = false
		starBtn.Visible = false
		return
	end
	local key = tostring(activeBundle.Id)
	local meta = metaCache[key]
	nameLabel.Text = activeBundle.Name
	local parts = {}
	if meta and meta.creator then parts[#parts + 1] = "by " .. meta.creator end
	parts[#parts + 1] = "ID " .. key
	creatorLabel.Text = table.concat(parts, "  ·  ")
	masterBadge.Visible = true
	applyBadge(masterBadge, meta)
	starBtn.Visible = true
	local saved = savedBookmarks[key] ~= nil
	starBtn.Text = saved and "★" or "☆"
	starBtn.TextColor3 = saved and C.gold or C.textDim
end

onMetaChanged = function(key)
	local refs = badgeRefs[key]
	if refs then
		for i = #refs, 1, -1 do
			local lbl = refs[i]
			if lbl.Parent then applyBadge(lbl, metaCache[key]) else table.remove(refs, i) end
		end
	end
	if activeBundle and tostring(activeBundle.Id) == key then refreshActiveInfo() end
	local bm = savedBookmarks[key]
	local mt = metaCache[key]
	if bm and mt then
		bm.Kind, bm.Amount, bm.Creator = mt.kind, mt.amount, mt.creator
		queueSave(SAVED_BUNDLES_FILE, savedBookmarks)
	end
	if mt then
		local touched = false
		for _, h in ipairs(historyList) do
			if tostring(h.Id) == key then
				h.Kind, h.Amount, h.Creator = mt.kind, mt.amount, mt.creator
				touched = true
			end
		end
		if touched then queueSave(HISTORY_FILE, historyList) end
	end
end

local function updateSelection()
	for _, c in ipairs(cards) do
		if c.box.Parent then c.restore() end
	end
end

local function runMasterPlayback(myGen, payload, slot, tracks)
	local refetched = false
	local idx, fails = 1, 0
	while myGen == masterGen do
		local tr = tracks[idx]
		local ok, played = false, nil
		if tr then ok, played = tryPlayTrack(masterAnimator, tr, #tracks == 1) end
		if myGen ~= masterGen then
			if played then pcall(function() played:Stop(0) end) end
			return nil
		end
		if ok then
			fails = 0
			masterTrack = played
			applySpeed(masterTrack, slot)
			setPreviewState("playing", slot)
			if #tracks == 1 then return true end
			played.Stopped:Wait()
			idx = idx % #tracks + 1
		else
			fails = fails + 1
			if fails >= 2 * math.max(#tracks, 1) then
				if refetched then return false end
				refetched, fails = true, 0
				clearAssetCache(payload.Id)
				task.wait(0.4)
				if myGen ~= masterGen then return nil end
				tracks = getTracksFor(payload, slot)
				idx = 1
				if #tracks == 0 then return false end
			else
				idx = idx % math.max(#tracks, 1) + 1
				task.wait(0.3)
			end
		end
	end
	return nil
end

playMaster = function(slot)
	local payload = currentBundleItems[slot]
	if not payload then return end
	masterGen = masterGen + 1
	local myGen = masterGen
	if masterTrack then pcall(function() masterTrack:Stop(0.1) end); masterTrack = nil end
	setPreviewState("loading", slot)
	task.spawn(function()
		if not masterAnimator or not masterRig or not masterRig.Parent then
			masterRig, masterAnimator = buildRig(masterViewport)
		end
		if myGen ~= masterGen then return end
		if not masterAnimator then setPreviewState("failed", slot); return end
		local tracks = {}
		for attempt = 0, 4 do
			tracks = getTracksFor(payload, slot)
			if #tracks > 0 then break end
			if myGen ~= masterGen then return end
			setPreviewState("retry", slot, attempt + 1)
			clearAssetCache(payload.Id)
			task.wait(math.min(0.5 + attempt * 0.3, 2.5))
			if myGen ~= masterGen then return end
		end
		if myGen ~= masterGen then return end
		if #tracks == 0 then setPreviewState("failed", slot); return end
		local result = runMasterPlayback(myGen, payload, slot, tracks)
		if result == false and myGen == masterGen then setPreviewState("failed", slot) end
	end)
end

inspectBundle = function(entry)
	activeBundle = {Id = entry.Id, Name = entry.Name or "Unknown"}
	inspectGen = inspectGen + 1
	local myInspect = inspectGen
	masterGen = masterGen + 1
	if masterTrack then pcall(function() masterTrack:Stop(0.1) end); masterTrack = nil end
	table.clear(currentBundleItems)
	hintLabel.Visible = false
	seedMeta(entry)
	ensureMeta(entry.Id, true)
	refreshActiveInfo()
	refreshSlotButtons()
	setActionsEnabled(false)
	setPreviewState("loading", selectedSlot)
	updateSelection()
	task.spawn(function()
		local details = getDetails(entry.Id)
		if myInspect ~= inspectGen then return end
		if not details then setPreviewState("failed", selectedSlot); return end
		for k, v in pairs(mapBundleItems(details.Items)) do currentBundleItems[k] = v end
		local slot = currentBundleItems[selectedSlot] and selectedSlot or nil
		if not slot then
			for _, name in ipairs(buttonOrder) do
				if currentBundleItems[name] then slot = name; break end
			end
		end
		if not slot then
			refreshSlotButtons()
			setPreviewState("missing", selectedSlot)
			return
		end
		selectedSlot = slot
		refreshSlotButtons()
		setActionsEnabled(true)
		masterRig, masterAnimator = nil, nil
		playMaster(slot)
	end)
end

for _, slot in ipairs(buttonOrder) do
	slotButtons[slot].MouseButton1Click:Connect(function()
		if not activeBundle or not currentBundleItems[slot] then return end
		selectedSlot = slot
		refreshSlotButtons()
		playMaster(slot)
	end)
end

wearSelBtn.MouseButton1Click:Connect(function()
	if not activeBundle or wearBusy then return end
	local payload = currentBundleItems[selectedSlot]
	if not payload then return end
	if not lp.Character then toast("No character found", "bad"); return end
	wearBusy = true
	wearSelBtn.Text = "Equipping…"
	local slot = selectedSlot
	task.spawn(function()
		local list = getTracksFor(payload, slot)
		if #list > 0 then
			preloadAnimations(list)
			local n = equipMap(lp.Character, {[slot] = list}, true)
			if n > 0 and activeBundle and not activeBundle.IsPreset then 
				recordHistory(activeBundle.Id, activeBundle.Name) 
				lastPackData = {Id = activeBundle.Id, Name = activeBundle.Name}
				saveJSON(LAST_PACK_FILE, lastPackData)
			end
			toast(n > 0 and ("Equipped " .. shortNames[slot]) or "Couldn't equip that slot", n > 0 and "ok" or "bad")
		else
			toast("Animation failed to load", "bad")
		end
		wearSelBtn.Text = "Wear Selected"
		wearBusy = false
	end)
end)

wearAllBtn.MouseButton1Click:Connect(function()
	if not activeBundle or wearBusy or next(currentBundleItems) == nil then return end
	if not lp.Character then toast("No character found", "bad"); return end
	wearBusy = true
	wearAllBtn.Text = "Equipping…"
	task.spawn(function()
		local n = applyBundleItemsToCharacter(currentBundleItems)
		if n > 0 and activeBundle and not activeBundle.IsPreset then 
			recordHistory(activeBundle.Id, activeBundle.Name) 
			lastPackData = {Id = activeBundle.Id, Name = activeBundle.Name}
			saveJSON(LAST_PACK_FILE, lastPackData)
		end
		toast(n > 0 and ("Equipped " .. n .. " animations") or "Nothing could be equipped", n > 0 and "ok" or "bad")
		wearAllBtn.Text = "Wear All"
		wearBusy = false
	end)
end)

local resetBusy = false
resetBtn.MouseButton1Click:Connect(function()
	if resetBusy then return end
	resetBusy = true
	resetBtn.Text = "RESETTING..."

	task.spawn(function()
		local function finishReset()
			if resetBtn and resetBtn.Parent then resetBtn.Text = "RESET TO DEFAULT" end
			resetBusy = false
		end

		for k in pairs(equippedAnims) do equippedAnims[k] = nil end
		saveJSON(EQUIPPED_FILE, equippedAnims)
		lastPackData = {}
		saveJSON(LAST_PACK_FILE, lastPackData)

		local char = lp.Character
		local human = char and char:FindFirstChildOfClass("Humanoid")
		if not char or not human then
			toast("No character found", "bad")
			finishReset()
			return
		end

		local okDesc, desc = pcall(function()
			return plrs:GetHumanoidDescriptionFromUserId(lp.UserId)
		end)
		if not okDesc or not desc then
			okDesc, desc = pcall(function() return human:GetAppliedDescription() end)
		end
		if not okDesc or not desc then
			toast("Couldn't retrieve default avatar animations", "bad")
			finishReset()
			return
		end

		pcall(function() human:ApplyDescription(desc) end)
		local animate = char:FindFirstChild("Animate") or char:WaitForChild("Animate", 5)
		if not animate then
			pcall(function() desc:Destroy() end)
			toast("Animate script not found", "bad")
			finishReset()
			return
		end

		for _, tr in ipairs(human:GetPlayingAnimationTracks()) do
			pcall(function() tr:Stop(0) end)
		end
		pcall(function() animate.Disabled = true end)

		local defaults = {
			idle = {"507766666", "507766951"},
			walk = {"507777826"},
			run = {"507767714"},
			jump = {"507765000"},
			fall = {"507767968"},
			climb = {"507765644"},
			swim = {"507784897"},
			swimidle = {"507785072"},
		}
		local descriptionProperties = {
			idle = "IdleAnimation", walk = "WalkAnimation", run = "RunAnimation",
			jump = "JumpAnimation", fall = "FallAnimation", climb = "ClimbAnimation",
			swim = "SwimAnimation", swimidle = "SwimAnimation",
		}
		local restored = 0
		for folderName, fallbackIds in pairs(defaults) do
			local folder = animate:FindFirstChild(folderName)
			if folder then
				local ids = fallbackIds
				local property = descriptionProperties[folderName]
				if property and desc then
					local okId, rawId = pcall(function() return desc[property] end)
					local assetId = okId and tonumber(rawId) or nil
					if assetId and assetId > 0 then ids = {tostring(math.floor(assetId))} end
				end
				local animations = {}
				for _, child in ipairs(folder:GetChildren()) do
					if child:IsA("Animation") then table.insert(animations, child) end
				end
				if #animations == 0 then
					local animation = Instance.new("Animation")
					animation.Name = folderName == "idle" and "Animation1" or (folderName == "walk" and "WalkAnim" or folderName .. "Anim")
					animation.Parent = folder
					animations[1] = animation
				end
				for index, animation in ipairs(animations) do
					local id = ids[math.min(index, #ids)]
					animation.AnimationId = "rbxassetid://" .. id
					if folderName == "idle" and index == 1 then animation.Name = "Animation1" end
					if folderName == "idle" and index == 2 then animation.Name = "Animation2" end
				end
				restored += 1
			end
		end

		task.wait(0.1)
		pcall(function() animate.Disabled = false end)
		pcall(function() desc:Destroy() end)

		if restored > 0 then
			toast("Animations reset to default", "ok")
		else
			toast("Could not restore default animation IDs", "bad")
		end
		finishReset()
	end)
end)

local function toggleBookmark(id, name)
	local key = tostring(id)
	local nowSaved
	if savedBookmarks[key] then
		savedBookmarks[key] = nil
		nowSaved = false
	else
		local mt = metaCache[key] or {}
		savedBookmarks[key] = {Id = id, Name = name, Fav = false, Time = tick(), Kind = mt.kind, Amount = mt.amount, Creator = mt.creator}
		nowSaved = true
		ensureMeta(id, true)
		captureBundleData(id)
	end
	saveJSON(SAVED_BUNDLES_FILE, savedBookmarks)
	for _, c in ipairs(cards) do
		if c.key == key then c.refreshSave() end
	end
	if activeBundle and tostring(activeBundle.Id) == key then refreshActiveInfo() end
	if currentTab == "Saved" and not nowSaved and refreshSavedView then refreshSavedView() end
	toast(nowSaved and "Saved to your list" or "Removed from saved", nowSaved and "ok" or nil)
	return nowSaved
end

starBtn.MouseButton1Click:Connect(function()
	if activeBundle then toggleBookmark(activeBundle.Id, activeBundle.Name) end
end)

local function clearSkeletons()
	for _, f in ipairs(skeletons) do stopShimmer(f); f:Destroy() end
	table.clear(skeletons)
end

local function clearGrid()
	for _, c in ipairs(cards) do
		if c.skeleton then stopShimmer(c.skeleton) end
		c.box:Destroy()
	end
	table.clear(cards)
end

local function showSkeletons()
	clearGrid()
	clearSkeletons()
	gridMsg.Visible = false
	for i = 1, PER_PAGE do
		local f = new("Frame", {BackgroundColor3 = C.surface, BackgroundTransparency = 0.35, BorderSizePixel = 0, Size = UDim2.fromOffset(CW, CH), LayoutOrder = i}, gridHolder)
		corner(f, s(12))
		applyShimmer(f, C.panel, C.surfaceHi)
		skeletons[#skeletons + 1] = f
	end
end

local function showGridMessage(msg)
	clearGrid()
	clearSkeletons()
	gridMsg.Text = msg
	gridMsg.Visible = true
end

local function updatePager(total)
	local pages = math.max(1, math.ceil(total / PER_PAGE))
	local more = (currentTab == "Discover" and catalogCursor ~= nil and not catalogCursor.IsFinished) and true or false
	pageLbl.Text = "Page " .. currentPageIndex .. " / " .. pages .. (more and "+" or "") .. "  ·  " .. total .. (more and "+" or "") .. " results"
	local canPrev = currentPageIndex > 1
	local canNext = currentPageIndex < pages or more
	setBtn(prevBtn, C.surfaceHi, canPrev and 0.3 or 0.7, canPrev and C.text or C.textMuted)
	setBtn(nextBtn, C.surfaceHi, canNext and 0.3 or 0.7, canNext and C.text or C.textMuted)
end

local cardOrder = 0
local function buildCard(entry, idx)
	local id = entry.Id
	local key = tostring(id)
	seedMeta(entry)
	ensureMeta(id, false)

	cardOrder += 1

	local box = new("ViewportFrame", {
		Name = "Card", BackgroundColor3 = C.panel, BackgroundTransparency = 0.25, BorderSizePixel = 0, ClipsDescendants = true,
		Size = UDim2.fromOffset(CW, CH),
		LayoutOrder = cardOrder,
		Ambient = Color3.fromRGB(160, 160, 165), LightColor = WHITE, LightDirection = Vector3.new(-1, -1.5, -1),
	}, gridHolder)
	corner(box, s(12))
	local boxStroke = stroke(box, C.stroke, 1, 0.5)

	local boxScale = new("UIScale", {Scale = 0.86}, box)
	tw(boxScale, 0.3 + idx * 0.05, {Scale = 1}, Enum.EasingStyle.Back)

	local skeleton = new("Frame", {Size = UDim2.fromScale(1, 1), BackgroundColor3 = C.surface, BackgroundTransparency = 0.15, BorderSizePixel = 0, ZIndex = 2}, box)
	corner(skeleton, s(12)); applyShimmer(skeleton, C.panel, C.surfaceHi)

	local cpad = s(6)
	new("TextLabel", {
		Position = UDim2.fromOffset(cpad + s(2), CH - AH - cpad - s(22)), Size = UDim2.fromOffset(CW - cpad * 2 - s(4), s(20)), BackgroundTransparency = 1,
		Text = entry.Name or "Unknown", Font = Enum.Font.GothamBold, TextSize = s(15), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left,
		TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 4,
	}, box)

	local badge = mkBadge(box, {Position = UDim2.fromOffset(cpad, cpad), ZIndex = 5})
	bindBadge(badge, id)

	local hit = new("TextButton", {Size = UDim2.new(1, 0, 1, -(AH + s(12))), BackgroundTransparency = 1, Text = "", ZIndex = 5}, box)

	local cardActions = new("Frame", {Position = UDim2.fromOffset(cpad, CH - AH - cpad), Size = UDim2.fromOffset(CW - cpad * 2, AH), BackgroundTransparency = 1, ZIndex = 6}, box)
	local saveBtn = mkBtn(cardActions, {Text = "+ Save", Size = UDim2.new(0.5, -s(3), 1, 0), TextSize = s(12), ZIndex = 6}, C.surfaceHi, 0.25, C.textDim)
	corner(saveBtn, s(6))
	local wearBtn = mkBtn(cardActions, {Text = "▶ Wear", Size = UDim2.new(0.5, -s(3), 1, 0), Position = UDim2.new(0.5, s(3), 0, 0), TextSize = s(12), ZIndex = 6}, C.surfaceHi, 0.15, C.text)
	corner(wearBtn, s(6))

	local function refreshSave()
		local saved = savedBookmarks[key] ~= nil
		saveBtn.Text = saved and "✓ Saved" or "+ Save"
		saveBtn.TextColor3 = saved and C.gold or C.textDim
	end
	refreshSave()

	if currentTab == "Saved" then
		local favBtn = mkBtn(box, {Text = (entry.Fav and "★" or "☆"), Size = UDim2.fromOffset(s(28), s(28)), Position = UDim2.new(1, -s(32), 0, s(4)), TextSize = s(18), ZIndex = 7}, C.overlay, 0.4, entry.Fav and C.gold or C.textDim)
		round(favBtn)
		favBtn.MouseButton1Click:Connect(function()
			local bm = savedBookmarks[key]
			if bm then
				bm.Fav = not bm.Fav
				saveJSON(SAVED_BUNDLES_FILE, savedBookmarks)
				favBtn.Text = bm.Fav and "★" or "☆"
				favBtn.TextColor3 = bm.Fav and C.gold or C.textDim
			end
		end)
	end

	local record = {key = key, box = box, skeleton = skeleton, refreshSave = refreshSave}
	record.restore = function()
		local selected = activeBundle ~= nil and tostring(activeBundle.Id) == key
		boxStroke.Color = selected and C.accent2 or C.stroke
		boxStroke.Thickness = selected and 2 or 1
		boxStroke.Transparency = selected and 0 or 0.5
	end
	cards[#cards + 1] = record
	record.restore()

	hit.MouseEnter:Connect(function()
		tw(box, 0.12, {BackgroundColor3 = C.surface})
		boxStroke.Color = C.accent2
		boxStroke.Transparency = 0
	end)
	hit.MouseLeave:Connect(function()
		tw(box, 0.15, {BackgroundColor3 = C.panel})
		record.restore()
	end)
	hit.MouseButton1Click:Connect(function() inspectBundle(entry) end)

	saveBtn.MouseButton1Click:Connect(function() toggleBookmark(id, entry.Name) end)
	wearBtn.MouseButton1Click:Connect(function()
		wearBtn.Text = "…"
		wearBundle(id, entry.Name, function()
			if wearBtn.Parent then wearBtn.Text = "▶ Wear" end
		end)
	end)

	task.spawn(function()
		task.wait(0.14 * idx)
		if not box.Parent then return end
		local function dropSkeleton()
			if skeleton.Parent then
				tw(skeleton, 0.25, {BackgroundTransparency = 1})
				task.delay(0.3, function()
					stopShimmer(skeleton)
					skeleton:Destroy()
				end)
			end
		end
		local details = getDetails(id)
		if not box.Parent then return end
		if not details then dropSkeleton(); return end
		local map = mapBundleItems(details.Items)
		local usable = {}
		for _, slot in ipairs(CARD_ORDER) do
			if map[slot] and #usable < 4 then usable[#usable + 1] = {slot = slot, payload = map[slot]} end
		end
		if #usable == 0 then dropSkeleton(); return end
		local rig, animator = buildRig(box)
		if not rig or not box.Parent then dropSkeleton(); return end
		local i, current = 1, nil
		while box.Parent do
			local u = usable[i]
			local list = getTracksFor(u.payload, u.slot)
			if #list > 0 then
				if current then pcall(function() current:Stop(0.1) end); current = nil end
				local ok, played = tryPlayTrack(animator, list[1], #list == 1)
				if ok then 
                    current = played 
                    applySpeed(current, u.slot)
                    dropSkeleton() 
                end
			end
			task.wait(2.6)
			i = i % #usable + 1
		end
		if current then pcall(function() current:Stop(0) end) end
	end)
end

drawGridPage = function(dataList)
	clearGrid()
	clearSkeletons()
	gridMsg.Visible = false

	if not dataList or #dataList == 0 then
		showGridMessage(currentTab == "Saved" and "No saved bundles match" or (currentTab == "Recent" and "No recently used bundles yet" or "No results"))
		updatePager(0)
		return
	end

	local start = (currentPageIndex - 1) * PER_PAGE + 1
	local ending = math.min(currentPageIndex * PER_PAGE, #dataList)
	if start > #dataList then
		currentPageIndex = 1
		start = 1
		ending = math.min(PER_PAGE, #dataList)
	end
	local idx = 1
	for i = start, ending do
		local entry = dataList[i]
		if not entry then break end
		buildCard(entry, idx)
		idx += 1
	end
	updatePager(#dataList)
end

refreshTags = function(q, spec)
	for _, p in ipairs(tagPills) do p:Destroy() end
	table.clear(tagPills)

	local issueSet = {}
	for _, issue in ipairs(spec.issues or {}) do issueSet[issue] = true end

	local any = false
	for _, tg in ipairs(q.tags) do
		any = true
		local txt = tg.key .. ":" .. tg.val
		local bad = issueSet[txt] == true
		local pillBtn = mkBtn(tagsRow, {
			Text = txt, AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, TAG_H),
			TextSize = s(14), LayoutOrder = #tagPills + 1,
		}, bad and C.badBg or C.pillBg, 0.25, bad and C.badTx or C.pillTx)
		corner(pillBtn, math.floor(TAG_H / 2))
		pad(pillBtn, s(15), s(15))
		table.insert(tagPills, pillBtn)

		pillBtn.MouseButton1Click:Connect(function()
			local newTags = {}
			for _, t in ipairs(q.tags) do
				if not (t.key == tg.key and t.val == tg.val) then newTags[#newTags + 1] = t end
			end
			local parts = {}
			if q.keyword ~= "" then parts[#parts + 1] = q.keyword end
			for _, t in ipairs(newTags) do
				local v = t.val
				if v:find("%s") then v = '"' .. v .. '"' end
				parts[#parts + 1] = t.key .. ":" .. v
			end
			local newText = table.concat(parts, " ")
			sb.Text = newText
			if runSearch then runSearch(newText) end
		end)
	end
	tagHint.Visible = not any

	for tabName, set in pairs(chipButtons) do
		for tag, b in pairs(set) do
			local shown = (tabName == currentTab)
			b.Visible = shown
			if shown then
				local active = false
				for _, t in ipairs(q.tags) do
					if (t.key .. ":" .. t.val) == tag then active = true; break end
				end
				if active then setBtn(b, C.surfaceHi, 0.1, C.text)
				else setBtn(b, C.surface, 0.35, C.textDim) end
			end
		end
	end
end

local CHIP_SETS = {
	Discover = {
		{label = "Free",     tag = "price:free"},
		{label = "Newest",   tag = "sort:newest"},
		{label = "Popular",  tag = "sort:mostfavorited"},
		{label = "Cheapest", tag = "sort:pricelowtohigh"},
		{label = "Featured", tag = "category:featured"},
		{label = "Sale",     tag = "sale:on"},
		{label = "Best sellers", tag = "sort:bestselling"},
		{label = "Priciest", tag = "sort:pricehightolow"},
		{label = "Under 100", tag = "price:0-100"},
		{label = "Under 500", tag = "price:0-500"},
		{label = "100–500", tag = "price:100-500"},
		{label = "Premium", tag = "category:premium"},
		{label = "Collectibles", tag = "category:collectibles"},
		{label = "Community", tag = "category:community"},
		{label = "Recommended", tag = "category:recommended"},
		{label = "This week", tag = "period:week"},
		{label = "This month", tag = "period:month"},
	},
	Saved = {
		{label = "Favorites", tag = "is:favorite"},
		{label = "Newest",    tag = "sort:newest"},
		{label = "Oldest",    tag = "sort:oldest"},
		{label = "A–Z",       tag = "sort:name"},
		{label = "Free",      tag = "is:free"},
		{label = "Cheapest",  tag = "sort:pricelowtohigh"},
		{label = "Offsale",   tag = "is:offsale"},
		{label = "Paid", tag = "is:paid"},
		{label = "Priciest", tag = "sort:pricehightolow"},
	},
	Presets = {
		{label = "Newest", tag = "sort:newest"},
		{label = "Oldest", tag = "sort:oldest"},
		{label = "A–Z",    tag = "sort:name"},
	},
	Recent = {
		{label = "Newest",   tag = "sort:newest"},
		{label = "Oldest",   tag = "sort:oldest"},
		{label = "A–Z",      tag = "sort:name"},
		{label = "Free",     tag = "is:free"},
		{label = "Paid",     tag = "is:paid"},
		{label = "Cheapest", tag = "sort:pricelowtohigh"},
		{label = "Priciest", tag = "sort:pricehightolow"},
		{label = "Offsale", tag = "is:offsale"},
	},
}
for tabName, set in pairs(CHIP_SETS) do
	chipButtons[tabName] = {}
	for i, c in ipairs(set) do
		local b = mkBtn(chipsRow, {
			Text = c.label, AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, CHIP_H),
			TextSize = s(13), LayoutOrder = i, Visible = (tabName == "Discover"),
		}, C.surface, 0.35, C.textDim)
		corner(b, math.floor(CHIP_H / 2))
		pad(b, s(14), s(14))
		chipButtons[tabName][c.tag] = b

		b.MouseButton1Click:Connect(function()
			local q = parseQuery(sb.Text)
			local found = false
			for j = #q.tags, 1, -1 do
				local tg = q.tags[j]
				if (tg.key .. ":" .. tg.val) == c.tag then table.remove(q.tags, j); found = true; break end
			end
			if not found then
				local k, v = c.tag:match("^(%a+):(.+)$")
				if k then
					for j = #q.tags, 1, -1 do
						if q.tags[j].key == k then table.remove(q.tags, j) end
					end
					q.tags[#q.tags + 1] = {key = k, val = v}
				end
			end
			local newText = buildQuery(q)
			sb.Text = newText
			if runSearch then runSearch(newText) end
		end)
	end
end

local applySuggestionRef
local TAG_VALUES = {
	sort = {"relevance", "newest", "mostfavorited", "bestselling", "price_low_to_high", "price_high_to_low"},
	price_min = {"50", "100", "500", "1000"},
	price_max = {"100", "300", "500", "1000"},
	price = {"free", "0-100", "100-500"},
	creator = {"Roblox"},
	sale = {"on", "all"},
	category = {"featured", "collectibles", "community", "premium", "recommended"},
	sales = {"collectibles", "premium", "all"},
	period = {"day", "week", "month", "alltime"},
	limit = {"10", "28", "30", "60", "120"},
	is = {"favorite", "free", "paid", "offsale"},
}
local SORT_SAVED_VALUES = {"newest", "oldest", "name", "price_low_to_high", "price_high_to_low", "favorites"}
local SORT_RECENT_VALUES = {"newest", "oldest", "name"}
local KEYS_DISCOVER = {"sort", "price", "price_min", "price_max", "creator", "sale", "category", "sales", "period", "limit"}
local KEYS_SAVED = {"sort", "price", "price_min", "price_max", "creator", "is"}
local KEYS_PRESETS = {"sort"}
local KEYS_RECENT = {"sort", "price", "price_min", "price_max", "creator", "is"}

local suggestStrip = new("Frame", {
	Name = "Suggest", Position = UDim2.fromOffset(PAD, TOPH - s(34)), Size = UDim2.fromOffset(LW, s(30)),
	BackgroundColor3 = C.overlay, BackgroundTransparency = 0.08, BorderSizePixel = 0, Visible = false, ZIndex = 60,
}, mf)
corner(suggestStrip, s(10)); stroke(suggestStrip, C.stroke, 1, 0.4)
local suggestRow = new("ScrollingFrame", {
	Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 0,
	CanvasSize = UDim2.new(0, 0, 0, 0), AutomaticCanvasSize = Enum.AutomaticSize.X,
	ScrollingDirection = Enum.ScrollingDirection.X, ZIndex = 61,
}, suggestStrip)
new("UIListLayout", {FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, s(5)), SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Center}, suggestRow)
pad(suggestRow, s(5), s(5), 0, 0)

local currentSuggestions = {}

local function getSuggestions()
	local text = sb.Text
	local token = text:match("(%S*)$") or ""
	local before = text:sub(1, #text - #token)
	local lower = token:lower()
	local out = {}

	local key, valPrefix = lower:match("^([%a_]+):(.*)$")
	local origKey = key
	if key then key = KEY_ALIASES[key] or key end
	if key and VALID_KEYS[key] then
		local vals
		if key == "sort" then
			vals = (currentTab == "Discover" and TAG_VALUES.sort) or (currentTab == "Saved" and SORT_SAVED_VALUES) or SORT_RECENT_VALUES
		elseif key == "is" and currentTab == "Recent" then
			vals = {"free", "paid", "offsale"}
		else
			vals = TAG_VALUES[key]
		end
		local normPrefix = ((valPrefix or ""):gsub("_", ""))
		for _, v in ipairs(vals or {}) do
			local normVal = (v:lower():gsub("_", ""))
			if normVal:find(normPrefix, 1, true) == 1 and normVal ~= normPrefix then
				out[#out + 1] = {label = origKey .. ":" .. v, apply = before .. origKey .. ":" .. v .. " ", run = true}
			end
		end
		return out
	end

	local used = {}
	for _, tg in ipairs(parseQuery(text).tags) do used[tg.key] = true end
	local keys = (currentTab == "Discover" and KEYS_DISCOVER) or (currentTab == "Saved" and KEYS_SAVED) or (currentTab == "Presets" and KEYS_PRESETS) or KEYS_RECENT
	for _, k in ipairs(keys) do
		local ak = KEY_ALIASES[k] or k
		if not used[ak] and (lower == "" or k:find(lower, 1, true) == 1) and (k .. ":") ~= lower then
			out[#out + 1] = {label = k .. ":", apply = before .. k .. ":", run = false}
		end
	end

	if #lower >= 2 then
		local seen, count = {}, 0
		local function consider(name)
			if count >= 4 or type(name) ~= "string" then return end
			local ln = name:lower()
			if seen[ln] or ln == lower or not ln:find(lower, 1, true) then return end
			seen[ln] = true
			count += 1
			out[#out + 1] = {label = name, apply = before .. name .. " ", run = true}
		end
		if currentTab == "Presets" then
			for _, v in ipairs(presetList) do consider(v.Name) end
		else
			for _, v in ipairs(historyList) do consider(v.Name) end
			for _, v in pairs(savedBookmarks) do if type(v) == "table" then consider(v.Name) end end
			for _, v in ipairs(searchResults) do consider(v.Name) end
		end
	end

	while #out > 8 do table.remove(out) end
	return out
end

local function refreshSuggestions()
	for _, c in ipairs(suggestRow:GetChildren()) do
		if c:IsA("GuiObject") then c:Destroy() end
	end
	if sb.Text == "" then suggestStrip.Visible = false; return end
	currentSuggestions = getSuggestions()
	if #currentSuggestions == 0 then suggestStrip.Visible = false; return end
	new("TextLabel", {
		Text = "Tab ⇥", LayoutOrder = 0, AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, s(22)),
		BackgroundTransparency = 1, Font = Enum.Font.GothamBold, TextSize = s(12), TextColor3 = C.textMuted, ZIndex = 62,
	}, suggestRow)
	for i, sg in ipairs(currentSuggestions) do
		local b = mkBtn(suggestRow, {
			Text = sg.label, AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, s(22)),
			TextSize = s(13), LayoutOrder = i, ZIndex = 62,
		}, i == 1 and C.surfaceHi or C.surface, i == 1 and 0 or 0.2, i == 1 and C.text or C.textDim)
		corner(b, s(11)); pad(b, s(10), s(10))
		b.MouseButton1Down:Connect(function() applySuggestionRef(sg) end)
	end
	suggestStrip.Visible = true
end

applySuggestionRef = function(sg)
	sb.Text = sg.apply
	if sg.run and runSearch then runSearch(sg.apply) end
	refreshSuggestions()
	task.delay(0.05, function()
		pcall(function()
			sb:CaptureFocus()
			sb.CursorPosition = #sb.Text + 1
		end)
	end)
end

sb:GetPropertyChangedSignal("Text"):Connect(refreshSuggestions)

track(uis.InputBegan:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.Tab and suggestStrip.Visible and currentSuggestions[1] then
		applySuggestionRef(currentSuggestions[1])
	end
end))

local renderPresets, presetPanel, presetEmpty, saveCurrentBtn, hideConfirm, confirmOverlay
do
	local ROW_H = s(54)
	local focusNameId
	local pendingDelete

	presetPanel = new("ScrollingFrame", {
		Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundColor3 = C.panel, BackgroundTransparency = 0.55,
		BorderSizePixel = 0, ScrollBarThickness = s(3), ScrollBarImageColor3 = C.surfaceHi, CanvasSize = UDim2.new(0, 0, 0, 0),
		AutomaticCanvasSize = Enum.AutomaticSize.Y, ScrollingDirection = Enum.ScrollingDirection.Y, Visible = false, ClipsDescendants = true, ZIndex = 2,
	}, left)
	corner(presetPanel, s(12)); stroke(presetPanel, C.stroke, 1, 0.7)
	new("UIListLayout", {Padding = UDim.new(0, s(6)), SortOrder = Enum.SortOrder.LayoutOrder, HorizontalAlignment = Enum.HorizontalAlignment.Center}, presetPanel)
	pad(presetPanel, s(8), s(8), s(8), s(8))

	presetEmpty = new("TextLabel", {
		Position = UDim2.fromOffset(0, GRID_Y), Size = UDim2.fromOffset(LW, GRID_H), BackgroundTransparency = 1, Text = "",
		Font = Enum.Font.GothamSemibold, TextSize = s(15), TextColor3 = C.textMuted, TextWrapped = true, Visible = false, ZIndex = 4,
	}, left)

	saveCurrentBtn = mkBtn(chipsRow, {
		Text = "+ Save current", AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, CHIP_H),
		TextSize = s(13), LayoutOrder = 0, Visible = false,
	}, C.surfaceHi, 0.15, C.text)
	corner(saveCurrentBtn, math.floor(CHIP_H / 2)); pad(saveCurrentBtn, s(14), s(14))

	confirmOverlay = new("TextButton", {
		Size = UDim2.fromScale(1, 1), BackgroundColor3 = C.overlay, BackgroundTransparency = 0.35, BorderSizePixel = 0, Text = "",
		AutoButtonColor = false, Visible = false, Active = true, ZIndex = 90,
	}, mf)
	local DW, DH = s(300), s(152)
	local dlg = new("Frame", {
		AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.fromScale(0.5, 0.5), Size = UDim2.fromOffset(DW, DH),
		BackgroundColor3 = C.panel, BorderSizePixel = 0, Active = true, ZIndex = 91,
	}, confirmOverlay)
	corner(dlg, s(14)); stroke(dlg, C.stroke, 1, 0.2)
	local dlgScale = new("UIScale", {Scale = 1}, dlg)
	new("TextLabel", {
		Position = UDim2.fromOffset(s(16), s(14)), Size = UDim2.fromOffset(DW - s(32), s(22)), BackgroundTransparency = 1,
		Text = "Delete preset?", Font = Enum.Font.GothamBlack, TextSize = s(18), TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, ZIndex = 92,
	}, dlg)
	local confirmMsg = new("TextLabel", {
		Position = UDim2.fromOffset(s(16), s(42)), Size = UDim2.fromOffset(DW - s(32), s(50)), BackgroundTransparency = 1,
		Text = "", Font = Enum.Font.Gotham, TextSize = s(14), TextColor3 = C.textDim, TextWrapped = true,
		TextXAlignment = Enum.TextXAlignment.Left, TextYAlignment = Enum.TextYAlignment.Top, ZIndex = 92,
	}, dlg)
	local btnW = math.floor((DW - s(32) - s(10)) / 2)
	local cancelBtn = mkBtn(dlg, {Text = "Cancel", Size = UDim2.fromOffset(btnW, s(36)), Position = UDim2.fromOffset(s(16), DH - s(16) - s(36)), TextSize = s(14), ZIndex = 92}, C.surfaceHi, 0.1, C.text)
	corner(cancelBtn, s(9))
	local deleteBtn = mkBtn(dlg, {Text = "Delete", Size = UDim2.fromOffset(btnW, s(36)), Position = UDim2.fromOffset(s(16) + btnW + s(10), DH - s(16) - s(36)), TextSize = s(14), ZIndex = 92}, Color3.fromRGB(160, 84, 84), 0, WHITE)
	corner(deleteBtn, s(9))

	hideConfirm = function()
		pendingDelete = nil
		confirmOverlay.Visible = false
	end
	local function showConfirm(p)
		pendingDelete = p
		confirmMsg.Text = 'Do you want to delete this preset?\n"' .. tostring(p.Name) .. '"'
		confirmOverlay.BackgroundTransparency = 1
		confirmOverlay.Visible = true
		dlgScale.Scale = 0.9
		tw(confirmOverlay, 0.15, {BackgroundTransparency = 0.35})
		tw(dlgScale, 0.22, {Scale = 1}, Enum.EasingStyle.Back)
	end
	cancelBtn.MouseButton1Click:Connect(hideConfirm)
	confirmOverlay.MouseButton1Click:Connect(hideConfirm)
	deleteBtn.MouseButton1Click:Connect(function()
		local p = pendingDelete
		hideConfirm()
		if not p then return end
		for i = #presetList, 1, -1 do
			if presetList[i] == p then table.remove(presetList, i) end
		end
		saveJSON(PRESETS_FILE, presetList)
		renderPresets(activeSpec or {})
		toast("Preset deleted")
	end)

	local function snapshotEquipped()
		local anims, any = {}, false
		for slot, list in pairs(equippedAnims) do
			if type(list) == "table" and #list > 0 then
				local copy = {}
				for _, a in ipairs(list) do copy[#copy + 1] = {Name = a.Name, AnimationId = a.AnimationId} end
				anims[slot] = copy
				any = true
			end
		end
		return anims, any
	end

	local function nextPresetName()
		local maxN = 0
		for _, p in ipairs(presetList) do
			local n = tonumber(string.match(p.Name or "", "^[Pp]reset (%d+)$"))
			if n and n > maxN then maxN = n end
		end
		return "Preset " .. (maxN + 1)
	end

	local function summary(p)
		local names, n = {}, 0
		for _, slot in ipairs(buttonOrder) do
			local l = p.Anims and p.Anims[slot]
			if type(l) == "table" and #l > 0 then
				n += 1
				names[#names + 1] = shortNames[slot]
			end
		end
		return n .. (n == 1 and " slot" or " slots") .. "  ·  " .. table.concat(names, " · ")
	end

	local function inspectPreset(p)
		local count = 0
		for _ in pairs(p.Anims or {}) do count += 1 end
		activeBundle = {Id = "preset:" .. tostring(p.Id), Name = p.Name, IsPreset = true, Count = count}
		inspectGen = inspectGen + 1
		masterGen = masterGen + 1
		if masterTrack then pcall(function() masterTrack:Stop(0.1) end); masterTrack = nil end
		table.clear(currentBundleItems)
		hintLabel.Visible = false
		for slot, list in pairs(p.Anims or {}) do
			if type(list) == "table" and #list > 0 then
				currentBundleItems[slot] = {Id = "preset:" .. tostring(p.Id) .. "/" .. slot, Name = p.Name, AssetType = slot, Direct = list}
			end
		end
		refreshActiveInfo()
		updateSelection()
		local slot = currentBundleItems[selectedSlot] and selectedSlot or nil
		if not slot then
			for _, name in ipairs(buttonOrder) do
				if currentBundleItems[name] then slot = name; break end
			end
		end
		if not slot then
			refreshSlotButtons()
			setActionsEnabled(false)
			setPreviewState("missing", selectedSlot)
			return
		end
		selectedSlot = slot
		refreshSlotButtons()
		setActionsEnabled(true)
		masterRig, masterAnimator = nil, nil
		playMaster(slot)
	end

	local function buildRow(p, order)
		local row = new("Frame", {
			Size = UDim2.new(1, 0, 0, ROW_H), BackgroundColor3 = C.surface, BackgroundTransparency = 0.3, BorderSizePixel = 0, LayoutOrder = order,
		}, presetPanel)
		corner(row, s(10)); stroke(row, C.stroke, 1, 0.6)
		local rowHit = new("TextButton", {Size = UDim2.fromScale(1, 1), BackgroundTransparency = 1, Text = "", AutoButtonColor = false, ZIndex = 2}, row)

		local prevW, wearW, renW, delW, g4 = s(56), s(44), s(60), s(52), s(4)
		local blockW = prevW + wearW + renW + delW + g4 * 3 + s(8)
		local yb = math.floor((ROW_H - s(30)) / 2)

		local nameBox = new("TextBox", {
			Position = UDim2.fromOffset(s(8), s(6)), Size = UDim2.new(1, -(blockW + s(14)), 0, s(22)), BackgroundColor3 = C.overlay, BackgroundTransparency = 1,
			BorderSizePixel = 0, Text = p.Name, ClearTextOnFocus = false, Font = Enum.Font.GothamBold, TextSize = s(15), TextColor3 = C.text,
			TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd, ClipsDescendants = true, ZIndex = 3,
		}, row)
		corner(nameBox, s(6)); pad(nameBox, s(4), s(4))
		new("TextLabel", {
			Position = UDim2.fromOffset(s(12), s(30)), Size = UDim2.new(1, -(blockW + s(20)), 0, s(16)), BackgroundTransparency = 1,
			Text = summary(p), Font = Enum.Font.Gotham, TextSize = s(12), TextColor3 = C.textDim, TextXAlignment = Enum.TextXAlignment.Left,
			TextTruncate = Enum.TextTruncate.AtEnd, ZIndex = 3,
		}, row)

		nameBox.Focused:Connect(function()
			tw(nameBox, 0.12, {BackgroundTransparency = 0.25})
		end)
		nameBox.FocusLost:Connect(function()
			tw(nameBox, 0.12, {BackgroundTransparency = 1})
			local newName = nameBox.Text
			newName = newName:gsub("^%s+", "")
			newName = newName:gsub("%s+$", "")
			newName = newName:sub(1, 24)
			if newName == "" then
				nameBox.Text = p.Name
			elseif newName ~= p.Name then
				p.Name = newName
				nameBox.Text = newName
				if activeBundle and activeBundle.IsPreset and activeBundle.Id == "preset:" .. tostring(p.Id) then
					activeBundle.Name = newName
					refreshActiveInfo()
				end
				saveJSON(PRESETS_FILE, presetList)
				toast("Renamed to " .. newName, "ok")
			else
				nameBox.Text = newName
			end
		end)

		local delBtn = mkBtn(row, {Text = "Delete", Size = UDim2.fromOffset(delW, s(30)), AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -s(8), 0, yb), TextSize = s(12), ZIndex = 3}, C.badBg, 0.1, C.badTx)
		corner(delBtn, s(8))
		local renBtn = mkBtn(row, {Text = "Rename", Size = UDim2.fromOffset(renW, s(30)), AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -(s(8) + delW + g4), 0, yb), TextSize = s(12), ZIndex = 3}, C.surfaceHi, 0.2, C.text)
		corner(renBtn, s(8))
		local wearBtn = mkBtn(row, {Text = "Wear", Size = UDim2.fromOffset(wearW, s(30)), AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -(s(8) + delW + renW + g4 * 2), 0, yb), TextSize = s(12), ZIndex = 3}, C.surfaceHi, 0.1, C.text)
		corner(wearBtn, s(8))
		local previewBtn = mkBtn(row, {Text = "Preview", Size = UDim2.fromOffset(prevW, s(30)), AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -(s(8) + delW + renW + wearW + g4 * 3), 0, yb), TextSize = s(12), ZIndex = 3}, C.surfaceHi, 0.1, C.text)
		corner(previewBtn, s(8))
		previewBtn.MouseButton1Click:Connect(function() inspectPreset(p) end)
		rowHit.MouseButton1Click:Connect(function() inspectPreset(p) end)

		renBtn.MouseButton1Click:Connect(function()
			task.delay(0.03, function()
				pcall(function()
					nameBox:CaptureFocus()
					nameBox.SelectionStart = 1
					nameBox.CursorPosition = #nameBox.Text + 1
				end)
			end)
		end)
		delBtn.MouseButton1Click:Connect(function() showConfirm(p) end)
		local busy = false
		wearBtn.MouseButton1Click:Connect(function()
			if busy then return end
			if not lp.Character then toast("No character found", "bad"); return end
			busy = true
			wearBtn.Text = "…"
			task.spawn(function()
				local n = equipMap(lp.Character, p.Anims or {}, true)
				toast(n > 0 and ("Equipped " .. p.Name) or "Couldn't equip that preset", n > 0 and "ok" or "bad")
				if wearBtn.Parent then wearBtn.Text = "Wear" end
				busy = false
			end)
		end)

		if focusNameId == p.Id then
			focusNameId = nil
			task.delay(0.12, function()
				pcall(function()
					nameBox:CaptureFocus()
					nameBox.SelectionStart = 1
					nameBox.CursorPosition = #nameBox.Text + 1
				end)
			end)
		end
	end

	renderPresets = function(spec)
		spec = spec or {}
		for _, c in ipairs(presetPanel:GetChildren()) do
			if c:IsA("Frame") then c:Destroy() end
		end
		local kw = string.lower(spec.keyword or "")
		local list = {}
		for _, p in ipairs(presetList) do
			if kw == "" or string.find(string.lower(p.Name or ""), kw, 1, true) then list[#list + 1] = p end
		end
		local mode = spec.sort or "new"
		if mode == "name" then
			table.sort(list, function(a, b) return string.lower(a.Name or "") < string.lower(b.Name or "") end)
		elseif mode ~= "old" then
			local rev = {}
			for i = #list, 1, -1 do rev[#rev + 1] = list[i] end
			list = rev
		end
		for i, p in ipairs(list) do buildRow(p, i) end
		pageLbl.Text = #list .. (#list == 1 and " preset" or " presets")
		if #presetList == 0 then
			presetEmpty.Text = "No presets yet\nWear some animations, then tap\n“+ Save current”"
			presetEmpty.Visible = true
		elseif #list == 0 then
			presetEmpty.Text = "No presets match"
			presetEmpty.Visible = true
		else
			presetEmpty.Visible = false
		end
	end

	saveCurrentBtn.MouseButton1Click:Connect(function()
		local anims, any = snapshotEquipped()
		if not any then toast("Nothing equipped yet. Wear an animation first", "bad"); return end
		local p = {Id = tostring(os.time()) .. tostring(math.random(1000, 9999)), Name = nextPresetName(), Anims = anims, Time = os.time()}
		presetList[#presetList + 1] = p
		saveJSON(PRESETS_FILE, presetList)
		focusNameId = p.Id
		sb.Text = ""
		if runSearch then runSearch("") end
		toast("Saved as " .. p.Name .. ". Type to rename", "ok")
	end)
end

refreshSavedView = function()
	if currentTab ~= "Saved" then return end
	if runSearch then runSearch(sb.Text) end
end

runSearch = function(text)
	searchGen += 1
	local myGen = searchGen

	local q = parseQuery(text or "")
	local spec = compileQuery(currentTab, q)
	activeSpec = spec
	currentPageIndex = 1

	refreshTags(q, spec)

	local isPresets = (currentTab == "Presets")
	gridHolder.Visible = not isPresets
	presetPanel.Visible = isPresets
	saveCurrentBtn.Visible = isPresets
	prevBtn.Visible = not isPresets
	nextBtn.Visible = not isPresets
	if isPresets then
		clearGrid()
		clearSkeletons()
		gridMsg.Visible = false
		renderPresets(spec)
		return
	end
	presetEmpty.Visible = false

	if currentTab ~= "Discover" then
		local function buildAndDraw()
			savedTabList = (currentTab == "Saved") and buildSavedList(spec) or buildHistoryList(spec)
			drawGridPage(savedTabList)
		end
		local needPrice = spec.min ~= nil or spec.max ~= nil or spec.is == "free" or spec.is == "paid" or spec.is == "offsale" or spec.sort == "low" or spec.sort == "high"
		if needPrice then
			local src = {}
			if currentTab == "Saved" then
				for _, v in pairs(savedBookmarks) do if type(v) == "table" and v.Id then src[#src + 1] = v end end
			else
				for _, v in ipairs(historyList) do src[#src + 1] = v end
			end
			task.spawn(function()
				resolveUnknownPrices(src)
				if myGen ~= searchGen then return end
				buildAndDraw()
			end)
		else
			buildAndDraw()
		end
		return
	end

	searchResults = {}
	catalogCursor = nil
	showSkeletons()
	pageLbl.Text = "Searching…"

	task.spawn(function()
		local params = buildParams(spec)
		local ok, pages = pcall(function() return aes:SearchCatalog(params) end)
		if myGen ~= searchGen then return end
		if not ok or not pages then
			showGridMessage("Search failed — try again")
			pageLbl.Text = "Error"
			return
		end
		catalogCursor = pages
		local filtered = {}
		local fetched = 0
		local wantPrice = spec.min ~= nil or spec.max ~= nil
		while true do
			local page = catalogCursor:GetCurrentPage()
			fetched += 1
			if wantPrice then resolveUnknownPrices(page) end
			local sawOverMax = false
			for _, entry in ipairs(page) do
				if passesClientFilter(entry, spec) then
					filtered[#filtered + 1] = entry
				elseif wantPrice and spec.max == 0 and spec.sort == nil then
					local mt = seedMeta(entry)
					if mt and mt.kind == "paid" and (mt.amount or 0) > 0 then sawOverMax = true end
				end
			end
			if myGen ~= searchGen then return end
			if #filtered > 0 or not wantPrice or sawOverMax or fetched >= 6 or catalogCursor.IsFinished then break end
			local okAdv = pcall(function() catalogCursor:AdvanceToNextPageAsync() end)
			if not okAdv or myGen ~= searchGen then break end
		end
		if myGen ~= searchGen then return end
		searchResults = filtered
		if #filtered == 0 then
			showGridMessage("No bundles matched your search")
			updatePager(0)
		else
			drawGridPage(searchResults)
		end
	end)
end

local function setTab(tab)
	if currentTab == tab then return end
	currentTab = tab
	local order = {Discover = 0, Saved = 1, Recent = 2, Presets = 3}
	local btns = {Discover = tabDiscover, Saved = tabSaved, Recent = tabRecent, Presets = tabPresets}
	for name, b in pairs(btns) do
		setBtn(b, C.surface, 1, name == tab and C.text or C.textMuted)
	end
	tw(tabIndicator, 0.2, {Position = UDim2.new(order[tab] / 4, s(order[tab] == 0 and 2 or 1), 0, s(2))})
	runSearch(sb.Text)
	refreshSuggestions()
end

tabDiscover.MouseButton1Click:Connect(function() setTab("Discover") end)
tabSaved.MouseButton1Click:Connect(function() setTab("Saved") end)
tabRecent.MouseButton1Click:Connect(function() setTab("Recent") end)
tabPresets.MouseButton1Click:Connect(function() setTab("Presets") end)
onHistoryChanged = function()
	if currentTab == "Recent" and runSearch then runSearch(sb.Text) end
end

searchBtn.MouseButton1Click:Connect(function() runSearch(sb.Text) end)

sb.FocusLost:Connect(function(enter)
	if not enter then return end
	if suggestStrip.Visible and currentSuggestions[1] then
		local cur = (sb.Text:gsub("%s+$", ""))
		local lastToken = cur:match("(%S*)$") or ""
		if lastToken:find(":", 1, true) then
			local sg = currentSuggestions[1]
			local ap = (sg.apply:gsub("%s+$", ""))
			if ap ~= cur and ap:sub(1, #cur) == cur then
				sb.Text = sg.apply
				if sg.run then runSearch(sg.apply) end
				return
			end
		end
	end
	runSearch(sb.Text)
end)

sb:GetPropertyChangedSignal("Text"):Connect(function()
	clearBtn.Visible = sb.Text ~= ""
end)
clearBtn.MouseButton1Click:Connect(function()
	sb.Text = ""
	runSearch("")
end)

prevBtn.MouseButton1Click:Connect(function()
	if pagingBusy or currentPageIndex <= 1 then return end
	currentPageIndex -= 1
	drawGridPage(currentTab ~= "Discover" and savedTabList or searchResults)
end)

nextBtn.MouseButton1Click:Connect(function()
	if pagingBusy then return end
	local list = currentTab ~= "Discover" and savedTabList or searchResults
	local pages = math.max(1, math.ceil(#list / PER_PAGE))
	local more = currentTab == "Discover" and catalogCursor ~= nil and not catalogCursor.IsFinished
	if currentPageIndex < pages then
		currentPageIndex += 1
		drawGridPage(list)
	elseif more then
		pagingBusy = true
		pageLbl.Text = "Loading…"
		task.spawn(function()
			local ok = pcall(function() catalogCursor:AdvanceToNextPageAsync() end)
			pagingBusy = false
			if not ok then toast("Couldn't load more results", "bad"); return end
			local spec = activeSpec or {}
			local before = #searchResults
			local tries = 0
			while true do
				local page = catalogCursor:GetCurrentPage()
				if spec.min ~= nil or spec.max ~= nil then resolveUnknownPrices(page) end
				for _, e in ipairs(page) do
					if passesClientFilter(e, spec) then searchResults[#searchResults + 1] = e end
				end
				tries += 1
				if #searchResults > before or catalogCursor.IsFinished or tries >= 5 then break end
				if not pcall(function() catalogCursor:AdvanceToNextPageAsync() end) then break end
			end
			if #searchResults == before then
				toast("No more matching results", "bad")
				pageLbl.Text = "Page " .. currentPageIndex
				drawGridPage(searchResults)
				return
			end
			currentPageIndex += 1
			drawGridPage(searchResults)
		end)
	end
end)

local helpOpen = false
helpBtn.MouseButton1Click:Connect(function()
	helpOpen = not helpOpen
    settingsPanel.Visible = false
	helpPanel.Visible = true
	if helpOpen then
		tw(helpPanel, 0.2, {BackgroundTransparency = 0.15})
		tw(helpText, 0.22, {TextTransparency = 0})
		setBtn(helpBtn, C.surfaceHi, 0.1, C.text)
        setBtn(settingsBtn, C.surface, 0.2, C.textDim)
	else
		tw(helpPanel, 0.18, {BackgroundTransparency = 1})
		tw(helpText, 0.18, {TextTransparency = 1})
		task.delay(0.2, function()
			if not helpOpen then helpPanel.Visible = false end
		end)
		setBtn(helpBtn, C.surface, 0.25, C.textDim)
	end
end)
pad(helpScroll, s(14), s(14), s(10), s(10))

local settingsOpen = false
settingsBtn.MouseButton1Click:Connect(function()
    settingsOpen = not settingsOpen
    helpPanel.Visible = false
    settingsPanel.Visible = true
    if settingsOpen then
        tw(settingsPanel, 0.2, {BackgroundTransparency = 0.15})
        setBtn(settingsBtn, C.surfaceHi, 0.1, C.text)
        setBtn(helpBtn, C.surface, 0.25, C.textDim)
        helpOpen = false
    else
        tw(settingsPanel, 0.18, {BackgroundTransparency = 1})
        task.delay(0.2, function()
            if not settingsOpen then settingsPanel.Visible = false end
        end)
        setBtn(settingsBtn, C.surface, 0.2, C.textDim)
    end
end)

local introTargets = nil

local function hideToPill()
	introTargets = collectFade(mf)
	for _, tg in ipairs(introTargets) do
		local props
		if tg.obj.BackgroundTransparency < 1 then props = {BackgroundTransparency = 1} end
		if tg.txtT and tg.obj.TextTransparency < 1 then
			props = props or {}
			props.TextTransparency = 1
		end
		if props then tw(tg.obj, 0.2, props) end
	end
	task.delay(0.24, function()
		mf.Visible = false
		pill.Visible = true
		pill.Size = UDim2.fromOffset(0, 0)
		tw(pill, 0.22, {Size = UDim2.fromOffset(s(58), s(58))}, Enum.EasingStyle.Back)
	end)
end

local function showFromPill()
	pill.Visible = false
	mf.Visible = true
	local targets = introTargets or collectFade(mf)
	for _, tg in ipairs(targets) do
		if tg.obj.Parent then
			local props
			if tg.obj.BackgroundTransparency ~= tg.bgT then props = {BackgroundTransparency = tg.bgT} end
			if tg.txtT and tg.obj.TextTransparency ~= tg.txtT then
				props = props or {}
				props.TextTransparency = tg.txtT
			end
			if props then tw(tg.obj, 0.28, props) end
		end
	end
end

closeBtn.MouseButton1Click:Connect(hideToPill)
minBtn.MouseButton1Click:Connect(hideToPill)
pill.MouseButton1Click:Connect(showFromPill)

local dragging, dragStart, startPos
top.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
		dragging = true
		dragStart = input.Position
		startPos = mf.Position
	end
end)
track(uis.InputChanged:Connect(function(input)
	if not dragging then return end
	if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then
		local delta = input.Position - dragStart
		mf.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y)
	end
end))
track(uis.InputEnded:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
		dragging = false
	end
end))

track(uis.InputBegan:Connect(function(input, gameProcessed)
	if gameProcessed then return end
	if input.KeyCode == Enum.KeyCode.RightShift then
		if mf.Visible then hideToPill() else showFromPill() end
	elseif input.KeyCode == Enum.KeyCode.Escape and confirmOverlay.Visible then
		hideConfirm()
	elseif input.KeyCode == Enum.KeyCode.Escape and helpOpen then
		helpOpen = false
		helpPanel.Visible = false
		helpText.TextTransparency = 1
		helpPanel.BackgroundTransparency = 1
		setBtn(helpBtn, C.surface, 0.25, C.textDim)
    elseif input.KeyCode == Enum.KeyCode.Escape and settingsOpen then
        settingsOpen = false
        settingsPanel.Visible = false
        settingsPanel.BackgroundTransparency = 1
        setBtn(settingsBtn, C.surface, 0.2, C.textDim)
	end
end))

env.UgcAnimStudioCleanup = function()
	for _, c in ipairs(connections) do pcall(function() c:Disconnect() end) end
	table.clear(connections)
	for obj in pairs(spinning) do spinning[obj] = nil end
	for obj, d in pairs(shimmers) do
		pcall(function() d[2]:Cancel() end)
		pcall(function() d[1]:Destroy() end)
		shimmers[obj] = nil
	end
	if masterTrack then pcall(function() masterTrack:Stop(0) end) end
	tweenShutdown()
	pcall(function() g:Destroy() end)
end

runSearch("")

task.spawn(function()
    task.wait(1)
    if scriptSettings.restoreLastPack and lastPackData.Id then
        wearBundle(lastPackData.Id, lastPackData.Name)
    end
end)

env.UgcAnimStudio = {gui = g, main = mf}

end)

if not uiOk then
	warn("[UGC Anim Studio] BUILD ERROR: " .. tostring(uiErr))
	dbg("BUILD ERROR: " .. tostring(uiErr))
else
	dbg("ui built successfully")

	pcall(function()
		local mf2 = env.UgcAnimStudio and env.UgcAnimStudio.main
		if not mf2 then return end

		local us = Instance.new("UIScale")
		us.Scale = 0.86
		us.Parent = mf2

		local origPos = mf2.Position
		mf2.Position = origPos + UDim2.fromOffset(0, 26)

		local targets = collectFade(mf2)

		for _, tg in ipairs(targets) do
			if tg.bgT < 1 then tg.obj.BackgroundTransparency = 1 end
			if tg.txtT and tg.txtT < 1 then tg.obj.TextTransparency = 1 end
		end

		tw(mf2, 0.55, {Position = origPos}, Enum.EasingStyle.Quint)
		tw(us, 0.7, {Scale = 1}, Enum.EasingStyle.Back)

		for _, tg in ipairs(targets) do
			local props
			if tg.bgT < 1 then props = {BackgroundTransparency = tg.bgT} end
			if tg.txtT and tg.txtT < 1 then
				props = props or {}
				props.TextTransparency = tg.txtT
			end
			if props then tw(tg.obj, 0.55, props, Enum.EasingStyle.Quint) end
		end
	end)
end

Features

  • Fe animation script 😛
  • Features:
  • R6 Detection
  • Advance Animation speed
  • Bla bla bla
Animation studio · RoScripts