local fusion = require("@packages/fusion")
local Cleanup = fusion.Cleanup

local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local checkbox = fusionComponents.common.checkbox
local input = fusionComponents.common.input
local selectMenu = fusionComponents.common.selectMenu

local tailwind = require("@packages/tailwind")

local theme = require("@src/theme")

type settings = {
	{
		name: string,
		fields: {
			[number]: {
				title: string,
				pluginSettingKey: string,
				description: string?,
				default: any,
				render: (value: fusion.Value<any>) -> any,
			},
		},
	}
}

local function checkboxField(value: fusion.Value<any>)
	return checkbox({
		Color = "blue",
		AppearanceOverride = {
			_global = {
				Background = { shadow = 2 },
			},
		},
		State = value,
		Padding = 4,
		Icon = {
			Name = "check",
			Size = 16,
		},
		Size = UDim2.new(0, 24, 0, 24),
	})
end

local function numericInputField(value: fusion.Value<any>)
	local inputDisplay = Value(tostring(value:get() or ""))

	return input({
		Variant = "default",
		Color = "gray",
		AppearanceOverride = {
			_global = {
				Background = {
					color = Computed(function()
						return theme.global.isDark:get() and tailwind.neutral[850] or tailwind.white
					end),
					shadow = 1,
				},
			},
			Focus = {
				Stroke = Computed(function()
					return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
				end),
			},
		},
		Text = inputDisplay,
		OnFocusLost = function(text)
			inputDisplay:set(text)
		end,
		Size = UDim2.new(0.75, 0, 0, 20),
		[Cleanup] = {
			Observer(inputDisplay):onChange(function()
				if inputDisplay:get() == tostring(value:get()) then
					return
				end

				-- check if value is a number
				if tonumber(inputDisplay:get()) then
					value:set(tonumber(inputDisplay:get()))
					return
				end

				inputDisplay:set(tostring(value:get()), true)
			end),
			Observer(value):onChange(function()
				inputDisplay:set(tostring(value:get()))
			end),
		},
	})
end

local function selectMenuField(options: {
	{
		Id: string,
		Label: string?,
		Icon: fusion.CanBeState<string>,
	}
})
	return function(value: fusion.Value<any>)
		local selectedOptions = Value({ value:get() })

		return selectMenu({
			Color = "gray",
			Variant = "default",
			MaxHeight = 4,
			AppearanceOverride = {
				_global = {
					Background = {
						color = Computed(function()
							return theme.global.isDark:get() and tailwind.neutral[850] or tailwind.white
						end),
						shadow = 1,
					},
				},
				Focus = {
					Stroke = Computed(function()
						return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
					end),
				},
			},
			Options = options,
			SelectedOptions = selectedOptions,
			OnOptionClicked = function(id, isVisible)
				selectedOptions:set({ id })
				isVisible:set(false)
			end,
			Size = UDim2.new(0.75, 0, 0, 20),
			[Cleanup] = {
				Observer(selectedOptions):onChange(function()
					value:set(selectedOptions:get()[1])
				end),
				Observer(value):onChange(function()
					selectedOptions:set({ value:get() })
				end),
			},
		})
	end
end

local function stringsToOptions(strings: { string })
	local options = {}

	for _, str in strings do
		table.insert(options, {
			Id = str:lower(),
			Label = str,
		})
	end

	return options
end

local settings: settings = {
	{
		name = "General",
		fields = {
			{
				title = "Lightweight Mode",
				description = "Turns the plugin into a practically seamless copy of the original properties window, for a decreased learning curve and seamless drop-in replacement.",
				default = false,
				pluginSettingKey = "lightweight_mode",
				render = checkboxField,
			},
			{
				title = "Max Instance Depth",
				pluginSettingKey = "max_depth",
				default = 3,
				render = numericInputField,
			},
			{
				title = "Max Size",
				description = 'Determines the upper limit for "Size" properties',
				pluginSettingKey = "max_size",
				default = 10,
				render = numericInputField,
			},
			{
				title = "Max Squash",
				description = 'Determines the upper limit for "Squash" properties',
				pluginSettingKey = "max_squash",
				default = 3,
				render = numericInputField,
			},
			{
				title = "Treeview Virtual Scroller",
				pluginSettingKey = "treeview_virtual_scroller",
				description = "Improves average performance by only rendering visible items",
				default = true,
				render = checkboxField,
			},
			{
				title = "Graph Editor Default Style",
				pluginSettingKey = "graph_editor_default_style",
				default = "linear",
				render = selectMenuField(
					stringsToOptions({ "Back", "Bezier", "Cubic", "Expo", "Linear", "Quad", "Sine" })
				),
			},
		},
	},
}

return settings


local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local theme = require("@src/theme")

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text

type props = {
	Title: string,
	Description: string?,
	Content: fusion.CanBeState<any>,
	Visible: fusion.CanBeState<boolean>,
}

local function settingsListOption(props: props)
	local useColor = theme:get("App", "gray", "default", "Base")

	return New("Frame")({
		Name = "ListOption",
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 0, 0),
		Visible = props.Visible,
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Center,
			}),
			New("Frame")({
				Name = "TextContainer",
				AutomaticSize = Enum.AutomaticSize.Y,
				BackgroundTransparency = 1,
				Size = UDim2.new(0.5, 0, 0, 0),
				[Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Vertical,
						HorizontalAlignment = Enum.HorizontalAlignment.Right,
						Padding = UDim.new(0, 0),
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Center,
					}),
					text({
						Appearance = useColor("Text", true),
						Text = {
							Label = props.Title,
							Font = theme.global.font,
							TextSize = 14,
						},
						Size = UDim2.new(1, 0, 0, 16),
						AutomaticSize = Enum.AutomaticSize.Y,
						TextXAlignment = Enum.TextXAlignment.Left,
						RichText = true,
						TextWrapped = true,
					}),
					props.Description and text({
						Appearance = useColor("SecondaryText", true),
						Text = {
							Label = props.Description,
							Font = theme.global.font,
							TextSize = 14,
						},
						Position = UDim2.new(0, 2, 0, 0),
						Size = UDim2.new(1, -2, 0, 14),
						AutomaticSize = Enum.AutomaticSize.Y,
						TextXAlignment = Enum.TextXAlignment.Left,
						RichText = true,
						TextWrapped = true,
					}),
				},
			}),

			New("Frame")({
				Name = "ValueContainer",
				BackgroundTransparency = 1,
				Size = UDim2.new(0.5, 0, 1, 0),
				[Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Horizontal,
						HorizontalAlignment = Enum.HorizontalAlignment.Right,
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Top,
					}),
					props.Content,
				},
			}),
		},
	})
end

return settingsListOption


local Selection = game:GetService("Selection")

local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local Hydrate = fusion.Hydrate
local New = fusion.New

local Clean = fusion.cleanup
local Computed = fusion.Computed
local ForPairs = fusion.ForPairs
local Observer = fusion.Observer
local Value = fusion.Value

local fusionUtils = require("@packages/fusionUtils")
local propertyProvider = fusionUtils.propertyProvider

local fusionComponents = require("@packages/fusionComponents")
local contextMenu = fusionComponents.common.contextMenu

local appTopbarItem = require("@components/appTopbarItem")

type props = {
	Items: fusion.Value<{ Instance }>,
	SelectedInstance: fusion.Value<Instance>,
	SharedContextMenuState: fusion.Value<boolean>,
}

local function AppTopbar(props: props)
	local visualSelectedInstance = Value(props.SelectedInstance:get(false))

	return Hydrate(New("ScrollingFrame")({
		Name = "Topbar",
		AutomaticCanvasSize = Enum.AutomaticSize.X,
		BackgroundTransparency = 1,
		CanvasSize = UDim2.new(0, 0, 0, 0),
		ScrollBarThickness = 0,
		ScrollingDirection = Enum.ScrollingDirection.X,
		Size = UDim2.new(1, 0, 0, 32),
		[Cleanup] = {
			Observer(props.SelectedInstance):onChange(function()
				visualSelectedInstance:set(props.SelectedInstance:get())
			end),
		},
		[Children] = {
			New("UIFlexItem")({
				FlexMode = Enum.UIFlexMode.Fill,
			}),
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				Padding = UDim.new(0, 4),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Bottom,
			}),
			New("UIPadding")({
				PaddingLeft = UDim.new(0, 2),
				PaddingRight = UDim.new(0, 2),
				PaddingTop = UDim.new(0, 2),
			}),
			ForPairs(props.Items, function(index, item)
				if typeof(item) ~= "Instance" or not game:IsAncestorOf(item) then
					return index, nil
				end

				local isContextMenuVisible = Value(false)

				local function closeSelf()
					isContextMenuVisible:set(false)

					if props.SharedContextMenuState:get() == isContextMenuVisible then
						props.SharedContextMenuState:set(false)
					end
				end

				return index,
					contextMenu(
						appTopbarItem({
							Item = item,
							Selected = Computed(function()
								return visualSelectedInstance:get() == item
							end),
							LayoutOrder = index,
							OnClick = function()
								task.spawn(function()
									props.SelectedInstance:set(item)
								end)

								visualSelectedInstance:set(item)
							end,
							OnClose = function()
								local oldItems = props.Items:get()
								oldItems[index] = nil
								props.Items:set(oldItems)

								if props.SelectedInstance:get() == item then
									local _, newInstance = next(oldItems)

									if newInstance then
										props.SelectedInstance:set(newInstance)
										visualSelectedInstance:set(newInstance)
									else
										visualSelectedInstance:set(nil)
										props.SelectedInstance:set(nil)
									end
								end
							end,
						}),
						{
							Color = "gray",
							Variant = "default",
							Visible = isContextMenuVisible,
							VisibleLogic = function(isEnabled)
								local currentState = props.SharedContextMenuState:get()

								if isEnabled then
									if typeof(currentState) ~= "boolean" then
										currentState:set(false)
									end

									isContextMenuVisible:set(true)
									props.SharedContextMenuState:set(isContextMenuVisible)
								elseif currentState:get() == isContextMenuVisible then
									isContextMenuVisible:set(false)
									props.SharedContextMenuState:set(false)
								end

								return true
							end,
							Options = {
								{
									{
										Label = "Select in Explorer",
										Icon = "list-tree",
										Callback = function()
											Selection:Set({ item })
											closeSelf()
										end,
									},
								},
								{
									{
										Label = "Close",
										Icon = "x",
										Callback = function()
											local oldItems = props.Items:get()
											oldItems[index] = nil
											props.Items:set(oldItems)

											closeSelf()
										end,
									},
									{
										Label = "Close to right",
										Icon = "panel-right-close",
										Callback = function()
											local oldItems = props.Items:get()
											local newItems = {}

											for i = 1, index do
												if oldItems[i] == nil then
													continue
												end

												newItems[i] = oldItems[i]
											end

											props.Items:set(newItems)

											closeSelf()
										end,
									},
									{
										Label = "Close others",
										Icon = "copy-x",
										Callback = function()
											props.Items:set({ item })

											closeSelf()
										end,
									},
									{
										Label = "Close all",
										Icon = "list-x",
										Callback = function()
											props.Items:set({})
											closeSelf()
										end,
									},
								},
							},
						}
					),
					item.AncestryChanged:Connect(function()
						if not game:IsAncestorOf(item) then
							local oldItems = props.Items:get()
							oldItems[index] = nil
							props.Items:set(oldItems)

							if props.SelectedInstance:get() == item then
								visualSelectedInstance:set(nil)
								props.SelectedInstance:set(nil)
							end
						end
					end)
			end, Clean),
		},
	}))(propertyProvider.getPropsAndSpecialKeys(props, "Frame"))
