local React = require("@pkg/@jsdotlua/react")

local Label = require("../Label")
local NumericInput = require("../NumericInput")
local TextInput = require("../TextInput")

local getTextSize = require("../../getTextSize")

local PADDING = 5
local INPUT_WIDTH = 40

local function format(n: number)
	return string.format(`%.3f`, n)
end

local noop = function() end

local function LabelledNumericInput(props: {
	Label: string,
	Value: number?,
	Disabled: boolean?,
	OnChanged: (value: number) -> (),
	OnSubmitted: (value: number) -> (),
	LayoutOrder: number,
	Min: number?,
	Max: number?,
})
	local textWidth = getTextSize(props.Label).X

	local input: React.ReactNode
	if props.Value and not props.Disabled then
		local value = props.Value :: number
		input = React.createElement(NumericInput, {
			Value = value,
			Min = props.Min,
			Max = props.Max,
			Step = 0,
			FormatValue = format,
			OnValidChanged = props.OnChanged,
			OnSubmitted = props.OnSubmitted,
		})
	else
		input = React.createElement(TextInput, {
			Text = if props.Value then format(props.Value) else "",
			OnChanged = noop,
			Disabled = true,
		})
	end

	return React.createElement("Frame", {
		Size = UDim2.new(0, textWidth + INPUT_WIDTH + PADDING, 1, 0),
		BackgroundTransparency = 1,
		LayoutOrder = props.LayoutOrder,
	}, {
		Label = React.createElement(Label, {
			Size = UDim2.new(0, textWidth, 1, 0),
			Text = props.Label,
			Disabled = props.Value == nil,
		}),
		Input = React.createElement("Frame", {
			AnchorPoint = Vector2.new(1, 0),
			Position = UDim2.fromScale(1, 0),
			Size = UDim2.new(0, INPUT_WIDTH, 1, 0),
			BackgroundTransparency = 1,
		}, input),
	})
end

return LabelledNumericInput


local React = require("@pkg/@jsdotlua/react")

local useMouseDrag = require("../../Hooks/useMouseDrag")
local useMouseIcon = require("../../Hooks/useMouseIcon")
local useTheme = require("../../Hooks/useTheme")

local PickerConstants = require("./Constants")

local CATCHER_SIZE = 15
local ENVELOPE_GRAB_HEIGHT = PickerConstants.EnvelopeHandleHeight
local ENVELOPE_TRANSPARENCY = PickerConstants.EnvelopeTransparency
local ENVELOPE_COLOR_STYLE = PickerConstants.EnvelopeColorStyle

local function EnvelopeHandle(props: {
	Top: boolean,
	Size: UDim2,
	OnDragBegan: () -> (),
	OnDragEnded: () -> (),
	OnEnvelopeDragged: (y: number, top: boolean) -> (),
	Disabled: boolean?,
})
	local theme = useTheme()

	local dragStart = React.useRef(0 :: number?)
	local dragOffset = React.useRef(0)

	local function onDragBegin(rbx: GuiObject, input: InputObject)
		local pos = input.Position.Y
		local reference
		if props.Top then
			reference = rbx.AbsolutePosition.Y
		else
			reference = rbx.AbsolutePosition.Y + rbx.AbsoluteSize.Y
		end
		dragStart.current = pos
		dragOffset.current = reference - pos
		props.OnDragBegan()
	end

	local drag = useMouseDrag(function(_, input: InputObject)
		local position = input.Position.Y
		if not dragStart.current or math.abs(position - dragStart.current) > 0 then
			local outPosition
			if props.Top then
				outPosition = position + dragOffset.current :: number + ENVELOPE_GRAB_HEIGHT
			else
				outPosition = position + dragOffset.current :: number - ENVELOPE_GRAB_HEIGHT
			end
			props.OnEnvelopeDragged(outPosition, props.Top)
			dragStart.current = nil
		end
	end, { props.OnEnvelopeDragged }, onDragBegin, props.OnDragEnded)

	local hovered, setHovered = React.useState(false)
	local mouseIcon = useMouseIcon()

	React.useEffect(function()
		if (hovered or drag.isActive()) and not props.Disabled then
			mouseIcon.setIcon("rbxasset://SystemCursors/SplitNS")
		else
			mouseIcon.clearIcon()
		end
	end, { hovered, drag.isActive(), props.Disabled } :: { unknown })

	React.useEffect(function()
		return function()
			mouseIcon.clearIcon()
		end
	end, {})

	return React.createElement("TextButton", {
		Text = "",
		AutoButtonColor = false,
		Size = props.Size,
		AnchorPoint = Vector2.new(0, if props.Top then 0 else 1),
		Position = UDim2.fromScale(0, if props.Top then 0 else 1),
		BackgroundTransparency = 1,
		BorderSizePixel = 0,
		[React.Event.InputBegan] = function(rbx, input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				setHovered(true)
			end
			drag.onInputBegan(rbx, input)
		end,
		[React.Event.InputChanged] = drag.onInputChanged,
		[React.Event.InputEnded] = function(rbx, input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				setHovered(false)
			end
			drag.onInputEnded(rbx, input)
		end,
		ZIndex = 2,
	}, {
		Visual = React.createElement("Frame", {
			AnchorPoint = Vector2.new(0.5, if props.Top then 0 else 1),
			Position = UDim2.fromScale(0.5, if props.Top then 0 else 1),
			Size = UDim2.fromOffset(if drag.isActive() or hovered then 3 else 1, ENVELOPE_GRAB_HEIGHT + 2),
			BorderSizePixel = 0,
			BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),
		}, {
			Bar = React.createElement("Frame", {
				AnchorPoint = Vector2.new(0.5, if props.Top then 1 else 0),
				Position = UDim2.fromScale(0.5, if props.Top then 1 else 0),
				Size = UDim2.fromOffset(9, if drag.isActive() or hovered then 3 else 1),
				BorderSizePixel = 0,
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),
			}),
		}),
	})
end

local function SequenceNode(props: {
	ContentSize: Vector2,
	Keypoint: NumberSequenceKeypoint,
	OnNodeDragged: (position: Vector2) -> (),
	OnEnvelopeDragged: (y: number, top: boolean) -> (),
	Active: boolean,
	OnHovered: () -> (),
	OnDragBegan: () -> (),
	OnDragEnded: () -> (),
	Disabled: boolean?,
})
	local theme = useTheme()
	local mouseIcon = useMouseIcon()

	local nodeDragStart = React.useRef(Vector2.zero :: Vector2?)
	local nodeDragOffset = React.useRef(Vector2.zero)
	local function onNodeDragBegin(rbx: GuiObject, input: InputObject)
		local pos = Vector2.new(input.Position.X, input.Position.Y)
		local corner = rbx.AbsolutePosition
		local center = corner + rbx.AbsoluteSize / 2
		nodeDragStart.current = pos
		nodeDragOffset.current = center - pos
		props.OnDragBegan()
	end
	local nodeDrag = useMouseDrag(function(_, input: InputObject)
		local position = Vector2.new(input.Position.X, input.Position.Y)
		if not nodeDragStart.current or (position - nodeDragStart.current).Magnitude > 0 then
			props.OnNodeDragged(position + nodeDragOffset.current :: Vector2)
			nodeDragStart.current = nil
		end
	end, { props.OnNodeDragged }, onNodeDragBegin, props.OnDragEnded)

	local px = math.round(props.Keypoint.Time * props.ContentSize.X)
	local py = math.round((1 - props.Keypoint.Value) * props.ContentSize.Y)

	local envelopeHeight = math.round(props.Keypoint.Envelope * props.ContentSize.Y) * 2 + 1
	local fullHeight = envelopeHeight + (ENVELOPE_GRAB_HEIGHT + 1) * 2
	local handleClearance = (fullHeight - CATCHER_SIZE) / 2 - 1

	local innerSize = if props.Active then 11 else 7

	local nodeHovered, setNodeHovered = React.useState(false)
	React.useEffect(function()
		if props.Active and nodeDrag.isActive() then
			mouseIcon.setIcon("rbxasset://SystemCursors/ClosedHand")
		elseif props.Active and nodeHovered then
			mouseIcon.setIcon("rbxasset://SystemCursors/OpenHand")
		else
			mouseIcon.clearIcon()
		end
	end, { props.Active, nodeHovered, nodeDrag.isActive() })

	React.useEffect(function()
		if props.Disabled then
			mouseIcon.clearIcon()
		end
		if nodeDrag.isActive() then
			nodeDrag.cancel()
		end
	end, { props.Disabled })

	local envelopeTransparency = ENVELOPE_TRANSPARENCY
	local mainModifier = Enum.StudioStyleGuideModifier.Default
	if props.Disabled then
		envelopeTransparency = 1 - 0.5 * (1 - envelopeTransparency)
		mainModifier = Enum.StudioStyleGuideModifier.Disabled
	end

	return React.createElement("Frame", {
		Position = UDim2.fromOffset(px - (CATCHER_SIZE - 1) / 2, py - (fullHeight - 1) / 2),
		Size = UDim2.fromOffset(CATCHER_SIZE, fullHeight),
		BackgroundTransparency = 1,
		ZIndex = 2,
	}, {
		Line = React.createElement("Frame", {
			AnchorPoint = Vector2.one / 2,
			Position = UDim2.fromScale(0.5, 0.5),
			Size = UDim2.fromOffset(1, envelopeHeight),
			BorderSizePixel = 0,
			BackgroundColor3 = theme:GetColor(ENVELOPE_COLOR_STYLE),
			BackgroundTransparency = envelopeTransparency,
			ZIndex = 0,
		}),

		Node = React.createElement("TextButton", {
			Text = "",
			AutoButtonColor = false,
			AnchorPoint = Vector2.one / 2,
			Position = UDim2.fromScale(0.5, 0.5),
			Size = UDim2.new(1, 0, 0, CATCHER_SIZE),
			BackgroundTransparency = 1,
			[React.Event.InputBegan] = function(rbx, input)
				if props.Disabled then
					return
				elseif input.UserInputType == Enum.UserInputType.MouseMovement then
					setNodeHovered(true)
					props.OnHovered()
				end
				nodeDrag.onInputBegan(rbx, input)
			end,
			[React.Event.InputChanged] = function(rbx, input)
				if props.Disabled then
					return
				end
				nodeDrag.onInputChanged(rbx, input)
			end,
			[React.Event.InputEnded] = function(rbx, input)
				if props.Disabled then
					return
				elseif input.UserInputType == Enum.UserInputType.MouseMovement then
					setNodeHovered(false)
				end
				nodeDrag.onInputEnded(rbx, input)
			end,
			ZIndex = 1,
		}, {
			Inner = React.createElement("Frame", {
				AnchorPoint = Vector2.one / 2,
				Position = UDim2.fromScale(0.5, 0.5),
				Size = UDim2.fromOffset(innerSize, innerSize),
				BackgroundColor3 = if props.Active
					then theme:GetColor(Enum.StudioStyleGuideColor.MainBackground, mainModifier)
					else theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, mainModifier),
				ZIndex = 2,
			}, {
				Stroke = React.createElement("UIStroke", {
					Color = if props.Active
						then theme:GetColor(Enum.StudioStyleGuideColor.MainText, mainModifier)
						else theme:GetColor(Enum.StudioStyleGuideColor.DimmedText, mainModifier),
					Thickness = if nodeDrag.isActive() then 2 else 1,
				}),
			}),
		}),

		Top = props.Active and React.createElement(EnvelopeHandle, {
			Top = true,
			Size = UDim2.new(1, 0, 0, math.min(ENVELOPE_GRAB_HEIGHT, handleClearance)),
			OnDragBegan = props.OnDragBegan,
			OnDragEnded = props.OnDragEnded,
			OnEnvelopeDragged = props.OnEnvelopeDragged,
		}),

		Bottom = props.Active and React.createElement(EnvelopeHandle, {
			Top = false,
			Size = UDim2.new(1, 0, 0, math.min(ENVELOPE_GRAB_HEIGHT, handleClearance)),
			OnDragBegan = props.OnDragBegan,
			OnDragEnded = props.OnDragEnded,
			OnEnvelopeDragged = props.OnEnvelopeDragged,
		}),
	})
end

return SequenceNode


--[=[
	@class NumberSequencePicker

	An interface for modifying [NumberSequence](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequence) values.
	This closely resembles the built-in NumberSequence picker for editing properties, with minor adjustments
	for improved readability.

	| Dark | Light |
	| - | - |
	| ![Dark](/StudioComponents/components/numbersequencepicker/dark.png) | ![Light](/StudioComponents/components/numbersequencepicker/light.png) |

	As this is a controlled component, you should pass a NumberSequence to the `Value` prop
	representing the current value, and a callback to the `OnChanged` prop which gets run when the
	user attempts to change the sequence using the interface. For example:

	```lua
	local function MyComponent()
		local sequence, setSequence = React.useState(NumberSequence.new(...))
		return React.createElement(StudioComponents.NumberSequencePicker, {
			Value = sequence,
			OnChanged = setSequence,
		})
	end
	```

	The default size of this component is exposed in [Constants.DefaultNumberSequencePickerSize].
	To keep all inputs accessible, it is recommended not to use a smaller size than this.
]=]

local React = require("@pkg/@jsdotlua/react")

local CommonProps = require("../../CommonProps")
local Constants = require("../../Constants")
local PickerConstants = require("./Constants")

