--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local Lucide = require(pluginRoot.Packages.Lucide)
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local BaseButton = require(uiRoot.Components.Shared.BaseButton)
local Hooks = require(uiRoot.Hooks)

local FreeDrag = require(script.FreeDrag)

export type Props = {
	OnMinimize: () -> ()?,
	OnCapture: (Rect) -> (boolean, string?),
	FocusLenseSizeRef: { current: Vector2? },
	Disabled: boolean,
}

type ActionBarProps = {
	OnMinimize: () -> ()?,
	OnCapture: (Rect) -> (boolean, string?),
	IsDragging: boolean,
	ContainerPosition: Vector2,
	ContainerSize: Vector2,
	Disabled: boolean,
}

local function ActionBar(props: ActionBarProps)
	local moveIcon = assert(Lucide.GetAsset("move", 16))
	local cameraIcon = assert(Lucide.GetAsset("camera", 16))
	local minimizeIcon = assert(Lucide.GetAsset("minimize", 16))

	local theme = StudioComponents.useTheme()
	local scaleOS = Hooks.useStatePath(function(state)
		return state.temporary.scaleOS
	end)

	local underFrameRef = React.useRef(nil)
	local underFrameAbsPosition = Hooks.useRefProperty(underFrameRef, "AbsolutePosition", Vector2.zero)
	local underFrameAbsSize = Hooks.useRefProperty(underFrameRef, "AbsoluteSize", Vector2.zero)
	local underFrameBottomY = underFrameAbsPosition.Y + underFrameAbsSize.Y
	local useUnderFrame = underFrameBottomY <= (props.ContainerPosition.Y + props.ContainerSize.Y)

	local shakeOffset, makeShake = Hooks.useShake()
	local errorOffset, makeError = Hooks.useShake()

	local overlayColor = React.useMemo(function()
		return theme
			:GetColor(Enum.StudioStyleGuideColor.MainBackground)
			:Lerp(theme:GetColor(Enum.StudioStyleGuideColor.ErrorText), 0.2)
	end, { theme })

	-- stylua: ignore
	local displaySize = Vector2.new(
		math.round(scaleOS.X * props.ContainerSize.X),
		math.round(scaleOS.Y * props.ContainerSize.Y)
	)

	local labelWithOffset = 0
	if props.OnMinimize then
		labelWithOffset = -24 - 4
	end

	return React.createElement("Frame", {
		BackgroundTransparency = 1,
		Size = UDim2.fromScale(1, 1),
	}, {
		ShakeFrame = React.createElement("Frame", {
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(1, 1),
			Position = UDim2.fromOffset(shakeOffset.X, shakeOffset.Y),
		}, {
			Overlay = shakeOffset.Z < 1 and React.createElement("Frame", {
				Active = false,
				BackgroundTransparency = math.lerp(0.6, 1, shakeOffset.Z ^ 2),
				BackgroundColor3 = overlayColor,
				Size = UDim2.fromScale(1, 1),
				ZIndex = 2,
			}),
			ActionBar = React.createElement("Frame", {
				Size = UDim2.fromScale(1, 1),
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
				BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
			}, {
				Padding = React.createElement("UIPadding", {
					PaddingLeft = UDim.new(0, 4),
					PaddingRight = UDim.new(0, 4),
					PaddingTop = UDim.new(0, 4),
					PaddingBottom = UDim.new(0, 4),
				}),
				Layout = React.createElement("UIListLayout", {
					SortOrder = Enum.SortOrder.LayoutOrder,
					FillDirection = Enum.FillDirection.Horizontal,
					VerticalAlignment = Enum.VerticalAlignment.Center,
					HorizontalAlignment = Enum.HorizontalAlignment.Left,
					Padding = UDim.new(0, 0),
				}),

				Icon = React.createElement("ImageLabel", {
					Image = moveIcon.Url,
					ImageRectSize = moveIcon.ImageRectSize,
					ImageRectOffset = moveIcon.ImageRectOffset,

					Size = UDim2.fromOffset(moveIcon.ImageRectSize.X, moveIcon.ImageRectSize.Y),
					BackgroundTransparency = 1,
					ImageColor3 = Color3.fromRGB(102, 102, 102),
					LayoutOrder = 1,
				}),

				Spacing1 = React.createElement("Frame", {
					Size = UDim2.new(0, 4, 1, 0),
					BackgroundTransparency = 1,
					LayoutOrder = 2,
				}),

				Label = React.createElement("TextLabel", {
					Size = UDim2.new(1, -24 - 16 - 4 - 6 + labelWithOffset, 1, 0),
					BackgroundTransparency = 1,

					Text = `{displaySize.X} x {displaySize.Y}`,
					TextColor3 = Color3.fromRGB(102, 102, 102),
					TextXAlignment = Enum.TextXAlignment.Center,
					TextYAlignment = Enum.TextYAlignment.Center,
					TextTruncate = Enum.TextTruncate.AtEnd,
					TextSize = 8,

					LayoutOrder = 3,
				}),

				Spacing2 = React.createElement("Frame", {
					Size = UDim2.new(0, 6, 1, 0),
					BackgroundTransparency = 1,
					LayoutOrder = 4,
				}),

				Minimize = props.OnMinimize and React.createElement(BaseButton, {
					Size = UDim2.new(0, 24, 1, 0),

					BackgroundColorStyle = Enum.StudioStyleGuideColor.Button,
					BorderColorStyle = Enum.StudioStyleGuideColor.Border,
					TextColorStyle = Enum.StudioStyleGuideColor.ButtonText,

					Icon = {
						Image = minimizeIcon.Url,
						RectSize = minimizeIcon.ImageRectSize,
						RectOffset = minimizeIcon.ImageRectOffset,

						Size = minimizeIcon.ImageRectSize,
						Transparency = if props.Disabled then 0.7 else 0,
						UseThemeColor = true,
					},

					OnActivated = props.OnMinimize,
					Disabled = props.Disabled,
					LayoutOrder = 5,
				}),

				Spacing3 = props.OnMinimize and React.createElement("Frame", {
					Size = UDim2.new(0, 4, 1, 0),
					BackgroundTransparency = 1,
					LayoutOrder = 6,
				}),

				Capture = React.createElement(StudioComponents.MainButton, {
					Size = UDim2.new(0, 24, 1, 0),

					Icon = {
						Image = cameraIcon.Url,
						RectSize = cameraIcon.ImageRectSize,
						RectOffset = cameraIcon.ImageRectOffset,

						Size = cameraIcon.ImageRectSize,
						Transparency = if props.Disabled then 0.7 else 0,
						Color = Color3.new(1, 1, 1),
					},

					OnActivated = function()
						local success, _err = props.OnCapture(Rect.new(Vector2.zero, displaySize))
						if not success then
							makeShake(1, Vector2.new(7, 4), Vector2.new(4, 2))
							makeError(1.5, Vector2.new(7, 4), Vector2.new(0, 0))
						end
					end,
					Disabled = props.Disabled,
					LayoutOrder = 7,
				}),
			}),
		}),

		ErrorLabel = errorOffset.Z < 1 and React.createElement("TextLabel", {
			BackgroundTransparency = 1,
			Position = if useUnderFrame then UDim2.new(0, 0, 1, 2) else UDim2.new(0, 0, -1, -2),
			Size = UDim2.fromScale(1, 1),
			FontFace = Font.fromName("SourceSansPro", Enum.FontWeight.Bold, Enum.FontStyle.Normal),
			Text = "Error: Check the studio output (Window > Output)",
			TextSize = 14,
			TextTransparency = errorOffset.Z ^ 20,
			TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.ErrorText),
			TextWrapped = true,
			TextXAlignment = Enum.TextXAlignment.Center,
			TextYAlignment = if useUnderFrame then Enum.TextYAlignment.Top else Enum.TextYAlignment.Bottom,
		}, {
			UIStroke = React.createElement("UIStroke", {
				Thickness = 1,
				Transparency = errorOffset.Z ^ 20,
				Color = Color3.new(0, 0, 0),
			}),
		}),
		UnderFrame = React.createElement("Frame", {
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(1, 1),
			Position = UDim2.new(0, 0, 1, 2),
			ref = underFrameRef,
		}),
	})
end

local function InnerBorder(props: { Thickness: number, Padding: number, Disabled: boolean, children: React.ReactNode })
	local theme = StudioComponents.useTheme()

	return React.createElement("Frame", {
		Size = UDim2.new(1, -props.Thickness * 2, 1, -props.Thickness * 2),
		Position = UDim2.fromScale(0.5, 0.5),
		AnchorPoint = Vector2.new(0.5, 0.5),
		BackgroundTransparency = 1,
	}, {
		StrokeFrame = React.createElement("Frame", {
			Size = UDim2.fromScale(1, 1),
			BackgroundTransparency = 1,
			ZIndex = 2,
		}, {
			UIStroke = React.createElement("UIStroke", {
				Thickness = props.Thickness,
				Transparency = 0,
				LineJoinMode = Enum.LineJoinMode.Miter,
				Color = theme:GetColor(
					Enum.StudioStyleGuideColor.InputFieldBorder,
					Enum.StudioStyleGuideModifier.Selected
				),
			}),
		}),
		ChildFrame = React.createElement("Frame", {
			Size = UDim2.new(1, -props.Padding * 2, 1, -props.Padding * 2),
			Position = UDim2.fromScale(0.5, 0.5),
			AnchorPoint = Vector2.new(0.5, 0.5),
			BackgroundTransparency = 1,
			ZIndex = 1,
		}, props.children),
	})
end

local function FullViewport(props: Props)
	local isDragging, setIsDragging = React.useState(false)
	local containerRef = React.useRef(nil :: GuiObject?)
	local containerSize = Hooks.useRefProperty(containerRef, "AbsoluteSize", Vector2.zero)
	local containerPosition = Hooks.useRefProperty(containerRef, "AbsolutePosition", Vector2.zero)

	React.useEffect(function()
		props.FocusLenseSizeRef.current = containerSize
	end, { containerSize })

	local draggerSize = Vector2.new(120, 28)
	if props.OnMinimize then
		draggerSize = Vector2.new(148, 28)
	end

	local draggerScale = 1
	if containerSize.X < draggerSize.X * 2 or containerSize.Y < draggerSize.Y * 2 then
		draggerScale = math.min(containerSize.X / (draggerSize.X * 2), containerSize.Y / (draggerSize.Y * 2))
	end

	return React.createElement("Frame", {
		Size = UDim2.fromScale(1, 1),
		BackgroundTransparency = 1,
		ref = containerRef,
	}, {
		Outline = React.createElement(InnerBorder, {
			Thickness = 2,
			Padding = 1,
			Disabled = false,
		}, {
			Dragger = React.createElement(FreeDrag, {
				SetIsDragging = setIsDragging,

				InitialPosition = UDim2.fromScale(0, 0),
				AnchorPoint = Vector2.new(0, 0),
				Size = UDim2.fromOffset(draggerSize.X, draggerSize.Y),

				Scale = draggerScale,

				Disabled = props.Disabled,
			}, {
				ActionBar = React.createElement(ActionBar, {
					IsDragging = isDragging,
					ContainerPosition = containerPosition,
					ContainerSize = containerSize,
					OnMinimize = props.OnMinimize,
					OnCapture = props.OnCapture,
					Disabled = props.Disabled,
				}),
			}),
		}),
	})
end

return FullViewport


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local Lucide = require(pluginRoot.Packages.Lucide)
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local LucideButton = require(uiRoot.Components.Shared.LucideButton)

local IMPORT_ICON = assert(Lucide.GetAsset("image-up", 16))
local RIGHT_ICON = assert(Lucide.GetAsset("chevron-right", 16))
local LEFT_ICON = assert(Lucide.GetAsset("chevron-left", 16))

export type Props = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	SelectedIndex: number,
	SetSelectedIndex: (number) -> (),
	SelectedCount: number,

	OnImport: () -> (),

	Disabled: boolean,
}

local function getImageButtonColor(theme: StudioTheme, modifier: Enum.StudioStyleGuideModifier)
	if modifier == Enum.StudioStyleGuideModifier.Selected then
		return theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, Enum.StudioStyleGuideModifier.Pressed)
	elseif modifier == Enum.StudioStyleGuideModifier.Hover then
		return theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, Enum.StudioStyleGuideModifier.Hover)
	end
	return theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, Enum.StudioStyleGuideModifier.Disabled)
end

