--!strict

local root = script.Parent.Parent.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local Setting = require(script.components.Setting);
local useStudioColors = require(root.DialogueEditor.hooks.useStudioColors);
local useChangeHistory = require(root.DialogueEditor.hooks.useChangeHistory);
local settingsMetadata = require(script.Parent.Parent.settingsMetadata);

type SettingsMetadataGroup = settingsMetadata.SettingMetadataGroup;

export type SettingsContainerProperties = {
  metadataGroup: SettingsMetadataGroup;
  settingContainer: Instance?;
  currentSettings: { [string]: {[string]: any} };
  settingsTarget: ModuleScript;
  layoutOrder: number;
}

local function SettingGroup(properties: SettingsContainerProperties)

  local metadataGroup = properties.metadataGroup;
  local settingContainer = properties.settingContainer;
  local currentSettings = properties.currentSettings;
  local settingsTarget = properties.settingsTarget;
  local layoutOrder = properties.layoutOrder;
  local colors = useStudioColors();
  local beginHistoryRecording, finishHistoryRecording = useChangeHistory();

  local settingComponents = {};

  for _, settingMetadata in metadataGroup.settings do

    local currentValue = if currentSettings[metadataGroup.name] then currentSettings[metadataGroup.name][settingMetadata.name] else nil;
    local settingComponent = React.createElement(Setting, {
      key = `{metadataGroup.name}.{settingMetadata.name}`;
      settingMetadata = settingMetadata;
      settingsTarget = settingsTarget;
      layoutOrder = #settingComponents + 1;
      value = currentValue;
      onReset = function()

        if not settingContainer then

          return;

        end;

        local categoryFolder = settingContainer:FindFirstChild(metadataGroup.name);
        if not categoryFolder then

          return;

        end;

        local settingInstance = categoryFolder:FindFirstChild(settingMetadata.name);
        if not settingInstance then

          return;

        end;

        local historyIdentifier = beginHistoryRecording(`Reset setting {metadataGroup.name}.{settingMetadata.name} to default`);

        settingInstance.Parent = nil;

        if #categoryFolder:GetChildren() == 0 then

          categoryFolder.Parent = nil;

        end;

        if #settingContainer:GetChildren() == 0 then

          settingContainer.Parent = nil;

        end;

        finishHistoryRecording(historyIdentifier);

      end;
      onChanged = function(newValue)

        local historyIdentifier = beginHistoryRecording(`Edit setting {metadataGroup.name}.{settingMetadata.name}`);

        local didChange, errorMessage = pcall(function()

          if settingMetadata.enum and newValue ~= "" then

            local enum = settingMetadata.enum:FromName(newValue);
            if not enum then

              error(`{newValue} is not valid for enum {settingMetadata.enum}`, 0);

            end;

          end;

          -- Create the settings folder if it doesn't exist.
          if not settingContainer then

            local newSettingsContainer = Instance.new("Folder");
            newSettingsContainer.Name = "Settings";
            newSettingsContainer.Parent = settingsTarget;
            settingContainer = newSettingsContainer;

          end;

          assert(settingContainer, "Settings folder not found even after creating it.");

          -- Create the category folder if it doesn't exist.
          local categoryFolder = settingContainer:FindFirstChild(metadataGroup.name);
          if not categoryFolder then

            local newCategoryFolder = Instance.new("Folder");
            newCategoryFolder.Name = metadataGroup.name;
            newCategoryFolder.Parent = settingContainer;
            categoryFolder = newCategoryFolder;

          end;

          assert(categoryFolder, "Category folder not found even after creating it.");

          -- Create the setting instance if it doesn't exist.
          local settingInstance = categoryFolder:FindFirstChild(settingMetadata.name);
          if not settingInstance then

            local newSettingInstance: Instance;

            if settingMetadata.type == "boolean" then

              newSettingInstance = Instance.new("BoolValue");

            elseif settingMetadata.type == "number" then

              newSettingInstance = Instance.new("NumberValue");

            elseif settingMetadata.type == "string" then

              newSettingInstance = Instance.new("StringValue");

              if settingMetadata.enum then

                newSettingInstance:SetAttribute("Enum", tostring(settingMetadata.enum));

              end;

            elseif settingMetadata.type == "Instance" then

              newSettingInstance = Instance.new("ObjectValue");

            else

              error(`Unsupported setting type: {settingMetadata.type}`);

            end;

            newSettingInstance.Name = settingMetadata.name;
            newSettingInstance.Parent = categoryFolder;
            settingInstance = newSettingInstance;

          end;

          assert(settingInstance, "Setting instance not found even after creating it.");

          -- Update the value of the setting instance.
          if newValue == nil then

            settingInstance.Parent = nil;

            if #categoryFolder:GetChildren() == 0 then

              categoryFolder.Parent = nil;

            end;

          elseif settingInstance:IsA("BoolValue") then

            assert(typeof(newValue) == "boolean", "Expected newValue to be a boolean.");
            settingInstance.Value = newValue;

          elseif settingInstance:IsA("StringValue") then

            assert(typeof(newValue) == "string", "Expected newValue to be a string.");
            settingInstance.Value = newValue;

          elseif settingInstance:IsA("NumberValue") then

            assert(typeof(newValue) == "number", "Expected newValue to be a number.");
            settingInstance.Value = newValue;

          elseif settingInstance:IsA("ObjectValue") then

            assert(typeof(newValue) == "Instance" or newValue == nil, "Expected newValue to be an Instance or nil.");
            settingInstance.Value = newValue;

          end;

          finishHistoryRecording(historyIdentifier);

        end);

        if not didChange then

          finishHistoryRecording(historyIdentifier, Enum.FinishRecordingOperation.Cancel);
          error(`Failed to change setting {metadataGroup.name}.{settingMetadata.name}: {errorMessage}`, 0);

        end;

      end;
    });

    table.insert(settingComponents, settingComponent);

  end;

  return React.createElement("Frame", {
    AutomaticSize = Enum.AutomaticSize.Y;
    BackgroundTransparency = 1;
    LayoutOrder = layoutOrder;
    Size = UDim2.fromScale(1, 0);
  }, {
    UIListLayout = React.createElement("UIListLayout", {
      SortOrder = Enum.SortOrder.LayoutOrder;
      Padding = UDim.new(0, 10);
    });
    Header = React.createElement("TextLabel", {
      AutomaticSize = Enum.AutomaticSize.XY;
      Text = `{metadataGroup.name:sub(1, 1):upper()}{metadataGroup.name:sub(2)} settings`;
      TextSize = 14;
      FontFace = Font.fromName("BuilderSans", Enum.FontWeight.Bold);
      LayoutOrder = 1;
      BackgroundTransparency = 1;
      TextXAlignment = Enum.TextXAlignment.Left;
      TextWrapped = true;
      TextColor3 = colors.text;
    });
    Settings = React.createElement("Frame", {
      AutomaticSize = Enum.AutomaticSize.Y;
      BackgroundTransparency = 1;
      LayoutOrder = 2;
      Size = UDim2.fromScale(1, 0);
    }, {
      UIListLayout = React.createElement("UIListLayout", {
        SortOrder = Enum.SortOrder.LayoutOrder;
        Padding = UDim.new(0, 10);
      });
      Content = React.createElement(React.Fragment, {}, settingComponents);
    });
  });

end;

return React.memo(SettingGroup);


--!strict

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

local root = script.Parent.Parent.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local TabSelector = require(root.DialogueEditor.components.TabSelector);
local TabSelectorButton = require(root.DialogueEditor.components.TabSelector.TabSelectorButton);

export type SettingTypeSelectorProperties = {
  settingsTarget: ModuleScript;
  initialSettingsTarget: ModuleScript;
  onSelectionChanged: (settingsTarget: ModuleScript) -> ();
};

local function SettingsTabSelector(properties: SettingTypeSelectorProperties)

  local initialSettingsTarget = properties.initialSettingsTarget;
  local settingsTarget = properties.settingsTarget;
  local onSelectionChanged = properties.onSelectionChanged;

  local settingsTargetType = (
    if settingsTarget:HasTag("DialogueMakerLoader") then "Client"
    elseif settingsTarget:HasTag("DialogueMakerConversationScript") then "Conversation"
    elseif settingsTarget:HasTag("DialogueMakerDialogueScript") then "Dialogue"
    else error(`Invalid settings target provided: {settingsTarget.Name}. Expected a DialogueMaker script.`)
  );

  local loaderScripts = CollectionService:GetTagged("DialogueMakerLoader");
  local loaderScript = nil;
  for _, possibleLoaderScript in loaderScripts do

    if not possibleLoaderScript:IsDescendantOf(root) then

      loaderScript = possibleLoaderScript;
      break;

    end

  end

  local buttons = {};
  local settingTypes = {"Client", "Conversation", "Dialogue"};

  for index, settingType in settingTypes do

    local settingTypeComponent = React.createElement(TabSelectorButton, {
      isSelected = settingType == settingsTargetType;
      text = settingType;
      layoutOrder = index;
      isDisabled = (settingType == "Client" and not loaderScript) or (settingType == "Dialogue" and not initialSettingsTarget:HasTag("DialogueMakerDialogueScript"));
      onSelected = function()

        if settingType == "Client" then

          onSelectionChanged(loaderScript);

        elseif settingType == "Conversation" then

          local possibleConversationScript = initialSettingsTarget;

          while not possibleConversationScript:HasTag("DialogueMakerConversationScript") and possibleConversationScript.Parent and possibleConversationScript.Parent:IsA("Folder") and possibleConversationScript.Parent.Parent and possibleConversationScript.Parent.Parent:IsA("ModuleScript") do

            possibleConversationScript = possibleConversationScript.Parent.Parent;

          end;

          if not possibleConversationScript:HasTag("DialogueMakerConversationScript") then

            error(`No valid conversation script found for {initialSettingsTarget.Name}.`);

          end

          onSelectionChanged(possibleConversationScript);

        elseif settingType == "Dialogue" then

          onSelectionChanged(initialSettingsTarget);

        end;

      end;
    });

    table.insert(buttons, settingTypeComponent);

  end;

  return React.createElement(TabSelector, {}, buttons);

end;

return React.memo(SettingsTabSelector);


--!strict

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local SettingGroup = require(script.components.SettingGroup);
local settingsMetadata = require(script.settingsMetadata);
local SettingsTabSelector = require(script.components.SettingsTabSelector);
local useRefreshDialogueMakerScripts = require(root.DialogueEditor.hooks.useRefreshDialogueMakerScripts);
local useStudioColors = require(root.DialogueEditor.hooks.useStudioColors);

export type SettingsProperties = {
  initialSettingsTarget: ModuleScript;
  layoutOrder: number;
}