local Button = require("../Button")
local useTheme = require("../../Hooks/useTheme")

local AxisLabel = require("./AxisLabel")
local DashedLine = require("./DashedLine")
local FreeLine = require("./FreeLine")
local LabelledNumericInput = require("./LabelledNumericInput")
local SequenceNode = require("./SequenceNode")

local function clampVector2(v: Vector2, vmin: Vector2, vmax: Vector2)
	return Vector2.new(math.clamp(v.X, vmin.X, vmax.X), math.clamp(v.Y, vmin.Y, vmax.Y))
end

--[=[
	@within NumberSequencePicker
	@interface Props
	@tag Component Props

	@field ... CommonProps
	@field Value NumberSequence
	@field OnChanged ((newValue: NumberSequence) -> ())?
]=]

type NumberSequencePickerProps = CommonProps.T & {
	Value: NumberSequence,
	OnChanged: ((newValue: NumberSequence) -> ())?,
}

local GRID_ROWS = 4
local GRID_COLS = 10

local ENVELOPE_TRANSPARENCY = PickerConstants.EnvelopeTransparency
local ENVELOPE_COLOR_STYLE = PickerConstants.EnvelopeColorStyle

local function identity(v)
	return v
end

local function NumberSequencePicker(props: NumberSequencePickerProps)
	local theme = useTheme()
	local sequence = props.Value

	local onChanged: (NumberSequence) -> () = props.OnChanged or function() end

	local hoveringIndex: number?, setHoveringIndex = React.useState(nil :: number?)
	local draggingIndex: number?, setDraggingIndex = React.useState(nil :: number?)

	React.useEffect(function()
		if props.Disabled then
			setHoveringIndex(nil)
			setDraggingIndex(nil)
		end
	end, { props.Disabled })

	local contentPos, setContentPos = React.useState(Vector2.zero)
	local contentSize, setContentSize = React.useState(Vector2.zero)

	local borders: { [string]: React.ReactNode } = {}
	for col = 0, GRID_COLS, 0.5 do
		local border = React.createElement(DashedLine, {
			Position = UDim2.fromOffset(math.round(col / GRID_COLS * contentSize.X), 0),
			Size = UDim2.new(0, 1, 1, 0),
			Direction = Enum.FillDirection.Vertical,
			Transparency = if col % 1 == 0 then 0 else 0.75,
			Disabled = props.Disabled,
		})
		borders[`BorderCol{col}`] = border
	end
	for row = 0, GRID_ROWS, 0.5 do
		local border = React.createElement(DashedLine, {
			Position = UDim2.fromOffset(0, math.round(row / GRID_ROWS * contentSize.Y)),
			Size = UDim2.new(1, 0, 0, 1),
			Direction = Enum.FillDirection.Horizontal,
			Transparency = if row % 1 == 0 then 0 else 0.75,
			Disabled = props.Disabled,
		})
		borders[`BorderRow{row}`] = border
	end

	local function onNodeDragged(index: number, position: Vector2)
		local offset = position - contentPos
		offset = clampVector2(offset / contentSize, Vector2.zero, Vector2.one)
		offset = Vector2.new(offset.X, 1 - offset.Y)

		local origin = sequence.Keypoints[index]
		local before = sequence.Keypoints[index - 1]
		local after = sequence.Keypoints[index + 1]

		local minTime = if index == #sequence.Keypoints then 1 elseif index == 1 then 0 else before.Time
		local maxTime = if index == 1 then 0 elseif index == #sequence.Keypoints then 1 else after.Time

		local newEnvelope = origin.Envelope
		if offset.Y + newEnvelope > 1 then
			newEnvelope = 1 - offset.Y
		end
		if offset.Y - newEnvelope < 0 then
			newEnvelope = offset.Y
		end

		local newKeypoint = NumberSequenceKeypoint.new(math.clamp(offset.X, minTime, maxTime), offset.Y, newEnvelope)
		local newKeypoints = table.clone(sequence.Keypoints)
		newKeypoints[index] = newKeypoint

		onChanged(NumberSequence.new(newKeypoints))
	end

	local function onEnvelopeDragged(index: number, y: number, top: boolean)
		local keypoint = sequence.Keypoints[index]
		local offset = (y - contentPos.Y) / contentSize.Y
		local value = 1 - keypoint.Value

		local newEnvelope
		local maxValue = math.min(value, 1 - value)
		if top then
			newEnvelope = math.clamp(value - offset, 0, maxValue)
		else
			newEnvelope = math.clamp(offset - value, 0, maxValue)
		end

		local newKeypoints = table.clone(sequence.Keypoints)
		newKeypoints[index] = NumberSequenceKeypoint.new(keypoint.Time, keypoint.Value, newEnvelope)

		onChanged(NumberSequence.new(newKeypoints))
	end

	local points: { [string]: React.ReactNode } = {}
	for i, keypoint in sequence.Keypoints do
		points[`Point{i}`] = React.createElement(SequenceNode, {
			ContentSize = contentSize,
			Keypoint = keypoint,
			Active = hoveringIndex == i or draggingIndex == i,
			Disabled = props.Disabled,
			OnHovered = function()
				if draggingIndex == nil and hoveringIndex ~= i then
					setHoveringIndex(i)
				end
			end,
			OnNodeDragged = function(position)
				onNodeDragged(i, position)
			end,
			OnEnvelopeDragged = function(y, top)
				onEnvelopeDragged(i, y, top)
			end,
			OnDragBegan = function()
				setDraggingIndex(i)
			end,
			OnDragEnded = function()
				setDraggingIndex(nil)
			end,
		})
	end

	local lines: { [string]: React.ReactNode } = {}
	for i = 1, #sequence.Keypoints - 1 do
		local kp0 = sequence.Keypoints[i]
		local kp1 = sequence.Keypoints[i + 1]
		local p0 = Vector2.new(kp0.Time, 1 - kp0.Value)
		local p1 = Vector2.new(kp1.Time, 1 - kp1.Value)
		lines[`Line{i}`] = React.createElement(FreeLine, {
			Pos0 = p0 * contentSize,
			Pos1 = p1 * contentSize,
			Color = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton),
			ZIndex = 1,
			Disabled = props.Disabled,
		})
	end

	local envelopes: { [string]: React.ReactNode } = {}
	for i = 1, #sequence.Keypoints - 1 do
		local kp0 = sequence.Keypoints[i]
		local kp1 = sequence.Keypoints[i + 1]
		if kp0.Envelope == 0 and kp1.Envelope == 0 then
			continue
		end
		local p0 = Vector2.new(kp0.Time, 1 - kp0.Value)
		local p1 = Vector2.new(kp1.Time, 1 - kp1.Value)
		envelopes[`EnvelopeOver{i}`] = React.createElement(FreeLine, {
			Pos0 = contentSize * Vector2.new(p0.X, p0.Y - kp0.Envelope), -- NB: roblox enforces bound
			Pos1 = contentSize * Vector2.new(p1.X, p1.Y - kp1.Envelope),
			Color = theme:GetColor(ENVELOPE_COLOR_STYLE),
			Transparency = ENVELOPE_TRANSPARENCY,
			ZIndex = 1,
			Disabled = props.Disabled,
		})
		envelopes[`EnvelopeUnder{i}`] = React.createElement(FreeLine, {
			Pos0 = contentSize * Vector2.new(p0.X, p0.Y + kp0.Envelope),
			Pos1 = contentSize * Vector2.new(p1.X, p1.Y + kp1.Envelope),
			Color = theme:GetColor(ENVELOPE_COLOR_STYLE),
			Transparency = ENVELOPE_TRANSPARENCY,
			ZIndex = 1,
			Disabled = props.Disabled,
		})
	end

	local axisLabels: { [string]: React.ReactNode } = {}
	for row = 1, GRID_ROWS do
		axisLabels[`AxisLabelRow{row}`] = React.createElement(AxisLabel, {
			AnchorPoint = Vector2.new(1, 0.5),
			Position = UDim2.new(0, -5, 1 - row / GRID_ROWS, 0),
			TextXAlignment = Enum.TextXAlignment.Right,
			Value = row / GRID_ROWS,
			Disabled = props.Disabled,
		})
	end
	for col = 1, GRID_COLS do
		axisLabels[`AxisLabelCol{col}`] = React.createElement(AxisLabel, {
			AnchorPoint = Vector2.new(0.5, 0),
			Position = UDim2.new(col / GRID_COLS, 0, 1, 5),
			Value = col / GRID_COLS,
			Disabled = props.Disabled,
		})
	end
	axisLabels["AxisLabelZero"] = React.createElement(AxisLabel, {
		AnchorPoint = Vector2.new(1, 0),
		Position = UDim2.new(0, -5, 1, 5),
		TextXAlignment = Enum.TextXAlignment.Right,
		Value = 0,
		Disabled = props.Disabled,
	})

	local function tryAddKeypoint(keypoint: NumberSequenceKeypoint)
		if #sequence.Keypoints == 20 then
			return
		end
		local newKeypoints = table.clone(sequence.Keypoints)
		for i = 1, #sequence.Keypoints do
			local kp0 = sequence.Keypoints[i]
			local kp1 = sequence.Keypoints[i + 1]
			if keypoint.Time >= kp0.Time and keypoint.Time <= kp1.Time then
				table.insert(newKeypoints, i + 1, keypoint)
				onChanged(NumberSequence.new(newKeypoints))
				setHoveringIndex(i + 1)
				break
			end
		end
	end

	local activeIndex = draggingIndex or hoveringIndex
	local activeKeypoint = if activeIndex then sequence.Keypoints[activeIndex] else nil

	local minTime, maxTime
	if activeKeypoint then
		local i = activeIndex :: number
		local before = sequence.Keypoints[i - 1]
		local after = sequence.Keypoints[i + 1]
		minTime = if not after then 1 elseif not before then 0 else before.Time
		maxTime = if not before then 0 elseif not after then 1 else after.Time
	end

	local minValue = 0
	local maxValue = 1

	local minEnvelope = 0
	local maxEnvelope
	if activeKeypoint then
		local current = activeKeypoint :: NumberSequenceKeypoint
		maxEnvelope = math.min(current.Value, 1 - current.Value)
	end

	local function updateKeypoint(keypointProps: {
		Time: number?,
		Value: number?,
		Envelope: number?,
	})
		local current = activeKeypoint :: NumberSequenceKeypoint
		local newKeypoints = table.clone(sequence.Keypoints)
		newKeypoints[activeIndex :: number] = NumberSequenceKeypoint.new(
			keypointProps.Time or current.Time,
			keypointProps.Value or current.Value,
			keypointProps.Envelope or current.Envelope
		)
		onChanged(NumberSequence.new(newKeypoints))
	end

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size or Constants.DefaultNumberSequencePickerSize,
		LayoutOrder = props.LayoutOrder,
		ZIndex = props.ZIndex,
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
		BorderSizePixel = 0,
	}, {
		Padding = React.createElement("UIPadding", {
			PaddingLeft = UDim.new(0, PickerConstants.EnvelopeHandleHeight + 1),
			PaddingRight = UDim.new(0, PickerConstants.EnvelopeHandleHeight + 1),
			PaddingTop = UDim.new(0, PickerConstants.EnvelopeHandleHeight + 1),
			PaddingBottom = UDim.new(0, 10),
		}),

		ContentArea = React.createElement("Frame", {
			Position = UDim2.fromOffset(22, 0), -- axis labels
			Size = UDim2.fromScale(1, 1)
				- UDim2.fromOffset(0, PickerConstants.EnvelopeHandleHeight + 4 + Constants.DefaultInputHeight)
				- UDim2.fromOffset(22, 10) -- axis labels
				- UDim2.fromOffset(1, 1), -- outer/lower border
			BackgroundTransparency = 1,
			[React.Change.AbsolutePosition] = function(rbx: Frame)
				setContentPos(rbx.AbsolutePosition)
			end,
			[React.Change.AbsoluteSize] = function(rbx: Frame)
				setContentSize(rbx.AbsoluteSize)
			end,
			[React.Event.InputBegan] = function(rbx: Frame, input: InputObject)
				if not draggingIndex and input.UserInputType == Enum.UserInputType.MouseButton1 then
					local offset = Vector2.new(input.Position.X, input.Position.Y) - rbx.AbsolutePosition
					offset = offset / rbx.AbsoluteSize
					offset = clampVector2(offset, Vector2.zero, Vector2.one)
					local newKeypoint = NumberSequenceKeypoint.new(offset.X, 1 - offset.Y)
					tryAddKeypoint(newKeypoint)
				end
			end :: () -> (),
		}, borders, points, lines, envelopes, axisLabels),

		ControlsArea = React.createElement("Frame", {
			AnchorPoint = Vector2.new(0, 1),
			Position = UDim2.fromScale(0, 1),
			Size = UDim2.new(1, 0, 0, Constants.DefaultInputHeight),
			BackgroundTransparency = 1,
			BorderSizePixel = 0,
		}, {
			Layout = React.createElement("UIListLayout", {
				FillDirection = Enum.FillDirection.Horizontal,
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UDim.new(0, 15),
			}),
			Time = React.createElement(LabelledNumericInput, {
				LayoutOrder = 1,
				Label = "Time",
				Min = minTime,
				Max = maxTime,
				Value = if activeKeypoint then activeKeypoint.Time else nil,
				Disabled = not activeIndex or activeIndex <= 1 or activeIndex >= #sequence.Keypoints,
				OnChanged = identity,
				OnSubmitted = function(newTime)
					updateKeypoint({ Time = math.clamp(newTime, minTime, maxTime) })
				end,
			}),
			Value = React.createElement(LabelledNumericInput, {
				LayoutOrder = 2,
				Label = "Value",
				Min = minValue,
				Max = maxValue,
				Value = if activeKeypoint then activeKeypoint.Value else nil,
				OnChanged = identity,
				OnSubmitted = function(newValue)
					updateKeypoint({ Value = math.clamp(newValue, minValue, maxValue) })
				end,
			}),
			Envelope = React.createElement(LabelledNumericInput, {
				LayoutOrder = 3,
				Label = "Envelope",
				Min = minEnvelope,
				Max = maxEnvelope,
				Value = if activeKeypoint then activeKeypoint.Envelope else nil,
				OnChanged = identity,
				OnSubmitted = function(newEnvelope)
					updateKeypoint({ Envelope = math.clamp(newEnvelope, minEnvelope, maxEnvelope) })
				end,
			}),
			Delete = React.createElement("Frame", {
				LayoutOrder = 4,
				Size = UDim2.new(0, 105, 1, 0),
				BackgroundTransparency = 1,
			}, {
				Button = React.createElement(Button, {
					Position = UDim2.fromOffset(0, 1),
					Size = UDim2.new(1, 0, 1, -2),
					Text = "Delete Keypoint",
					Disabled = activeKeypoint == nil or activeIndex <= 1 or activeIndex >= #sequence.Keypoints,
					OnActivated = function()
						if activeIndex then
							local newKeypoints = table.clone(sequence.Keypoints)
							table.remove(newKeypoints, activeIndex)
							onChanged(NumberSequence.new(newKeypoints))
						end
					end,
				}),
			}),
		}),
	})