local function ActionBar(props: Props)
	local theme = StudioComponents.useTheme()

	local function incrementIndex(step: number)
		local newIndex = ((props.SelectedIndex + step - 1) % props.SelectedCount) + 1
		props.SetSelectedIndex(newIndex)
	end

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Titlebar),
		BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
	}, {
		Padding = React.createElement("UIPadding", {
			PaddingLeft = UDim.new(0, 8),
			PaddingRight = UDim.new(0, 4),
			PaddingTop = UDim.new(0, 4),
			PaddingBottom = UDim.new(0, 4),
		}),

		ImageIndexer = React.createElement("Frame", {
			BackgroundTransparency = 1,
			AnchorPoint = Vector2.new(0, 0.5),
			Position = UDim2.fromScale(0, 0.5),
			Size = UDim2.fromOffset(16 * 6, 16),
		}, {
			LeftButton = React.createElement(LucideButton, {
				AnchorPoint = Vector2.new(0, 0.5),
				Position = UDim2.new(0, 0, 0.5, 0),
				Size = UDim2.fromOffset(LEFT_ICON.ImageRectSize.X, LEFT_ICON.ImageRectSize.Y),
				Disabled = props.Disabled or props.SelectedCount == 0,
				Icon = LEFT_ICON,

				GetColor = function(modifier)
					return getImageButtonColor(theme, modifier)
				end,

				OnActivated = function()
					incrementIndex(-1)
				end,
			}),

			CountLabel = React.createElement("TextLabel", {
				BackgroundTransparency = 1,
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.5, 0.5),
				Size = UDim2.new(1, -40, 1, 0),

				Text = `{math.min(props.SelectedIndex, props.SelectedCount)} / {props.SelectedCount}`,
				TextColor3 = theme:GetColor(
					Enum.StudioStyleGuideColor.ButtonText,
					Enum.StudioStyleGuideModifier.Disabled
				),
				TextXAlignment = Enum.TextXAlignment.Center,
				TextYAlignment = Enum.TextYAlignment.Center,
				TextTruncate = Enum.TextTruncate.AtEnd,
			}),

			RightButton = React.createElement(LucideButton, {
				AnchorPoint = Vector2.new(1, 0.5),
				Position = UDim2.new(1, 0, 0.5, 0),
				Size = UDim2.fromOffset(RIGHT_ICON.ImageRectSize.X, RIGHT_ICON.ImageRectSize.Y),
				Disabled = props.Disabled or props.SelectedCount == 0,
				Icon = RIGHT_ICON,

				GetColor = function(modifier)
					return getImageButtonColor(theme, modifier)
				end,

				OnActivated = function()
					incrementIndex(1)
				end,
			}),
		}),

		Buttons = React.createElement("Frame", {
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(1, 1),
		}, {
			Layout = React.createElement("UIListLayout", {
				SortOrder = Enum.SortOrder.LayoutOrder,
				FillDirection = Enum.FillDirection.Horizontal,
				VerticalAlignment = Enum.VerticalAlignment.Center,
				HorizontalAlignment = Enum.HorizontalAlignment.Right,
				Padding = UDim.new(0, 4),
			}),

			Import = false and React.createElement(StudioComponents.MainButton, {
				LayoutOrder = -1,
				Size = UDim2.new(0, 70, 1, 0),
				TextXAlignment = Enum.TextXAlignment.Center,
				Text = "Import",
				Disabled = props.Disabled,
				OnActivated = function()
					props.OnImport()
				end,

				Icon = {
					Image = IMPORT_ICON.Url,
					RectSize = IMPORT_ICON.ImageRectSize,
					RectOffset = IMPORT_ICON.ImageRectOffset,
					Size = Vector2.new(IMPORT_ICON.ImageRectSize.X, IMPORT_ICON.ImageRectSize.Y),
					UseThemeColor = true,
				},
			}),
		}),
	})
end

return ActionBar


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local Lucide = require(pluginRoot.Packages.Lucide)
local React = require(pluginRoot.Packages.React)

local CAN_TOGGLE_EXPANDS = false

export type NodeInfo = {
	Name: string,
	Node: React.Node?,
}

export type Props = {
	Title: string,
	List: { NodeInfo },
	HorizontalSplit: number,

	LayoutOrder: number,
	Disabled: boolean,
}

local function ExpandedElements(props: Props)
	local theme = StudioComponents.useTheme()

	local elements: { [string]: React.Node } = {}
	for i, entry in props.List do
		elements[entry.Name] = React.createElement("Frame", {
			LayoutOrder = i,
			Size = UDim2.fromScale(1, 1 / #props.List),
			BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
			BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
		}, {
			LeftSection = React.createElement("Frame", {
				BackgroundTransparency = 1,
				Size = UDim2.new(props.HorizontalSplit, -1, 1, 0),
			}, {
				Padding = React.createElement("UIPadding", {
					PaddingLeft = UDim.new(0, 20),
					PaddingRight = UDim.new(0, 4),
					PaddingTop = UDim.new(0, 4),
					PaddingBottom = UDim.new(0, 4),
				}),
				Label = React.createElement(StudioComponents.Label, {
					Position = UDim2.fromOffset(0, 0),
					Size = UDim2.new(1, 0, 1, 0),
					Text = entry.Name,
					TextTruncate = Enum.TextTruncate.SplitWord,
					TextXAlignment = Enum.TextXAlignment.Left,
				}),
			}),
			Line = React.createElement("Frame", {
				BorderSizePixel = 0,
				Size = UDim2.new(0, 1, 1, 0),
				Position = UDim2.fromScale(props.HorizontalSplit, 0),
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
			}),
			RightSection = React.createElement("Frame", {
				BackgroundTransparency = 1,
				Position = UDim2.new(props.HorizontalSplit, 0, 0, 0),
				Size = UDim2.new(1 - props.HorizontalSplit, 0, 1, 0),
			}, {
				Content = entry.Node,
			}),
		})
	end

	return React.createElement(React.Fragment, {}, {
		Layout = React.createElement("UIListLayout", {
			SortOrder = Enum.SortOrder.LayoutOrder,
			FillDirection = Enum.FillDirection.Vertical,
			VerticalAlignment = Enum.VerticalAlignment.Top,
			HorizontalAlignment = Enum.HorizontalAlignment.Center,
			Padding = UDim.new(0, 0),
		}),
		Elements = React.createElement(React.Fragment, {}, elements),
	})
end

local function SettingsGroup(props: Props)
	local theme = StudioComponents.useTheme()
	local expanded, setExpanded = React.useState(true)
	local hovering, setHovering = React.useState(false)

	local arrowIcon = if expanded
		then assert(Lucide.GetAsset("chevron-down", 16))
		else assert(Lucide.GetAsset("chevron-right", 16))

	local filteredEntries = {}
	for _i, entry in props.List do
		if entry.Node then
			table.insert(filteredEntries, entry)
		end
	end

	return React.createElement("Frame", {
		Size = UDim2.new(1, 0, 0, 26 + 24 * (if expanded then #filteredEntries else 0)),
		BackgroundTransparency = 0.1,
		LayoutOrder = props.LayoutOrder,
	}, {
		TitleBar = React.createElement("Frame", {
			Size = UDim2.new(1, 0, 0, 26),
			BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Titlebar),
			BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
			[React.Event.InputBegan] = function(_, input: InputObject)
				if input.UserInputType == Enum.UserInputType.MouseButton1 then
					if CAN_TOGGLE_EXPANDS and not props.Disabled then
						setExpanded(not expanded)
					end
				end
			end,
			[React.Event.MouseEnter] = function()
				if CAN_TOGGLE_EXPANDS then
					setHovering(true)
				end
			end,
			[React.Event.MouseLeave] = function()
				if CAN_TOGGLE_EXPANDS then
					setHovering(false)
				end
			end,
		}, {
			Arrow = React.createElement("ImageLabel", {
				Image = arrowIcon.Url,
				ImageRectSize = arrowIcon.ImageRectSize,
				ImageRectOffset = arrowIcon.ImageRectOffset,

				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.new(0, 9, 0.5, 0),
				Size = UDim2.fromOffset(arrowIcon.ImageRectSize.X, arrowIcon.ImageRectSize.Y),
				BackgroundTransparency = 1,
				ImageColor3 = theme:GetColor(
					Enum.StudioStyleGuideColor.MainText,
					if props.Disabled
						then Enum.StudioStyleGuideModifier.Disabled
						elseif hovering then Enum.StudioStyleGuideModifier.Hover
						else Enum.StudioStyleGuideModifier.Default
				),
			}),
			Label = React.createElement("TextLabel", {
				BackgroundTransparency = 1,
				Position = UDim2.fromOffset(20, 0),
				Size = UDim2.new(1, 0, 1, 0),
				TextSize = 14,
				Text = props.Title,
				TextXAlignment = Enum.TextXAlignment.Left,
				FontFace = Font.fromName("SourceSansPro", Enum.FontWeight.Bold),
				TextColor3 = theme:GetColor(
					Enum.StudioStyleGuideColor.MainText,
					if props.Disabled
						then Enum.StudioStyleGuideModifier.Disabled
						elseif hovering then Enum.StudioStyleGuideModifier.Hover
						else Enum.StudioStyleGuideModifier.Default
				),
			}),
		}),
		List = expanded and React.createElement("Frame", {
			BackgroundTransparency = 1,
			Position = UDim2.fromOffset(0, 26),
			Size = UDim2.new(1, 0, 1, -26),
		}, {
			Content = React.createElement(ExpandedElements, {
				Title = props.Title,
				List = filteredEntries,
				HorizontalSplit = props.HorizontalSplit,

				LayoutOrder = props.LayoutOrder,
				Disabled = props.Disabled,
			}),
		}),
	})
end

return SettingsGroup


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local Lucide = require(pluginRoot.Packages.Lucide)
local React = require(pluginRoot.Packages.React)

export type Props = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,
}

local function DisplayNoImage(props: Props)
	local theme = StudioComponents.useTheme()
	local imageIcon = assert(Lucide.GetAsset("images", 48))

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
		BackgroundTransparency = 1,
	}, {
		Layout = React.createElement("UIListLayout", {
			SortOrder = Enum.SortOrder.LayoutOrder,
			FillDirection = Enum.FillDirection.Vertical,
			VerticalAlignment = Enum.VerticalAlignment.Center,
			HorizontalAlignment = Enum.HorizontalAlignment.Center,
			Padding = UDim.new(0, 4),
		}),

		Icon = React.createElement("ImageLabel", {
			LayoutOrder = 0,
			Size = UDim2.fromOffset(imageIcon.ImageRectSize.X, imageIcon.ImageRectSize.Y),
			BackgroundTransparency = 1,
			Image = imageIcon.Url,
			ImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),
			ImageTransparency = 0.7,
			ImageRectSize = imageIcon.ImageRectSize,
			ImageRectOffset = imageIcon.ImageRectOffset,
		}),
		Label1 = React.createElement("TextLabel", {
			LayoutOrder = 1,
			Size = UDim2.new(0, 200, 0, 28),
			BackgroundTransparency = 1,
			Text = "Photobooth Gallery",
			TextScaled = true,
			TextTransparency = 0.7,
			TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),
		}),
		Label2 = React.createElement("TextLabel", {
			LayoutOrder = 3,
			Size = UDim2.new(0, 200, 0, 18),
			BackgroundTransparency = 1,
			Text = "No Image Selected",
			TextScaled = true,
			TextTransparency = 0.7,
			TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),
		}),
	})
end

return DisplayNoImage


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local Hooks = require(uiRoot.Hooks)

local NoImage = require(script.NoImage)
local DisplayImage = require(script.DisplayImage)

export type Props = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	EditableImageInfo: Hooks.EditableImageInfo?,

	BackgroundType: string,
	SetBackgroundType: (string) -> (),

	Padding: {
		PaddingTop: UDim?,
		PaddingLeft: UDim?,
		PaddingRight: UDim?,
		PaddingBottom: UDim?,
	}?,
}

local function ImageViewer(props: Props)
	return React.createElement("Frame", {
		BackgroundTransparency = 1,
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
	}, {
		NoImage = not props.EditableImageInfo and React.createElement(NoImage, {
			AnchorPoint = Vector2.new(0.5, 0.5),
			Position = UDim2.fromScale(0.5, 0.5),
			Size = UDim2.new(1, -44, 1, -44),
		}),

		DisplayImage = props.EditableImageInfo and React.createElement(DisplayImage, {
			Size = UDim2.new(1, -44, 1, -44),
			Position = UDim2.fromScale(0.5, 0.5),
			AnchorPoint = Vector2.new(0.5, 0.5),

			Name = props.EditableImageInfo.Name,
			EditableImage = props.EditableImageInfo.EditableImage,

			BackgroundType = props.BackgroundType,
			SetBackgroundType = props.SetBackgroundType,
		}),
	})
end

return ImageViewer


local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local Lucide = require(pluginRoot.Packages.Lucide)
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local VerticalScroll = require(uiRoot.Components.Shared.VerticalScroll)
local Hooks = require(uiRoot.Hooks)

local UPLOAD_ICON = assert(Lucide.GetAsset("upload", 16))

export type Props = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	SelectedIndex: number,
	SetSelectedIndex: (number) -> (),

	UploadExclusions: { [Instance]: boolean },
	SetUploadExclusions: ({ [Instance]: boolean }) -> (),

	TitleHeight: number,
	ElementHeight: number,

	OnUpload: () -> (),

	EditableImageInfos: { Hooks.EditableImageInfo },

	SinkInput: boolean,
	Disabled: boolean,
}