end

return AppTopbar


local fusion = require("@packages/fusion")
local Spring = fusion.Spring
local Tween = fusion.Tween

local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New
local Event = fusion.OnEvent

local Computed = fusion.Computed
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local baseFrame = fusionComponents.base.frame
local baseText = fusionComponents.base.text
local baseImageLabel = fusionComponents.base.imageLabel
local button = fusionComponents.common.button

local theme = require("@src/theme")

type props = {
	Item: Instance,
	Selected: fusion.Computed<boolean>,
	LayoutOrder: number,
	OnClick: () -> nil,
	OnClose: () -> nil,
}

local function AppTopbarItem(props: props)
	local isHovering = Value(false)
	local isPressing = Value(false)

	local useColor = theme:get(
		"AppTopbarItem",
		"gray",
		"default",
		Computed(function()
			local selected = props.Selected:get()
			local pressing = isPressing:get()
			local hovering = isHovering:get()

			if selected then
				return "Selected"
			elseif pressing then
				return "Pressing"
			elseif hovering then
				return "Hover"
			else
				return "Base"
			end
		end)
	)

	local classIcon = theme.getClassIcon(props.Item.ClassName)
	local itemName = Value(props.Item.Name)

	return New("TextButton")({
		Name = "AppTopbarItemWrapper",
		AutomaticSize = Enum.AutomaticSize.X,
		BackgroundTransparency = 1,
		Size = UDim2.new(0, 0, 1, 0),
		[Event("MouseButton1Down")] = function()
			isPressing:set(true)
		end,
		[Event("MouseEnter")] = function()
			isHovering:set(true)
		end,
		[Event("MouseButton1Up")] = function()
			isPressing:set(false)
			props.OnClick()
		end,
		[Event("MouseLeave")] = function()
			isHovering:set(false)
			isPressing:set(false)
		end,
		[Cleanup] = {
			props.Item:GetPropertyChangedSignal("Name"):Connect(function()
				itemName:set(props.Item.Name)
			end),
		},
		[Children] = {
			baseFrame({
				Name = "AppTopbarItem",
				AutomaticSize = Enum.AutomaticSize.XY,
				Appearance = useColor("Background", true),
				Stroke = useColor("Stroke", true),
				Size = Spring(
					Computed(function()
						if props.Selected:get() then
							return UDim2.new(0, 0, 1, 0)
						else
							return UDim2.new(0, 0, 0.8, 0)
						end
					end),
					40,
					1
				),
				[Children] = {
					-- this REALLY shouldnt be necessary
					New("Frame")({
						Name = "StrokeFiller",
						AnchorPoint = Vector2.new(0, 1),
						BackgroundColor3 = Tween(
							Computed(function()
								return useColor("Background").color
							end),
							TweenInfo.new(0.2, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)
						),
						BackgroundTransparency = Spring(
							Computed(function()
								if props.Selected:get() then
									return 0
								else
									return 1
								end
							end),
							30,
							1
						),
						Position = UDim2.new(0, 0, 1, 0),
						Size = UDim2.new(1, 0, 0.1, 0),
						ZIndex = 2,
					}),
					New("Frame")({
						Name = "CornerFiller",
						AnchorPoint = Vector2.new(0.5, 1),
						AutomaticSize = Enum.AutomaticSize.None,
						BackgroundColor3 = Tween(
							Computed(function()
								return useColor("Background").color
							end),
							TweenInfo.new(0.2, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)
						),
						BackgroundTransparency = Tween(
							Computed(function()
								if props.Selected:get() then
									return useColor("Background").transparency
								else
									return 1
								end
							end),
							TweenInfo.new(0.1, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)
						),
						Position = UDim2.new(0.5, 0, 1, 0),
						Size = UDim2.new(1, 0, 0.5, 0),
						ZIndex = -1,
						[Children] = {
							New("UIStroke")({
								Color = Tween(
									Computed(function()
										return useColor("Stroke").color
									end),
									TweenInfo.new(0.2, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)
								),
								Transparency = Tween(
									Computed(function()
										return useColor("Stroke").transparency or 1
									end),
									TweenInfo.new(0.2, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)
								),
							}),
						},
					}),
				},
				Content = {
					New("UIPadding")({
						PaddingBottom = UDim.new(0, 4),
						PaddingLeft = UDim.new(0, 4),
						PaddingRight = UDim.new(0, 4),
						PaddingTop = UDim.new(0, 4),
					}),
					New("Frame")({
						Name = "Header",
						AutomaticSize = Enum.AutomaticSize.XY,
						BackgroundTransparency = 1,
						[Children] = {
							New("UIListLayout")({
								FillDirection = Enum.FillDirection.Horizontal,
								HorizontalAlignment = Enum.HorizontalAlignment.Center,
								Padding = UDim.new(0, 4),
								SortOrder = Enum.SortOrder.LayoutOrder,
								VerticalAlignment = Enum.VerticalAlignment.Center,
							}),
							baseText({
								Name = "Text",
								Appearance = useColor("Text", true),
								Text = itemName,
								AutomaticSize = Enum.AutomaticSize.XY,
								BackgroundTransparency = 1,
								LayoutOrder = 1,
							}),
							baseImageLabel({
								Name = "Icon",
								Image = Computed(function()
									return classIcon:get().Image
								end),
								ImageRectOffset = Computed(function()
									return classIcon:get().ImageRectOffset
								end),
								ImageRectSize = Computed(function()
									return classIcon:get().ImageRectSize
								end),
								Size = UDim2.new(0, 16, 0, 16),
								Appearance = useColor("Text", true),
								LayoutOrder = 0,
							}),
						},
					}),
					button({
						Name = "Close",
						Icon = { Name = "x", Size = 16 },
						Color = "gray",
						Variant = "ghost",
						Padding = 1,
						LayoutOrder = 2,
						OnClick = props.OnClose,
					}),
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Horizontal,
						HorizontalAlignment = Enum.HorizontalAlignment.Center,
						Padding = UDim.new(0, 6),
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Center,
					}),
				},
			}),
		},
	})
end

return AppTopbarItem


local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local Hydrate = fusion.Hydrate
local New = fusion.New

local Clean = fusion.cleanup
local Computed = fusion.Computed
local Value = fusion.Value

local fusionUtils = require("@packages/fusionUtils")
local propertyProvider = fusionUtils.propertyProvider

local classDataHandler = require("@src/lib/classDataHandler")

local instanceTreeItem = require("@src/components/instanceTreeItem")

type childMap = {
	[Instance]: {
		[Instance]: childMap,
	},
}

type props = {
	RootInstance: Instance,
	Query: fusion.Value<string>,
	ChildrenReplacement: fusion.Value<{ Instance }>,
	SearchMode: fusion.Value<{ string }>,
	MaxDepth: number,
	IsSelfSelected: fusion.Value<boolean>,
	SharedContextMenuState: fusion.Value<boolean | fusion.Value<boolean>>,
	Events: instanceTreeItem.events,
	IsDestroyed: fusion.Value<boolean>,
}

export type events = instanceTreeItem.events

local ALLOWED_CLASSNAMES = {
	"ParticleEmitter",
	"Beam",
	"Trail",
	"Attachment",
}

