local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))
local Menu = require(script.Parent:WaitForChild("Menu"))
type Menu = typeof(Menu.new())
type MenuInstance = typeof(Menu.new()._instance)

local DEFAULT = {
	Adornee = false,
	RightAlign = false,
	Value = Color3.new(1, 0, 0),
	Visible = false,
}

type Properties = typeof(DEFAULT) & TextButton

local rainbowSequence = ColorSequence.new({
	ColorSequenceKeypoint.new(0, Color3.fromRGB(255, 0, 0)),
	ColorSequenceKeypoint.new(1 / 6, Color3.fromRGB(255, 255, 0)),
	ColorSequenceKeypoint.new(1 / 3, Color3.fromRGB(0, 255, 0)),
	ColorSequenceKeypoint.new(0.5, Color3.fromRGB(0, 255, 255)),
	ColorSequenceKeypoint.new(2 / 3, Color3.fromRGB(0, 0, 255)),
	ColorSequenceKeypoint.new(5 / 6, Color3.fromRGB(255, 0, 255)),
	ColorSequenceKeypoint.new(1.0, Color3.fromRGB(255, 0, 0)),
})

local hexInputSize = UI.compute(function()
	local fontSize = UI.Theme.FontSize()
	local padding = UI.Theme.Padding().Offset
	return UDim2.fromOffset(math.max(80, 3.5 * fontSize + padding), fontSize + padding)
end)

local hueSliderSize = UI.compute(function()
	local padding = UI.Theme.Padding().Offset
	local height = UI.Theme.FontSize() + padding
	return UDim2.new(0, height, 0, UI.Theme.FontSize() + hexInputSize().X.Offset + padding * 2)
end)

local svSize = UI.compute(function()
	local size = hexInputSize().X.Offset
	return UDim2.fromOffset(size, size)
end)

local inputColor = UI.compute(function()
	return if UI.getLuminance(UI.Theme.Primary()) > 0.5 then Color3.new() else Color3.new(1, 1, 1)
end)

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local cleanup = {}

	local dragging = UI.state(false)
	table.insert(cleanup, dragging)

	local hue = 0
	local hueSlider = UI.new "Slider" {
		Name = "HueSlider",
		Vertical = true,
		Size = hueSliderSize,
		Value = function()
			local h, s, v = self.Value():ToHSV()
			if h == 1 and hue == 0 then
				h = 0
			end
			hue = if s > 0 and v > 0 then h else hue
			return hue
		end,

		UI.new "Frame" {
			Name = "Rainbow",
			BackgroundColor3 = Color3.new(1, 1, 1),
			AnchorPoint = Vector2.new(0.5, 0.5),
			Size = UDim2.new(1, 0, 1, 0),
			Position = UDim2.new(0.5, 0, 0.5, 0),

			UI.new "UICorner" {
				CornerRadius = UI.Theme.CornerPadded,
			},
			UI.new "Stroke" {},
			UI.new "UIGradient" {
				Color = rainbowSequence,
				Rotation = 90,
			},
		},

		[UI.Hook] = {
			Value = function(value)
				if value == hue then
					return
				end

				local _, s, v = self.Value._value:ToHSV()
				local color = Color3.fromHSV(value, s, v)
				hue = value
				self.Value(color, true)
			end,
		},
	}
	self.hueSlider = hueSlider

	local hueColor = UI.compute(function()
		return Color3.fromHSV(hueSlider.Value(), 1, 1)
	end)
	UI.edit(hueSlider._slider, {
		BackgroundColor3 = hueColor,
		UI.new "UIStroke" {
			ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
			Color = Color3.new(1, 1, 1),
			Thickness = 1.5,
		},
	})
	hueSlider._instance:FindFirstChild("Filled"):Destroy()

	local hexInput = UI.new "Input" {
		FontFace = UI.Theme.FontMono,
		BackgroundColor3 = inputColor,
		TextColor3 = inputColor,
		Size = hexInputSize,
		Padding = UDim.new(0, 5),
		Validate = function(value)
			return pcall(Color3.fromHex, value)
		end,
		Value = function()
			return `#{self.Value():ToHex()}`
		end,

		[UI.Hook] = {
			Value = function(value)
				if value == `#{self.Value():ToHex()}` then
					return
				end
				self.Value(Color3.fromHex(value), true)
			end,
		},
	}
	self.hexInput = hexInput

	local sv, dragInput
	local sX = UI.state(0)
	local function update(input)
		if not dragInput then
			return
		end

		local x = math.clamp((input.Position.X - sv.AbsolutePosition.X - 8) / (sv.AbsoluteSize.X - 16), 0, 1)
		local y = 1 - math.clamp((input.Position.Y - sv.AbsolutePosition.Y - 8) / (sv.AbsoluteSize.Y - 16), 0, 1)
		sX(x, true)

		local color = Color3.fromHSV(hue, x, y)
		self.Value(color, true)
	end

	local function inputBegan(input)
		if
			input.UserInputType == Enum.UserInputType.MouseButton1
			or input.UserInputType == Enum.UserInputType.Touch
		then
			UI.Sound.Hover03:Play()
			dragInput = input
			dragging(true)
			local con
			con = input:GetPropertyChangedSignal("UserInputState"):Connect(function()
				if input.UserInputState == Enum.UserInputState.End then
					con:Disconnect()
					if dragInput == input then
						UI.Sound.Hover01:Play()
						dragInput = nil
						dragging(false)
					end
				end
			end)
			update(input)
		end
	end

	table.insert(cleanup, UI.UserInputService.InputChanged:Connect(update))

	sv = UI.new "Frame" {
		Name = "SaturationValue",
		BackgroundColor3 = hueColor,
		Size = svSize,
		SizeConstraint = Enum.SizeConstraint.RelativeYY,

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},
		UI.new "Stroke" {},
		UI.new "Frame" {
			Name = "Saturation",
			BackgroundColor3 = Color3.new(1, 1, 1),
			Size = UDim2.new(1, 0, 1, 0),

			UI.new "UICorner" {
				CornerRadius = UI.Theme.CornerPadded2,
			},
			UI.new "UIGradient" {
				Color = ColorSequence.new(Color3.new(1, 1, 1)),
				Transparency = NumberSequence.new(0, 1),
			},
		},
		UI.new "Frame" {
			Name = "Value",
			BackgroundColor3 = Color3.new(0, 0, 0),
			Size = UDim2.new(1, 0, 1, 0),
			ZIndex = 2,

			UI.new "UICorner" {
				CornerRadius = UI.Theme.CornerPadded2,
			},
			UI.new "UIGradient" {
				Color = ColorSequence.new(Color3.new()),
				Transparency = NumberSequence.new(0, 1),
				Rotation = -90,
			},
		},
		UI.new "Frame" {
			Name = "PickerContainer",
			BackgroundTransparency = 1,
			Size = UDim2.new(1, 0, 1, 0),
			ZIndex = 3,

			UI.new "UIPadding" {
				PaddingLeft = UDim.new(0, 8),
				PaddingRight = UDim.new(0, 8),
				PaddingTop = UDim.new(0, 8),
				PaddingBottom = UDim.new(0, 8),
			},
			UI.new "Frame" {
				Name = "Picker",
				AnchorPoint = Vector2.new(0.5, 0.5),
				BackgroundColor3 = self.Value,
				Size = UI.tween(function()
					return if dragging() then UDim2.new(0, 16, 0, 16) else UDim2.new(0, 8, 0, 8)
				end, UI.Theme.TweenOut),
				Position = function()
					local _, s, v = self.Value():ToHSV()
					local x = sX()
					return UDim2.new(if v == 0 then x else s, 0, 1 - v, 0)
				end,

				UI.new "UICorner" {
					CornerRadius = UI.Theme.CornerDiameter,
				},
				UI.new "UIStroke" {
					ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
					Color = Color3.new(1, 1, 1),
					Thickness = 1.5,
				},

				InputBegan = inputBegan,
			},
		},

		InputBegan = inputBegan,
	}
	self.sv = sv

	self._menu = UI.new "Menu" {
		Name = "ColorPicker",
		Adornee = self.Adornee,
		RightAlign = self.RightAlign,
		AutomaticSize = Enum.AutomaticSize.XY,
		Size = UDim2.new(),
		Visible = self.Visible,

		UI.new "UIPadding" {
			PaddingLeft = UI.Theme.Padding,
			PaddingRight = UI.Theme.Padding,
			PaddingTop = UI.Theme.Padding,
			PaddingBottom = UI.Theme.Padding,
		},

		hueSlider,
		UI.new "Frame" {
			BackgroundTransparency = 1,
			AutomaticSize = Enum.AutomaticSize.XY,

			UI.new "UIListLayout" {
				SortOrder = Enum.SortOrder.LayoutOrder,
				FillDirection = Enum.FillDirection.Vertical,
				Padding = UI.Theme.Padding,
			},

			sv,
			hexInput,
		},
		[UI.Clean] = cleanup,
	} :: Menu
	self._menu._list.FillDirection = Enum.FillDirection.Horizontal
	self._menu._content.AutomaticSize = Enum.AutomaticSize.XY
	UI.edit(self._menu._list, { Padding = UI.Theme.Padding })
	self._instance = self._menu._instance :: MenuInstance

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	AnchorPoint = Vector2.new(0.5, 0.5),
	Position = UDim2.new(0.5, 0, 0.5, 0),
	Text = "",
	TextSize = UI.Theme.FontSize,
	TextColor3 = UI.Theme.PrimaryText,
	TextStrokeColor3 = UI.Theme.Primary,
	TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
	TextXAlignment = Enum.TextXAlignment.Left,
	Title = "",
	TitleSize = UI.Theme.FontSizeLargest,
	TitleTextXAlignment = Enum.TextXAlignment.Left,
	Activated = false,
	ActionText = UI.Nil,
	ActionTextSize = UI.Theme.FontSize,
	Action = UI.Nil,
	Duration = UI.Nil,
	Draggable = false,
	LeftAction = UI.Nil,
	RightAction = UI.Nil,
	ExitButton = false,
	Close = UI.Function,
	Selectable = false,
	Visible = true,
	Width = 256,
}

type Properties = typeof(DEFAULT) & TextButton

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