end

return NumberSequencePicker


--[=[
	@class NumericInput

	An input field matching the appearance of a [TextInput] but which filters inputted text to only
	allow numeric values, optionally with arrow and slider controls.

	| Dark | Light |
	| - | - |
	| ![Dark](/StudioComponents/components/numericinput/dark.png) | ![Light](/StudioComponents/components/numericinput/light.png) |

	This is a controlled component with similar behavior to [TextInput]. The current numeric value
	to display should be passed to the `Value` prop, and a callback should be passed to the
	`OnValidChanged` prop which is run when the user types a (valid) numeric input.

	Optionally, a minimum value can be passed to the `Min` prop, as well as a maximum value to the
	`Max` prop. A step (increment) value may also be passed to the the `Step` prop, which defaults
	to 1 (allowing only whole number values). Passing a non-integer step value will also allow a
	decimal point to be typed in the input box.

	Use the `Arrows` and `Slider` props to specify whether up/down arrows and a slider should be
	included. If arrows or a slider are displayed, they will increment the value by the amount of the step.
	Completing a slide with a slider will call the `OnSubmitted` prop (if provided) with the latest value.

	Only decimal inputs are allowed (so, for example, hex characters a-f will not be permitted).
]=]

local RunService = game:GetService("RunService")

local React = require("@pkg/@jsdotlua/react")

local BaseTextInput = require("./Foundation/BaseTextInput")
local Slider = require("./Slider")

local Constants = require("../Constants")
local useFreshCallback = require("../Hooks/useFreshCallback")
local useTheme = require("../Hooks/useTheme")

--[=[
	@within NumericInput
	@interface Props
	@tag Component Props

	@field ... CommonProps

	@field Value number
	@field OnValidChanged ((n: number) -> ())?
	@field Min number?
	@field Max number?
	@field Step number?
	@field OnSubmitted ((n: number) -> ())?
	@field FormatValue ((n: number) -> string)?
	@field Arrows boolean?
	@field Slider boolean?
	@field PlaceholderText string?
	@field ClearTextOnFocus boolean?
	@field OnFocused (() -> ())?
	@field OnFocusLost ((text: string, enterPressed: boolean, input: InputObject) -> ())?
]=]

type NumericInputProps = BaseTextInput.BaseTextInputConsumerProps & {
	OnValidChanged: ((n: number) -> ())?,
	Value: number,
	Min: number?,
	Max: number?,
	Step: number?,
	OnSubmitted: ((n: number) -> ())?,
	FormatValue: ((n: number) -> string)?,
	Arrows: boolean?,
	Slider: boolean?,
}

local MAX = 2 ^ 53
local ARROW_WIDTH = 19
local SLIDER_SPLIT = 0.45

local function roundToStep(n: number, step: number): number
	if step == 0 then
		return n
	end
	return math.round(n / step) * step
end

local function tonumberPositiveZero(s: string): number?
	local n = tonumber(s)
	if n == -0 then
		return 0
	end
	return n
end

local function applyFormat(n: number, formatter: ((n: number) -> string)?)
	if n == -0 then
		n = 0
	end
	if formatter ~= nil then
		return formatter(n)
	end
	return tostring(n)
end

local function NumericInputArrow(props: {
	HeightBinding: React.Binding<number>,
	Side: number,
	Callback: (side: number) -> (),
	Disabled: boolean?,
})
	local theme = useTheme()
	local connection = React.useRef(nil) :: { current: RBXScriptConnection? }

	local hovered, setHovered = React.useState(false)
	local pressed, setPressed = React.useState(false)
	local modifier = Enum.StudioStyleGuideModifier.Default
	if props.Disabled then
		modifier = Enum.StudioStyleGuideModifier.Disabled
	elseif pressed then
		modifier = Enum.StudioStyleGuideModifier.Pressed
	elseif hovered then
		modifier = Enum.StudioStyleGuideModifier.Hover
	end

	local maybeActivate = useFreshCallback(function()
		if hovered then
			props.Callback(props.Side)
		end
	end, { hovered, props.Callback, props.Side } :: { unknown })

	local function startHolding()
		if connection.current then
			connection.current:Disconnect()
		end
		local nextScroll = os.clock() + 0.35
		connection.current = RunService.PostSimulation:Connect(function()
			if os.clock() >= nextScroll then
				maybeActivate()
				nextScroll += 0.05
			end
		end)
		props.Callback(props.Side)
	end

	local function stopHolding()
		if connection.current then
			connection.current:Disconnect()
			connection.current = nil
		end
	end
	React.useEffect(stopHolding, {})

	React.useEffect(function()
		if props.Disabled and pressed then
			stopHolding()
			setPressed(false)
		end
		if props.Disabled then
			setHovered(false)
		end
	end, { props.Disabled, pressed })

	return React.createElement("TextButton", {
		AutoButtonColor = false,
		BorderSizePixel = 0,
		Text = "",
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Button, modifier),
		Size = props.HeightBinding:map(function(height: number)
			if props.Side == 0 then
				return UDim2.new(1, 0, 0, math.floor(height / 2))
			else
				return UDim2.new(1, 0, 0, math.ceil(height / 2) - 1)
			end
		end),
		Position = props.HeightBinding:map(function(height: number)
			if props.Side == 0 then
				return UDim2.fromOffset(0, 0)
			else
				return UDim2.fromOffset(0, math.floor(height / 2) + 1)
			end
		end),
		[React.Event.InputBegan] = function(_, input)
			if props.Disabled then
				return
			elseif input.UserInputType == Enum.UserInputType.MouseMovement then
				setHovered(true)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				setPressed(true)
				startHolding()
			end
		end,
		[React.Event.InputEnded] = function(_, input)
			if props.Disabled then
				return
			elseif input.UserInputType == Enum.UserInputType.MouseMovement then
				setHovered(false)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				setPressed(false)
				stopHolding()
			end
		end,
	}, {
		Image = React.createElement("ImageLabel", {
			Image = "rbxassetid://14699332993",
			Size = UDim2.fromOffset(7, 4),
			ImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, modifier),
			BackgroundTransparency = 1,
			AnchorPoint = Vector2.new(0.5, 0.5),
			Position = UDim2.fromScale(0.5, 0.5),
			ImageRectSize = Vector2.new(7, 4),
			ImageRectOffset = Vector2.new(0, if props.Side == 0 then 0 else 4),
			ImageTransparency = if props.Disabled then 0.2 else 0,
		}),
	})
end

local function NumericInput(props: NumericInputProps)
	local theme = useTheme()

	local min = math.max(props.Min or -MAX, -MAX)
	local max = math.min(props.Max or MAX, MAX)
	local step = props.Step or 1
	assert(max >= min, `max ({max}) was less than min ({min})`)
	assert(step >= 0, `step ({step}) was less than 0`)

	local pattern = if step % 1 == 0 and step ~= 0 then "[^%-%d]" else "[^%-%.%d]"
	local lastCleanText, setLastCleanText = React.useState(applyFormat(props.Value, props.FormatValue))

	local onValidChanged: (number) -> () = props.OnValidChanged or function() end

	React.useEffect(function()
		setLastCleanText(function(freshLastCleanText)
			if tonumberPositiveZero(freshLastCleanText) ~= props.Value then
				return applyFormat(props.Value, props.FormatValue)
			end
			return freshLastCleanText
		end)
	end, { props.Value, props.FormatValue } :: { unknown })

	local heightBinding, setHeightBinding = React.useBinding(0)
	local function buttonCallback(side: number)
		local usingStep = (if step == 0 then 1 else step) * (if side == 0 then 1 else -1)
		local newValue = math.clamp(props.Value + usingStep, min, max)
		if newValue ~= props.Value then
			onValidChanged(newValue)
		end
	end

	local modifier = Enum.StudioStyleGuideModifier.Default
	if props.Disabled then
		modifier = Enum.StudioStyleGuideModifier.Disabled
	end

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size or UDim2.new(1, 0, 0, Constants.DefaultInputHeight),
		LayoutOrder = props.LayoutOrder,
		ZIndex = props.ZIndex,
		BorderSizePixel = 0,
		BackgroundTransparency = 1,
	}, {
		InputHolder = React.createElement("Frame", {
			Size = UDim2.fromScale(if props.Slider then SLIDER_SPLIT else 1, 1),
			BackgroundTransparency = 1,
		}, {
			Input = React.createElement(BaseTextInput, {
				Disabled = props.Disabled,
				Size = UDim2.fromScale(1, 1),
				ClearTextOnFocus = props.ClearTextOnFocus,
				PlaceholderText = props.PlaceholderText,
				Text = lastCleanText,
				RightPaddingExtra = if props.Arrows then ARROW_WIDTH + 1 else 0,
				OnChanged = function(newText: string)
					local cleanText = string.gsub(newText, pattern, "")
					local number = tonumberPositiveZero(cleanText)
					if number ~= nil then
						if number >= min and number <= max and roundToStep(number, step) == number then
							onValidChanged(number)
						end
					end
					setLastCleanText(cleanText)
				end,
				OnFocusLost = function(_, enterPressed: boolean, inputObject: InputObject)
					local number = tonumberPositiveZero(lastCleanText)
					local outValue = props.Value
					if number ~= nil then
						outValue = math.clamp(roundToStep(number, step), min, max)
					end
					onValidChanged(outValue)
					if props.OnSubmitted then
						props.OnSubmitted(outValue)
					end
					setLastCleanText(applyFormat(outValue, props.FormatValue))
					if props.OnFocusLost then
						props.OnFocusLost(lastCleanText, enterPressed, inputObject)
					end
				end,
				OnFocused = props.OnFocused,
			}),
			Arrows = props.Arrows and React.createElement("Frame", {
				BorderSizePixel = 0,
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Button, modifier),
				AnchorPoint = Vector2.new(1, 0),
				Position = UDim2.new(1, -1, 0, 1),
				Size = UDim2.new(0, ARROW_WIDTH, 1, -2),
				[React.Change.AbsoluteSize] = function(rbx)
					setHeightBinding(rbx.AbsoluteSize.Y)
				end,
			}, {
				Up = React.createElement(NumericInputArrow, {
					Disabled = props.Disabled or props.Value >= max,
					Callback = buttonCallback,
					HeightBinding = heightBinding,
					Side = 0,
				}),
				Down = React.createElement(NumericInputArrow, {
					Disabled = props.Disabled or props.Value <= min,
					Callback = buttonCallback,
					HeightBinding = heightBinding,
					Side = 1,
				}),
			}),
		}),
		Slider = props.Slider and React.createElement(Slider, {
			AnchorPoint = Vector2.new(1, 0),
			Position = UDim2.fromScale(1, 0),
			Size = UDim2.new(1 - SLIDER_SPLIT, -5, 1, 0),
			Value = props.Value,
			Min = min,
			Max = max,
			Step = step,
			OnChanged = props.OnValidChanged,
			OnCompleted = props.OnSubmitted,
			Disabled = props.Disabled,
		}),
	})
end

return NumericInput