local function ToggleScrollList(props: Props)
	local theme = StudioComponents.useTheme()
	local hasCreateAssetPermission = Hooks.useHasCreateAssetPermission()
	local isStory = Hooks.useIsStory()

	local nSelected = #props.EditableImageInfos
	local uploadHeight = if isStory or hasCreateAssetPermission then 36 else 0

	local lines: { [string]: React.Node } = {}
	for i, info in props.EditableImageInfos do
		lines[`Line{i}`] = React.createElement("TextButton", {
			Size = UDim2.new(1, 0, 0, props.ElementHeight),
			AutoButtonColor = false,
			Text = "",

			BackgroundColor3 = if i == props.SelectedIndex
				then theme:GetColor(Enum.StudioStyleGuideColor.DialogMainButton)
				else theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
			BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),

			LayoutOrder = i,

			[React.Event.Activated] = function()
				props.SetSelectedIndex(i)
			end,
		}, {
			Padding = React.createElement("UIPadding", {
				PaddingLeft = UDim.new(0, 4),
				PaddingRight = UDim.new(0, 4),
				PaddingTop = UDim.new(0, 0),
				PaddingBottom = UDim.new(0, 0),
			}),
			Checkbox = React.createElement(StudioComponents.Checkbox, {
				AnchorPoint = Vector2.new(0, 0.5),
				Position = UDim2.fromScale(0, 0.5),
				Size = UDim2.new(0, StudioComponents.Constants.DefaultToggleHeight, 1, 0),

				Value = not props.UploadExclusions[info.Instance],
				OnChanged = function()
					props.UploadExclusions[info.Instance] = not props.UploadExclusions[info.Instance]
					props.SetUploadExclusions(table.clone(props.UploadExclusions))
				end,

				Disabled = props.Disabled,
			}),
			Label = React.createElement(StudioComponents.Label, {
				AnchorPoint = Vector2.new(0, 0.5),
				Position = UDim2.new(0, StudioComponents.Constants.DefaultToggleHeight + 2, 0.5, 0),
				Size = UDim2.new(1, -StudioComponents.Constants.DefaultToggleHeight, 1, 0),
				TextXAlignment = Enum.TextXAlignment.Left,
				TextYAlignment = Enum.TextYAlignment.Center,
				TextTruncate = Enum.TextTruncate.AtEnd,
				TextColorStyle = if i == props.SelectedIndex
					then Enum.StudioStyleGuideColor.DialogMainButtonText
					else Enum.StudioStyleGuideColor.MainText,
				Text = info.Name,
			}),
		})
	end

	local exclusionCount = React.useMemo(function()
		local count = 0
		for _, info in props.EditableImageInfos do
			if props.UploadExclusions[info.Instance] then
				count = count + 1
			end
		end
		return count
	end, { props.UploadExclusions })

	local inclusionCount = nSelected - exclusionCount

	return React.createElement("Frame", {
		BackgroundTransparency = 1,
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
	}, {
		Title = React.createElement("Frame", {
			AnchorPoint = Vector2.new(0, 0),
			Position = UDim2.fromScale(0, 0),
			Size = UDim2.new(1, 0, 0, props.TitleHeight),

			BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Light),
			BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),

			ZIndex = 2,
		}, {
			BorderLine = React.createElement("Frame", {
				BorderSizePixel = 0,
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
				Position = UDim2.fromOffset(0, 1),
				Size = UDim2.new(1, 0, 0, 1),
				Visible = not (props.SinkInput or props.Disabled),
			}),
			Clipped = React.createElement("Frame", {
				BackgroundTransparency = 1,
				Size = UDim2.fromScale(1, 1),
				ClipsDescendants = true,
			}, {
				Padding = React.createElement("UIPadding", {
					PaddingLeft = UDim.new(0, 4),
					PaddingRight = UDim.new(0, 8),
					PaddingTop = UDim.new(0, 0),
					PaddingBottom = UDim.new(0, 0),
				}),
				Checkbox = React.createElement(StudioComponents.Checkbox, {
					AnchorPoint = Vector2.new(0, 0.5),
					Position = UDim2.fromScale(0, 0.5),
					Size = UDim2.fromScale(1, 1),

					Value = inclusionCount > 0,
					OnChanged = function()
						if inclusionCount > 0 then
							local exclusions = {}
							for _, info in props.EditableImageInfos do
								exclusions[info.Instance] = true
							end
							props.SetUploadExclusions(exclusions)
						else
							props.SetUploadExclusions({})
						end
					end,

					Disabled = props.Disabled or (#props.EditableImageInfos == 0),
				}),
				Label = React.createElement(StudioComponents.Label, {
					AnchorPoint = Vector2.new(0, 0.5),
					Position = UDim2.new(0, StudioComponents.Constants.DefaultToggleHeight + 2, 0.5, 0),
					Size = UDim2.new(1, -StudioComponents.Constants.DefaultToggleHeight, 1, 0),
					TextXAlignment = Enum.TextXAlignment.Left,
					TextYAlignment = Enum.TextYAlignment.Center,
					TextTruncate = Enum.TextTruncate.AtEnd,
					TextColorStyle = Enum.StudioStyleGuideColor.MainText,
					Text = `{inclusionCount} selected`,
				}),
			}),
		}),

		Scroll = React.createElement(VerticalScroll, {
			AnchorPoint = Vector2.zero,
			Position = UDim2.fromOffset(0, props.TitleHeight),
			Size = UDim2.new(1, 0, 1, -props.TitleHeight - uploadHeight),

			GradientColor = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
			GradientHeight = 8,

			Disabled = props.Disabled or props.SinkInput,
		}, {
			Layout = React.createElement("UIListLayout", {
				FillDirection = Enum.FillDirection.Vertical,
				SortOrder = Enum.SortOrder.LayoutOrder,
				Padding = UDim.new(0, 0),
			}),
		}, lines),

		Upload = React.createElement("Frame", {
			BackgroundTransparency = 1,
			AnchorPoint = Vector2.new(0, 1),
			Position = UDim2.fromScale(0, 1),
			Size = UDim2.new(1, 0, 0, uploadHeight),
			Visible = uploadHeight > 0,
			ClipsDescendants = true,
		}, {
			Padding = React.createElement("UIPadding", {
				PaddingLeft = UDim.new(0, 3),
				PaddingRight = UDim.new(0, 3),
				PaddingTop = UDim.new(0, 3),
				PaddingBottom = UDim.new(0, 3),
			}),
			ClipFrame = React.createElement("Frame", {
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 1, 0),
				ClipsDescendants = true,
			}, {
				Button = React.createElement(StudioComponents.Button, {
					LayoutOrder = -2,
					Size = UDim2.new(1, -2, 1, -2),
					AnchorPoint = Vector2.new(0.5, 0.5),
					Position = UDim2.fromScale(0.5, 0.5),
					TextXAlignment = Enum.TextXAlignment.Center,
					Text = "Upload",
					Disabled = props.Disabled or inclusionCount == 0,
					OnActivated = props.OnUpload,

					Icon = {
						Image = UPLOAD_ICON.Url,
						RectSize = UPLOAD_ICON.ImageRectSize,
						RectOffset = UPLOAD_ICON.ImageRectOffset,
						Size = Vector2.new(UPLOAD_ICON.ImageRectSize.X, UPLOAD_ICON.ImageRectSize.Y),
						UseThemeColor = true,
					},
				}),
			}),
		}),
	})
end

return ToggleScrollList


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local Lucide = require(pluginRoot.Packages.Lucide)
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local LucideButton = require(uiRoot.Components.Shared.LucideButton)
local Hooks = require(uiRoot.Hooks)

local ToggleScrollList = require(script.ToggleScrollList)
local ImageViewer = require(script.ImageViewer)

local SPLIT_UDIM = UDim.new(0.25, 0)

local MAXIMIZE_ICON = assert(Lucide.GetAsset("chevrons-right", 16))
local MINIMIZE_ICON = assert(Lucide.GetAsset("chevrons-left", 16))

export type Props = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	EditableImageInfos: { Hooks.EditableImageInfo },

	IsExpanded: boolean,
	SetIsExpanded: (boolean) -> (),

	SelectedIndex: number,
	SetSelectedIndex: (number) -> (),

	UploadExclusions: { [Instance]: boolean },
	SetUploadExclusions: ({ [Instance]: boolean }) -> (),

	BackgroundType: string,
	SetBackgroundType: (string) -> (),

	OnUpload: () -> (),

	SinkInput: boolean,
	Disabled: boolean,
}

local function getImageButtonColor(theme: StudioTheme, modifier: Enum.StudioStyleGuideModifier)
	if modifier == Enum.StudioStyleGuideModifier.Selected then
		return theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, Enum.StudioStyleGuideModifier.Pressed)
	elseif modifier == Enum.StudioStyleGuideModifier.Hover then
		return theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, Enum.StudioStyleGuideModifier.Hover)
	end
	return theme:GetColor(Enum.StudioStyleGuideColor.ButtonText, Enum.StudioStyleGuideModifier.Disabled)
end

local function ImageInterface(props: Props)
	local theme = StudioComponents.useTheme()

	local leftRef = React.useRef(nil :: GuiObject?)
	local leftAbsSize = Hooks.useRefProperty(leftRef, "AbsoluteSize", Vector2.zero)

	local minMaxIcon = if props.IsExpanded then MINIMIZE_ICON else MAXIMIZE_ICON
	local splitUDim = if props.IsExpanded then SPLIT_UDIM else UDim.new(0, 0)

	local selectedInfo = props.EditableImageInfos[props.SelectedIndex]
	local prevSelectedInstanceRef = React.useRef(if selectedInfo then selectedInfo.Instance else nil)

	React.useEffect(function()
		local info = props.EditableImageInfos[props.SelectedIndex]
		prevSelectedInstanceRef.current = if info then info.Instance else nil
	end, { props.SelectedIndex })

	React.useEffect(function()
		local index = 1
		local exclusions = {}
		local prevSelectedInstance = prevSelectedInstanceRef.current
		for i = #props.EditableImageInfos, 1, -1 do
			local info = props.EditableImageInfos[i]
			if prevSelectedInstance and info.Instance == prevSelectedInstance then
				index = i
			end

			if props.UploadExclusions[info.Instance] then
				exclusions[info.Instance] = true
			end
		end

		local info = props.EditableImageInfos[index]
		prevSelectedInstanceRef.current = if info then info.Instance else nil

		props.SetSelectedIndex(index)
		props.SetUploadExclusions(exclusions)
	end, { props.EditableImageInfos })

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
		BackgroundTransparency = 1,
	}, {
		Left = React.createElement("Frame", {
			BackgroundTransparency = 0,
			AnchorPoint = Vector2.new(0, 0),
			Position = UDim2.fromOffset(0, 0),
			Size = UDim2.new(splitUDim.Scale, splitUDim.Offset, 1, 0),
			BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
			BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
			ref = leftRef,
		}, {
			SizeConstraint = props.IsExpanded and React.createElement("UISizeConstraint", {
				MinSize = Vector2.new(150, 0),
				MaxSize = Vector2.new(200, math.huge),
			}),

			MinMaxButton = React.createElement(LucideButton, {
				AnchorPoint = Vector2.new(0, 1),
				Position = UDim2.new(1, 0, 1, 0),
				Size = UDim2.fromOffset(minMaxIcon.ImageRectSize.X, minMaxIcon.ImageRectSize.Y),

				Icon = minMaxIcon,
				GetColor = function(modifier)
					return getImageButtonColor(theme, modifier)
				end,

				OnActivated = function()
					props.SetIsExpanded(not props.IsExpanded)
				end,

				Disabled = props.Disabled,
			}),

			ImageList = React.createElement(ToggleScrollList, {
				AnchorPoint = Vector2.new(0, 0),
				Position = UDim2.fromOffset(0, 0),
				Size = UDim2.new(1, 0, 1, 0),

				SelectedIndex = props.SelectedIndex,
				SetSelectedIndex = function(i)
					local info = props.EditableImageInfos[i]
					prevSelectedInstanceRef.current = if info then info.Instance else nil
					props.SetSelectedIndex(i)
				end,

				UploadExclusions = props.UploadExclusions,
				SetUploadExclusions = props.SetUploadExclusions,

				TitleHeight = 28,
				ElementHeight = 24,

				EditableImageInfos = props.EditableImageInfos,

				OnUpload = props.OnUpload,

				SinkInput = props.SinkInput,
				Disabled = props.Disabled,
			}),
		}),

		Right = React.createElement("Frame", {
			BackgroundTransparency = 1,
			AnchorPoint = Vector2.new(1, 0),
			Position = UDim2.fromScale(1, 0),
			Size = UDim2.new(1, -leftAbsSize.X, 1, 0),
			ZIndex = -1,
		}, {
			ImageViewer = React.createElement(ImageViewer, {
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.5, 0.5),
				Size = UDim2.new(1, 0, 1, 0),

				EditableImageInfo = props.EditableImageInfos[props.SelectedIndex],

				BackgroundType = props.BackgroundType,
				SetBackgroundType = props.SetBackgroundType,
			}),
		}),
	})
end