function Settings(properties: SettingsProperties)

  local settingsTarget, setSettingsTarget = React.useState(properties.initialSettingsTarget);
  local layoutOrder = properties.layoutOrder;
  local colors = useStudioColors();

  local targetType = React.useMemo(function()

    if settingsTarget:HasTag("DialogueMakerLoader") then

      return "loader";

    elseif settingsTarget:HasTag("DialogueMakerConversationScript") then

      return "conversation";

    elseif settingsTarget:HasTag("DialogueMakerDialogueScript") then

      return "dialogue";

    else

      error(`Invalid settings target provided: {settingsTarget.Name}. Expected a DialogueMaker script.`);

    end;

  end, { settingsTarget });

  local getSettingsContainer = React.useCallback(function()

    return settingsTarget:FindFirstChild("Settings");

  end, { settingsTarget });

  local getCurrentSettings = React.useCallback(function(): { [string]: {[string]: any} }

    local settingsContainer = getSettingsContainer();

    if not settingsContainer then

      return {};

    end;

    local currentSettings = {};

    for _, categoryFolder in settingsContainer:GetChildren() do

      if categoryFolder:IsA("Folder") then

        currentSettings[categoryFolder.Name] = currentSettings[categoryFolder.Name] or {};

        for _, settingInstance in categoryFolder:GetChildren() do

          if settingInstance:IsA("BoolValue") or settingInstance:IsA("ObjectValue") or settingInstance:IsA("NumberValue") or settingInstance:IsA("StringValue") then

            currentSettings[categoryFolder.Name][settingInstance.Name] = settingInstance.Value;

          end;

        end;

      end;

    end;

    return currentSettings;

  end, { getSettingsContainer });

  local currentSettings, setCurrentSettings = React.useState(getCurrentSettings());
  local metadataGroupMap = settingsMetadata[targetType];
  local settingGroups = {};

  for _, metadataGroup in metadataGroupMap do

    local settingGroup = React.createElement(SettingGroup, {
      metadataGroup = metadataGroup;
      settingContainer = getSettingsContainer();
      currentSettings = currentSettings;
      settingsTarget = settingsTarget;
      key = metadataGroup.name;
      layoutOrder = #settingGroups + 1;
    });

    table.insert(settingGroups, settingGroup);

  end;

  local refreshDialogueMakerScripts = useRefreshDialogueMakerScripts();
  React.useEffect(function(): ()

    refreshDialogueMakerScripts();

    local connections: {RBXScriptConnection} = {};
    local function disconnectConnections()

      for _, connection in connections do

        connection:Disconnect();

      end;

      connections = {};

    end;

    local function refreshCurrentSettings()

      setCurrentSettings(getCurrentSettings());

    end;

    local settingsContainer = getSettingsContainer();
    if settingsContainer then

      table.insert(connections, settingsContainer.ChildAdded:Connect(refreshCurrentSettings));
      table.insert(connections, settingsContainer.ChildRemoved:Connect(refreshCurrentSettings));

      for _, child in settingsContainer:GetChildren() do

        if child:IsA("Folder") then

          table.insert(connections, child.ChildAdded:Connect(refreshCurrentSettings));
          table.insert(connections, child.ChildRemoved:Connect(refreshCurrentSettings));

          for _, settingInstance in child:GetChildren() do

            if settingInstance:IsA("ValueBase") then

              table.insert(connections, settingInstance:GetPropertyChangedSignal("Value"):Connect(refreshCurrentSettings));

            end;

          end;

        end;

      end;

    end;

    table.insert(connections, settingsTarget.ChildAdded:Connect(refreshCurrentSettings));
    table.insert(connections, settingsTarget.ChildRemoved:Connect(refreshCurrentSettings));
    refreshCurrentSettings();

    return function()

      disconnectConnections();

    end;

  end, {getSettingsContainer :: unknown, settingsTarget});

  return React.createElement("ScrollingFrame", {
    Size = UDim2.fromScale(1, 1);
    BackgroundTransparency = 1;
    LayoutOrder = layoutOrder;
    AutomaticCanvasSize = Enum.AutomaticSize.Y;
    CanvasSize = UDim2.fromScale(1, 0);
    ScrollingDirection = Enum.ScrollingDirection.Y;
    BorderSizePixel = 0;
    ScrollBarImageColor3 = colors.border;
  }, {
    UIListLayout = React.createElement("UIListLayout", {
      HorizontalAlignment = Enum.HorizontalAlignment.Center;
    });
    UIFlexItem = React.createElement("UIFlexItem", {
      FlexMode = Enum.UIFlexMode.Shrink;
    });
    Content = React.createElement("Frame", {
      Size = UDim2.fromScale(1, 1);
      AutomaticSize = Enum.AutomaticSize.Y;
      BackgroundTransparency = 1;
    }, {
      UISizeConstraint = React.createElement("UISizeConstraint", {
        MaxSize = Vector2.new(750, math.huge);
      });
      UIPadding = React.createElement("UIPadding", {
        PaddingLeft = UDim.new(0, 15);
        PaddingRight = UDim.new(0, 15);
        PaddingTop = UDim.new(0, 15);
        PaddingBottom = UDim.new(0, 15);
      });
      UIListLayout = React.createElement("UIListLayout", {
        SortOrder = Enum.SortOrder.LayoutOrder;
        Padding = UDim.new(0, 10);
      });
      SettingsTabSelector = React.createElement(SettingsTabSelector, {
        settingsTarget = settingsTarget;
        initialSettingsTarget = properties.initialSettingsTarget;
        onSelectionChanged = function(newTarget: ModuleScript)

          setSettingsTarget(newTarget);

        end;
      });
      Settings = React.createElement(React.Fragment, {}, settingGroups);
    });
  });

end;

return React.memo(Settings);


--!strict

local root = script.Parent.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local useStudioColors = require(root.DialogueEditor.hooks.useStudioColors);

export type TabSelectorButtonProperties = {
  isSelected: boolean;
  isDisabled: boolean?;
  layoutOrder: number;
  text: string;
  onSelected: () -> ();
}

local function TabSelectorButton(properties: TabSelectorButtonProperties)

  local layoutOrder = properties.layoutOrder;
  local text = properties.text;
  local isSelected = properties.isSelected;
  local isDisabled = properties.isDisabled or false;

  local colors = useStudioColors();

  return React.createElement("TextButton", {
    AutomaticSize = Enum.AutomaticSize.XY;
    BackgroundColor3 = if isSelected then colors.primaryButton else colors.toolbar;
    Text = text;
    TextColor3 = colors.text;
    TextTransparency = if isDisabled then 0.5 else 0;
    AutoButtonColor = if isDisabled then false else true;
    TextSize = 12;
    FontFace = Font.fromName("BuilderSans", Enum.FontWeight.Regular);
    LayoutOrder = layoutOrder;
    [React.Event.Activated] = function()

      if not isSelected and not isDisabled then

        properties.onSelected();

      end;

    end;
  }, {
    UIPadding = React.createElement("UIPadding", {
      PaddingLeft = UDim.new(0, 10);
      PaddingRight = UDim.new(0, 10);
      PaddingTop = UDim.new(0, 5);
      PaddingBottom = UDim.new(0, 5);
    });
    UICorner = React.createElement("UICorner", {
      CornerRadius = UDim.new(1, 0);
    });
  });

end;

return React.memo(TabSelectorButton);


--!strict

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);

export type TabSelectorProperties = {
  children: React.ReactNode;
};

local function TabSelector(properties: TabSelectorProperties)

  local children = properties.children;

  return React.createElement("Frame", {
    AutomaticSize = Enum.AutomaticSize.XY;
    BackgroundTransparency = 1;
  }, {
    UIListLayout = React.createElement("UIListLayout", {
      SortOrder = Enum.SortOrder.LayoutOrder;
      Padding = UDim.new(0, 10);
      FillDirection = Enum.FillDirection.Horizontal;
    });
    TabSelectorButtons = React.createElement(React.Fragment, {}, children);
  });

end;

return React.memo(TabSelector);


--!strict

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local useStudioColors = require(root.DialogueEditor.hooks.useStudioColors);

export type TextInputProperties = {
  value: string?;
  placeholder: string?;
  onChanged: (value: string) -> ();
  layoutOrder: number;
}

local function TextInput(properties: TextInputProperties)

  local colors = useStudioColors();
  local placeholderText = properties.placeholder or "";
  local layoutOrder = properties.layoutOrder or 1;
  local onChanged = properties.onChanged;
  local text, setText = React.useState(properties.value or "");

  React.useEffect(function()

    setText(properties.value or "");

  end, { properties.value });

  return React.createElement("TextBox", {
    Text = text;
    TextColor3 = colors.text;
    PlaceholderText = placeholderText;
    TextSize = 14;
    BackgroundTransparency = 0;
    LayoutOrder = layoutOrder;
    FontFace = Font.fromName("BuilderSans", Enum.FontWeight.Regular);
    Size = UDim2.new(1, 0, 0, 30);
    ClearTextOnFocus = false;
    BackgroundColor3 = colors.input;
    [React.Change.Text] = function(self: TextBox)

      local newText = self.Text;
      setText(newText);

    end,
    [React.Event.FocusLost] = function(self: TextBox)

      setText(properties.value or "");
      onChanged(self.Text);

    end;
  }, {
    UIPadding = React.createElement("UIPadding", {
      PaddingLeft = UDim.new(0, 10);
      PaddingRight = UDim.new(0, 10);
    });
    UICorner = React.createElement("UICorner", {
      CornerRadius = UDim.new(0, 5);
    });
    UISizeConstraint = React.createElement("UISizeConstraint", {
      MaxSize = Vector2.new(100, math.huge);
    });
  });

end;

return React.memo(TextInput);


--!strict

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local useStudioColors = require(root.DialogueEditor.hooks.useStudioColors);

export type ToggleInputProperties = {
  isEnabled: boolean;
  onChanged: (value: boolean) -> ();
  layoutOrder: number;
}

local function ToggleInput(properties: ToggleInputProperties)

  local isEnabled = properties.isEnabled;
  local colors = useStudioColors();
  local layoutOrder = properties.layoutOrder or 1;

  return React.createElement("Frame", {
    AutomaticSize = Enum.AutomaticSize.XY;
    BackgroundTransparency = 1;
    LayoutOrder = layoutOrder;
  }, {
    UIListLayout = React.createElement("UIListLayout", {
      SortOrder = Enum.SortOrder.LayoutOrder;
      FillDirection = Enum.FillDirection.Horizontal;
      HorizontalAlignment = Enum.HorizontalAlignment.Left;
      VerticalAlignment = Enum.VerticalAlignment.Center;
      Padding = UDim.new(0, 15);
    });
    TextLabel = React.createElement("TextLabel", {
      AutomaticSize = Enum.AutomaticSize.XY;
      BackgroundTransparency = 1;
      Text = if isEnabled then "On" else "Off";
      TextColor3 = colors.text;
      TextSize = 14;
      LayoutOrder = 1;
      FontFace = Font.fromName("BuilderSans", Enum.FontWeight.Regular);
    });
    ToggleButton = React.createElement("TextButton", {
      AutomaticSize = Enum.AutomaticSize.XY;
      BackgroundColor3 = colors.primaryButton;
      LayoutOrder = 2;
      Text = "";
      Size = UDim2.fromOffset(50, 30);
      BackgroundTransparency = if isEnabled then 0.2 else 1;
      [React.Event.Activated] = function()

        properties.onChanged(not isEnabled);

      end;
    }, {
      UIStroke = if not isEnabled then
        React.createElement("UIStroke", {
          Color = colors.text;
          Thickness = 1;
          ApplyStrokeMode = Enum.ApplyStrokeMode.Border;
        })
      else nil;
      UIPadding = React.createElement("UIPadding", {
        PaddingLeft = UDim.new(0, 5);
        PaddingRight = UDim.new(0, 5);
      });
      UICorner = React.createElement("UICorner", {
        CornerRadius = UDim.new(0, 20);
      });
      Dot = React.createElement("Frame", {
        AnchorPoint = Vector2.new(if isEnabled then 1 else 0, 0.5);
        Position = UDim2.fromScale(if isEnabled then 1 else 0, 0.5);
        Size = UDim2.fromOffset(20, 20);
        BackgroundColor3 = colors.text;
      }, {
        UICorner = React.createElement("UICorner", {
          CornerRadius = UDim.new(1, 0);
        });
      });
    });
  });

end;

return React.memo(ToggleInput);


--!strict

local TweenService = game:GetService("TweenService");
local UserInputService = game:GetService("UserInputService");

local root = script.Parent.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local useStudioColors = require(root.DialogueEditor.hooks.useStudioColors);

export type ToolbarButtonProps = {
  iconImage: string;
  text: string?;
  layoutOrder: number;
  isDisabled: boolean?;
  onClick: () -> ();
  plugin: Plugin;
};

local function ToolbarButton(props: ToolbarButtonProps)

  local colors = useStudioColors();
  local buttonRef = React.useRef(nil :: TextButton?);
  local plugin = props.plugin;

  React.useEffect(function()

    local button = buttonRef.current;
    if not button then

      return;

    end;

    local isUserHovering = false;
    local isMouseDown = false;
    local function playTween()

      local tween = TweenService:Create(button, TweenInfo.new(0.1), {
        BackgroundTransparency = if isMouseDown then 0.75 elseif isUserHovering then 0.25 else 1;
      });

      tween:Play();

    end;

    local mouseEnterConnection = button.MouseEnter:Connect(function()

      isUserHovering = true;
      playTween();

    end);

    local mouseLeaveConnection = button.MouseLeave:Connect(function()

      isUserHovering = false;
      playTween();

    end);

    local mouseButton1DownConnection = button.InputBegan:Connect(function(input)

      if input.UserInputType == Enum.UserInputType.MouseButton1 and isUserHovering and not isMouseDown then

        isMouseDown = true;
        playTween();

      end;

    end);

    local mouseButton1UpConnection = button.InputEnded:Connect(function(input)

      if input.UserInputType == Enum.UserInputType.MouseButton1 then

        isMouseDown = false;
        playTween();

      end;

    end);

    return function()

      mouseEnterConnection:Disconnect();
      mouseLeaveConnection:Disconnect();
      mouseButton1DownConnection:Disconnect();
      mouseButton1UpConnection:Disconnect();

    end;

  end, {props.isDisabled});

  return React.createElement("TextButton", {
    LayoutOrder = props.layoutOrder;
    BackgroundColor3 = colors.toolbarButton;
    BackgroundTransparency = 1;
    AutoButtonColor = false;
    ref = buttonRef;
    Text = "";
    Size = UDim2.new();
    AutomaticSize = Enum.AutomaticSize.XY;
    BorderSizePixel = 0;
    [React.Event.Activated] = function()

      if (not props.isDisabled) then

        props.onClick();

      end;

    end;
  }, {
    UIPadding = React.createElement("UIPadding", {
      PaddingLeft = UDim.new(0, 10);
      PaddingRight = UDim.new(0, 10);
    });
    UIListLayout = React.createElement("UIListLayout", {
      SortOrder = Enum.SortOrder.LayoutOrder;
      FillDirection = Enum.FillDirection.Horizontal;
      VerticalAlignment = Enum.VerticalAlignment.Center;
      Padding = UDim.new(0, 5);
    });
    UISizeConstraint = React.createElement("UISizeConstraint", {
      MinSize = Vector2.new(0, 35);
    });
    IconImageLabel = React.createElement("ImageLabel", {
      LayoutOrder = 1;
      BackgroundTransparency = 1;
      Image = props.iconImage;
      Size = UDim2.new(0, 24, 0, 24);
      ImageColor3 = if props.isDisabled then colors.textDisabled else colors.text;
    });
    TextLabel = if props.text then
      React.createElement("TextLabel", {
        LayoutOrder = 2;
        BackgroundTransparency = 1;
        Text = props.text;
        TextSize = 12;
        Size = UDim2.new(0, 0, 1, 0);
        AutomaticSize = Enum.AutomaticSize.X;
        TextColor3 = if props.isDisabled then colors.textDisabled else colors.text;
        FontFace = Font.fromId(11702779517);
      })
    else nil;
    UICorner = React.createElement("UICorner", {
      CornerRadius = UDim.new(0, 5);
    });
  });