local function instanceTree(props: props)
	-- recursively iterate through the instance and its children to build the instance map
	-- cascading lines are lines that "cascade" through children of an instance, for example for parents. the index is the depth, the value is an array of lines
	-- both instanceMap as a variable and the children of a parent should be populated
	local function buildChildMap(rootInstance: Instance, parent: childMap): nil
		local children = rootInstance:GetChildren()

		for _, child in children do
			if not table.find(ALLOWED_CLASSNAMES, child.ClassName) then
				continue
			end

			if not parent[rootInstance] then
				parent[rootInstance] = {}
			end

			parent[rootInstance][child] = {}
			buildChildMap(child, parent[rootInstance])
		end

		return nil
	end

	local instanceMapConnections = {}
	local mapChanged = Value(false)

	local function buildInstanceMap(
		instance: Instance,
		depth: number,
		parent: { instanceTreeItem.instanceMapEntry },
		childrenList: { Instance },
		childMap: childMap,
		cascadingLines: { [number]: { instanceTreeItem.line } }
	): nil
		local properties = classDataHandler.getProperties(instance) or {}
		local children = instance:GetChildren()
		local newChildren = table.clone(children)
		local queryFilteredChildren = {}

		if depth < props.MaxDepth then
			for i, child in children do
				if not table.find(ALLOWED_CLASSNAMES, child.ClassName) then
					table.remove(newChildren, i)
				elseif props.SearchMode:get()[1] == "instance" and props.Query:get() ~= "" then
					-- build queryfilteredchildren list. how this works is as simple:
					-- iterate through the children of the instance. if the instance name or any of its descendants match the query, add it to the list
					if child.Name:lower():find(props.Query:get():lower()) then
						table.insert(queryFilteredChildren, child)
					else
						-- we recursively iterate through the child map to find any descendants that match the query
						local function findInChildMap(map: childMap): nil
							for childInstance, mapContents in map do
								if childInstance.Name:lower():find(props.Query:get():lower()) then
									if not table.find(queryFilteredChildren, child) then
										table.insert(queryFilteredChildren, child)
									end

									return
								end

								findInChildMap(mapContents)
							end

							return nil
						end

						findInChildMap(childMap[instance][child])
					end
				elseif props.SearchMode:get()[1] == "properties" and props.Query:get() ~= "" then
					-- search our own properties
					local foundMatch = false
					local childProperties = classDataHandler.getProperties(child)

					if childProperties then
						for _, category in childProperties do
							local foundValidProperty = false
							local propertyList = category.properties or { category }

							for _, property in propertyList do
								if property.name:lower():find(props.Query:get():lower()) then
									if not table.find(queryFilteredChildren, child) then
										table.insert(queryFilteredChildren, child)
									end

									foundValidProperty = true
									foundMatch = true
									break
								end
							end

							if foundValidProperty then
								break
							end
						end

						if foundMatch then
							continue
						end
					end

					local function findInChildMap(map: childMap): nil
						for childInstance, mapContents in map do
							local childInstanceProperties = classDataHandler.getProperties(childInstance)

							if childInstanceProperties == nil then
								continue
							end

							for _, category in childInstanceProperties do
								local foundValidProperty = false
								local propertyList = category.properties or { category }

								for _, property in propertyList do
									if property.name:lower():find(props.Query:get():lower()) then
										if not table.find(queryFilteredChildren, child) then
											if not table.find(queryFilteredChildren, child) then
												table.insert(queryFilteredChildren, child)
											end

											foundValidProperty = true
											break
										end
									end
								end

								if foundValidProperty then
									break
								end
							end

							findInChildMap(mapContents)
						end

						return nil
					end

					findInChildMap(childMap[instance][child])
				else
					table.insert(queryFilteredChildren, child)
				end
			end
		end

		local linesStart: { instanceTreeItem.line } = {}

		local selfIndex = table.find(childrenList, instance)

		if selfIndex or depth == 0 then
			local isLast = selfIndex == #childrenList
			local isFirst = selfIndex == 1

			local lines: { instanceTreeItem.line } = {}

			if not isLast then
				table.insert(lines, isFirst and "StartVertical" or "Vertical")
			else
				table.insert(lines, "Horizontal")
				table.insert(lines, "HalfVertical")
			end

			if not isLast and not isFirst then
				table.insert(lines, "Horizontal")
			end

			if isFirst then
				table.insert(lines, "Horizontal")
				table.insert(lines, isLast and "HalfVertical" or "Vertical")
			end

			-- remove duplicates
			local seen = {}

			for i = #lines, 1, -1 do
				if seen[lines[i]] then
					table.remove(lines, i)
				else
					seen[lines[i]] = true
				end
			end

			linesStart = lines

			local previousLines = table.clone(cascadingLines)

			if #properties > 0 and #queryFilteredChildren > 0 then
				previousLines[depth + 1] = { "StartVertical" }
			end

			if not isLast and (depth ~= 0 or #queryFilteredChildren > 0) then
				previousLines[depth] = { "Vertical" }
			else
				previousLines[depth] = nil
				previousLines[depth - 1] = nil
			end

			cascadingLines = previousLines
		end

		local entry: instanceTreeItem.instanceMapEntry = {
			instance = instance,
			children = {},
			properties = properties,
			linesStart = linesStart,
			linesCascading = cascadingLines,
		}

		table.insert(parent, entry)

		if childMap and childMap[instance] then
			for _, child in children do
				buildInstanceMap(
					child,
					depth + 1,
					entry.children,
					queryFilteredChildren,
					childMap[instance],
					cascadingLines
				)

				if table.find(queryFilteredChildren, child) then
					table.insert(
						instanceMapConnections,
						child:GetPropertyChangedSignal("Parent"):Connect(function()
							mapChanged:set(true, true)
						end)
					)

					table.insert(
						instanceMapConnections,
						child.DescendantAdded:Connect(function()
							mapChanged:set(true, true)
						end)
					)

					table.insert(
						instanceMapConnections,
						child.DescendantRemoving:Connect(function()
							mapChanged:set(true, true)
						end)
					)
				end
			end
		end

		return nil
	end

	local instanceMap: fusion.Computed<instanceTreeItem.instanceMapEntry> = Computed(function()
		if mapChanged:get() then
			mapChanged:set(false)
		end

		if props.ChildrenReplacement then
			local maps = {}

			for _, child in props.ChildrenReplacement:get() do
				local map = {}
				local childMap = {}

				buildChildMap(child, childMap)
				buildInstanceMap(child, 0, map, {}, childMap, {})

				table.insert(maps, map[1])
			end

			return maps :: instanceTreeItem.instanceMapEntry
		else
			local map = {}
			local childMap = {}

			Clean(instanceMapConnections)
			buildChildMap(props.RootInstance, childMap)
			buildInstanceMap(props.RootInstance, 0, map, {}, childMap, {})

			return map[1]
		end
	end)

	local component = New("Frame")({
		Name = "InstanceTreeRoot",
		AutomaticSize = Enum.AutomaticSize.Y,
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 0, 0),
		[Cleanup] = {
			props.ChildrenReplacement == nil and props.RootInstance.DescendantAdded:Connect(function()
				mapChanged:set(true, true)
			end),
			props.ChildrenReplacement == nil and props.RootInstance.DescendantRemoving:Connect(function()
				mapChanged:set(true, true)
			end),
			props.Events,
		},
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 0),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			Computed(function()
				if props.IsDestroyed:get() then
					return {}
				end

				if props.ChildrenReplacement == nil then
					return instanceTreeItem({
						LayoutOrder = 0,
						Instance = props.RootInstance,
						Query = props.Query,
						Depth = 0,
						Events = props.Events,
						InstanceMap = instanceMap,
						IsRootInstanceSelected = props.IsSelfSelected,
						MaxDepth = props.MaxDepth,
						SearchMode = props.SearchMode,
						SharedState = {
							OpenedContextMenu = props.SharedContextMenuState,
							SelectedProperty = Value({}),
							CopiedValue = Value(),
						},
						IsDestroyed = props.IsDestroyed,
					})
				end

				local instanceTreeItemList = {}

				local sharedState = {
					OpenedContextMenu = props.SharedContextMenuState,
					SelectedProperty = Value({}),
					CopiedValue = Value(),
				}

				local usedMaps = instanceMap:get(false)

				for i, instance in props.ChildrenReplacement:get() do
					table.insert(
						instanceTreeItemList,
						instanceTreeItem({
							LayoutOrder = 0,
							Instance = instance,
							SearchMode = props.SearchMode,
							Depth = 0,
							Events = props.Events,
							HideMainContent = #props.ChildrenReplacement:get() == 1,
							InstanceMap = Value(usedMaps[i]),
							IsRootInstanceSelected = props.IsSelfSelected,
							MaxDepth = props.MaxDepth,
							Query = props.Query,
							SharedState = sharedState,
							IsDestroyed = props.IsDestroyed,
						})
					)
				end

				return instanceTreeItemList
			end, Clean),
		},
	})

	return Hydrate(component)(propertyProvider.getPropsAndSpecialKeys(props, "Frame"))
end

return instanceTree


local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local theme = require("@src/theme")

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text

type props = {
	Size: fusion.CanBeState<UDim2>,
	Position: fusion.CanBeState<UDim2>?,
	OptionName: string,
	Content: fusion.CanBeState<{ GuiObject }>,
}

local function listOption(props: props)
	local useColor = theme:get("InstanceTreeItem", "gray", "default", "Base")

	return New("Frame")({
		Name = "ListOption",
		AutomaticSize = Enum.AutomaticSize.X,
		Size = props.Size,
		Position = props.Position,
		BackgroundTransparency = 1,
		[Children] = {
			New("UIListLayout")({
				Padding = UDim.new(0, 0),
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				VerticalAlignment = Enum.VerticalAlignment.Center,
				SortOrder = Enum.SortOrder.LayoutOrder,
			}),
			text({
				Appearance = useColor("Text", true),
				Text = props.OptionName,
				Size = UDim2.new(0.5, 0, 0, 20),
				TextXAlignment = Enum.TextXAlignment.Left,
			}),
			New("Frame")({
				Name = "ValueContainer",
				Size = UDim2.new(0.5, 0, 1, 0),
				BackgroundTransparency = 1,
				[Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Horizontal,
						HorizontalAlignment = Enum.HorizontalAlignment.Left,
						VerticalAlignment = Enum.VerticalAlignment.Center,
						SortOrder = Enum.SortOrder.LayoutOrder,
					}),
					props.Content,
				},
			}),
		},
	})
end

return listOption


local fusion = require("@packages/fusion")
local New = fusion.New

local theme = require("@src/theme")

local fusionComponents = require("@packages/fusionComponents")
local text = fusionComponents.base.text

type props = {
	OptionName: string,
	Content: fusion.CanBeState<{ GuiObject }>,
}

local function modalOptionWrapper(props: props)
	local useColor = theme:get("InstanceTreeItem", "gray", "default", "Base")

	return New("Frame")({
		Name = "ModalOptionWrapper",
		Size = UDim2.new(1, 0, 0, 0),
		BackgroundTransparency = 1,
		AutomaticSize = Enum.AutomaticSize.Y,
		[fusion.Children] = {
			New("UIListLayout")({
				Padding = UDim.new(0, 0),
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				VerticalAlignment = Enum.VerticalAlignment.Center,
				SortOrder = Enum.SortOrder.LayoutOrder,
			}),
			text({
				Appearance = useColor("Text", true),
				Text = props.OptionName,
				Size = UDim2.new(0.35, 0, 0, 20),
				TextXAlignment = Enum.TextXAlignment.Left,
			}),
			New("Frame")({
				Name = "ValueContainer",
				Size = UDim2.new(0.65, 0, 1, 0),
				BackgroundTransparency = 1,
				[fusion.Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Horizontal,
						HorizontalAlignment = Enum.HorizontalAlignment.Right,
						VerticalAlignment = Enum.VerticalAlignment.Center,
						SortOrder = Enum.SortOrder.LayoutOrder,
					}),
					props.Content,
				},
			}),
		},
	})
end

return modalOptionWrapper


local fusion = require("@packages/fusion")

local fusionComponents = require("@packages/fusionComponents")
local checkbox = fusionComponents.common.checkbox

type props = {
	Value: fusion.Value<boolean>,
	LayoutOrder: number,
}

local function checkboxPropertyField(props: props)
	return checkbox({
		Color = "blue",
		AppearanceOverride = {
			_global = {
				Background = { shadow = 2 },
			},
		},
		State = props.Value,
		LayoutOrder = props.LayoutOrder,
		Padding = 4,
		Icon = {
			Name = "check",
			Size = 12,
		},
		Size = UDim2.new(0, 20, 0, 20),
	})
end

return checkboxPropertyField


local HttpService = game:GetService("HttpService")

local fusion = require("@packages/fusion")
local Spring = fusion.Spring
local Tween = fusion.Tween

local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New
local Event = fusion.OnEvent
local Ref = fusion.Ref

local Clean = fusion.cleanup
local Computed = fusion.Computed
local ForPairs = fusion.ForPairs
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local icon = fusionComponents.base.icon
local text = fusionComponents.base.text
local baseButton = fusionComponents.base.button
local inputCollector = fusionComponents.utility.inputCollector
local input = fusionComponents.common.input
local button = fusionComponents.common.button
local colorPicker = fusionComponents.common.colorPicker

local fusionUtils = require("@packages/fusionUtils")
local topLayerProvider = fusionUtils.topLayerProvider
local peek = fusionUtils.peek

local widget = require("@components/widget")

local theme = require("@src/theme")

type props = {
	Value: fusion.Value<ColorSequence>,
	useColor: theme.useColorFunction,
}