--[=[
	@class PluginProvider

	This component provides an interface to plugin apis for other components in the tree. It should
	be provided with a single `Plugin` prop that must point to `plugin` (your plugin's root instance).

	You do not have to use this component unless you want custom mouse icons via the [useMouseIcon]
	hook. Right now, the only built-in component that relies on this is [Splitter]. Theming and all
	other functionality will work regardless of whether this component is used.

	You should only render one PluginProvider in your tree. Commonly, this is done at the top of
	the tree with the rest of your plugin as children/descendants.

	Example of usage:

	```lua
	local function MyComponent()
		return React.createElement(StudioComponents.PluginProvider, {
			Plugin = plugin,
		}, {
			MyExample = React.createElement(MyExample, ...)
		})
	end
	```
]=]

local HttpService = game:GetService("HttpService")

local React = require("@pkg/@jsdotlua/react")

local PluginContext = require("../Contexts/PluginContext")

type IconStackEntry = {
	id: string,
	icon: string,
}

--[=[
	@within PluginProvider
	@interface Props
	@tag Component Props

	@field Plugin Plugin
	@field children React.ReactNode
]=]

type PluginProviderProps = {
	Plugin: Plugin,
	children: React.ReactNode,
}

local function PluginProvider(props: PluginProviderProps)
	local plugin = props.Plugin
	local iconStack = React.useRef({}) :: { current: { IconStackEntry } }

	local function updateMouseIcon()
		local top = iconStack.current[#iconStack.current]
		plugin:GetMouse().Icon = if top then top.icon else ""
	end

	local function pushMouseIcon(icon)
		local id = HttpService:GenerateGUID(false)
		table.insert(iconStack.current, { id = id, icon = icon })
		updateMouseIcon()
		return id
	end

	local function popMouseIcon(id)
		for i = #iconStack.current, 1, -1 do
			local item = iconStack.current[i]
			if item.id == id then
				table.remove(iconStack.current, i)
			end
		end
		updateMouseIcon()
	end

	React.useEffect(function()
		return function()
			table.clear(iconStack.current)
			plugin:GetMouse().Icon = ""
		end
	end, {})

	return React.createElement(PluginContext.Provider, {
		value = {
			plugin = plugin,
			pushMouseIcon = pushMouseIcon,
			popMouseIcon = popMouseIcon,
		},
	}, props.children)
end

return PluginProvider


--[=[
	@class ProgressBar

	A basic progress indicator. This should be used for longer or more detailed loading processes.
	For shorter loading processes, consider using a [LoadingDots] component.

	| Dark | Light |
	| - | - |
	| ![Dark](/StudioComponents/components/progressbar/dark.png) | ![Light](/StudioComponents/components/progressbar/light.png) |

	Pass a number representing the current progress into the `Value` prop. You can optionally pass a
	number representing the maximum value into the `Max` prop, which defaults to 1 if not provided.
	The `Value` prop should be between 0 and `Max`. For example:

	```lua
	local function MyComponent()
		return React.createElement(StudioComponents.ProgressBar, {
			Value = 5, -- loaded 5 items
			Max = 10, -- out of a total of 10 items
		})
	end
	```

	By default, the progress bar will display text indicating the progress as a percentage,
	rounded to the nearest whole number. This can be customized by providing a prop to `Formatter`,
	which should be a function that takes two numbers representing the current value and the maximum value
	and returns a string to be displayed. For example:

	```lua
	local function MyComponent()
		return React.createElement(StudioComponents.ProgressBar, {
			Value = 3,
			Max = 20,
			Formatter = function(current, max)
				return `Loaded {current} / {max} assets...`
			end,
		})
	end
	```

	By default, the height of a progress bar is equal to the value in [Constants.DefaultProgressBarHeight].
	This can be configured via props.
]=]

local React = require("@pkg/@jsdotlua/react")

local CommonProps = require("../CommonProps")
local Constants = require("../Constants")
local useTheme = require("../Hooks/useTheme")

--[=[
	@within ProgressBar
	@interface Props
	@tag Component Props

	@field ... CommonProps
	@field Value number
	@field Max number?
	@field Formatter ((value: number, max: number) -> string)?
]=]

type ProgressBarProps = CommonProps.T & {
	Value: number,
	Max: number?,
	Formatter: ((value: number, max: number) -> string)?,
}

local function defaultFormatter(value: number, max: number)
	return string.format("%i%%", 100 * value / max)
end

local function ProgressBar(props: ProgressBarProps)
	local theme = useTheme()

	local formatter: (number, number) -> string = defaultFormatter
	if props.Formatter then
		formatter = props.Formatter
	end

	local max = props.Max or 1
	local value = math.clamp(props.Value, 0, max)
	local alpha = value / max
	local text = formatter(value, max)

	local modifier = Enum.StudioStyleGuideModifier.Default
	if props.Disabled then
		modifier = Enum.StudioStyleGuideModifier.Disabled
	end

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size or UDim2.new(1, 0, 0, Constants.DefaultProgressBarHeight),
		LayoutOrder = props.LayoutOrder,
		ZIndex = props.ZIndex,
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, modifier),
		BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder, modifier),
	}, {
		Bar = React.createElement("Frame", {
			BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, modifier),
			BorderSizePixel = 0,
			Size = UDim2.fromScale(alpha, 1),
			ClipsDescendants = true,
			ZIndex = 1,
		}, {
			Left = React.createElement("TextLabel", {
				BackgroundTransparency = 1,
				Size = UDim2.fromScale(1 / alpha, 1) - UDim2.fromOffset(0, 1),
				Font = Constants.DefaultFont,
				TextSize = Constants.DefaultTextSize,
				TextColor3 = Color3.fromRGB(12, 12, 12),
				TextTransparency = if props.Disabled then 0.5 else 0,
				Text = text,
			}),
		}),
		Under = React.createElement("TextLabel", {
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(1, 1) - UDim2.fromOffset(0, 1),
			Font = Constants.DefaultFont,
			TextSize = Constants.DefaultTextSize,
			TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText, modifier),
			Text = text,
			ZIndex = 0,
		}),
	})
end

return ProgressBar


--[=[
	@class RadioButton

	An input element similar to a [Checkbox] which can either be selected or not selected.
	This should be used for an option in a mutually exclusive group of options (the user can
	only select one out of the group). This grouping behavior is not included and must be
	implemented separately.

	| Dark | Light |
	| - | - |
	| ![Dark](/StudioComponents/components/radiobutton/dark.png) | ![Light](/StudioComponents/components/radiobutton/light.png) |

	The props and behavior for this component are the same as [Checkbox]. Importantly, this is
	also a controlled component, which means it does not manage its own selected state. A value
	must be passed to the `Value` prop and a callback should be passed to the `OnChanged` prop.
	For example:

	```lua
	local function MyComponent()
		local selected, setSelected = React.useState(false)
		return React.createElement(StudioComponents.RadioButton, {
			Value = selected,
			OnChanged = setSelected,
		})
	end
	```

	For more information about customizing this component via props, refer to the documentation
	for [Checkbox]. The default height for this component is found in [Constants.DefaultToggleHeight].
]=]

local React = require("@pkg/@jsdotlua/react")

local BaseLabelledToggle = require("./Foundation/BaseLabelledToggle")
local useTheme = require("../Hooks/useTheme")

local PADDING = 1
local INNER_PADDING = 3

--[=[
	@within RadioButton
	@interface Props
	@tag Component Props

	@field ... CommonProps
	@field Value boolean?
	@field OnChanged (() -> ())?
	@field Label string?
	@field ContentAlignment HorizontalAlignment?
	@field ButtonAlignment HorizontalAlignment?
]=]

type RadioButtonProps = BaseLabelledToggle.BaseLabelledToggleConsumerProps & {
	Value: boolean,
}

local function RadioButton(props: RadioButtonProps)
	local theme = useTheme()
	local mergedProps = table.clone(props) :: BaseLabelledToggle.BaseLabelledToggleProps

	function mergedProps.RenderButton(subProps: { Hovered: boolean })
		local mainModifier = Enum.StudioStyleGuideModifier.Default
		if props.Disabled then
			mainModifier = Enum.StudioStyleGuideModifier.Disabled
		elseif subProps.Hovered then
			mainModifier = Enum.StudioStyleGuideModifier.Hover
		end

		return React.createElement("Frame", {
			BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.CheckedFieldBackground, mainModifier),
			BackgroundTransparency = 0,
			Size = UDim2.new(1, -PADDING * 2, 1, -PADDING * 2),
			Position = UDim2.fromOffset(1, PADDING),
		}, {
			Corner = React.createElement("UICorner", {
				CornerRadius = UDim.new(0.5, 0),
			}),
			Stroke = React.createElement("UIStroke", {
				Color = theme:GetColor(Enum.StudioStyleGuideColor.CheckedFieldBorder, mainModifier),
				Transparency = 0,
			}),
			Inner = if props.Value == true
				then React.createElement("Frame", {
					Size = UDim2.new(1, -INNER_PADDING * 2, 1, -INNER_PADDING * 2),
					Position = UDim2.fromOffset(INNER_PADDING, INNER_PADDING),
					BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton, mainModifier),
					BackgroundTransparency = 0.25,
				}, {
					Corner = React.createElement("UICorner", {
						CornerRadius = UDim.new(0.5, 0),
					}),
				})
				else nil,
		})
	end

	return React.createElement(BaseLabelledToggle, mergedProps)
end

return RadioButton


return {
	ScrollBarThickness = 16,
	WheelScrollAmount = 48,
	ArrowScrollAmount = 16,
}


local React = require("@pkg/@jsdotlua/react")

local useMouseDrag = require("../../Hooks/useMouseDrag")
local useTheme = require("../../Hooks/useTheme")

local Constants = require("./Constants")
local ScrollBarArrow = require("./ScrollBarArrow")
local Types = require("./Types")

local SCROLLBAR_THICKNESS = Constants.ScrollBarThickness
local INPUT_MOVE = Enum.UserInputType.MouseMovement

local function flipVector2(vector: Vector2, shouldFlip: boolean)
	return if shouldFlip then Vector2.new(vector.Y, vector.X) else vector
end

local function flipUDim2(udim: UDim2, shouldFlip: boolean)
	return if shouldFlip then UDim2.new(udim.Height, udim.Width) else udim
end

type ScrollData = Types.ScrollData

type ScrollBarProps = {
	BumpScroll: (scrollVector: Vector2) -> (),
	Orientation: Types.BarOrientation,
	ScrollData: React.Binding<ScrollData>,
	ScrollOffset: React.Binding<Vector2>,
	SetScrollOffset: (offset: Vector2) -> (),
	Disabled: boolean?,
}

local function ScrollBar(props: ScrollBarProps)
	local vertical = props.Orientation == "Vertical"
	local scrollDataBinding = props.ScrollData

	local theme = useTheme()

	local hovered, setHovered = React.useState(false)
	local dragStartMouse = React.useRef(nil) :: { current: Vector2? }
	local dragStartCanvas = React.useRef(nil) :: { current: Vector2? }

	local function onDragStarted(_, input: InputObject)
		dragStartMouse.current = Vector2.new(input.Position.X, input.Position.Y)
		dragStartCanvas.current = props.ScrollOffset:getValue()
	end

	local function onDragEnded()
		dragStartMouse.current = nil
		dragStartCanvas.current = nil
	end

	local function onDragged(_, input: InputObject)
		local scrollData = scrollDataBinding:getValue()
		local contentSize = scrollData.ContentSize
		local windowSize = scrollData.WindowSize
		local innerBarSize = scrollData.InnerBarSize

		local offsetFrom = dragStartCanvas.current :: Vector2
		local mouseFrom = dragStartMouse.current :: Vector2
		local mouseDelta = Vector2.new(input.Position.X, input.Position.Y) - mouseFrom

		local shiftAlpha = mouseDelta / (innerBarSize - scrollData.BarSize)
		local overflow = contentSize - windowSize

		local newOffset = offsetFrom + overflow * shiftAlpha
		newOffset = newOffset:Min(overflow)
		newOffset = newOffset:Max(Vector2.zero)

		local freshScrollOffset = props.ScrollOffset:getValue()
		if vertical then
			props.SetScrollOffset(Vector2.new(freshScrollOffset.X, newOffset.Y))
		else
			props.SetScrollOffset(Vector2.new(newOffset.X, freshScrollOffset.Y))
		end
	end

	local dragDeps = { props.ScrollOffset, props.SetScrollOffset } :: { unknown }
	local drag = useMouseDrag(onDragged, dragDeps, onDragStarted, onDragEnded)

	React.useEffect(function()
		if props.Disabled and drag.isActive() then
			drag.cancel()
			onDragEnded()
		end
		-- if props.Disabled then
		-- 	setHovered(false)
		-- end
	end, { props.Disabled, drag.isActive() })

	local modifier = Enum.StudioStyleGuideModifier.Default
	if props.Disabled then
		modifier = Enum.StudioStyleGuideModifier.Disabled
	elseif hovered or drag.isActive() then
		modifier = Enum.StudioStyleGuideModifier.Pressed
	end

	return React.createElement("Frame", {
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ScrollBarBackground),
		BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
		Visible = scrollDataBinding:map(function(data: ScrollData)
			return if vertical then data.BarVisible.Y else data.BarVisible.X
		end),
		AnchorPoint = flipVector2(Vector2.new(1, 0), not vertical),
		Position = flipUDim2(UDim2.fromScale(1, 0), not vertical),
		Size = scrollDataBinding:map(function(data)
			local extra = if (vertical and data.BarVisible.X) or (not vertical and data.BarVisible.Y)
				then -SCROLLBAR_THICKNESS - 1
				else 0
			return flipUDim2(UDim2.new(0, SCROLLBAR_THICKNESS, 1, extra), not vertical)
		end),
	}, {
		Arrow0 = React.createElement(ScrollBarArrow, {
			Side = 0,
			Orientation = props.Orientation,
			BumpScroll = props.BumpScroll,
			Disabled = props.Disabled,
		}),
		Arrow1 = React.createElement(ScrollBarArrow, {
			Side = 1,
			Orientation = props.Orientation,
			BumpScroll = props.BumpScroll,
			Position = flipUDim2(UDim2.fromScale(0, 1), not vertical),
			AnchorPoint = flipVector2(Vector2.new(0, 1), not vertical),
			Disabled = props.Disabled,
		}),
		Region = React.createElement("Frame", {
			BackgroundTransparency = 1,
			Position = flipUDim2(UDim2.fromOffset(0, SCROLLBAR_THICKNESS + 1), not vertical),
			Size = flipUDim2(UDim2.new(1, 0, 1, -(SCROLLBAR_THICKNESS + 1) * 2), not vertical),
		}, {
			Handle = React.createElement("Frame", {
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ScrollBar, modifier),
				BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border, modifier),
				Size = scrollDataBinding:map(function(data: ScrollData)
					local size = if vertical then data.BarSize.Y else data.BarSize.X
					return flipUDim2(UDim2.new(1, 0, 0, size), not vertical)
				end),
				Position = scrollDataBinding:map(function(data: ScrollData)
					local position = if vertical then data.BarPosition.Y else data.BarPosition.X
					return flipUDim2(UDim2.fromScale(0, position), not vertical)
				end),
				AnchorPoint = scrollDataBinding:map(function(data: ScrollData)
					local position = if vertical then data.BarPosition.Y else data.BarPosition.X
					return flipVector2(Vector2.new(0, position), not vertical)
				end),
				[React.Event.InputBegan] = function(rbx: Frame, input: InputObject)
					if input.UserInputType == INPUT_MOVE then
						setHovered(true)
					end
					if not props.Disabled then
						drag.onInputBegan(rbx, input)
					end
				end,
				[React.Event.InputChanged] = function(rbx: Frame, input: InputObject)
					if not props.Disabled then
						drag.onInputChanged(rbx, input)
					end
				end,
				[React.Event.InputEnded] = function(rbx: Frame, input: InputObject)
					if input.UserInputType == INPUT_MOVE then
						setHovered(false)
					end
					if not props.Disabled then
						drag.onInputEnded(rbx, input)
					end
				end,
			}),
		}),
	})