-- alert, simple, confirmation dialogs
-- allow timed dialogs using announce design, fire an event when duration ends so cleanup can be handled dynamically?
-- maximum of 2 actions
function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local dragInput, dragStart, startOffset
	local dragging = UI.state(false)
	self._dragging = dragging

	local function dragUpdate(input)
		if not dragInput then
			return
		end

		local delta = input.Position - dragStart
		local size = self._instance.AbsoluteSize
		local offset = size * self._instance.AnchorPoint
		local min = self._instance.Parent.AbsolutePosition + offset
		local max = self._instance.Parent.AbsoluteSize - (size - offset)

		self.Position(
			UDim2.fromOffset(
				math.clamp(math.round(startOffset.X + delta.X), min.X, math.max(offset.X, max.X)),
				math.clamp(math.round(startOffset.Y + delta.Y), math.max(offset.Y, min.Y), math.max(offset.Y, max.Y))
			),
			true
		)
	end

	local function dragInputBegan(input, gameProcessed)
		if gameProcessed or not self.Draggable._value then
			return
		end
		if
			input.UserInputType == Enum.UserInputType.MouseButton1
			or input.UserInputType == Enum.UserInputType.Touch
		then
			startOffset = (self._instance.AbsolutePosition + self._instance.AbsoluteSize * self._instance.AnchorPoint)
				- self._instance.Parent.AbsolutePosition
			dragInput, dragStart = input, input.Position
			dragging(true)
			local con
			con = input:GetPropertyChangedSignal("UserInputState"):Connect(function()
				if input.UserInputState == Enum.UserInputState.End then
					con:Disconnect()
					if dragInput == input then
						dragInput = nil
						dragging(false)
					end
				end
			end)
		end
	end

	local con
	local function activate(active)
		if UI.raw(self.Activated) then
			return
		end
		self.Activated(true)
		self.Action(active, true)

		if con then
			con:Disconnect()
		end
		if self.Close and type(self.Close._value) == "function" then
			self.Close._value(self)
		else
			self:Destroy()
		end
	end

	local progress = UI.new "Frame" {
		ZIndex = -1,
		Name = "Progress",
		BackgroundTransparency = 1,
		AnchorPoint = Vector2.new(0, 1),
		Position = UDim2.fromScale(0, 1),
		Size = UDim2.new(1, 0, 0, 4),
		ClipsDescendants = true,
		Visible = if self.Duration and self.Duration._value > 0 then true else false,

		UI.new "Frame" {
			Name = "Bar",
			AnchorPoint = Vector2.new(0, 1),
			BackgroundColor3 = UI.Theme.Secondary,
			Position = UDim2.fromScale(0, 1),
			Size = UDim2.new(),
			UI.new "UICorner" {
				CornerRadius = UI.Theme.CornerRadius,
			},
		},
	}

	local hovering = UI.state(false)
	self._hovering = hovering

	if self.Duration and self.Duration._value > 0 then
		local duration = self.Duration._value
		local elapsed = 0
		con = game:GetService("RunService").PreRender:Connect(function(dt: number)
			if hovering._value then
				return
			end

			elapsed += dt
			local alpha = elapsed / duration
			progress.Size = UDim2.new(math.clamp(1 - alpha, 0, 1), 0, 0, 4)
			if alpha >= 1 then
				con:Disconnect()
				if self.Close and type(self.Close._value) == "function" then
					self.Close._value(self)
				else
					self:Destroy()
				end
			end
		end)
	end

	local buttonSize = UI.compute(function()
		return UDim2.new(0, UI.Theme.FontSizeLargest(), 0, UI.Theme.FontSizeLargest())
	end)
	self._buttonSize = buttonSize

	local exitButton = UI.new "Button" {
		LayoutOrder = 9,
		AnchorPoint = Vector2.new(1, 0),
		BackgroundColor3 = Color3.fromRGB(200, 0, 0),
		BackgroundTransparency = 1,
		HoverTransparency = 0,
		Position = UDim2.new(1, 0, 0, 0),
		Size = buttonSize,
		Icon = UI.Theme.Image.Close,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
			Size = UDim2.fromScale(0.5, 0.5),
		},
		Text = "",
		Activated = function()
			if self.Close and type(self.Close._value) == "function" then
				self.Close._value(self)
			else
				self:Destroy()
			end
		end,
		Visible = function()
			return if self.ExitButton() then true else false
		end,
	}
	exitButton._instance:FindFirstChildOfClass("UIStroke"):Destroy()
	self._exitButton = exitButton

	self._content = UI.new "Frame" {
		Name = "UIContent",
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.fromScale(1, 0),

		UI.new "UIListLayout" {
			SortOrder = Enum.SortOrder.LayoutOrder,
			Padding = UI.Theme.Padding,
			HorizontalFlex = Enum.UIFlexAlignment.Fill,
		},

		UI.new "TextButton" {
			Name = "Title",
			AutoLocalize = false,
			AutomaticSize = Enum.AutomaticSize.XY,
			BackgroundTransparency = 1,
			Text = "",
			Visible = function()
				local title = self.Title()
				return title and title ~= ""
			end,

			UI.new "UIListLayout" {
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UI.Theme.Padding,
				FillDirection = Enum.FillDirection.Horizontal,
			},

			UI.new "TextLabel" {
				AutoLocalize = false,
				BackgroundTransparency = 1,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				Text = self.Title,
				TextSize = self.TitleSize,
				TextTruncate = Enum.TextTruncate.SplitWord,
				TextXAlignment = self.TitleTextXAlignment,
				FontFace = UI.Theme.FontHeavy,
				RichText = true,

				AutomaticSize = Enum.AutomaticSize.XY,
				UI.new "UIFlexItem" {
					FlexMode = Enum.UIFlexMode.Fill,
				},
			},

			if UI.raw(self.Title) then exitButton else nil,

			InputBegan = dragInputBegan,
		},

		UI.new "TextBox" {
			AutoLocalize = false,
			Name = "Text",
			BackgroundTransparency = 1,
			ClearTextOnFocus = false,
			Interactable = self.Selectable,
			TextEditable = false,
			TextColor3 = self.TextColor3,
			Text = self.Text,
			TextSize = self.TextSize,
			TextWrapped = true,
			TextXAlignment = self.TextXAlignment,
			FontFace = UI.Theme.Font,
			RichText = true,
			Visible = function()
				local text = self.Text()
				return text and text ~= ""
			end,

			AutomaticSize = Enum.AutomaticSize.XY,

			if UI.raw(self.Title) then nil else exitButton,
		},
	}

	self._instance = UI.new "ImageButton" {
		Name = "Dialog",
		Active = false,
		AutoButtonColor = false,
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundColor3 = function()
			return if UI.Theme.BackgroundGradientEnabled() then Color3.new(1, 1, 1) else UI.Theme.Primary()
		end,
		BackgroundTransparency = UI.Theme.Transparency,
		Image = UI.Theme.BackgroundImage,
		ImageTransparency = UI.Theme.BackgroundImageTransparency,
		ScaleType = UI.Theme.BackgroundImageScaleType,
		ClipsDescendants = true,
		AnchorPoint = self.AnchorPoint,
		Position = self.Position,
		Size = function()
			return UDim2.fromOffset(self.Width(), 0)
		end,
		Visible = self.Visible,

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerRadius,
		},
		UI.new "Gradient" {},
		UI.new "Stroke" {},
		UI.new "UIListLayout" {
			SortOrder = Enum.SortOrder.LayoutOrder,
		},

		UI.new "Frame" {
			AutomaticSize = Enum.AutomaticSize.Y,
			BackgroundTransparency = 1,
			Size = UDim2.new(1, 0, 0, 0),

			UI.new "UIPadding" {
				PaddingLeft = UI.Theme.Padding,
				PaddingRight = UI.Theme.Padding,
				PaddingTop = UI.Theme.Padding,
				PaddingBottom = UI.Theme.Padding,
			},
			UI.new "UIListLayout" {
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UI.Theme.Padding,
			},

			self._content,
			if self.LeftAction or self.RightAction
				then UI.new "Frame" {
					Name = "Actions",
					AutomaticSize = Enum.AutomaticSize.Y,
					BackgroundTransparency = 1,

					Size = UDim2.new(1, 0, 0, 0),

					UI.new "UIListLayout" {
						SortOrder = Enum.SortOrder.LayoutOrder,
						FillDirection = Enum.FillDirection.Horizontal,
						HorizontalAlignment = Enum.HorizontalAlignment.Right,
						VerticalAlignment = Enum.VerticalAlignment.Center,
						VerticalFlex = Enum.UIFlexAlignment.Fill,
						Padding = UI.Theme.Padding,
					},

					UI.new "TextLabel" {
						AutoLocalize = false,
						Name = "Text",
						BackgroundTransparency = 1,
						TextColor3 = UI.Theme.PrimaryText,
						TextStrokeColor3 = UI.Theme.Primary,
						TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
						Text = self.ActionText,
						TextSize = self.ActionTextSize,
						TextWrapped = true,
						TextTruncate = Enum.TextTruncate.SplitWord,
						TextXAlignment = "Left",
						FontFace = UI.Theme.FontBold,
						RichText = true,
						Visible = function()
							local text = self.ActionText()
							return text and text ~= ""
						end,

						AutomaticSize = Enum.AutomaticSize.XY,
					},
					UI.new "Spacer" {},
					UI.new "Button" {
						Name = "LeftAction",
						AutomaticSize = Enum.AutomaticSize.None,
						BackgroundTransparency = 1,
						Size = buttonSize,
						Icon = UI.Theme.Image.Close_Bold,
						IconProperties = {
							ImageColor3 = UI.Theme.Invalid,
							Size = UDim2.fromScale(0.5, 0.5),
						},
						Text = "",
						Activated = function()
							activate(false)
						end,
					},
					UI.new "Button" {
						Name = "RightAction",
						AutomaticSize = Enum.AutomaticSize.None,
						BackgroundTransparency = 1,
						Size = buttonSize,
						Icon = UI.Theme.Image.Check_Bold,
						IconProperties = {
							ImageColor3 = UI.Theme.Valid,
							Size = UDim2.fromScale(0.625, 0.625),
						},
						Text = "",
						Activated = function()
							activate(true)
						end,
					},
				}
				else nil,
		},
		progress,

		InputChanged = function(input, processed)
			if processed then
				return
			end
			if
				input.UserInputType == Enum.UserInputType.MouseMovement
				or input.UserInputType == Enum.UserInputType.Touch
			then
				hovering(true)
			end
		end,
		inputEnded = function(input)
			if
				input.UserInputType == Enum.UserInputType.MouseMovement
				or input.UserInputType == Enum.UserInputType.Touch
			then
				hovering(false)
			end
		end,

		[UI.Clean] = {
			UI.UserInputService.InputChanged:Connect(function(input)
				if not (self.Draggable._value and self._instance.Visible) then
					return
				end
				if
					input.UserInputType == Enum.UserInputType.MouseMovement
					or input.UserInputType == Enum.UserInputType.Touch
				then
					if dragging._value then
						dragUpdate(input)
					end
				end
			end),
			hovering,
		},
	} :: TextButton

	local sizeState = UI.state(self._instance, "AbsoluteSize")
	self.sizeState = sizeState

	UI.edit(progress.Bar, {
		Size = function()
			local size = sizeState()
			return UDim2.fromOffset(size.X, size.Y)
		end,
	})

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	Enabled = UI.Theme.BackgroundGradientEnabled,
	Start = UI.Theme.BackgroundGradient,
	Stop = UI.Theme.BackgroundGradientStop,
	Rotation = UI.Theme.BackgroundGradientAngle,
}

type Properties = typeof(DEFAULT) & UIGradient

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	self._instance = UI.new "UIGradient" {
		Color = function()
			return ColorSequence.new(self.Start(), self.Stop())
		end,
		Enabled = self.Enabled,
		Rotation = self.Rotation,
	} :: UIGradient

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local ContentProvider = game:GetService("ContentProvider")
local TextChatService = game:GetService("TextChatService")
local UserInputService = game:GetService("UserInputService")
local SoundService = game:GetService("SoundService")

local ChatInputBarConfiguration = TextChatService:FindFirstChildOfClass("ChatInputBarConfiguration")

local module = {
	enabled = true,
	everyTextBox = true,
}

local clacks = "rbxassetid://136112106699293"
task.spawn(ContentProvider.PreloadAsync, ContentProvider, { clacks })

local lastHash, lastKey
local random = Random.new()

function module.sound(keycode)
	local keyHash = keycode.Value % 8
	local sound = Instance.new("Sound")
	sound.Volume = random:NextNumber(1.5, 2)
	if keyHash == lastHash and keycode ~= lastKey then
		sound.PlaybackSpeed = random:NextNumber(0.9, 1.1)
	end
	sound.SoundId = clacks
	sound.PlaybackRegionsEnabled = true
	sound.PlaybackRegion = NumberRange.new(keyHash * 0.3, keyHash * 0.3 + 0.15)
	sound.Parent = SoundService
	sound:Play()
	task.delay(1, sound.Destroy, sound)
	lastHash, lastKey = keyHash, keycode
end

function module.registerAll()
	module.registerAll = nil :: any
	local focused, textBoxFocused = {}, false
	UserInputService.TextBoxFocused:Connect(function(textbox)
		if not (textbox and textbox.TextEditable) then
			return
		end
		focused[textbox] = true
		textBoxFocused = textbox
	end)

	UserInputService.TextBoxFocusReleased:Connect(function(textbox)
		if not (textbox and textbox.TextEditable) then
			return
		end
		focused[textbox] = nil
		if next(focused) then
			return
		end
		textBoxFocused = nil
	end)

	UserInputService.InputBegan:Connect(function(input, gameProcessedEvent)
		if
			not module.enabled
			or not (textBoxFocused or ChatInputBarConfiguration.IsFocused)
			or input.KeyCode == Enum.KeyCode.Unknown
		then
			return
		end
		if module.everyTextBox or (textBoxFocused and textBoxFocused:HasTag("KeyClackSound")) then
			task.spawn(module.sound, input.KeyCode)
		end
	end)
	return module
end

return module


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))
local Clack = require(script.Clack).registerAll()

Clack.enabled = UI.raw(UI.Theme.TypingSounds)
Clack.everyTextBox = UI.raw(UI.Theme.TypingSoundsOnEveryTextBox)
UI.Theme.TypingSounds:Connect(function(value)
	Clack.enabled = value and UI.Theme.SoundEnabled._value
end)
UI.Theme.TypingSoundsOnEveryTextBox:Connect(function(value)
	Clack.everyTextBox = value
end)

local function stripNewLines(text: string)
	return string.gsub(string.gsub(text, "\n", " "), "\r", " ")
end