local function editor(props: props)
	local isWidgetEnabled = Value(false)
	local isHoveringColor = Value(false)

	local colorFrame = Value()

	local mousePosition = Value(Vector2.new())
	local displayedInputText = Value("")

	local selectedNode = Value(1)
	local sequenceNodes: fusion.Value<{ fusion.Value<ColorSequenceKeypoint> }> = Value({})

	local temporaryGradient = Value(peek(sequenceNodes))
	local showTemporaryGradient = Value(false)

	local function refreshText()
		displayedInputText:set(string.format("%.2f", peek(peek(sequenceNodes)[peek(selectedNode)]).Time), true)
	end

	-- resets the current nodes to the new value; primarily used for keeping the property and editor in sync and supporting undo/redo
	local function refreshNodes()
		local currentKeypoints = props.Value:get(false).Keypoints
		local usedSequenceNodes = table.clone(sequenceNodes:get(false))

		for idx, node in currentKeypoints do
			if usedSequenceNodes[idx] then
				usedSequenceNodes[idx]:set(node)
			else
				usedSequenceNodes[idx] = Value(node)
			end
		end

		for idx = #currentKeypoints + 1, #usedSequenceNodes do
			usedSequenceNodes[idx] = nil
		end

		sequenceNodes:set(usedSequenceNodes)

		if #sequenceNodes:get(false) < selectedNode:get(false) then
			selectedNode:set(#sequenceNodes:get(false))
		else
			selectedNode:set(selectedNode:get(false), true)
		end
	end

	-- i thought this pattern was rly ugly, so i made it a function
	local function transformKeypoints(transformationFunction: ({ ColorSequenceKeypoint }) -> { ColorSequenceKeypoint })
		local newKeypoints = table.clone(props.Value:get().Keypoints)
		transformationFunction(newKeypoints)

		table.sort(newKeypoints, function(a, b)
			return a.Time < b.Time
		end)

		return newKeypoints
	end

	local observers = {
		Observer(selectedNode):onChange(refreshText),
		Observer(props.Value):onChange(refreshNodes),
	}

	refreshNodes()
	refreshText()

	local colorValue = Value(peek(peek(sequenceNodes)[peek(selectedNode)]).Value)

	local createdWidget

	local function setupWidget()
		return widget({
			Name = "Color Picker",
			Id = HttpService:GenerateGUID(),
			InitialDockTo = Enum.InitialDockState.Float,
			InitialEnabled = false,
			ForceInitialEnabled = true,
			FloatingSize = Vector2.new(220, 300),
			Enabled = isWidgetEnabled,
			MinimumSize = Vector2.new(200, 300),
			[Children] = {
				Computed(function()
					if isWidgetEnabled:get() then
						return ({
							topLayerProvider.new(New("Frame")({
								Name = "ColorPicker",
								BackgroundTransparency = 1,
								Size = UDim2.new(1, 0, 1, 0),
								[Children] = {
									New("UIPadding")({
										PaddingBottom = UDim.new(0, 4),
										PaddingLeft = UDim.new(0, 4),
										PaddingRight = UDim.new(0, 4),
										PaddingTop = UDim.new(0, 4),
									}),
									colorPicker({
										Color = "white",
										Value = colorValue,
										Size = UDim2.new(1, 0, 1, 0),
										OnDragStart = function()
											temporaryGradient:set(table.clone(peek(props.Value).Keypoints))
											showTemporaryGradient:set("colorPicker")
										end,
										OnDragEnd = function()
											showTemporaryGradient:set(false)
										end,
										OnColorSet = function(color)
											local allowChange = true

											local newKeypoints = transformKeypoints(function(keypoints)
												local targetNode = keypoints[selectedNode:get()]
												keypoints[selectedNode:get()] =
													ColorSequenceKeypoint.new(targetNode.Time, color)
											end)

											if not allowChange then
												return
											end

											props.Value:set(ColorSequence.new(newKeypoints))
										end,
										OnColorChange = function(color)
											if showTemporaryGradient:get() == "colorPicker" then -- if dragging
												local newKeypoints = transformKeypoints(function(keypoints)
													local targetNode = keypoints[selectedNode:get()]
													keypoints[selectedNode:get()] =
														ColorSequenceKeypoint.new(targetNode.Time, color)
												end)

												temporaryGradient:set(newKeypoints)
											end
										end,
									}),
								},
							})),
						})[1]
					end

					return nil
				end, Clean),
			},
		})
	end

	local component = topLayerProvider.new(New("Frame")({
		Name = "ColorSequenceEditor",
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		[Cleanup] = {
			function()
				-- not destroying it, see https://devforum.roblox.com/t/2853087
				isWidgetEnabled:set(false)

				if createdWidget then
					createdWidget:Destroy()
				end
			end,
			observers,
		},
		[Children] = {
			New("UIPadding")({
				PaddingBottom = UDim.new(0, 8),
				PaddingLeft = UDim.new(0, 12),
				PaddingRight = UDim.new(0, 12),
				PaddingTop = UDim.new(0, 8),
			}),
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Vertical,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 4),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Top,
			}),
			New("Frame")({
				Name = "Color",
				Size = UDim2.new(1, 0, 1, -64),
				[Ref] = colorFrame,
				[Event("MouseEnter")] = function()
					isHoveringColor:set(true)
				end,
				[Event("MouseLeave")] = function()
					isHoveringColor:set(false)
				end,
				[Children] = {
					New("UIGradient")({
						Color = Computed(function()
							if showTemporaryGradient:get() then
								return ColorSequence.new(temporaryGradient:get())
							end

							return props.Value:get()
						end),
					}),
					New("UICorner")({
						CornerRadius = UDim.new(0, 4),
					}),
					New("UIStroke")({
						Color = Computed(function()
							return (props.useColor("Line") :: theme.color).color
						end),
					}),
					New("Frame")({
						Name = "Line",
						BackgroundColor3 = Computed(function()
							return (props.useColor("Line") :: theme.color).color
						end),
						BackgroundTransparency = Spring(
							Computed(function()
								return isHoveringColor:get() and 0.5 or 1
							end),
							45,
							1
						),
						Position = Spring(
							Computed(function()
								local mousePos = mousePosition:get()

								if not colorFrame:get() then
									return UDim2.new(0, 0, 0, 0)
								end

								local colorFrameAbsoluteSize = math.clamp(colorFrame:get().AbsoluteSize.X, 1, math.huge)

								local relativePos = mousePos - colorFrame:get().AbsolutePosition
								local time = math.clamp(relativePos.X / colorFrameAbsoluteSize, 0, 1)

								for _, node in sequenceNodes:get() do
									if math.abs(node:get(false).Time - time) < 0.01 then
										time = node:get(false).Time
										break
									end
								end

								return UDim2.new(time, 0, 0, 0)
							end),
							30,
							1
						),
						Size = UDim2.new(0, 1, 1, 0),
					}),
				},
			}),
			New("Frame")({
				Name = "ColorTimeline",
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 36),
				[Children] = {
					ForPairs(sequenceNodes, function(index, node)
						local isHovering = Value(false)
						local isPressing = Value(false)

						local lastClick = 0

						local timelineButton = New("TextButton")({
							Name = tostring(index),
							AnchorPoint = Vector2.new(0.5, 0),
							BackgroundTransparency = 1,
							LayoutOrder = index,
							Position = Computed(function()
								isHovering:set(false)
								return UDim2.new(node:get().Time, 0, 0, 0)
							end),
							Size = UDim2.new(0, 16, 1, 0),
							Text = "",
							ZIndex = index,
							[Event("MouseEnter")] = function()
								isHovering:set(true)
							end,
							[Event("MouseLeave")] = function()
								isHovering:set(false)
							end,
							[Event("MouseButton1Up")] = function()
								isPressing:set(false)
								showTemporaryGradient:set(false)
							end,
							[Event("MouseButton1Down")] = function()
								isPressing:set(true)
								selectedNode:set(index)
								colorValue:set(node:get().Value)

								if tick() - lastClick < 0.5 then
									if createdWidget == nil then
										createdWidget = setupWidget()
									end

									isWidgetEnabled:set(true)
								end

								lastClick = tick()
							end,
							[Event("MouseButton2Click")] = function()
								local newKeypoints = transformKeypoints(function(keypoints)
									table.remove(keypoints, index)
								end)

								-- min 2 keypoints and first and last keypoint must be at 0 and 1
								if
									#newKeypoints >= 2
									and newKeypoints[1].Time == 0
									and newKeypoints[#newKeypoints].Time == 1
								then
									if selectedNode:get() == index then
										selectedNode:set(1)
										colorValue:set(newKeypoints[1].Value)
									end

									isHovering:set(false)
									props.Value:set(ColorSequence.new(newKeypoints))
								end
							end,
							[Children] = {
								icon({
									Icon = "chevron-up",
									Color = props.useColor("Text", true),
									Position = Computed(function()
										return UDim2.new(0, 0, 0, isHovering:get() and -4 or 0)
									end),
								}),
								New("Frame")({
									Name = "Color",
									BackgroundColor3 = Computed(function()
										local baseColor = node:get().Value

										-- if hovering, darken color
										if isHovering:get() then
											local lerpColor = theme.global.isDark:get() and Color3.new(1, 1, 1)
												or Color3.new(0, 0, 0)
											baseColor = baseColor:Lerp(lerpColor, 0.2)
										end

										return baseColor
									end),
									Position = UDim2.new(0, 0, 0, 16),
									Size = UDim2.new(0, 16, 0, 16),
									[Children] = {
										New("UIStroke")({
											Color = Computed(function()
												return props.useColor("Line").color
											end),
										}),
										New("UICorner")({
											CornerRadius = UDim.new(0, 4),
										}),
									},
								}),
							},
						})

						local isMouseInRange = Value(false)
						local dragStartPosition = UDim2.new(node:get(false).Time, 0, 0, 0)
						local currentEndPosition = UDim2.new(node:get(false).Time, 0, 0, 0)
						local wasMoved = false

						local meetsVisibilityCriteria = Computed(function()
							local isVisible = isPressing:get()
								and (isHovering:get() or isMouseInRange:get())
								and index ~= 1
								and index ~= #sequenceNodes:get()

							if not isVisible and currentEndPosition ~= dragStartPosition and wasMoved then
								local newKeypoint =
									ColorSequenceKeypoint.new(currentEndPosition.X.Scale, node:get(false).Value)

								local newKeypoints = transformKeypoints(function(keypoints)
									keypoints[index] = newKeypoint
								end)

								selectedNode:set(table.find(newKeypoints, newKeypoint), true)
								props.Value:set(ColorSequence.new(newKeypoints))
								showTemporaryGradient:set(false)
								wasMoved = false
							elseif isVisible then
								dragStartPosition = timelineButton.Position
							end

							return isVisible
						end)

						inputCollector({
							ReferenceObject = timelineButton,
							Visible = meetsVisibilityCriteria,
							OnMouseMove = function(position)
								mousePosition:set(position)

								isMouseInRange:set(
									(mousePosition:get() - timelineButton.AbsolutePosition).Magnitude < 96
								)

								if meetsVisibilityCriteria:get() then
									wasMoved = true
									currentEndPosition = UDim2.new(
										math.clamp(
											(position.X - colorFrame:get().AbsolutePosition.X)
												/ colorFrame:get().AbsoluteSize.X,
											0,
											1
										),
										0,
										0,
										0
									)

									timelineButton.Position = currentEndPosition

									local newKeypoints = transformKeypoints(function(keypoints)
										if node:get(false).Value == nil then
											return
										end

										keypoints[index] =
											ColorSequenceKeypoint.new(currentEndPosition.X.Scale, node:get(false).Value)
									end)

									temporaryGradient:set(newKeypoints)
									showTemporaryGradient:set("timeline")
								end
							end,
							OnMouseButton1Changed = function(pressing, wasHover)
								if wasHover then
									return
								end

								if not pressing then
									isPressing:set(false)
									showTemporaryGradient:set(false)
								end

								return
							end,
						})

						return index, timelineButton
					end, Clean),
				},
			}),
			New("Frame")({
				Name = "ColorToolbar",
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 24),
				[Children] = {
					New("UIListLayout")({
						FillDirection = Enum.FillDirection.Horizontal,
						HorizontalAlignment = Enum.HorizontalAlignment.Left,
						Padding = UDim.new(0, 4),
						SortOrder = Enum.SortOrder.LayoutOrder,
						VerticalAlignment = Enum.VerticalAlignment.Center,
					}),
					text({
						Text = "Time",
						Appearance = props.useColor("Text", true),
						Size = UDim2.new(0, 0, 1, 0),
						AutomaticSize = Enum.AutomaticSize.X,
						TextXAlignment = Enum.TextXAlignment.Left,
					}),
					input({
						Variant = "default",
						Color = "gray",
						Text = displayedInputText,
						Size = UDim2.new(0, 48, 0, 24),
						OnFocusLost = function(inputText)
							local node = sequenceNodes:get()[selectedNode:get()]
							local textToNumber = tonumber(inputText)

							if not node or textToNumber == nil then
								return
							end

							textToNumber = math.clamp(textToNumber, 0, 1)

							-- start at 0, end at 1
							if selectedNode:get() == 1 and textToNumber ~= 0 then
								textToNumber = 0
							elseif selectedNode:get() == #sequenceNodes:get() and textToNumber ~= 1 then
								textToNumber = 1
							end

							local createdKeypoint = ColorSequenceKeypoint.new(textToNumber, node:get(false).Value)
							local newKeypoints = transformKeypoints(function(keypoints)
								keypoints[selectedNode:get()] = createdKeypoint
							end)

							colorValue:set(createdKeypoint.Value)
							props.Value:set(ColorSequence.new(newKeypoints), true)
							selectedNode:set(table.find(newKeypoints, createdKeypoint), true)
						end,
					}),
					baseButton({
						BackgroundColor3 = Tween(
							Computed(function()
								local node = sequenceNodes:get()[selectedNode:get()]
								return node and node:get().Value or Color3.new()
							end),
							TweenInfo.new(0.2, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)
						),
						Stroke = props.useColor("Line", true),
						Size = UDim2.new(0, 24, 0, 24),
						OnClick = function()
							if createdWidget == nil then
								createdWidget = setupWidget()
							end

							isWidgetEnabled:set(true)
						end,
					}),
					button({
						Size = UDim2.new(0, 24, 0, 24),
						Icon = "trash",
						Variant = "solid",
						Color = "red",
						ButtonText = "Delete",
						Stroke = props.useColor("Line", true),
						Disabled = Computed(function()
							if selectedNode:get() == 1 or selectedNode:get() == #sequenceNodes:get() then
								return true
							end

							return false
						end),
						OnClick = function()
							local newKeypoints = transformKeypoints(function(keypoints)
								table.remove(keypoints, selectedNode:get(false))
							end)

							-- min 2 keypoints and first and last keypoint must be at 0 and 1
							if
								#newKeypoints >= 2
								and newKeypoints[1].Time == 0
								and newKeypoints[#newKeypoints].Time == 1
							then
								props.Value:set(ColorSequence.new(newKeypoints))
								selectedNode:set(1)
								colorValue:set(newKeypoints[1].Value)
							end
						end,
					}),
				},
			}),
		},
	}))

	inputCollector({
		ReferenceObject = colorFrame:get(),
		Visible = isHoveringColor,
		OnMouseMove = function(position)
			mousePosition:set(position)
		end,
		OnClick = function(wasHovering)
			if not colorFrame:get() or not isHoveringColor:get() or wasHovering then
				return
			end

			local relativePosition = mousePosition:get() - colorFrame:get().AbsolutePosition
			local time = math.clamp(relativePosition.X / colorFrame:get().AbsoluteSize.X, 0, 1)

			-- find the current color at this time, interpolating between colors if necessary
			local keypoints = props.Value:get().Keypoints

			if #keypoints >= 20 then
				return
			end

			-- check if our new keypoint is too close to an existing keypoint
			for _, node in keypoints do
				if math.abs(node.Time - time) < 0.01 then
					return
				end
			end

			local color = Color3.new()

			if #keypoints == 1 then
				color = keypoints[1].Value
			else
				local lastKeypoint = keypoints[1]
				for i = 2, #keypoints do
					local currentKeypoint = keypoints[i]

					if time >= lastKeypoint.Time and time <= currentKeypoint.Time then
						local alpha = (time - lastKeypoint.Time) / (currentKeypoint.Time - lastKeypoint.Time)
						color = lastKeypoint.Value:Lerp(currentKeypoint.Value, alpha)
						break
					end

					lastKeypoint = currentKeypoint
				end
			end

			-- add a new keypoint
			local newKeypoints = table.clone(keypoints)

			-- add keypoint to list of keypoints, sorted by time
			local keypoint = ColorSequenceKeypoint.new(time, color)
			table.insert(newKeypoints, keypoint)

			table.sort(newKeypoints, function(a, b)
				return a.Time < b.Time
			end)

			props.Value:set(ColorSequence.new(newKeypoints))
			selectedNode:set(table.find(newKeypoints, keypoint), true)
			colorValue:set(color)
		end,
	})

	return component