end

return ScrollBar


local RunService = game:GetService("RunService")

local React = require("@pkg/@jsdotlua/react")

local useFreshCallback = require("../../Hooks/useFreshCallback")
local useTheme = require("../../Hooks/useTheme")

local Constants = require("./Constants")
local Types = require("./Types")

local ARROW_IMAGE = "rbxassetid://6677623152"
local SCROLLBAR_THICKNESS = Constants.ScrollBarThickness
local ARROW_SCROLL_AMOUNT = Constants.ArrowScrollAmount

local function getArrowImageOffset(orientation: Types.BarOrientation, side: number)
	if orientation == "Vertical" then
		return Vector2.new(0, side * SCROLLBAR_THICKNESS)
	end
	return Vector2.new(SCROLLBAR_THICKNESS, side * SCROLLBAR_THICKNESS)
end

local function getScrollVector(orientation: Types.BarOrientation, side: number)
	local scrollAmount = ARROW_SCROLL_AMOUNT * (if side == 0 then -1 else 1)
	local scrollVector = Vector2.new(0, scrollAmount)
	if orientation == "Horizontal" then
		scrollVector = Vector2.new(scrollAmount, 0)
	end
	return scrollVector
end

type ScrollBarArrowProps = {
	BumpScroll: (scrollVector: Vector2) -> (),
	Orientation: Types.BarOrientation,
	Side: number,
	Position: UDim2?,
	AnchorPoint: Vector2?,
	Disabled: boolean?,
}

local function ScrollBarArrow(props: ScrollBarArrowProps)
	local theme = useTheme()
	local connection = React.useRef(nil) :: { current: RBXScriptConnection? }

	local pressed, setPressed = React.useState(false)
	local hovered, setHovered = React.useState(false)

	local modifier = Enum.StudioStyleGuideModifier.Default
	if props.Disabled then
		modifier = Enum.StudioStyleGuideModifier.Disabled
	elseif pressed then
		modifier = Enum.StudioStyleGuideModifier.Pressed
	end

	local maybeScroll = useFreshCallback(function()
		if hovered then
			props.BumpScroll(getScrollVector(props.Orientation, props.Side))
		end
	end, { hovered, props.BumpScroll, props.Orientation, props.Side } :: { unknown })

	local function startHolding()
		if connection.current then
			connection.current:Disconnect()
		end
		local nextScroll = os.clock() + 0.35
		connection.current = RunService.PostSimulation:Connect(function()
			if os.clock() >= nextScroll then
				maybeScroll()
				nextScroll += 0.05
			end
		end)
		props.BumpScroll(getScrollVector(props.Orientation, props.Side))
	end

	local function stopHolding()
		if connection.current then
			connection.current:Disconnect()
			connection.current = nil
		end
	end
	React.useEffect(stopHolding, {})

	React.useEffect(function()
		if props.Disabled and pressed then
			stopHolding()
			setPressed(false)
		end
		if props.Disabled then
			setHovered(false)
		end
	end, { props.Disabled, pressed })

	local hostClass = "ImageLabel"
	local hostProps = {
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ScrollBar, modifier),
		BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border, modifier),
		Size = UDim2.fromOffset(SCROLLBAR_THICKNESS, SCROLLBAR_THICKNESS),
		Image = ARROW_IMAGE,
		ImageRectSize = Vector2.new(SCROLLBAR_THICKNESS, SCROLLBAR_THICKNESS),
		ImageRectOffset = getArrowImageOffset(props.Orientation, props.Side),
		ImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.TitlebarText, modifier),
		Position = props.Position,
		AnchorPoint = props.AnchorPoint,
	}

	if props.Disabled ~= true then
		hostClass = "ImageButton"
		hostProps.AutoButtonColor = false
		hostProps[React.Event.InputBegan] = function(_, input: InputObject)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				setHovered(true)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				setPressed(true)
				startHolding()
			end
		end
		hostProps[React.Event.InputEnded] = function(_, input: InputObject)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				setHovered(false)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				setPressed(false)
				stopHolding()
			end
		end
	end

	return React.createElement(hostClass, hostProps)
end

return ScrollBarArrow


export type ScrollData = {
	ContentSize: Vector2,
	WindowSize: Vector2,
	InnerBarSize: Vector2,
	BarVisible: { X: boolean, Y: boolean },
	BarSize: Vector2,
	BarPosition: Vector2,
}

export type BarOrientation = "Horizontal" | "Vertical"

return {}


--[=[
	@class ScrollFrame

	A container with scrollable contents. This works the same way as a built-in [ScrollingFrame] but
	has visual changes to match the appearance of built-in scrollers in Studio.

	| Dark | Light |
	| - | - |
	| ![Dark](/StudioComponents/components/scrollframe/dark.png) | ![Light](/StudioComponents/components/scrollframe/light.png) |

	ScrollFrames automatically size their canvas to fit their contents, which are passed via the
	`children` parameters in createElement. By default, children are laid out with a [UIListLayout];
	this can be overriden via the `Layout` prop. Either "UIListLayout" or "UIGridLayout" may be
	passed to `Layout.ClassName`. Any other properties to be applied to the layout should also be
	passed in the `Layout` prop. For example:

	```lua
	local function MyComponent()
		return React.createElement(StudioComponents.ScrollFrame, {
			Layout = {
				ClassName = "UIListLayout",
				Padding = UDim.new(0, 10),
				FillDirection = Enum.FillDirection.Horizontal,
			}
		}, {
			SomeChild = React.createElement(...),
			AnotherChild = React.createElement(...),
		})
	end
	```

	By default, scrolling on both the X and Y axes is enabled. To configure this, pass an
	[Enum.ScrollingDirection] value to the `ScrollingDirection` prop. Padding around the outside of
	contents can also be configured via the `PaddingLeft`, `PaddingRight`, `PaddingTop`, and
	`PaddingBottom` props.

	:::info
	The built-in Studio scrollers were changed during this project's lifetime to be significantly
	narrower. This component retains the old, wider, size because it is more accessible.
	:::
]=]

local React = require("@pkg/@jsdotlua/react")

local CommonProps = require("../../CommonProps")
local useTheme = require("../../Hooks/useTheme")

local Constants = require("./Constants")
local ScrollBar = require("./ScrollBar")
local Types = require("./Types")

local SCROLL_WHEEL_SPEED = Constants.WheelScrollAmount
local SCROLLBAR_THICKNESS = Constants.ScrollBarThickness
local SCROLLBAR_MIN_LENGTH = SCROLLBAR_THICKNESS

local function clampVector2(v: Vector2, vmin: Vector2, vmax: Vector2)
	return Vector2.new(math.clamp(v.X, vmin.X, vmax.X), math.clamp(v.Y, vmin.Y, vmax.Y))
end

type ScrollData = Types.ScrollData

local defaultLayout = {
	ClassName = "UIListLayout",
	SortOrder = Enum.SortOrder.LayoutOrder,
}

--[=[
	@within ScrollFrame
	@interface Props
	@tag Component Props

	@field ... CommonProps
	@field Layout { ClassName: string, [string]: any }?
	@field ScrollingDirection Enum.ScrollingDirection?
	@field PaddingLeft UDim?
	@field PaddingRight UDim?
	@field PaddingTop UDim?
	@field PaddingBottom UDim?
	@field OnScrolled ((scrollOffset: Vector2) -> ())?
	@field children React.ReactNode
]=]

type ScrollFrameProps = CommonProps.T & {
	ScrollingDirection: Enum.ScrollingDirection?,
	PaddingLeft: UDim?,
	PaddingRight: UDim?,
	PaddingTop: UDim?,
	PaddingBottom: UDim?,
	OnScrolled: ((scrollOffset: Vector2) -> ())?,
	Layout: {
		ClassName: string, -- Luau: should be "UIListLayout | "UIGridLayout" but causes problems
		[string]: any, -- native props
	}?,
	children: React.ReactNode,
}

local function computePaddingSize(data: {
	PaddingLeft: UDim?,
	PaddingRight: UDim?,
	PaddingTop: UDim?,
	PaddingBottom: UDim?,
	WindowSize: Vector2,
})
	local paddingX = (data.PaddingLeft or UDim.new(0, 0)) + (data.PaddingRight or UDim.new(0, 0))
	local paddingY = (data.PaddingTop or UDim.new(0, 0)) + (data.PaddingBottom or UDim.new(0, 0))
	return Vector2.new(
		paddingX.Scale * data.WindowSize.X + paddingX.Offset,
		paddingY.Scale * data.WindowSize.Y + paddingY.Offset
	)
end

local function getRegionData(props: ScrollFrameProps, contentSize: Vector2, windowSize: Vector2)
	local scrollingEnabled = {
		X = props.ScrollingDirection ~= Enum.ScrollingDirection.Y,
		Y = props.ScrollingDirection ~= Enum.ScrollingDirection.X,
	}

	local paddingSize = computePaddingSize({
		PaddingLeft = props.PaddingLeft,
		PaddingRight = props.PaddingRight,
		PaddingTop = props.PaddingTop,
		PaddingBottom = props.PaddingBottom,
		WindowSize = windowSize,
	})
	contentSize += paddingSize

	local overflow = contentSize - windowSize
	local visible = { X = false, Y = false }
	for _ = 1, 2 do -- in case Y relayout affects X, we do it twice
		if overflow.X > 0 and not visible.X and scrollingEnabled.X then
			windowSize -= Vector2.new(0, SCROLLBAR_THICKNESS + 1)
			visible.X = true
		end
		if overflow.Y > 0 and not visible.Y and scrollingEnabled.Y then
			windowSize -= Vector2.new(SCROLLBAR_THICKNESS + 1, 0)
			visible.Y = true
		end
		overflow = contentSize - windowSize
	end

	return {
		ContentSize = contentSize,
		WindowSize = windowSize,
		Overflow = overflow,
		Visible = visible,
	}
end