return ImageInterface


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local Signal = require(pluginRoot.Packages.Signal)
local Trove = require(pluginRoot.Packages.Trove)
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local VerticalScroll = require(uiRoot.Components.Shared.VerticalScroll)
local Hooks = require(uiRoot.Hooks)

local UploadLineItem = require(script.UploadLineItem)

local MAX_RETRIES = 1
local SEQUENTIAL_UPLOAD = true

type Output = { { Name: string, AssetId: number } }
type UploadStatus = "Waiting" | "Queued" | "InProgress" | "Complete" | "Fail" | "Cancel"

export type Props = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	UploadInfos: { Hooks.EditableImageInfo },
	SetUploadInfos: ({ Hooks.EditableImageInfo }?) -> (),

	TryUpload: (number, string, EditableImage) -> number?,
	OnUploadComplete: (Output) -> (),

	OnCancel: () -> (),

	Disabled: boolean,
}

local function useCreatorTargets()
	return { "Me" }
end

local function UploadWindow(props: Props)
	local theme = StudioComponents.useTheme()

	local creatorTargets = useCreatorTargets()
	local creator, setCreator = React.useState(creatorTargets[1])

	local cancelSignal = Hooks.useConstant(function()
		return Signal.new() :: Signal.Signal<number>
	end)

	local closeTrove = Hooks.useConstant(function()
		return Trove.new()
	end)

	React.useEffect(function()
		return closeTrove:WrapClean()
	end, {})

	local assetIdRef = React.useRef({})
	local outputRef = React.useRef({})

	local uploadState, setIsUploadState = React.useState("NotStarted")
	local uploadStatusesBinding, setUploadStatuses =
		React.useBinding(table.create(#props.UploadInfos, "Waiting" :: UploadStatus))
	local uploadStatuses = Hooks.useBindingState(uploadStatusesBinding) :: { UploadStatus }

	local lines: { [string]: React.Node } = {}
	for i, info in props.UploadInfos do
		local status = uploadStatuses[i]

		lines[`Line{i}`] = React.createElement(UploadLineItem, {
			AnchorPoint = Vector2.zero,
			Position = UDim2.fromScale(0, 0),
			Size = UDim2.new(1, 0, 0, 64),

			Name = info.Name,
			EditableImage = info.EditableImage,

			AssetId = assetIdRef.current[i],
			Progress = if status == "Complete" then 1 elseif status == "InProgress" then 0.05 else 0,
			StatusText = if status == "Waiting"
				then "Waiting"
				elseif status == "Fail" then "Failed"
				elseif status == "Cancel" then "Canceled"
				else nil,

			OnCancel = function()
				cancelSignal:Fire(i)
			end,

			LayoutOrder = i,
			Disabled = not (status == "Queued" or status == "InProgress" or status == "Complete"),
		})
	end

	return React.createElement("Frame", {
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
		BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
	}, {
		UISizeConstraint = React.createElement("UISizeConstraint", {
			MinSize = Vector2.new(0, 0),
			MaxSize = Vector2.new(350, 400),
		}),
		ClipFrame = React.createElement("Frame", {
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(1, 1),
			ClipsDescendants = true,
		}, {
			Padding = React.createElement("UIPadding", {
				PaddingLeft = UDim.new(0, 4),
				PaddingRight = UDim.new(0, 4),
				PaddingTop = UDim.new(0, 4),
				PaddingBottom = UDim.new(0, 4),
			}),
			Layout = React.createElement("UIListLayout", {
				SortOrder = Enum.SortOrder.LayoutOrder,
				FillDirection = Enum.FillDirection.Vertical,
				VerticalAlignment = Enum.VerticalAlignment.Top,
				HorizontalAlignment = Enum.HorizontalAlignment.Center,
				Padding = UDim.new(0, 6),
			}),

			Title = React.createElement("TextLabel", {
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 24),
				Text = "Upload",
				TextXAlignment = Enum.TextXAlignment.Left,
				TextYAlignment = Enum.TextYAlignment.Bottom,
				TextColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainText),
				Font = StudioComponents.Constants.DefaultFont,
				TextScaled = true,
				LayoutOrder = 1,
			}, {
				Padding = React.createElement("UIPadding", {
					PaddingLeft = UDim.new(0, 4),
					PaddingRight = UDim.new(0, 0),
					PaddingTop = UDim.new(0, 0),
					PaddingBottom = UDim.new(0, 0),
				}),
			}),

			FileList = React.createElement("Frame", {
				Size = UDim2.new(1, 0, 1, -24 - 6 - 32 - 6),
				BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Titlebar),
				BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
				LayoutOrder = 2,
			}, {
				Scroll = React.createElement(VerticalScroll, {
					AnchorPoint = Vector2.zero,
					Position = UDim2.fromScale(0, 0),
					Size = UDim2.fromScale(1, 1),

					GradientColor = theme:GetColor(Enum.StudioStyleGuideColor.Titlebar),
					GradientHeight = 8,

					Disabled = props.Disabled,
				}, {
					Layout = React.createElement("UIListLayout", {
						FillDirection = Enum.FillDirection.Vertical,
						SortOrder = Enum.SortOrder.LayoutOrder,
						Padding = UDim.new(0, 0),
					}),
				}, lines),
			}),

			BottomRow = React.createElement("Frame", {
				BackgroundTransparency = 1,
				Size = UDim2.new(1, 0, 0, 32),
				LayoutOrder = 3,
			}, {
				Buttons = React.createElement("Frame", {
					BackgroundTransparency = 1,
					Size = UDim2.fromScale(1, 1),
				}, {
					Layout = React.createElement("UIListLayout", {
						SortOrder = Enum.SortOrder.LayoutOrder,
						FillDirection = Enum.FillDirection.Horizontal,
						VerticalAlignment = Enum.VerticalAlignment.Center,
						HorizontalAlignment = Enum.HorizontalAlignment.Right,
						Padding = UDim.new(0, 4),
					}),

					Confirm = React.createElement(StudioComponents.MainButton, {
						LayoutOrder = -1,
						Size = UDim2.new(0, 70, 1, 0),
						TextXAlignment = Enum.TextXAlignment.Center,
						Text = if uploadState == "Complete" then "Open" else "Upload",
						Disabled = props.Disabled or uploadState == "Uploading",
						OnActivated = function()
							if uploadState == "Complete" then
								props.OnUploadComplete(outputRef.current)
							else
								local cleanupTrove = Trove.new()
								closeTrove:Add(cleanupTrove)
								closeTrove:Add(task.spawn(function()
									setIsUploadState("Uploading")

									outputRef.current = {}

									local uploadThreads = {}
									local uploadCompleteSignal = Signal.new()

									cleanupTrove:Add(cancelSignal:Connect(function(i)
										local statuses = uploadStatusesBinding:getValue()
										if statuses[i] == "InProgress" or statuses[i] == "Queued" then
											statuses[i] = "Cancel"
											setUploadStatuses(table.clone(statuses))

											local uploadThread = uploadThreads[i]
											if uploadThread and coroutine.status(uploadThread) == "suspended" then
												task.cancel(uploadThread)
											end

											uploadCompleteSignal:Fire(i, true)
										end
									end))

									local statuses = uploadStatusesBinding:getValue()
									for i, _info in props.UploadInfos do
										statuses[i] = "Queued"
									end
									setUploadStatuses(table.clone(statuses))

									local function uploadYielding(i: number, info: Hooks.EditableImageInfo)
										statuses = uploadStatusesBinding:getValue()
										statuses[i] = "InProgress"
										setUploadStatuses(table.clone(statuses))

										local assetId: number?
										uploadThreads[i] = cleanupTrove:Add(task.spawn(function()
											for _ = 1, MAX_RETRIES do
												local success, err = pcall(function()
													assetId = props.TryUpload(i, info.Name, info.EditableImage)
												end)

												if not success and err then
													warn(err)
												end

												if assetId then
													break
												end
											end
											uploadCompleteSignal:Fire(i, false)
										end))

										-- signal library has an annoying warn on cleanup so can't use signal:Wait()
										local isCancelled = false
										local myThread = coroutine.running()
										local connection =
											cleanupTrove:Add(uploadCompleteSignal:Connect(function(j, canceled)
												if i == j then
													isCancelled = canceled
													task.spawn(myThread)
												end
											end))

										coroutine.yield()
										connection:Disconnect()

										if not isCancelled then
											statuses = uploadStatusesBinding:getValue()

											if assetId then
												assetIdRef.current[i] = assetId
												statuses[i] = "Complete"
												table.insert(outputRef.current, {
													Name = info.Name,
													AssetId = assetId,
												})
											else
												statuses[i] = "Fail"
											end

											setUploadStatuses(table.clone(statuses))
										end
									end

									local count = 0
									local thread = coroutine.running()
									for i, info in props.UploadInfos do
										local status = uploadStatusesBinding:getValue()[i]
										if status == "Queued" then
											if SEQUENTIAL_UPLOAD then
												uploadYielding(i, info)
												count = count + 1
											else
												cleanupTrove:Add(task.spawn(function()
													uploadYielding(i, info)
													count = count + 1
													if count == #props.UploadInfos then
														task.spawn(thread)
													end
												end))
											end
										elseif status == "Cancel" then
											count = count + 1
										end
									end

									if count < #props.UploadInfos then
										coroutine.yield()
									end
									cleanupTrove:Destroy()

									setIsUploadState("Complete")
								end))
							end
						end,
					}),
					Cancel = React.createElement(StudioComponents.Button, {
						LayoutOrder = -2,
						Size = UDim2.new(0, 70, 1, 0),
						TextXAlignment = Enum.TextXAlignment.Center,
						Text = if uploadState == "Complete" then "Close" else "Cancel",
						Disabled = props.Disabled,
						OnActivated = function()
							closeTrove:Clean()
							props.OnCancel()
						end,
					}),
				}),

				Creator = React.createElement("Frame", {
					BackgroundTransparency = 1,
					Size = UDim2.new(1, -152, 1, 0),
					Position = UDim2.fromOffset(4, 0),
				}, {
					Layout = React.createElement("UIListLayout", {
						SortOrder = Enum.SortOrder.LayoutOrder,
						FillDirection = Enum.FillDirection.Horizontal,
						VerticalAlignment = Enum.VerticalAlignment.Center,
						HorizontalAlignment = Enum.HorizontalAlignment.Left,
						Padding = UDim.new(0, 4),
					}),

					Label = React.createElement(StudioComponents.Label, {
						BackgroundTransparency = 1,
						Size = UDim2.new(0, 44, 1, 0),
						Text = "Creator:",
						TextXAlignment = Enum.TextXAlignment.Left,
						TextYAlignment = Enum.TextYAlignment.Center,
						LayoutOrder = 1,
					}),

					Container = React.createElement("Frame", {
						BackgroundTransparency = 1,
						Size = UDim2.new(1, -48, 0, StudioComponents.Constants.DefaultDropdownHeight),
						LayoutOrder = 2,
					}, {
						UISizeConstraint = React.createElement("UISizeConstraint", {
							MaxSize = Vector2.new(70, math.huge),
						}),
						Dropdown = React.createElement(StudioComponents.Dropdown, {
							Disabled = props.Disabled or uploadState == "Uploading",
							Size = UDim2.fromScale(1, 1),
							Items = creatorTargets,
							SelectedItem = creator,
							OnItemSelected = function(item)
								setCreator(item :: string)
							end,
						}),
					}),
				}),
			}),
		}),
	})
end

return UploadWindow


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local Hooks = require(uiRoot.Hooks)

local ConfigurationWindow = require(script.ConfigurationWindow)
local ImageInterface = require(script.ImageInterface)
local UploadWindow = require(script.UploadWindow)
local ActionBar = require(script.ActionBar)

export type Props = {
	IsExpanded: boolean,
	SetIsExpanded: (boolean) -> (),

	BackgroundType: string,
	SetBackgroundType: (string) -> (),

	TryUpload: (number, string, EditableImage) -> number?,
	OnUploadComplete: ({ { Name: string, AssetId: number } }) -> (),

	OnImport: () -> (),

	Disabled: boolean,
}

local function useUploadInfos()
	local uploadInfos, setUploadInfos = React.useState(nil :: { Hooks.EditableImageInfo }?)
	return uploadInfos :: { Hooks.EditableImageInfo }?, setUploadInfos :: ({ Hooks.EditableImageInfo }?) -> ()
end