end

return editor


local HttpService = game:GetService("HttpService")

local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New

local Clean = fusion.cleanup
local Computed = fusion.Computed
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local button = fusionComponents.base.button

local widget = require("@components/widget")
local editor = require("./editor")

local theme = require("@src/theme")

type props = {
	Instance: Instance,
	PropertyName: string,
	Value: fusion.Value<ColorSequence>,
	LayoutOrder: number,
}

local function colorSequencePropertyField(props: props, useColor: theme.useColorFunction)
	local isWidgetEnabled = Value(false)
	local wasEnabled = false

	local createdWidget

	local colorButton = button({
		Appearance = { color = Color3.new(1, 1, 1), transparency = 1, shadow = 2 },
		Stroke = useColor("Line", true),
		Size = UDim2.new(0, 20, 0, 20),
		LayoutOrder = props.LayoutOrder,
		OnClick = function()
			if createdWidget == nil then
				createdWidget = widget({
					Name = `Color Sequence Editor ({props.Instance}.{props.PropertyName})`,
					Id = HttpService:GenerateGUID(),
					InitialDockTo = Enum.InitialDockState.Float,
					InitialEnabled = false,
					ForceInitialEnabled = true,
					FloatingSize = Vector2.new(500, 200),
					MinimumSize = Vector2.new(500, 200),
					Enabled = isWidgetEnabled,
					[Children] = {
						Computed(function()
							local widgetEnabled = isWidgetEnabled:get()

							if widgetEnabled and not wasEnabled then
								wasEnabled = true

								return editor({
									Value = props.Value,
									useColor = useColor,
								})
							end

							if not widgetEnabled then
								wasEnabled = false
							end

							return nil
						end, Clean),
					},
				})
			end

			isWidgetEnabled:set(true)
		end,
		[Cleanup] = {
			function()
				-- https://devforum.roblox.com/t/2853087 causes issues while using flipbook here
				isWidgetEnabled:set(false)

				if createdWidget then
					createdWidget:Destroy()
				end
			end,
		},
		[Children] = {
			New("Frame")({
				Name = "Color",
				Size = UDim2.new(1, 0, 1, 0),
				[Children] = {
					New("UIGradient")({
						Color = props.Value,
					}),
					New("UICorner")({
						CornerRadius = UDim.new(0, 4),
					}),
				},
			}),
		},
	})

	return colorButton
end

return colorSequencePropertyField


local fusion = require("@packages/fusion")
local Cleanup = fusion.Cleanup

local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local selectMenu = fusionComponents.common.selectMenu

local tailwind = require("@packages/tailwind")

local theme = require("@src/theme")

local function tableEquals(a: { any }, b: { any })
	if #a ~= #b then
		return false
	end

	for i = 1, #a do
		if a[i] ~= b[i] then
			return false
		end
	end

	return true
end

type props = {
	Value: fusion.Value<string | { string }>,
	LayoutOrder: number,
	Disabled: fusion.CanBeState<boolean>?,
	Multiple: boolean?,
	Options: {
		{
			Id: string,
			Label: string?,
			Icon: string?,
		}
	},
}

local function dropdownPropertyField(props: props)
	local selectedOptions = props.Multiple and Value(table.clone(props.Value:get(false)))
		or Value({ props.Value:get(false) })

	local disconnectObserver = Observer(selectedOptions):onChange(function()
		local usedSelectedOptions = selectedOptions:get()

		if props.Multiple then
			if typeof(props.Value:get()) == "table" and tableEquals(usedSelectedOptions, props.Value:get() :: any) then
				return
			end

			props.Value:set(table.clone(usedSelectedOptions))
		else
			props.Value:set(usedSelectedOptions[1])
		end
	end)

	local onPropertyChange = Observer(props.Value):onChange(function()
		if props.Multiple then
			if
				typeof(props.Value:get()) == "table" and tableEquals(selectedOptions:get(), props.Value:get() :: any)
			then
				return
			end

			selectedOptions:set(table.clone(props.Value:get()))
		else
			selectedOptions:set({ props.Value:get() })
		end
	end)

	return selectMenu({
		Color = "gray",
		Variant = "default",
		MaxHeight = 3,
		Disabled = props.Disabled,
		Multiple = props.Multiple,
		AppearanceOverride = {
			_global = {
				Background = {
					color = Computed(function()
						return theme.global.isDark:get() and tailwind.neutral[850] or tailwind.white
					end),
					shadow = 1,
				},
			},
			Focus = {
				Stroke = Computed(function()
					return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
				end),
			},
		},
		Options = props.Options,
		SelectedOptions = selectedOptions,
		OnOptionClicked = function(id, isVisible)
			if props.Multiple then
				local index = table.find(selectedOptions:get(), id)

				if index then
					table.remove(selectedOptions:get(), index)
				else
					table.insert(selectedOptions:get(), id)
				end
			else
				selectedOptions:set({ id })
				isVisible:set(false)
			end
		end,
		Size = UDim2.new(1, 0, 0, 20),
		[Cleanup] = function()
			disconnectObserver()
			onPropertyChange()
		end,
	})
end

return dropdownPropertyField


local fusion = require("@packages/fusion")
local Cleanup = fusion.Cleanup

local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local selectMenu = fusionComponents.common.selectMenu

local tailwind = require("@packages/tailwind")

local theme = require("@src/theme")

type props = {
	Value: fusion.Value<EnumItem>,
	LayoutOrder: number,
	Disabled: fusion.CanBeState<boolean>?,
}

local function enumDropdownPropertyField(props: props)
	local parentEnum = props.Value:get(false).EnumType
	local selectedOptions = Value({ props.Value:get(false).Name })

	local disconnectObserver = Observer(selectedOptions):onChange(function()
		props.Value:set(parentEnum[selectedOptions:get()[1]])
	end)

	local onPropertyChange = Observer(props.Value):onChange(function()
		selectedOptions:set({ props.Value:get().Name })
	end)

	return selectMenu({
		Color = "gray",
		Variant = "default",
		MaxHeight = 3,
		Disabled = props.Disabled,
		AppearanceOverride = {
			_global = {
				Background = {
					color = Computed(function()
						return theme.global.isDark:get() and tailwind.neutral[850] or tailwind.white
					end),
					shadow = 1,
				},
			},
			Focus = {
				Stroke = Computed(function()
					return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
				end),
			},
		},
		Options = Computed(function()
			local options = {}
			for _, enumItem in props.Value:get().EnumType:GetEnumItems() do
				table.insert(options, {
					Id = enumItem.Name,
				})
			end

			return options
		end),
		SelectedOptions = selectedOptions,
		OnOptionClicked = function(id, isVisible)
			selectedOptions:set({ id })
			isVisible:set(false)
		end,
		Size = UDim2.new(1, 0, 0, 20),
		[Cleanup] = function()
			disconnectObserver()
			onPropertyChange()
		end,
	})