-- NOTE: only works for single line inputs
-- TODO: fallback for multiline
local function bytePositionToWidth(text, size, font, richText, bytePosition)
	if bytePosition == 1 then
		return 0
	end

	local params = Instance.new("GetTextBoundsParams")
	params.Font = font
	params.RichText = richText
	params.Size = size
	params.Text = string.sub(text, 1, bytePosition - 1)
	params.Width = math.huge

	return math.ceil(UI.retryGetTextBoundsAsync(params).X)
end

local function updateSelection(selection, display, input, inputOffset, cursorWidth)
	cursorWidth = cursorWidth
		or bytePositionToWidth(input.Text, display.TextSize, display.FontFace, display.RichText, input.CursorPosition)
	local selectWidth =
		bytePositionToWidth(input.Text, display.TextSize, display.FontFace, display.RichText, input.SelectionStart)
	local deltaWidth = cursorWidth - selectWidth
	selection.Size = UDim2.fromOffset(deltaWidth, display.TextSize)
	selection.Position = UDim2.new(0, selectWidth - inputOffset._value, 0.5, 0)
	selection.Visible = true
end

local function updateCursor(cursor, display, input, inputMask, inputOffset, selection)
	local cursorWidth =
		bytePositionToWidth(input.Text, display.TextSize, display.FontFace, display.RichText, input.CursorPosition)
	local visibleWidth = cursor.Parent.AbsoluteSize.X
	local cursorOffset = cursorWidth - inputOffset._value

	local margin = if (display.TextBounds.X > visibleWidth or inputOffset._value > 0) and cursorWidth > 32
		then 32
		else 0
	if cursorOffset > visibleWidth then
		inputOffset(math.min(display.TextBounds.X, inputOffset._value + cursorOffset - visibleWidth))
		cursorOffset = visibleWidth
	elseif cursorOffset < margin then
		inputOffset(inputOffset._value + cursorOffset - margin)
		if margin == 0 or inputOffset._value <= display.TextSize / 2 then
			inputOffset(math.min(0, inputOffset._value))
		end
		cursorOffset = margin
	end

	input.Position = UDim2.fromOffset(-inputOffset._value, 0)
	cursor.Position = UDim2.fromOffset(cursorOffset, 1)
	cursor.Visible = true

	if input.SelectionStart ~= -1 then
		updateSelection(selection, display, input, inputOffset, cursorWidth)
	end
end

local MODIFIERS = {
	Enum.KeyCode.LeftControl,
	Enum.KeyCode.RightControl,
	Enum.KeyCode.LeftShift,
	Enum.KeyCode.RightShift,
	Enum.KeyCode.LeftAlt,
	Enum.KeyCode.RightAlt,
	Enum.KeyCode.LeftMeta,
	Enum.KeyCode.RightMeta,
	Enum.KeyCode.LeftSuper,
	Enum.KeyCode.RightSuper,
}

local DEFAULT = {
	Cursor = "_",
	DisplayText = "",
	Placeholder = "",
	Value = "",
	ClearTextButton = false,
	--- Fills to the size of the text input.
	Fill = false,
	FontFace = UI.Theme.Font,
	FontSize = UI.Theme.FontSize,
	TextColor3 = UI.Theme.PrimaryText,
	TextStrokeColor3 = UI.Theme.Primary,
	TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
	Padding = UI.Theme.Padding,
	Icon = "",
	IconProperties = {},
	IconRightAlign = false,
	HotkeyInput = false,
	Hotkey = UI.Nil,
	Modifiers = UI.Nil,
	MaxChars = math.huge,
	NumberOnly = false,
	IntegerOnly = false,
	NumberRange = NumberRange.new(0, 1),
	Validate = UI.Function,
}

type Properties = typeof(DEFAULT) & TextButton

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

-- TODO: send/clear button
function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)
	self.IconProperties = {}

	local cleanup = {}
	local textState = UI.state(UI.raw(self.Value))

	local inputSize = function()
		local fill = self.Fill()
		return UDim2.new(if fill then 1 else 0, if fill then 0 else self.FontSize(), 1, 0)
	end

	local displayLabel = UI.new "TextLabel" {
		AutomaticSize = Enum.AutomaticSize.X,
		AutoLocalize = false,
		Size = inputSize,
		BackgroundTransparency = 1,
		TextSize = self.FontSize,
		TextColor3 = self.TextColor3,
		FontFace = self.FontFace,
		RichText = true,
		TextXAlignment = Enum.TextXAlignment.Left,
		Text = function()
			local display = self.DisplayText()
			local placeholder = self.Placeholder()
			local input = textState()
			local maxChars = self.MaxChars()

			local text = if display ~= "" then display elseif input ~= "" then input else placeholder
			text = stripNewLines(text)

			return if maxChars ~= math.huge then string.sub(text, 1, maxChars :: number) else text
		end,
		TextTransparency = function()
			return if textState() == "" and self.DisplayText() == "" then 0.5 else 0
		end,
	}
	self._displayLabel = displayLabel

	self._input = UI.new "TextBox" {
		AutomaticSize = Enum.AutomaticSize.X,
		AutoLocalize = false,
		ClearTextOnFocus = false,
		Size = inputSize,
		BackgroundTransparency = 1,
		TextTransparency = 1,
		TextSize = self.FontSize,
		Text = self.Value,
		FontFace = self.FontFace,
		RichText = true,
		TextXAlignment = Enum.TextXAlignment.Left,
		CursorPosition = -1,

		displayLabel,

		FocusLost = function()
			local text = self._input.Text
			local numberOnly = self.NumberOnly._value
			local integerOnly = self.IntegerOnly._value

			if text == "" and (numberOnly or integerOnly) then
				self.Value(self.NumberRange._value.Min)
				return
			end

			if (numberOnly or integerOnly) and text ~= "" then
				local number = tonumber(text)
				if not number or (integerOnly and string.find(text :: string, "%D")) then
					self._input.Text = self.Value._value
					return
				end
				local numberClamped = math.clamp(number, self.NumberRange._value.Min, self.NumberRange._value.Max)
				text = if number == numberClamped then self._input.Text else numberClamped
				self._input.Text = text
				local textLength = #tostring(text)
				if self._input.CursorPosition - 1 > textLength then
					self._input.CursorPosition = textLength + 1
				end
			end

			if self.Hotkey and self.Hotkey._value then
				local key = UI.UserInputService:GetStringForKeyCode(self.Hotkey._value)
				if key ~= string.sub(text, -1) then
					text ..= key
				end
			end

			if self.MaxChars._value ~= math.huge then
				text = string.sub(text, 1, self.MaxChars._value)
				self._input.CursorPosition = math.min(self.MaxChars._value + 1, self._input.CursorPosition)
				self._input.Text = text
			end

			if self.Validate and not self.Validate._value(text) then
				self._input.Text = if self.MaxChars._value ~= math.huge
					then string.sub(self.Value._value, 1, self.MaxChars._value)
					else self.Value._value
				return
			end

			self.Value(text)
		end,
	}
	self._input:AddTag("KeyClackSound")

	if self.HotkeyInput._value then
		table.insert(
			cleanup,
			self._input.Focused:Connect(function()
				self.Value("")
			end)
		)
		UI.UserInputService.InputBegan:Connect(function(input, gameProcessed)
			if not self._input:IsFocused() then
				return
			end
			if input.UserInputType == Enum.UserInputType.Keyboard then
				local mods = {}
				mods.Ctrl = input:IsModifierKeyDown(Enum.ModifierKey.Ctrl)
				mods.Shift = input:IsModifierKeyDown(Enum.ModifierKey.Shift)
				mods.Alt = input:IsModifierKeyDown(Enum.ModifierKey.Alt)
				mods.Meta = input:IsModifierKeyDown(Enum.ModifierKey.Meta)

				local hotkeyText =
					`{mods.Ctrl and "Ctrl+" or ""}{mods.Shift and "Shift+" or ""}{mods.Alt and "Alt+" or ""}{mods.Meta and "Meta+" or ""}`
				if table.find(MODIFIERS, input.KeyCode) then
					self.Value(hotkeyText)
					return
				end

				self.Hotkey(input.KeyCode)
				self.Modifiers(mods)
				self.Value(hotkeyText .. UI.UserInputService:GetStringForKeyCode(input.KeyCode), true)
				self._input:ReleaseFocus(true)
			end
		end)
	end

	local selection = UI.new "Frame" {
		Name = "Selection",
		AnchorPoint = Vector2.new(0, 0.5),
		BackgroundTransparency = 0.75,
		Size = UDim2.new(0, 0, 0, self.FontSize),
		Position = UDim2.new(0, 0, 0.5, 0),
		BackgroundColor3 = UI.Theme.Secondary,
		BorderSizePixel = 0,
		Visible = false,
	}

	local cursor = UI.new "TextLabel" {
		Name = "Cursor",
		AutoLocalize = false,
		AutomaticSize = Enum.AutomaticSize.X,
		Size = UDim2.new(0, 0, 1, 0),
		Position = UDim2.new(0, 0, 0, 0),
		BackgroundTransparency = 1,
		TextSize = self.FontSize,
		TextColor3 = UI.Theme.PrimaryText,
		TextStrokeColor3 = UI.Theme.Primary,
		TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
		FontFace = self.FontFace,
		RichText = true,
		TextXAlignment = Enum.TextXAlignment.Left,
		Text = self.Cursor,
		Visible = false,
	}
	local lastCursorChange = 0
	task.defer(function() -- blinking cursor
		while cursor and cursor.Parent do
			if cursor.Visible and tick() - lastCursorChange > 0.25 then
				cursor.TextTransparency = if cursor.TextTransparency == 1 then 0 else 1
			end
			task.wait(0.5)
		end
	end)

	local automaticFill = function()
		return if self.Fill() then Enum.AutomaticSize.None else Enum.AutomaticSize.X
	end

	local inputMask = UI.new "Frame" {
		Name = "InputMask",
		AutomaticSize = automaticFill,
		ClipsDescendants = true,
		BackgroundTransparency = 1,
		Position = UDim2.new(0, 0, 0, 0),
		Size = UDim2.new(1, 0, 1, 0),

		self._input,
		selection,
	}

	self._inputOffset = UI.state(0)
	self._maskSize = UI.state(inputMask, "AbsoluteSize")
	self._bounds = UI.state(self._input, "TextBounds")

	local isUpdating = false
	self._input:GetPropertyChangedSignal("Text"):Connect(function()
		if isUpdating then
			return
		end
		isUpdating = true

		local text = self._input.Text
		text = if self.MaxChars._value ~= math.huge then string.sub(text, 1, self.MaxChars._value) else text
		self._input.Text = text
		textState(text, true)

		isUpdating = false
	end)

	UI.edit(self._input, {
		Position = function()
			local xOffset = if self._bounds().X > self._maskSize().X then -self._inputOffset() else 0
			return UDim2.fromOffset(xOffset, 0)
		end,
	})

	local icon = UI.new "ImageLabel" {
		LayoutOrder = 1,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		SizeConstraint = Enum.SizeConstraint.RelativeYY,
		Image = self.Icon,
		ImageColor3 = UI.Theme.Secondary,
		Visible = function()
			return self.Icon() ~= ""
		end,
	}
	if properties and properties.IconProperties then
		UI.edit(icon, properties.IconProperties)
	end

	self._instance = UI.new "Frame" {
		Name = "Input",
		AutomaticSize = automaticFill,
		ClipsDescendants = true,
		BackgroundColor3 = UI.Theme.Secondary,
		BackgroundTransparency = UI.Theme.TransparencyHeavy,
		Size = function()
			return UDim2.new(1, 0, 0, self.FontSize() + self.Padding().Offset * 2)
		end,

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},
		UI.new "Stroke" {},
		UI.new "UIPadding" {
			PaddingLeft = self.Padding,
			PaddingRight = self.Padding,
		},
		UI.new "UIListLayout" {
			VerticalAlignment = Enum.VerticalAlignment.Center,
			FillDirection = Enum.FillDirection.Horizontal,
			SortOrder = Enum.SortOrder.LayoutOrder,
			Padding = self.Padding,
		},

		icon,

		UI.new "Frame" {
			LayoutOrder = 2,
			AutomaticSize = Enum.AutomaticSize.X,
			Name = "InputFrame",
			BackgroundTransparency = 1,
			Size = UDim2.new(0, 0, 1, 0),

			cursor,
			inputMask,
			UI.new "UIFlexItem" {
				FlexMode = Enum.UIFlexMode.Fill,
			},
		},

		UI.new "ImageButton" {
			LayoutOrder = 3,
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(0.5, 0.5),
			SizeConstraint = Enum.SizeConstraint.RelativeYY,
			Image = UI.Theme.Image.Close,
			ImageColor3 = UI.Theme.Secondary,
			Visible = function()
				return self.ClearTextButton() and textState() ~= ""
			end,

			Activated = function()
				self._input.Text = ""
				self.DisplayText("", true)
				self.Value("", true)
				textState("", true)
			end,
		},

		[UI.Clean] = cleanup,
	}

	table.insert(
		cleanup,
		self._input:GetPropertyChangedSignal("SelectionStart"):Connect(function()
			if self._input.SelectionStart == -1 then
				selection.Visible = false
			else
				updateSelection(selection, displayLabel, self._input, self._inputOffset)
			end
		end)
	)

	table.insert(
		cleanup,
		self._input:GetPropertyChangedSignal("CursorPosition"):Connect(function()
			lastCursorChange = tick()
			cursor.TextTransparency = 0
			if self._input.CursorPosition == -1 then
				cursor.Visible = false
			else
				-- defer for text to register
				task.defer(updateCursor, cursor, displayLabel, self._input, inputMask, self._inputOffset, selection)
			end
		end)
	)

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	Tooltip = UI.Nil,
}

