--!strict

--> Modules -------------------------------------------------------------------------------------------
local Components = script.Parent
local Container = require(Components.Container)
local TextInput = require(Components.TextInput)

local CoreComponents = Components.Core
local TextLabel = require(CoreComponents.TextLabel)
local Squircle = require(CoreComponents.Squircle)

local Modules = script.Parent.Parent.Modules
local Component = require(Modules.Component)
local TrackProperty = require(Modules.TrackProperty)
local ForceToState = require(Modules.ForceToState)
local TableUtils = require(Modules.TableUtils)

local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Types ---------------------------------------------------------------------------------------------
type OnItemUpdateFn = (itemName: string, itemIdx: number, newValue: unknown, oldValue: unknown) -> ()

type MapValueProps =
  Fusion.UsedAs<string>--[[ | Fusion.UsedAs<boolean> | Fusion.UsedAs<EnumItem>]] |
  {
    Text: Fusion.UsedAs<string>?,
    Placeholder: Fusion.UsedAs<string>?,
    Validation: TextInput.ValidationProps?
  }

type MapProps = {
  Header: Container.HeaderProps?,
  Sections: {{ Key: string, Value: MapValueProps }},

  ["@OnItemUpdate"]: OnItemUpdateFn?
}
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local Children, OnEvent, peek = Fusion.Children, Fusion.OnEvent, Fusion.peek

local TableTake = TableUtils.Take

local IMAGE_KEY_BOTTOM, IMAGE_KEY_TOP, IMAGE_KEY_SOLO =
  "rbxassetid://130737618977026", "rbxassetid://90426745217576", "rbxassetid://132086360563600"
local IMAGE_KEY_BOTTOM_STROKE, IMAGE_KEY_TOP_STROKE, IMAGE_KEY_SOLO_STROKE =
  "rbxassetid://95022398903594", "rbxassetid://94720840277798", "rbxassetid://136153743765334"

local IMAGE_VALUE_BOTTOM, IMAGE_VALUE_TOP, IMAGE_VALUE_SOLO =
  "rbxassetid://133800092992868", "rbxassetid://123833703714055", "rbxassetid://95895141756049"
local IMAGE_VALUE_BOTTOM_STROKE, IMAGE_VALUE_TOP_STROKE, IMAGE_VALUE_SOLO_STROKE =
  "rbxassetid://123891250075098", "rbxassetid://125766254710634", "rbxassetid://94176076719291"
local IMAGE_VALUE_BOTTOM_FOCUS, IMAGE_VALUE_TOP_FOCUS, IMAGE_VALUE_SOLO_FOCUS =
  "rbxassetid://80207168749216", "rbxassetid://108901529727203", "rbxassetid://108367553025401"

local IMAGE_MID_FOCUS = "rbxassetid://113349926010177"
local IMAGE_MID, IMAGE_MID_STROKE = "rbxassetid://71155288687975", "rbxassetid://112998806065140"
-------------------------------------------------------------------------------------------------------

--> Functions -----------------------------------------------------------------------------------------
local function MapKey(
  scope: Component.Scope, key: string, isFocusState: Fusion.Value<boolean>,
  computeSectionsLength: Fusion.Computed<number>, computeAdjustedIdx: Fusion.Computed<number>, headerDisabled: Fusion.UsedAs<boolean>?
)
  local image = scope:Computed(function(use)
    local usedAdjIdx, usedItemsLength = use(computeAdjustedIdx), use(computeSectionsLength)
    return
      if usedItemsLength == 1 then IMAGE_KEY_SOLO
      elseif usedAdjIdx == 1 then IMAGE_KEY_TOP
      elseif usedAdjIdx == usedItemsLength then IMAGE_KEY_BOTTOM
      else IMAGE_MID
  end)

  local strokeImage = scope:Computed(function(use)
    local usedAdjIdx, usedItemsLength = use(computeAdjustedIdx), use(computeSectionsLength)
    return
      if usedItemsLength == 1 then IMAGE_KEY_SOLO_STROKE
      elseif usedAdjIdx == 1 then IMAGE_KEY_TOP_STROKE
      elseif usedAdjIdx == usedItemsLength then IMAGE_KEY_BOTTOM_STROKE
      else IMAGE_MID_STROKE
  end)

  return scope:New "Frame" {
    Size = UDim2.fromScale(1, 1),
    BackgroundTransparency = 1,

    [Children] = {
      Squircle(scope, {
        Size = UDim2.new(1,2, 0,25),
        Position = UDim2.fromScale(.5, .5),
        AnchorPoint = Vector2.new(.5, .5),
        BackgroundColor3 = scope:GetThemeItem("Background/Secondary"),
        ClipsDescendants = true,
        Image = image,

        [Children] = {
          Squircle(scope, {
            Size = UDim2.fromScale(1, 1),
            BackgroundColor3 = scope:GetThemeItem("Background/Stroke"),
            Image = strokeImage,
          }),

          scope:New "Frame" {
            Size = UDim2.fromScale(1, 1),
            BackgroundTransparency = 1,

            [Children] = {
              TextLabel(scope, {
                Size = UDim2.fromScale(1, 1),
                Text = key,

                [Children] = {
                  scope:New "UIPadding" { PaddingLeft = UDim.new(0, 7), PaddingRight = UDim.new(0, 8) }
                }
              })
            }
          }
        }
      }),

      scope:New "UIFlexItem" { FlexMode = Enum.UIFlexMode.Fill },
    }
  }
end

local function MapValue(
  scope: Component.Scope, key: string, value: MapValueProps, isHoverState: Fusion.Value<boolean>, isFocusState: Fusion.Value<boolean>,
  computeSectionsLength: Fusion.Computed<number>, idx: number, computeAdjustedIdx: Fusion.Computed<number>, headerDisabled: Fusion.UsedAs<boolean>?,
  onItemUpdateFn: OnItemUpdateFn?
)
  local valueIsPropsArray = type(value) == "table" and not (value :: any).kind
  local valueText: Fusion.Value<string> = if valueIsPropsArray then (value :: any).Text else value
  local valuePlaceholder = valueIsPropsArray and (value :: any).Placeholder or nil
  local valueValidation = valueIsPropsArray and (value :: any).Validation or nil

  local image = scope:Computed(function(use)
    local usedAdjIdx, usedItemsLength = use(computeAdjustedIdx), use(computeSectionsLength)
    return
      if usedItemsLength == 1 then IMAGE_VALUE_SOLO
      elseif usedAdjIdx == 1 then IMAGE_VALUE_TOP
      elseif usedAdjIdx == usedItemsLength then IMAGE_VALUE_BOTTOM
      else IMAGE_MID
  end)

  local strokeImage = scope:Computed(function(use)
    local usedAdjIdx, usedItemsLength = use(computeAdjustedIdx), use(computeSectionsLength)
    return
      if usedItemsLength == 1 then IMAGE_VALUE_SOLO_STROKE
      elseif usedAdjIdx == 1 then IMAGE_VALUE_TOP_STROKE
      elseif usedAdjIdx == usedItemsLength then IMAGE_VALUE_BOTTOM_STROKE
      else IMAGE_MID_STROKE
  end)

  local focusImage = scope:Computed(function(use)
    local usedAdjIdx, usedItemsLength = use(computeAdjustedIdx), use(computeSectionsLength)
    return
      if usedItemsLength == 1 then IMAGE_VALUE_SOLO_FOCUS
      elseif usedAdjIdx == 1 then IMAGE_VALUE_TOP_FOCUS
      elseif usedAdjIdx == usedItemsLength then IMAGE_VALUE_BOTTOM_FOCUS
      else IMAGE_MID_FOCUS
  end)

  local lastValue = peek(value)

  return scope:New "Frame" {
    Size = UDim2.fromScale(1, 1),
    BackgroundTransparency = 1,

    [Children] = {
      Squircle(scope, {
        Size = UDim2.new(1,2, 0,25),
        Position = UDim2.fromScale(.5, .5),
        AnchorPoint = Vector2.new(.5, .5),
        BackgroundColor3 = scope:GetThemeItem("Background/Secondary"),
        Image = image,

        [Children] = {
          scope:New "Frame" {
            Size = UDim2.fromScale(1, 1),
            BackgroundTransparency = 1,

            [Children] = {
              (TextInput(scope, {
                Size = UDim2.fromScale(1, 1),
                Value = valueText,
                BackgroundTransparency = 0,
                IsHover = isHoverState,
                IsFocus = isFocusState,
                StrokeImage = strokeImage,
                FocusImage = focusImage,
                Placeholder = valuePlaceholder,
                Validation = valueValidation,

                [OnEvent "FocusLost"] = onItemUpdateFn and function()
                  local peekedValue = peek(value)
                  if lastValue == peekedValue then return end
                  onItemUpdateFn(key, idx, peekedValue, lastValue)

                  lastValue = peekedValue
                end or nil
              }))
            }
          }
        }
      }),

      scope:New "UIFlexItem" { FlexMode = Enum.UIFlexMode.Fill },
    }
  }
end

local function MapPair(
  scope: Component.Scope, key: string, value: MapValueProps, computeSectionsLength: Fusion.Computed<number>,
  idx: number, headerDisabled: Fusion.UsedAs<boolean>?, onItemUpdateFn: OnItemUpdateFn?
)
  local isHoverState, isFocusState = scope:Value(false), scope:Value(false)
  local computeAdjustedIdx = scope:Computed(function(use) return use(headerDisabled) and idx or idx + 1 end)

  return scope:New "Frame" {
    ZIndex = scope:Computed(function(use) return if use(isFocusState) then 999 elseif use(isHoverState) then 998 else 0 end),
    Size = UDim2.new(1,0, 0,23),
    BackgroundTransparency = 1,
    Name = "Map:Pair",

    [Children] = {
      MapKey(scope, key, isFocusState, computeSectionsLength, computeAdjustedIdx, headerDisabled),
      MapValue(scope, key, value, isHoverState, isFocusState, computeSectionsLength, idx, computeAdjustedIdx, headerDisabled, onItemUpdateFn),

      scope:New "UIListLayout" {
        FillDirection = Enum.FillDirection.Horizontal,
        SortOrder = Enum.SortOrder.LayoutOrder,
        VerticalAlignment = Enum.VerticalAlignment.Center,
        Padding = UDim.new(0, 1)
      }
    }
  }
end
-------------------------------------------------------------------------------------------------------

return Component(function(scope, props: MapProps)
  local header: Container.HeaderProps?, sections = TableTake(props, "Header"), TableTake(props, "Sections", {})
  local onItemUpdateFn: OnItemUpdateFn? = TableTake(props, "@OnItemUpdate")

  local headerDisabled = if header then header.Disabled else true

  local computeSectionsLength = scope:Computed(function(use)
    return use(headerDisabled) and #use(sections) or #use(sections) + 1
  end)

  local containerRef: Fusion.Value<Instance> = scope:Value(nil :: any)
  local computeContainerAbsSize = TrackProperty(scope, containerRef, "AbsoluteSize")

  local computeIsOpen = scope:Computed(function(use)
    local usedContainerAbsSize: Vector2 = use(computeContainerAbsSize)
    if not usedContainerAbsSize then return true end
    local containerAbsHeight = usedContainerAbsSize.Y

    local usedItemsLength = use(computeSectionsLength)
    return containerAbsHeight == (25 * usedItemsLength) - (usedItemsLength - 1)
  end)

  -- We need to force all of the `Value` properties of each section to be state objects.
  for _,data in sections do
    local dataValue = data.Value
    if type(dataValue) == "table" and not dataValue.kind then
      dataValue.Text = ForceToState(scope, dataValue.Text)
    else
      data.Value = ForceToState(scope, dataValue)
    end
  end

  return containerRef:set(Container(scope, {
    Header = header,
    BackgroundColor3 = scope:GetThemeItem("Background/Stroke"),
    ChildrenZIndex = 1000,
    ClipsDescendants = scope:Computed(function(use) return if use(computeIsOpen) then false else true end),
    OccluderZIndex = scope:Computed(function(use) return if use(computeIsOpen) then 999 else 1001 end),

    [Children] = {
      scope:ForPairs(sections, function(use, scope, idx, data)
        return idx, MapPair(scope, data.Key, data.Value, computeSectionsLength, idx, headerDisabled, onItemUpdateFn)
      end) :: Fusion.Child,

      scope:New "UIListLayout" {
        FillDirection = Enum.FillDirection.Vertical,
        Padding = UDim.new(0, 1)
      },

      scope:New "UIPadding" {
        PaddingLeft = UDim.new(0, 1), PaddingRight = UDim.new(0, 1),
        PaddingTop = scope:Computed(function(use) return UDim.new(0, use(headerDisabled) and 1 or 0) end), PaddingBottom = UDim.new(0, 1)
      }
    }
  }, props :: any))
end)