end;

return React.memo(ToolbarButton);


--!strict

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

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);

local function useAutomaticClose(closeDialogueEditor: () -> ())

  React.useEffect(function(): ()

    local selectionChangedConnection = Selection.SelectionChanged:Connect(function()

      local selection = Selection:Get();
      if #selection == 0 then

        closeDialogueEditor();

      end;

    end);

    return function()

      selectionChangedConnection:Disconnect();

    end;

  end, { closeDialogueEditor });

end;

return useAutomaticClose;


--!strict

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local VirtualService = require(root.VirtualService);

local function useChangeHistory()

  local game = VirtualService.globals.game;
  local ChangeHistoryService = game:GetService("ChangeHistoryService");

  local startRecording = React.useCallback(function(label: string)

    if ChangeHistoryService:IsRecordingInProgress() then

      ChangeHistoryService:FinishRecording("", Enum.FinishRecordingOperation.Cancel);

    end

    local identifier = ChangeHistoryService:TryBeginRecording(label);

    return identifier;

  end, {});

  local finishRecording = React.useCallback(function(identifier: string, operation: Enum.FinishRecordingOperation?)

    ChangeHistoryService:FinishRecording(identifier, operation or Enum.FinishRecordingOperation.Commit);

  end, {});

  return startRecording, finishRecording;

end;

return useChangeHistory;


--!strict

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

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);

local function useDialogueMakerPackages()

  local getDialogueMakerPackages = React.useCallback(function()

    local dialogueMakerKits = CollectionService:GetTagged("DialogueMakerKit");
    local dialogueMakerKit;
    for _, kit in dialogueMakerKits do

      if not kit:IsDescendantOf(root) then

        dialogueMakerKit = kit;
        break;

      end;

    end;

    local dialogueMakerPackageContainers = CollectionService:GetTagged("DialogueMakerPackages");
    local dialogueMakerPackages = if dialogueMakerKit then dialogueMakerKit:FindFirstChild("Packages") else nil;

    if not dialogueMakerPackages then

      for _, packageContainer in dialogueMakerPackageContainers do

        if not packageContainer:IsDescendantOf(root) then

          dialogueMakerPackages = packageContainer;
          break;

        end;

      end;

    end;

    return dialogueMakerPackages;

  end, {});

  local dialogueMakerPackages: Instance?, setDialogueMakerPackages = React.useState(getDialogueMakerPackages());

  React.useEffect(function()

    local function updateDialogueMakerPackages()

      setDialogueMakerPackages(getDialogueMakerPackages());

    end;

    local dialogueMakerPackagesAddedConnection = CollectionService:GetInstanceAddedSignal("DialogueMakerPackages"):Connect(updateDialogueMakerPackages);
    local dialogueMakerPackagesRemovedConnection = CollectionService:GetInstanceAddedSignal("DialogueMakerKit"):Connect(updateDialogueMakerPackages);
    local dialogueMakerKitAddedConnection = CollectionService:GetInstanceRemovedSignal("DialogueMakerPackages"):Connect(updateDialogueMakerPackages);
    local dialogueMakerKitRemovedConnection = CollectionService:GetInstanceRemovedSignal("DialogueMakerKit"):Connect(updateDialogueMakerPackages);

    return function()

      dialogueMakerPackagesAddedConnection:Disconnect();
      dialogueMakerPackagesRemovedConnection:Disconnect();
      dialogueMakerKitAddedConnection:Disconnect();
      dialogueMakerKitRemovedConnection:Disconnect();

    end;

  end, {getDialogueMakerPackages});

  return dialogueMakerPackages;

end;

return useDialogueMakerPackages;


--!strict

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local VirtualService = require(root.VirtualService);

local function useDialogueMakerScriptSelection()

  local game = VirtualService.globals.game;
  local Selection = game:GetService("Selection");

  local getScripts = React.useCallback(function()

    local selection = Selection:Get();
    if #selection ~= 1 then

      return nil, nil;

    end;

    local selectedInstance = selection[1];
    local isSelectionAModuleScript = selectedInstance:IsA("ModuleScript");

    if not isSelectionAModuleScript and selectedInstance:IsA("Folder") and selectedInstance.Parent and selectedInstance.Parent:IsA("ModuleScript") then

      selectedInstance = selectedInstance.Parent;
      isSelectionAModuleScript = true;

    end;

    local conversationScript = if isSelectionAModuleScript and selectedInstance:HasTag("DialogueMakerConversationScript") then selectedInstance else nil;
    if not conversationScript then

      local parent = selectedInstance.Parent;
      while parent do

        if not parent:IsA("ModuleScript") then

          break;

        end;

        if parent:HasTag("DialogueMakerConversationScript") then

          conversationScript = parent;
          break;

        end;

        parent = parent.Parent;

      end;

    end;

    local selectedScript = if conversationScript == selectedInstance or (isSelectionAModuleScript and selectedInstance:HasTag("DialogueMakerDialogueScript")) then selectedInstance else nil;

    return selectedScript, conversationScript;

  end, {});
  local initialSelectedScript, initialConversationScript = React.useMemo(getScripts, {});
  local selectedScript, setSelectedScript = React.useState(initialSelectedScript);
  local conversationScript, setConversationScript = React.useState(initialConversationScript);

  React.useEffect(function()

    local function checkSelection()

      local newSelectedScript, newConversationScript = getScripts();

      if newSelectedScript ~= selectedScript then

        setSelectedScript(newSelectedScript);

      end;

      if newConversationScript ~= conversationScript then

        setConversationScript(newConversationScript);

      end;

    end;

    local selectionChangedConnection = Selection.SelectionChanged:Connect(checkSelection);
    task.spawn(checkSelection);

    return function()

      selectionChangedConnection:Disconnect();

    end;

  end, {selectedScript, conversationScript});

  return selectedScript, conversationScript;

end;

return useDialogueMakerScriptSelection;


--!strict

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);

local function useDynamicSize(gui: GuiBase2d, minimumWidth: number?, minimumHeight: number?): boolean

  local checkCondition = React.useCallback(function()

    local isAtOrAboveMinimumHeight = minimumHeight == nil or gui.AbsoluteSize.Y >= minimumHeight;
    local isAtOrAboveMinimumWidth = minimumWidth == nil or gui.AbsoluteSize.X >= minimumWidth;

    return isAtOrAboveMinimumHeight and isAtOrAboveMinimumWidth;

  end);

  local isConditionMet, setIsConditionMet = React.useState(checkCondition());

  React.useEffect(function()

    local function updateStatus()

      setIsConditionMet(checkCondition());

    end;

    local viewportChangedEvent = gui:GetPropertyChangedSignal("AbsoluteSize"):Connect(updateStatus);

    task.spawn(updateStatus);

    return function()

      viewportChangedEvent:Disconnect();

    end;

  end, {gui :: unknown, checkCondition});

  return isConditionMet;

end

return useDynamicSize;


--!strict

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

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local useDialogueMakerPackages = require(root.DialogueEditor.hooks.useDialogueMakerPackages);

local function useRefreshDialogueMakerScripts()

  local dialogueMakerPackages = useDialogueMakerPackages();

  return React.useCallback(function()

    assert(dialogueMakerPackages, `Dialogue Maker packages not found. Make sure you have the Dialogue Maker packages in your game, and tag the folder with "DialogueMakerPackages".`);

    local conversationScripts = CollectionService:GetTagged("DialogueMakerConversationScript");
    local dialogueScripts = CollectionService:GetTagged("DialogueMakerDialogueScript");
    local loaderScripts = CollectionService:GetTagged("DialogueMakerLoader");

    for _, autoGeneratedScripts in {conversationScripts, dialogueScripts, loaderScripts} do

      for _, autoGeneratedScript in autoGeneratedScripts do

        if autoGeneratedScript:IsDescendantOf(root) then

          continue;

        end;

        -- Determine the correct template and target script based on the type.
        local isLoader = autoGeneratedScript:HasTag("DialogueMakerLoader");
        local templateScript = (
          if autoGeneratedScript:HasTag("DialogueMakerConversationScript") then root.Templates.ConversationTemplate
          elseif autoGeneratedScript:HasTag("DialogueMakerDialogueScript") then root.Templates.DialogueTemplate
          elseif isLoader then root.Templates.LoaderTemplate
          else nil
        );

        if not templateScript then

          warn(`No template found for script: {autoGeneratedScript:GetFullName()}.`);
          return;

        end;

        -- Reset the script contents to the template.
        autoGeneratedScript.Source = templateScript.Source;

        -- Replace packages.
        autoGeneratedScript.Source = autoGeneratedScript.Source:gsub("local packages = [^\n]*", `local packages = game.{dialogueMakerPackages:GetFullName()};\n`);

        -- Replace the properties string in the script.
        -- First, edit the settings.
        local settingsFolder = autoGeneratedScript:FindFirstChild("Settings");
        settingsFolder = if isLoader then settingsFolder or Instance.new("Folder") else nil;
        local settingsString = "settings = {};";
        if settingsFolder then

          settingsFolder = settingsFolder:Clone();

          if isLoader then

            local themesFolder = settingsFolder:FindFirstChild("theme");

            if not themesFolder then

              themesFolder = Instance.new("Folder");
              themesFolder.Name = "theme";
              themesFolder.Parent = settingsFolder;

            end;

            assert(themesFolder);

            local componentScriptValue = themesFolder:FindFirstChild("componentScript");

            if not componentScriptValue then

              componentScriptValue = Instance.new("ObjectValue");
              componentScriptValue.Name = "componentScript";
              componentScriptValue.Parent = themesFolder;

            end;

          end;

          settingsString = "settings = {";

          local extraSpaces = if isLoader then "        " else "";

          for _, categoryFolder in settingsFolder:GetChildren() do

            if settingsFolder:IsA("Folder") then

              settingsString = `{settingsString}\n    {extraSpaces}{categoryFolder.Name} = \{`;

              for _, settingInstance in categoryFolder:GetChildren() do

                if settingInstance:IsA("ValueBase") then

                  local key = settingInstance.Name;
                  key = if key == "componentScript" then "component" else key;

                  local enumName = settingInstance:GetAttribute("Enum");
                  local enumPrefix = if enumName then `Enum.{enumName}:FromName(` else "";
                  local valueFullName = `script.Settings.{categoryFolder.Name}.{settingInstance.Name}.Value`;
                  local enumSuffix = if enumName then ")" else "";
                  local value = `{enumPrefix}{valueFullName}{enumSuffix}`;
                  value = if key == "component" then `require({if settingInstance.Value then value else "packages.StandardTheme"})` else value;
                  settingsString = `{settingsString}\n      {extraSpaces}{key} = {value};`;

                end;

              end;

              settingsString = `{settingsString}\n    {extraSpaces}};`;

            end;

          end;

          settingsString = `{settingsString}\n  {extraSpaces}};`;

        end;

        autoGeneratedScript.Source = autoGeneratedScript.Source:gsub("-- START SETTINGS REPLACEMENT.*-- END SETTINGS REPLACEMENT[^\n]*", settingsString);

        -- Then, replace the content.
        local contentScript = autoGeneratedScript:FindFirstChild("ContentScript");
        if contentScript then

          local contentString = "local content = require(script.ContentScript);";
          autoGeneratedScript.Source = autoGeneratedScript.Source:gsub("local content = [^\n]*", contentString);

          contentScript.Source = contentScript.Source:gsub("local packages = [^\n]*", `local packages = game.{dialogueMakerPackages:GetFullName()}; -- Automatically generated by plugin.`);

        end;

        -- Finally, replace the scripts.
        local functionMap = {
          runInitializationAction = "InitializationActionScript";
          runCleanupAction = "CleanupActionScript";
          runCompletionAction = "CompletionActionScript";
          verifyCondition = "ConditionScript";
        };
        local actionString = "";
        for functionName, scriptName in functionMap do

          if actionString ~= "" then

            actionString = `{actionString}\n`;

          end;

          local actionScript = autoGeneratedScript:FindFirstChild(scriptName);
          if actionScript then

            actionScript.Source = actionScript.Source:gsub("local packages = [^\n]*", `local packages = game.{dialogueMakerPackages:GetFullName()}; -- Automatically generated by plugin.`);
            actionString = `{actionString}{if actionString == "" then "" else "  "}{functionName} = require(script.{scriptName});`;

          end;

        end;

        autoGeneratedScript.Source = autoGeneratedScript.Source:gsub("-- START SCRIPT REPLACEMENT.*-- END SCRIPT REPLACEMENT[^\n]*", actionString);

      end;

    end;

  end, {dialogueMakerPackages});