local function ScrollFrame(props: ScrollFrameProps)
	local theme = useTheme()

	local scrollOffsetBinding, setScrollOffsetBinding = React.useBinding(Vector2.zero)
	local contentSizeBinding, setContentSizeBinding = React.useBinding(Vector2.zero)
	local windowSizeBinding, setWindowSizeBinding = React.useBinding(Vector2.zero)

	local function setScrollOffset(offset: Vector2)
		if offset ~= scrollOffsetBinding:getValue() then
			setScrollOffsetBinding(offset)
			if props.OnScrolled then
				props.OnScrolled(offset)
			end
		end
	end

	local function getScrollData(): ScrollData
		local regionData = getRegionData(props, contentSizeBinding:getValue(), windowSizeBinding:getValue())
		local contentSize = regionData.ContentSize
		local windowSize = regionData.WindowSize
		local overflow = regionData.Overflow
		local visible = regionData.Visible

		local alpha = clampVector2(windowSize / contentSize, Vector2.zero, Vector2.one)
		local innerBarSize = windowSize - Vector2.one * (SCROLLBAR_THICKNESS + 1) * 2

		local scrollOffset = scrollOffsetBinding:getValue()
		local offset = alpha * innerBarSize
		offset = offset:Max(Vector2.one * SCROLLBAR_MIN_LENGTH)
		offset = offset:Min(innerBarSize + Vector2.one * 2)
		offset = offset:Max(Vector2.zero)

		local sizeX = if visible.X then offset.X else 0
		local sizeY = if visible.Y then offset.Y else 0
		local size = Vector2.new(sizeX, sizeY)
		local position = clampVector2(scrollOffset / overflow, Vector2.zero, Vector2.one)

		return {
			ContentSize = contentSize,
			WindowSize = windowSize,
			InnerBarSize = innerBarSize,
			BarVisible = visible,
			BarSize = size,
			BarPosition = position,
		}
	end

	local function revalidateScrollOffset()
		local regionData = getRegionData(props, contentSizeBinding:getValue(), windowSizeBinding:getValue())
		local currentOffset = scrollOffsetBinding:getValue()
		local maxOffset = regionData.Overflow:Max(Vector2.zero)
		maxOffset = maxOffset:Min(
			Vector2.new(
				if regionData.Visible.X then maxOffset.X else 0,
				if regionData.Visible.Y then maxOffset.Y else 0
			)
		)
		local newOffset = currentOffset:Min(maxOffset)
		if newOffset ~= currentOffset then
			setScrollOffset(newOffset)
		end
	end

	React.useEffect(
		revalidateScrollOffset,
		{
			props.ScrollingDirection,
			props.Layout,
			props.PaddingLeft,
			props.PaddingRight,
			props.PaddingBottom,
			props.PaddingTop,
		} :: { unknown }
	)

	local function bumpScroll(scrollVector: Vector2)
		local scrollData = getScrollData()
		local windowSize = scrollData.WindowSize
		local contentSize = scrollData.ContentSize
		local scrollOffset = scrollOffsetBinding:getValue()
		local scrollTarget = scrollOffset + scrollVector
		local scrollBounds = contentSize - windowSize
		scrollBounds = scrollBounds:Max(Vector2.zero)
		setScrollOffset(clampVector2(scrollTarget, Vector2.zero, scrollBounds))
	end

	local function handleMainInput(_, input: InputObject)
		if props.Disabled then
			return
		elseif input.UserInputType == Enum.UserInputType.MouseWheel then
			local amount = SCROLL_WHEEL_SPEED * -input.Position.Z
			local scrollData = getScrollData()
			local shiftHeld = input:IsModifierKeyDown(Enum.ModifierKey.Shift)
			local scrollVector
			if shiftHeld then
				if scrollData.BarVisible.X then
					scrollVector = Vector2.new(amount, 0)
				end
			elseif scrollData.BarVisible.Y then
				scrollVector = Vector2.new(0, amount)
			elseif scrollData.BarVisible.X then
				scrollVector = Vector2.new(amount, 0)
			end
			if scrollVector then
				bumpScroll(scrollVector)
			end
		end
	end

	local scrollDataBinding = React.joinBindings({
		contentSizeBinding,
		windowSizeBinding,
		scrollOffsetBinding,
	}):map(getScrollData)

	local modifier = Enum.StudioStyleGuideModifier.Default
	if props.Disabled then
		modifier = Enum.StudioStyleGuideModifier.Disabled
	end

	local layoutBase = table.clone(props.Layout or defaultLayout)
	local layoutProps: { [any]: any } = {}
	for key, val in layoutBase :: { [string]: any } do
		if key ~= "ClassName" then
			layoutProps[key] = val
		end
	end
	layoutProps[React.Change.AbsoluteContentSize] = function(rbx: UIListLayout | UIGridLayout)
		setContentSizeBinding(rbx.AbsoluteContentSize)
		revalidateScrollOffset()
	end

	--[[
	Children need to be able to use actual window size for their sizes.
	To facilitate this, the parent Window frame's size equals the clipping area.
	However, this causes the layout in e.g. layout mode Center to lay elements out
	... from the center of the Window, rather than the Canvas.
	We fix this by simply overriding the alignments when the bars are visible
	... since those properties make no difference in those cases.
	]]
	layoutProps.HorizontalAlignment = scrollDataBinding:map(function(data: ScrollData)
		return if data.BarVisible.X then Enum.HorizontalAlignment.Left else layoutBase.HorizontalAlignment
	end)
	layoutProps.VerticalAlignment = scrollDataBinding:map(function(data: ScrollData)
		return if data.BarVisible.Y then Enum.VerticalAlignment.Top else layoutBase.VerticalAlignment
	end)

	local mainSize = scrollDataBinding:map(function(data: ScrollData)
		local windowSize = data.WindowSize:Max(Vector2.zero)
		return UDim2.fromOffset(windowSize.X, windowSize.Y)
	end)

	return React.createElement("Frame", {
		-- sinks scroll input that would otherwise zoom the studio camera
		-- also prevents the drag-box appearing on lmb-drag
		Active = true,
		LayoutOrder = props.LayoutOrder,
		ZIndex = props.ZIndex,
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size or UDim2.fromScale(1, 1),
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground, modifier),
		BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border, modifier),
		[React.Event.InputBegan] = handleMainInput,
		[React.Event.InputChanged] = handleMainInput,
		[React.Change.AbsoluteSize] = function(rbx: Frame)
			setWindowSizeBinding(rbx.AbsoluteSize)
			revalidateScrollOffset()
		end :: any,
	}, {
		Cover = if props.Disabled
			then React.createElement("Frame", {
				ZIndex = 2,
				Size = mainSize,
				BorderSizePixel = 0,
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
				BackgroundTransparency = 0.25,
			})
			else nil,
		Clipping = React.createElement("Frame", {
			Size = mainSize,
			BackgroundTransparency = 1,
			ClipsDescendants = true,
		}, {
			Window = React.createElement("Frame", {
				Size = UDim2.fromScale(1, 1),
				BackgroundTransparency = 1,
				Position = scrollOffsetBinding:map(function(offset: Vector2)
					return UDim2.fromOffset(-offset.X, -offset.Y)
				end),
			}, {
				Padding = React.createElement("UIPadding", {
					PaddingLeft = props.PaddingLeft,
					PaddingRight = props.PaddingRight,
					PaddingTop = props.PaddingTop,
					PaddingBottom = props.PaddingBottom,
				}),
				Layout = React.createElement(layoutBase.ClassName, layoutProps),
			}, props.children),
		}),
		VerticalScrollBar = React.createElement(ScrollBar, {
			Orientation = "Vertical" :: "Vertical", -- Luau
			ScrollData = scrollDataBinding,
			ScrollOffset = scrollOffsetBinding,
			SetScrollOffset = setScrollOffset,
			BumpScroll = bumpScroll,
			Disabled = props.Disabled,
		}),
		HorizontalScrollBar = React.createElement(ScrollBar, {
			Orientation = "Horizontal" :: "Horizontal", -- Luau
			ScrollData = scrollDataBinding,
			ScrollOffset = scrollOffsetBinding,
			SetScrollOffset = setScrollOffset,
			BumpScroll = bumpScroll,
			Disabled = props.Disabled,
		}),
	})
end

return ScrollFrame


--[=[
	@class Slider

	A component for selecting a numeric value from a range of values with an optional increment.
	These are seen in some number-valued properties in the built-in Properties widget, as well as in
	various built-in plugins such as the Terrain Editor.

	| Dark | Light |
	| - | - |
	| ![Dark](/StudioComponents/components/slider/dark.png) | ![Light](/StudioComponents/components/slider/light.png) |

	As with other components in this library, this is a controlled component. You should pass a
	value to the `Value` prop representing the current value, as well as a callback to the `OnChanged`
	prop which will be run when the user changes the value via dragging or clicking on the slider.

	In addition to these, you must also provide a `Min` and a `Max` prop, which together define the
	range of the slider. Optionally, a `Step` prop can be provided, which defines the increment of
	the slider. This defaults to 0, which allows any value in the range. For a complete example:

	```lua
	local function MyComponent()
		local value, setValue = React.useState(1)
		return React.createElement(StudioComponents.Slider, {
			Value = value,
			OnChanged = setValue,
			Min = 0,
			Max = 10,
			Step = 1,
		})
	end
	```

	Optionally, an `OnCompleted` callback prop can be provided. This will be called with the latest
	value of the Slider when sliding is finished. It is also called if, while sliding is in progress,
	the component becomes Disabled via props or is unmounted.

	Two further props can optionally be provided:
	1. `Border` determines whether a border is drawn around the component.
	This is useful for giving visual feedback when the slider is hovered or selected.
	2. `Background` determines whether the component has a visible background.
	If this is value is missing or set to `false`, any border will also be hidden.

	Both of these props default to `true`.

	By default, the height of sliders is equal to the value found in [Constants.DefaultSliderHeight].
	While this can be overriden by props, in order to keep inputs accessible it is not recommended
	to make the component any smaller than this.
]=]

local React = require("@pkg/@jsdotlua/react")

local CommonProps = require("../CommonProps")
local Constants = require("../Constants")

local useFreshCallback = require("../Hooks/useFreshCallback")
local useMouseDrag = require("../Hooks/useMouseDrag")
local useTheme = require("../Hooks/useTheme")

--[=[
	@within Slider
	@interface Props
	@tag Component Props

	@field ... CommonProps
	@field Value number
	@field OnChanged ((newValue: number) -> ())?
	@field OnCompleted ((newValue: number) -> ())?
	@field Min number
	@field Max number
	@field Step number?
	@field Border boolean?
	@field Background boolean?
]=]

type SliderProps = CommonProps.T & {
	Value: number,
	OnChanged: ((newValue: number) -> ())?,
	OnCompleted: ((newValue: number) -> ())?,
	Min: number,
	Max: number,
	Step: number?,
	Border: boolean?,
	Background: boolean?,
}

local PADDING_BAR_SIDE = 3
local PADDING_REGION_TOP = 1
local PADDING_REGION_SIDE = 6

local INPUT_MOVE = Enum.UserInputType.MouseMovement

local function Slider(props: SliderProps)
	local theme = useTheme()

	local onChanged: (number) -> () = props.OnChanged or function() end

	local dragCallback = function(rbx: GuiObject, input: InputObject)
		local regionPos = rbx.AbsolutePosition.X + PADDING_REGION_SIDE
		local regionSize = rbx.AbsoluteSize.X - PADDING_REGION_SIDE * 2
		local inputPos = input.Position.X

		local alpha = (inputPos - regionPos) / regionSize
		local step = props.Step or 0

		local value = props.Min * (1 - alpha) + props.Max * alpha
		if step > 0 then
			value = math.round(value / step) * step
		end
		value = math.clamp(value, props.Min, props.Max)
		if value ~= props.Value then
			onChanged(value)
		end
	end

	local dragEndedCallback = useFreshCallback(function()
		if props.OnCompleted then
			props.OnCompleted(props.Value)
		end
	end, { props.Value, props.OnCompleted } :: { unknown })

	local dragDeps = { props.Value, props.Min, props.Max, props.Step, props.OnCompleted, onChanged } :: { unknown }
	local drag = useMouseDrag(dragCallback, dragDeps, nil, dragEndedCallback)

	local hovered, setHovered = React.useState(false)
	local mainModifier = Enum.StudioStyleGuideModifier.Default
	if props.Disabled then
		mainModifier = Enum.StudioStyleGuideModifier.Disabled
	end

	local handleModifier = Enum.StudioStyleGuideModifier.Default
	if props.Disabled then
		handleModifier = Enum.StudioStyleGuideModifier.Disabled
	elseif hovered or drag.isActive() then
		handleModifier = Enum.StudioStyleGuideModifier.Hover
	end

	local handleFill = theme:GetColor(Enum.StudioStyleGuideColor.Button, handleModifier)
	local handleBorder = theme:GetColor(Enum.StudioStyleGuideColor.Border, handleModifier)

	React.useEffect(function()
		if props.Disabled and drag.isActive() then
			drag.cancel()
		end
	end, { props.Disabled, drag.isActive() })

	local function inputBegan(rbx: Frame, input: InputObject)
		if input.UserInputType == INPUT_MOVE then
			setHovered(true)
		end
		if not props.Disabled then
			drag.onInputBegan(rbx, input)
		end
	end

	local function inputChanged(rbx: Frame, input: InputObject)
		if not props.Disabled then
			drag.onInputChanged(rbx, input)
		end
	end

	local function inputEnded(rbx: Frame, input: InputObject)
		if input.UserInputType == INPUT_MOVE then
			setHovered(false)
		end
		if not props.Disabled then
			drag.onInputEnded(rbx, input)
		end
	end

	-- if we use a Frame here, the 2d studio selection rectangle will appear when dragging
	-- we could prevent that using Active = true, but that displays the Click cursor
	-- ... the best workaround is a TextButton with Active = false
	return React.createElement("TextButton", {
		Text = "",
		Active = false,
		AutoButtonColor = false,
		Size = props.Size or UDim2.new(1, 0, 0, Constants.DefaultSliderHeight),
		Position = props.Position,
		AnchorPoint = props.AnchorPoint,
		LayoutOrder = props.LayoutOrder,
		ZIndex = props.ZIndex,
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground, mainModifier),
		BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.InputFieldBorder, handleModifier),
		BorderMode = Enum.BorderMode.Inset,
		BorderSizePixel = if props.Border == false then 0 else 1,
		BackgroundTransparency = if props.Background == false then 1 else 0,
		[React.Event.InputBegan] = inputBegan,
		[React.Event.InputChanged] = inputChanged,
		[React.Event.InputEnded] = inputEnded,
	}, {
		Bar = React.createElement("Frame", {
			ZIndex = 1,
			AnchorPoint = Vector2.new(0, 0.5),
			Position = UDim2.new(0, PADDING_BAR_SIDE, 0.5, 0),
			Size = UDim2.new(1, -PADDING_BAR_SIDE * 2, 0, 2),
			BorderSizePixel = 0,
			BackgroundTransparency = props.Disabled and 0.4 or 0,
			BackgroundColor3 = theme:GetColor(
				-- surprising values, but provides correct colors
				Enum.StudioStyleGuideColor.TitlebarText,
				Enum.StudioStyleGuideModifier.Disabled
			),
		}),
		HandleRegion = React.createElement("Frame", {
			ZIndex = 2,
			Position = UDim2.fromOffset(PADDING_REGION_SIDE, PADDING_REGION_TOP),
			Size = UDim2.new(1, -PADDING_REGION_SIDE * 2, 1, -PADDING_REGION_TOP * 2),
			BackgroundTransparency = 1,
		}, {
			Handle = React.createElement("Frame", {
				AnchorPoint = Vector2.new(0.5, 0),
				Position = UDim2.fromScale((props.Value - props.Min) / (props.Max - props.Min), 0),
				Size = UDim2.new(0, 10, 1, 0),
				BorderMode = Enum.BorderMode.Inset,
				BorderSizePixel = 1,
				BorderColor3 = handleBorder:Lerp(handleFill, props.Disabled and 0.5 or 0),
				BackgroundColor3 = handleFill,
			}),
		}),
	})