type Properties = typeof(DEFAULT) & TextBox

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class:_updateSelection()
	local instance = self._instance
	if instance:IsFocused() and (instance.SelectionStart ~= 1 or instance.CursorPosition ~= #instance.Text + 1) then
		instance.SelectionStart = 1
		instance.CursorPosition = #instance.Text + 1
	end
end

function Class:Select()
	self._instance:CaptureFocus()
	self:_updateSelection()
end

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local function update()
		self:_updateSelection()
	end

	local cleanup = {}

	self._instance = UI.new "TextBox" {
		AutoLocalize = false,
		AutomaticSize = Enum.AutomaticSize.X,
		BackgroundColor3 = UI.Theme.Secondary,
		BackgroundTransparency = 0.25,
		FontFace = UI.Theme.FontMono,
		TextSize = UI.Theme.FontSize,
		TextColor3 = UI.Theme.SecondaryText,
		TextStrokeColor3 = UI.Theme.Secondary,
		TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
		TextEditable = false,
		ClearTextOnFocus = false,

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},
		UI.new "UIPadding" {
			PaddingLeft = UI.Theme.Padding,
			PaddingRight = UI.Theme.Padding,
		},

		Focused = update,
		FocusLost = function()
			self._instance.SelectionStart = -1
			self._instance.CursorPosition = -1
		end,

		[UI.Clean] = cleanup,
		[UI.Event] = {
			CursorPosition = update,
			SelectionStart = update,
		},
	} :: TextBox & { UICorner: UICorner, UIPadding: UIPadding, UISizeConstraint: UISizeConstraint }

	local richText = UI.state(self._instance, "RichText")
	local text = UI.state(self._instance, "Text")

	UI.new "UISizeConstraint" {
		Parent = self._instance,
		MinSize = function()
			local params = UI.new "GetTextBoundsParams" {
				Width = math.huge,
				Font = properties and properties.FontFace or UI.Theme.FontMono,
				Size = properties and properties.TextSize or UI.Theme.FontSize,
				RichText = richText,
				Text = text,
			}

			return Vector2.new(
				UI.retryGetTextBoundsAsync(params).X + UI.Theme.Padding().Offset * 2,
				UI.Theme.FontSize + UI.Theme.Padding().Offset * 2
			)
		end,
		MaxSize = Vector2.new(math.huge, math.huge),
	}

	if UI.UserInputService.KeyboardEnabled or self.Tooltip then
		local hover = UI.state(false)
		self._tooltip = UI.new "Tooltip" {
			Parent = self._instance,
			FontFace = UI.Theme.FontMono,
			Hovering = hover,
			Text = self.Tooltip or "<b>Copy:</b> Ctrl + C",
		}
		table.insert(cleanup, self._tooltip)
		table.insert(
			cleanup,
			self._instance.Focused:Connect(function()
				hover(true)
			end)
		)
		table.insert(
			cleanup,
			self._instance.FocusLost:Connect(function()
				hover(false)
			end)
		)
	end

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	Collapsible = false,
	Collapsed = false,
	Label = "",
	Padding = UI.Theme.Padding,
}

type Properties = typeof(DEFAULT) & Frame

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local cleanup = {}

	local listLayout = UI.new "UIListLayout" {
		SortOrder = Enum.SortOrder.LayoutOrder,
		Padding = self.Padding,
		HorizontalAlignment = Enum.HorizontalAlignment.Left,
		VerticalAlignment = Enum.VerticalAlignment.Top,
	}

	local contentSize = UI.state(listLayout, "AbsoluteContentSize")
	table.insert(cleanup, contentSize)

	local headerTransparency = UI.tween(function()
		return if self.Collapsed() then 0.5 else 0
	end, UI.Theme.TweenOut)
	table.insert(cleanup, headerTransparency)

	self._instance = UI.new "Frame" {
		Name = "List",
		BackgroundColor3 = UI.Theme.Secondary,
		BackgroundTransparency = UI.Theme.TransparencyMax,
		ClipsDescendants = true,
		Size = UI.tween(function()
			local fontSize, padding = UI.Theme.FontSize(), UI.Theme.Padding().Offset
			local height = if self.Collapsed()
				then fontSize + padding * 4
				else contentSize().Y + (if self.Label() ~= "" then fontSize + padding * 6 else padding * 2)
			return UDim2.new(1, 0, 0, height)
		end, UI.Theme.TweenOut),

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},
		UI.new "Stroke" { Transparency = UI.Theme.TransparencyBalanced },

		UI.new "TextButton" {
			AutoLocalize = false,
			Name = "Header",
			Active = true,
			AutomaticSize = Enum.AutomaticSize.Y,
			BackgroundTransparency = 1,
			TextTransparency = 1,
			Text = "",
			Size = UDim2.new(1, 0, 0, 0),
			Visible = function()
				return self.Collapsible() or self.Label() ~= ""
			end,

			UI.new "UIPadding" {
				PaddingLeft = self.Padding,
				PaddingRight = self.Padding,
				PaddingTop = self.Padding,
				PaddingBottom = self.Padding,
			},

			UI.new "ImageLabel" {
				Name = "Arrow",
				BackgroundTransparency = 1,
				AnchorPoint = Vector2.new(0, 0.5),
				Position = UDim2.new(0, 0, 0.5, 0),
				Size = UDim2.new(0.75, 0, 0.75, 0),
				Image = UI.Theme.Image.Down,
				ImageColor3 = UI.Theme.PrimaryText,
				ImageTransparency = headerTransparency,
				Visible = self.Collapsible,
				Rotation = UI.tween(function()
					return if self.Collapsed() then -90 else 0
				end, UI.Theme.TweenOut),

				UI.new "UIAspectRatioConstraint" {},
			},

			UI.new "TextLabel" {
				AutoLocalize = false,
				Name = "Label",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Position = function()
					return UDim2.new(0, if self.Collapsible() then UI.Theme.FontSize() else UI.Theme.Padding(), 0, 0)
				end,
				Size = function()
					return UDim2.new(1, -UI.Theme.FontSize(), 0, 0)
				end,
				FontFace = UI.Theme.FontBold,
				Text = function()
					return string.upper(self.Label() :: string)
				end,
				TextSize = UI.Theme.FontSize,
				TextColor3 = UI.Theme.PrimaryText,
				TextStrokeColor3 = UI.Theme.Primary,
				TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
				TextTransparency = headerTransparency,
				TextTruncate = Enum.TextTruncate.SplitWord,
				TextXAlignment = "Left",
			},

			Activated = function()
				if self.Collapsible._value then
					self.Collapsed(not self.Collapsed._value)
					if self.Collapsed._value then
						UI.Sound.Hover01:Play()
					else
						UI.Sound.Hover03:Play()
					end
				end
			end,
		},
		[UI.Clean] = cleanup,
	} :: Frame & { UIListLayout: UIListLayout, UIPadding: UIPadding }

	self._content = UI.new "Frame" {
		Name = "UIContent",
		Parent = self._instance,
		BackgroundTransparency = 1,
		Position = function()
			local padding = self.Padding().Offset
			return UDim2.new(0, 0, 0, (if self.Label() ~= "" then UI.Theme.FontSize + padding * 2 else padding))
		end,
		Size = UI.tween(function()
			local collapse = self.Collapsed()
			return UDim2.new(
				1,
				0,
				if collapse then 0 else 1,
				if collapse then 0 else -UI.Theme.FontSize + self.Padding().Offset * 2
			)
		end, UI.Theme.TweenOut),
		ClipsDescendants = true,

		listLayout,
		UI.new "UIPadding" {
			PaddingLeft = self.Padding,
			PaddingRight = self.Padding,
			PaddingTop = UDim.new(0, 1),
			PaddingBottom = UDim.new(0, 1),
		},
	} :: Frame & {
		UICorner: UICorner,
		UIStroke: UIStroke,
		Header: TextButton & {
			Arrow: ImageLabel & { UIAspectRatioConstraint: UIAspectRatioConstraint },
			Label: TextLabel,
			UIPadding: UIPadding,
		},
	}

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	Text = "ListItem",
	ContentAutomaticSize = true,
	ContentListLayout = true,
	Tooltip = UI.Nil,
}

type Properties = typeof(DEFAULT) & Frame

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local cleanup = {}

	self._label = UI.new "TextLabel" {
		AutoLocalize = false,
		AutomaticSize = Enum.AutomaticSize.XY,
		AnchorPoint = Vector2.new(0, 0.5),
		BackgroundTransparency = 1,
		Position = UDim2.fromScale(0, 0.5),
		Size = UDim2.new(0, 0, 0, 0),
		FontFace = UI.Theme.Font,
		RichText = true,
		Text = self.Text,
		TextColor3 = UI.Theme.PrimaryText,
		TextSize = UI.Theme.FontSize,
		TextStrokeColor3 = UI.Theme.Primary,
		TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
		TextXAlignment = Enum.TextXAlignment.Left,
	} :: TextLabel

	local labelSize = UI.state(self._label, "AbsoluteSize")
	local automaticSize = UI.compute(function()
		return self.ContentAutomaticSize() and Enum.AutomaticSize.Y or Enum.AutomaticSize.None
	end)
	table.insert(cleanup, labelSize)
	table.insert(cleanup, automaticSize)

	self._content = UI.new "Frame" {
		Name = "UIContent",
		AnchorPoint = Vector2.new(1, 0),
		AutomaticSize = automaticSize,
		Size = function()
			return UDim2.new(1, -labelSize().X - UI.Theme.Padding().Offset, self.ContentAutomaticSize() and 0 or 1, 0)
		end,
		Position = UDim2.new(1, 0, 0, 0),
		BackgroundTransparency = 1,

		UI.new "UIPadding" {
			PaddingRight = UDim.new(0, 1),
		},
	} :: Frame & { UIPadding: UIPadding }

	local layout = UI.new "UIListLayout" {
		Parent = self._content,
		HorizontalAlignment = Enum.HorizontalAlignment.Right,
		VerticalAlignment = Enum.VerticalAlignment.Center,
		FillDirection = Enum.FillDirection.Horizontal,
		SortOrder = Enum.SortOrder.LayoutOrder,
		Padding = UI.Theme.Padding,
	}

	self._layoutParentCompute = UI.compute(function()
		layout.Parent = if self.ContentListLayout() then self._content else nil
	end)

	self._instance = UI.new "Frame" {
		Name = self.Text._value,
		AutomaticSize = automaticSize,
		BackgroundTransparency = 1,
		Size = function()
			return UDim2.new(1, 0, 0, self.ContentAutomaticSize() and 0 or labelSize().Y + UI.Theme.Padding().Offset)
		end,
		self._label,
		self._content,
		if self.Tooltip then UI.new "Tooltip" { Text = self.Tooltip } else nil,
		[UI.Clean] = cleanup,
	} :: Frame & { TextLabel: TextLabel, UIContent: typeof(self._content) }

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	Adornee = false,
	RightAlign = false,
	Visible = false,
}

type Properties = typeof(DEFAULT) & TextButton

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