end;

return useRefreshDialogueMakerScripts;


--!strict

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

local React = require(script.Parent.Parent.Parent.roblox_packages.react);
local Colors = require(script.Parent.Parent.Parent.Colors);
type ColorDictionary = Colors.ColorDictionary;

local function useStudioColors(): ColorDictionary

  local getColors = React.useCallback(function()

    if not RunService:IsStudio() then

      return Colors.Dark;

    end

    local themeName = settings().Studio.Theme.Name
    return Colors[themeName] or Colors.Dark

  end, {});

  local colors, setColors = React.useState(getColors())

  React.useEffect(function(): ()

    if not RunService:IsStudio() then

      return;

    end

    local function onThemeChanged()

      setColors(getColors());

    end

    local themeChangedConnection = settings().Studio.ThemeChanged:Connect(onThemeChanged)

    return function()

      themeChangedConnection:Disconnect()

    end

  end, {getColors})

  return colors;

end

return useStudioColors;


--!strict

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

local root = script.Parent.Parent.Parent;
local React = require(root.roblox_packages.react);
local Icons = require(root.Icons);

local function useStudioIcons()

  local getIcons = React.useCallback(function()

    if not RunService:IsStudio() then

      return Icons.Dark;

    end

    local themeName = settings().Studio.Theme.Name
    return Icons[themeName] or Icons.Dark

  end, {});

  local icons, setIcons = React.useState(getIcons())

  React.useEffect(function(): ()

    if not RunService:IsStudio() then

      return;

    end

    local function onThemeChanged()

      setIcons(getIcons());

    end

    local themeChangedConnection = settings().Studio.ThemeChanged:Connect(onThemeChanged)

    return function()

      themeChangedConnection:Disconnect()

    end

  end, {getIcons})

  return icons;

end

return useStudioIcons;


--!strict

local root = script.Parent;
local React = require(root.roblox_packages.react);
local Toolbar = require(script.components.Toolbar);
local Explorer = require(script.components.Explorer);
local Settings = require(script.components.Settings);
local InitialSetupScreen = require(script.components.InitialSetupScreen);
local useDialogueMakerScriptSelection = require(script.hooks.useDialogueMakerScriptSelection);
local useAutomaticClose = require(script.hooks.useAutomaticClose);
local useDialogueMakerPackages = require(script.hooks.useDialogueMakerPackages);

export type DialogueEditorProperties = {
  plugin: Plugin;
  pluginGUI: DockWidgetPluginGui;
  closeDialogueEditor: () -> ();
}

local function DialogueEditor(props: DialogueEditorProperties)

  local selectedScript = useDialogueMakerScriptSelection();
  local settingsTarget, setSettingsTarget = React.useState(nil :: ModuleScript?);
  local closeDialogueEditor = props.closeDialogueEditor;

  useAutomaticClose(closeDialogueEditor);

  local dialogueMakerPackages = useDialogueMakerPackages();

  if not dialogueMakerPackages then

    return React.createElement(InitialSetupScreen, {
      plugin = props.plugin;
      pluginGUI = props.pluginGUI;
      closeDialogueEditor = closeDialogueEditor;
    });

  end;

  return React.createElement("Frame", {
    Size = UDim2.new(1, 0, 1, 0);
    BackgroundTransparency = 1;
  }, {
    UIListLayout = React.createElement("UIListLayout", {
      SortOrder = Enum.SortOrder.LayoutOrder;
      Padding = UDim.new(0, 0);
    });
    Toolbar = React.createElement(Toolbar, {
      plugin = props.plugin;
      pluginGUI = props.pluginGUI;
      selectedScript = selectedScript;
      settingsTarget = settingsTarget;
      setSettingsTarget = setSettingsTarget;
      layoutOrder = 1;
    });
    Explorer = if not settingsTarget then
      React.createElement(Explorer, {
        plugin = props.plugin;
        setSettingsTarget = setSettingsTarget;
        layoutOrder = 2;
      })
    else nil;
    Settings = if settingsTarget then
      React.createElement(Settings, {
        initialSettingsTarget = settingsTarget;
        layoutOrder = 2;
      })
    else nil;
  });

end;

return React.memo(DialogueEditor);


--!strict

local HttpService = game:GetService("HttpService");

local VirtualChangeHistoryService = {};

function VirtualChangeHistoryService.new()

  local virtualChangeHistoryService = {
    currentHistory = {};
    currentIdentifier = nil;
  };

  function virtualChangeHistoryService:TryBeginRecording()

    if self.currentIdentifier then

      return;

    end

    self.currentIdentifier = HttpService:GenerateGUID(false);
    self.currentHistory[self.currentIdentifier] = true; -- Simulate a successful start
    return self.currentIdentifier;

  end;

  function virtualChangeHistoryService:IsRecordingInProgress()

    return self.currentIdentifier ~= nil;

  end;

  function virtualChangeHistoryService:FinishRecording(identifier, operation)

    if not self.currentHistory[identifier] then

      return;

    end

    if operation == Enum.FinishRecordingOperation.Cancel then
      self.currentHistory[identifier] = nil;
    end

    self.currentIdentifier = nil;

  end;

  return virtualChangeHistoryService;

end;

return VirtualChangeHistoryService;


--!strict

local VirtualSelection = {};

function VirtualSelection.new()

  local selectionChangedEvent = Instance.new("BindableEvent");
  local virtualSelection = {
    currentSelection = {};
    SelectionChanged = selectionChangedEvent.Event;
  };

  function virtualSelection:Set(selection: {Instance}): ()

    local oldSelection = self.currentSelection;
    self.currentSelection = selection;

    if not oldSelection[1] and #selection > 0 then

      selectionChangedEvent:Fire();
      return;

    end;

    local checkedInstances = {};
    for _, instance in oldSelection do

      if not table.find(selection, instance) then

        selectionChangedEvent:Fire();
        return;

      end;

      table.insert(checkedInstances, instance);

    end;

    for _, instance in selection do

      if not table.find(checkedInstances, instance) then

        selectionChangedEvent:Fire();
        return;

      end;

    end;

  end;

  function virtualSelection:Get(): {Instance}

    return self.currentSelection;

  end;

  function virtualSelection:Add(instances: {Instance}): ()

    local shouldFire = false;

    for _, instance in instances do

      if not table.find(self.currentSelection, instance) then

        table.insert(self.currentSelection, instance);
        shouldFire = true;

      end;

    end;

    if shouldFire then

      selectionChangedEvent:Fire();

    end;

  end;

  function virtualSelection:Remove(instances: {Instance}): ()

    local shouldFire = false;

    for _, instance in instances do

      local index = table.find(self.currentSelection, instance);

      if index then

        table.remove(self.currentSelection, index);
        shouldFire = true;

      end;

    end;

    if shouldFire then

      selectionChangedEvent:Fire();

    end;

  end;

  return virtualSelection;

end;

return VirtualSelection;


--!strict

local root = script.Parent;
local Button = require(root.DialogueEditor.components.Button);
local Paragraph = require(root.DialogueEditor.components.Paragraph);
local React = require(root.roblox_packages.react);
local ReactErrorBoundary = require(root.roblox_packages.ReactErrorBoundary);
local useStudioColors = require(root.DialogueEditor.hooks.useStudioColors);

type FallbackProps = ReactErrorBoundary.FallbackProps;

local function ErrorComponent(props: FallbackProps)

  local colors = useStudioColors();

  return React.createElement("Frame", {
    BackgroundColor3 = colors.toolbar;
    Size = UDim2.fromScale(1, 1);
  }, {
    Content = React.createElement("Frame", {
      Size = UDim2.fromScale(1, 1);
      BackgroundTransparency = 1;
      AnchorPoint = Vector2.new(0.5, 0.5);
      Position = UDim2.fromScale(0.5, 0.5);
    }, {
      UIPadding = React.createElement("UIPadding", {
        PaddingLeft = UDim.new(0, 15);
        PaddingRight = UDim.new(0, 15);
        PaddingTop = UDim.new(0, 15);
        PaddingBottom = UDim.new(0, 15);
      });
      UIListLayout = React.createElement("UIListLayout", {
        FillDirection = Enum.FillDirection.Vertical,
        HorizontalAlignment = Enum.HorizontalAlignment.Center,
        VerticalAlignment = Enum.VerticalAlignment.Center,
        SortOrder = Enum.SortOrder.LayoutOrder,
        Padding = UDim.new(0, 10);
      });
      WarningIcon = React.createElement("ImageLabel", {
        Image = "rbxassetid://126149752885425";
        Size = UDim2.fromOffset(50, 50);
        BackgroundTransparency = 1;
        LayoutOrder = 1;
      });
      TextLabel = React.createElement(Paragraph, {
        text = "A critical error occurred. Dialogue Maker Plugin cannot continue.";
        layoutOrder = 2;
        fontWeight = Enum.FontWeight.Bold;
        fontSize = Enum.FontSize.Size24;
      });
      TextBox = React.createElement("TextBox", {
        Text = tostring(props.error),
        TextColor3 = colors.text,
        AutomaticSize = Enum.AutomaticSize.Y;
        Size = UDim2.fromScale(1, 0);
        TextSize = 14;
        FontFace = Font.fromName("Inconsolata", Enum.FontWeight.Regular);
        BackgroundColor3 = colors.input;
        TextWrapped = true,
        TextXAlignment = Enum.TextXAlignment.Left;
        TextEditable = false;
        ClearTextOnFocus = false;
        LayoutOrder = 3;
      }, {
        UICorner = React.createElement("UICorner", {
          CornerRadius = UDim.new(0, 5);
        });
        UIPadding = React.createElement("UIPadding", {
          PaddingLeft = UDim.new(0, 10);
          PaddingRight = UDim.new(0, 10);
          PaddingTop = UDim.new(0, 10);
          PaddingBottom = UDim.new(0, 10);
        });
      });
      TextButton = React.createElement(Button, {
        text = "Restart plugin",
        BorderSizePixel = 0;
        layoutOrder = 4;
        onClick = function()

          props.resetErrorBoundary();

        end;
      });
      UISizeConstraint = React.createElement("UISizeConstraint", {
        MaxSize = Vector2.new(750, math.huge);
      });
    });
  });

end;

return React.memo(ErrorComponent);


--!strict

local EventProvider = {};

export type EventProvider<InstanceType, T> = {
  getSignal: (self: EventProvider<InstanceType, T>, instance: InstanceType) -> RBXScriptSignal<T>;
  fireEvent: (self: EventProvider<InstanceType, T>, instance: InstanceType, ...T) -> ();
};

function EventProvider.new<InstanceType, T>(instanceType: InstanceType, eventName: string, ...: T)

  local events = {};
  local connections = {};
  local eventProvider: EventProvider<InstanceType, T> = {} :: EventProvider<InstanceType, T>;

  local function getBindableEvent(instance: InstanceType): BindableEvent

    if events[instance] then

      return events[instance];

    end;

    local bindableEvent = Instance.new("BindableEvent");
    events[instance] = bindableEvent;

    if not connections[instance] then

      connections[instance] = ((instance :: any)[eventName] :: RBXScriptSignal):Connect(function(...)

        eventProvider:fireEvent(instance, ...);

      end);

    end;

    return bindableEvent;

  end;

  local function getSignal(self, instance: InstanceType): RBXScriptSignal<T>

    return getBindableEvent(instance).Event;

  end;

  local function fireEvent(self, instance: InstanceType, ...): ()

    getBindableEvent(instance):Fire(...);

  end;

  eventProvider = {
    getSignal = getSignal;
    fireEvent = fireEvent;
  }

  return eventProvider;