end

return Slider


--[=[
	@class Splitter

	A container frame similar to a [Background] but split into two panels, with a draggable control
	for	resizing the panels within the container. Resizing one section to be larger will reduce the
	size of the other section, and vice versa. This is useful for letting users resize content.

	| Dark | Light |
	| - | - |
	| ![Dark](/StudioComponents/components/splitter/dark.png) | ![Light](/StudioComponents/components/splitter/light.png) |

	This is a controlled component. The current split location should be passed as a number between
	0 and 1 to the `Alpha` prop, and a callback should be passed to the `OnChanged` prop, which
	is run with the new alpha value when the user uses the splitter.

	You can also optionally provide `MinAlpha` and `MaxAlpha` props (numbers between 0 and 1) which
	limit the resizing. These values default to 0.1 and 0.9.

	To render children in each side, use the `children` parameters in createElement and provide the
	keys `Side0` and `Side1`. For a complete example:

	```lua
	local function MyComponent()
		local division, setDivision = React.useState(0.5)
		return React.createElement(StudioComponents.Splitter, {
			Alpha = division,
			OnChanged = setDivision,
		}, {
			Side0 = React.createElement(...),
			Side1 = React.createElement(...),
		})
	end
	```

	By default, the split is horizontal, which means that the frame is split into a left and right
	side. This can be changed, for example to a vertical split (top and bottom), by providing an
	[Enum.FillDirection] value to the `FillDirection` prop.

	This component can use your system's splitter mouse icons when interacting with the splitter bar.
	To enable this behavior, ensure you have rendered a [PluginProvider] somewhere higher up in
	the tree.
]=]

local React = require("@pkg/@jsdotlua/react")

local CommonProps = require("../CommonProps")

local useMouseDrag = require("../Hooks/useMouseDrag")
local useMouseIcon = require("../Hooks/useMouseIcon")
local useTheme = require("../Hooks/useTheme")

local function flipVector2(vector: Vector2, shouldFlip: boolean)
	return if shouldFlip then Vector2.new(vector.Y, vector.X) else vector
end

local function flipUDim2(udim: UDim2, shouldFlip: boolean)
	return if shouldFlip then UDim2.new(udim.Height, udim.Width) else udim
end

local HANDLE_THICKNESS = 6
local DEFAULT_MIN_ALPHA = 0.1
local DEFAULT_MAX_ALPHA = 0.9

--[=[
	@within Splitter
	@interface Props
	@tag Component Props

	@field ... CommonProps

	@field Alpha number
	@field OnChanged ((newAlpha: number) -> ())?
	@field FillDirection Enum.FillDirection?
	@field MinAlpha number?
	@field MaxAlpha number?

	@field children { Side0: React.ReactNode?, Side1: React.ReactNode? }?
]=]

export type SplitterProps = CommonProps.T & {
	Alpha: number,
	OnChanged: ((newAlpha: number) -> ())?,
	FillDirection: Enum.FillDirection?,
	MinAlpha: number?,
	MaxAlpha: number?,
	children: {
		Side0: React.ReactNode,
		Side1: React.ReactNode,
	}?,
}

local icons = {
	[Enum.FillDirection.Horizontal] = "rbxasset://SystemCursors/SplitEW",
	[Enum.FillDirection.Vertical] = "rbxasset://SystemCursors/SplitNS",
}

local function Splitter(props: SplitterProps)
	local theme = useTheme()
	local mouseIcon = useMouseIcon()

	local fillDirection = props.FillDirection or Enum.FillDirection.Horizontal
	local children = props.children or {
		Side0 = nil,
		Side1 = nil,
	}

	local drag = useMouseDrag(function(bar: GuiObject, input: InputObject)
		local region = bar.Parent :: Frame
		local position = Vector2.new(input.Position.X, input.Position.Y)
		local alpha = (position - region.AbsolutePosition) / region.AbsoluteSize
		alpha = alpha:Max(Vector2.one * (props.MinAlpha or DEFAULT_MIN_ALPHA))
		alpha = alpha:Min(Vector2.one * (props.MaxAlpha or DEFAULT_MAX_ALPHA))
		if props.OnChanged then
			if fillDirection == Enum.FillDirection.Horizontal and alpha.X ~= props.Alpha then
				props.OnChanged(alpha.X)
			elseif fillDirection == Enum.FillDirection.Vertical and alpha.Y ~= props.Alpha then
				props.OnChanged(alpha.Y)
			end
		end
	end, { props.Alpha, props.OnChanged, props.MinAlpha, props.MaxAlpha, fillDirection } :: { unknown })

	React.useEffect(function()
		if props.Disabled and drag.isActive() then
			drag.cancel()
		end
	end, { props.Disabled, drag.isActive() })

	local hovered, setHovered = React.useState(false)

	React.useEffect(function()
		if (hovered or drag.isActive()) and not props.Disabled then
			local icon = icons[fillDirection]
			mouseIcon.setIcon(icon)
		else
			mouseIcon.clearIcon()
		end
	end, { mouseIcon, hovered, drag.isActive(), props.Disabled, fillDirection } :: { unknown })

	local function onInputBegan(rbx: Frame, input: InputObject)
		if input.UserInputType == Enum.UserInputType.MouseMovement then
			setHovered(true)
		end
		if not props.Disabled then
			drag.onInputBegan(rbx, input)
		end
	end
	local function onInputChanged(rbx: Frame, input: InputObject)
		if not props.Disabled then
			drag.onInputChanged(rbx, input)
		end
	end
	local function onInputEnded(rbx: Frame, input: InputObject)
		if input.UserInputType == Enum.UserInputType.MouseMovement then
			setHovered(false)
		end
		if not props.Disabled then
			drag.onInputEnded(rbx, input)
		end
	end

	local shouldFlip = fillDirection == Enum.FillDirection.Vertical
	local alpha = props.Alpha
	alpha = math.max(alpha, props.MinAlpha or DEFAULT_MIN_ALPHA)
	alpha = math.min(alpha, props.MaxAlpha or DEFAULT_MAX_ALPHA)

	local handleTransparency = if props.Disabled then 0.75 else 0
	local handleColorStyle = Enum.StudioStyleGuideColor.DialogButton
	if props.Disabled then
		handleColorStyle = Enum.StudioStyleGuideColor.Border
	end

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size or UDim2.fromScale(1, 1),
		LayoutOrder = props.LayoutOrder,
		ZIndex = props.ZIndex,
		BackgroundTransparency = 1,
	}, {
		Handle = React.createElement("Frame", {
			Active = true, -- prevents the drag-box when in coregui mode
			AnchorPoint = flipVector2(Vector2.new(0.5, 0), shouldFlip),
			Position = flipUDim2(UDim2.fromScale(alpha, 0), shouldFlip),
			Size = flipUDim2(UDim2.new(0, HANDLE_THICKNESS, 1, 0), shouldFlip),
			BackgroundTransparency = handleTransparency,
			BackgroundColor3 = theme:GetColor(handleColorStyle),
			BorderSizePixel = 0,
			[React.Event.InputBegan] = onInputBegan,
			[React.Event.InputChanged] = onInputChanged,
			[React.Event.InputEnded] = onInputEnded,
			ZIndex = 1,
		}, {
			LeftBorder = not props.Disabled and React.createElement("Frame", {
				Size = flipUDim2(UDim2.new(0, 1, 1, 0), shouldFlip),
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
				BorderSizePixel = 0,
			}),
			RightBorder = not props.Disabled and React.createElement("Frame", {
				Position = flipUDim2(UDim2.new(1, -1, 0, 0), shouldFlip),
				Size = flipUDim2(UDim2.new(0, 1, 1, 0), shouldFlip),
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
				BorderSizePixel = 0,
			}),
		}),
		Side0 = React.createElement("Frame", {
			Size = flipUDim2(UDim2.new(alpha, -math.floor(HANDLE_THICKNESS / 2), 1, 0), shouldFlip),
			BackgroundTransparency = 1,
			ClipsDescendants = true,
			ZIndex = 0,
		}, {
			Child = children.Side0,
		}),
		Side1 = React.createElement("Frame", {
			AnchorPoint = flipVector2(Vector2.new(1, 0), shouldFlip),
			Position = flipUDim2(UDim2.fromScale(1, 0), shouldFlip),
			Size = flipUDim2(UDim2.new(1 - alpha, -math.ceil(HANDLE_THICKNESS / 2), 1, 0), shouldFlip),
			BackgroundTransparency = 1,
			ClipsDescendants = true,
			ZIndex = 0,
		}, {
			Child = children.Side1,
		}),
	})
end

return Splitter


--[=[
	@class TextInput

	A basic input field for entering any kind of text. This matches the appearance of the search
	boxes in the Explorer and Properties widgets, among other inputs in Studio.

	| Dark | Light |
	| - | - |
	| ![Dark](/StudioComponents/components/textinput/dark.png) | ![Light](/StudioComponents/components/textinput/light.png) |

	This is a controlled component, which means the current text should be passed in to the
	`Text` prop and a callback value to the `OnChanged` prop which gets run when the user attempts
	types in the input field. For example:

	```lua
	local function MyComponent()
		local text, setText = React.useState("")
		return React.createElement(StudioComponents.TextInput, {
			Text = text,
			OnChanged = setText,
		})
	end
	```

	This allows complete control over the text displayed and keeps the source of truth in your own
	code. This is helpful for consistency and controlling the state from elsewhere in the tree. It
	also allows you to easily filter what can be typed into the text input. For example, to only
	permit entering lowercase letters:

	```lua
	local function MyComponent()
		local text, setText = React.useState("")
		return React.createElement(StudioComponents.TextInput, {
			Text = text,
			OnChanged = function(newText),
				local filteredText = string.gsub(newText, "[^a-z]", "")
				setText(filteredText)
			end,
		})
	end
	```

	By default, the height of this component is equal to the value in [Constants.DefaultInputHeight].
	While this can be overriden by props, in order to keep inputs accessible it is not recommended
	to make the component any smaller than this.
]=]

local React = require("@pkg/@jsdotlua/react")

local BaseTextInput = require("./Foundation/BaseTextInput")
local Constants = require("../Constants")

--[=[
	@within TextInput
	@interface Props
	@tag Component Props

	@field ... CommonProps

	@field Text string
	@field OnChanged ((newText: string) -> ())?

	@field PlaceholderText string?
	@field ClearTextOnFocus boolean?
	@field OnFocused (() -> ())?
	@field OnFocusLost ((text: string, enterPressed: boolean, input: InputObject) -> ())?
]=]

type TextInputProps = BaseTextInput.BaseTextInputConsumerProps & {
	Text: string,
	OnChanged: ((newText: string) -> ())?,
}

local function TextInput(props: TextInputProps)
	return React.createElement(BaseTextInput, {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size or UDim2.new(1, 0, 0, Constants.DefaultInputHeight),
		LayoutOrder = props.LayoutOrder,
		ZIndex = props.ZIndex,
		Disabled = props.Disabled,
		Text = props.Text,
		PlaceholderText = props.PlaceholderText,
		ClearTextOnFocus = props.ClearTextOnFocus,
		OnFocused = props.OnFocused,
		OnFocusLost = props.OnFocusLost,
		OnChanged = props.OnChanged or function() end,
	}, props.children)
end

return TextInput


--[=[
	@class Constants
	This module exposes values that are read from in various components.
	These can be used to, for example, match the appearance of custom components with components
	from this library.

	:::warning
	The table returned by this module is read-only. It is not a config.
	:::
]=]