end

return enumDropdownPropertyField


local fusion = require("@packages/fusion")
local Cleanup = fusion.Cleanup

local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local input = fusionComponents.common.input

local tailwind = require("@packages/tailwind")

local theme = require("@src/theme")

type props = {
	Value: fusion.Value<string>,
	LayoutOrder: number,
}

local function inputPropertyField(props: props)
	local inputDisplay = Value(tostring(props.Value:get(false)))

	local onPropertyChange = Observer(props.Value):onChange(function()
		inputDisplay:set(tostring(props.Value:get()))
	end)

	return input({
		Variant = "default",
		Color = "gray",
		AppearanceOverride = {
			_global = {
				Background = {
					color = Computed(function()
						return theme.global.isDark:get() and tailwind.neutral[850] or tailwind.white
					end),
					shadow = 1,
				},
			},
			Focus = {
				Stroke = Computed(function()
					return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
				end),
			},
		},
		Text = inputDisplay,
		OnFocusLost = function(text)
			props.Value:set(text)
		end,
		Size = UDim2.new(0.75, 0, 0, 20),
		LayoutOrder = props.LayoutOrder,
		[Cleanup] = { onPropertyChange },
	})
end

return inputPropertyField


return {
	In = function(a, b, t)
		local s = 1.70158
		return a + (b - a) * t * t * ((s + 1) * t - s)
	end,
	Out = function(a, b, t)
		local s = 1.70158
		t = t - 1
		return a + (b - a) * (t * t * ((s + 1) * t + s) + 1)
	end,
	InOut = function(a, b, t)
		local s = 1.70158 * 1.525
		t = t * 2
		if t < 1 then
			return a + (b - a) * 0.5 * (t * t * ((s + 1) * t - s))
		else
			t = t - 2
			return a + (b - a) * 0.5 * (t * t * ((s + 1) * t + s) + 2)
		end
	end,
}


local function getCubicBezier(t: number, p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2): Vector2
	return (1 - t) ^ 3 * p0 + 3 * (1 - t) ^ 2 * t * p1 + 3 * (1 - t) * t ^ 2 * p2 + t ^ 3 * p3
end

local function fixedBezier(n, t, p0, p1, p2, p3)
	local length, ranges, sums = 0, {}, {}

	for i = 0, n - 1 do
		local startPoint, endPoint = getCubicBezier(i / n, p0, p1, p2, p3), getCubicBezier((i + 1) / n, p0, p1, p2, p3)
		local dist = (endPoint - startPoint).Magnitude

		ranges[length] = { dist, startPoint, endPoint }

		table.insert(sums, length)
		length = length + dist
	end

	-- find how far along the length we should be
	local T, near = t * length, 0
	-- get the nearest point we calculated
	for _, element in next, sums do
		if (T - element) < 0 then
			break
		end
		near = element
	end
	local set = ranges[near]
	-- linearly interpolate between that point and its neighbor
	local percent = (T - near) / set[1]
	return set[2] + (set[3] - set[2]) * percent
end

return {
	Ease = function(t, p0, p1, p2, p3)
		-- there is optimization to be done here, but there's no need for it (yet)
		return fixedBezier(10, t, p0, p1, p2, p3)
	end,
}


return {
	In = function(a, b, t)
		return a + (b - a) * t * t * t
	end,
	Out = function(a, b, t)
		return a + (b - a) * ((t - 1) * (t - 1) * (t - 1) + 1)
	end,
	InOut = function(a, b, t)
		if t < 0.5 then
			return a + (b - a) * 4 * t * t * t
		else
			return a + (b - a) * ((t - 1) * (2 * t - 2) * (2 * t - 2) + 1)
		end
	end,
}


return {
	In = function(a, b, t)
		return a + (b - a) * 2 ^ (10 * (t - 1))
	end,
	Out = function(a, b, t)
		return a + (b - a) * (1 - 2 ^ (-10 * t))
	end,
	InOut = function(a, b, t)
		if t < 0.5 then
			return a + (b - a) * 0.5 * 2 ^ (20 * t - 10)
		else
			return a + (b - a) * (1 - 0.5 * 2 ^ (-20 * t + 10))
		end
	end,
}


return {
	back = require(script.back),
	bezier = require(script.bezier),
	cubic = require(script.cubic),
	expo = require(script.expo),
	linear = require(script.linear),
	quad = require(script.quad),
	sine = require(script.sine),
}


return {
	In = function(a, b, t)
		return a + (b - a) * t
	end,
	Out = function(a, b, t)
		return a + (b - a) * t
	end,
	InOut = function(a, b, t)
		return a + (b - a) * t
	end,
}


return {
	In = function(a, b, t)
		return a + (b - a) * t * t
	end,
	Out = function(a, b, t)
		return a + (b - a) * t * (2 - t)
	end,
	InOut = function(a, b, t)
		if t < 0.5 then
			return a + (b - a) * 2 * t * t
		else
			return a + (b - a) * (1 - 2 * (1 - t) * (1 - t))
		end
	end,
}


return {
	In = function(a, b, t)
		return a + (b - a) * (1 - math.cos(t * math.pi / 2))
	end,
	Out = function(a, b, t)
		return a + (b - a) * math.sin(t * math.pi / 2)
	end,
	InOut = function(a, b, t)
		return a + (b - a) * (1 - math.cos(t * math.pi)) / 2
	end,
}


local fusion = require("@packages/fusion")

local Computed = fusion.Computed

local fusionUtils = require("@packages/fusionUtils")
local use = fusionUtils.use

local viewport2D = require("@components/viewport2D")

local cube = script.assets.Cube
local node = script.assets.Node
local cap = script.assets.Cap

type graphKeypoint = {
	index: number,
	value: number,
	envelope: number,
}

type props = {
	Points: fusion.CanBeState<{ graphKeypoint }>,
	LineColor: Color3,
	EnvelopeColor: Color3,
	PixelScale: number, -- thickness
	Resolution: number,
}

function interpolate(a: number, b: number, alpha: number): number
	a = not a and 0 or a :: number
	b = not b and 0 or b :: number

	return a * (1 - alpha) + b * alpha
end

function extrapolate(a: graphKeypoint, b: graphKeypoint, alpha: number, valueName: string)
	local aValue = a[valueName] or 0
	local bValue = b[valueName] or 0

	return (aValue + (alpha - a.index) / (b.index - a.index) * (bValue - aValue))
end