end;

return EventProvider;


--!strict

local EventProvider = require(script.EventProvider);

local VirtualService = {
  globals = {
    plugin = plugin;
    game = game;
    script = script;
  };
  events = {
    GuiButton = {
      Activated = EventProvider.new((nil :: unknown) :: GuiButton, "Activated");
    }
  }
};

return VirtualService;


--!strict

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

local Icons = require(script.Icons);
local React = require(script.roblox_packages.react);
local ReactErrorBoundary = require(script.roblox_packages.ReactErrorBoundary);
local ErrorComponent = require(script.ErrorComponent);
local ErrorBoundary = ReactErrorBoundary.ErrorBoundary;
local ReactRoblox = require(script.roblox_packages["react-roblox"]);
local DialogueEditor = require(script.DialogueEditor);

if RunService:IsRunning() then

  return;

end;

local toolbar = plugin:CreateToolbar("Dialogue Maker");
local themeName = settings().Studio.Theme.Name;
local dialogueEditorButton = toolbar:CreateButton("Dialogue Maker", "Toggles the Dialogue Maker window.", Icons[themeName].logo);
local pluginGUI: DockWidgetPluginGui?;

local function getSelectedInstance(): Instance?

  local selectedObjects = Selection:Get();
  if #selectedObjects ~= 1 then return nil; end;

  local instance = selectedObjects[1];

  return instance;

end;

local function refreshToolbar()

  local themeName = settings().Studio.Theme.Name;
  local selectedInstance = getSelectedInstance();
  dialogueEditorButton.Icon = Icons[themeName].logo;
  dialogueEditorButton.Enabled = selectedInstance ~= nil;
  dialogueEditorButton:SetActive(pluginGUI ~= nil);

end;

local pluginGUIRoot: ReactRoblox.RootType? = nil;
local function closeDialogueEditor(): ()

  if pluginGUIRoot then

    pluginGUIRoot:unmount();
    pluginGUIRoot = nil;

  end;

  if pluginGUI then

    pluginGUI:Destroy();
    pluginGUI = nil;

  end;

  refreshToolbar();

end;

local function openDialogueEditor(): ()

  local widgetInfo = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, true, true, 584, 300, 1, 1);
  local title = `Dialogue Maker Plugin {script:GetAttribute("Version")}`;
  local newPluginGui = plugin:CreateDockWidgetPluginGui(title, widgetInfo);
  pluginGUI = newPluginGui;

  newPluginGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling;
  newPluginGui.Title = title;
  newPluginGui:BindToClose(closeDialogueEditor);

  local newPluginGUIRoot = ReactRoblox.createRoot(newPluginGui);
  newPluginGUIRoot:render(
    React.createElement(ErrorBoundary, {
      FallbackComponent = ErrorComponent :: any;
      onError = function(errorObject)

        task.spawn(function()

          error(errorObject);

        end);

      end;
    }, {
      React.createElement(DialogueEditor, {
        plugin = plugin;
        pluginGUI = newPluginGui;
        closeDialogueEditor = closeDialogueEditor;
      })
    })
  );
  pluginGUIRoot = newPluginGUIRoot;

  refreshToolbar();

end;

dialogueEditorButton.Click:Connect(function()

  if pluginGUI then

    closeDialogueEditor();

  else

    openDialogueEditor();

  end;

end);

settings().Studio.ThemeChanged:Connect(refreshToolbar);
Selection.SelectionChanged:Connect(refreshToolbar);
refreshToolbar();

plugin.Unloading:Connect(function()

  closeDialogueEditor();

end)


local IMConstants = {
	SLOT_NAME_PREFIX                        = "SLOT_",
	HOVER_NAME_PREFIX                       = "HOVER_PROMPT_",
	FILLED_GUI_NAME                         = "FILLED",
	DUMMY_FILLED_GUI_NAME                   = "D_FILLED",
	GRID_LAYOUT_NAME                        = "IMGridLayout",

	CONTAINER_ATTRIBUTE_NAME                = "IMContainer",
	SLOT_INDEX_ATTRIBUTE_NAME               = "IMSlot",
	CONTAINER_HOTBAR_NAME                   = "Hotbar",

    DEFAULT_CELL_PADDING                    = UDim2.new(0, 10, 0, 16),
    DEFAULT_CELL_SIZE                       = UDim2.new(0, 60, 0, 60),
    DEFAULT_FILL_DIRECTION                  = Enum.FillDirection.Horizontal,
    DEFAULT_FILL_DIRECTION_MAX_CELLS        = 6,
    DEFAULT_START_CORNER                    = Enum.StartCorner.TopLeft,
    DEFAULT_H_ALIGN                         = Enum.HorizontalAlignment.Left,
    DEFAULT_V_ALIGN                         = Enum.VerticalAlignment.Top,

}

return IMConstants


--!strict

local BIT_FLAGS = {
	CLOSED 							= 0b1,
	NO_FILTERING 					= 0b10,
	NO_DRAGGING 					= 0b100,
	NO_STACKING						= 0b1000,
	HOTBAR_UNTOGGLED_HOVER 			= 0b10000,
	HOTBAR_COLLAPSE					= 0b100000,
	HOTBAR_FILL_FIRST				= 0b1000000,
	NO_AUTO_EQUIP					= 0b10000000,
	NO_EQUIPPED_STATE				= 0b100000000,
    NO_SORTING                      = 0b1000000000,
}

local IMContainer = {
	FLAGS = BIT_FLAGS
}

local camera = (workspace::{ Camera: Camera }).Camera

local DraggableUI = require(script.Parent.IMDraggableUI)
local Constants = require(script.Parent.IMConstants)
local UserType = require(script.Parent.IMUserType)
local Utility = require(script.Parent.IMUtility)
local StaticContainer = require(script.Parent.IMStaticContainer)

type _StaticContainer = StaticContainer._StaticContainer
type ITEM_TYPE = UserType.USER_ITEM_TYPE

type GridLayoutParams = {
	CellPadding: UDim2?,
	CellSize: UDim2?,
	FillDirection: Enum.FillDirection?,
	FillDirectionMaxCells: number?,
	StartCorner: Enum.StartCorner?,
	HorizontalAlignment: Enum.HorizontalAlignment?,
	VerticalAlignment: Enum.VerticalAlignment?,
	ParentContainer: GuiObject
}

export type ItemPositionInfo = {
	SlotIndex: number,
	ContainerIndex: number,
	ContainerName: string
}

export type EmptyConfigs = {
	GuiTemplate: GuiObject,
	RenderFunc: ((slotIndex: number, emptyGui: GuiObject, containerIndex: number) -> ())?
}

export type FilledConfigs = {
	GuiTemplate: GuiObject,
	RenderFunc: (slotIndex: number, item: ITEM_TYPE, filledGui: GuiObject, containerIndex: number) -> ()
}

export type HoverPromptConfigs = {
	GuiTemplate: GuiObject,
	FixedPosition: boolean,
	Offset: Vector2?,
	RenderFunc: (item: ITEM_TYPE, hoverPromptGui: GuiObject, positionInfo: ItemPositionInfo) -> (() -> ())?,
	ShowPromptAfterDragging: boolean?,
}

export type HotbarKey = number | string | nil
export type HotbarKeybinds = { HotbarKey }

export type HotbarConfigs = {
	Keybinds: HotbarKeybinds,
	OnKeyPress: (item: ITEM_TYPE, filledGui: GuiObject, emptyGui: GuiObject, info: ItemPositionInfo) -> (),
    ScrollingHotbar: boolean,
}

export type _HoverPromptInfo = {
	_TEMPLATE: GuiObject,
	_OFFSET: UDim2,
	_FIXED_POSITION: boolean,
	_RENDER_FUNC: (item: ITEM_TYPE, gui: GuiObject, positionInfo: ItemPositionInfo) -> (() -> ())?,
	_GUI: GuiObject?,
	_INSET: UDim2?,
	_OPEN_HOVER_PROMPT_AFTER_DRAGGING: boolean,
    _RENDER_FUNC_CLEANUP: (() -> ())?,
}

export type ActivatedCallback = (item: ITEM_TYPE, filledGui: GuiObject, slotIndex: number) -> ()
export type OnItemAddedCallback = (item: ITEM_TYPE, filledGui: GuiObject, info: ItemPositionInfo) -> ()
export type AllowedItemsCallback = (item: ITEM_TYPE) -> boolean
export type OnItemShiftedCallback = (item: ITEM_TYPE, info: ItemPositionInfo) -> ()
export type EquippedRenderCleanupFunc = (
    lastItem: ITEM_TYPE,
    lastFilledGui: GuiObject?,
    lastEmptyGui: GuiObject,
    lastInfo: ItemPositionInfo
) -> ()
export type EquippedOldGuiRenderCleanupFunc = (
    lastItem: ITEM_TYPE,
    oldEmptyGui: GuiObject,
    lastInfo: ItemPositionInfo
) -> ()
export type EquippedRenderFunc = (
    item: ITEM_TYPE,
    filledGui: GuiObject,
    emptyGui: GuiObject,
    info: ItemPositionInfo
) -> (EquippedRenderCleanupFunc, EquippedOldGuiRenderCleanupFunc?)

export type Container = {
    --[[
        Define the layout of the container, which is determined using a UIGridLayout.
        At minimum, the `ParentContainer` field must be defined; it is the gui object
        that the UIGridLayout will be parented to.

            type GridLayoutParams = {
                CellPadding: UDim2?,
                CellSize: UDim2?,
                FillDirection: Enum.FillDirection?,
                FillDirectionMaxCells: number?,
                StartCorner: Enum.StartCorner?,
                HorizontalAlignment: Enum.HorizontalAlignment?,
                VerticalAlignment: Enum.VerticalAlignment?,
                ParentContainer: GuiObject
            }
    ]]
	DefineLayout: (self: Container, params: GridLayoutParams) -> Container,

    --[[
        Define how empty slots are rendered, providing a gui template and a render function.
        The render function should only handle UI logic.

            type EmptyConfigs = {
                GuiTemplate: GuiObject,
                RenderFunc: (
                    slotIndex: number,
                    emptyGui: GuiObject,
                    containerIndex: number
                ) -> ()
            }
    ]]
	DefineEmptySlot: (self: Container, configs: EmptyConfigs) -> (),

    --[[
        Define how filled slots are rendered, providing a gui template and a render function.
        The render function should only handle UI logic.

            type FilledConfigs = {
                GuiTemplate: GuiObject,
                RenderFunc: (
                    slotIndex: number,
                    item: ITEM_TYPE,
                    filledGui: GuiObject,
                    containerIndex: number
                ) -> ()
            }
    ]]
	DefineFilledSlot: (self: Container, configs: FilledConfigs) -> (),

    --[[
        Takes in a table that define how you want to configure your hover prompt. The GuiTemplate
        defines the hover prompt gui to use for displaying the hover information. The RenderFunc defines
        how you display the item information to the gui. It passes in the item data and the hover prompt gui.

        The GuiTmeplate is shared across all items; it's a singleton. So be sure to manage your properties
        appropriately. For example, if hovering over a rare item changes the color to blue, then you'd probably
        change the color back to normal when hovering over a normal item.

        You can provide just the GuiTemplate and RenderFunc and ignore the other fields if you wish to use
        the default values. It will default to FixedPosition = false and Offset = UDim2.new(0, 20, 0, 0).

        If FixedPosition = true, the hover prompt will be position to the top right corner of the
        inventory item. The offset will apply relative to the top right corner. If no offset is
        provided, it will default to UDim2.new(0, 0, 0, 0).

        If FixedPosition = false, the hover prompt will follow the mouse as long as it's moving within
        the inventory item. The offset will apply relative to the mouse position.

        If ShowPromptAfterDragging is true, the hover prompt, if defined, will immediately appear after
        dragging is done.
    ]]
	DefineHoverPrompt: (self: Container, configs: HoverPromptConfigs) -> (),

    --[[
        Define a callback that gets called whenever an item is activated in this container.
        An item is activated when it is left-clicked (mobile), double-tapped (mobile, nonhotbar),
        or tapped (mobile, tapped)

            type ActivatedCallback = (item: ITEM_TYPE, filledGui: GuiObject, slotIndex: number) -> ()
    ]]
	DefineActivatedCallback: (self: Container, callback: ActivatedCallback) -> (),

    --[[
        Define how the hotbar container is configured, taking in an array of keybinds,
        whose length should be the same as the capacity of the container, and a
        `OnKeyPress` callback that gets called when an item in the hotbar is
        pressed with its keybind.

            type HotbarConfigs = {
                Keybinds: HotbarKeybinds,
                OnKeyPress: (
                    item: ITEM_TYPE,
                    filledGui: GuiObject,
                    emptyGui: GuiObject,
                    info: ItemPositionInfo
                ) -> ()
            }
    ]]
	SetHotbarConfigs: (self: Container, hbConfigs: HotbarConfigs) -> (),

    --[[
        Define a callback that gets called whenever a new item is added to the container.

            type ItemPositionInfo = {
                SlotIndex: number,
                ContainerIndex: number,
                ContainerName: string
            }
            type OnItemAddedCallback = (item: ITEM_TYPE, filledGui: GuiObject, info: ItemPositionInfo) -> ()
    ]]
	DefineOnItemAddedCallback: (self: Container, callback: OnItemAddedCallback) -> (),

    --[[
        Define a callback that determines if an item is allowed in the container.

            type AllowedItemsCallback = (item: ITEM_TYPE) -> boolean
    ]]
	DefineAllowedItems: (self: Container, callback: AllowedItemsCallback) -> (),

    --[[
        Define how equipped slots are rendered in the container by providing
        a render function. This render function must also return a cleanup
        function to undo the UI effects of the equipping. This cleanup function is called
        when the equipped item is "unequipped", such as when another item is equipped.

        An item is "equipped" when it is clicked with left-clicked, when triggered with
        a hotbar keybind when it is in a hotbar container, and when it's a new item that's
        been added with AddItem() (when NO_AUTO_EQUIP flag is not enabled).
        Only one item can be "equipped" at a time.

        This "equipped" works for all containers that don't have the NO_EQUIPPED_STATE flag enabled.
        This "equipped" state is only a logical state; it does not internally handle the physical
        equipping of objects.

        **You cannot equip an item while it is being dragged.**
    ]]
    DefineRenderEquippedSlot: (self: Container, renderFunc: EquippedRenderFunc) -> (),
}