--!strict

--> Modules -------------------------------------------------------------------------------------------
local Components = script.Parent
local CoreComponents = Components.Core
local Squircle = require(CoreComponents.Squircle)

local Modules = script.Parent.Parent.Modules
local Component = require(Modules.Component)
local ComputeTransforms = require(Modules.ComputeTransforms)
local ForceToState = require(Modules.ForceToState)
local TableUtils = require(Modules.TableUtils)
local CombineProps = require(Modules.CombineProps)

local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Types ---------------------------------------------------------------------------------------------
type NavigationProps = {
  Sections: { Component.DynamicSection },
  ActiveSection: Fusion.UsedAs<number>?,
  DefaultActiveSection: Fusion.UsedAs<number?>?,
  FillDirection: Fusion.UsedAs<Enum.FillDirection>?
}
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local ComputeTweenInfo = ComputeTransforms.TweenInfo

local TableTake = TableUtils.Take

local Children, OnEvent, peek = Fusion.Children, Fusion.OnEvent, Fusion.peek
-------------------------------------------------------------------------------------------------------

--> Functions -----------------------------------------------------------------------------------------
local function HandleNavigationSectionContentSize(
  scope: Component.Scope, contentSizeState: Fusion.Value<UDim2>,
  contentAutoSizeState: Fusion.Value<Enum.AutomaticSize>, content: Fusion.UsedAs<GuiObject>
)
  local peekedContent = peek(content)
  if peekedContent then
    contentSizeState:set(peekedContent.Size)
    contentAutoSizeState:set(peekedContent.AutomaticSize)
    peekedContent.Size = UDim2.fromScale(1, 1)
  end
  peekedContent = nil :: any

  if typeof(content) == "table" and content.kind == "Value" then
    scope:Observer(content):onChange(function()
      local peekedContent = peek(content)
      contentSizeState:set(peekedContent.Size)
      contentAutoSizeState:set(peekedContent.AutomaticSize)
      peekedContent.Size = UDim2.fromScale(1, 1)
    end)
  end
end