local function Gallery(props: Props)
	local selectedTab, setSelectedTab = React.useState("Viewer")

	local selection = Hooks.useSelection()
	local editImageInfos = Hooks.useEditableImageInfos(selection)
	local selectedIndex, setSelectedIndex = React.useState(1)
	local uploadExclusions, setUploadExclusions = React.useState({})
	local uploadInfos, setUploadInfos = useUploadInfos()

	return React.createElement("Frame", {
		BackgroundTransparency = 1,
		Size = UDim2.fromScale(1, 1),
		ClipsDescendants = true,
		ZIndex = -1, -- Fixes dropdown issues
	}, {
		LoadingOverlay = props.Disabled and React.createElement("ImageButton", {
			Size = UDim2.fromScale(1, 1),
			BackgroundColor3 = Color3.new(0, 0, 0),
			BackgroundTransparency = 0.7,
			AutoButtonColor = false,
			ZIndex = 4,
		}, {
			Loading = React.createElement(StudioComponents.LoadingDots, {
				Size = UDim2.fromScale(1, 1),
			}),
		}),

		Tabs = React.createElement(StudioComponents.TabContainer, {
			SelectedTab = selectedTab,
			OnTabSelected = setSelectedTab,
			Disabled = (props.Disabled or selectedTab == "Settings"),
			Size = UDim2.new(1, 0, 0, 30),
			ZIndex = if selectedTab ~= "Viewer" then 2 else 1,
		}, {
			["Viewer"] = {
				LayoutOrder = 1,
			},
			["Settings"] = {
				LayoutOrder = 2,
			},
		}),

		Overlay = (uploadInfos or selectedTab == "Settings") and React.createElement("ImageButton", {
			Size = UDim2.fromScale(1, 1),
			BackgroundColor3 = Color3.new(0, 0, 0),
			BackgroundTransparency = 0.7,
			AutoButtonColor = false,
			ZIndex = 3,
		}, {
			Upload = uploadInfos and React.createElement(UploadWindow, {
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.5, 0.5),
				Size = UDim2.new(1, -20, 1, -20),

				UploadInfos = uploadInfos,
				SetUploadInfos = setUploadInfos,

				TryUpload = props.TryUpload,
				OnUploadComplete = props.OnUploadComplete,

				OnCancel = function()
					setUploadInfos(nil)
				end,

				Disabled = props.Disabled,
			}),
			Configuration = selectedTab == "Settings" and React.createElement(ConfigurationWindow, {
				AnchorPoint = Vector2.new(0.5, 0.5),
				Position = UDim2.fromScale(0.5, 0.5),
				Size = UDim2.new(1, -20, 1, -20),

				OnConfirm = function()
					setSelectedTab("Viewer")
				end,

				Disabled = props.Disabled,
			}),
		}),

		ImageInterface = React.createElement(ImageInterface, {
			AnchorPoint = Vector2.new(0, 0),
			Position = UDim2.fromOffset(0, 30),
			Size = UDim2.new(1, 0, 1, -66),

			EditableImageInfos = editImageInfos,

			IsExpanded = props.IsExpanded,
			SetIsExpanded = props.SetIsExpanded,

			SelectedIndex = selectedIndex,
			SetSelectedIndex = setSelectedIndex,

			UploadExclusions = uploadExclusions,
			SetUploadExclusions = setUploadExclusions,

			BackgroundType = props.BackgroundType,
			SetBackgroundType = props.SetBackgroundType,

			OnUpload = function()
				local filteredInfos = {}
				for _, info in editImageInfos do
					if not uploadExclusions[info.Instance] then
						table.insert(filteredInfos, info)
					end
				end

				setUploadInfos(filteredInfos)
			end,

			SinkInput = props.Disabled or selectedTab == "Settings" or not not uploadInfos,
			Disabled = props.Disabled or selectedTab == "Settings",
		}),

		ActionBar = React.createElement(ActionBar, {
			AnchorPoint = Vector2.new(0, 1),
			Position = UDim2.fromScale(0, 1),
			Size = UDim2.new(1, 0, 0, 36),

			SelectedIndex = selectedIndex,
			SetSelectedIndex = setSelectedIndex,
			SelectedCount = #editImageInfos,

			OnImport = props.OnImport,

			Disabled = props.Disabled or selectedTab == "Settings",
		}),
	})
end

return Gallery


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Lucide = require(pluginRoot.Packages.Lucide)
local React = require(pluginRoot.Packages.React)

export type Props = {
	Disabled: boolean?,

	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	OnActivated: () -> ()?,

	GetColor: (Enum.StudioStyleGuideModifier) -> Color3,
	Icon: typeof(assert(Lucide.GetAsset(nil :: any, nil :: any))),
}

local function LucideButton(props: Props)
	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 and hovered then
		modifier = Enum.StudioStyleGuideModifier.Pressed
	elseif hovered then
		modifier = Enum.StudioStyleGuideModifier.Hover
	end

	local ref = React.useRef(nil :: ImageButton?)
	React.useEffect(function()
		local button = ref.current :: ImageButton
		local conn1 = button:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
			setHovered(false)
		end)

		local conn2 = button:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
			setHovered(false)
		end)

		return function()
			conn1:Disconnect()
			conn2:Disconnect()
		end
	end, {})

	return React.createElement("ImageButton", {
		BackgroundTransparency = 1,

		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,

		Image = props.Icon.Url,
		ImageRectSize = props.Icon.ImageRectSize,
		ImageRectOffset = props.Icon.ImageRectOffset,
		ImageColor3 = props.GetColor(modifier),

		[React.Event.InputBegan] = function(_, input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				setHovered(true)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				setPressed(true)
			end
		end,
		[React.Event.InputEnded] = function(_, input)
			if input.UserInputType == Enum.UserInputType.MouseMovement then
				setHovered(false)
			elseif input.UserInputType == Enum.UserInputType.MouseButton1 then
				setPressed(false)
			end
		end,
		[React.Event.Activated] = function()
			if not props.Disabled and props.OnActivated then
				props.OnActivated()
			end
		end,

		ref = ref,
	})
end

return LucideButton


local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local React = require(pluginRoot.Packages.React)

local uiRoot = script:FindFirstAncestor("UserInterface")
local Icons = require(uiRoot.Icons)
local Hooks = require(uiRoot.Hooks)

export type Props = {
	AnchorPoint: Vector2,
	Position: UDim2,
	Size: UDim2,

	ScrollInset: UDim?,
	ScrollBarThickness: number?,
	ScrollBarTransparency: number?,

	GradientHeight: number,
	GradientColor: Color3,

	Disabled: boolean,

	children: React.ReactNode,
}

local function VerticalScroll(props: Props)
	local theme = StudioComponents.useTheme()

	local contentRef = React.useRef(nil :: Frame?)
	local scrollRef = React.useRef(nil :: ScrollingFrame?)

	local contentSize = Hooks.useRefProperty(contentRef, "AbsoluteSize", Vector2.zero)
	local canvasPosition = Hooks.useRefProperty(scrollRef, "CanvasPosition", Vector2.zero)

	local scrollInset = props.ScrollInset or UDim.new(0, 0)

	return React.createElement("Frame", {
		BackgroundTransparency = 1,
		AnchorPoint = props.AnchorPoint,
		Position = props.Position,
		Size = props.Size,
	}, {
		Clipped = React.createElement("Frame", {
			BackgroundTransparency = 1,
			Size = UDim2.fromScale(1, 1),
			ClipsDescendants = true,
			ZIndex = -2,
		}, {
			Content = React.createElement("Frame", {
				Position = UDim2.fromOffset(0, -canvasPosition.Y),
				AutomaticSize = Enum.AutomaticSize.Y,
				Size = UDim2.fromScale(1, 0),
				BackgroundTransparency = 1,
				ref = contentRef,
			}, props.children),
		}),

		Gradient = React.createElement("Frame", {
			BorderSizePixel = 0,
			BackgroundColor3 = props.GradientColor,
			AnchorPoint = Vector2.new(0, 1),
			Position = UDim2.fromScale(0, 1),
			Size = UDim2.new(1, 0, 0, props.GradientHeight),
			ZIndex = -1,
		}, {
			Gradient = React.createElement("UIGradient", {
				Transparency = NumberSequence.new(0, 1),
				Rotation = -90,
			}),
		}),

		ScrollingFrame = React.createElement("ScrollingFrame", {
			BorderSizePixel = 0,
			BackgroundTransparency = 1,
			Size = UDim2.new(1 + scrollInset.Scale, scrollInset.Offset, 1, 0),
			CanvasSize = UDim2.fromOffset(0, contentSize.Y),

			ScrollingEnabled = not props.Disabled,
			ScrollBarThickness = props.ScrollBarThickness or 4,
			ScrollingDirection = Enum.ScrollingDirection.Y,
			ScrollBarImageColor3 = theme:GetColor(Enum.StudioStyleGuideColor.DimmedText),
			ScrollBarImageTransparency = props.ScrollBarTransparency or 0,

			TopImage = Icons.scroll["scroll-top"],
			MidImage = Icons.scroll["scroll-middle"],
			BottomImage = Icons.scroll["scroll-bottom"],

			ref = scrollRef,
		}),
	})
end

return VerticalScroll


--!strict

local RunService = game:GetService("RunService") :: RunService

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Trove = require(pluginRoot.Packages.Trove)
local React = require(pluginRoot.Packages.React)

local useConstant = require(script.Parent.Parent.Generic.useConstant)

local TAU = math.pi * 2

local function useShake()
	local trove = useConstant(function()
		return Trove.new()
	end)

	local shake, setShake = React.useState(Vector3.new(0, 0, 1))
	local makeShake = React.useCallback(function(length: number, frequency: Vector2, magnitude: Vector2)
		local startAt = os.clock()
		local fX, fY = frequency.X, frequency.Y
		local mX, mY = magnitude.X, magnitude.Y

		trove:Clean()
		trove:Add(task.spawn(function()
			while true do
				local dt = os.clock() - startAt
				local alpha = math.min(1, dt / length)
				local intensity = (1 - alpha * alpha)

				local offsetX = math.sin(alpha * TAU * fX) * intensity * mX
				local offsetY = math.sin(alpha * TAU * fY) * intensity * mY

				setShake(Vector3.new(offsetX, offsetY, alpha))

				if alpha >= 1 then
					break
				end

				RunService.RenderStepped:Wait()
			end
		end))
	end, {})

	return shake, makeShake
end

return useShake


local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local function useConstant<T>(create: () -> T): T
	local ref = React.useRef(nil :: any)

	if not ref.current then
		ref.current = { value = create() }
	end

	return ref.current.value
end

return useConstant


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local function useRefProperty<T>(ref: { current: Instance? }, property: string, default: T)
	local value, setValue = React.useState(default)

	React.useEffect(function()
		if not ref.current then
			setValue(default)
			return
		end

		setValue((ref.current :: any)[property])
		local connection = ref.current:GetPropertyChangedSignal(property):Connect(function()
			setValue((ref.current :: any)[property])
		end)

		return function()
			connection:Disconnect()
		end
	end, { ref.current, property } :: { any })

	return value
end

return useRefProperty


--!strict

local SelectionService = game:GetService("Selection") :: Selection

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local function useSelection()
	local selection, setSelection = React.useState(SelectionService:Get())

	React.useEffect(function()
		setSelection(SelectionService:Get())
		local connection = SelectionService.SelectionChanged:Connect(function()
			setSelection(SelectionService:Get())
		end)

		return function()
			connection:Disconnect()
		end
	end, {})

	return selection
end

return useSelection


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local CLASS_TO_CONTENT_PROPERTY: { [string]: string } = {
	-- ["Decal"] = "TextureContent",
	["MeshPart"] = "TextureContent",
	-- ["ImageLabel"] = "ImageContent",
	-- ["ImageButton"] = "ImageContent",
}

export type EditableImageInfo = {
	Name: string,
	Instance: Instance,
	EditableImage: EditableImage,
}

local function tryGetEditImage(instance: Instance, contentProperty: string): EditableImage?
	local content = (instance :: any)[contentProperty] :: Content
	local object = content and content.Object

	if object and object:IsA("EditableImage") then
		return object
	end

	return nil
end

local function getEditableImageInfos(instances: { Instance })
	local editableImagesInfo: { EditableImageInfo } = {}
	for _, instance in instances do
		for class, contentProperty in CLASS_TO_CONTENT_PROPERTY do
			if instance:IsA(class) then
				local editImage = tryGetEditImage(instance, contentProperty)
				if editImage then
					table.insert(editableImagesInfo, {
						Name = instance.Name,
						Instance = instance,
						EditableImage = editImage,
					})

					break
				end
			end
		end
	end
	return editableImagesInfo
end

local function useEditableImageInfos(instances: { Instance })
	local editableImageInfos, setEditableImageInfos = React.useState(getEditableImageInfos(instances))

	React.useEffect(function()
		setEditableImageInfos(getEditableImageInfos(instances))

		local connections = {}
		for _, instance in instances do
			for class, contentProperty in CLASS_TO_CONTENT_PROPERTY do
				if instance:IsA(class) then
					table.insert(
						connections,
						instance:GetPropertyChangedSignal(contentProperty):Connect(function()
							setEditableImageInfos(getEditableImageInfos(instances))
						end)
					)

					table.insert(
						connections,
						instance:GetPropertyChangedSignal("Name"):Connect(function()
							setEditableImageInfos(getEditableImageInfos(instances))
						end)
					)
				end
			end
		end

		return function()
			for _, connection in connections do
				connection:Disconnect()
			end
		end
	end, { instances })

	return editableImageInfos
end

return useEditableImageInfos


--!strict

local MarketplaceService = game:GetService("MarketplaceService") :: MarketplaceService

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Semver = require(pluginRoot.Packages.Semver)
local Signal = require(pluginRoot.Packages.Signal)
local React = require(pluginRoot.Packages.React)

local finished = Signal.new()
local resultVersion = "0.0.0"

task.spawn(function()
	local success, result =
		pcall(MarketplaceService.GetProductInfoAsync, MarketplaceService, 87864471469006, Enum.InfoType.Asset)
	if success then
		if Semver.validateVersion(result.Description) then
			resultVersion = result.Description
			finished:Fire()
		end
	end
end)

local function useLatestVersion()
	local latestVersion, setLatestVersion = React.useState(resultVersion)

	React.useEffect(function()
		setLatestVersion(resultVersion)
		local connection = finished:Connect(function()
			setLatestVersion(resultVersion)
		end)

		return function()
			connection:Disconnect()
		end
	end, {})

	return latestVersion
end

return useLatestVersion


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)
local State = require(pluginRoot.Main.State)