export type _Container = {
    _setHotbarKeybinds: (self: _Container, keybinds: HotbarKeybinds) -> (),
	_isItemAllowed: (self: _Container, item: ITEM_TYPE) -> boolean,

	_name: string,
	_capacity: number,
	_currSize: number,
	_draggables: { [GuiObject]: DraggableUI.Draggable? },	-- { [filledGui]: Draggable? }
	_invStartIndex: number,
	_invEndIndex: number,
	_flags: number,
	_slotGuiObjs: { [number]: GuiObject | false },		-- { [containerIndex]: SlotGui | false }
	_dummyFilledGuis: { [number]: GuiObject? },			-- { [containerIndex]: DummyFilledGui | nil }: used for splitting
	_filteringEnabled: boolean,
	_stackingEnabled: boolean,
	_closedContainer: boolean,
	_draggingEnabled: boolean,
	_hoverPromptParent: ScreenGui,
	_hotbarConfigs: HotbarConfigs,
	_inItemspace: boolean,
    _type: string,

	_PARENT_CONTAINER: GuiObject,
	_GRID_LAYOUT: UIGridLayout,
	_EMPTY_TEMPLATE: GuiObject,
	_FILLED_TEMPLATE: GuiObject,
	_RENDER_EMPTY_FUNC: ((slotIndex: number, emptyGui: GuiObject, containerIndex: number) -> ())?,
	_RENDER_FILLED_FUNC: (slotIndex: number, item: ITEM_TYPE, filledGui: GuiObject, containerIndex: number) -> (),
	_HOVER_INFO: _HoverPromptInfo?,

	_CALLBACKS: {
		_ACTIVATED_FUNC: ActivatedCallback,
		_ALLOWED_ITEMS_FUNC: AllowedItemsCallback,
		_ON_ITEM_ADDED_FUNC: OnItemAddedCallback,
        _ON_ITEM_SHIFTED_FUNC: OnItemShiftedCallback,
        _EQUIPPED_RENDER_FUNC: EquippedRenderFunc,
	}
} & Container

local DEFAULT_CELL_PADDING = Constants.DEFAULT_CELL_PADDING
local DEFAULT_CELL_SIZE = Constants.DEFAULT_CELL_SIZE
local DEFAULT_FILL_DIRECTION = Constants.DEFAULT_FILL_DIRECTION
local DEFAULT_FILL_DIRECTION_MAX_CELLS = Constants.DEFAULT_FILL_DIRECTION_MAX_CELLS
local DEFAULT_START_CORNER = Constants.DEFAULT_START_CORNER
local DEFAULT_H_ALIGN = Constants.DEFAULT_H_ALIGN
local DEFAULT_V_ALIGN = Constants.DEFAULT_V_ALIGN

local function getContainerIndexFromSlotIndex(container: _Container, slotIndex: number): number
	return slotIndex - container._invStartIndex + 1
end

local function destroyDraggable(props: _Container, filledGui: GuiObject)
	local draggable = props._draggables[filledGui]
	if not draggable then return end

	DraggableUI.Destroy(draggable)
	props._draggables[filledGui] = nil
end

--[[
	Destroys the slot, which also destroys the filledGui at that slot, if there is one.
	Used when calling AddItemAtIndex() or when container capacity is decreased.

	If decreaseCapacity = true, the slotGuiObj at the containerIndex will be set to nil
	in the array; if decreaseCapacity = false, it will be set to false instead.

	If it is set to nil, the array must be reconciled with IMContainer.ReconcileSlotGuiObjs()

]]
local function destroySlot(props: _Container, slotIndex: number, decreaseCapacity: boolean)
	local guiContainer = props._PARENT_CONTAINER:FindFirstChild(Constants.SLOT_NAME_PREFIX..tostring(slotIndex))
	if guiContainer then
		local filledGui = guiContainer:FindFirstChild(Constants.FILLED_GUI_NAME)
		if filledGui and filledGui:IsA("GuiObject") then
			destroyDraggable(props, filledGui)
			filledGui:Destroy()
			props._currSize -= 1
		end
		guiContainer:Destroy()

		if not decreaseCapacity then
			props._slotGuiObjs[getContainerIndexFromSlotIndex(props, slotIndex)] = false
		else
			props._slotGuiObjs[getContainerIndexFromSlotIndex(props, slotIndex)] = nil
		end
	end
end

local function destroyFilledGui(props: _Container, i: number)
	local guiContainer = props._PARENT_CONTAINER:FindFirstChild(Constants.SLOT_NAME_PREFIX..tostring(i))
	if guiContainer then
		local filledGui = guiContainer:FindFirstChild(Constants.FILLED_GUI_NAME)
		if filledGui and filledGui:IsA("GuiObject")  then
			destroyDraggable(props, filledGui)
			filledGui:Destroy()
			props._currSize -= 1
		end
	end
end

local function createEmptySlot(props: _Container, i: number, containerIndex: number): GuiObject
	local gui = props._EMPTY_TEMPLATE:Clone()
	if props._RENDER_EMPTY_FUNC then
		props._RENDER_EMPTY_FUNC(i, gui, containerIndex)
	end
	gui:SetAttribute(Constants.SLOT_INDEX_ATTRIBUTE_NAME, i)
	gui.Name = Constants.SLOT_NAME_PREFIX..tostring(i)
	gui.LayoutOrder = i
	gui.Visible = true
	gui.Parent = props._PARENT_CONTAINER

	props._slotGuiObjs[containerIndex] = gui

	return gui
end

local function createFilledSlot(props: _Container, i: number, item: ITEM_TYPE, containerIndex: number): (GuiObject, GuiObject)
	local slot = createEmptySlot(props, i, containerIndex)

	local filledSlotGui = props._FILLED_TEMPLATE:Clone()
	filledSlotGui.Visible = true
	props._RENDER_FILLED_FUNC(i, item, filledSlotGui, containerIndex)
	filledSlotGui.Name = Constants.FILLED_GUI_NAME
	filledSlotGui.Parent = slot

	props._currSize += 1

	if props._draggingEnabled then
		props._draggables[filledSlotGui] = DraggableUI.new(filledSlotGui, false)
	end

	return filledSlotGui, slot
end

local function createDummyFilledGui(props: _Container, slotIndex: number, item: ITEM_TYPE, containerIndex: number): (GuiObject, GuiObject)
	local dummyGui = props._FILLED_TEMPLATE:Clone()
	dummyGui.Visible = true
	props._RENDER_FILLED_FUNC(slotIndex, item, dummyGui, containerIndex)
	dummyGui.Name = Constants.DUMMY_FILLED_GUI_NAME

	local slot = props._slotGuiObjs[containerIndex]::GuiObject
	dummyGui.Parent = slot

	return dummyGui, slot
end

function IMContainer.new(name: string, capacity: number, flags: number): Container
	local _props = {
		_name = name,
		_dragging = false,
		_capacity = capacity,
		_currSize = 0,
		_HOVER_INFO = nil,
		_draggables = {},
		_slotGuiObjs = {},
        _type = "Container",
		_dummyFilledGuis = {},
		_flags = flags,
		_inItemspace = false,
		_filteringEnabled = not IMContainer.IsBitFlagEnabled(flags, BIT_FLAGS.NO_FILTERING),
		_closedContainer = IMContainer.IsBitFlagEnabled(flags, BIT_FLAGS.CLOSED),
		_draggingEnabled = not IMContainer.IsBitFlagEnabled(flags, BIT_FLAGS.NO_DRAGGING),
		_stackingEnabled = not IMContainer.IsBitFlagEnabled(flags, BIT_FLAGS.NO_STACKING),
		_CALLBACKS = {
			_ALLOWED_ITEMS_FUNC = function(item: ITEM_TYPE) return true end
		},
		_invStartIndex = 0,
		_invEndIndex = 0,

		_setHotbarKeybinds = function(self: _Container, keybinds: HotbarKeybinds)
			if self._name ~= Constants.CONTAINER_HOTBAR_NAME then
				error("[InventoryMaker]: This method can only be called for the hotbar container.")
			end

			if not self._hotbarConfigs then error("[InventoryMaker]: Cannot call this method; hotbar configs not defined.") end
			self._hotbarConfigs.Keybinds = keybinds
		end,

		_isItemAllowed = function(self: _Container, item: ITEM_TYPE): boolean
			return self._CALLBACKS._ALLOWED_ITEMS_FUNC(item)
		end,

		DefineLayout = function(self: _Container, params: GridLayoutParams): Container
			if (params.ParentContainer::GuiObject?) == nil then error("[Inventory]: A parent GUI container must be provided for grid layout.") end
			if not params.ParentContainer:IsA("GuiObject") then error("[Inventory]: The provided parent GUI container must be a GuiObject.") end

			local layout = Instance.new("UIGridLayout")
			layout.Name = Constants.GRID_LAYOUT_NAME
			layout.CellPadding = params.CellPadding or DEFAULT_CELL_PADDING
			layout.CellSize = params.CellSize or DEFAULT_CELL_SIZE
			layout.FillDirection = params.FillDirection or DEFAULT_FILL_DIRECTION
			layout.FillDirectionMaxCells = params.FillDirectionMaxCells or DEFAULT_FILL_DIRECTION_MAX_CELLS
			layout.StartCorner = params.StartCorner or DEFAULT_START_CORNER
			layout.HorizontalAlignment = params.HorizontalAlignment or DEFAULT_H_ALIGN
			layout.VerticalAlignment = params.VerticalAlignment or DEFAULT_V_ALIGN
			layout.SortOrder = Enum.SortOrder.LayoutOrder

			self._PARENT_CONTAINER = params.ParentContainer
			self._GRID_LAYOUT = layout

			return self
		end,

		DefineEmptySlot = function(self: _Container, configs: EmptyConfigs)
			self._EMPTY_TEMPLATE = configs.GuiTemplate
			self._RENDER_EMPTY_FUNC = configs.RenderFunc or nil
		end,

		DefineFilledSlot = function(self: _Container, configs: FilledConfigs)
			self._FILLED_TEMPLATE = configs.GuiTemplate
			self._RENDER_FILLED_FUNC = configs.RenderFunc
		end,

		DefineHoverPrompt = function(self: _Container, configs: HoverPromptConfigs)
			local offset: UDim2
			if configs.Offset then
				offset = UDim2.fromOffset(configs.Offset.X, configs.Offset.Y)
			else
				offset = UDim2.fromOffset(0, 0)
			end

			self._HOVER_INFO = {
				_TEMPLATE = configs.GuiTemplate,
				_FIXED_POSITION = configs.FixedPosition or false,
				_OFFSET = offset,
				_RENDER_FUNC = configs.RenderFunc,
				_OPEN_HOVER_PROMPT_AFTER_DRAGGING = configs.ShowPromptAfterDragging or false
			}::_HoverPromptInfo
		end,

		DefineActivatedCallback = function(self: _Container, callback: ActivatedCallback)
			self._CALLBACKS._ACTIVATED_FUNC = callback
		end,

		SetHotbarConfigs = function(self: _Container, hbConfigs: HotbarConfigs)
			if self._name ~= Constants.CONTAINER_HOTBAR_NAME then
				error("[InventoryMaker]: This method can only be called for the hotbar container.")
			end
			if not hbConfigs then error("[InventoryMaker]: Missing hotbar configs for hotbar container.") end
			if not hbConfigs.Keybinds then error("[InventoryMaker]: Missing hotbar keybinds.") end
			if not hbConfigs.OnKeyPress then error("[InventoryMaker]: Missing OnKeyPress callback.") end

			self._hotbarConfigs = hbConfigs
            if (self._hotbarConfigs.ScrollingHotbar::boolean?) == nil then
                self._hotbarConfigs.ScrollingHotbar = false
            end
		end,

		DefineOnItemAddedCallback = function(self: _Container, callback: OnItemAddedCallback)
			self._CALLBACKS._ON_ITEM_ADDED_FUNC = callback
		end,

		DefineAllowedItems = function(self: _Container, callback: AllowedItemsCallback)
			self._CALLBACKS._ALLOWED_ITEMS_FUNC = callback
		end,

        DefineRenderEquippedSlot = function(self: _Container, renderFunc: EquippedRenderFunc)
            self._CALLBACKS._EQUIPPED_RENDER_FUNC = renderFunc
        end
	}

	return _props::Container