local function NavigationSection(
  scope: Component.Scope, idx: number, activeSectionState: Fusion.Value<number?>, defaultActiveSection: Fusion.UsedAs<number?>,
  hoveredSectionState: Fusion.Value<number?>, isHoverState: Fusion.Value<boolean>, isFirstHoverState: Fusion.Value<boolean>, content: string
)
  local computeIsActive = scope:Computed(function(use): boolean? return use(activeSectionState) == idx end)

  local computeTransparency = scope:Tween(
    scope:Computed(function(use) return use(computeIsActive) and .78 or 1 end),
    TweenInfo.new(.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
  )

  if typeof(peek(content)) == "string" then
    content = scope:New "TextLabel" {
      Size = UDim2.new(0,0, 0,25),
      AutomaticSize = Enum.AutomaticSize.X,
      FontFace = Font.fromId(16658221428, Enum.FontWeight.Medium),
      Text = content,
      TextSize = 14,
      TextColor3 = scope:GetThemeItem("Text/Title"),
      BackgroundTransparency = 1,

      [Children] = {
        scope:New "UIPadding" {
          PaddingLeft = UDim.new(0, 7), PaddingRight = UDim.new(0, 7)
        }
      }
    } :: any
  end

  -- Wrapping the content element with another UI element may mess up its Size so we need to propagate the size and AutomaticSize properties
  -- to the wrapper element and then set the size of the content element to `UDim2.fromScale(1,1)` so that it fills the wrapper element.
  local contentSizeState, contentAutoSizeState = scope:Value(UDim2.new()), scope:Value(Enum.AutomaticSize.None)
  HandleNavigationSectionContentSize(scope, contentSizeState, contentAutoSizeState, (content :: any) :: Fusion.UsedAs<GuiObject>)

  return Squircle (scope, {
    As = "ImageButton" :: "ImageButton",
    Size = contentSizeState,
    AutomaticSize = contentAutoSizeState,
    BackgroundColor3 = scope:GetThemeItem("Text/Title"),
    BackgroundTransparency = computeTransparency,
    Name = "Navigation:Section",

    [OnEvent "MouseButton1Click"] = function()
      local peekedActiveSection = peek(activeSectionState)

      -- Deselecting.
      if peekedActiveSection == idx then
        local peekedDefaultActiveSection = peek(defaultActiveSection)
        if peekedDefaultActiveSection and peekedActiveSection == peekedDefaultActiveSection and idx == peekedDefaultActiveSection then return end
        activeSectionState:set(peekedDefaultActiveSection or nil)
        isHoverState:set(true)
        isFirstHoverState:set(false)
        hoveredSectionState:set(idx)

      -- Selecting.
      else
        activeSectionState:set(idx)
        isHoverState:set(false)
        isFirstHoverState:set(true)
      end

    end :: any,

    [OnEvent "MouseEnter"] = function()
      if peek(computeIsActive) then
        isHoverState:set(false)
        isFirstHoverState:set(true)
      else
        isHoverState:set(true)
        hoveredSectionState:set(idx)
        isFirstHoverState:set(false)
      end
    end :: any,

    [Children] = {
      Squircle (scope, {
        Size = UDim2.fromScale(1, 1),
        Image = "rbxassetid://108824901287727",
        BackgroundTransparency = computeTransparency,
        Name = "Navigation:Section:Highlight"
      }),

      content :: any
    }
  })
end
-------------------------------------------------------------------------------------------------------

return Component(function(scope, props: NavigationProps)
  local sections, defaultActiveSection: Fusion.UsedAs<number?>, fillDirection: Fusion.UsedAs<Enum.FillDirection> =
    TableTake(props, "Sections"), TableTake(props, "DefaultActiveSection"), TableTake(props, "FillDirection", Enum.FillDirection.Horizontal)
  local activeSectionState: Fusion.Value<number?> = ForceToState(scope, TableTake(props, "ActiveSection", peek(defaultActiveSection) or false)) :: any

  local navigationRef: Fusion.Value<Instance> = scope:Value(nil :: any)

  local hoveredSectionState: Fusion.Value<number?> = scope:Value(nil)
  local isFirstHoverState: Fusion.Value<boolean> = scope:Value(true)
  local isHoverState = scope:Value(false)

  local sectionInstances = scope:ForPairs(sections, function(use, scope, idx, content)
    return idx, NavigationSection(scope, idx, activeSectionState, defaultActiveSection, hoveredSectionState, isHoverState, isFirstHoverState, content)
  end)

  local tweenSpeed = scope:Computed(function(use) return (use(isFirstHoverState) and 0) or (use(isHoverState) and .2 or 0) end)
  local computeTransformsTweenInfo = ComputeTweenInfo(scope, tweenSpeed, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)

  local computeHoverSize = scope:Tween(scope:Computed(function(use): UDim2
    local usedHoveredSectionIdx = use(hoveredSectionState)
    if not usedHoveredSectionIdx then return UDim2.new() end

    local hoverSizeAbs = (use(sectionInstances)[usedHoveredSectionIdx] :: GuiBase2d).AbsoluteSize
    return UDim2.fromOffset(hoverSizeAbs.X, hoverSizeAbs.Y)
  end), computeTransformsTweenInfo)

  local computeHoverPos = scope:Tween(scope:Computed(function(use): UDim2
    local usedHoveredSectionIdx = use(hoveredSectionState)
    if not usedHoveredSectionIdx then return UDim2.new() end

    local usedNavigationRef = (use(navigationRef) :: any) :: Frame?
    if not usedNavigationRef then return UDim2.new() end
    local usedNavigationRefAbsPos = usedNavigationRef.AbsolutePosition

    local hoverPosAbs = (use(sectionInstances)[usedHoveredSectionIdx] :: GuiBase2d).AbsolutePosition
    return UDim2.fromOffset(hoverPosAbs.X - usedNavigationRefAbsPos.X, hoverPosAbs.Y - usedNavigationRefAbsPos.Y)
  end), computeTransformsTweenInfo)

  local computeHoverTransparency = scope:Tween(
    scope:Computed(function(use) return use(isHoverState) and .92 or 1 end),
    TweenInfo.new(.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
  )

  return navigationRef:set(scope:New "Frame" (CombineProps({
    AutomaticSize = Enum.AutomaticSize.XY,
    BackgroundTransparency = 1,

    [OnEvent "MouseLeave"] = function()
      isHoverState:set(false)
      isFirstHoverState:set(true)
    end,

    [Children] = {
      Squircle (scope, {
        Size = computeHoverSize,
        Position = computeHoverPos,
        Transparency = computeHoverTransparency,
        BackgroundColor3 = scope:GetThemeItem("Text/Title"),
        ZIndex = 0,
        Name = "Navigation:Hover"
      }),

      scope:New "Frame" {
        AutomaticSize = Enum.AutomaticSize.XY,
        BackgroundTransparency = 1,
        ZIndex = 1,
        Name = "Navigation:Items",

        [Children] = {
          sectionInstances :: any,

          scope:New "UIListLayout" {
            FillDirection = fillDirection,
            HorizontalFlex = scope:Computed(function(use)
              return use(fillDirection) == Enum.FillDirection.Horizontal and Enum.UIFlexAlignment.Fill or Enum.UIFlexAlignment.None
            end),
          }
        }
      }
    }
  }, props)))
end)


--!strict

--> Modules -------------------------------------------------------------------------------------------
local CoreComponents = script.Parent.Core
local Squircle = require(CoreComponents.Squircle)
local TextLabel = require(CoreComponents.TextLabel)

local Modules = script.Parent.Parent.Modules
local Component = require(Modules.Component)
local ComputeMath = require(Modules.ComputeMath)
local ComputeTransforms = require(Modules.ComputeTransforms)
local TableUtils = require(Modules.TableUtils)

local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Types ---------------------------------------------------------------------------------------------
type ProgressBarProps = {
  Width: Fusion.UsedAs<UDim>?,
  ProgressPercent: Fusion.UsedAs<number>?,
  Sections: Fusion.UsedAs<number? | { string }?>?,
}
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local ComputeClamp = ComputeMath.Clamp
local ComputeUDim, ComputeUDim2 = ComputeTransforms.UDim, ComputeTransforms.UDim2

local TableTake = TableUtils.Take

local Children = Fusion.Children
-------------------------------------------------------------------------------------------------------

--> Functions -----------------------------------------------------------------------------------------
local function ForRange(scope: Fusion.Scope<typeof(Fusion)>, range: Fusion.UsedAs<number>, fn: (use: Fusion.Use, idx: number) -> Instance)
  return (scope:Computed(function(use)
    local usedRange = use(range)

    local children = table.create(usedRange)
    for idx = 1, usedRange do children[idx] = fn(use, idx) end

    return children
  end) :: any) :: Instance
end

local function SectionTitle(scope: Component.Scope, idx: number, text: string)
  return TextLabel (scope, {
    Text = text,
    Focus = "Title" :: "Title",
    Weight = Enum.FontWeight.SemiBold,
    Size = UDim2.new(1,0, 0,25),
    AutomaticSize = Enum.AutomaticSize.Y,
    TextXAlignment = Enum.TextXAlignment.Center,
    LayoutOrder = idx,
    Name = "Progress:Title",
  })
end

local function ProgressBar(scope: Component.Scope, idx: number, sections: number, progressPercentState: Fusion.Spring<number>)
  local computeLocalProgressPercent = scope:Computed(function(use)
   return math.clamp((use(progressPercentState) / (1 / sections)) - (idx - 1), 0, 1)
  end)

  return Squircle (scope, {
    Size = UDim2.new(1,0, 0,4),
    BackgroundColor3 = scope:GetThemeItem("Accent/Primary"),
    BackgroundTransparency = .65,
    Image = "rbxassetid://104271355998011",
    SliceCenter = Rect.new(8,4,8,4),
    LayoutOrder = idx,
    Name = "Progress:Bar",

    [Children] = Squircle (scope, {
      Size = ComputeUDim2(scope, ComputeUDim(scope, computeLocalProgressPercent, 0), UDim.new(1, 0)),
      BackgroundColor3 = scope:GetThemeItem("Accent/Primary"),
      Image = "rbxassetid://104271355998011",
      SliceCenter = Rect.new(8,4,8,4),
      Name = "Progress:Bar:Inner"
    })
  })
end

local function ProgressList(scope: Component.Scope, name: string, layoutOrder: number, ...: Fusion.Child)
  return scope:New "Frame" {
    Size = UDim2.fromScale(1, 0),
    AutomaticSize = Enum.AutomaticSize.Y,
    BackgroundTransparency = 1,
    LayoutOrder = layoutOrder,

    [Children] = {
      ...,

      scope:New "UIListLayout" {
        FillDirection = Enum.FillDirection.Horizontal,
        VerticalAlignment = Enum.VerticalAlignment.Center,
        HorizontalFlex = Enum.UIFlexAlignment.Fill,
        Padding = UDim.new(0, 4)
      }
    }
  }
end
-------------------------------------------------------------------------------------------------------

return Component(function(scope, props: ProgressBarProps)
  local width, progressPercentState, sections =
    TableTake(props, "Width", UDim.new(1,0)) :: Fusion.UsedAs<UDim>,
    ComputeClamp(scope, TableTake(props, "ProgressPercent", 1), 0, 1) :: Fusion.Computed<number>,
    TableTake(props, "Sections", 1) :: Fusion.UsedAs<number>

  local computeSectionsLength = scope:Computed(function(use)
    local usedSections = use(sections)
    return math.floor(math.max((typeof(usedSections) == "table" and #usedSections or usedSections) :: number, 1))
  end)

  return scope:New "Frame" {
    Size = ComputeUDim2(scope, width, UDim.new(0, 0)),
    AutomaticSize = Enum.AutomaticSize.Y,
    BackgroundTransparency = 1,
    Name = "Progress",

    [Children] = {
      ProgressList(scope, "Progress:Titles", 0, scope:Computed(function(use)
        local usedSections = use(sections)
        if typeof(usedSections) == "number" then return nil end
        return scope:ForPairs(usedSections, function(use, scope, key, value) return key, SectionTitle(scope, key, value) end) :: any
      end)),

      ProgressList(scope, "Progress:Bars", 1, scope:Computed(function(use)
        local usedSections = use(sections)

        if typeof(usedSections) == "number" then
          return ForRange(scope, sections, function(use, idx)
            return ProgressBar(scope, idx, use(computeSectionsLength), scope:Spring(progressPercentState, 25, 1))
          end) :: any

        else
          return scope:ForPairs(usedSections, function(use, scope, key, value)
            return key, ProgressBar(scope, key, use(computeSectionsLength), scope:Spring(progressPercentState, 25, 1))
          end) :: any
        end
      end)),

      scope:New "UIListLayout" {
        FillDirection = Enum.FillDirection.Vertical,
        Padding = UDim.new(0, 2)
      }
    }
  }
end)


--!strict

--> Modules -------------------------------------------------------------------------------------------
local Modules = script.Parent.Parent.Modules
local Component = require(Modules.Component)
local CombineProps = require(Modules.CombineProps)
local ForceToState = require(Modules.ForceToState)
local TableUtils = require(Modules.TableUtils)

local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Types ---------------------------------------------------------------------------------------------
type ScrollableProps = {
  CanvasPositionY: Fusion.UsedAs<number>?
}
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local Children, OnEvent, peek = Fusion.Children, Fusion.OnEvent, Fusion.peek

local TableTake = TableUtils.Take
-------------------------------------------------------------------------------------------------------

--> Functions -----------------------------------------------------------------------------------------
local function ChildrenContainsInstance(children: { Instance } | Instance, instName: string)
  if type(children) ~= "table" then
    if typeof(children) == "Instance" and children.ClassName == instName then return true end
  else
    for _,child in children do
      if typeof(child) == "Instance" and child.ClassName == instName then return true end
    end
  end

	return false
end
-------------------------------------------------------------------------------------------------------

return Component(function(scope, props: Fusion.PropertyTable)
  local children = TableTake(props, Children)
  local canvasPositionY: Fusion.Value<number> = ForceToState(scope, TableTake(props, "CanvasPositionY", 0)) :: any
  local canvasSize = TableTake(props, "CanvasSize")

  local scrollFrameRef: Fusion.Value<Instance> = scope:Value(nil) :: any
  local scrollBarBackgroundRef: Fusion.Value<Instance> = scope:Value(nil) :: any

  local scrollFrameCanvasPosState = scope:Value(Vector2.zero)

  local function moveScrollCursor()
		local peekedScrollBarBackground: Frame? = peek(scrollBarBackgroundRef) :: Frame
		local peekedScrollFrame: ScrollingFrame? = peek(scrollFrameRef) :: ScrollingFrame

    if peekedScrollBarBackground == nil or peekedScrollFrame == nil then return end

		local scrollBackgroundAbsHeight = peekedScrollBarBackground.AbsoluteSize.Y
		local scrollBackgroundAbsPosY = peekedScrollBarBackground.AbsolutePosition.Y
		local scrollFrameAbsCanvasHeight = peekedScrollFrame.AbsoluteCanvasSize.Y

		local peekedCanvasPositionY = peek(canvasPositionY)

		local newScrollPositionPercent = math.clamp((peekedCanvasPositionY - scrollBackgroundAbsPosY) / scrollBackgroundAbsHeight, 0, 1)
		local newScrollPosition = (scrollFrameAbsCanvasHeight * newScrollPositionPercent) - (scrollBackgroundAbsHeight / 2);

		scrollFrameCanvasPosState:set(Vector2.new(0, newScrollPosition))
	end

  return scope:New "Frame" (CombineProps({
    ClipsDescendants = true,
		BackgroundTransparency = 1,
    Name = "Scrollable",

    [Children] = {
      scrollBarBackgroundRef:set(scope:New "ImageButton" {
        Size = UDim2.new(0,4, 1,0),
        Position = UDim2.fromScale(1, .5),
				AnchorPoint = Vector2.new(1,.5),
				ImageColor3 = scope:GetThemeItem("Background/Tertiary"),
        BackgroundTransparency = 1,
        ScaleType = Enum.ScaleType.Slice,
        SliceCenter = Rect.new(4, 4, 4, 4),
        Image = "rbxassetid://82161439810808",
				Name = "Scrollable:ScrollBarBackground",

        [OnEvent "MouseButton1Down"] = moveScrollCursor,
				[OnEvent "MouseMoved"] = function(_, y) canvasPositionY:set(y) end,
      }),

      scrollFrameRef:set(scope:New "ScrollingFrame" {
        Size = UDim2.fromScale(1,1),
				ScrollBarThickness = 4,
        TopImage = "rbxassetid://75722704152620",
				MidImage = "rbxassetid://86201773979334",
				BottomImage = "rbxassetid://120208654241752",
        BackgroundTransparency = 1,
				AutomaticCanvasSize = Enum.AutomaticSize.Y,
				CanvasSize = canvasSize or UDim2.fromScale(0,0),
				ScrollBarImageColor3 = scope:GetThemeItem("Background/Stroke", "Highlight"),
				CanvasPosition = scope:Tween(scrollFrameCanvasPosState, TweenInfo.new(.25)),
        Name = "Scrollable:ScrollFrame",

        [Children] = {
          (children and (not ChildrenContainsInstance(children, "UIPadding"))) and
						scope:New "UIPadding" {
							PaddingRight = UDim.new(0, 8)
						} or nil :: any,

          (children and (not ChildrenContainsInstance(children, "UIListLayout"))) and
						scope:New "UIListLayout" {
							FillDirection = Enum.FillDirection.Vertical,
							VerticalAlignment = Enum.VerticalAlignment.Top,
							Padding = UDim.new(0, 4),
							SortOrder = Enum.SortOrder.LayoutOrder
						} or nil :: any,

          children
        }
      })
    }
  }, props))
end)


--!strict

--> Modules -------------------------------------------------------------------------------------------
local Modules = script.Parent.Parent.Modules
local Component = require(Modules.Component)
local ComputeTransforms = require(Modules.ComputeTransforms)
local ForceToState = require(Modules.ForceToState)
local TableUtils = require(Modules.TableUtils)
local CombineProps = require(Modules.CombineProps)

local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Types ---------------------------------------------------------------------------------------------
type SwitchProps = {
  Enabled: Fusion.UsedAs<boolean>?,
  Size: Fusion.UsedAs<"Small" | "Large">?
}
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local ComputeUDim2FromOffset = ComputeTransforms.UDim2FromOffset

local Children, OnEvent, peek = Fusion.Children, Fusion.OnEvent, Fusion.peek

local TableTake = TableUtils.Take

local OUTER_IMAGE_LARGE, OUTER_IMAGE_SMALL = "rbxassetid://108821029442118", "rbxassetid://70799998969729"
local INNER_IMAGE_LARGE, INNER_IMAGE_SMALL = "rbxassetid://133577879822525", "rbxassetid://107631384534110"

local OUTER_HIGHLIGHT_LARGE, OUTER_HIGHLIGHT_SMALL = "rbxassetid://134592438319778", "rbxassetid://86038953565577"

local OUTER_SIZE_LARGE, OUTER_SIZE_SMALL = UDim2.fromOffset(32, 18), UDim2.fromOffset(24, 14)
local INNER_SIZE_LARGE, INNER_SIZE_SMALL = 14, 10
-------------------------------------------------------------------------------------------------------

return Component(function(scope, props: SwitchProps)
  local size = TableTake(props, "Size", "Large") :: Fusion.UsedAs<"Small" | "Large">
  local enabledState = ForceToState(scope, TableTake(props, "Enabled", scope:Value(true))) :: Fusion.Value<boolean>

  local isHover, isFocus = scope:Value(false), scope:Value(false)

  local computeBackgroundColor = scope:GetThemeItem(
    scope:Computed(function(use) return use(enabledState) and "Accent/Primary" or "Text/Body" end) :: any,
    scope:Computed(function(use) return if use(isFocus) then "Focus" elseif use(isHover) then "Hover" else nil :: any end)
  )

  local computeOuterImage = scope:Computed(function(use) return use(size) == "Large" and OUTER_IMAGE_LARGE or OUTER_IMAGE_SMALL end)
  local computerInnerImage = scope:Computed(function(use) return use(size) == "Large" and INNER_IMAGE_LARGE or INNER_IMAGE_SMALL end)

  local computeHighlightImage = scope:Computed(function(use) return use(size) == "Large" and OUTER_HIGHLIGHT_LARGE or OUTER_HIGHLIGHT_SMALL end)

  local computeOuterSize = scope:Computed(function(use) return use(size) == "Large" and OUTER_SIZE_LARGE or OUTER_SIZE_SMALL end)

  local computeInnerHeight = scope:Computed(function(use) return use(size) == "Large" and INNER_SIZE_LARGE or INNER_SIZE_SMALL end)
  local computeInnerWidth = scope:Computed(function(use) return use(isFocus) and use(computeInnerHeight) * 1.35 or use(computeInnerHeight) end)

  local computeSliceCenter = scope:Computed(function(use)
    local usedComputeInnerHeight = use(computeInnerHeight)
    return Rect.new(usedComputeInnerHeight, usedComputeInnerHeight, usedComputeInnerHeight, usedComputeInnerHeight)
  end)

  local computeInnerPos = scope:Tween(scope:Computed(function(use)
    return use(enabledState) and UDim2.new(1,-use(computeInnerWidth), .5,0) or UDim2.fromScale(0, .5)
  end), TweenInfo.new(.125, Enum.EasingStyle.Quad, Enum.EasingDirection.Out))

  -- Handles the edge case of the user dragging the cursor off of the switch and then releasing the mouse button.
  scope:AddRootEvent("InputEnded", function() isFocus:set(false) end)

  return scope:New "ImageButton" (CombineProps({
    Size = computeOuterSize,
    Image = computeOuterImage,
    ImageColor3 = computeBackgroundColor,
    BackgroundTransparency = 1,
    Name = "Switch",

    [OnEvent "MouseEnter"] = function() isHover:set(true) end :: any,
    [OnEvent "MouseLeave"] = function() isHover:set(false) end :: any,

    [OnEvent "MouseButton1Down"] = function() isFocus:set(true) end :: any,
    [OnEvent "MouseButton1Up"] = function() isFocus:set(false) end :: any,

    [OnEvent "MouseButton1Click"] = function() enabledState:set(not peek(enabledState)) end,

    [Children] = {
      scope:New "Frame" {
        Size = UDim2.fromScale(1, 1),
        BackgroundTransparency = 1,
        Name = "Switch:MainContent",

        [Children] = {
          scope:New "ImageLabel" {
            Size = scope:Tween(
              ComputeUDim2FromOffset(scope, computeInnerWidth, computeInnerHeight),
              TweenInfo.new(.125, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
            ),
            Image = computerInnerImage,
            Position = computeInnerPos,
            AnchorPoint = Vector2.new(0, .5),
            ImageColor3 = scope:GetThemeItem("Background/Primary"),
            BackgroundTransparency = 1,
            ScaleType = Enum.ScaleType.Slice,
            SliceCenter = computeSliceCenter,
            Name = "Switch:Inner"
          },

          scope:New "UIPadding" { PaddingLeft = UDim.new(0, 2), PaddingRight = UDim.new(0, 2) }
        }
      },

      scope:New "ImageLabel" {
        Size = UDim2.fromScale(1,1),
        Image = computeHighlightImage,
        BackgroundTransparency = 1,
        Name = "Switch:Highlight"
      },
    }
  }, props))

end)


--!strict

--> Services ------------------------------------------------------------------------------------------
local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")
-------------------------------------------------------------------------------------------------------

--> Modules -------------------------------------------------------------------------------------------
local Components = script.Parent
local CoreComponents = Components.Core
local Squircle = require(CoreComponents.Squircle)
local TextLabel = require(CoreComponents.TextLabel)
local FocusVisual = require(CoreComponents.FocusVisual)

local Modules = script.Parent.Parent.Modules
local Component = require(Modules.Component)
local ComputeTransforms = require(Modules.ComputeTransforms)
local TableUtils = require(Modules.TableUtils)
local CombineProps = require(Modules.CombineProps)
local ForceToState = require(Modules.ForceToState)

local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Types ---------------------------------------------------------------------------------------------
export type ValidationProps = Fusion.UsedAs<string> | {
  Pattern: Fusion.UsedAs<string>,
  Mode: Fusion.UsedAs<"Restrict" | "ErrorVisual" | nil>
}

type Variant = Fusion.UsedAs<"Primary" | "Secondary" | nil>

type TextInputProps = {
  Width: Fusion.UsedAs<UDim>?,
  Placeholder: Fusion.UsedAs<string?>,
  Value: Fusion.UsedAs<string?>,
  Variant: Variant?,
  Keybind: Fusion.UsedAs<{ Enum.KeyCode }>?,
  Validation: ValidationProps?,
  IsFocus: Fusion.UsedAs<boolean>?,
  IsHover: Fusion.UsedAs<boolean>?,
  StrokeImage: Fusion.UsedAs<string>?,
  FocusImage: Fusion.UsedAs<string>?
}
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local ComputeUDim2, ComputeUDim = ComputeTransforms.UDim2, ComputeTransforms.UDim

local TableTake = TableUtils.Take

local Children, OnEvent, OnChange, peek = Fusion.Children, Fusion.OnEvent, Fusion.OnChange, Fusion.peek

local IS_WINDOWS: boolean = GuiService.IsWindows
-------------------------------------------------------------------------------------------------------

--> Private Functions ---------------------------------------------------------------------------------
local function MakePatternLocal(pattern: string)
  pattern = string.gsub(pattern, "^\^+", "")
  pattern = string.gsub(pattern, "\$+$", "")
  return pattern
end

local function GetEventsFromProps(props)
  local events = {}

  for key,value in props do
    if typeof(key) ~= "table" then continue end
    if key.kind ~= "OnEvent" then continue end
    events[key] = TableTake(props, key)
  end

  return events
end

local function TextBoxValidation(
  scope: Component.Scope, rawValue: Fusion.Value<string>, value: Fusion.Value<string>, textBoxRef: Fusion.Value<Instance>,
  computeParsedPattern: Fusion.Computed<string>, validationMode: Fusion.UsedAs<"Restrict" | "ErrorVisual" | nil>
)
  -- Makes sure `rawValue` is up to date if `value` changes.
  local updateRawValue = true
  scope:Observer(value):onChange(function()
    if not updateRawValue then return end
    rawValue:set(peek(value))
  end)

  return function(useOrPeek: <T>(T) -> any)
    local usedParsedPattern = useOrPeek(computeParsedPattern)
    if not usedParsedPattern then return false end
    local usedRawValue = useOrPeek(rawValue)

    if (useOrPeek(validationMode) or "ErrorVisual") == "ErrorVisual" then
      updateRawValue = false
      value:set(usedRawValue)
      updateRawValue = true
      return string.match(usedRawValue, usedParsedPattern) and true or false

    -- Since the validation mode is `Restrict` we need to get
    -- the first match and set the TextBox's text to it.
    else
      local matches = string.gmatch(usedRawValue, usedParsedPattern)

      local match
      for m in matches do match = m; break end
      if match == nil then match = "" end

      local usedTextBoxRef: TextBox = useOrPeek(textBoxRef) :: any

      updateRawValue = false
      value:set(match)
      updateRawValue = true
      if usedTextBoxRef then usedTextBoxRef.Text = match end  -- Make sure the text has updated.
      return true
    end
  end
end

local function KeyCodeEnumToLabel(scope: Component.Scope, enum: Enum.KeyCode, isWindows: boolean, textColor: Fusion.UsedAs<Color3>): any
  if not isWindows and enum == Enum.KeyCode.LeftControl then
    return scope:New "ImageLabel" {
      Size = UDim2.fromOffset(8, 8),
      BackgroundTransparency = 1,
      Image = "rbxassetid://80547592081199",
      ImageColor3 = textColor
    }
  end

  local text = (
    if enum == Enum.KeyCode.LeftControl then "LCNTRL"
    elseif enum == Enum.KeyCode.RightControl then "RCNTRL"
    elseif enum == Enum.KeyCode.LeftShift then "LSHFT"
    elseif enum == Enum.KeyCode.RightShift then "RSHFT"
    elseif enum == Enum.KeyCode.LeftAlt then "LALT"
    elseif enum == Enum.KeyCode.RightAlt then "RALT"
    else enum.Name
  )

  return TextLabel(scope, { Text = text, TextColor3 = textColor })
end

local function KeybindPrompt(scope: Component.Scope, keybind: Fusion.UsedAs<{ Enum.KeyCode }>)
  local textColor = scope:GetThemeItem("Text/Body", function(self) return self:Lerp(Color3.new(1,1,1), .05) end, false)

  return Squircle(scope, {
    Size = UDim2.new(0,0, 0,15),
    AutomaticSize = Enum.AutomaticSize.X,
    Image = "rbxassetid://77469521389584",
    BackgroundColor3 = scope:GetThemeItem("Background/Secondary", function(self) return self:Lerp(Color3.new(1,1,1), .05) end, false),
    SliceCenter = Rect.new(15, 15, 15, 15),
    LayoutOrder = 1,

    [Children] = {
      Squircle(scope, {
        Size = UDim2.fromScale(1, 1),
        Image = "rbxassetid://101010257373189",
        BackgroundColor3 = scope:GetThemeItem("Background/Stroke", function(self) return self:Lerp(Color3.new(1,1,1), .05) end, false),
        SliceCenter = Rect.new(15, 15, 15, 15),
      }),

      scope:New "Frame" {
        Size = UDim2.new(0,0, 0,15),
        AutomaticSize = Enum.AutomaticSize.X,
        BackgroundTransparency = 1,

        [Children] = {
          scope:ForPairs(keybind, function(use, scope, idx, enum)
            return idx, KeyCodeEnumToLabel(scope, enum, IS_WINDOWS, textColor)
          end) :: any,

          scope:New "UIListLayout" {
            FillDirection = Enum.FillDirection.Horizontal,
            HorizontalAlignment = Enum.HorizontalAlignment.Center,
            VerticalAlignment = Enum.VerticalAlignment.Center,
            Padding = ComputeUDim(scope, 0, 3)
          },

          scope:New "UIPadding" {
            PaddingLeft = UDim.new(0,5), PaddingRight = UDim.new(0,5)
          }
        }
      },
    }
  })
end

local function KeybindIsPressed(pressedKeys: { Enum.KeyCode }, keybind: { Enum.KeyCode }?)
  if not keybind then return false end
  local pressed = 0

  for _,pressedKey in pressedKeys do
    for _,key in keybind do
      if pressedKey == key then pressed += 1 end
    end
  end

  return if pressed == #keybind then true else false
end
-------------------------------------------------------------------------------------------------------

return Component(function(scope, props: TextInputProps)
  local width: Fusion.UsedAs<UDim>, variant: Variant? = TableTake(props, "Width", UDim.new(1,0)), TableTake(props, "Variant")
  local placeholder, value = TableTake(props, "Placeholder", "Text Input"), ForceToState(scope, TableTake(props, "Value", ""))

  local validation: ValidationProps? = TableTake(props, "Validation")
  local validationIsString = validation and type(validation) == "string"
  local validationPattern = if not validation then nil else if validationIsString then validation else (validation :: any).Pattern
  local validationMode = if not validation then nil else if validationIsString then nil else (validation :: any).Mode

  local keybind: Fusion.UsedAs<{ Enum.KeyCode }>? = TableTake(props, "Keybind")
  local isHoverState, isFocusState = ForceToState(scope, TableTake(props, "IsHover", false)), ForceToState(scope, TableTake(props, "IsFocus", false))
  local strokeImage, focusImage = TableTake(props, "StrokeImage"), TableTake(props, "FocusImage")
  local children = TableTake(props, "Children")

  local events = GetEventsFromProps(props)
  local focusLostEvent = TableTake(events, OnEvent("FocusLost"))

  local textBoxRef: Fusion.Value<Instance> = scope:Value(nil :: any)

  isHoverState:set(false)

  local function updateIsFocus()
    if not peek(isFocusState) then return end
    local peekedTextBox = peek(textBoxRef) :: TextBox
    if not peekedTextBox then return end
    if peekedTextBox:IsFocused() then return end
    peekedTextBox:CaptureFocus()
  end

  scope:Observer(isFocusState):onChange(updateIsFocus)
  scope:Observer(textBoxRef):onChange(updateIsFocus)

  local computeParsedPattern = scope:Computed(function(use)
    local pattern = MakePatternLocal(use(validationPattern))
    return
      if (use(validationMode) or "ErrorVisual") == "ErrorVisual" then `^{pattern}$`
      else pattern
  end)

  -- When the text box is changed this state will be updated, then when `computeIsValidated` runs
  -- the actual `value` state will be updated with the (parsed) text.
  -- If there is no validation then `rawValue` will just be `value`.
  local rawValue: Fusion.Value<string> = validation and scope:Value(peek(value)) or value

  local isValidated: Fusion.UsedAs<boolean> = true
  if validation then
    local textBoxValidation = TextBoxValidation(scope, rawValue, value, textBoxRef, computeParsedPattern, validationMode)
    textBoxValidation(peek)
    isValidated = scope:Computed(function(use) return textBoxValidation(use) end)
  end

  local computeBackgroundColor = scope:GetThemeItem(scope:Computed(function(use)
    return `Background/{use(variant) == "Secondary" and "Tertiary" or "Secondary"}` :: any
  end)) :: Fusion.Computed<Color3>

  local computeStrokeThemeItem = scope:Computed(function(use): any
    if use(isValidated) == false then return "Accent/Destructive" end
    if use(isFocusState) then return "Accent/Primary" end
    return "Background/Stroke"
  end)

  local computeStrokeFeedbackMode = scope:Computed(function(use) return (use(isHoverState) and "Highlight" or nil) :: any end)

  local selectedKeys = {}
  local function InputBegan(key: InputObject)
    table.insert(selectedKeys, key.KeyCode)

    if not KeybindIsPressed(selectedKeys, peek(keybind)) then return end;
    (peek(textBoxRef) :: TextBox):CaptureFocus()
    table.clear(selectedKeys)
  end

  local function InputEnded(key: InputObject)
    table.remove(selectedKeys, table.find(selectedKeys, key.KeyCode))
  end

  scope:AddRootEvent("InputBegan", InputBegan)
  scope:AddRootEvent("InputEnded", InputEnded)
  local inputBeganConn = UserInputService.InputBegan:Connect(InputBegan)
  local inputEndedConn = UserInputService.InputEnded:Connect(InputEnded)

  table.insert(scope, function()
    inputBeganConn:Disconnect()
    inputEndedConn:Disconnect()
  end)

  return Squircle (scope, {
    As = "ImageButton" :: "ImageButton",
    Size = ComputeUDim2(scope, width, UDim.new(0, 25)),
    Image = "rbxassetid://116342589966879",
    BackgroundColor3 = computeBackgroundColor,
    Name = "TextInput",

    [Children] = {
      Squircle (scope, {
        Size = UDim2.fromScale(1, 1),
        Image = strokeImage or "rbxassetid://72425541434885",
        BackgroundColor3 = scope:GetThemeItem(computeStrokeThemeItem, computeStrokeFeedbackMode),
        Name = "TextInput:Stroke"
      }),

      FocusVisual(scope, isFocusState, computeStrokeThemeItem, focusImage),

      children,

      scope:New "Frame" {
        Size = UDim2.new(1,0,1,0),
        BackgroundTransparency = 1,
        Name = "TextInput:Content",

        [Children] = {
          textBoxRef:set(scope:New "TextBox" (CombineProps({
            Size = UDim2.new(1,0, 1,0),
            BackgroundTransparency = 1,
            TextSize = 14,
            Text = value,
            PlaceholderText = placeholder,
            PlaceholderColor3 = scope:GetThemeItem("Text/Body"),
            TextXAlignment = Enum.TextXAlignment.Left,
            FontFace = Font.fromId(16658221428, Enum.FontWeight.Medium),
            TextColor3 = scope:GetThemeItem("Text/Title"),
            ClipsDescendants = true,
            LayoutOrder = 0,

            [OnEvent "Focused"] = function() isFocusState:set(true) end,
            [OnEvent "FocusLost"] = function()
              isFocusState:set(false)
              if focusLostEvent then focusLostEvent() end
            end,

            [OnEvent "MouseEnter"] = function() isHoverState:set(true) end,
            [OnEvent "MouseLeave"] = function() isHoverState:set(false) end,

            [OnChange "Text"] = function(newText) rawValue:set(newText) end,

            [Children] = scope:New "UIFlexItem" { FlexMode = Enum.UIFlexMode.Fill }
          }, events))),

          scope:Computed(function(use)
            local usedKeybind = use(keybind)
            return if usedKeybind then KeybindPrompt(scope, usedKeybind) else nil
          end) :: any,

          scope:New "UIPadding" {
            PaddingRight = ComputeUDim(scope, 0, scope:Computed(function(use) return use(keybind) and 5 or 8 end)),
            PaddingLeft = UDim.new(0, 8)
          },

          scope:New "UIListLayout" {
            FillDirection = Enum.FillDirection.Horizontal,
            HorizontalFlex = Enum.UIFlexAlignment.SpaceBetween,
            SortOrder = Enum.SortOrder.LayoutOrder,
            VerticalAlignment = Enum.VerticalAlignment.Center,
            Padding = UDim.new(0, 5)
          },
        }
      }

    }
  }, props :: any)
end)


--!strict

--> Services ------------------------------------------------------------------------------------------
local StarterGui = game:GetService("StarterGui")
-------------------------------------------------------------------------------------------------------

--> Modules -------------------------------------------------------------------------------------------
local Modules = script.Parent.Parent.Modules
local RootEventHandlersUtils = require(Modules.RootEventHandlersUtils)
local Component = require( Modules.Component)
local CombineProps = require(Modules.CombineProps)
local TableUtils = require(Modules.TableUtils)

local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)

local Components = script.Parent.Parent.Components
local Background = require(Components.Background)
-------------------------------------------------------------------------------------------------------

--> Types ---------------------------------------------------------------------------------------------
type WindowSettingsProps = {
	Id: string,
  Title: (string | Fusion.Value<string> | Fusion.Computed<string>)?,
	InitDockState: Enum.InitialDockState?,
	InitEnabled: boolean?,
	OverrideEnabledRestore: boolean?,
	FloatXSize: number?,
	FloatYSize: number?,
	MinWidth: number?,
	MinHeight: number?,
	DebugMode: boolean?
}
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local TableTake = TableUtils.Take
local AddRootToHandler = RootEventHandlersUtils.AddRootToHandler

local Children = Fusion.Children
-------------------------------------------------------------------------------------------------------

return Component(function(scope, props: WindowSettingsProps)
  local id, title, initDockState, initEnabled, overrideEnabledRestore, floatXSize, floatYSize, minWidth, minHeight, debugMode =
	  TableTake(props, "Id"), TableTake(props, "Title"), TableTake(props, "InitDockState"), TableTake(props, "InitEnabled"),
    TableTake(props, "OverrideEnabledRestore"), TableTake(props, "FloatXSize"), TableTake(props, "FloatYSize"),
    TableTake(props, "MinWidth"), TableTake(props, "MinHeight"), TableTake(props, "DebugMode")

  local children = TableTake(props, Children)

  local widget = script:FindFirstAncestorOfClass("Plugin"):CreateDockWidgetPluginGui(id, DockWidgetPluginGuiInfo.new(
    initDockState or Enum.InitialDockState.Float, initEnabled or true, overrideEnabledRestore or true,
    floatXSize or 0, floatYSize or 0,
    minWidth or floatXSize or 0, minHeight or floatYSize or 0
  ))

  widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling

  local root = scope:New "Frame" {
    Size = UDim2.fromScale(1, 1),
    ZIndex = 1099,
    BackgroundTransparency = 1,
    Active = false,
    Selectable = false,
    Name = "Widget:Root"
  }
  table.insert(scope, AddRootToHandler(scope, root))

  local content = Background (scope, (CombineProps({
    [Children] = {
      root,

      scope:New "Frame" {
        Size = UDim2.fromScale(1, 1),
        BackgroundTransparency = 1,
        Name = "Widget:Content",

        [Children] = children
      }
    }
  }, props)))

  local debugGuiName = `CAMERONPCAMPBELL:IGNITE_DEBUG[{id}]`
  local found = StarterGui:FindFirstChild(debugGuiName)
  if found then found:Destroy() end

  if debugMode then
    scope:New "ScreenGui" {
      Parent = StarterGui,
      Name = debugGuiName,
      [Children] = content:Clone()
    }
  end

  return scope:Hydrate(widget) {
    Title = title or id,
    [Children] = content,
  }
end)


--[[

MIT License

Copyright (c) 2024 Cameron Campbell

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

]]


--!strict

--> Modules -------------------------------------------------------------------------------------------
local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local Children = Fusion.Children
-------------------------------------------------------------------------------------------------------

return function(...: Fusion.PropertyTable): Fusion.PropertyTable
	local propTablesLength = select("#", ...)
	if propTablesLength == 1 then return ... end

	local combinedProps: Fusion.PropertyTable = {}
	local combinedChildren: { Instance } = {}

	for i = 1, propTablesLength do
		local propTble = select(i, ...)

		for propName,propValue in propTble do

			if propName == Children then
				if typeof(propValue) == "table" and not propValue.kind then
					for _,child in propValue do table.insert(combinedChildren, child) end
				else
					table.insert(combinedChildren, propValue)
				end
				continue
			end

			combinedProps[propName] = propValue
		end
	end

	if #combinedChildren then
		combinedProps[Children] = combinedChildren
	end

	return combinedProps
end


--> Modules -------------------------------------------------------------------------------------------
local Modules = script.Parent.Parent
local RootEventHandlersUtils = require(Modules.RootEventHandlersUtils)
local ThemeHandler = require(Modules.ThemeHandler)

local Packages = script.Parent.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Types ---------------------------------------------------------------------------------------------
type CustomScope = {
  AddRootEvent: typeof(RootEventHandlersUtils.AddRootEvent),

  SetTheme: typeof(ThemeHandler.SetThemeForScope),
  GetThemeItem: typeof(ThemeHandler.GetThemeItemForScope),
}

export type Scope = Fusion.Scope<CustomScope & typeof(Fusion)>
-------------------------------------------------------------------------------------------------------

return nil


--!strict

--> Modules -------------------------------------------------------------------------------------------
local ComponentTypes = require(script["Component.Types"])

local Modules = script.Parent
local CombineProps = require(Modules.CombineProps)

local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Types ---------------------------------------------------------------------------------------------
type BaseProps = {
  [string | Fusion.SpecialKey]: unknown?
}

export type Scope = ComponentTypes.Scope

export type DynamicSection = string | Fusion.Child
-------------------------------------------------------------------------------------------------------

return function<Props>(fn: (scope: Scope, props: BaseProps & Props) -> Instance)
  return function(scope: Scope, ...: Props & BaseProps)
    local combinedProps = CombineProps(...) :: BaseProps & Props
    return fn(scope, combinedProps)
  end
end


--!strict

--> Services ------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------

--> Modules -------------------------------------------------------------------------------------------
local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Functions -----------------------------------------------------------------------------------------
local function ComputeMultiply(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>, multiplyBy: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return use(number) :: number * use(multiplyBy) end)
end

local function ComputeDivide(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>, divideBy: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return use(number) :: number / use(divideBy) end)
end

local function ComputeFloorDivide(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>, floorDivideBy: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return use(number) :: number // use(floorDivideBy) end)
end

local function ComputePow(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>, toPower: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return math.pow(use(number), use(toPower)) end)
end

local function ComputeAdd(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>, toAdd: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return use(number) :: number + use(toAdd) end)
end

local function ComputeSubtract(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>, subtractBy: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return use(number) :: number + use(subtractBy) end)
end

local function ComputeMin(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>, min: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return math.min(use(number), use(min)) end)
end

local function ComputeMax(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>, max: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return math.max(use(number), use(max)) end)
end

local function ComputeClamp(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>, min: Fusion.UsedAs<number>, max: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return math.clamp(use(number), use(min), use(max)) end)
end

local function ComputeFloor(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return math.floor(use(number)) end)
end

local function ComputeCeil(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return math.ceil(use(number)) end)
end

local function ComputeRound(scope: Fusion.Scope<typeof(Fusion)>, number: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return math.round(use(number)) end)
end
-------------------------------------------------------------------------------------------------------

return {
  Multiply = ComputeMultiply,
  Divide = ComputeDivide,
  FloorDivide = ComputeFloorDivide,

  Pow = ComputePow,

  Add = ComputeAdd,
  Subtract = ComputeSubtract,

  Min = ComputeMin,
  Max = ComputeMax,
  Clamp = ComputeClamp,

  Floor = ComputeFloor,
  Ceil = ComputeCeil,
  Round = ComputeRound
}


--!strict

--> Services ------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------

--> Modules -------------------------------------------------------------------------------------------
local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Functions -----------------------------------------------------------------------------------------
local function ComputeUDim(scope: Fusion.Scope<typeof(Fusion)>, scale: Fusion.UsedAs<number>, offset: Fusion.UsedAs<number>)
  return scope:Computed(function(use) return UDim.new(use(scale), use(offset)) end)
end

local function ComputeUDim2(scope: Fusion.Scope<typeof(Fusion)>, width: Fusion.UsedAs<UDim>, height: Fusion.UsedAs<UDim>)
  return scope:Computed(function(use)
    local usedWidth, usedHeight = use(width) or UDim.new(0,0), use(height) or UDim.new(0,0)
    return UDim2.new(usedWidth, usedHeight)
  end)
end

local function ComputeUDim2FromScale(scope: Fusion.Scope<typeof(Fusion)>, scaleWidth: Fusion.UsedAs<number>, scaleHeight: Fusion.UsedAs<number>)
  return scope:Computed(function(use)
    local usedScaleWidth, usedScaleHeight = use(scaleWidth), use(scaleHeight)
    return UDim2.fromScale(usedScaleWidth, usedScaleHeight)
  end)
end

local function ComputeUDim2FromOffset(scope: Fusion.Scope<typeof(Fusion)>, offsetWidth: Fusion.UsedAs<number>, offsetHeight: Fusion.UsedAs<number>)
  return scope:Computed(function(use)
    local usedScaleWidth, usedScaleHeight = use(offsetWidth), use(offsetHeight)
    return UDim2.fromOffset(usedScaleWidth, usedScaleHeight)
  end)
end

local function ComputeTweenInfo(
  scope: Fusion.Scope<typeof(Fusion)>, time: Fusion.UsedAs<number>,
  easingStyle: Fusion.UsedAs<Enum.EasingStyle>?, easingDirection: Fusion.UsedAs<Enum.EasingDirection>?,
  repeatCount: Fusion.UsedAs<number>?, reverses: Fusion.UsedAs<number>?, delayTime: Fusion.UsedAs<number>?
)
  return scope:Computed(function(use)
   return TweenInfo.new(
    use(time),
    use(easingStyle) or Enum.EasingStyle.Linear,
    use(easingDirection) or Enum.EasingDirection.In,
    use(repeatCount) or 0,
    use(reverses) or false,
    use(delayTime) or 0
  )
  end)
end
-------------------------------------------------------------------------------------------------------

return {
  UDim = ComputeUDim,

  UDim2 = ComputeUDim2,
  UDim2FromScale = ComputeUDim2FromScale,
  UDim2FromOffset = ComputeUDim2FromOffset,

  TweenInfo = ComputeTweenInfo
}


--!strict

--> Modules -------------------------------------------------------------------------------------------
local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

return function<T>(
	scope: Fusion.Scope<typeof(Fusion)>, maybeState: T | Fusion.Value<T>
): Fusion.Value<T>
	return ((typeof(maybeState) == "table" and maybeState["type"] == "State") and maybeState or scope:Value(maybeState)) :: Fusion.Value<T>
end


return {
	CountKeys = function(tble: { [any]: any }): number
		local length = 0
		for _ in tble do length += 1 end
		return length
	end,

	HasItems = function(tble: { [any]: any }): boolean
		for _ in tble do return true end
		return false
	end,

	Take = function<K, V>(tble: { [K]: V }, keyOrIndex: K, default: V?)
		local value = tble[keyOrIndex]
		tble[keyOrIndex] = nil

		if value == nil then return default end
		return value
	end,

	ContainsInst = function(tble: { [any]: any }, className: string)
		for _,value in tble do
			if not (typeof(value) == "Instance" and value.ClassName == className) then continue end
			return true
		end

		return false
	end,

	InsertMany = function(tble: { [any]: any }, ...: any)
		for i = 1, select("#", ...) do table.insert(tble, select(i, ...)) end
	end
}


export type ThemeConfig = {
  Background: {
    Primary: Color3,
    Secondary: Color3,
    Tertiary: Color3,
    Stroke: Color3
  },

  Text: {
    Title: Color3,
    Body: Color3
  },

  Accent: {
    Primary: Color3,
    Secondary: Color3,
    Destructive: Color3
  }
}

export type ThemeFlatConfig = {
  ["Background/Primary"]: Color3,
  ["Background/Secondary"]: Color3,
  ["Background/Tertiary"]: Color3,
  ["Background/Stroke"]: Color3,

  ["Text/Title"]: Color3,
  ["Text/Body"]: Color3,

  ["Accent/Primary"]: Color3,
  ["Accent/Secondary"]: Color3,
  ["Accent/Destructive"]: Color3,
}

export type ThemeItem = "Background/Primary" | "Background/Secondary" | "Background/Tertiary" | "Background/Stroke" | "Text/Title" | "Text/Body" | "Accent/Primary" | "Accent/Secondary" | "Accent/Destructive"

export type MutatorPreset = "Default" | "Hover" | "Focus" | "Highlight"

return nil


--!strict

--> Modules -------------------------------------------------------------------------------------------
local Types = require(script["ThemeHandler.Types"])
local DefaultThemes = require(script.DefaultThemes)

local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Functions -----------------------------------------------------------------------------------------
local function FlattenThemeConfig(theme: Types.ThemeConfig): Types.ThemeFlatConfig
  local backgroud, text, accent = theme.Background, theme.Text, theme.Accent

  return {
    ["Background/Primary"] = backgroud.Primary,
    ["Background/Secondary"] = backgroud.Secondary,
    ["Background/Tertiary"] = backgroud.Tertiary,
    ["Background/Stroke"] = backgroud.Stroke,

    ["Text/Title"] = text.Title,
    ["Text/Body"] = text.Body,

    ["Accent/Primary"] = accent.Primary,
    ["Accent/Secondary"] = accent.Secondary,
    ["Accent/Destructive"] = accent.Destructive,
  }
end
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local ThemeForScopes: { [Fusion.Scope]: Fusion.Value<Types.ThemeFlatConfig> } = {}
local DefaultDarkTheme = FlattenThemeConfig(DefaultThemes.Dark)

local peek = Fusion.peek
-------------------------------------------------------------------------------------------------------

--> Functions -----------------------------------------------------------------------------------------
local function GetThemeForScope(scope: Fusion.Scope, useOrPeek: Fusion.Use)
  return useOrPeek(ThemeForScopes[scope]) or DefaultDarkTheme
end

local function ApplyHoverFeedbackMode(scope: Fusion.Scope, color: Color3)
  local theme = GetThemeForScope(scope, peek)

  return color:Lerp(theme["Background/Primary"], .25)
end

local function ApplyFocusFeedbackMode(scope: Fusion.Scope, color: Color3)
  local theme = GetThemeForScope(scope, peek)

  return color:Lerp(theme["Background/Primary"], .35)
end

local function ApplyHighlightFeedbackMode(scope: Fusion.Scope, color: Color3)
  return color:Lerp(Color3.new(1,1,1), .1)
end

local function SetThemeForScope(scope: Fusion.Scope, theme: Types.ThemeConfig | DefaultThemes.DefaultTheme)
  local flatTheme =
    if theme == "Dark" then DefaultDarkTheme
    else FlattenThemeConfig(theme)

  -- Makes sure the theme is cleaned up when the scope is destroyed.
  if not ThemeForScopes[scope] then
    table.insert(scope, function() ThemeForScopes[scope] = nil end)

    ThemeForScopes[scope] = scope:Value(flatTheme)
  end

  ThemeForScopes[scope]:set(flatTheme)
end

local function GetThemeItemForScope(
  scope: Fusion.Scope, item: Fusion.UsedAs<Types.ThemeItem>,
  mutator: Fusion.UsedAs<Types.MutatorPreset | (self: Color3) -> Color3 | nil>?,
  twInfo: Fusion.UsedAs<TweenInfo?>? | false
)
  if twInfo == nil then twInfo = TweenInfo.new(.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) end

  local computed = scope:Computed(function(use)
    local usedItem = use(item)
    local color = if typeof(usedItem) == "Color3" then usedItem else GetThemeForScope(scope, use)[usedItem]

    local usedMutator = use(mutator)
    if (not usedMutator) or usedMutator == "Default" then return color end

    return
      if usedMutator == "Hover" then ApplyHoverFeedbackMode(scope, color)
      elseif usedMutator == "Highlight" then ApplyHighlightFeedbackMode(scope, color)
      elseif usedMutator == "Focus" then ApplyFocusFeedbackMode(scope, color)
      elseif typeof(usedMutator) == "function" then usedMutator(color)
      else color
  end) :: Fusion.Computed<Color3>

  return
    if twInfo == false then computed
    else scope:Tween(computed, twInfo :: TweenInfo)
end
-------------------------------------------------------------------------------------------------------

return {
  SetThemeForScope = SetThemeForScope,
  GetThemeItemForScope = GetThemeItemForScope
}


--!strict

--> Modules -------------------------------------------------------------------------------------------
local Packages = script.Parent.Parent.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local peek = Fusion.peek
-------------------------------------------------------------------------------------------------------

--> Private Functions ---------------------------------------------------------------------------------
local function Track(inst: Instance, propName: string, trackedProp: Fusion.Value<any>): RBXScriptConnection?
	if not inst then return end

	trackedProp:set((inst :: any)[propName])

	return inst:GetPropertyChangedSignal(propName):Connect(function()
		trackedProp:set((inst :: any)[propName])
	end)
end
-------------------------------------------------------------------------------------------------------

return function(scope: Fusion.Scope<typeof(Fusion)>, inst: Instance? | Fusion.Value<Instance>, propName: string | Fusion.Value<string>): Fusion.Value<any>
	local trackedProp = scope:Value((inst :: any)[propName])

	if typeof(inst) == "Instance" then
		Track(inst, peek(propName), trackedProp)

	else
		local instIsState = type(inst) == "table" and inst.dependentSet
		local propNameIsState = type(propName) == "table" and propName.dependentSet

		local event: RBXScriptConnection? = nil

		if instIsState then
			scope:Observer(inst :: Fusion.Value<Instance>):onChange(function()
				if event then event:Disconnect() end
				event = Track(peek(inst), peek(propName), trackedProp)
			end)
		end

		if propNameIsState then
			scope:Observer(propName :: Fusion.Value<string>):onChange(function()
				if event then event:Disconnect() end
				event = Track(peek(inst), peek(propName), trackedProp)
			end)
		end
	end

	return trackedProp
end


--!strict

--> Services ------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------

--> Modules -------------------------------------------------------------------------------------------
local Modules = script.Modules
local RootEventHandlersUtils = require(Modules.RootEventHandlersUtils)
local ThemeHandler = require(Modules.ThemeHandler)

local Components = script.Components
local CoreComponents = Components.Core
local PaddingComponents = Components.Padding

local Packages = script.Packages
local Fusion = require(Packages.Fusion)
-------------------------------------------------------------------------------------------------------

--> Variables -----------------------------------------------------------------------------------------
local AddRootEvent = RootEventHandlersUtils.AddRootEvent

local innerScope = Fusion.innerScope
-------------------------------------------------------------------------------------------------------

return {
  SetTheme = ThemeHandler.SetThemeForScope,
  GetThemeItem = ThemeHandler.GetThemeItemForScope,

  AddRootEvent = AddRootEvent,

  -- Plugins.
  Widget = require(Components.Widget),

  -- Wrappers.
  Accordion = require(Components.Accordion),
  Background = require(Components.Background),
  Container = require(Components.Container),
  EmbeddedContainer = require(Components.EmbeddedContainer),
  ImageBackground = require(Components.ImageBackground),
  Map = require(Components.Map),

  -- ...ables
  Scrollable = require(Components.Scrollable),

  -- Labels.
  TextLabel = require(CoreComponents.TextLabel),
  CodeLabel = require(CoreComponents.CodeLabel),

  -- Inputs.
  TextInput = require(Components.TextInput),

  -- Buttons.
  Button = require(Components.Button),
  Checkbox = require(Components.Checkbox),
  Slider = require(Components.Slider),
  Switch = require(Components.Switch),
  Navigation = require(Components.Navigation),
  Tabs = require(Components.Tabs),

  -- Other
  Progress = require(Components.Progress),

  -- Padding.
  PaddingSmall = require(PaddingComponents.PaddingSmall),
  PaddingLarge = require(PaddingComponents.PaddingLarge),
}


local STAGES = {
	"Created",
	"Initialized",
	"Started",
	"Destroyed",
}

local function NewBootloader(ModuleScripts: { ModuleScript })
	local Bootloader = {
		Stage = STAGES[1],
		Systems = {},
	}

	function Bootloader:Destroy()
		assert(Bootloader.Stage == STAGES[3], `Must be at stage {STAGES[3]}.`)
		Bootloader.Stage = STAGES[4]

		for _, System in ipairs(Bootloader.Systems) do
			if System.Destroy ~= nil then
				task.spawn(function()
					System:Destroy()
				end)
			end
		end
	end

	function Bootloader:Start()
		assert(Bootloader.Stage == STAGES[2], `Must be at stage {STAGES[2]}.`)
		Bootloader.Stage = STAGES[3]

		for _, System in ipairs(Bootloader.Systems) do
			if System.Start ~= nil then
				task.spawn(function()
					System:Start()
				end)
			end
		end
	end

	function Bootloader:Initialize()
		assert(Bootloader.Stage == STAGES[1], `Must be at stage {STAGES[1]}.`)
		Bootloader.Stage = STAGES[2]

		for _, ModuleScript in ipairs(ModuleScripts) do
			table.insert(Bootloader.Systems, require(ModuleScript))
		end
	end

	return Bootloader
end

return NewBootloader


local Players = game:GetService("Players")
local StarterGui = game:GetService("StarterGui")
local TextChatService = game:GetService("TextChatService")

local MouseLockController = Players.LocalPlayer.PlayerScripts
	:WaitForChild("PlayerModule")
	:WaitForChild("CameraModule")
	:WaitForChild("MouseLockController")

local CHAT_WINDOW_THEME = {
	BackgroundColor3 = Color3.fromRGB(0, 0, 0),
	BackgroundTransparency = 0.3,
	TextSize = 15,
	TextStrokeTransparency = 0.6,
}
local CHAT_WINDOW_INPUT_THEME = {
	BackgroundTransparency = 1,
	TextColor3 = Color3.fromRGB(205, 205, 205),
	TextSize = 15,
	TextStrokeTransparency = 1,
}
local BUBBLE_CHAT_THEME = {
	BackgroundColor3 = Color3.fromRGB(20, 20, 20),
	BackgroundTransparency = 0.2,
	TextColor3 = Color3.fromRGB(205, 205, 205),
	TextSize = 17,
}
local SHIFT_LOCK_KEYS = "LeftControl"
local AVATAR_CONTEXT_MENU_THEME = {
	BackgroundTransparency = 1,
	BackgroundImage = "rbxassetid://16819084929",
	BackgroundImageSliceCenter = Rect.new(12, 12, 12, 12),
	BackgroundImageTransparency = 0.3,
	NameTagColor = Color3.fromRGB(20, 20, 20),
	ButtonFrameTransparency = 1,
	ButtonColor = Color3.fromRGB(255, 255, 255),
	ButtonTransparency = 1,
	ButtonHoverTransparency = 0.9,
}
local SELECTION_INDICATOR_OPTIONS = {
	Name = "Indicator",
	Locked = true,
	CanCollide = false,
	Anchored = true,
	Color = Color3.fromRGB(170, 170, 170),
	Material = Enum.Material.Neon,
	Size = Vector3.new(0.45, 0.45, 0.45),
}

type AvatarContextMenuButton = {}

local CoreThemer = {
	_AvatarSelectionHighlight = Instance.new("Highlight"),
	_AvatarSelectionConnection = nil,
}

function CoreThemer:SetDefaults()
	self:SetChatWindowTheme()
	self:SetChatWindowInputTheme()
	self:SetBubbleChatTheme()
	self:SetShiftLockKeys()
	self:SetAvatarContextMenuTheme()
end

function CoreThemer:SetAvatarContextMenuEnabled(Enabled: boolean)
	if Enabled == nil then
		Enabled = true
	end

	self:CallCore("SetCore", "AvatarContextMenuEnabled", Enabled)

	if Enabled then
		self:_StartAvatarSelectionHandler()
	else
		self:_StopAvatarSelectionHandler()
	end
end

function CoreThemer:SetAvatarContextMenuTheme(Options: { [string]: any }?)
	if Options == nil then
		Options = AVATAR_CONTEXT_MENU_THEME

		if Options ~= nil then
			local SelectionIndicator = Instance.new("Part")
			self:_SetProperties(SelectionIndicator, SELECTION_INDICATOR_OPTIONS)
			Options.SelectedCharacterIndicator = SelectionIndicator
		end
	end

	self:CallCore("SetCore", "AvatarContextMenuTheme", Options)
end

function CoreThemer:SetShiftLockKeys(Keys: string?)
	if Keys == nil then
		Keys = SHIFT_LOCK_KEYS
	end

	local BoundKeys = MouseLockController:FindFirstChild("BoundKeys")
	if BoundKeys then
		BoundKeys.Value = Keys
	else
		BoundKeys = Instance.new("StringValue")
		BoundKeys.Name = "BoundKeys"
		BoundKeys.Value = Keys
		BoundKeys.Parent = MouseLockController
	end
end

function CoreThemer:SetChatWindowTheme(Options: { [string]: any }?)
	if Options == nil then
		Options = CHAT_WINDOW_THEME
	end

	self:_SetProperties(TextChatService.ChatWindowConfiguration, Options)
end

function CoreThemer:SetChatWindowInputTheme(Options: { [string]: any }?)
	if Options == nil then
		Options = CHAT_WINDOW_INPUT_THEME
	end

	self:_SetProperties(TextChatService.ChatInputBarConfiguration, Options)
end

function CoreThemer:SetBubbleChatTheme(Options: { [string]: any }?)
	if Options == nil then
		Options = BUBBLE_CHAT_THEME
	end

	self:_SetProperties(TextChatService.BubbleChatConfiguration, Options)

	if Options == nil then
		local UICorner = TextChatService.BubbleChatConfiguration:FindFirstChild("UICorner")
		if UICorner then
			UICorner.CornerRadius = UDim.new(0, 16)
		end
	end
end

function CoreThemer:CallCore(Method, ...)
	local Result = {}

	for _ = 1, 8 do
		Result = { pcall(StarterGui[Method], StarterGui, ...) }
		if Result[1] then
			break
		end
		task.wait()
	end

	return unpack(Result)
end

function CoreThemer:_StartAvatarSelectionHandler()
	self:_StopAvatarSelectionHandler()

	local Mouse = Players.LocalPlayer:GetMouse()
	self._AvatarSelectionConnection = Mouse.Move:Connect(function()
		self._AvatarSelectionHighlight.Adornee = nil

		local Target = Mouse.Target
		if Target then
			local Character = Target:FindFirstAncestorOfClass("Model")
			if Character then
				local Player = Players:GetPlayerFromCharacter(Character)
				if Player then
					self._AvatarSelectionHighlight.Adornee = Character
				end
			end
		end
	end)
end

function CoreThemer:_StopAvatarSelectionHandler()
	if self._AvatarSelectionConnection then
		self._AvatarSelectionConnection:Disconnect()
		self._AvatarSelectionConnection = nil
	end

	self._AvatarSelectionHighlight.Adornee = nil
end

function CoreThemer:_SetProperties(Instance: Instance, Properties: { [string]: any })
	for Property, Value in pairs(Properties) do
		Instance[Property] = Value
	end
end

function CoreThemer:_Initialize()
	self._AvatarSelectionHighlight.Parent = Players.LocalPlayer.PlayerGui
	self._AvatarSelectionHighlight.FillTransparency = 1
	self._AvatarSelectionHighlight.OutlineTransparency = 0.1
	self._AvatarSelectionHighlight.DepthMode = Enum.HighlightDepthMode.Occluded
end

CoreThemer:_Initialize()

return CoreThemer


export type SerEnumItem = {
	EnumType: string,
	Value: number,
}

local TableUtil = require(script.Parent.TableUtil)

local EnumSerDes = {}

function EnumSerDes.DeserializeTree(Tree)
	local NewTree = TableUtil.Copy(Tree, true)

	for Key, Value in pairs(Tree) do
		if typeof(Value) == "table" then
			if (typeof(Value.EnumType) == "string") and (typeof(Value.Value) == "number") then
				local EnumItem = EnumSerDes.Deserialize(Value)
				if EnumItem ~= nil then
					NewTree[Key] = EnumItem
				end
			else
				NewTree[Key] = EnumSerDes.DeserializeTree(Value)
			end
		end
	end

	return NewTree
end

function EnumSerDes.Deserialize(SerEnumItem: SerEnumItem): EnumItem?
	local EnumObject: Enum = Enum[SerEnumItem.EnumType]

	if EnumObject then
		local EnumItems: { EnumItem } = EnumObject:GetEnumItems()

		for _, EnumItem in pairs(EnumItems) do
			if EnumItem.Value == SerEnumItem.Value then
				return EnumItem
			end
		end
	end

	return nil
end

function EnumSerDes.SerializeTree(Tree)
	local NewTree = TableUtil.Copy(Tree, true)

	for Key, Value in pairs(Tree) do
		if typeof(Value) == "EnumItem" then
			NewTree[Key] = EnumSerDes.Serialize(Value)
		elseif typeof(Value) == "table" then
			NewTree[Key] = EnumSerDes.SerializeTree(Value)
		end
	end

	return NewTree
end

function EnumSerDes.Serialize(EnumItem: EnumItem): SerEnumItem
	return {
		EnumType = tostring(EnumItem.EnumType),
		Value = EnumItem.Value,
	}
end

return EnumSerDes


return {
	Brick = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Climb = {
		SoundId = "rbxassetid://145180175",
		Volume = 0.3,
	},
	Cobblestone = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Concrete = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	CorrodedMetal = {
		SoundId = "rbxassetid://16829038373",
		Volume = 1.2,
	},
	DiamondPlate = {
		SoundId = "rbxassetid://16829038373",
		Volume = 1.2,
	},
	Fabric = {
		SoundId = "rbxassetid://16829153060",
		Volume = 0.7,
	},
	Carpet = {
		SoundId = "rbxassetid://16829153060",
		Volume = 0.7,
	},
	Foil = {
		SoundId = "rbxassetid://16829153060",
		Volume = 0.8,
	},
	ForceField = {
		SoundId = "rbxassetid://16829153060",
		Volume = 0.8,
	},
	Glass = {
		SoundId = "rbxassetid://16829038373",
		Volume = 1.2,
	},
	Granite = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Pavement = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Salt = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Basalt = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Asphalt = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Grass = {
		SoundId = "rbxassetid://16829038667",
		Volume = 0.35,
	},
	Ice = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	LeafyGrass = {
		SoundId = "rbxassetid://16829038202",
		Volume = 0.35,
	},
	Marble = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Metal = {
		SoundId = "rbxassetid://16829038373",
		Volume = 1.2,
	},
	Neon = {
		SoundId = "rbxassetid://16829153060",
		Volume = 0.8,
	},
	Pebble = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Plastic = {
		SoundId = "rbxassetid://16829153060",
		Volume = 0.8,
	},
	Ground = {
		SoundId = "rbxassetid://16829153060",
		Volume = 0.8,
	},
	Rubber = {
		SoundId = "rbxassetid://16829153060",
		Volume = 0.8,
	},
	Sand = {
		SoundId = "rbxassetid://16829037901",
		Volume = 0.35,
	},
	Slate = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	SmoothPlastic = {
		SoundId = "rbxassetid://16829153060",
		Volume = 0.8,
	},
	Snow = {
		SoundId = "rbxassetid://16829038510",
		Volume = 0.3,
	},
	Wood = {
		SoundId = "rbxassetid://16829037092",
		Volume = 0.45,
	},
	WoodPlanks = {
		SoundId = "rbxassetid://16829037092",
		Volume = 0.45,
	},
	Limestone = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Mud = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	Sandstone = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
	CrackedLava = {
		SoundId = "rbxassetid://16829037315",
		Volume = 0.4,
	},
}


local Players = game:GetService("Players")

local DefaultSounds = require(script.DefaultSounds)

local BLACKLISTEED_MATERIALS = { Enum.Material.Air, Enum.Material.Water }

local Footstepper = {
	BaseVolume = 5,
}

function Footstepper:UpdateSounds(Sounds: { Sound }, SoundProperties: { [string]: any })
	for _, Sound in ipairs(Sounds) do
		local SoundInfo = SoundProperties[Sound.Name]
		if SoundInfo then
			for Name, Value in SoundInfo do
				if Sound[Name] then
					Sound[Name] = Value
				end
			end
		end
	end
end

function Footstepper:_HandleCharacter(Character: Model)
	local HumanoidRootPart = Character:FindFirstChild("HumanoidRootPart")
	local Humanoid = Character:FindFirstChild("Humanoid")

	if Humanoid and HumanoidRootPart then
		local Sounds = self:_SetUpSounds(HumanoidRootPart)

		local DefaultSound = HumanoidRootPart:FindFirstChild("Running")
		if DefaultSound then
			DefaultSound:Destroy()
		end

		local FloorMaterial: Enum.Material = Enum.Material.Plastic
		local WalkSpeed: number = 0
		local LastFloorMaterial: Enum.Material = Enum.Material.Plastic
		local LastWalkSpeed: number = 0

		Humanoid.Running:Connect(function(Speed: number)
			WalkSpeed = Speed

			if WalkSpeed ~= LastWalkSpeed then
				self:_UpdatePlayingSound(Sounds, WalkSpeed, FloorMaterial)
			end
			LastWalkSpeed = WalkSpeed
		end)
		Humanoid:GetPropertyChangedSignal("FloorMaterial"):Connect(function()
			FloorMaterial = Humanoid.FloorMaterial

			if FloorMaterial ~= LastFloorMaterial then
				self:_UpdatePlayingSound(Sounds, WalkSpeed, FloorMaterial)
			end
			LastFloorMaterial = FloorMaterial
		end)
	end
end

function Footstepper:_UpdatePlayingSound(Sounds: Attachment, WalkSpeed: number, FloorMaterial: Enum.Material)
	for _, Sound in ipairs(Sounds:GetChildren()) do
		Sound.Playing = false
		Sound.Looped = false
	end

	if WalkSpeed >= WalkSpeed / 2 then
		local Sound = Sounds:FindFirstChild(string.split(tostring(FloorMaterial), "Enum.Material.")[2])
		if Sound == nil and table.find(BLACKLISTEED_MATERIALS, FloorMaterial) == nil then
			Sound = Sounds:FindFirstChild("Plastic")
		end

		if Sound then
			Sound.PlaybackSpeed = WalkSpeed / 13
			Sound.Playing = true
			Sound.Looped = true
		end
	end
end

function Footstepper:_SetUpSounds(HumanoidRootPart: BasePart)
	local SoundGroup = Instance.new("SoundGroup")
	SoundGroup.Name = "FootstepSounds"
	SoundGroup.Volume = self.BaseVolume

	local Sounds = Instance.new("Attachment")
	Sounds.Name = "Sounds"
	Sounds.Parent = HumanoidRootPart

	for Name, _ in pairs(DefaultSounds) do
		local Sound = Instance.new("Sound")
		Sound.Name = Name
		Sound.Looped = true
		Sound.RollOffMaxDistance = 50
		Sound.RollOffMinDistance = 5
		Sound.SoundGroup = SoundGroup
		Sound.Parent = Sounds
	end

	SoundGroup.Parent = HumanoidRootPart

	self:UpdateSounds(Sounds:GetChildren(), DefaultSounds)

	return Sounds
end

function Footstepper:_HandlePlayer(Player: Player)
	Player.CharacterAdded:Connect(function(Character)
		self:_HandleCharacter(Character)
	end)

	if Player.Character then
		self:_HandleCharacter(Player.Character)
	end
end

function Footstepper:Start()
	Players.PlayerAdded:Connect(function(Player)
		self:_HandlePlayer(Player)
	end)

	for _, Player in ipairs(Players:GetPlayers()) do
		self:_HandlePlayer(Player)
	end
end

return Footstepper


local AvatarAssetTypes = {
	Types = {
		RigidAccessories = {
			Enum.AvatarAssetType.BackAccessory,
			Enum.AvatarAssetType.FaceAccessory,
			Enum.AvatarAssetType.HairAccessory,
			Enum.AvatarAssetType.NeckAccessory,
			Enum.AvatarAssetType.FrontAccessory,
			Enum.AvatarAssetType.WaistAccessory,
			Enum.AvatarAssetType.ShoulderAccessory,
			Enum.AvatarAssetType.Hat,
		},
		LayeredClothing = {
			Enum.AvatarAssetType.PantsAccessory,
			Enum.AvatarAssetType.ShirtAccessory,
			Enum.AvatarAssetType.TShirtAccessory,
			Enum.AvatarAssetType.ShortsAccessory,
			Enum.AvatarAssetType.DressSkirtAccessory,
			Enum.AvatarAssetType.JacketAccessory,
			Enum.AvatarAssetType.SweaterAccessory,
			Enum.AvatarAssetType.RightShoeAccessory,
			Enum.AvatarAssetType.LeftShoeAccessory,
		},
		ClassicClothing = {
			Enum.AvatarAssetType.Shirt,
			Enum.AvatarAssetType.TShirt,
			Enum.AvatarAssetType.Pants,
		},
		Emotes = {
			Enum.AvatarAssetType.EmoteAnimation,
		},
		Animations = {
			Enum.AvatarAssetType.RunAnimation,
			Enum.AvatarAssetType.FallAnimation,
			Enum.AvatarAssetType.IdleAnimation,
			Enum.AvatarAssetType.WalkAnimation,
			Enum.AvatarAssetType.SwimAnimation,
			Enum.AvatarAssetType.ClimbAnimation,
			Enum.AvatarAssetType.JumpAnimation,
			Enum.AvatarAssetType.MoodAnimation,
		},
		BodyParts = {
			Enum.AvatarAssetType.Torso,
			Enum.AvatarAssetType.LeftArm,
			Enum.AvatarAssetType.RightArm,
			Enum.AvatarAssetType.LeftLeg,
			Enum.AvatarAssetType.RightLeg,
			Enum.AvatarAssetType.Head,
			Enum.AvatarAssetType.DynamicHead,
		},
		ClassicFaces = {
			Enum.AvatarAssetType.Face,
		},
		FacialFeatures = {
			Enum.AvatarAssetType.EyebrowAccessory,
			Enum.AvatarAssetType.EyelashAccessory,
		},
		Gear = {
			Enum.AvatarAssetType.Gear,
		},
	},
}

function AvatarAssetTypes:Combine(AssetsTypeLists: { { Enum.AvatarAssetType } }): { Enum.AvatarAssetType }
	local CombinedAssetTypes = {}

	for _, AssetTypes in ipairs(AssetsTypeLists) do
		for _, AssetType in ipairs(AssetTypes) do
			if table.find(CombinedAssetTypes, AssetType) == nil then
				table.insert(CombinedAssetTypes, AssetType)
			end
		end
	end

	return CombinedAssetTypes
end

function AvatarAssetTypes:Flatten(AssetTypeCategories: { [string]: { Enum.AvatarAssetType } })
	local AssetsTypeLists = {}

	for _, AssetsTypeList in pairs(AssetTypeCategories) do
		table.insert(AssetsTypeLists, AssetsTypeList)
	end

	return AssetsTypeLists
end

AvatarAssetTypes.Types.All = AvatarAssetTypes:Combine(AvatarAssetTypes:Flatten(AvatarAssetTypes.Types))
AvatarAssetTypes.Types.Clothing =
	AvatarAssetTypes:Combine({ AvatarAssetTypes.Types.ClassicClothing, AvatarAssetTypes.Types.LayeredClothing })
AvatarAssetTypes.Types.Accessories = AvatarAssetTypes:Combine({
	AvatarAssetTypes.Types.RigidAccessories,
	AvatarAssetTypes.Types.LayeredClothing,
	AvatarAssetTypes.Types.FacialFeatures,
})

return AvatarAssetTypes