local function useState()
	local value, setValue = React.useState(false)
	local stateRef = React.useRef(State.read())

	React.useEffect(function()
		local flipValue = not value
		setValue(flipValue)
		State.onChanged(function()
			flipValue = not flipValue
			setValue(flipValue)
		end)
	end, {})

	return stateRef.current
end

return useState


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)
local State = require(pluginRoot.Main.State)

local function useStatePath<T>(readFromPath: (State.State) -> T)
	local value, setValue = React.useState(readFromPath(State.read()))

	React.useEffect(function()
		setValue(readFromPath(State.read()))
		return State.onChanged(function()
			setValue(readFromPath(State.read()))
		end)
	end, {})

	return value
end

return useStatePath


--!strict

local AssetService = game:GetService("AssetService") :: AssetService

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Signal = require(pluginRoot.Packages.Signal)
local React = require(pluginRoot.Packages.React)

local success = false
local finished = Signal.new()
task.spawn(function()
	local model = Instance.new("Model")
	-- attempt to upload something as Roblox which will error if the api isn't available this includes uploading from a cloud plugin
	-- the request itself will fail but not error if the api is enabled b/c chances are that you're not roblox
	success = pcall(AssetService.CreateAssetAsync, AssetService, model, Enum.AssetType.Model, { CreatorId = 1 })
	model:Destroy()
	finished:Fire()
end)

local function useHasCreateAssetPermission()
	local hasPermission, setHasPermission = React.useState(success)

	React.useEffect(function()
		setHasPermission(success)
		local connection = finished:Connect(function()
			setHasPermission(success)
		end)

		return function()
			connection:Disconnect()
		end
	end, {})

	return hasPermission
end

return useHasCreateAssetPermission


--!strict

local usePlugin = require(script.Parent.usePlugin)

local function useIsLocalPlugin()
	local plugin = usePlugin()
	local pluginName = plugin and plugin.Name

	if pluginName then
		return not not (
			pluginName:find(".rbxm$")
			or pluginName:find(".rbxmx$")
			or pluginName:find(".lua$")
			or pluginName:find(".luau$")
		)
	end

	return false
end

return useIsLocalPlugin


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local userInterfaceRoot = script:FindFirstAncestor("UserInterface")
local IsStoryContext = require(userInterfaceRoot.Stories.Helpers.CreateStory.IsStoryContext)

local function useIsStory()
	return React.useContext(IsStoryContext)
end

return useIsStory


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)

local function usePlugin()
	return StudioComponents.usePlugin() :: Plugin?
end

return usePlugin


--!strict

local HttpService = game:GetService("HttpService")

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

local usePlugin = require(script.Parent.usePlugin)

export type Props = {
	Title: string,
	Icon: string,

	Actions: {
		{
			Text: string,
			Icon: string,
			OnTriggered: () -> (),
		}
	},
}

local function usePluginMenu(props: Props)
	local plugin = assert(usePlugin())
	local uniqueRef = React.useRef(HttpService:GenerateGUID() :: string)
	local unique = uniqueRef.current

	local isOpen = React.useRef(false)
	local menu, setMenu = React.useState(nil :: PluginMenu?)
	React.useEffect(function()
		local pluginMenu = plugin:CreatePluginMenu(unique, props.Title, props.Icon)
		setMenu(pluginMenu)

		local actions = {}
		for i, actionInfo in props.Actions do
			local action = pluginMenu:AddNewAction(`{unique}_{i}`, actionInfo.Text, actionInfo.Icon)
			action.Triggered:Connect(actionInfo.OnTriggered)
			table.insert(actions, action)
		end

		return function()
			for _, action in actions do
				action:Destroy()
			end
			pluginMenu:Destroy()
		end
	end, { props })

	return function()
		if menu and not isOpen.current then
			isOpen.current = true
			menu:ShowAsync()
			isOpen.current = false
		end
	end
end

return usePluginMenu


--!strict

-- Generic
local useConstant = require(script.Generic.useConstant)
local useMouseDrag = require(script.Generic.useMouseDrag)
local useSelection = require(script.Generic.useSelection)
local useRefProperty = require(script.Generic.useRefProperty)
local useBindingState = require(script.Generic.useBindingState)

-- Animation
local useShake = require(script.Animation.useShake)

-- Plugin
local usePlugin = require(script.Plugin.usePlugin)
local useIsStory = require(script.Plugin.useIsStory)
local usePluginMenu = require(script.Plugin.usePluginMenu)
local useIsLocalPlugin = require(script.Plugin.useIsLocalPlugin)
local useHasCreateAssetPermission = require(script.Plugin.useHasCreateAssetPermission)

-- Photobooth specific
local useState = require(script.Photobooth.useState)
local useStatePath = require(script.Photobooth.useStatePath)
local useLatestVersion = require(script.Photobooth.useLatestVersion)
local useEditableImageInfos = require(script.Photobooth.useEditableImageInfos)

export type EditableImageInfo = useEditableImageInfos.EditableImageInfo

-- Exported

return {
	useConstant = useConstant,
	useMouseDrag = useMouseDrag,
	useSelection = useSelection,
	useRefProperty = useRefProperty,
	useBindingState = useBindingState,

	useShake = useShake,

	usePlugin = usePlugin,
	useIsStory = useIsStory,
	usePluginMenu = usePluginMenu,
	useIsLocalPlugin = useIsLocalPlugin,
	useHasCreateAssetPermission = useHasCreateAssetPermission,

	useState = useState,
	useStatePath = useStatePath,
	useLatestVersion = useLatestVersion,
	useEditableImageInfos = useEditableImageInfos,
}


-- This file is automatically @generated by Asphalt.
-- It is not intended for manual editing.
local assets = {
	menu = {
		black = "rbxassetid://73878266326043",
		checkerboard = "rbxassetid://93079146271264",
		none = "rbxassetid://138670824179138",
		white = "rbxassetid://78794694520178",
	},
	misc = {
		checkerboard = "rbxassetid://127279904260661",
	},
	processed = {
		dark = {
			["camera-plugin-button"] = "rbxassetid://109606882325089",
			["focus-plugin-action"] = "rbxassetid://118914127192350",
			["fullscreen-plugin-action"] = "rbxassetid://115506050588424",
			["gallery-plugin-button"] = "rbxassetid://86823072946017",
			["import-plugin-button"] = "rbxassetid://101423894146319",
			["serve-png-button"] = "rbxassetid://86771791070420",
			["ui-plugin-button"] = "rbxassetid://93830801691598",
		},
		light = {
			["camera-plugin-button"] = "rbxassetid://76387769938530",
			["focus-plugin-action"] = "rbxassetid://99011692237493",
			["fullscreen-plugin-action"] = "rbxassetid://109309951860852",
			["gallery-plugin-button"] = "rbxassetid://107438370050453",
			["import-plugin-button"] = "rbxassetid://80623667390118",
			["serve-png-button"] = "rbxassetid://109458359827842",
			["ui-plugin-button"] = "rbxassetid://90379892249929",
		},
	},
	scroll = {
		["scroll-bottom"] = "rbxassetid://125431368815795",
		["scroll-middle"] = "rbxassetid://71196174943615",
		["scroll-top"] = "rbxassetid://88645796043475",
	},
	swatches = {
		black = "rbxassetid://73878266326043",
		blue = "rbxassetid://123403130151217",
		green = "rbxassetid://113114545252619",
		red = "rbxassetid://120852760080312",
		white = "rbxassetid://120365773250610",
	},
}

return assets


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local ReactRoblox = require(pluginRoot.Packages.ReactRoblox)
local React = require(pluginRoot.Packages.React)

local State = require(pluginRoot.Main.State)
State.storybook()

local Constants = require(pluginRoot.Constants)

local uiRoot = script:FindFirstAncestor("UserInterface")
local CroppedViewport = require(uiRoot.Components.CroppedViewport)

local stories = script:FindFirstAncestor("Stories")
local CreateStory = require(stories.Helpers.CreateStory)

local function story()
	local isDisabled, setIsDisabled = React.useState(false)
	local focusLenseSizeRef = React.useRef(Vector2.zero)

	return React.createElement(CroppedViewport, {
		OnMaximize = if Constants.AllowFullscreen
			then function()
				print("Maximize")
			end
			else nil,

		OnCapture = function(rect)
			setIsDisabled(true)
			print(focusLenseSizeRef.current)
			print(table.concat({
				"",
				"Viewport Capture",
				`> Rect: {rect}`,
				`> Size: ({rect.Width}, {rect.Height})`,
			}, "\n"))
			task.wait(1)
			setIsDisabled(false)
			return true
		end,

		InitialSize = UDim2.fromScale(0.5, 0.5),
		FocusLenseSizeRef = focusLenseSizeRef,
		Disabled = isDisabled,
	})
end

return {
	react = React,
	reactRoblox = ReactRoblox,
	story = CreateStory.theme(story),
}


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local ReactRoblox = require(pluginRoot.Packages.ReactRoblox)
local React = require(pluginRoot.Packages.React)

local State = require(pluginRoot.Main.State)
State.storybook()

local Constants = require(pluginRoot.Constants)

local uiRoot = script:FindFirstAncestor("UserInterface")
local FullViewport = require(uiRoot.Components.FullViewport)

local stories = script:FindFirstAncestor("Stories")
local CreateStory = require(stories.Helpers.CreateStory)

local function story()
	local isDisabled, setIsDisabled = React.useState(false)
	local focusLenseSizeRef = React.useRef(Vector2.zero)

	return React.createElement("Frame", {
		AnchorPoint = Vector2.new(0.5, 0.5),
		Position = UDim2.fromScale(0.5, 0.5),
		Size = UDim2.new(1, 0, 1, 0),
		BackgroundTransparency = 1,
	}, {
		FullViewport = React.createElement(FullViewport, {
			OnMinimize = if Constants.AllowFullscreen
				then function()
					print("Minimize")
				end
				else nil,

			OnCapture = function(rect)
				setIsDisabled(true)
				print(focusLenseSizeRef.current)
				print(table.concat({
					"",
					"Viewport Capture",
					`> Rect: {rect}`,
					`> Size: ({rect.Width}, {rect.Height})`,
				}, "\n"))
				task.wait(1)
				setIsDisabled(false)
				return true
			end,

			FocusLenseSizeRef = focusLenseSizeRef,
			Disabled = isDisabled,
		}),
	})
end

return {
	react = React,
	reactRoblox = ReactRoblox,
	story = CreateStory.theme(story),
}


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local ReactRoblox = require(pluginRoot.Packages.ReactRoblox)
local React = require(pluginRoot.Packages.React)

local State = require(pluginRoot.Main.State)
State.storybook()

local uiRoot = script:FindFirstAncestor("UserInterface")
local Gallery = require(uiRoot.Components.Gallery)

local stories = script:FindFirstAncestor("Stories")
local CreateStory = require(stories.Helpers.CreateStory)

local function Widget(props: { Size: UDim2 })
	local theme = StudioComponents.useTheme()
	local backgroundType, setBackgroundType = React.useState("Checkerboard")
	local isExpanded, setIsExpanded = React.useState(true)

	return React.createElement("Frame", {
		AnchorPoint = Vector2.new(0.5, 0.5),
		Position = UDim2.fromScale(0.5, 0.5),
		Size = props.Size,
		BackgroundColor3 = theme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
		BorderColor3 = theme:GetColor(Enum.StudioStyleGuideColor.Border),
	}, {
		Widget = React.createElement(Gallery, {
			IsExpanded = isExpanded,
			SetIsExpanded = setIsExpanded,

			BackgroundType = backgroundType,
			SetBackgroundType = setBackgroundType,

			TryUpload = function(i)
				task.wait(1)
				return i
			end,
			OnUploadComplete = function(completed)
				print(completed)
			end,

			OnImport = function()
				print("Import!")
			end,

			Disabled = false,
		}),
	})
end

local function story()
	local selectedTab, setSelectedTab = React.useState("Small")

	return React.createElement(StudioComponents.TabContainer, {
		SelectedTab = selectedTab,
		OnTabSelected = setSelectedTab,
	}, {
		["Small"] = {
			LayoutOrder = 1,
			Content = React.createElement(Widget, {
				Size = UDim2.fromOffset(500, 350),
			}),
		},
		["Medium"] = {
			LayoutOrder = 2,
			Content = React.createElement(Widget, {
				Size = UDim2.fromScale(0.5, 0.5),
			}),
		},
		["Large"] = {
			LayoutOrder = 3,
			Content = React.createElement(Widget, {
				Size = UDim2.new(1, -30, 1, -30),
			}),
		},
	})