end

function IMContainer.Init(container: _Container, data: { ITEM_TYPE | false },
	callback: (filledGui: GuiObject, slot: GuiObject, item: ITEM_TYPE, c: _Container, slotIndex: number, containerIndex: number) -> ()
)

	local containerIndex = 1
	for i = container._invStartIndex, container._invEndIndex do
		local item = data[i]
		if item then
			local filledGui, slot = createFilledSlot(container, i, item, containerIndex)
			callback(filledGui, slot, item, container, i, containerIndex)
		else
			createEmptySlot(container, i, containerIndex)
		end
		containerIndex += 1
	end

	container._PARENT_CONTAINER:SetAttribute(Constants.CONTAINER_ATTRIBUTE_NAME, container._name)
	container._GRID_LAYOUT.Parent = container._PARENT_CONTAINER
end

--[[
	Destroys the entire slot at the index (including the filledGui, if there is one),
	creates a new slot in its place, creates the filledGui in the new slot and runs the callback after.
]]
function IMContainer.AddItemAtIndex(container: _Container, slotIndex: number, item: ITEM_TYPE,
	callback: (newFilledGui: GuiObject, newSlot: GuiObject, itemRef: ITEM_TYPE, containerRef: _Container, slotIndex: number, containerIndex: number) -> ()
)
	local containerIndex = getContainerIndexFromSlotIndex(container, slotIndex)
	destroySlot(container, slotIndex, false)
	local newFilledGui, newSlot = createFilledSlot(container, slotIndex, item, containerIndex)
	callback(newFilledGui, newSlot, item, container, slotIndex, containerIndex)

	local onItemAddedCallback = container._CALLBACKS._ON_ITEM_ADDED_FUNC
	if onItemAddedCallback then
		onItemAddedCallback(item, newFilledGui, {
			SlotIndex = slotIndex,
			ContainerIndex = containerIndex,
			ContainerName = container._name
		})
	end
end

--[[
	Destroys the filled GUI at the slot index and decreases the current size of the container.
	Destroying the filled GUI also destroys its draggable, if any, as well and removes it
	from the dictionary of draggables maintained by the container
]]
function IMContainer.DestroyFilledGui(container: _Container, slotIndex: number)
	destroyFilledGui(container, slotIndex)
end

--[[
	Destroy the entire slot at the slotIndex. If there is a filledGui at the
	slot, its draggable will be destroyed.

	decreaseCapacity should be true if this function is being used to decrease
	the container's capacity, which will set the slot in _slotGuiObjs to nil,
	instead of false, after it has been destroyed.

	If decreaseCapacity is false, then the slot will just be set to false, since
	that means a new slot would be created to take its place.
]]
function IMContainer.DestroySlot(container: _Container, slotIndex: number, decreaseCapacity: boolean)
	destroySlot(container, slotIndex, decreaseCapacity)
end

--[[
	Shifts the indicies of the _slotGuiObjs array so that there are no holes--
	nil values--from when .DestroySlot() was called with `decreaseCapacity` set to true
]]
function IMContainer.ReconcileSlotGuiObjs(container: _Container, prevSlotGuiObjsLen: number, nilCount: number)
	local slots = container._slotGuiObjs

	local newSlotGuiObjsArr = table.create(prevSlotGuiObjsLen-nilCount, false)::{ [number]: GuiObject | false }
	local index = 1
	for i = 1, prevSlotGuiObjsLen do
		local slot = slots[i]
		if slot == nil then continue end
		newSlotGuiObjsArr[index] = slot
		index += 1
	end

	container._slotGuiObjs = newSlotGuiObjsArr
end

--[[
	Creates a dummy filled gui at the slotIndex given the item. A dummy filled gui is not meant to be
	interactable; it is completely visual. Creating a dummy filled gui at the slotIndex requires there
	be an actual filledGui at the slotIndex, aka it must be actual filled slot.

	Creating a dummy filled gui DOES NOT:
	- affect container size
	- create a new draggable
]]
function IMContainer.CreateDummyFilledGui(container: _Container, slotIndex: number, item: ITEM_TYPE): GuiObject
	local containerIndex = getContainerIndexFromSlotIndex(container, slotIndex)
	if not container._slotGuiObjs[containerIndex] then error("No filled gui at that slotIndex") end

	local newDummyFilledGui = createDummyFilledGui(container, slotIndex, item, containerIndex)
	container._dummyFilledGuis[containerIndex] = newDummyFilledGui

	return newDummyFilledGui
end

--[[
	Destroys the dummy filled gui at the slotIndex that was created with CreateDummyFilledGui()
]]
function IMContainer.DestroyDummyFilledGui(container: _Container, slotIndex: number)
	local containerIndex = getContainerIndexFromSlotIndex(container, slotIndex)
	local dummyFilledGui = container._dummyFilledGuis[containerIndex]
	if not dummyFilledGui then error("No dummy filled gui at slotIndex") end

	dummyFilledGui:Destroy()
	container._dummyFilledGuis[containerIndex] = nil
end

--[[
	Creates x empty slots appended to the container, increases the capacity by x, and
	updates the container's _invEndIndex to account for the empty slots
]]
function IMContainer.AppendNewEmptySlots(container: _Container, amount: number)
	container._capacity += amount

	local startIndex = container._invEndIndex+1
	local endIndex = startIndex+amount-1
	container._invEndIndex = endIndex

	local containerIndex = getContainerIndexFromSlotIndex(container, startIndex)
	for i = startIndex, endIndex do
		createEmptySlot(container, i, containerIndex)
		containerIndex += 1
	end
end

function IMContainer.GetHoverPromptSingleton(container: _Container | _StaticContainer, screenGui: ScreenGui): GuiObject?
	local hoverInfo = container._HOVER_INFO
	if not hoverInfo then return nil end

	local gui = hoverInfo._GUI
	if not gui then
		gui = (hoverInfo._TEMPLATE::GuiObject):Clone()
		hoverInfo._GUI = gui
		hoverInfo._INSET = hoverInfo._FIXED_POSITION and -UDim2.new(0, 0, 0, Utility.GetGuiInsetY(screenGui.IgnoreGuiInset)) or
			UDim2.new(0, 0, 0, Utility.GetGuiInsetY(not screenGui.IgnoreGuiInset))
		gui.Name = Constants.HOVER_NAME_PREFIX..container._name
		gui.Visible = false
		gui.AnchorPoint = Vector2.zero

        if container._type == "Container" then
            gui.ZIndex = 100
        end

		gui.Parent = screenGui
		container._hoverPromptParent = screenGui
	end

	return gui
end

function IMContainer.FixedHoverPrompt(container: _Container | _StaticContainer, prompt: GuiObject, item: ITEM_TYPE, filledGui: GuiObject, slotIndex: number, containerIndex: number)
	local hoverInfo = container._HOVER_INFO::_HoverPromptInfo?
	if not hoverInfo then return end

    local itemPositionInfo: ItemPositionInfo = {
        ContainerName = container._name,
        ContainerIndex = containerIndex,
        SlotIndex = slotIndex
    }

	prompt.Visible = true
	if hoverInfo._RENDER_FUNC then
		local cleanupFunc = hoverInfo._RENDER_FUNC(item, prompt, itemPositionInfo)
        hoverInfo._RENDER_FUNC_CLEANUP = cleanupFunc
	end

	local pos = filledGui.AbsolutePosition
	local size = filledGui.AbsoluteSize

	local promptSize = prompt.AbsoluteSize
	local hoverOffset = hoverInfo._OFFSET
	local rightSideX = pos.X + size.X + promptSize.X + hoverOffset.X.Offset

	local finalPromptPos = prompt.Position
	if rightSideX > camera.ViewportSize.X then -- position hover prompt on the left side instead
		finalPromptPos = UDim2.fromOffset(pos.X - promptSize.X, pos.Y)
			- hoverInfo._INSET::UDim2
			+ UDim2.fromOffset(-hoverOffset.X.Offset, hoverOffset.Y.Offset)
	else
		finalPromptPos = UDim2.fromOffset(pos.X + size.X, pos.Y) + hoverOffset - hoverInfo._INSET::UDim2
	end

	local somePadding = 5
	local ignoreGuiInset = container._hoverPromptParent.IgnoreGuiInset
	local guiInsetY = Utility.GetGuiInsetY(ignoreGuiInset)
	local bottomSideY = finalPromptPos.Y.Offset + promptSize.Y + somePadding + guiInsetY
	local viewportY = camera.ViewportSize.Y - Utility.GetGuiInsetY(not ignoreGuiInset)

	if bottomSideY > viewportY then
		local offset = (promptSize.Y - size.Y) - 1
		finalPromptPos -= UDim2.new(0, 0, 0, offset)
	end

	prompt.Position = UDim2.fromOffset(finalPromptPos.X.Offset, math.max(guiInsetY, finalPromptPos.Y.Offset))
end

function IMContainer.NonFixedHoverPrompt(container: _Container | _StaticContainer, prompt: GuiObject, item: ITEM_TYPE, filledGui: GuiObject, mouse: Vector2, slotIndex: number, containerIndex: number)
	local hoverInfo = container._HOVER_INFO::_HoverPromptInfo?
	if not hoverInfo then return end

    local itemPositionInfo: ItemPositionInfo = {
        ContainerName = container._name,
        ContainerIndex = containerIndex,
        SlotIndex = slotIndex,
    }

	if not prompt.Visible then
		prompt.Visible = true::any
		if hoverInfo._RENDER_FUNC then
			local cleanupFunc = hoverInfo._RENDER_FUNC(item, prompt, itemPositionInfo)
            hoverInfo._RENDER_FUNC_CLEANUP = cleanupFunc
		end
	end

	local promptSize = prompt.AbsoluteSize
	local hoverOffset = hoverInfo._OFFSET
	local rightSideX = mouse.X + promptSize.X + hoverOffset.X.Offset
    local inset = 0
    if not container._hoverPromptParent.IgnoreGuiInset then
        inset = Utility.GetGuiInsetY(not container._hoverPromptParent.IgnoreGuiInset)
    end

	if rightSideX > camera.ViewportSize.X then -- position hover prompt on the left side instead
		prompt.Position = UDim2.fromOffset(mouse.X - promptSize.X, mouse.Y - inset) + UDim2.fromOffset(-hoverOffset.X.Offset, hoverOffset.Y.Offset)
	else
		prompt.Position = UDim2.fromOffset(mouse.X, mouse.Y - inset) + hoverInfo._OFFSET
	end

    if container._hoverPromptParent.IgnoreGuiInset then
        inset = Utility.GetGuiInsetY(container._hoverPromptParent.IgnoreGuiInset)
    end
	local promptPos = prompt.AbsolutePosition
	local somePadding = 5
	local bottomSideY = promptPos.Y + promptSize.Y + inset + somePadding
	local viewportY = camera.ViewportSize.Y

	if bottomSideY > viewportY then
		local offset = promptSize.Y
		prompt.Position -= UDim2.new(0, 0, 0, offset)
	end

	local finalPos = prompt.Position
	prompt.Position = UDim2.fromOffset(finalPos.X.Offset, math.max(inset, finalPos.Y.Offset))
end

function IMContainer.HideHoverPrompt(container: _Container | _StaticContainer, prompt: GuiObject?)
	if not prompt then return end
    if not prompt.Visible then return end
	prompt.Visible = false

    local hoverInfo = container._HOVER_INFO::_HoverPromptInfo?
    if not hoverInfo then return end

    local hoverCleanupFunc = hoverInfo._RENDER_FUNC_CLEANUP
    if hoverCleanupFunc then
        hoverCleanupFunc()
    end