local Constants = {}

--- @within Constants
--- @prop DefaultFont Font
--- The default font for text.
Constants.DefaultFont = Enum.Font.SourceSans

--- @within Constants
--- @prop DefaultTextSize number
--- The default size for text.
Constants.DefaultTextSize = 14

--- @within Constants
--- @prop DefaultButtonHeight number
--- The default height of buttons.
Constants.DefaultButtonHeight = 24

--- @within Constants
--- @prop DefaultToggleHeight number
--- The default height of toggles (Checkbox and RadioButton).
Constants.DefaultToggleHeight = 20

--- @within Constants
--- @prop DefaultInputHeight number
--- The default height of text and numeric inputs.
Constants.DefaultInputHeight = 22

--- @within Constants
--- @prop DefaultSliderHeight number
--- The default height of sliders.
Constants.DefaultSliderHeight = 22

--- @within Constants
--- @prop DefaultDropdownHeight number
--- The default height of the permanent section of dropdowns.
Constants.DefaultDropdownHeight = 20

--- @within Constants
--- @prop DefaultDropdownRowHeight number
--- The default height of rows in dropdown lists.
Constants.DefaultDropdownRowHeight = 16

--- @within Constants
--- @prop DefaultProgressBarHeight number
--- The default height of progress bars.
Constants.DefaultProgressBarHeight = 14

--- @within Constants
--- @prop DefaultColorPickerSize UDim2
--- The default window size of color pickers.
Constants.DefaultColorPickerSize = UDim2.fromOffset(260, 285)

--- @within Constants
--- @prop DefaultNumberSequencePickerSize UDim2
--- The default window size of number sequence pickers.
Constants.DefaultNumberSequencePickerSize = UDim2.fromOffset(425, 285)

--- @within Constants
--- @prop DefaultDatePickerSize UDim2
--- The default window size of date pickers.
Constants.DefaultDatePickerSize = UDim2.fromOffset(202, 160)

return table.freeze(Constants)


local React = require("@pkg/@jsdotlua/react")

export type PluginContext = {
	plugin: Plugin,
	pushMouseIcon: (icon: string) -> string,
	popMouseIcon: (id: string) -> (),
}

return React.createContext(nil :: PluginContext?)


local React = require("@pkg/@jsdotlua/react")

return React.createContext(nil :: StudioTheme?)


local React = require("@pkg/@jsdotlua/react")

type Callback<Args..., Rets...> = (Args...) -> Rets...

local function useFreshCallback<Args..., Rets...>(
	-- stylua: ignore
	callback: Callback<Args..., Rets...>,
	deps: { any }?
): Callback<Args..., Rets...>
	local ref = React.useRef(callback) :: { current: Callback<Args..., Rets...> }

	React.useEffect(function()
		ref.current = callback
	end, deps)

	return function(...)
		return ref.current(...)
	end
end

return useFreshCallback


local React = require("@pkg/@jsdotlua/react")

local useFreshCallback = require("../Hooks/useFreshCallback")

local function useMouseDrag(
	callback: (rbx: GuiObject, input: InputObject) -> (),
	deps: { any }?,
	onBeganCallback: ((rbx: GuiObject, input: InputObject) -> ())?, -- NB: consumer needs to guard against stale state
	onEndedCallback: (() -> ())?
)
	local freshCallback = useFreshCallback(callback, deps)

	-- we use a state so consumers can re-render
	-- ... as well as a ref so we have an immediately-updated/available value
	local holdingState, setHoldingState = React.useState(false)
	local holding = React.useRef(false)

	local lastRbx = React.useRef(nil :: GuiObject?)
	local moveInput = React.useRef(nil :: InputObject?)
	local moveConnection = React.useRef(nil :: RBXScriptConnection?)

	local function runCallback(input: InputObject)
		freshCallback(lastRbx.current :: GuiObject, input)
	end

	local function connect()
		if moveConnection.current then
			moveConnection.current:Disconnect()
		end
		local input = moveInput.current :: InputObject
		local signal = input:GetPropertyChangedSignal("Position")
		moveConnection.current = signal:Connect(function()
			runCallback(input)
		end)
		runCallback(input)
	end

	local function disconnect()
		if moveConnection.current then
			moveConnection.current:Disconnect()
			moveConnection.current = nil
		end
		if onEndedCallback and holding.current == true then
			onEndedCallback()
		end
	end

	-- React.useEffect(function()
	-- 	if moveInput.current then
	-- 		runCallback(moveInput.current)
	-- 	end
	-- end, deps)

	React.useEffect(function()
		return disconnect
	end, {})

	local function onInputBegan(rbx: GuiObject, input: InputObject)
		lastRbx.current = rbx
		if input.UserInputType == Enum.UserInputType.MouseMovement then
			moveInput.current = input
		elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
			holding.current = true
			setHoldingState(true)
			if onBeganCallback then
				onBeganCallback(rbx, input)
			end
			if moveInput.current then
				connect()
			else
				-- case: clicked without move input first
				-- this can happen if the instance moves to be under the mouse
				runCallback(input)
			end
		end
	end

	local function onInputChanged(rbx: GuiObject, input: InputObject)
		lastRbx.current = rbx
		if input.UserInputType == Enum.UserInputType.MouseMovement then
			moveInput.current = input
			if holding.current and not moveConnection.current then
				-- handles the case above and connects listener on first move
				connect()
			end
		end
	end

	local function onInputEnded(rbx: GuiObject, input: InputObject)
		lastRbx.current = rbx
		if input.UserInputType == Enum.UserInputType.MouseButton1 then
			disconnect()
			holding.current = false
			setHoldingState(false)
		end
	end

	local function isActive()
		return holdingState == true
	end

	local function cancel()
		disconnect()
		holding.current = false
		moveInput.current = nil
		setHoldingState(false)
	end

	return {
		isActive = isActive,
		cancel = cancel,
		onInputBegan = onInputBegan,
		onInputChanged = onInputChanged,
		onInputEnded = onInputEnded,
	}
end

return useMouseDrag


--[=[
	@class useMouseIcon

	A hook used internally by components for setting and clearing custom mouse icons. To use this
	hook, you need to also render a single [PluginProvider] somewhere higher up in the tree.

	To set the mouse icon, use the `setIcon` function and pass an asset url. All components under
	the PluginProvider that use this hook share an icon stack; the most recent component to call
	`setIcon` will have its icon set as the final mouse icon. Calling `setIcon` twice without
	clearing it in between will override the previous icon set by this component.

	Calling `clearIcon` removes the icon set by this component from the stack, which may mean the
	mouse icon falls back to the next icon on the stack set by another component. Ensure you call
	`clearIcon` on unmount otherwise your icon may never get unset. For example:

	```lua
	local function MyComponent()
		local mouseIconApi = useMouseIcon()

		React.useEffect(function() -- clear icon on unmount
			return function()
				mouseIconApi.clearIcon()
			end
		end, {})

		return React.createElement(SomeComponent, {
			OnHoverStart = function()
				mouseIconApi.setIcon(...) -- some icon for hover
			end,
			OnHoverEnd = function()
				mouseIconApi.clearIcon()
			end
		})
	end
	```
]=]

--[=[
	@within useMouseIcon
	@interface mouseIconApi

	@field setIcon (icon: string) -> ()
	@field getIcon () -> string?
	@field clearIcon () -> ()
]=]

local React = require("@pkg/@jsdotlua/react")

local PluginContext = require("../Contexts/PluginContext")
local useFreshCallback = require("../Hooks/useFreshCallback")

local function useMouseIcon()
	local plugin = React.useContext(PluginContext)

	local lastIconId: string?, setLastIconId = React.useState(nil :: string?)
	local lastIconAssetUrl: string?, setLastIconAssetUrl = React.useState(nil :: string?)

	local function getIcon(): string?
		return lastIconAssetUrl
	end

	local function setIcon(assetUrl: string)
		if plugin ~= nil and assetUrl ~= lastIconAssetUrl then
			if lastIconId ~= nil then
				plugin.popMouseIcon(lastIconId)
			end
			local newId = plugin.pushMouseIcon(assetUrl)
			setLastIconId(newId)
			setLastIconAssetUrl(assetUrl)
		end
	end

	local clearIcon = useFreshCallback(function()
		if plugin ~= nil and lastIconId ~= nil then
			plugin.popMouseIcon(lastIconId)
			setLastIconId(nil)
			setLastIconAssetUrl(nil)
		end
	end, { lastIconId })

	React.useEffect(function()
		return clearIcon
	end, {})

	return {
		getIcon = getIcon,
		setIcon = setIcon,
		clearIcon = clearIcon,
	}
end

return useMouseIcon


--[=[
	@class usePlugin

	A hook used to obtain a reference to the root [plugin](https://create.roblox.com/docs/reference/engine/classes/Plugin)
	instance associated with the current plugin. It requires a single [PluginProvider] to be present
	higher up in the tree.

	```lua
	local function MyComponent()
		local plugin = usePlugin()
		...
	end
	```
]=]

local React = require("@pkg/@jsdotlua/react")

local PluginContext = require("../Contexts/PluginContext")

local function usePlugin()
	local pluginContext = React.useContext(PluginContext)
	return pluginContext and pluginContext.plugin
end

return usePlugin


--[=[
	@class useTheme

	A hook used internally by components for reading the selected Studio Theme and thereby visually
	theming components appropriately. It is exposed here so that custom components can use this
	API to achieve the same effect. Calling the hook returns a [StudioTheme] instance. For example:

	```lua
	local function MyThemedComponent()
		local theme = useTheme()
		local color = theme:GetColor(
			Enum.StudioStyleGuideColor.ScriptBackground,
			Enum.StudioStyleGuideModifier.Default
		)
		return React.createElement("Frame", {
			BackgroundColor3 = color,
			...
		})
	end
	```
]=]

local Studio = settings().Studio

local React = require("@pkg/@jsdotlua/react")

local ThemeContext = require("../Contexts/ThemeContext")

local function useTheme()
	local theme = React.useContext(ThemeContext)
	local studioTheme, setStudioTheme = React.useState(Studio.Theme)

	React.useEffect(function()
		if theme then
			return
		end
		local connection = Studio.ThemeChanged:Connect(function()
			setStudioTheme(Studio.Theme)
		end)
		return function()
			connection:Disconnect()
		end
	end, { theme })

	return theme or studioTheme
end

return useTheme


local React = require("@pkg/@jsdotlua/react")

local Background = require("../Components/Background")
local createStory = require("./Helpers/createStory")

local function Story()
	return React.createElement(Background)
end

return createStory(Story)


local React = require("@pkg/@jsdotlua/react")

local Button = require("../Components/Button")
local createStory = require("./Helpers/createStory")

local function StoryButton(props: {
	Text: string?,
	HasIcon: boolean?,
	Disabled: boolean?,
})
	return React.createElement(Button, {
		LayoutOrder = if props.Disabled then 2 else 1,
		Icon = props.HasIcon and {
			Image = "rbxassetid://18786011824",
			UseThemeColor = true,
			Size = Vector2.new(16, 16),
			Alignment = Enum.HorizontalAlignment.Left,
			RectOffset = Vector2.new(1000, 0),
			RectSize = Vector2.new(16, 16),
		} :: any,
		Text = props.Text,
		OnActivated = if not props.Disabled then function() end else nil,
		Disabled = props.Disabled,
		AutomaticSize = Enum.AutomaticSize.XY,
	})
end

local function StoryItem(props: {
	LayoutOrder: number,
	Text: string?,
	HasIcon: boolean?,
	Disabled: boolean?,
})
	local height, setHeight = React.useBinding(0)

	return React.createElement("Frame", {
		Size = height:map(function(value)
			return UDim2.new(1, 0, 0, value)
		end),
		LayoutOrder = props.LayoutOrder,
		BackgroundTransparency = 1,
	}, {
		Layout = React.createElement("UIListLayout", {
			Padding = UDim.new(0, 10),
			SortOrder = Enum.SortOrder.LayoutOrder,
			FillDirection = Enum.FillDirection.Horizontal,
			HorizontalAlignment = Enum.HorizontalAlignment.Center,
			[React.Change.AbsoluteContentSize] = function(rbx)
				setHeight(rbx.AbsoluteContentSize.Y)
			end,
		}),
		Enabled = React.createElement(StoryButton, {
			Text = props.Text,
			HasIcon = props.HasIcon,
		}),
		Disabled = React.createElement(StoryButton, {
			Text = props.Text,
			HasIcon = props.HasIcon,
			Disabled = true,
		}),
	})
end

local function Story()
	return React.createElement(React.Fragment, {}, {
		Icon = React.createElement(StoryItem, {
			LayoutOrder = 1,
			HasIcon = true,
		}),
		Text = React.createElement(StoryItem, {
			LayoutOrder = 2,
			Text = "Example Text",
		}),
		TextLonger = React.createElement(StoryItem, {
			LayoutOrder = 3,
			Text = "Example Longer Text",
		}),
		TextMulti = React.createElement(StoryItem, {
			LayoutOrder = 4,
			Text = "Example Text\nover two lines",
		}),
		IconTextIcon = React.createElement(StoryItem, {
			LayoutOrder = 5,
			HasIcon = true,
			Text = "Example Text with Icon",
		}),
	})
end

return createStory(Story)