end

return {
	react = React,
	reactRoblox = ReactRoblox,
	story = CreateStory.theme(story),
}


local pluginRoot = script:FindFirstAncestor("PluginRoot")
local React = require(pluginRoot.Packages.React)

return React.createContext(false)


--!strict

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local ReactRoblox = require(pluginRoot.Packages.ReactRoblox)
local React = require(pluginRoot.Packages.React)

local State = require(pluginRoot.Main.State)
State.storybook()

return {
	name = "Photobooth",
	storyRoots = {
		script.Parent.Parent,
	},
	packages = {
		React = React,
		ReactRoblox = ReactRoblox,
	},
}


--!strict

local CollectionService = game:GetService("CollectionService") :: CollectionService

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local ReactRoblox = require(pluginRoot.Packages.ReactRoblox)
local React = require(pluginRoot.Packages.React)

local State = require(pluginRoot.Main.State)
State.storybook()

local Photo = require(pluginRoot.Main.Photo)

local LOADED_TAG = "PhotoboothStoryImage"

return {
	react = React,
	reactRoblox = ReactRoblox,
	story = function()
		return React.createElement(React.Fragment, nil, {
			Layout = React.createElement("UIListLayout", {
				SortOrder = Enum.SortOrder.LayoutOrder,
				FillDirection = Enum.FillDirection.Horizontal,
				VerticalAlignment = Enum.VerticalAlignment.Top,
				HorizontalAlignment = Enum.HorizontalAlignment.Left,
				Padding = UDim.new(0, 8),
			}),

			Load = React.createElement(StudioComponents.MainButton, {
				Size = UDim2.fromOffset(100, 32),
				Text = "Load",
				OnActivated = function()
					local results = Photo.importFiles(game:GetService("StarterGui"))
					for _, meshPart in results do
						CollectionService:AddTag(meshPart, LOADED_TAG)
					end
				end,
			}),
			CleanUp = React.createElement(StudioComponents.Button, {
				Size = UDim2.fromOffset(100, 32),
				Text = "Clean Up",
				OnActivated = function()
					for _, instance in CollectionService:GetTagged(LOADED_TAG) do
						instance:Destroy()
					end
				end,
			}),
		})
	end,
}


--!strict

local CoreGui = game:GetService("CoreGui") :: CoreGui
local StarterGui = game:GetService("StarterGui") :: StarterGui
local HttpService = game:GetService("HttpService") :: HttpService
local SelectionService = game:GetService("Selection") :: Selection
local AssetService = game:GetService("AssetService") :: AssetService
local ScriptEditorService = game:GetService("ScriptEditorService") :: ScriptEditorService

local pluginRoot = script:FindFirstAncestor("PluginRoot")

local StudioComponents = require(pluginRoot.Packages.StudioComponents)
local ReactRoblox = require(pluginRoot.Packages.ReactRoblox)
local React = require(pluginRoot.Packages.React)
local Trove = require(pluginRoot.Packages.Trove)

local Constants = require(pluginRoot.Constants)
local Wally = require(pluginRoot.Wally)

local Camera = require(pluginRoot.Utilities.Camera)
local State = require(pluginRoot.Main.State)
local Photo = require(pluginRoot.Main.Photo)

local Captures = require(pluginRoot.Main.Photo.Captures)
local UIHider = require(pluginRoot.Main.Photo.Tools.UIHider)

local Hooks = require(script.Hooks)
local Icons = require(script.Icons)

local Gallery = require(script.Components.Gallery)
local FullViewport = require(script.Components.FullViewport)
local CroppedViewport = require(script.Components.CroppedViewport)

local UserInterface = {}

-- Private

local function getIcon(iconName: string)
	local theme = settings().Studio.Theme
	return assert(Icons["processed"][theme.Name:lower()][iconName] :: string?)
end

-- Public

function UserInterface.mountCaptureUI(plugin: Plugin, toolbar: PluginToolbar)
	-- stylua: ignore
	local captureButton = toolbar:CreateButton(
		"egomoose_photobooth_capture_ui_button",
		"Capture UI images",
		"", "UI"
	)

	local function onThemeChanged()
		captureButton.Icon = getIcon("ui-plugin-button")
	end

	onThemeChanged()
	settings().Studio.ThemeChanged:Connect(onThemeChanged)

	local function getSelectedGuiObject()
		local selection = SelectionService:Get()
		local singleSelection = if #selection == 1 then selection[1] else nil
		return if singleSelection and singleSelection:IsA("GuiObject") then singleSelection else nil
	end

	local selection: GuiObject?
	local function refreshSelection()
		selection = getSelectedGuiObject()
		captureButton.Enabled = not not selection
	end

	refreshSelection()
	local selectionChangedConnection = SelectionService.SelectionChanged:Connect(function()
		refreshSelection()
	end)

	local clickConnection = captureButton.Click:Connect(function()
		if selection and not State.read().temporary.isCapturing then
			local captureTypeIndex = State.read().temporary.uiCaptureTypeIndex
			local captureType = assert(Captures.UserInterface.Order[captureTypeIndex], "No capture type found")

			local captures = Photo.captureUI({
				subject = selection,
				type = captureType :: Captures.UICaptureTypes,
			})

			local exported = Photo.export(captures, StarterGui)
			SelectionService:Set(exported :: any)
		end
	end)

	plugin.Unloading:Connect(function()
		selectionChangedConnection:Disconnect()
		clickConnection:Disconnect()
	end)
end

function UserInterface.mountCaptureViewport(plugin: Plugin, toolbar: PluginToolbar)
	-- stylua: ignore
	local openButton = toolbar:CreateButton(
		"egomoose_photobooth_viewport_button",
		"Capture viewport images",
		"", "Viewport"
	)

	local focusAction: PluginAction
	local actionTrove = Trove.new()

	local function onThemeChanged()
		openButton.Icon = getIcon("camera-plugin-button")
		actionTrove:Clean()

		focusAction = actionTrove:Add(
			plugin:CreatePluginAction(
				"egomoose_photobooth_focus_action",
				"Photobooth Viewport Focus",
				"Focuses the camera to fit the selected PVInstance to the Photobooth capture viewport.",
				getIcon("focus-plugin-action"),
				true
			)
		)
	end

	onThemeChanged()
	settings().Studio.ThemeChanged:Connect(onThemeChanged)

	local folder = Instance.new("Folder")
	folder.Name = "EgoMoosePhotobooth"
	folder.Archivable = false
	folder.Parent = CoreGui

	local function ViewportApp(): React.ReactNode?
		local isEnabled, setIsEnabled = React.useState(false)

		local isCapturing = Hooks.useStatePath(function(state)
			return state.temporary.isCapturing
		end)

		local isFullscreen = Hooks.useStatePath(function(state)
			return state.temporary.isFullscreen
		end)

		local initialLenseSizeRef = React.useRef(UDim2.fromScale(0.5, 0.5))
		local focusLenseSizeRef = React.useRef(nil :: Vector2?)
		local screenGuiRef = React.useRef(nil :: ScreenGui?)

		local onFullscreenCallbackKey = if isFullscreen then "OnMinimize" else "OnMaximize"

		local updateInitialLenseSizeRef = React.useCallback(function()
			local focusLenseSize = focusLenseSizeRef.current
			if not State.read().temporary.isFullscreen and focusLenseSize then
				initialLenseSizeRef.current = UDim2.fromOffset(focusLenseSize.X, focusLenseSize.Y)
			end
		end, {})

		React.useEffect(function()
			local trove = Trove.new()
			trove:Add(openButton.Click:Connect(function()
				updateInitialLenseSizeRef()
				setIsEnabled(not isEnabled)
			end))

			return function()
				trove:Destroy()
			end
		end, { isEnabled })

		React.useEffect(function()
			local connection = focusAction.Triggered:Connect(function()
				if isEnabled then
					local focusLenseSize = focusLenseSizeRef.current
					if focusLenseSize then
						Camera.focus(workspace.CurrentCamera, SelectionService:Get(), focusLenseSize)
					end
				end
			end)

			return function()
				connection:Disconnect()
			end
		end, { isEnabled })

		openButton:SetActive(isEnabled)

		-- stylua: ignore
		return isEnabled and React.createElement("ScreenGui", {
			Name = "ViewportApp",
			Archivable = false,
			IgnoreGuiInset = true,
			ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
			ref = screenGuiRef,
		}, {
			Content = React.createElement(if isFullscreen then FullViewport else CroppedViewport, {
				[onFullscreenCallbackKey] = Constants.AllowFullscreen and function()
					updateInitialLenseSizeRef()
					State.mutate(function(state)
						state.temporary.isFullscreen = not state.temporary.isFullscreen
					end)
				end,
				OnCapture = function(rect)
					local captureTypeIndex = State.read().temporary.viewportCaptureTypeIndex
					local captureType = assert(Captures.Viewport.Order[captureTypeIndex], "No capture type found")

					local trove = Trove.new()
					if screenGuiRef.current and UIHider.isViewportAppExcluded(rect) then
						trove:Add(UIHider.addExclusion(screenGuiRef.current))
					end

					local captures, err = Photo.captureViewport({
						rect = rect,
						type = captureType :: Captures.ViewportCaptureTypes,
					})

					trove:Destroy()

					local selection = Photo.export(captures, StarterGui)
					SelectionService:Set(selection :: any)
					return not err, err
				end,
				InitialSize = initialLenseSizeRef.current,
				FocusLenseSizeRef = focusLenseSizeRef,
				Disabled = isCapturing,
			})
		})
	end

	local coreGuiRoot = ReactRoblox.createRoot(folder)
	coreGuiRoot:render(React.createElement(StudioComponents.PluginProvider, {
		Plugin = plugin,
	}, {
		App = React.createElement(ViewportApp),
	}))

	plugin.Unloading:Connect(function()
		coreGuiRoot:unmount()
		actionTrove:Destroy()
		folder:Destroy()
	end)
end

function UserInterface.mountGallery(plugin: Plugin, toolbar: PluginToolbar)
	-- stylua: ignore
	local openButton = toolbar:CreateButton(
		"egomoose_photobooth_gallery_button",
		"View photobooth images",
		"", "Gallery"
	)

	local function onThemeChanged()
		openButton.Icon = getIcon("gallery-plugin-button")
	end

	onThemeChanged()
	settings().Studio.ThemeChanged:Connect(onThemeChanged)

	local info = DockWidgetPluginGuiInfo.new(
		Enum.InitialDockState.Float, -- Widget will be initialized in floating panel
		false, -- Widget will be initially enabled
		false, -- Don't override the previous enabled state
		Constants.DockWidgetSize.X, -- Default width of the floating window
		Constants.DockWidgetSize.Y, -- Default height of the floating window
		Constants.DockWidgetSize.X, -- Minimum width of the floating window
		Constants.DockWidgetSize.Y -- Minimum height of the floating window
	)

	local dock = plugin:CreateDockWidgetPluginGuiAsync("PhotoboothGallery", info)
	dock.Name = "PhotoboothGallery"
	dock.Title = "Photobooth"
	dock.ZIndexBehavior = Enum.ZIndexBehavior.Sibling

	local function GalleryApp(): React.ReactNode
		local isEnabled, setIsEnabled = React.useState(dock.Enabled)

		local isCapturing = Hooks.useStatePath(function(state)
			return state.temporary.isCapturing
		end)

		local galleryListExpanded = Hooks.useStatePath(function(state)
			return state.saved.galleryListExpanded
		end)

		local backgroundType, setBackgroundType = React.useState("Checkerboard")

		React.useEffect(function()
			local buttonConnection = openButton.Click:Connect(function()
				setIsEnabled(not dock.Enabled)
			end)

			dock:BindToClose(function()
				setIsEnabled(false)
			end)

			return function()
				dock:BindToClose(nil)
				buttonConnection:Disconnect()
			end
		end, {})

		dock.Enabled = isEnabled
		openButton:SetActive(isEnabled)

		return isEnabled
			and React.createElement(Gallery, {
				IsExpanded = galleryListExpanded,
				SetIsExpanded = function()
					State.mutate(function(state)
						state.saved.galleryListExpanded = not state.saved.galleryListExpanded
					end)
				end,

				BackgroundType = backgroundType,
				SetBackgroundType = setBackgroundType,

				TryUpload = function(_i, name, editImage)
					local result: Enum.CreateAssetResult
					local value: number | string

					result, value = AssetService:CreateAssetAsync(editImage, Enum.AssetType.Image, {
						Name = name,
						Description = table.concat({
							"Uploaded with the Photobooth plugin.",
							"",
							HttpService:JSONEncode({
								Dimensions = { editImage.Size.X, editImage.Size.Y },
								Version = Wally.package.version,
							}),
						}, "\n"),
					})

					if result == Enum.CreateAssetResult.Success then
						return value :: number
					end

					warn(value :: string)
					return nil
				end,
				OnUploadComplete = function(completed)
					local module = Instance.new("ModuleScript")
					module.Name = `{DateTime.now():ToIsoDate()} - Photobooth Upload`
					ScriptEditorService:UpdateSourceAsync(module, function()
						local output = "return {\n"
						for _, entry in completed do
							output = output .. `\t["{entry.Name}"] = "rbxassetid://{entry.AssetId}",\n`
						end
						return output .. "}"
					end)
					module.Parent = StarterGui

					ScriptEditorService:OpenScriptDocumentAsync(module)
					SelectionService:Set({ module })
				end,

				OnImport = function()
					local result = Photo.importFiles(StarterGui)
					SelectionService:Set(result :: { any })
				end,

				Disabled = isCapturing,
			})
	end

	local dockRoot = ReactRoblox.createRoot(dock)
	dockRoot:render(React.createElement(StudioComponents.PluginProvider, {
		Plugin = plugin,
	}, {
		App = React.createElement(GalleryApp),
	}))

	plugin.Unloading:Connect(function()
		dockRoot:unmount()
	end)