local LayerTopSize = UI.state(UI.LayerTop, "AbsoluteSize")

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local cleanup = {}

	self._list = UI.new "UIListLayout" {
		SortOrder = Enum.SortOrder.LayoutOrder,
		FillDirection = Enum.FillDirection.Vertical,
		VerticalAlignment = Enum.VerticalAlignment.Top,
	}

	local listContentSize = UI.state(self._list, "AbsoluteContentSize")
	table.insert(cleanup, listContentSize)

	self._content = UI.new "ScrollingFrame" {
		Name = "Menu",
		ZIndex = 100,
		BackgroundTransparency = 1,
		ClipsDescendants = false,
		Size = UDim2.new(1, 0, 1, 0),
		CanvasSize = function()
			return UDim2.new(0, 0, 0, listContentSize().Y)
		end,
		AutomaticCanvasSize = Enum.AutomaticSize.Y,
		ScrollBarThickness = 0,

		self._list,

		[UI.Event] = {
			CanvasPosition = function()
				UI.clearState("hover")
			end,
		},
	} :: ScrollingFrame & { UIListLayout: UIListLayout }

	self._instance = UI.new "TextButton" {
		Name = "Menu",
		ZIndex = 100,
		Visible = self.Visible,
		Parent = UI.LayerTop,
		AnchorPoint = function()
			return if self.RightAlign() then Vector2.new(1, 0) else Vector2.zero
		end,
		BackgroundColor3 = UI.Theme.Primary,
		ClipsDescendants = true,

		AutoButtonColor = false,
		Text = "",

		self._content,
		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerRadius,
		},
		UI.new "Stroke" {},

		[UI.Clean] = cleanup,
	} :: TextButton & { UIContent: typeof(self._content), UICorner: UICorner, UIStroke: UIStroke }

	local positionState = UI.state(Vector2.zero)
	local sizeState = UI.state(Vector2.zero)
	local menuSize = UI.state(self._instance, "AbsoluteSize")
	table.insert(cleanup, positionState)
	table.insert(cleanup, sizeState)
	table.insert(cleanup, menuSize)

	local menuPosition = UI.compute(function()
		local size = menuSize()
		local layerSize = LayerTopSize()
		local absolutePos = positionState()
		local absoluteSize = sizeState()
		local rightAlign = self.RightAlign()
		local padding = UI.Theme.Padding().Offset
		local height = UI.TopbarInset().Height

		local xPos = absolutePos.X + if rightAlign then absoluteSize.X else 0
		local yPos = absolutePos.Y + absoluteSize.Y + padding + height

		return UDim2.fromOffset(
			if rightAlign
				then math.clamp(xPos, size.X, math.max(size.X, layerSize.X))
				else math.clamp(xPos, 0, math.max(0, layerSize.X - size.X)),
			math.clamp(yPos, 0, math.max(0, layerSize.Y - size.Y))
		)
	end)

	UI.edit(self._instance, {
		Position = menuPosition,
	})

	table.insert(
		cleanup,
		UI.UserInputService.InputBegan:Connect(function(input, gameProcessed)
			if
				(
					input.UserInputType == Enum.UserInputType.MouseButton1
					or input.UserInputType == Enum.UserInputType.Touch
					or input.UserInputType == Enum.UserInputType.TextInput
				)
				and self.Visible._value
				and not UI.pointInGuiObject(input.Position.X, input.Position.Y, self.Adornee._value)
				and not UI.pointInGuiObject(input.Position.X, input.Position.Y, self._instance)
			then
				UI.deactivateState(self.Visible, "floating")
			end
		end)
	)

	table.insert(
		cleanup,
		self._instance:GetPropertyChangedSignal("Visible"):Connect(function()
			local adornee = self.Adornee._value
			if self._instance.Visible and typeof(adornee) == "Instance" and adornee:IsA("GuiObject") then
				positionState(adornee.AbsolutePosition, true)
				sizeState(adornee.AbsoluteSize, true)
				positionState:_bindToProperty(adornee, "AbsolutePosition")
				sizeState:_bindToProperty(adornee, "AbsoluteSize")
			else
				if positionState._bindConnection then
					positionState._bindConnection:Disconnect()
				end
				if sizeState._bindConnection then
					sizeState._bindConnection:Disconnect()
				end
			end
		end)
	)

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {}

type Properties = typeof(DEFAULT) & ScrollingFrame

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	self._instance = UI.new "ScrollingFrame" {
		Name = "Scroller",
		BorderSizePixel = 0,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		ScrollBarThickness = 8,
		ScrollBarImageColor3 = UI.Theme.Secondary,
		ScrollBarImageTransparency = UI.Theme.TransparencyClamped,
		TopImage = UI.Theme.ScrollTopImage,
		MidImage = UI.Theme.ScrollMidImage,
		BottomImage = UI.Theme.ScrollBottomImage,
		CanvasSize = UDim2.new(0, 0, 0, 0),
		AutomaticCanvasSize = Enum.AutomaticSize.Y,
		VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,

		UI.new "UIListLayout" { SortOrder = Enum.SortOrder.LayoutOrder, Padding = UI.Theme.Padding },

		[UI.Event] = {
			CanvasPosition = UI.clearActiveStates,
		},
	} :: ScrollingFrame & { UIListLayout: UIListLayout, UIPadding: UIPadding }

	local padding = UI.new "UIPadding" {
		Parent = self._instance,
		PaddingBottom = UI.Theme.PaddingWithStroke,
		PaddingTop = UI.Theme.PaddingWithStroke,
		PaddingLeft = UI.Theme.PaddingStroke,
		PaddingRight = UI.Theme.PaddingWithStroke,
	}

	local function updatePadding()
		UI.edit(padding, {
			PaddingRight = if self._instance.AbsoluteCanvasSize.Y > self._instance.AbsoluteWindowSize.Y
				then UI.Theme.PaddingWithStroke
				else UDim.new(),
		})
	end
	updatePadding()

	UI.edit(padding, {
		[UI.Clean] = {
			self._instance:GetPropertyChangedSignal("AbsoluteWindowSize"):Connect(updatePadding),
			self._instance:GetPropertyChangedSignal("AbsoluteCanvasSize"):Connect(updatePadding),
		},
	})

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local RunService = game:GetService("RunService")
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local UI = require(script.Parent.Parent) :: any

local Defer = {}
do
	local defaultBudget, expireTime = 1 / 240, 0
	-- Call at start of process to prevent unnecessarily waiting.
	function Defer.reset(budget: number?)
		local now = os.clock()
		if now >= expireTime then
			expireTime = os.clock() + (defaultBudget or budget)
		end
	end

	function Defer.wait(budget: number?)
		if os.clock() >= expireTime then
			task.wait()
			Defer.reset(budget)
		end
	end
end

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

local filterRichFormat = `<font transparency="0.5">%s</font><b>%s</b><font transparency="0.5">%s</font>`
local function defaultFilter(self, list)
	local filter = string.lower(self._input._input.Text)
	self._filter = filter
	if string.find(filter, "^%s*$") then
		return list
	end
	local new = {}
	for k, text in list :: { string } do
		local filterIndex, filterEnd = string.find(string.lower(text), filter, 1, true)
		if filterIndex then
			table.insert(
				new,
				string.format(
					filterRichFormat,
					Class.escapeRichText(string.sub(text, 1, filterIndex - 1)),
					Class.escapeRichText(string.sub(text, filterIndex, filterEnd)),
					Class.escapeRichText(string.sub(text, filterEnd + 1))
				)
			)
		end
	end
	return new
end