local function getPoint(points: { graphKeypoint }, pointIndex): graphKeypoint
	local lastPoint: number
	local lastIndex: number

	for i, point in points do
		if lastPoint then
			if pointIndex > lastPoint and pointIndex < point.index then
				-- falls inbetween
				local perc = (pointIndex - lastPoint) / (point.index - lastPoint)

				return {
					value = interpolate(points[lastIndex].value, points[i].value, perc),
					index = pointIndex,
					envelope = interpolate(points[lastIndex].envelope, points[i].envelope, perc),
				}
			end

			if pointIndex == point.index and i then
				return table.clone(point)
			end
		end

		lastPoint = point.index
		lastIndex = i
	end

	return {
		index = pointIndex,
		value = pointIndex > points[#points].index
				and extrapolate(points[#points - 1], points[#points], pointIndex, "value")
			or extrapolate(points[1], points[2], pointIndex, "value"),
		envelope = pointIndex > points[#points].index
				and extrapolate(points[#points - 1], points[#points], pointIndex, "envelope")
			or extrapolate(points[1], points[2], pointIndex, "envelope"),
	}
end

local function compressPoints(points: { graphKeypoint }, resolution: number, start: number, bounds: viewport2D.bounds)
	resolution = math.min(#points - 1, resolution)
	local rangeWidth = bounds.x.max - bounds.x.min
	local resolutionIncrement = rangeWidth / resolution
	local compressedPoints: { graphKeypoint } = {}

	for i = 0, resolution + 1 do
		table.insert(compressedPoints, getPoint(points, start + (resolutionIncrement * i)))
	end

	local validPoints = {}
	local lastPoint: graphKeypoint?

	for i, point in compressedPoints do
		if lastPoint and points[i - 2] then
			local slopes = {}

			-- compare self to slope between i-2 and i-1 and exchange self with i-1 if same
			-- batch points of similiar slope together
			-- value
			slopes[1] = {}

			do
				local diffX = point.index - lastPoint.index
				local diffY = point.value - lastPoint.value
				slopes[1][1] = diffY / diffX
			end

			do
				local diffX = points[i - 2].index - lastPoint.index
				local diffY = points[i - 2].value - lastPoint.value

				slopes[1][2] = diffY / diffX
			end

			slopes[2] = {}

			-- envelope
			do
				local distanceToBounds =
					math.clamp(math.min(bounds.y.max - point.value, point.value - bounds.y.min), 0, bounds.y.max)
				local lastDistanceToBounds = math.clamp(
					math.min(bounds.y.max - lastPoint.value, lastPoint.value - bounds.y.min),
					0,
					bounds.y.max
				)

				local envelopeConstrained = math.clamp(point.envelope, 0, distanceToBounds)
				local lastEnvelopeConstrained = math.clamp(lastPoint.envelope, 0, lastDistanceToBounds)

				local diffX = point.index - lastPoint.index
				local diffY = envelopeConstrained - lastEnvelopeConstrained
				slopes[2][1] = diffY / diffX
			end

			do
				local distanceToBounds = math.clamp(
					math.min(bounds.y.max - points[i - 2].value, points[i - 2].value - bounds.y.min),
					0,
					bounds.y.max
				)
				local lastDistanceToBounds = math.clamp(
					math.min(bounds.y.max - lastPoint.value, lastPoint.value - bounds.y.min),
					0,
					bounds.y.max
				)

				local envelopeConstrained = math.clamp(points[i - 2].envelope, 0, distanceToBounds)
				local lastEnvelopeConstrained = math.clamp(lastPoint.envelope, 0, lastDistanceToBounds)

				local diffX = points[i - 2].index - lastPoint.index
				local diffY = envelopeConstrained - lastEnvelopeConstrained
				slopes[2][2] = diffY / diffX
			end

			local canNotBeExchanged = false

			for _, slope in slopes do
				if math.abs(slope[1] - slope[2]) > 1e-4 then
					canNotBeExchanged = true
					break
				end
			end

			if canNotBeExchanged then
				table.insert(validPoints, point)
				lastPoint = point
				continue
			else
				-- replace i-1
				validPoints[#validPoints] = point
				lastPoint = point
				continue
			end
		end

		table.insert(validPoints, point)
		lastPoint = point
	end

	return validPoints
end

local function graphComponent(props: props)
	local graphKeypoints = Computed(function()
		local points = use(props.Points)
		local dereferencedKeypoints = {}

		for _, point in points do
			table.insert(dereferencedKeypoints, {
				index = use(point.index),
				value = use(point.value),
				envelope = use(point.envelope),
			})
		end

		return dereferencedKeypoints
	end)

	local pointBase = node:Clone()
	pointBase.Color = props.LineColor

	local pointCap = cap:Clone()
	pointCap.Color = props.LineColor
	pointCap.Parent = pointBase

	return function(
		convertToVector: viewport2D.convertToVector,
		bounds: fusion.Computed<viewport2D.bounds>,
		size: fusion.Value<Vector2>
	)
		if size:get() == nil then
			return {}
		end

		-- not using forPairs as the values are inherently linked, so going value by value is not needed
		local usedBounds = bounds:get()
		local rangeWidth = usedBounds.x.max - usedBounds.x.min
		local verticalScale = rangeWidth / 2 * 2
		local pxScale = props.PixelScale / size:get().Y
		local lineSize = verticalScale * pxScale
		pointCap.Size = Vector3.new(lineSize, lineSize, lineSize)

		local points = compressPoints(graphKeypoints:get(), props.Resolution, usedBounds.x.min, usedBounds)
		local children = {}

		local lastPoint: graphKeypoint?
		local lineData = {}

		for i, point in points do
			if lastPoint then
				local startPosition = convertToVector(Vector2.new(lastPoint.index, lastPoint.value))
				local endPosition = convertToVector(Vector2.new(point.index, point.value))
				local inbetweenPosition = (startPosition + endPosition) / 2

				local rot = math.atan2(endPosition.Y - startPosition.Y, endPosition.X - startPosition.X)
				local distance = (endPosition - startPosition).Magnitude

				local line = pointBase:Clone()
				line.Size = Vector3.new(distance, 1, lineSize)
				line.CFrame = CFrame.new(inbetweenPosition) * CFrame.Angles(0, 0, rot)

				line.Cap.CFrame = CFrame.new(endPosition)

				if point.envelope ~= 0 or lastPoint.envelope ~= 0 then
					local distanceToBounds = math.clamp(
						math.min(usedBounds.y.max - point.value, point.value - usedBounds.y.min),
						0,
						usedBounds.y.max
					)
					local lastDistanceToBounds = math.clamp(
						math.min(usedBounds.y.max - lastPoint.value, lastPoint.value - usedBounds.y.min),
						0,
						usedBounds.y.max
					)

					local envelopeConstrained = math.clamp(point.envelope, 0, distanceToBounds)
					local lastEnvelopeConstrained = math.clamp(lastPoint.envelope, 0, lastDistanceToBounds)

					local verticalSize = convertToVector(envelopeConstrained + point.value) - endPosition.Y
					local lastEnvelope = convertToVector(lastEnvelopeConstrained + lastPoint.value) - startPosition.Y

					local envelopeCube = cube:Clone()
					envelopeCube.Color = props.EnvelopeColor
					envelopeCube.Parent = line

					local pastSegment = lineData[i - 1]

					envelopeCube.TopLeft.WorldPosition = pastSegment and pastSegment[1] - Vector3.new(0.001, 0, 0)
						or startPosition + Vector3.new(0, lastEnvelope, 0)
					envelopeCube.TopRight.WorldPosition = endPosition + Vector3.new(0, verticalSize, 0)

					envelopeCube.BottomLeft.WorldPosition = pastSegment and pastSegment[2] - Vector3.new(0.001, 0, 0)
						or startPosition - Vector3.new(0, lastEnvelope, 0)
					envelopeCube.BottomRight.WorldPosition = endPosition - Vector3.new(0, verticalSize, 0)

					lineData[i] = {
						envelopeCube.TopRight.WorldPosition,
						envelopeCube.BottomRight.WorldPosition,
					}
				end

				table.insert(children, line)
			end

			lastPoint = point
		end

		return children
	end
end

return graphComponent


local fusion = require("@packages/fusion")
local Children = fusion.Children
local New = fusion.New

local Computed = fusion.Computed

local fusionUtils = require("@packages/fusionUtils")
local use = fusionUtils.use

type props = {
	color: fusion.CanBeState<Color3>,
	spacingX: fusion.CanBeState<number>,
	spacingY: fusion.CanBeState<number>,
}

local function grid(props: props)
	return New("Frame")({
		Name = "Grid",
		BackgroundTransparency = 1,
		Size = UDim2.new(1, 0, 1, 0),
		ZIndex = -1,
		[Children] = Computed(function()
			local children = {}

			for i = 1, 1 / use(props.spacingX) - 1 do
				table.insert(
					children,
					New("Frame")({
						BackgroundColor3 = props.color,
						Position = UDim2.new(use(props.spacingX) * i, 0, 0, 0),
						Size = UDim2.new(0, 1, 1, 0),
					})
				)
			end

			for i = 1, 1 / use(props.spacingY) - 1 do
				table.insert(
					children,
					New("Frame")({
						BackgroundColor3 = props.color,
						Position = UDim2.new(0, 0, use(props.spacingY) * i, 0),
						Size = UDim2.new(1, 0, 0, 1),
					})
				)
			end

			return children
		end),
	})
end

return grid


local fusion = require("@packages/fusion")

local viewport2D = require("@components/viewport2D")

type props = {
	HandlePosition: Vector2,
	PointPosition: Vector2,
	Color: Color3,
}

-- This component only *renders* the bezier handles. The actual handle functionality is in editor.lua
local function bezierHandle(props: props)
	return function(
		convertToVector: viewport2D.convertToVector,
		bounds: fusion.Computed<viewport2D.bounds>,
		size: fusion.Value<Vector2>
	)
		if size:get() == nil then
			return {}
		end

		local usedBounds = bounds:get()
		local rangeWidth = usedBounds.x.max - usedBounds.x.min
		local verticalScale = rangeWidth / 2 * 2

		local circleSize = verticalScale * (8 / size:get().Y)
		local lineSize = verticalScale * (2 / size:get().Y)

		local handlePosition = convertToVector(props.HandlePosition)
		local pointPosition = convertToVector(props.PointPosition)
		local inbetweenPosition = (handlePosition + pointPosition) / 2

		local circle = Instance.new("Part")
		circle.Size = Vector3.new(circleSize, circleSize, circleSize)
		circle.Shape = Enum.PartType.Ball
		circle.Color = props.Color
		circle.Position = handlePosition

		local rot = math.atan2(handlePosition.Y - pointPosition.Y, handlePosition.X - pointPosition.X)
		local distance = (handlePosition - pointPosition).Magnitude

		local line = Instance.new("Part")
		line.Color = props.Color
		line.Size = Vector3.new(distance, lineSize, circleSize)
		line.CFrame = CFrame.new(inbetweenPosition) * CFrame.Angles(0, 0, rot)
		line.Parent = circle

		return circle
	end
end

return bezierHandle


local HttpService = game:GetService("HttpService")

local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New

local Clean = fusion.cleanup
local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local button = fusionComponents.common.button
local input = fusionComponents.common.input

local tailwind = require("@packages/tailwind")

local widget = require("@components/widget")
local editor = require("./editor")

local theme = require("@src/theme")

type props = {
	Instance: Instance,
	PropertyName: string,
	Value: fusion.Value<NumberSequence>,
	LayoutOrder: number,
	SupportsEnvelope: boolean,
	Min: number,
	Max: number,
}

local function numberSequencePropertyField(props: props, useColor: theme.useColorFunction)
	local maxValueInSequence = props.Max

	for _, keypoint in props.Value:get(false).Keypoints do
		if keypoint.Value > maxValueInSequence then
			maxValueInSequence = keypoint.Value
		end
	end

	props.Max = math.max(maxValueInSequence, props.Max)

	local isWidgetEnabled = Value(false)
	local wasEnabled = false

	local createdWidget

	local function getInputText()
		-- if the value is a number sequence that only has 2 keypoints, we can represent it as a number
		local value = props.Value:get(false)
		local keypoints = value.Keypoints

		if #keypoints == 2 then
			local keypoint1 = keypoints[1]
			local keypoint2 = keypoints[2]

			if keypoint1.Time == 0 and keypoint2.Time == 1 and keypoint1.Value == keypoint2.Value then
				return tostring(keypoint1.Value)
			end
		end

		return "<sequence>"
	end

	local inputDisplay = Value(getInputText())

	local onPropertyChange = Observer(props.Value):onChange(function()
		inputDisplay:set(getInputText())
	end)

	local buttons = New("Frame")({
		BackgroundTransparency = 1,
		AutomaticSize = Enum.AutomaticSize.X,
		Size = UDim2.new(1, 0, 0, 16),
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				VerticalAlignment = Enum.VerticalAlignment.Center,
				SortOrder = Enum.SortOrder.LayoutOrder,
				HorizontalFlex = Enum.UIFlexAlignment.SpaceBetween,
				Padding = UDim.new(0, 4),
			}),
			button({
				Color = "white",
				Variant = "solid",
				Icon = {
					Name = "line-chart",
					Size = 16,
				},
				Padding = 2,
				Size = UDim2.new(0, 16, 0, 16),
				LayoutOrder = props.LayoutOrder,
				OnClick = function()
					if createdWidget == nil then
						createdWidget = widget({
							Name = `Number Sequence Editor ({props.Instance}.{props.PropertyName})`,
							Id = HttpService:GenerateGUID(),
							InitialDockTo = Enum.InitialDockState.Float,
							InitialEnabled = false,
							ForceInitialEnabled = true,
							FloatingSize = Vector2.new(500, 250),
							MinimumSize = Vector2.new(500, 250),
							Enabled = isWidgetEnabled,
							[Children] = {
								Computed(function()
									local widgetEnabled = isWidgetEnabled:get()

									if widgetEnabled and not wasEnabled then
										wasEnabled = true

										return editor({
											Value = props.Value,
											useColor = useColor,
											Instance = props.Instance,
											PropertyName = props.PropertyName,
											SupportsEnvelope = props.SupportsEnvelope,
											Min = props.Min,
											Max = props.Max,
										})
									end

									if not widgetEnabled then
										wasEnabled = false
									end

									return nil
								end, Clean),
							},
						})
					end

					isWidgetEnabled:set(true)
				end,
				[Cleanup] = {
					function()
						-- https://devforum.roblox.com/t/2853087
						isWidgetEnabled:set(false)

						if createdWidget then
							createdWidget:Destroy()
						end
					end,
				},
			}),
			input({
				Variant = "default",
				Color = "gray",
				AppearanceOverride = {
					_global = {
						Background = {
							color = Computed(function()
								return theme.global.isDark:get() and tailwind.neutral[850] or tailwind.white
							end),
							shadow = 1,
						},
					},
					Focus = {
						Stroke = Computed(function()
							return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
						end),
					},
				},
				Text = inputDisplay,
				OnFocusLost = function(text)
					-- value has to be a number, so we can convert it to a number sequence
					local value = tonumber(text)

					if value then
						props.Value:set(NumberSequence.new(value))
					end

					inputDisplay:set(getInputText())
				end,
				Size = UDim2.new(1, -24, 0, 20),
				LayoutOrder = props.LayoutOrder,
				[Cleanup] = { onPropertyChange },
			}),
		},
	})

	return buttons
end

return numberSequencePropertyField


local fusion = require("@packages/fusion")
local Cleanup = fusion.Cleanup
local New = fusion.New

local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local button = fusionComponents.common.button
local imageLabel = fusionComponents.base.imageLabel
local text = fusionComponents.base.text

local tailwind = require("@packages/tailwind")

local fusionUtils = require("@packages/fusionUtils")
local awaitLayer = fusionUtils.awaitLayer
local onDestroy = fusionUtils.onDestroy

local signal = require("@packages/signal")

local theme = require("@src/theme")

type props = {
	Value: fusion.Value<Instance>,
	LayoutOrder: number,
	Filter: (instance: Instance) -> boolean,
	Events: { [string]: signal.Signal<Instance> },
}

local sharedSelectState = Value(nil)

local function objectPropertyField(props: props, useColor: theme.useColorFunction)
	local instanceClassIcon = Computed(function()
		if props.Value:get() then
			return theme.getClassIcon(props.Value:get().ClassName):get()
		end

		return {}
	end)

	local isSelectingItem = Value(false)
	local selectionConnection

	local instanceName = Value()
	local lastConnection

	local function updateName()
		if lastConnection then
			lastConnection:Disconnect()
		end

		if props.Value:get(false) then
			instanceName:set(props.Value:get(false).Name)
			lastConnection = props.Value:get(false):GetPropertyChangedSignal("Name"):Connect(function()
				instanceName:set(props.Value:get().Name)
			end)
		else
			instanceName:set("None")
		end
	end

	updateName()

	local component = button({
		Variant = "solid",
		Color = "white",
		DisableHoverLighting = true,
		AppearanceOverride = {
			Pressing = {
				Stroke = Computed(function()
					return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
				end),
			},
		},
		LayoutOrder = props.LayoutOrder,
		[Cleanup] = {
			Observer(props.Value):onChange(updateName),
			selectionConnection,
		},
		OnClick = function()
			local currentState = isSelectingItem:get()
			local usedSharedSelectState = sharedSelectState:get()

			if usedSharedSelectState == isSelectingItem and currentState then
				sharedSelectState:set(nil)
				isSelectingItem:set(false)
			else
				if usedSharedSelectState ~= nil and usedSharedSelectState ~= isSelectingItem then
					usedSharedSelectState:set(false)
				end

				sharedSelectState:set(isSelectingItem)
				isSelectingItem:set(true)

				selectionConnection = props.Events.OnItemClicked:Connect(function(instance)
					if isSelectingItem:get() then
						if props.Filter(instance) then
							props.Value:set(instance)
						end

						sharedSelectState:set(nil)
						isSelectingItem:set(false)
					end
				end)
			end
		end,
		Padding = 3,
		Content = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				Padding = UDim.new(0, 4),
				SortOrder = Enum.SortOrder.LayoutOrder,
			}),
			imageLabel({
				Size = UDim2.new(0, 16, 0, 16),
				Image = Computed(function()
					return instanceClassIcon:get().Image or ""
				end),
				ImageRectSize = Computed(function()
					return instanceClassIcon:get().ImageRectSize or Vector2.new(0, 0)
				end),
				ImageRectOffset = Computed(function()
					return instanceClassIcon:get().ImageRectOffset or Vector2.new(0, 0)
				end),
				Visible = Computed(function()
					return not isSelectingItem:get() and instanceClassIcon:get().Image ~= nil
				end),
				BackgroundTransparency = 1,
			}),
			text({
				Name = "Text",
				AutomaticSize = Enum.AutomaticSize.X,
				Size = UDim2.new(0, 0, 0, 16),
				Appearance = useColor("Text", true),
				Text = Computed(function()
					return isSelectingItem:get() and "Select instance.." or instanceName:get()
				end),
				LayoutOrder = 2,
			}),
		},
	})

	awaitLayer(component, function(layer)
		onDestroy(component, {
			Observer(layer.state.mouseDown):onChange(function()
				if isSelectingItem:get() and not layer.state.mouseDown:get() then
					isSelectingItem:set(false)
					sharedSelectState:set(nil)
					selectionConnection:Disconnect()
				end
			end),
		})
	end)

	return component
end

return objectPropertyField


local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New

local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local range = fusionComponents.common.range
local input = fusionComponents.common.input

local tailwind = require("@packages/tailwind")

local theme = require("@src/theme")

local function roundDecimals(value: number, decimals: number): number
	local multiplier = 10 ^ decimals
	return math.floor(value * multiplier + 0.5) / multiplier
end

type props = {
	Value: fusion.Value<NumberRange>,
	LayoutOrder: number,
	Min: number,
	Max: number,
	Step: number,
}

local function rangePropertyField(props: props)
	local function roundToStep(value: number): number
		local closestStep = math.round((value - props.Min) / props.Step) * props.Step + props.Min

		if closestStep <= props.Max and closestStep >= props.Min then
			return roundDecimals(closestStep, 3)
		else
			local roundingFunction = closestStep < props.Min and math.ceil or math.floor
			return roundDecimals(roundingFunction((value - props.Min) / props.Step) * props.Step + props.Min, 3)
		end
	end

	local textValue = Value(`{roundToStep(props.Value:get(false).Min)}, {roundToStep(props.Value:get(false).Max)}`)
	local sliderValue = Value(props.Value:get(false))

	local connections = {
		Observer(sliderValue):onChange(function()
			-- propagate value to text
			textValue:set(`{roundToStep(sliderValue:get().Min)}, {roundToStep(sliderValue:get().Max)}`)
		end),
		Observer(props.Value):onChange(function()
			if props.Value:get() ~= sliderValue:get() then
				sliderValue:set(props.Value:get())
			end
		end),
	}

	return New("Frame")({
		BackgroundTransparency = 1,
		LayoutOrder = props.LayoutOrder,
		Size = UDim2.new(1, 0, 0, 24),
		[Cleanup] = connections,
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				Padding = UDim.new(0, 4),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Center,
			}),
			range({
				Color = "blue",
				Width = UDim.new(1, -48),
				Min = props.Min,
				Max = props.Max,
				Tooltip = true,
				Value = Computed(function()
					local value = sliderValue:get()
					return { value.Min, value.Max }
				end),
				LayoutOrder = props.LayoutOrder,
				Step = props.Step,
				BarHeight = UDim.new(0, 6),
				HandleSize = UDim2.new(0, 12, 0, 12),
				OnValueChanged = function(index, value)
					local currentValue = sliderValue:get()
					sliderValue:set(
						index == 1 and NumberRange.new(value, currentValue.Max)
							or NumberRange.new(currentValue.Min, value)
					)
				end,
				OnDrag = function(isDragging, values)
					if not isDragging then
						props.Value:set(NumberRange.new(values[1], values[2]))
					end
				end,
			}),
			input({
				Variant = "default",
				Color = "gray",
				AppearanceOverride = {
					_global = {
						Background = {
							color = Computed(function()
								return theme.global.isDark:get() and tailwind.neutral[850] or tailwind.white
							end),
							shadow = 1,
						},
					},
					Focus = {
						Stroke = Computed(function()
							return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
						end),
					},
				},
				Size = UDim2.new(0, 48, 0, 20),
				Text = textValue,
				OnFocusLost = function(text)
					local values = text:split(",")

					for _, value in values do
						if not tonumber(value) then
							local currentSliderValue = sliderValue:get()
							textValue:set(
								`{roundToStep(currentSliderValue.Min)}, {roundToStep(currentSliderValue.Max)}`
							)
							return
						end
					end

					local newValue = NumberRange.new(tonumber(values[1]) :: number, tonumber(values[2]) :: number)

					sliderValue:set(newValue)
					props.Value:set(newValue)
				end,
			}),
		},
	})