end

function IMContainer.GetContainerIndexFromSlotIndex(container: _Container, slotIndex: number)
	return getContainerIndexFromSlotIndex(container, slotIndex)
end

function IMContainer.GetFilledGuiFromIndex(container: _Container, slotIndex: number): GuiObject?
	local slotGui = container._slotGuiObjs[getContainerIndexFromSlotIndex(container, slotIndex)]
	if not slotGui then return nil end
	return slotGui:FindFirstChild(Constants.FILLED_GUI_NAME)::GuiObject?
end

function IMContainer.IsBitFlagEnabled(containerFlags: number, bitFlag: number): boolean
	return bit32.band(containerFlags, bitFlag) == bitFlag
end

--[[
	Changes the name, layout order, and slot attribute of the slot to match the new slot index of the slot gui.

	Rerenders the empty slot.
]]
function IMContainer.UpdateSlotIndexOfSlot(container: _Container, oldSlotIndex: number, newSlotIndex: number)
	local containerIndex = getContainerIndexFromSlotIndex(container, oldSlotIndex)
	local slot = container._slotGuiObjs[containerIndex]
	if not slot then error("No slot to update") end

    local renderEmptyFunc = container._RENDER_EMPTY_FUNC
    if renderEmptyFunc then
        renderEmptyFunc(newSlotIndex, slot, containerIndex)
    end
	slot:SetAttribute(Constants.SLOT_INDEX_ATTRIBUTE_NAME, newSlotIndex)
	slot.LayoutOrder = newSlotIndex
	slot.Name = Constants.SLOT_NAME_PREFIX..tostring(newSlotIndex)

end

function IMContainer.GetSlotFromSlotIndex(container: _Container, slotIndex: number): GuiObject
	local containerIndex = getContainerIndexFromSlotIndex(container, slotIndex)
	local slot = container._slotGuiObjs[containerIndex]
	if not slot then
		error(string.format("[InventoryMaker] (internal): no slot gui at slot index %d", slotIndex))
	end
	return slot
end

function IMContainer.GetSlotIndexFromContainerIndex(container: _Container, containerIndex: number)
    return container._invStartIndex + containerIndex  - 1
end

return IMContainer


--!strict
--[[
	DraggableUI forked from v1.3.3 (used in InventoryMaker v1.0.0) 5/17/2025
	------------------------------------------------------------------------

	Forked Version Updates:
	--------------
	v1.3.5 5/18/25
		- set z indes on input began instead of when drag initially starts

	v1.3.4 5/17/25
		- add _currDraggable to limit only 1 gui object being dragged at a time
]]
local DraggableUI = {
	DEBUG_PRINT_MODE = false
}

local UIS = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer::Player
local playerGui = player.PlayerGui

local IMInput = require(script.Parent.IMInput)

export type Draggable = {
	ID: number,
	--[[
		If BeforeDragStart returns true, the drag will start, else it will not start.
	]]
	BeforeDragStart: (() -> boolean)?,
	OnDragStart: (() -> ())?,
	OnDragMove: ((mousePos: Vector2) -> ())?,
	OnDragEnd: ((mousePos: Vector2, guisAtPos: { GuiObject }, wasDragged: boolean) -> ())?
}

type _Draggable = {
	_instance: GuiObject,
	_dragStartInputDown: boolean,
	_dragging: boolean,
	_parent: any,
	_size: Vector2,
	_anchorPoint: Vector2,
	_pos: Vector2,
	_zIndex: number,
	_dragParent: ScreenGui,
	_parentSize: UDim2,
	_parentPos: UDim2,
	_moveCon: RBXScriptConnection?,
	_inputBegan: RBXScriptConnection?,
	_inputEnded: RBXScriptConnection?,
	_guiInsetIgnored: boolean,
	_dragFromCenter: boolean,
}

local _objs: { [number]: _Draggable | boolean } = {}
local _currDraggable: Draggable? = nil

local function disconnectCon(con: RBXScriptConnection?)
	if con and con.Connected then
		con:Disconnect()
	end
end

local function revertToParentProps(_props: _Draggable)
	local guiObj =_props._instance
	guiObj.Parent = _props._parent
	guiObj.Size = _props._parentSize
	guiObj.Position = _props._parentPos
	guiObj.ZIndex = _props._zIndex
	guiObj.AnchorPoint = _props._anchorPoint
end

local function saveParentProps(_props: _Draggable)
	local guiObj =_props._instance
	_props._size = guiObj.AbsoluteSize
	_props._pos = guiObj.AbsolutePosition

	_props._parent = guiObj.Parent
	_props._parentSize = guiObj.Size
	_props._parentPos = guiObj.Position
	_props._zIndex = guiObj.ZIndex
	_props._anchorPoint = guiObj.AnchorPoint
end

function DraggableUI.new(guiObj: GuiObject, dragFromCenter: boolean?): Draggable
	local screenGui = guiObj:FindFirstAncestorWhichIsA("ScreenGui") -- used as the parent to guiObj when dragging
	if not screenGui then error("Could not find an ancestor screenGui to anchor the draggable object", 0) end

	local _props = {
		_instance = guiObj,
		_dragStartInputDown = false,
		_dragging = false,
		_parent = nil,
		_anchorPoint = Vector2.zero,
		_size = Vector2.zero,
		_pos = Vector2.zero,
		_zIndex = 1,
		_dragParent = screenGui,
		_parentSize = UDim2.new(),
		_parentPos = UDim2.new(),
		_dragFromCenter = dragFromCenter or false,
		_guiInsetIgnored = screenGui.IgnoreGuiInset::boolean
	}::_Draggable

	local id
	local vacantSpotFilled = false
	for i,v in ipairs(_objs) do
		if v then continue end
		_objs[i] = _props
		vacantSpotFilled = true
		id = i
		break
	end

	if not vacantSpotFilled then
		table.insert(_objs, _props)
		id = #_objs
	end

	local props = {
		ID = id
	}::Draggable

	if DraggableUI.DEBUG_PRINT_MODE then
		warn("Draggable created; ID =", props.ID)
	end

	return props
end

function DraggableUI.IsDragging(draggableObj: Draggable): boolean
	local _props: _Draggable = _objs[draggableObj.ID]::_Draggable
	return _props._dragging
end

function DraggableUI.Enable(draggableObj: Draggable)
	local _props: _Draggable = _objs[draggableObj.ID]::_Draggable
	local guiObj = _props._instance

	local canStartDrag = true
	local dragFromCenter = _props._dragFromCenter

	_props._inputBegan = guiObj.InputBegan:Connect(function(input: InputObject)
		if IMInput.IsInputActive(IMInput.DRAG_START, input) then
			if draggableObj.BeforeDragStart then
				canStartDrag = draggableObj.BeforeDragStart()
			end

			if not canStartDrag then return end
			if _currDraggable then return end

			_props._dragStartInputDown = true
            _currDraggable = draggableObj
            if _props._dragParent.IgnoreGuiInset ~= _props._guiInsetIgnored then
                _props._guiInsetIgnored = _props._dragParent.IgnoreGuiInset
            end

			saveParentProps(_props)
			local guiInset = _props._guiInsetIgnored and GuiService:GetGuiInset() or Vector2.zero
			local initialMousePos = UIS:GetMouseLocation()
			local offset = _props._pos - initialMousePos + guiInset -- used for when dragFromCenter = false
			guiObj.ZIndex = 100

			_props._moveCon = UIS.InputChanged:Connect(function(uisInput: InputObject)
				if IMInput.IsInputActive(IMInput.DRAG_MOVE, uisInput) and _props._dragStartInputDown and _currDraggable == draggableObj then
					if not _props._dragging then
						guiObj.Parent = _props._dragParent
						guiObj.Size = UDim2.new(0, _props._size.X, 0, _props._size.Y)

						if dragFromCenter then
							guiObj.AnchorPoint = Vector2.new(0.5, 0.5)
							guiObj.Position = UDim2.new(0, initialMousePos.X, 0, initialMousePos.Y)
						else
							guiObj.AnchorPoint = Vector2.zero
							guiObj.Position = UDim2.new(0, math.ceil(_props._pos.X), 0, (_props._pos.Y+guiInset.Y))
						end

						if draggableObj.OnDragStart then task.spawn(draggableObj.OnDragStart) end
					end

					_props._dragging = true
					local mouse = UIS:GetMouseLocation()
					if dragFromCenter then
						guiObj.Position = UDim2.new(0, mouse.X, 0, mouse.Y)
					else
						guiObj.Position = UDim2.new(0, mouse.X + offset.X, 0, mouse.Y + offset.Y)
					end
					if draggableObj.OnDragMove then task.spawn(draggableObj.OnDragMove, mouse) end
				end
			end)
		end
	end)

	_props._inputEnded = guiObj.InputEnded:Connect(function(input: InputObject)
		if IMInput.IsInputActive(IMInput.DRAG_END, input) then
			if not canStartDrag then return end
			if _currDraggable ~= draggableObj then return end

			_props._dragStartInputDown = false
			_currDraggable = nil
			revertToParentProps(_props)
			disconnectCon(_props._moveCon)

			local wasDragged = _props._dragging
			_props._dragging = false

			if draggableObj.OnDragEnd then
				local mousePos = UIS:GetMouseLocation()
				local guisAtPos = playerGui:GetGuiObjectsAtPosition(mousePos.X, mousePos.Y - GuiService:GetGuiInset().Y)
				task.spawn(draggableObj.OnDragEnd, mousePos, guisAtPos, wasDragged)
			end
		end
	end)
end

function DraggableUI.Disable(draggableObj: Draggable)
	local _props: _Draggable = _objs[draggableObj.ID]::_Draggable
	_props._dragStartInputDown = false

	if _props._instance.Parent ~= nil and _props._dragging then
		revertToParentProps(_props)
	end
	disconnectCon(_props._inputBegan)
	disconnectCon(_props._inputEnded)
	disconnectCon(_props._moveCon)
end

function DraggableUI.Destroy(draggableObj: Draggable)
	if _objs[draggableObj.ID] == nil then error("[DragagbleUI]: Attempt to call DraggableUI.Destroy on nil;", 0) end

	DraggableUI.Disable(draggableObj)
	_objs[draggableObj.ID] = false
end

return DraggableUI


--!strict
local IMFilter = {}

local IMContainer = require(script.Parent.IMContainer)
local IMUserType = require(script.Parent.IMUserType)

export type CustomFilterFunc = (slotIndex: number, item: IMUserType.USER_ITEM_TYPE, containerIndex: number) -> boolean

local function searchFilter(
	container: IMContainer._Container,
	text: string,
	filterFunc: (text: string, item: any) -> boolean,
	data: any,
	startIdx: number, endIdx: number
)
	local slots = container._slotGuiObjs

	for i = startIdx, endIdx do
		local item = data[i]
		local slot = slots[IMContainer.GetContainerIndexFromSlotIndex(container, i)]::GuiObject

		local check = item and filterFunc(text, item) or false

		if not check and text ~= "" then
			slot.Visible = false
		else
			slot.Visible = true
		end
	end

end

function IMFilter.ConnectSearchBarEvent(
	containers: { IMContainer._Container },
	inputGui: TextBox, textFunc: (text: string) -> string, filterFunc: (text: string, item: any) -> boolean,
	data: any
): RBXScriptConnection

	local updating = false

	return inputGui:GetPropertyChangedSignal("Text"):Connect(function()
		if updating then return end

		local text = textFunc(inputGui.Text)
		updating = true

		inputGui.Text = text

		for i = 1, #containers do
			local container = containers[i]
			if not container._filteringEnabled then continue end
			searchFilter(container::IMContainer._Container, text, filterFunc, data, container._invStartIndex, container._invEndIndex)
		end

		task.defer(function()
			updating = false
		end)
	end)
end

function IMFilter.ApplyFilterToContainer(container: IMContainer._Container, filterFunc: CustomFilterFunc, data: any)
	local slots = container._slotGuiObjs
	local startIdx = container._invStartIndex
	local endIdx = container._invEndIndex

	for slotIndex = startIdx, endIdx do
		local item = data[slotIndex]::(IMUserType.USER_ITEM_TYPE | false)
		local containerIndex = IMContainer.GetContainerIndexFromSlotIndex(container, slotIndex)
		local slot = slots[containerIndex]::GuiObject

		local check = item and filterFunc(slotIndex, item, containerIndex) or false

		if not check then
			slot.Visible = false
		else
			slot.Visible = true
		end
	end
end

function IMFilter.ClearFilterFromContainer(container: IMContainer._Container)
	local slots = container._slotGuiObjs

	for i = 1, #slots do
		local slot = slots[i]
		if slot ~= false and slot.Visible == false then
			slot.Visible = true
		end
	end
end

return IMFilter