function Class.escapeRichText(s: string): string
	s = string.gsub(s, "&", "&") -- first because substitutes contain it
	s = string.gsub(s, "<", "<")
	s = string.gsub(s, ">", ">")
	s = string.gsub(s, '"', """)
	s = string.gsub(s, "'", "'")
	return s
end

local queueBudget = 1 / 90
local updateQueue = {}
local cloneQueue = {}
local refreshQueue = {}
local renderQueue = {}
RunService.Heartbeat:connect(function()
	local start = os.clock()

	-- UPDATE
	local queue = updateQueue
	updateQueue = {}
	for scroller in queue do
		if not scroller._instance.Visible or os.clock() - start > queueBudget then
			scroller:updateList()
			continue
		end
		scroller:cloneRaw()
		scroller:refreshList()
	end

	if os.clock() - start > queueBudget then
		return
	end

	-- CLONE
	queue = cloneQueue
	cloneQueue = {}
	for scroller in queue do
		if not scroller._instance.Visible or os.clock() - start > queueBudget then
			scroller:cloneRaw()
			continue
		end
		if scroller.DictList._value then
			local list, index = scroller._sortedList ~= scroller.List._value and scroller._sortedList or {}, 1
			table.clear(list)
			for key in scroller.List._value do
				list[index] = key
				index += 1
			end
			scroller._sortedList = list
		elseif scroller._sortFunction then
			scroller._sortedList = table.clone(scroller.List._value)
		else
			scroller._sortedList = scroller.List._value
		end
		if scroller._sortFunction then
			table.sort(scroller._sortedList, scroller._sortFunction)
		end
	end

	if os.clock() - start > queueBudget then
		return
	end

	-- REFRESH
	queue = refreshQueue
	refreshQueue = {}
	for scroller in queue do
		if not scroller._instance.Visible or os.clock() - start > queueBudget then
			scroller:refreshList()
			continue
		end
		scroller._filteredList = if scroller._filterFunction
			then scroller._filterFunction(scroller, scroller._sortedList)
			else scroller._sortedList
		-- pause canvas
		if not scroller.DictList._value then
			local lineHeight = scroller.ItemSize._value.Y.Offset
			if scroller._lastValue and scroller._scroller.CanvasPosition.Y > lineHeight then
				local change = #scroller._filteredList - scroller._lastLength
				if change == 0 then
					change = (table.find(scroller._filteredList, scroller._lastValue) or 1) - 1
				end
				scroller._scroller.CanvasPosition += Vector2.new(0, change * lineHeight)
			end
			scroller._lastLength = #scroller._filteredList
			scroller._lastValue = scroller._filteredList[1]
		end
		scroller:render(true)
	end

	if os.clock() - start > queueBudget then
		return
	end

	-- RENDER
	queue = renderQueue
	renderQueue = {}
	for scroller in queue do
		if not scroller._instance.Visible or os.clock() - start > queueBudget then
			scroller:render()
			continue
		end
		scroller:forceRender()
	end
end)

function Class:forceRender()
	if not (self.Enabled._value and self._instance.Visible and self._filteredList) then
		return
	end
	local length = #self._filteredList
	local lineHeight = self.ItemSize._value.Y.Offset
	local startIndex = math.max(0, math.floor(self._scroller.CanvasPosition.Y / lineHeight)) + 1
	local endIndex = math.min(startIndex + math.ceil(self._scroller.AbsoluteWindowSize.Y / lineHeight), length)

	local padding = self._scroller:FindFirstChild("UIPadding")
	padding = if padding then padding.PaddingTop.Offset + padding.PaddingBottom.Offset else 0
	self._scroller.CanvasSize = UDim2.new(0, 0, 0, length * lineHeight + padding)

	local usedItems = {}
	for i = math.max(1, startIndex - 4), math.min(length, endIndex + 4) do
		local lineData = if self.ReverseOrder._value then self._filteredList[length - i + 1] else self._filteredList[i]
		local lineItem = self._lineItemCache[lineData]
		if not lineItem then
			lineItem = table.remove(self._lineItemFree)
				or if self.CreateItem
					then self.CreateItem._value(self, lineData)
					else UI.new "TextLabel" {
						AutoLocalize = false,
						BackgroundTransparency = 1,
						Size = self.ItemSize,
						RichText = true,
						FontFace = UI.Theme.FontMono,
						TextSize = UI.Theme.FontSize,
						TextColor3 = UI.Theme.PrimaryText,
						TextStrokeColor3 = UI.Theme.Primary,
						TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
						TextTruncate = Enum.TextTruncate.SplitWord,
						TextXAlignment = Enum.TextXAlignment.Left,
					}
			self._lineItemCache[lineData] = lineItem

			if self.RenderItem then
				self.RenderItem._value(self, lineItem, lineData)
			else
				lineItem.Text = lineData
			end

			local instance = if type(lineItem) == "table" then lineItem._instance else lineItem
			instance.Position = UDim2.new(0, 0, 0, (i - 1) * lineHeight)
			instance.Parent = self._scroller
		end

		usedItems[lineData] = lineItem
	end

	self:freeCache(usedItems)
end

function Class:freeCache(usedItems: { [any]: any }?)
	for data, item in self._lineItemCache do
		if usedItems and usedItems[data] then
			continue
		end
		self._lineItemCache[data] = nil
		table.insert(self._lineItemFree, item)
		local instance = if type(item) == "table" then item._instance else item
		instance.Parent = nil
	end
end

-- Used to render changes from the _rawList
function Class:updateList()
	updateQueue[self] = true
end

function Class:cloneRaw()
	cloneQueue[self] = true
end

function Class:refreshList()
	refreshQueue[self] = true
end

function Class:render(bustCache: boolean?)
	if bustCache then
		self:freeCache()
	end
	renderQueue[self] = true
end

function Class:filter(filterFunction: (...any) -> { any })
	self._filterFunction = filterFunction
	self.Enabled(true)
	self:refreshList()
end

function Class:sort(sortFunction: (...any) -> any)
	self._sortFunction = sortFunction
	if self._instance.Visible then
		if sortFunction then
			if self._sortedList then
				table.sort(self._sortedList, sortFunction)
			end
		else -- unsort
			self:cloneRaw()
		end
		self:refreshList()
	end
end

local DEFAULT = {
	Enabled = true,
	ItemSize = function()
		return UDim2.new(1, 0, 0, UI.Theme.FontSize() + UI.Theme.PaddingDouble().Offset)
	end,
	List = {},
	DictList = false,
	FilterInput = false,
	FilterInputDebounce = 0,
	CreateItem = UI.Function,
	RenderItem = UI.Function,
	ReverseOrder = false,
	Visible = true,
}

type Properties = typeof(DEFAULT) & Frame

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	self._lineItemCache = {}
	self._lineItemFree = {}
	self._lastSizeY = 0
	self._lastScrollY = 0

	local function render()
		UI.clearState("hover")

		local sizeY = self._scroller.AbsoluteWindowSize.Y
		local scrollY = self._scroller.CanvasPosition.Y

		if sizeY ~= self._lastSizeY or scrollY ~= self._lastScrollY then
			if math.abs(scrollY - self._lastScrollY) >= sizeY then
				self:forceRender()
			else
				self:render()
			end
			self._lastSizeY, self._lastScrollY = sizeY, scrollY
		end
	end

	self._scroller = UI.new "ScrollingFrame" {
		Name = "ScrollerFast",
		BorderSizePixel = 0,
		BackgroundTransparency = 1,
		Position = UDim2.new(0, 0, 0, 0),
		Size = UDim2.new(1, 0, 1, 0),
		ScrollBarThickness = 8,
		ScrollBarImageColor3 = UI.Theme.Secondary,
		ScrollBarImageTransparency = UI.Theme.TransparencyClamped,
		TopImage = UI.Theme.ScrollTopImage,
		MidImage = UI.Theme.ScrollMidImage,
		BottomImage = UI.Theme.ScrollBottomImage,
		VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,

		UI.new "UIFlexItem" {
			FlexMode = Enum.UIFlexMode.Fill,
		},

		[UI.Event] = {
			AbsoluteWindowSize = render,
			CanvasPosition = render,
		},
	} :: ScrollingFrame & { UIFlexItem: UIFlexItem, UIPadding: UIPadding }

	local padding = UI.new "UIPadding" {
		Parent = self._scroller,
		PaddingBottom = UI.Theme.PaddingWithStroke,
		PaddingTop = UI.Theme.PaddingWithStroke,
		PaddingLeft = UI.Theme.PaddingStroke,
		PaddingRight = UI.Theme.PaddingWithStroke,
	}

	local function updatePadding()
		UI.edit(padding, {
			PaddingRight = if self._scroller.AbsoluteCanvasSize.Y > self._scroller.AbsoluteWindowSize.Y
				then UI.Theme.PaddingWithStroke
				else UDim.new(),
		})
	end
	updatePadding()

	local cleanup = {
		self._scroller:GetPropertyChangedSignal("AbsoluteWindowSize"):Connect(updatePadding),
		self._scroller:GetPropertyChangedSignal("AbsoluteCanvasSize"):Connect(updatePadding),
	}

	self._instance = UI.new "Frame" {
		Name = "ScrollerFast",
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		Visible = self.Visible,

		UI.new "UIListLayout" { SortOrder = Enum.SortOrder.LayoutOrder, Padding = UI.Theme.Padding },
		self._scroller,

		[UI.Clean] = cleanup,
		[UI.Event] = {
			Parent = render,
		},
	} :: Frame & { UIListLayout: UIListLayout, ScrollerFast: typeof(self._scroller) }

	self = setmetatable(self, Class)

	table.insert(
		cleanup,
		self.List:Connect(function()
			self:updateList()
		end)
	)
	self:updateList()

	local function refresh()
		self:refreshList()
	end
	table.insert(cleanup, UI.Theme.FontSize:Connect(refresh))
	table.insert(cleanup, UI.Theme.Padding:Connect(refresh))

	if self.FilterInput._value then
		local thread = false
		local function debounce()
			if typeof(thread) == "thread" and coroutine.status(thread :: thread) == "suspended" then
				task.cancel(thread)
			end
			thread = task.delay(self.FilterInputDebounce._value, self.refreshList, self)
		end
		self._input = UI.new "Input" {
			LayoutOrder = -1,
			Parent = self._instance,
			ClearTextButton = true,
			Fill = true,
			Placeholder = "Search",
			Icon = UI.Theme.Image.Search,
			IconProperties = {
				ImageColor3 = UI.Theme.PrimaryText,
				Size = UDim2.fromOffset(18, 18),
			},
		}
		table.insert(cleanup, self._input._input:GetPropertyChangedSignal("Text"):Connect(debounce))
		self:filter(defaultFilter)
	end

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))
local Button = require(script.Parent:WaitForChild("Button"))
local Menu = require(script.Parent:WaitForChild("Menu"))

type Button = typeof(Button.new())
type Menu = typeof(Menu.new())

local DEFAULT = {
	Value = "Default",
	Choices = { "Default", "Choice A", "Choice B", "Choice C" },
	FontFace = UI.Theme.Font,
}

type Properties = typeof(DEFAULT) & Frame

local function getName(choice)
	local name = tostring(choice)
	local typeOf = typeof(choice)

	if typeOf == "EnumItem" or typeOf == "Instance" then
		name = choice.Name
	elseif typeOf == "Font" then
		name = UI.getFontName(choice)
	elseif typeOf == "table" then
		name = choice.Name or choice.name
	end

	return name
end

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local itemSize = UI.compute(function()
		local choices = self.Choices()
		local font = self.FontFace()
		local size = UI.Theme.FontSize()
		local padding = if self.Padding then self.Padding().Offset else UI.Theme.Padding().Offset
		local maxWidth = size

		local params = Instance.new("GetTextBoundsParams")
		params.Size = size
		params.Width = math.huge
		params.RichText = true

		for _, choice in choices do
			if typeof(choice) == "Font" then
				font = choice
			elseif typeof(choice) == "EnumItem" then
				if choice.Name == "Unknown" then
					continue
				end
				if choice.EnumType == Enum.Font then
					font = Font.fromEnum(choice :: Enum.Font)
				end
			end

			params.Font = font
			params.Text = getName(choice)

			maxWidth = math.max(maxWidth, math.ceil(UI.retryGetTextBoundsAsync(params).X))
		end

		return UDim2.fromOffset(maxWidth + padding * 2 + size, size + padding)
	end)

	local headerButton = UI.new "Button" {
		Name = "Header",
		ActiveSound = false,
		BackgroundColor3 = UI.Theme.Secondary,
		BackgroundTransparency = UI.Theme.TransparencyHeavy,
		Icon = UI.Theme.Image.Down,
		IconRightAlign = true,
		IconProperties = {
			ImageColor3 = UI.Theme.PrimaryText,
			Position = UDim2.new(0.5, 0, 0.5, 1),
			Size = UDim2.fromScale(0.5, 0.5),
		},
		Label = function()
			return getName(self.Value())
		end,
		Size = itemSize,
		FontFace = self.FontFace,
		TextXAlignment = Enum.TextXAlignment.Left,

		Activated = function()
			UI.toggleState(self.menu.Visible, "floating")
			local value = UI.raw(self.menu.Visible)
			if value then
				UI.Sound.Hover03:Play()
			else
				UI.Sound.Hover01:Play()
			end
		end,
	} :: Button

	UI.edit(headerButton._content.Label.UIPadding, { PaddingRight = UDim.new() })

	local cleanup = { itemSize }

	self._instance = UI.new "Frame" {
		ZIndex = 100,
		Name = "Select",
		BackgroundTransparency = 1,
		Size = itemSize,
		headerButton,
		[UI.Clean] = cleanup,
	} :: Frame & { Header: Button }

	local absoluteSize = UI.state(self._instance, "AbsoluteSize")
	table.insert(cleanup, absoluteSize)

	self.menu = UI.new "Menu" {
		Adornee = self._instance,
		Size = function()
			return UDim2.new(0, absoluteSize().X, 0, math.min(#self.Choices(), 7.5) * itemSize().Y.Offset)
		end,
	} :: Menu
	table.insert(cleanup, self.menu)

	UI.edit(self.menu._instance.UICorner, {
		CornerRadius = UI.Theme.CornerPadded,
	})

	local choiceCleanup = {}

	local selectedButton
	local function updateChoices()
		local selected = self.Value._value
		for i, choice in self.Choices._value do
			local name = getName(choice)
			local font
			if typeof(choice) == "Font" then
				font = choice
			elseif typeof(choice) == "EnumItem" then
				if choice.Name == "Unknown" then
					continue
				end
				if choice.EnumType == Enum.Font then
					font = Font.fromEnum(choice :: Enum.Font)
				end
			end

			local choiceButton
			choiceButton = UI.new "Button" {
				LayoutOrder = math.min(2 ^ 31 - 1, i),
				ZIndex = 1e3,
				Parent = self.menu._content,
				Name = tostring(name),
				Label = name,
				FontFace = font,
				TextSize = UI.Theme.FontSize,
				TextXAlignment = Enum.TextXAlignment.Left,
				Size = itemSize,

				Activated = function()
					selectedButton = choiceButton._instance
					self.Value(choice, true)
					UI.deactivateState(self.menu.Visible, "floating")
				end,
			}
			choiceButton._instance:FindFirstChildOfClass("UIStroke"):Destroy()
			UI.edit(choiceButton._instance, {
				BackgroundColor3 = function()
					local hovering = choiceButton._hovering()
					return if self.Value() == choice and not hovering
						then UI.Theme.Secondary()
						elseif hovering or UI.Theme.Transparency() > 0 then UI.Theme.Secondary()
						else UI.Theme.Primary()
				end,
				BackgroundTransparency = function()
					return if not choiceButton._hovering()
							and UI.Theme.Transparency() > 0
							and self.Value() ~= choice
						then 1
						else UI.Theme.TransparencyHeavy()
				end,
			})
			table.insert(
				choiceCleanup,
				choiceButton._hovering:Connect(function(value)
					if value then
						UI.Sound.Hover02:Play()
					end
				end)
			)

			if choice == selected then
				selectedButton = choiceButton._instance
			end
		end
	end

	table.insert(
		cleanup,
		self.menu.Visible:Connect(function(value)
			if selectedButton and value then
				local buttonHeight = selectedButton.AbsoluteSize.Y
				local buttonPosition = selectedButton.AbsolutePosition.Y + self.menu._content.CanvasPosition.Y
				local offset = math.round((self.menu._content.AbsoluteSize.Y / 2) / buttonHeight) * buttonHeight
				self.menu._content.CanvasPosition =
					Vector2.new(0, buttonPosition - self.menu._content.AbsolutePosition.Y - offset)
			end
		end)
	)

	table.insert(
		cleanup,
		self.Choices:Connect(function(value)
			for _, child in self.menu._content:GetChildren() do
				if not child:IsA("UIBase") then
					child:Destroy()
				end
			end
			UI.cleanup(choiceCleanup)
			updateChoices()
		end)
	)

	updateChoices()

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	Snap = 0,
	SnapSound = true,
	Value = 1,
	Vertical = false,
}

type Properties = typeof(DEFAULT) & Frame

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local cleanup = {}

	local dragInput
	local dragging = UI.state(false)
	self._dragging = dragging

	local valueAlpha = UI.compute(function()
		local snap = self.Snap()
		local value = self.Value()
		return if snap > 1 then math.max(0, value - 1) / (snap - 1) else value
	end)

	table.insert(cleanup, valueAlpha)

	local function update(input)
		if not dragInput then
			return
		end
		-- stylua: ignore
		local value = if self.Vertical._value
			then math.clamp((input.Position.Y - self._instance.AbsolutePosition.Y - 12) / (self._instance.AbsoluteSize.Y - 24), 0, 1)
			else math.clamp((input.Position.X - self._instance.AbsolutePosition.X - 12) / (self._instance.AbsoluteSize.X - 24), 0, 1)
		local snap = self.Snap._value

		if snap > 1 then
			value = math.round(1 + value * (snap - 1))

			if self.SnapSound._value and self.Value._value ~= value then
				UI.Sound.Hover02:Play()
			end
		end

		self.Value(value, true)
	end

	local function inputBegan(input)
		if
			input.UserInputType == Enum.UserInputType.MouseButton1
			or input.UserInputType == Enum.UserInputType.Touch
		then
			UI.Sound.Hover03:Play()
			dragInput = input
			dragging(true)
			local con
			con = input:GetPropertyChangedSignal("UserInputState"):Connect(function()
				if input.UserInputState == Enum.UserInputState.End then
					con:Disconnect()
					if dragInput == input then
						UI.Sound.Hover01:Play()
						dragInput = nil
						dragging(false)
					end
				end
			end)
			update(input)
		end
	end

	table.insert(cleanup, UI.UserInputService.InputChanged:Connect(update))

	self._slider = UI.new "Frame" {
		Name = "Slider",
		AnchorPoint = Vector2.new(0.5, 0.5),
		BackgroundColor3 = UI.Theme.SecondaryText,
		Position = function()
			return if self.Vertical() then UDim2.new(0.5, 0, valueAlpha(), 0) else UDim2.new(valueAlpha(), 0, 0.5, 0)
		end,
		Size = UI.tween(function()
			local size = UI.Theme.FontSize()
			local half = size / 2
			return if dragging() then UDim2.new(0, size, 0, size) else UDim2.new(0, half, 0, half)
		end, UI.Theme.TweenOut),

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},

		InputBegan = inputBegan,
	} :: Frame & { UICorner: UICorner }

	local padding = UI.compute(function()
		return UDim.new(0, UI.Theme.FontSizeSmaller() + UI.Theme.PaddingHalf().Offset)
	end)

	table.insert(cleanup, padding)

	self._instance = UI.new "Frame" {
		Name = "Slider",
		Active = true,
		BackgroundColor3 = UI.Theme.Secondary,
		BackgroundTransparency = UI.Theme.TransparencyHeavy,
		Size = function()
			local height = UI.Theme.FontSize() + UI.Theme.Padding().Offset
			return if self.Vertical() then UDim2.new(0, height, 1, 0) else UDim2.new(1, 0, 0, height)
		end,

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},
		UI.new "Stroke" {},

		UI.new "Frame" {
			BackgroundTransparency = 1,
			Size = UDim2.new(1, 0, 1, 0),
			ZIndex = 2,

			UI.new "UIPadding" {
				PaddingTop = padding,
				PaddingBottom = padding,
				PaddingLeft = padding,
				PaddingRight = padding,
			},
			self._slider,
		},

		UI.new "Frame" {
			Name = "Filled",
			BackgroundColor3 = UI.Theme.Secondary,
			BorderSizePixel = 0,
			Size = function()
				local alpha = valueAlpha()
				local buffer = (UI.Theme.FontSize() + UI.Theme.Padding().Offset) * (1 - alpha)
				return if self.Vertical() then UDim2.new(1, 0, alpha, buffer) else UDim2.new(alpha, buffer, 1, 0)
			end,

			UI.new "UICorner" {
				CornerRadius = UI.Theme.CornerPadded,
			},
		},

		InputBegan = inputBegan,

		[UI.Clean] = cleanup,
	} :: Frame & {
		UICorner: UICorner,
		UIStroke: UIStroke,
		Frame: Frame & { UIPadding: UIPadding, Slider: typeof(self._slider) },
		Filled: Frame & { UICorner: UICorner },
	}

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {}

type Properties = typeof(DEFAULT) & Frame

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	self._instance = UI.new "Frame" {
		Name = "Spacer",
		BackgroundTransparency = 1,

		UI.new "UIFlexItem" {
			FlexMode = Enum.UIFlexMode.Fill,
		},
	} :: Frame & { UIFlexItem: UIFlexItem }

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {}

type Properties = typeof(DEFAULT) & UIStroke

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	self._instance = UI.new "UIStroke" {
		Enabled = UI.Theme.StrokeEnabled,
		ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
		Transparency = UI.Theme.TransparencyLight,
		Color = Color3.new(1, 1, 1),

		UI.new "UIGradient" {
			Color = function()
				return ColorSequence.new(UI.Theme.Border(), UI.Theme.BorderStop())
			end,
			Rotation = UI.Theme.BorderAngle,
		},
	} :: UIStroke

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	Value = false,
}

type Properties = typeof(DEFAULT) & TextButton

local itemHeight = UI.compute(function()
	return UI.Theme.FontSize() + UI.Theme.Padding().Offset
end)

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local cleanup = {}

	local backgroundTransparency = UI.tween(function()
		return if self.Value() then 0 else UI.Theme.TransparencyHeavy()
	end, UI.Theme.TweenOut)
	table.insert(cleanup, backgroundTransparency)

	local backgroundTransparencyClamped = UI.tween(function()
		return if self.Value() then 0 else UI.Theme.TransparencyClamped()
	end, UI.Theme.TweenOut)
	table.insert(cleanup, backgroundTransparencyClamped)

	self._instance = UI.new "TextButton" {
		AutoLocalize = false,
		Name = "Switch",
		Active = true,
		AutoButtonColor = false,
		AnchorPoint = Vector2.new(1, 0),
		BackgroundColor3 = UI.Theme.Secondary,
		BackgroundTransparency = backgroundTransparency,
		Position = UDim2.new(1, 0, 0, 0),
		Text = "",
		TextTransparency = 1,
		Size = function()
			local height = itemHeight()
			return UDim2.new(0, height * 2, 0, height)
		end,

		UI.new "UICorner" {
			CornerRadius = UI.Theme.CornerPadded,
		},
		UI.new "Stroke" {},

		UI.new "Frame" {
			Name = "Circle",
			BackgroundColor3 = UI.Theme.SecondaryText,
			BackgroundTransparency = backgroundTransparencyClamped,
			Size = UDim2.new(1, -2, 1, -2),
			SizeConstraint = Enum.SizeConstraint.RelativeYY,
			Position = UI.tween(function()
				return UDim2.new(0, if self.Value() then itemHeight() + 1 else 1, 0, 1)
			end, UI.Theme.TweenOut),

			UI.new "UICorner" {
				CornerRadius = UI.Theme.CornerPadded,
			},
		},

		Activated = function()
			local value = not UI.raw(self.Value)
			self.Value(value)
			if value then
				UI.Sound.Hover03:Play()
			else
				UI.Sound.Hover01:Play()
			end
		end,

		[UI.Clean] = cleanup,
	} :: TextButton & { UICorner: UICorner, UIStroke: UIStroke, Circle: Frame & { UICorner: UICorner } }

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local DEFAULT = {
	CurrentPage = false,
	Tabs = {},
	Vertical = false,
}

type Properties = typeof(DEFAULT) & Frame

local Class = {}
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local cleanup = {}

	local currentPage = self.CurrentPage()
	if type(currentPage) == "table" and currentPage._instance then
		self.CurrentPage(currentPage._instance)
	end

	local tabOffset = UI.state(0)
	local lastScroll

	table.insert(cleanup, tabOffset)

	local tabList = UI.new "UIListLayout" {
		FillDirection = function()
			return if self.Vertical() then Enum.FillDirection.Vertical else Enum.FillDirection.Horizontal
		end,
		HorizontalFlex = function()
			return if self.Vertical() then Enum.UIFlexAlignment.Fill else Enum.UIFlexAlignment.None
		end,
		SortOrder = Enum.SortOrder.LayoutOrder,
		Padding = UI.Theme.Padding,
	}

	local tabs
	tabs = UI.new "ScrollingFrame" {
		Name = "Tabs",
		Active = false,
		AutomaticCanvasSize = Enum.AutomaticSize.XY,
		AutomaticSize = function()
			return if self.Vertical() then Enum.AutomaticSize.X else Enum.AutomaticSize.Y
		end,
		BackgroundTransparency = 1,
		ClipsDescendants = false,
		CanvasPosition = UI.tween(function()
			return if self.Vertical() then Vector2.new(0, tabOffset()) else Vector2.new(tabOffset(), 0)
		end, UI.Theme.TweenOut),
		CanvasSize = UDim2.new(),
		Size = UDim2.new(1, 0, 1, 0),
		ScrollBarThickness = 0,

		MouseWheelForward = function()
			if self.Vertical() then
				lastScroll = nil
				return
			end
			local absoluteStart = self._instance.AbsolutePosition.X
			local closest, closestChild = math.huge, nil
			for _, child in tabs:GetChildren() do
				if child:IsA("GuiObject") then
					local childStart = child.AbsolutePosition.X
					if childStart < absoluteStart - 2 and absoluteStart - childStart < closest then
						closest = math.floor(absoluteStart - childStart)
						closestChild = child
					end
				end
			end
			if closest == math.huge then
				return
			end
			lastScroll = closestChild
			tabOffset(math.max(0, closestChild.AbsolutePosition.X - (tabs.AbsolutePosition.X - tabs.CanvasPosition.X)))
		end,
		MouseWheelBackward = function()
			if self.Vertical() then
				lastScroll = nil
				return
			end
			local absoluteStart = self._instance.AbsolutePosition.X
				+ (if lastScroll then lastScroll.AbsoluteSize.X else 0)
			local closest, closestChild = math.huge, nil
			for _, child in tabs:GetChildren() do
				if child:IsA("GuiObject") then
					local childStart = child.AbsolutePosition.X
					if childStart > absoluteStart + 2 and childStart - absoluteStart < closest then
						closest = math.floor(childStart - absoluteStart)
						closestChild = child
					end
				end
			end
			if closest == math.huge then
				return
			end
			local maxOffset = tabList.AbsoluteContentSize.X - tabs.AbsoluteSize.X

			if maxOffset > 0 then
				lastScroll = closestChild
			end
			tabOffset(closestChild.AbsolutePosition.X - (tabs.AbsolutePosition.X - tabs.CanvasPosition.X))
		end,

		[UI.Event] = {
			CanvasPosition = function()
				if tabs.CanvasPosition.Y == 1 and not self.Vertical() then
					tabs.CanvasPosition = Vector2.new(tabs.CanvasPosition.X, 0)
				end
			end,
		},

		tabList,
	} :: ScrollingFrame

	table.insert(
		cleanup,
		tabs:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
			if self.Vertical() then
				tabOffset(
					math.clamp(tabOffset._value, 0, tabList.AbsoluteContentSize.Y + tabs.AbsoluteSize.Y),
					false,
					true
				)
			else
				tabOffset(
					math.clamp(tabOffset._value, 0, tabList.AbsoluteContentSize.X + tabs.AbsoluteSize.X),
					false,
					true
				)
			end
			-- TODO: keep current page visible when resizing down?
		end)
	)

	local pageCache, containerCache = {}, {}

	local pages = UI.new "UIPageLayout" {
		Animated = false,
		SortOrder = Enum.SortOrder.LayoutOrder,
		ScrollWheelInputEnabled = false,
		Padding = UI.Theme.PaddingWithStroke,
	}
	self._pages = pages
	self._pageCache = pageCache
	self._containerCache = containerCache

	table.insert(
		cleanup,
		pages.PageEnter:Connect(function(container)
			local page = pageCache[container]
			if not page then
				return
			end
			self.CurrentPage(page)
			page.Visible = true
			page.Parent = container
		end)
	)

	table.insert(
		cleanup,
		pages.PageLeave:Connect(function(container)
			local page = pageCache[container]
			if not page then
				return
			end
			task.defer(function()
				if pages.CurrentPage ~= container then
					page.Parent = nil
					page.Visible = false
				end
			end)
		end)
	)

	local tabPadding = UI.compute(function()
		return if self.Vertical() then UI.Theme.Padding() else UDim.new(0, 1)
	end)
	table.insert(cleanup, tabPadding)

	self._instance = UI.new "Frame" {
		Name = "TabController",
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),

		UI.new "UIListLayout" {
			FillDirection = function()
				return if self.Vertical() then Enum.FillDirection.Horizontal else Enum.FillDirection.Vertical
			end,
			SortOrder = Enum.SortOrder.LayoutOrder,
		},

		UI.new "Frame" {
			Name = "Bar",
			BackgroundTransparency = 1,
			ClipsDescendants = true,
			AutomaticSize = function()
				return if self.Vertical() then Enum.AutomaticSize.X else Enum.AutomaticSize.Y
			end,
			Size = function()
				return if self.Vertical() then UDim2.new(0, 0, 1, 0) else UDim2.new(1, 0, 0, 0)
			end,

			tabs,

			UI.new "UIPadding" {
				PaddingTop = tabPadding,
				PaddingBottom = tabPadding,
				PaddingLeft = UI.Theme.Padding,
				PaddingRight = UI.Theme.Padding,
			},
		},
		[UI.Clean] = cleanup,
	} :: Frame

	local sidePadding = UI.compute(function()
		return if self.Vertical() then UI.Theme.PaddingStroke else UI.Theme.PaddingWithStroke
	end)
	table.insert(cleanup, sidePadding)

	local PagesFrame = UI.new "Frame" {
		Name = "PagesFrame",
		AnchorPoint = Vector2.new(0, 1),
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		Position = UDim2.new(0, 0, 1, 0),

		UI.new "UIPadding" {
			PaddingTop = UI.Theme.PaddingStroke,
			PaddingBottom = UI.Theme.PaddingStroke,
			PaddingLeft = sidePadding,
			PaddingRight = sidePadding,
		},

		pages,
	}

	self._content = UI.new "Frame" {
		LayoutOrder = 3,
		Name = "UIContent",
		Parent = self._instance,
		AnchorPoint = Vector2.new(0, 1),
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		Position = UDim2.new(0, 0, 1, 0),
		ClipsDescendants = true,

		UI.new "UIFlexItem" {
			FlexMode = Enum.UIFlexMode.Fill,
		},
		PagesFrame,

		ChildAdded = function(page)
			local container = containerCache[page]
			if container then
				task.defer(function()
					page.Parent = if self.CurrentPage() == container then container else nil
				end)
			end
		end,
	} :: Frame

	for i, v in UI.raw(self.Tabs) do
		local page, icon = unpack(v)
		if type(page) == "table" then
			page = page._instance
		end

		if containerCache[page] then
			return
		end

		local container = UI.new "Frame" {
			Parent = PagesFrame,
			Name = page.Name,
			LayoutOrder = i,
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(1, 1),
		}
		pageCache[container] = page
		containerCache[page] = container

		if self.CurrentPage() == page then
			page.Parent = container
			pages:JumpTo(container)
		end

		local nameState = UI.state(page, "Name")
		local button
		button = UI.new "Button" {
			Parent = tabs,
			LayoutOrder = i,
			Name = nameState,
			Icon = icon,
			IconProperties = {
				Size = function()
					local size = UI.Theme.FontSize + UI.Theme.PaddingHalf().Offset
					return UDim2.fromOffset(size, size)
				end,
			},
			Size = function()
				local padding = UI.Theme.Padding().Offset
				local params = Instance.new("GetTextBoundsParams")
				params.Text = nameState()
				params.Font = UI.Theme.FontBold()
				params.Size = UI.Theme.FontSize()
				local width = UI.retryGetTextBoundsAsync(params).X + padding * 2
				return UDim2.new(0, math.ceil(width), 0, UI.Theme.FontSize + padding * 2)
			end,
			Label = function()
				return `<b>{nameState()}</b>`
			end,
			TextXAlignment = Enum.TextXAlignment.Left,

			Activated = function()
				UI.clearActiveStates()
				pages:JumpTo(container)

				local axis = if self.Vertical() then "Y" else "X"
				local absoluteStart = tabs.Parent.AbsolutePosition[axis]
				local absoluteEnd = absoluteStart + tabs.Parent.AbsoluteSize[axis]
				local buttonStart = button._instance.AbsolutePosition[axis]
				local buttonEnd = buttonStart + button._instance.AbsoluteSize[axis] + tabList.Padding.Offset

				if tabList.AbsoluteContentSize[axis] >= tabs.AbsoluteSize[axis] and absoluteEnd < buttonEnd + 32 then
					tabOffset(
						math.min(
							tabList.AbsoluteContentSize[axis] - tabs.AbsoluteSize[axis],
							tabOffset + (32 + (buttonEnd - absoluteEnd))
						)
					)
				elseif buttonStart - 32 < absoluteStart then
					tabOffset(math.max(0, tabOffset - ((absoluteStart - buttonStart) + 32 + tabList.Padding.Offset)))
				end
			end,
		}
		table.insert(cleanup, button)

		UI.edit(button._content.Label, {
			TextTransparency = UI.tween(function()
				return if self.CurrentPage() == page then 0 elseif button._hovering() then 0.125 else 0.5
			end, UI.Theme.TweenOut),
		})

		UI.edit(button._content.Label.UIPadding, {
			PaddingLeft = UI.Theme.Padding,
		})

		UI.edit(button._instance.UIStroke, {
			Transparency = UI.tween(function()
				return if self.CurrentPage() == page
					then UI.Theme.TransparencyLight
					elseif button._hovering() then UI.Theme.TransparencyBalanced
					else 1
			end, UI.Theme.TweenOut),
		})

		UI.edit(button._instance, {
			BackgroundTransparency = UI.tween(function()
				return if self.CurrentPage() == page
					then UI.Theme.TransparencyHeavy
					elseif button._hovering() then UI.Theme.TransparencyMax
					else 1
			end, UI.Theme.TweenOut),
		})

		local function hideButton()
			local visible = (page.Parent == container or page.Parent == nil) and page:GetAttribute("Enabled") ~= false
			button._instance.Visible = visible
			container.Visible = visible
		end
		table.insert(cleanup, page:GetPropertyChangedSignal("Parent"):Connect(hideButton))
		table.insert(cleanup, page:GetAttributeChangedSignal("Enabled"):Connect(hideButton))
		hideButton()
	end

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class


local UI = require(script.Parent.Parent) :: any
local BaseClass = require(script.Parent:WaitForChild("BaseClass"))

local layerSizeState = UI.state(UI.LayerTop, "AbsoluteSize")
local positionState = UI.state(Vector2.zero)
local sizeState = UI.state(Vector2.zero)
local tipSizeState = UI.state(Vector2.zero)

local tooltip = UI.new "TextLabel" {
	Parent = UI.LayerTop,
	AutoLocalize = false,
	AutomaticSize = Enum.AutomaticSize.XY,
	Name = "Tooltip",
	ZIndex = 9e8,
	BackgroundColor3 = UI.Theme.Primary,
	FontFace = UI.Theme.Font,
	TextSize = UI.Theme.FontSize,
	TextColor3 = UI.Theme.PrimaryText,
	TextStrokeColor3 = UI.Theme.Primary,
	TextStrokeTransparency = UI.Theme.TextStrokeTransparency,
	TextXAlignment = Enum.TextXAlignment.Left,
	TextWrapped = true,
	RichText = true,
	Visible = false,

	Position = function()
		local tipSize = tipSizeState()
		local max = layerSizeState()
		local position, size = positionState(), sizeState()
		local padding = UI.Theme.Padding().Offset
		local yOffset = position.Y + size.Y + padding + UI.TopbarInset().Height
		local tipBoundX = math.max(0, max.X - tipSize.X)
		local tipBoundY = math.max(0, max.Y - tipSize.Y)
		local xOffset = 0

		if yOffset > tipBoundY then
			yOffset = position.Y - tipSize.Y - padding + UI.TopbarInset().Height
			if yOffset < 0 then
				xOffset = size.X + padding
				if position.X + size.X + padding > tipBoundX then
					xOffset = -(tipSize.X + padding)
				end
			end
		end

		return UDim2.fromOffset(math.clamp(position.X + xOffset, 0, tipBoundX), math.clamp(yOffset, 0, tipBoundY))
	end,

	UI.new "UICorner" {
		CornerRadius = UI.Theme.CornerRadius,
	},
	UI.new "UIPadding" {
		PaddingLeft = UI.Theme.Padding,
		PaddingRight = UI.Theme.Padding,
		PaddingTop = UI.Theme.Padding,
		PaddingBottom = UI.Theme.Padding,
	},
	UI.new "UISizeConstraint" {
		MinSize = Vector2.new(8, 8),
		MaxSize = Vector2.new(256, math.huge),
	},
	UI.new "UIStroke" {
		Enabled = UI.Theme.StrokeEnabled,
		ApplyStrokeMode = Enum.ApplyStrokeMode.Border,
		Transparency = UI.Theme.TransparencyLight,
		Color = Color3.new(1, 1, 1),

		UI.new "UIGradient" {
			Color = function()
				return ColorSequence.new(UI.Theme.Border(), UI.Theme.BorderStop())
			end,
			Rotation = UI.Theme.BorderAngle,
		},
	},
}

tipSizeState:_bindToProperty(tooltip, "AbsoluteSize")

local invalidProperties = { "Enabled", "Hovering", "Parent", "_instance" }
local function updateTooltip(visible: boolean, adornee: GuiObject?, properties: { [string]: any }?)
	tooltip.Visible = visible
	if visible and properties and typeof(adornee) == "Instance" and adornee:IsA("GuiObject") then
		properties = table.clone(properties)
		for _, key in invalidProperties do
			properties[key] = nil
		end
		UI.edit(tooltip, properties)
		positionState(adornee.AbsolutePosition, true)
		sizeState(adornee.AbsoluteSize, true)
		positionState:_bindToProperty(adornee, "AbsolutePosition")
		sizeState:_bindToProperty(adornee, "AbsoluteSize")
	else
		if positionState._bindConnection then
			positionState._bindConnection:Disconnect()
		end
		if sizeState._bindConnection then
			sizeState._bindConnection:Disconnect()
		end
	end
end

local DEFAULT = {
	Enabled = true,
	Hovering = false,
	Visible = false,
	FontFace = UI.Theme.Font,
	Text = "Example tooltip text.",
}

type Properties = typeof(DEFAULT) & Folder

local Class = { Cache = {} }
Class.__index = Class
setmetatable(Class, BaseClass)

function Class.new(properties: Properties?)
	local self = table.clone(DEFAULT)
	UI.makeStatefulDefaults(self, properties)

	local parent
	if properties then
		parent = properties.Parent
		table.clear(properties)
		properties.Parent = parent
	end

	local Adornee
	local hoverConnections = {}
	local hovering = self.Hovering
	local hoverStart

	local function hover(value)
		if value and self.Enabled._value then
			local start = tick()
			hoverStart = start
			task.delay(0.4, function()
				if hoverStart == start and hovering._value then
					self.Visible(true)
				end
			end)
		else
			self.Visible(false)
		end
	end

	local function hoverEnter()
		UI.activateState(hovering, "hover")
	end
	local function hoverLeave()
		UI.deactivateState(hovering, "hover")
	end

	local function inputChanged(input, processed)
		if processed then
			return
		end
		if
			input.UserInputType == Enum.UserInputType.MouseMovement
			or input.UserInputType == Enum.UserInputType.Touch
		then
			if UI.sinkInput(input.Position.X, input.Position.Y, Adornee) then
				hoverLeave()
				return
			end
			hoverEnter()
		end
	end

	local function inputEnded(input)
		if
			input.UserInputType == Enum.UserInputType.MouseMovement
			or input.UserInputType == Enum.UserInputType.Touch
		then
			hoverLeave()
		end
	end

	local function updateAdornee(adornee: GuiObject)
		if Adornee then
			for _, v in hoverConnections do
				v:Disconnect()
			end
		end
		Class.Cache[adornee] = self
		table.insert(hoverConnections, adornee.InputChanged:Connect(inputChanged))
		table.insert(hoverConnections, adornee.InputEnded:Connect(inputEnded))
		table.insert(hoverConnections, adornee.SelectionGained:Connect(hoverEnter))
		table.insert(hoverConnections, adornee.SelectionLost:Connect(hoverLeave))
		Adornee = adornee
	end

	self._instance = UI.new "Folder" {
		Name = "TooltipReference",

		[UI.Clean] = {
			hoverState = hovering,
			hoverDisconnect = hovering:Connect(hover),
			self.Visible:Connect(function(visible)
				if visible then
					updateTooltip(true, Adornee, self)
				else
					updateTooltip(false)
				end
			end),
			UI.UserInputService.TouchMoved:Connect(function()
				hovering(false)
			end),
		},

		[UI.Event] = {
			Parent = function()
				local newParent = self._instance.Parent
				if newParent and newParent ~= UI.LayerTop then
					updateAdornee(newParent)
				end
			end,
		},
	} :: Folder

	return setmetatable(self, Class) :: typeof(self) & typeof(Class)
end

return Class