end

return rangePropertyField


local fusion = require("@packages/fusion")
local Children = fusion.Children
local Cleanup = fusion.Cleanup
local New = fusion.New

local Computed = fusion.Computed
local Observer = fusion.Observer
local Value = fusion.Value

local fusionComponents = require("@packages/fusionComponents")
local slider = fusionComponents.common.slider
local input = fusionComponents.common.input

local tailwind = require("@packages/tailwind")

local theme = require("@src/theme")

local function roundDecimals(value: number, decimals: number): number
	local multiplier = 10 ^ decimals
	return math.floor(value * multiplier + 0.5) / multiplier
end

type props = {
	Value: fusion.Value<number>,
	LayoutOrder: number,
	Min: number,
	Max: number,
	Step: number,
}

local function sliderPropertyField(props: props)
	local function roundToStep(value: number): number
		local closestStep = math.round((value - props.Min) / props.Step) * props.Step + props.Min

		if closestStep <= props.Max and closestStep >= props.Min then
			return roundDecimals(closestStep, 3)
		else
			local roundingFunction = closestStep < props.Min and math.ceil or math.floor
			return roundDecimals(roundingFunction((value - props.Min) / props.Step) * props.Step + props.Min, 3)
		end
	end

	local textValue = Value(tostring(roundToStep(props.Value:get(false))))
	local sliderValue = Value(props.Value:get(false))

	local connections = {
		Observer(sliderValue):onChange(function()
			-- propagate value to text
			textValue:set(tostring(roundToStep(sliderValue:get())))
		end),
		Observer(props.Value):onChange(function()
			if props.Value:get() ~= sliderValue:get() then
				sliderValue:set(props.Value:get())
			end
		end),
	}

	return New("Frame")({
		BackgroundTransparency = 1,
		LayoutOrder = props.LayoutOrder,
		Size = UDim2.new(1, 0, 0, 20),
		[Cleanup] = connections,
		[Children] = {
			New("UIListLayout")({
				FillDirection = Enum.FillDirection.Horizontal,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				Padding = UDim.new(0, 4),
				SortOrder = Enum.SortOrder.LayoutOrder,
				VerticalAlignment = Enum.VerticalAlignment.Center,
			}),
			slider({
				Color = "blue",
				AppearanceOverride = {
					Background = Computed(function()
						return {
							color = theme.global.isDark:get() and tailwind.neutral[815] or tailwind.white,
							shadow = 1,
						}
					end),
				},
				Width = UDim.new(1, -32),
				Min = props.Min,
				Max = props.Max,
				Tooltip = true,
				Value = sliderValue,
				LayoutOrder = props.LayoutOrder,
				Step = props.Step,
				BarHeight = UDim.new(0, 6),
				HandleSize = UDim2.new(0, 12, 0, 12),
				OnValueChanged = function(value)
					sliderValue:set(value)
				end,
				OnDrag = function(isDragging, value)
					if not isDragging then
						props.Value:set(value)
					end
				end,
			}),
			input({
				Variant = "default",
				Color = "gray",
				AppearanceOverride = {
					_global = {
						Background = {
							color = Computed(function()
								return theme.global.isDark:get() and tailwind.neutral[850] or tailwind.white
							end),
							shadow = 1,
						},
					},
					Focus = {
						Stroke = Computed(function()
							return theme.global.isDark:get() and tailwind.blue[400] or tailwind.blue[500]
						end),
					},
				},
				Size = UDim2.new(0, 28, 0, 20),
				Text = textValue,
				OnFocusLost = function(text)
					-- filter value, then propagate to slider
					local value = tonumber(text)

					if not value then
						textValue:set(tostring(roundToStep(sliderValue:get())), true)
						return
					end

					sliderValue:set(value)
					props.Value:set(value)
				end,
			}),
		},
	})
end

return sliderPropertyField