end

return UserInterface


--!strict

local Camera = {}

type Distortion = {
	anchor: Vector2,
	position: Vector2,
	size: Vector2,
	zoom: number,
	nearZ: number?,
	rotation: number,
}

function Camera.distort(camera: Camera, distortion: Distortion)
	local viewportSize = camera.ViewportSize
	local aspectRatio = viewportSize.X / viewportSize.Y

	local hFactor = math.tan(math.rad(camera.FieldOfView) / 2)
	local wFactor = hFactor * aspectRatio

	local center = distortion.position - distortion.size * distortion.anchor + distortion.size / 2

	local w, h = distortion.size.X, distortion.size.Y
	local dx = 2 * wFactor * (0.5 - center.X)
	local dy = -2 * hFactor * (0.5 - center.Y)
	local dz = distortion.zoom

	local m = math.max(math.abs(w), math.abs(h), math.abs(dx), math.abs(dy), math.abs(dz))

	local nearZ = CFrame.identity
	if distortion.nearZ then
		local nz = ((camera.NearPlaneZ + distortion.nearZ) / camera.NearPlaneZ) * (dz / m)
		nearZ = CFrame.new(0, 0, 0, 1 / nz, 0, 0, 0, 1 / nz, 0, 0, 0, 1 / nz)
	end

	-- stylua: ignore
	return CFrame.fromEulerAnglesXYZ(0, 0, distortion.rotation) * CFrame.new(0, 0, 0,
		w / m, 0, 0,
		0, h / m, 0,
		dx / m, dy / m, dz / m
	) * nearZ
end

function Camera.focus(camera: Camera, selection: { Instance }, viewportSize: Vector2)
	local bounds = {}
	local centerSum = Vector3.zero
	for _, selected in selection do
		if selected:IsA("PVInstance") then
			local boundCF, boundSize
			if selected:IsA("BasePart") then
				boundCF = selected.CFrame
				boundSize = selected.Size
			elseif selected:IsA("Model") then
				boundCF, boundSize = selected:GetBoundingBox()
			end

			table.insert(bounds, {
				CFrame = boundCF,
				Size = boundSize,
			})

			centerSum = centerSum + boundCF.Position
		end
	end

	if #bounds > 0 then
		local center = centerSum / #bounds

		local maxRadius = 0
		for _, bound in bounds do
			local radius = (bound.Size / 2).Magnitude
			local distance = (bound.CFrame.Position - center).Magnitude
			maxRadius = math.max(maxRadius, distance + radius)
		end

		local aspect = viewportSize.X / viewportSize.Y
		local scaleY = viewportSize.Y / camera.ViewportSize.Y
		local scaledFov2 = math.atan(math.tan(math.rad(camera.FieldOfView / 2)) * scaleY)
		local sinFov2 = math.sin(scaledFov2 * math.min(1, aspect))

		local focusCF = camera.CFrame.Rotation + center

		camera.Focus = focusCF
		camera.CFrame = focusCF * CFrame.new(0, 0, maxRadius / sinFov2)
	end
end

return Camera


--!strict

local Timeout = require(script.Parent.Timeout)
local Image = require(script.Parent.Image)

local IntegralImageStatic = {}

local IntegralImageClass = {}
IntegralImageClass.__index = IntegralImageClass
IntegralImageClass.ClassName = "IntegralImage"

export type IntegralImage = typeof(setmetatable(
	{} :: {
		buffer: buffer,
		width: number,
		height: number,
	},
	IntegralImageClass
))

function IntegralImageStatic.fromBuffer(imgBuffer: buffer, imgSize: Vector2, processImgRead: (number) -> number)
	local self = setmetatable({}, IntegralImageClass) :: IntegralImage

	local width, height = imgSize.X, imgSize.Y
	local integralBuffer = buffer.create(width * height * 4)

	local function read(x: number, y: number)
		local i = ((x - 1) + (y - 1) * width) * 4
		return buffer.readu32(integralBuffer, i)
	end

	local function write(x: number, y: number, value: number)
		local i = ((x - 1) + (y - 1) * width) * 4
		buffer.writeu32(integralBuffer, i, value)
	end

	local function readImage(x: number, y: number)
		local i = ((x - 1) + (y - 1) * width) * 4
		return processImgRead(buffer.readu32(imgBuffer, i))
	end

	write(1, 1, readImage(1, 1))

	for x = 2, width do
		write(x, 1, readImage(x, 1) + read(x - 1, 1))
		Timeout.tryYield()
	end

	for y = 2, height do
		write(1, y, readImage(1, y) + read(1, y - 1))
		Timeout.tryYield()
	end

	for x = 2, width do
		for y = 2, height do
			write(x, y, readImage(x, y) + read(x, y - 1) + read(x - 1, y) - read(x - 1, y - 1))
			Timeout.tryYield()
		end
	end

	self.buffer = integralBuffer
	self.width = width
	self.height = height

	return self
end

function IntegralImageStatic.fromImage(img: Image.Image, processImgRead: (number) -> number)
	return IntegralImageStatic.fromBuffer(img.buffer, img.size, processImgRead)
end

function IntegralImageClass.read(self: IntegralImage, x: number, y: number)
	local cx = math.min(x, self.width)
	local cy = math.min(y, self.height)
	if cx > 0 and cy > 0 then
		local i = ((cx - 1) + (cy - 1) * self.width) * 4
		return buffer.readu32(self.buffer, i)
	end
	return 0
end

function IntegralImageClass.readRadius(self: IntegralImage, x: number, y: number, radius: number)
	return self:read(x + radius, y + radius)
		- self:read(x - radius - 1, y + radius)
		- self:read(x + radius, y - radius - 1)
		+ self:read(x - radius - 1, y - radius - 1)
end

return IntegralImageStatic


--!strict

local RootModule = assert(script:FindFirstAncestorWhichIsA("ModuleScript") :: ModuleScript?)
local WorkerCallback = require(RootModule:FindFirstChild("Worker")) :: any

return WorkerCallback(script)


--!strict

return function(script: LuaSourceContainer)
	local actor = assert(script:GetActor(), "No actor found.")

	local moduleRef = assert(actor:FindFirstAncestorWhichIsA("ObjectValue"), "No module found.")
	local module = assert(moduleRef.Value, "No value in the ObjectValue.")
	assert(module:IsA("ModuleScript"), "ModuleRef Value is not a ModuleScript.")

	local binding = assert(moduleRef:FindFirstChild("Binding"), "Not binding instance found.")
	assert(binding:IsA("BindableEvent"), "Not a BindableEvent.")

	local workerAction = require(module) :: any
	actor:BindToMessageParallel("Work", function(k, ...)
		binding:Fire(k, workerAction(...))
	end)

	actor:SetAttribute("IsReady", true)
end


--!strict

local RunService = game:GetService("RunService") :: RunService

local pluginRoot = script:FindFirstAncestor("PluginRoot")
local Future = require(pluginRoot.Packages.Future)
local Trove = require(pluginRoot.Packages.Trove)

local MODULE_REF_FOLDER = Instance.new("Folder")
MODULE_REF_FOLDER.Name = "ModuleRefs"
MODULE_REF_FOLDER.Parent = script

-- Class

local ParallelStatic = {}

local ParallelClass = {}
ParallelClass.__index = ParallelClass
ParallelClass.ClassName = "Parallel"

export type Required<T..., U...> = {
	script: ModuleScript,
	typecast: (T...) -> U...,
}

export type Parallel<T..., U...> = typeof(setmetatable(
	{} :: {
		trove: Trove.Trove,

		ticket: number,
		isWorking: boolean,
		required: Required<T..., U...>,

		binding: BindableEvent,
		isReadyFuture: Future.Future<()>,

		actorEntries: {
			{
				actor: Actor,
				scheduled: { () -> T... },
			}
		},
	},
	ParallelClass
))

function ParallelStatic.new<T..., U...>(n: number, required: Required<T..., U...>)
	local self = setmetatable({}, ParallelClass) :: Parallel<T..., U...>

	self.trove = Trove.new()

	self.ticket = 0
	self.actorEntries = {}
	self.required = required

	self.isWorking = false
	self.isReadyFuture = Future.new()

	local moduleRef = Instance.new("ObjectValue")
	moduleRef.Name = `Parallel_{required.script.Name}`
	moduleRef.Value = required.script
	moduleRef.Archivable = false
	moduleRef.Parent = MODULE_REF_FOLDER
	self.trove:Add(moduleRef)

	self.binding = Instance.new("BindableEvent")
	self.binding.Name = "Binding"
	self.binding.Parent = moduleRef
	self.trove:Add(self.binding)

	local template = script.ClientWorker
	if script:FindFirstAncestorWhichIsA("Plugin") then
		template = script.PluginWorker
	elseif RunService:IsServer() then
		template = script.ServerWorker
	end

	for i = 1, n do
		local actor = Instance.new("Actor")
		actor.Name = tostring(i)
		actor.Parent = moduleRef

		local worker = template:Clone()
		worker.Enabled = true
		worker.Parent = actor

		self.actorEntries[i] = {
			actor = actor,
			scheduled = {},
		}
	end

	task.spawn(function()
		for _, actorEntry in self.actorEntries do
			while not actorEntry.actor:GetAttribute("IsReady") do
				actorEntry.actor:GetAttributeChangedSignal("IsReady"):Wait()
			end
		end

		self.isReadyFuture:complete()
	end)

	return self
end

-- Public

function ParallelClass.WaitForReady<T..., U...>(self: Parallel<T..., U...>)
	self.isReadyFuture:expect()
end

function ParallelClass.GetActorCount<T..., U...>(self: Parallel<T..., U...>)
	assert(self.isReadyFuture:isCompleted(), "Not ready!")
	return #self.actorEntries
end

function ParallelClass.Schedule<T..., U...>(self: Parallel<T..., U...>, ...: T...)
	assert(self.isReadyFuture:isCompleted(), "Not ready!")

	self.ticket = self.ticket + 1
	local myTicket = self.ticket

	local actorIndex = ((myTicket - 1) % #self.actorEntries) + 1
	local actorEntry = self.actorEntries[actorIndex]

	table.insert(actorEntry.scheduled, Future.packAsCallback(...))
end

function ParallelClass.Clear<T..., U...>(self: Parallel<T..., U...>)
	assert(self.isReadyFuture:isCompleted(), "Not ready!")

	self.ticket = 0
	for _, actorEntry in self.actorEntries do
		table.clear(actorEntry.scheduled)
	end
end

function ParallelClass.Work<T..., U...>(self: Parallel<T..., U...>)
	assert(self.isReadyFuture:isCompleted(), "Not ready!")
	assert(not self.isWorking, "Already working!")
	self.isWorking = true

	local futures = {}
	local connection = self.binding.Event:Connect(function(k: number, ...)
		futures[k]:complete(Future.packAsCallback(...))
	end)

	for i, actorEntry in self.actorEntries do
		for j, unpackArgs in actorEntry.scheduled do
			local k = ((j - 1) * #self.actorEntries) + i
			futures[k] = Future.new()
			actorEntry.actor:SendMessage("Work", k, unpackArgs())
		end
	end

	self:Clear()

	local results = {}
	for i, future in futures do
		local castedFuture = (future :: any) :: Future.Future<() -> U...>
		results[i] = castedFuture:expect()
	end

	connection:Disconnect()

	self.isWorking = false
	return results
end

function ParallelClass.Destroy<T..., U...>(self: Parallel<T..., U...>)
	self.trove:Destroy()
	self.actorEntries = {}
end

--

return ParallelStatic


--!strict

local Players = game:GetService("Players") :: Players
local RunService = game:GetService("RunService") :: RunService

local StudioMode = {}

function StudioMode.isTeamCreate()
	return not not (
		RunService:IsStudio()
		and RunService:IsEdit()
		and Players.LocalPlayer
		and not Players.LocalPlayer:FindFirstChildWhichIsA("PlayerGui")
	)
end

return StudioMode