---@class Element
---@field id string
---@field autosizing {width:boolean, height:boolean} -- Whether the element should automatically size to fit its children
---@field x number|string -- X coordinate of the element
---@field y number|string -- Y coordinate of the element
---@field z number -- Z-index for layering (default: 0)
---@field width number|string -- Width of the element
---@field height number|string -- Height of the element
---@field top number? -- Offset from top edge (CSS-style positioning)
---@field right number? -- Offset from right edge (CSS-style positioning)
---@field bottom number? -- Offset from bottom edge (CSS-style positioning)
---@field left number? -- Offset from left edge (CSS-style positioning)
---@field children table<integer, Element> -- Children of this element
---@field parent Element? -- Parent element (nil if top-level)
---@field border Border -- Border configuration for the element
---@field opacity number
---@field borderColor Color -- Color of the border
---@field backgroundColor Color -- Background color of the element
---@field cornerRadius number|{topLeft:number?, topRight:number?, bottomLeft:number?, bottomRight:number?}? -- Corner radius for rounded corners (default: 0)
---@field prevGameSize {width:number, height:number} -- Previous game size for resize calculations
---@field text string? -- Text content to display in the element
---@field textColor Color -- Color of the text content
---@field textAlign TextAlign -- Alignment of the text content
---@field gap number|string -- Space between children elements (default: 10)
---@field padding {top?:number, right?:number, bottom?:number, left?:number}? -- Padding around children (default: {top=0, right=0, bottom=0, left=0})
---@field margin {top?:number, right?:number, bottom?:number, left?:number} -- Margin around children (default: {top=0, right=0, bottom=0, left=0})
---@field positioning Positioning -- Layout positioning mode (default: RELATIVE)
---@field flexDirection FlexDirection -- Direction of flex layout (default: HORIZONTAL)
---@field justifyContent JustifyContent -- Alignment of items along main axis (default: FLEX_START)
---@field alignItems AlignItems -- Alignment of items along cross axis (default: STRETCH)
---@field alignContent AlignContent -- Alignment of lines in multi-line flex containers (default: STRETCH)
---@field flexWrap FlexWrap -- Whether children wrap to multiple lines (default: NOWRAP)
---@field justifySelf JustifySelf -- Alignment of the item itself along main axis (default: AUTO)
---@field alignSelf AlignSelf -- Alignment of the item itself along cross axis (default: AUTO)
---@field textSize number? -- Resolved font size for text content in pixels
---@field minTextSize number?
---@field maxTextSize number?
---@field fontFamily string? -- Font family name from theme or path to font file
---@field autoScaleText boolean -- Whether text should auto-scale with window size (default: true)
---@field transform TransformProps -- Transform properties for animations and styling
---@field transition TransitionProps -- Transition settings for animations
---@field onEvent fun(element:Element, event:InputEvent)? -- Callback function for interaction events
---@field onEventDeferred boolean? -- Whether onEvent callback should be deferred until after canvases are released (default: false)
---@field onFocus fun(element:Element)? -- Callback function when element receives focus
---@field onFocusDeferred boolean? -- Whether onFocus callback should be deferred (default: false)
---@field onBlur fun(element:Element)? -- Callback function when element loses focus
---@field onBlurDeferred boolean? -- Whether onBlur callback should be deferred (default: false)
---@field onTextInput fun(element:Element, text:string)? -- Callback function for text input
---@field onTextInputDeferred boolean? -- Whether onTextInput callback should be deferred (default: false)
---@field onTextChange fun(element:Element, text:string)? -- Callback function when text changes
---@field onTextChangeDeferred boolean? -- Whether onTextChange callback should be deferred (default: false)
---@field onEnter fun(element:Element)? -- Callback function when Enter key is pressed
---@field onEnterDeferred boolean? -- Whether onEnter callback should be deferred (default: false)
---@field units table -- Original unit specifications for responsive behavior
---@field _eventHandler EventHandler -- Event handler instance for input processing
---@field _explicitlyAbsolute boolean?
---@field _originalPositioning Positioning? -- Original positioning value set by user
---@field gridRows number? -- Number of rows in the grid
---@field gridColumns number? -- Number of columns in the grid
---@field columnGap number|string? -- Gap between grid columns
---@field rowGap number|string? -- Gap between grid rows
---@field theme string? -- Theme component to use for rendering
---@field themeComponent string?
---@field _themeState string? -- Current theme state (normal, hover, pressed, active, disabled)
---@field _themeManager ThemeManager -- Internal: theme manager instance
---@field _stateId string? -- State manager ID for this element
---@field disabled boolean? -- Whether the element is disabled (default: false)
---@field active boolean? -- Whether the element is active/focused (for inputs, default: false)
---@field disableHighlight boolean? -- Whether to disable the pressed state highlight overlay (default: false)
---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Multiplier for auto-sized content dimensions
---@field scaleCorners number? -- Scale multiplier for 9-patch corners/edges. E.g., 2 = 2x size (overrides theme setting)
---@field scalingAlgorithm "nearest"|"bilinear"? -- Scaling algorithm for 9-patch corners: "nearest" (sharp/pixelated) or "bilinear" (smooth) (overrides theme setting)
---@field contentBlur {radius:number, quality:number?}? -- Blur the element's content including children (radius: pixels, quality: 1-10, default: 5)
---@field backdropBlur {radius:number, quality:number?}? -- Blur content behind the element (radius: pixels, quality: 1-10, default: 5)
---@field _blurInstance table? -- Internal: cached blur effect instance
---@field editable boolean -- Whether the element is editable (default: false)
---@field multiline boolean -- Whether the element supports multiple lines (default: false)
---@field textWrap boolean|"word"|"char" -- Text wrapping mode (default: false for single-line, "word" for multi-line)
---@field maxLines number? -- Maximum number of lines (default: nil)
---@field maxLength number? -- Maximum text length in characters (default: nil)
---@field placeholder string? -- Placeholder text when empty (default: nil)
---@field passwordMode boolean -- Whether to display text as password (default: false)
---@field inputType "text"|"number"|"email"|"url" -- Input type for validation (default: "text")
---@field textOverflow "clip"|"ellipsis"|"scroll" -- Text overflow behavior (default: "clip")
---@field scrollable boolean -- Whether text is scrollable (default: false for single-line, true for multi-line)
---@field autoGrow boolean -- Whether element auto-grows with text (default: false)
---@field selectOnFocus boolean -- Whether to select all text on focus (default: false)
---@field cursorColor Color? -- Cursor color (default: nil, uses textColor)
---@field selectionColor Color? -- Selection background color (default: nil, uses theme or default)
---@field cursorBlinkRate number -- Cursor blink rate in seconds (default: 0.5)
---@field _cursorPosition number? -- Internal: cursor character position (0-based)
---@field _cursorLine number? -- Internal: cursor line number (1-based)
---@field _cursorColumn number? -- Internal: cursor column within line
---@field _cursorBlinkTimer number? -- Internal: cursor blink timer
---@field _cursorVisible boolean? -- Internal: cursor visibility state
---@field _cursorBlinkPaused boolean? -- Internal: whether cursor blink is paused (e.g., while typing)
---@field _cursorBlinkPauseTimer number? -- Internal: timer for how long cursor blink has been paused
---@field _selectionStart number? -- Internal: selection start position
---@field _selectionEnd number? -- Internal: selection end position
---@field _selectionAnchor number? -- Internal: selection anchor point
---@field _focused boolean? -- Internal: focus state
---@field _textBuffer string? -- Internal: text buffer for editable elements
---@field _lines table? -- Internal: split lines for multi-line text
---@field _wrappedLines table? -- Internal: wrapped line data
---@field _textDirty boolean? -- Internal: flag to recalculate lines/wrapping
---@field _textEditor TextEditor? -- Internal: TextEditor instance for editable elements
---@field imagePath string? -- Path to image file (auto-loads via ImageCache)
---@field image love.Image? -- Image object to display
---@field objectFit "fill"|"contain"|"cover"|"scale-down"|"none"? -- Image fit mode (default: "fill")
---@field objectPosition string? -- Image position like "center center", "top left", "50% 50%" (default: "center center")
---@field imageOpacity number? -- Image opacity 0-1 (default: 1, combines with element opacity)
---@field imageRepeat "no-repeat"|"repeat"|"repeat-x"|"repeat-y"|"space"|"round"? -- Image repeat/tiling mode (default: "no-repeat")
---@field imageTint Color? -- Color to tint the image (default: nil/white, no tint)
---@field onImageLoad fun(element:Element, image:love.Image)? -- Callback when image loads successfully
---@field onImageLoadDeferred boolean? -- Whether onImageLoad callback should be deferred (default: false)
---@field onImageError fun(element:Element, error:string)? -- Callback when image fails to load
---@field onImageErrorDeferred boolean? -- Whether onImageError callback should be deferred (default: false)
---@field _loadedImage love.Image? -- Internal: cached loaded image
---@field hideScrollbars boolean|{vertical:boolean, horizontal:boolean}? -- Hide scrollbars (boolean for both, or table for individual control)
---@field userdata table?
---@field _renderer Renderer -- Internal: Renderer instance for visual rendering
---@field _layoutEngine LayoutEngine -- Internal: LayoutEngine instance for layout calculations
---@field _scrollManager ScrollManager? -- Internal: ScrollManager instance for scroll handling
---@field _borderBoxWidth number? -- Internal: cached border-box width
---@field _borderBoxHeight number? -- Internal: cached border-box height
---@field overflow string? -- Overflow behavior for both axes
---@field overflowX string? -- Overflow behavior for horizontal axis
---@field overflowY string? -- Overflow behavior for vertical axis
---@field scrollbarWidth number? -- Scrollbar width in pixels
---@field scrollbarColor Color? -- Scrollbar thumb color
---@field scrollbarTrackColor Color? -- Scrollbar track color
---@field scrollbarRadius number? -- Scrollbar corner radius
---@field scrollbarPadding number? -- Scrollbar padding from edges
---@field scrollSpeed number? -- Scroll speed multiplier
---@field _overflowX boolean? -- Internal: whether content overflows horizontally
---@field _overflowY boolean? -- Internal: whether content overflows vertically
---@field _contentWidth number? -- Internal: total content width
---@field _contentHeight number? -- Internal: total content height
---@field _scrollX number? -- Internal: horizontal scroll position
---@field _scrollY number? -- Internal: vertical scroll position
---@field _maxScrollX number? -- Internal: maximum horizontal scroll
---@field _maxScrollY number? -- Internal: maximum vertical scroll
---@field _scrollbarHoveredVertical boolean? -- Internal: vertical scrollbar hover state
---@field _scrollbarHoveredHorizontal boolean? -- Internal: horizontal scrollbar hover state
---@field _scrollbarDragging boolean? -- Internal: scrollbar dragging state
---@field _hoveredScrollbar table? -- Internal: currently hovered scrollbar info
---@field _scrollbarDragOffset number? -- Internal: scrollbar drag offset
---@field _scrollbarPressHandled boolean? -- Internal: scrollbar press handled flag
---@field _pressed table? -- Internal: button press state tracking
---@field _mouseDownPosition number? -- Internal: mouse down position for drag tracking
---@field _textDragOccurred boolean? -- Internal: whether text drag occurred
---@field animation table? -- Animation instance for this element
local Element = {}
Element.__index = Element

---Initialize Element module with required dependencies
---@param deps table Dependency table containing all required modules
function Element.init(deps)
  Element._ErrorHandler = deps.ErrorHandler
  Element._Color = deps.Color
  Element._Context = deps.Context
  Element._Units = deps.Units
  Element._utils = deps.utils
  Element._InputEvent = deps.InputEvent
  Element._EventHandler = deps.EventHandler
  Element._Renderer = deps.Renderer
  Element._LayoutEngine = deps.LayoutEngine
  Element._TextEditor = deps.TextEditor
  Element._ScrollManager = deps.ScrollManager
  Element._Theme = deps.Theme
  Element._RoundedRect = deps.RoundedRect
  Element._NinePatch = deps.NinePatch
  Element._ImageRenderer = deps.ImageRenderer
  Element._ImageCache = deps.ImageCache
  Element._ImageScaler = deps.ImageScaler
  Element._Blur = deps.Blur
  Element._Transform = deps.Transform
  Element._Grid = deps.Grid
  Element._StateManager = deps.StateManager
  Element._GestureRecognizer = deps.GestureRecognizer
  Element._Performance = deps.Performance
end

---@param props ElementProps
---@return Element
function Element.new(props)
  local self = setmetatable({}, Element)

  -- Create dependency subsets for sub-modules (defined once, used throughout)
  local eventHandlerDeps = {
    InputEvent = Element._InputEvent,
    Context = Element._Context,
    utils = Element._utils,
  }

  local rendererDeps = {
    Color = Element._Color,
    RoundedRect = Element._RoundedRect,
    NinePatch = Element._NinePatch,
    ImageRenderer = Element._ImageRenderer,
    ImageCache = Element._ImageCache,
    Theme = Element._Theme,
    Blur = Element._Blur,
    Transform = Element._Transform,
    utils = Element._utils,
  }

  local layoutEngineDeps = {
    utils = Element._utils,
    Grid = Element._Grid,
    Units = Element._Units,
    Context = Element._Context,
    ErrorHandler = Element._ErrorHandler,
  }

  local textEditorDeps = {
    Context = Element._Context,
    StateManager = Element._StateManager,
    Color = Element._Color,
    utils = Element._utils,
  }

  local scrollManagerDeps = {
    utils = Element._utils,
    Color = Element._Color,
  }

  -- Normalize flexDirection: convert "row"→"horizontal", "column"→"vertical"
  if props.flexDirection == "row" then
    props.flexDirection = "horizontal"
  elseif props.flexDirection == "column" then
    props.flexDirection = "vertical"
  end

  -- Normalize padding: convert single value to table with all sides
  if props.padding ~= nil and type(props.padding) ~= "table" then
    local singleValue = props.padding
    props.padding = {
      top = singleValue,
      right = singleValue,
      bottom = singleValue,
      left = singleValue,
    }
  end

  -- Normalize margin: convert single value to table with all sides
  if props.margin ~= nil and type(props.margin) ~= "table" then
    local singleValue = props.margin
    props.margin = {
      top = singleValue,
      right = singleValue,
      bottom = singleValue,
      left = singleValue,
    }
  end

  self.children = {}
  self.onEvent = props.onEvent

  -- Auto-generate ID in immediate mode if not provided
  if Element._Context._immediateMode and (not props.id or props.id == "") then
    self.id = Element._StateManager.generateID(props, props.parent)
  else
    self.id = props.id or ""
  end

  self.userdata = props.userdata

  self.onFocus = props.onFocus
  self.onFocusDeferred = props.onFocusDeferred or false
  self.onBlur = props.onBlur
  self.onBlurDeferred = props.onBlurDeferred or false
  self.onTextInput = props.onTextInput
  self.onTextInputDeferred = props.onTextInputDeferred or false
  self.onTextChange = props.onTextChange
  self.onTextChangeDeferred = props.onTextChangeDeferred or false
  self.onEnter = props.onEnter
  self.onEnterDeferred = props.onEnterDeferred or false

  -- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
  self._stateId = self.id

  -- In immediate mode, restore EventHandler state from StateManager
  local eventHandlerConfig = {
    onEvent = self.onEvent,
    onEventDeferred = props.onEventDeferred,
  }
  if Element._Context._immediateMode and self._stateId and self._stateId ~= "" then
    local state = Element._StateManager.getState(self._stateId)
    if state then
      -- Restore EventHandler state from StateManager (sparse storage - provide defaults)
      eventHandlerConfig._pressed = state._pressed or {}
      eventHandlerConfig._lastClickTime = state._lastClickTime
      eventHandlerConfig._lastClickButton = state._lastClickButton
      eventHandlerConfig._clickCount = state._clickCount or 0
      eventHandlerConfig._dragStartX = state._dragStartX or {}
      eventHandlerConfig._dragStartY = state._dragStartY or {}
      eventHandlerConfig._lastMouseX = state._lastMouseX or {}
      eventHandlerConfig._lastMouseY = state._lastMouseY or {}
      eventHandlerConfig._hovered = state._hovered
    end
  end

  self._eventHandler = Element._EventHandler.new(eventHandlerConfig, eventHandlerDeps)

  self._themeManager = Element._Theme.Manager.new({
    theme = props.theme or Element._Context.defaultTheme,
    themeComponent = props.themeComponent or nil,
    disabled = props.disabled or false,
    active = props.active or false,
    disableHighlight = props.disableHighlight,
    themeStateLock = props.themeStateLock or false,
    scaleCorners = props.scaleCorners,
    scalingAlgorithm = props.scalingAlgorithm,
  })
  
  -- Validate themeStateLock after ThemeManager is created
  if props.themeStateLock and props.themeComponent then
    self._themeManager:validateThemeStateLock()
  end

  -- Expose theme properties for backward compatibility
  self.theme = self._themeManager.theme
  self.themeComponent = self._themeManager.themeComponent
  self.disabled = self._themeManager.disabled
  self.active = self._themeManager.active
  self._themeState = self._themeManager:getState()

  -- disableHighlight defaults to true when using themeComponent (themes handle their own visual feedback)
  -- Can be explicitly overridden by setting props.disableHighlight
  if props.disableHighlight ~= nil then
    self.disableHighlight = props.disableHighlight
  else
    self.disableHighlight = self.themeComponent ~= nil
  end

  -- Initialize contentAutoSizingMultiplier after theme is set
  -- Priority: element props > theme component > theme default
  if props.contentAutoSizingMultiplier then
    self.contentAutoSizingMultiplier = props.contentAutoSizingMultiplier
  else
    local multiplier = self._themeManager:getContentAutoSizingMultiplier()
    self.contentAutoSizingMultiplier = multiplier or { 1, 1 }
  end

  -- Expose 9-patch corner scaling properties for backward compatibility
  self.scaleCorners = self._themeManager.scaleCorners
  self.scalingAlgorithm = self._themeManager.scalingAlgorithm

  self.contentBlur = props.contentBlur
  self.backdropBlur = props.backdropBlur
  self._blurInstance = nil

  self.editable = props.editable or false
  self.multiline = props.multiline or false
  self.passwordMode = props.passwordMode or false

  -- Validate property combinations: passwordMode disables multiline
  if self.passwordMode and props.multiline then
    Element._ErrorHandler:warn("Element", "ELEM_006")
    self.multiline = false
  elseif self.passwordMode then
    self.multiline = false
  end

  self.textWrap = props.textWrap
  if self.textWrap == nil then
    self.textWrap = self.multiline and "word" or false
  end

  self.maxLines = props.maxLines
  self.maxLength = props.maxLength
  self.placeholder = props.placeholder
  self.inputType = props.inputType or "text"

  self.textOverflow = props.textOverflow or "clip"
  self.scrollable = props.scrollable
  if self.scrollable == nil then
    self.scrollable = self.multiline
  end
  -- autoGrow defaults to true for multiline, false for single-line
  if props.autoGrow ~= nil then
    self.autoGrow = props.autoGrow
  else
    self.autoGrow = self.multiline
  end
  self.selectOnFocus = props.selectOnFocus or false

  self.cursorColor = props.cursorColor
  self.selectionColor = props.selectionColor
  self.cursorBlinkRate = props.cursorBlinkRate or 0.5

  if self.editable then
    self._textEditor = Element._TextEditor.new({
      editable = self.editable,
      multiline = self.multiline,
      passwordMode = self.passwordMode,
      textWrap = self.textWrap,
      maxLines = self.maxLines,
      maxLength = self.maxLength,
      placeholder = self.placeholder,
      inputType = self.inputType,
      textOverflow = self.textOverflow,
      scrollable = self.scrollable,
      autoGrow = self.autoGrow,
      selectOnFocus = self.selectOnFocus,
      cursorColor = self.cursorColor,
      selectionColor = self.selectionColor,
      cursorBlinkRate = self.cursorBlinkRate,
      text = props.text or "",
      onFocus = props.onFocus,
      onBlur = props.onBlur,
      onTextInput = props.onTextInput,
      onTextChange = props.onTextChange,
      onEnter = props.onEnter,
    }, textEditorDeps)
    -- Initialize will be called after self is fully constructed
  end

  -- Set parent first so it's available for size calculations
  self.parent = props.parent

  ------ add non-hereditary ------
  --- self drawing---
  -- OPTIMIZATION: Handle border - only create table if border exists
  -- This saves ~80 bytes per element without borders
  if type(props.border) == "table" then
    -- Check if any border side is truthy
    local hasAnyBorder = props.border.top or props.border.right or props.border.bottom or props.border.left
    if hasAnyBorder then
      self.border = {
        top = props.border.top or false,
        right = props.border.right or false,
        bottom = props.border.bottom or false,
        left = props.border.left or false,
      }
    else
      self.border = nil
    end
  elseif props.border then
    -- If border is a number or truthy value, keep it as-is
    self.border = props.border
  else
    -- No border specified - use nil instead of table with all false
    self.border = nil
  end
  self.borderColor = props.borderColor or Element._Color.new(0, 0, 0, 1)
  self.backgroundColor = props.backgroundColor or Element._Color.new(0, 0, 0, 0)

  -- Validate and set opacity
  if props.opacity ~= nil then
    Element._utils.validateRange(props.opacity, 0, 1, "opacity")
  end
  self.opacity = props.opacity or 1

  -- Set visibility property (default: "visible")
  self.visibility = props.visibility or "visible"

  -- Set transform property (optional)
  self.transform = props.transform or nil

  -- OPTIMIZATION: Handle cornerRadius - store as number or table, nil if all zeros
  -- This saves ~80 bytes per element without rounded corners
  if props.cornerRadius then
    if type(props.cornerRadius) == "number" then
      -- Store as number for uniform radius (compact)
      if props.cornerRadius ~= 0 then
        self.cornerRadius = props.cornerRadius
      else
        self.cornerRadius = nil
      end
    else
      -- Store as table only if non-zero values exist
      local hasNonZero = props.cornerRadius.topLeft or props.cornerRadius.topRight or props.cornerRadius.bottomLeft or props.cornerRadius.bottomRight
      if hasNonZero then
        self.cornerRadius = {
          topLeft = props.cornerRadius.topLeft or 0,
          topRight = props.cornerRadius.topRight or 0,
          bottomLeft = props.cornerRadius.bottomLeft or 0,
          bottomRight = props.cornerRadius.bottomRight or 0,
        }
      else
        self.cornerRadius = nil
      end
    end
  else
    -- No cornerRadius specified - use nil instead of table with all zeros
    self.cornerRadius = nil
  end

  -- For editable elements, default text to empty string if not provided
  if self.editable and props.text == nil then
    self.text = ""
  else
    self.text = props.text
  end

  -- Sync self.text with restored _textBuffer for editable elements in immediate mode
  if self.editable and Element._Context._immediateMode and self._textBuffer then
    self.text = self._textBuffer
  end

  -- Validate and set textAlign
  if props.textAlign then
    Element._utils.validateEnum(props.textAlign, Element._utils.enums.TextAlign, "textAlign")
  end
  self.textAlign = props.textAlign or Element._utils.enums.TextAlign.START

  -- Image properties
  self.imagePath = props.imagePath
  self.image = props.image

  -- Validate objectFit
  if props.objectFit then
    local validObjectFit = { fill = "fill", contain = "contain", cover = "cover", ["scale-down"] = "scale-down", none = "none" }
    Element._utils.validateEnum(props.objectFit, validObjectFit, "objectFit")
  end
  self.objectFit = props.objectFit or "fill"
  self.objectPosition = props.objectPosition or "center center"

  -- Validate and set imageOpacity
  if props.imageOpacity ~= nil then
    Element._utils.validateRange(props.imageOpacity, 0, 1, "imageOpacity")
  end
  self.imageOpacity = props.imageOpacity or 1

  -- Validate and set imageRepeat
  if props.imageRepeat then
    local validImageRepeat = {
      ["no-repeat"] = "no-repeat",
      ["repeat"] = "repeat",
      ["repeat-x"] = "repeat-x",
      ["repeat-y"] = "repeat-y",
      space = "space",
      round = "round",
    }
    Element._utils.validateEnum(props.imageRepeat, validImageRepeat, "imageRepeat")
  end
  self.imageRepeat = props.imageRepeat or "no-repeat"

  -- Set imageTint
  self.imageTint = props.imageTint

  -- Image callbacks
  self.onImageLoad = props.onImageLoad
  self.onImageLoadDeferred = props.onImageLoadDeferred or false
  self.onImageError = props.onImageError
  self.onImageErrorDeferred = props.onImageErrorDeferred or false

  -- Auto-load image if imagePath is provided
  if self.imagePath and not self.image then
    local loadedImage, err = Element._ImageCache.load(self.imagePath)
    if loadedImage then
      self._loadedImage = loadedImage
      -- Call onImageLoad callback if provided
      if self.onImageLoad and type(self.onImageLoad) == "function" then
        if self.onImageLoadDeferred then
          Element._Context.deferCallback(function()
            local success, callbackErr = pcall(self.onImageLoad, self, loadedImage)
            if not success then
              print(string.format("[Element] onImageLoad error: %s", tostring(callbackErr)))
            end
          end)
        else
          local success, callbackErr = pcall(self.onImageLoad, self, loadedImage)
          if not success then
            print(string.format("[Element] onImageLoad error: %s", tostring(callbackErr)))
          end
        end
      end
    else
      -- Image failed to load
      self._loadedImage = nil
      -- Call onImageError callback if provided
      if self.onImageError and type(self.onImageError) == "function" then
        if self.onImageErrorDeferred then
          Element._Context.deferCallback(function()
            local success, callbackErr = pcall(self.onImageError, self, err or "Unknown error")
            if not success then
              print(string.format("[Element] onImageError error: %s", tostring(callbackErr)))
            end
          end)
        else
          local success, callbackErr = pcall(self.onImageError, self, err or "Unknown error")
          if not success then
            print(string.format("[Element] onImageError error: %s", tostring(callbackErr)))
          end
        end
      end
    end
  elseif self.image then
    self._loadedImage = self.image
    -- Call onImageLoad for directly provided images
    if self.onImageLoad and type(self.onImageLoad) == "function" then
      if self.onImageLoadDeferred then
        Element._Context.deferCallback(function()
          local success, callbackErr = pcall(self.onImageLoad, self, self.image)
          if not success then
            print(string.format("[Element] onImageLoad error: %s", tostring(callbackErr)))
          end
        end)
      else
        local success, callbackErr = pcall(self.onImageLoad, self, self.image)
        if not success then
          print(string.format("[Element] onImageLoad error: %s", tostring(callbackErr)))
        end
      end
    end
  else
    self._loadedImage = nil
  end

  -- Initialize Renderer module for visual rendering
  self._renderer = Element._Renderer.new({
    backgroundColor = self.backgroundColor,
    borderColor = self.borderColor,
    opacity = self.opacity,
    border = self.border,
    cornerRadius = self.cornerRadius,
    theme = self.theme,
    themeComponent = self.themeComponent,
    scaleCorners = self.scaleCorners,
    scalingAlgorithm = self.scalingAlgorithm,
    imagePath = self.imagePath,
    image = self.image,
    _loadedImage = self._loadedImage,
    objectFit = self.objectFit,
    objectPosition = self.objectPosition,
    imageOpacity = self.imageOpacity,
    imageRepeat = self.imageRepeat,
    imageTint = self.imageTint,
    contentBlur = self.contentBlur,
    backdropBlur = self.backdropBlur,
  }, rendererDeps)

  --- self positioning ---
  local viewportWidth, viewportHeight = Element._Units.getViewport()

  ---- Sizing ----
  local gw, gh = love.window.getMode()
  self.prevGameSize = { width = gw, height = gh }
  self.autosizing = { width = false, height = false }

  -- Initialize LayoutEngine early with default values for auto-sizing calculations
  -- It will be re-configured later with actual layout properties
  self._layoutEngine = Element._LayoutEngine.new({
    positioning = Element._utils.enums.Positioning.RELATIVE,
    flexDirection = Element._utils.enums.FlexDirection.HORIZONTAL,
    flexWrap = Element._utils.enums.FlexWrap.NOWRAP,
    justifyContent = Element._utils.enums.JustifyContent.FLEX_START,
    alignItems = Element._utils.enums.AlignItems.STRETCH,
    alignContent = Element._utils.enums.AlignContent.STRETCH,
    gap = 0,
    gridRows = 1,
    gridColumns = 1,
    columnGap = 0,
    rowGap = 0,
  }, layoutEngineDeps)
  self._layoutEngine:initialize(self)

  -- Store unit specifications for responsive behavior
  self.units = {
    width = { value = nil, unit = "px" },
    height = { value = nil, unit = "px" },
    x = { value = nil, unit = "px" },
    y = { value = nil, unit = "px" },
    textSize = { value = nil, unit = "px" },
    gap = { value = nil, unit = "px" },
    padding = {
      top = { value = nil, unit = "px" },
      right = { value = nil, unit = "px" },
      bottom = { value = nil, unit = "px" },
      left = { value = nil, unit = "px" },
      horizontal = { value = nil, unit = "px" }, -- Shorthand for left/right
      vertical = { value = nil, unit = "px" }, -- Shorthand for top/bottom
    },
    margin = {
      top = { value = nil, unit = "px" },
      right = { value = nil, unit = "px" },
      bottom = { value = nil, unit = "px" },
      left = { value = nil, unit = "px" },
      horizontal = { value = nil, unit = "px" }, -- Shorthand for left/right
      vertical = { value = nil, unit = "px" }, -- Shorthand for top/bottom
    },
  }

  local scaleX, scaleY = Element._Context.getScaleFactors()

  self.minTextSize = props.minTextSize
  self.maxTextSize = props.maxTextSize

  -- Set autoScaleText BEFORE textSize processing (needed for correct initialization)
  if props.autoScaleText == nil then
    self.autoScaleText = true
  else
    self.autoScaleText = props.autoScaleText
  end

  -- Handle fontFamily (can be font name from theme or direct path to font file)
  -- Priority: explicit props.fontFamily > parent fontFamily > theme default
  if props.fontFamily then
    -- Explicitly set fontFamily takes highest priority
    self.fontFamily = props.fontFamily
  elseif self.parent and self.parent.fontFamily then
    -- Inherit from parent if parent has fontFamily set
    self.fontFamily = self.parent.fontFamily
  elseif props.themeComponent then
    -- If using themeComponent, try to get default from theme via ThemeManager
    local defaultFont = self._themeManager:getDefaultFontFamily()
    self.fontFamily = defaultFont and "default" or nil
  else
    self.fontFamily = nil
  end

  -- Handle textSize BEFORE width/height calculation (needed for auto-sizing)
  if props.textSize then
    if type(props.textSize) == "string" then
      -- Check if it's a preset first
      local presetValue, presetUnit = Element._utils.resolveTextSizePreset(props.textSize)
      local value, unit

      if presetValue then
        -- It's a preset, use the preset value and unit
        value, unit = presetValue, presetUnit
        self.units.textSize = { value = value, unit = unit }
      else
        -- Not a preset, parse normally
        value, unit = Element._Units.parse(props.textSize)
        self.units.textSize = { value = value, unit = unit }
      end

      -- Resolve textSize based on unit type
      if unit == "%" or unit == "vh" then
        -- Percentage and vh are relative to viewport height
        self.textSize = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
      elseif unit == "vw" then
        -- vw is relative to viewport width
        self.textSize = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
      elseif unit == "ew" then
        -- ew is relative to element width (use viewport width as fallback during initialization)
        -- Will be re-resolved after width is set
        self.textSize = (value / 100) * viewportWidth
      elseif unit == "eh" then
        -- eh is relative to element height (use viewport height as fallback during initialization)
        -- Will be re-resolved after height is set
        self.textSize = (value / 100) * viewportHeight
      elseif unit == "px" then
        -- Pixel units
        self.textSize = value
      else
        Element._ErrorHandler:error("Element", "ELEM_002", {
          unit = unit,
        })
      end
    else
      -- Validate pixel textSize value
      if props.textSize <= 0 then
        Element._ErrorHandler:error("Element", "ELEM_001", {
          value = tostring(props.textSize),
        })
      end

      -- Pixel textSize value
      if self.autoScaleText and Element._Context.baseScale then
        -- With base scaling: store original pixel value and scale relative to base resolution
        self.units.textSize = { value = props.textSize, unit = "px" }
        self.textSize = props.textSize * scaleY
      elseif self.autoScaleText then
        -- Without base scaling: convert to viewport units for auto-scaling
        -- Calculate what percentage of viewport height this represents
        local vhValue = (props.textSize / viewportHeight) * 100
        self.units.textSize = { value = vhValue, unit = "vh" }
        self.textSize = props.textSize -- Initial size is the specified pixel value
      else
        -- No auto-scaling: apply base scaling if set, otherwise use raw value
        self.textSize = Element._Context.baseScale and (props.textSize * scaleY) or props.textSize
        self.units.textSize = { value = props.textSize, unit = "px" }
      end
    end
  else
    -- No textSize specified - use auto-scaling default
    if self.autoScaleText and Element._Context.baseScale then
      -- With base scaling: use 12px as default and scale
      self.units.textSize = { value = 12, unit = "px" }
      self.textSize = 12 * scaleY
    elseif self.autoScaleText then
      -- Without base scaling: default to 1.5vh (1.5% of viewport height)
      self.units.textSize = { value = 1.5, unit = "vh" }
      self.textSize = (1.5 / 100) * viewportHeight
    else
      -- No auto-scaling: use 12px with optional base scaling
      self.textSize = Element._Context.baseScale and (12 * scaleY) or 12
      self.units.textSize = { value = nil, unit = "px" }
    end
  end

  -- Handle width (both w and width properties, prefer w if both exist)
  local widthProp = props.width
  local tempWidth = 0 -- Temporary width for padding resolution
  if widthProp then
    if type(widthProp) == "string" then
      local value, unit = Element._Units.parse(widthProp)
      self.units.width = { value = value, unit = unit }
      local parentWidth = self.parent and self.parent.width or viewportWidth
      tempWidth = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
    else
      tempWidth = Element._Context.baseScale and (widthProp * scaleX) or widthProp
      self.units.width = { value = widthProp, unit = "px" }
    end
    self.width = tempWidth
  else
    self.autosizing.width = true
    -- Special case: if textWrap is enabled and parent exists, constrain width to parent
    -- Text wrapping requires a width constraint, so use parent's content width
    if props.textWrap and self.parent and self.parent.width then
      tempWidth = self.parent.width
      self.width = tempWidth
      self.units.width = { value = 100, unit = "%" } -- Mark as parent-constrained
      self.autosizing.width = false -- Not truly autosizing, constrained by parent
    else
      tempWidth = self:calculateAutoWidth()
      self.width = tempWidth
      self.units.width = { value = nil, unit = "auto" } -- Mark as auto-sized
    end
  end

  -- Handle height (both h and height properties, prefer h if both exist)
  local heightProp = props.height
  local tempHeight = 0 -- Temporary height for padding resolution
  if heightProp then
    if type(heightProp) == "string" then
      local value, unit = Element._Units.parse(heightProp)
      self.units.height = { value = value, unit = unit }
      local parentHeight = self.parent and self.parent.height or viewportHeight
      tempHeight = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
    else
      -- Apply base scaling to pixel values
      tempHeight = Element._Context.baseScale and (heightProp * scaleY) or heightProp
      self.units.height = { value = heightProp, unit = "px" }
    end
    self.height = tempHeight
  else
    self.autosizing.height = true
    -- Calculate auto-height without padding first
    tempHeight = self:calculateAutoHeight()
    self.height = tempHeight
    self.units.height = { value = nil, unit = "auto" } -- Mark as auto-sized
  end

  --- child positioning ---
  if props.gap then
    if type(props.gap) == "string" then
      local value, unit = Element._Units.parse(props.gap)
      self.units.gap = { value = value, unit = unit }
      -- Gap percentages should be relative to the element's own size, not parent
      -- For horizontal flex, gap is based on width; for vertical flex, based on height
      local flexDir = props.flexDirection or Element._utils.enums.FlexDirection.HORIZONTAL
      local containerSize = (flexDir == Element._utils.enums.FlexDirection.HORIZONTAL) and self.width or self.height
      self.gap = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, containerSize)
    else
      self.gap = props.gap
      self.units.gap = { value = props.gap, unit = "px" }
    end
  else
    self.gap = 0
    self.units.gap = { value = 0, unit = "px" }
  end

  -- BORDER-BOX MODEL: For auto-sizing, we need to add padding to content dimensions
  -- For explicit sizing, width/height already include padding (border-box)

  -- Check if we should use 9-patch content padding for auto-sizing
  local use9PatchPadding = false
  local ninePatchContentPadding = nil
  if self._themeManager:hasThemeComponent() then
    local component = self._themeManager:getComponent()
    if component and component._ninePatchData and component._ninePatchData.contentPadding then
      -- Only use 9-patch padding if no explicit padding was provided
      if
        not props.padding
        or (
          not props.padding.top
          and not props.padding.right
          and not props.padding.bottom
          and not props.padding.left
          and not props.padding.horizontal
          and not props.padding.vertical
        )
      then
        use9PatchPadding = true
        ninePatchContentPadding = component._ninePatchData.contentPadding
      end
    end
  end

  -- First, resolve padding using temporary dimensions
  -- For auto-sized elements, this is content width; for explicit sizing, this is border-box width
  local tempPadding
  if use9PatchPadding then
    -- Get scaled 9-patch content padding from ThemeManager
    local scaledPadding = self._themeManager:getScaledContentPadding(tempWidth, tempHeight)
    if scaledPadding then
      tempPadding = scaledPadding
    else
      -- Fallback if scaling fails
      tempPadding = {
        left = ninePatchContentPadding.left,
        top = ninePatchContentPadding.top,
        right = ninePatchContentPadding.right,
        bottom = ninePatchContentPadding.bottom,
      }
    end
  else
    tempPadding = Element._Units.resolveSpacing(props.padding, self.width, self.height)
  end

  -- Margin percentages are relative to parent's dimensions (CSS spec)
  local parentWidth = self.parent and self.parent.width or viewportWidth
  local parentHeight = self.parent and self.parent.height or viewportHeight
  self.margin = Element._Units.resolveSpacing(props.margin, parentWidth, parentHeight)

  -- For auto-sized elements, add padding to get border-box dimensions
  if self.autosizing.width then
    self._borderBoxWidth = self.width + tempPadding.left + tempPadding.right
  else
    -- For explicit sizing, width is already border-box
    self._borderBoxWidth = self.width
  end

  if self.autosizing.height then
    self._borderBoxHeight = self.height + tempPadding.top + tempPadding.bottom
  else
    -- For explicit sizing, height is already border-box
    self._borderBoxHeight = self.height
  end

  -- Set final padding
  if use9PatchPadding then
    -- Use 9-patch content padding
    self.padding = {
      left = ninePatchContentPadding.left,
      top = ninePatchContentPadding.top,
      right = ninePatchContentPadding.right,
      bottom = ninePatchContentPadding.bottom,
    }
  else
    -- Re-resolve padding based on final border-box dimensions (important for percentage padding)
    self.padding = Element._Units.resolveSpacing(props.padding, self._borderBoxWidth, self._borderBoxHeight)
  end

  -- Calculate final content dimensions by subtracting padding from border-box
  self.width = math.max(0, self._borderBoxWidth - self.padding.left - self.padding.right)
  self.height = math.max(0, self._borderBoxHeight - self.padding.top - self.padding.bottom)

  -- Re-resolve ew/eh textSize units now that width/height are set
  if props.textSize and type(props.textSize) == "string" then
    -- Check if it's a preset first (presets don't need re-resolution)
    local presetValue, presetUnit = Element._utils.resolveTextSizePreset(props.textSize)
    if not presetValue then
      -- Not a preset, parse and check for ew/eh units
      local value, unit = Element._Units.parse(props.textSize)
      if unit == "ew" then
        -- Element width relative (now that width is set)
        self.textSize = (value / 100) * self.width
      elseif unit == "eh" then
        -- Element height relative (now that height is set)
        self.textSize = (value / 100) * self.height
      end
    end
  end

  -- Apply min/max constraints (also scaled)
  local minSize = self.minTextSize and (Element._Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize)
  local maxSize = self.maxTextSize and (Element._Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize)

  if minSize and self.textSize < minSize then
    self.textSize = minSize
  end
  if maxSize and self.textSize > maxSize then
    self.textSize = maxSize
  end

  -- Protect against too-small text sizes (minimum 1px)
  if self.textSize < 1 then
    self.textSize = 1 -- Minimum 1px
  end

  -- Store original spacing values for proper resize handling
  -- Store shorthand properties first (horizontal/vertical)
  if props.padding then
    if props.padding.horizontal then
      if type(props.padding.horizontal) == "string" then
        local value, unit = Element._Units.parse(props.padding.horizontal)
        self.units.padding.horizontal = { value = value, unit = unit }
      else
        self.units.padding.horizontal = { value = props.padding.horizontal, unit = "px" }
      end
    end
    if props.padding.vertical then
      if type(props.padding.vertical) == "string" then
        local value, unit = Element._Units.parse(props.padding.vertical)
        self.units.padding.vertical = { value = value, unit = unit }
      else
        self.units.padding.vertical = { value = props.padding.vertical, unit = "px" }
      end
    end
  end

  -- Initialize all padding sides
  for _, side in ipairs({ "top", "right", "bottom", "left" }) do
    if props.padding and props.padding[side] then
      if type(props.padding[side]) == "string" then
        local value, unit = Element._Units.parse(props.padding[side])
        self.units.padding[side] = { value = value, unit = unit, explicit = true }
      else
        self.units.padding[side] = { value = props.padding[side], unit = "px", explicit = true }
      end
    else
      -- Mark as derived from shorthand (will use shorthand during resize if available)
      self.units.padding[side] = { value = self.padding[side], unit = "px", explicit = false }
    end
  end

  -- Store margin shorthand properties
  if props.margin then
    if props.margin.horizontal then
      if type(props.margin.horizontal) == "string" then
        local value, unit = Element._Units.parse(props.margin.horizontal)
        self.units.margin.horizontal = { value = value, unit = unit }
      else
        self.units.margin.horizontal = { value = props.margin.horizontal, unit = "px" }
      end
    end
    if props.margin.vertical then
      if type(props.margin.vertical) == "string" then
        local value, unit = Element._Units.parse(props.margin.vertical)
        self.units.margin.vertical = { value = value, unit = unit }
      else
        self.units.margin.vertical = { value = props.margin.vertical, unit = "px" }
      end
    end
  end

  -- Initialize all margin sides
  for _, side in ipairs({ "top", "right", "bottom", "left" }) do
    if props.margin and props.margin[side] then
      if type(props.margin[side]) == "string" then
        local value, unit = Element._Units.parse(props.margin[side])
        self.units.margin[side] = { value = value, unit = unit, explicit = true }
      else
        self.units.margin[side] = { value = props.margin[side], unit = "px", explicit = true }
      end
    else
      -- Mark as derived from shorthand (will use shorthand during resize if available)
      self.units.margin[side] = { value = self.margin[side], unit = "px", explicit = false }
    end
  end

  -- Grid properties are set later in the constructor

  ------ add hereditary ------
  if props.parent == nil then
    table.insert(Element._Context.topElements, self)

    -- Handle x position with units
    if props.x then
      if type(props.x) == "string" then
        local value, unit = Element._Units.parse(props.x)
        self.units.x = { value = value, unit = unit }
        self.x = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
      else
        -- Apply base scaling to pixel positions
        self.x = Element._Context.baseScale and (props.x * scaleX) or props.x
        self.units.x = { value = props.x, unit = "px" }
      end
    else
      self.x = 0
      self.units.x = { value = 0, unit = "px" }
    end

    -- Handle y position with units
    if props.y then
      if type(props.y) == "string" then
        local value, unit = Element._Units.parse(props.y)
        self.units.y = { value = value, unit = unit }
        self.y = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
      else
        -- Apply base scaling to pixel positions
        self.y = Element._Context.baseScale and (props.y * scaleY) or props.y
        self.units.y = { value = props.y, unit = "px" }
      end
    else
      self.y = 0
      self.units.y = { value = 0, unit = "px" }
    end

    self.z = props.z or 0

    -- Set textColor with priority: props > theme text color > black
    if props.textColor then
      self.textColor = props.textColor
    else
      -- Try to get text color from theme via ThemeManager
      local themeToUse = self._themeManager:getTheme()
      if themeToUse and themeToUse.colors and themeToUse.colors.text then
        self.textColor = themeToUse.colors.text
      else
        -- Fallback to black
        self.textColor = Element._Color.new(0, 0, 0, 1)
      end
    end

    -- Track if positioning was explicitly set
    if props.positioning then
      Element._utils.validateEnum(props.positioning, Element._utils.enums.Positioning, "positioning")
      self.positioning = props.positioning
      self._originalPositioning = props.positioning
      self._explicitlyAbsolute = (props.positioning == Element._utils.enums.Positioning.ABSOLUTE)
    else
      self.positioning = Element._utils.enums.Positioning.RELATIVE
      self._originalPositioning = nil -- No explicit positioning
      self._explicitlyAbsolute = false
    end
  else
    -- Set positioning first and track if explicitly set
    self._originalPositioning = props.positioning -- Track original intent
    if props.positioning == Element._utils.enums.Positioning.ABSOLUTE then
      self.positioning = Element._utils.enums.Positioning.ABSOLUTE
      self._explicitlyAbsolute = true -- Explicitly set to absolute by user
    elseif props.positioning == Element._utils.enums.Positioning.FLEX then
      self.positioning = Element._utils.enums.Positioning.FLEX
      self._explicitlyAbsolute = false
    elseif props.positioning == Element._utils.enums.Positioning.GRID then
      self.positioning = Element._utils.enums.Positioning.GRID
      self._explicitlyAbsolute = false
    else
      -- Default: children in flex/grid containers participate in parent's layout
      -- children in relative/absolute containers default to relative
      if self.parent.positioning == Element._utils.enums.Positioning.FLEX or self.parent.positioning == Element._utils.enums.Positioning.GRID then
        self.positioning = Element._utils.enums.Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
        self._explicitlyAbsolute = false -- Participate in parent's layout
      else
        self.positioning = Element._utils.enums.Positioning.RELATIVE
        self._explicitlyAbsolute = false -- Default for relative/absolute containers
      end
    end

    -- Set initial position
    if self.positioning == Element._utils.enums.Positioning.ABSOLUTE then
      -- Absolute positioning is relative to parent's content area (padding box)
      local baseX = self.parent.x + self.parent.padding.left
      local baseY = self.parent.y + self.parent.padding.top

      -- Handle x position with units
      if props.x then
        if type(props.x) == "string" then
          local value, unit = Element._Units.parse(props.x)
          self.units.x = { value = value, unit = unit }
          local parentWidth = self.parent.width
          local offsetX = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
          self.x = baseX + offsetX
        else
          -- Apply base scaling to pixel positions
          local scaledOffset = Element._Context.baseScale and (props.x * scaleX) or props.x
          self.x = baseX + scaledOffset
          self.units.x = { value = props.x, unit = "px" }
        end
      else
        self.x = baseX
        self.units.x = { value = 0, unit = "px" }
      end

      -- Handle y position with units
      if props.y then
        if type(props.y) == "string" then
          local value, unit = Element._Units.parse(props.y)
          self.units.y = { value = value, unit = unit }
          local parentHeight = self.parent.height
          local offsetY = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
          self.y = baseY + offsetY
        else
          -- Apply base scaling to pixel positions
          local scaledOffset = Element._Context.baseScale and (props.y * scaleY) or props.y
          self.y = baseY + scaledOffset
          self.units.y = { value = props.y, unit = "px" }
        end
      else
        self.y = baseY
        self.units.y = { value = 0, unit = "px" }
      end

      self.z = props.z or 0
    else
      -- Children in flex containers start at parent position but will be repositioned by layoutChildren
      -- Children in absolute/relative containers start at parent's content area (accounting for padding)
      local baseX = self.parent.x + self.parent.padding.left
      local baseY = self.parent.y + self.parent.padding.top

      if props.x then
        if type(props.x) == "string" then
          local value, unit = Element._Units.parse(props.x)
          self.units.x = { value = value, unit = unit }
          local parentWidth = self.parent.width
          local offsetX = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
          self.x = baseX + offsetX
        else
          -- Apply base scaling to pixel offsets
          local scaledOffset = Element._Context.baseScale and (props.x * scaleX) or props.x
          self.x = baseX + scaledOffset
          self.units.x = { value = props.x, unit = "px" }
        end
      else
        self.x = baseX
        self.units.x = { value = 0, unit = "px" }
      end

      if props.y then
        if type(props.y) == "string" then
          local value, unit = Element._Units.parse(props.y)
          self.units.y = { value = value, unit = unit }
          parentHeight = self.parent.height
          local offsetY = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
          self.y = baseY + offsetY
        else
          -- Apply base scaling to pixel offsets
          local scaledOffset = Element._Context.baseScale and (props.y * scaleY) or props.y
          self.y = baseY + scaledOffset
          self.units.y = { value = props.y, unit = "px" }
        end
      else
        self.y = baseY
        self.units.y = { value = 0, unit = "px" }
      end

      self.z = props.z or self.parent.z or 0
    end

    if props.textColor then
      self.textColor = props.textColor
    elseif self.parent.textColor then
      self.textColor = self.parent.textColor
    else
      local themeToUse = self._themeManager:getTheme()
      if themeToUse and themeToUse.colors and themeToUse.colors.text then
        self.textColor = themeToUse.colors.text
      else
        -- Fallback to black
        self.textColor = Element._Color.new(0, 0, 0, 1)
      end
    end

    props.parent:addChild(self)
  end

  -- Handle positioning properties for ALL elements (with or without parent)
  -- Handle top positioning with units
  if props.top then
    if type(props.top) == "string" then
      local value, unit = Element._Units.parse(props.top)
      self.units.top = { value = value, unit = unit }
      self.top = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
    else
      self.top = props.top
      self.units.top = { value = props.top, unit = "px" }
    end
  else
    self.top = nil
    self.units.top = nil
  end

  -- Handle right positioning with units
  if props.right then
    if type(props.right) == "string" then
      local value, unit = Element._Units.parse(props.right)
      self.units.right = { value = value, unit = unit }
      self.right = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
    else
      self.right = props.right
      self.units.right = { value = props.right, unit = "px" }
    end
  else
    self.right = nil
    self.units.right = nil
  end

  -- Handle bottom positioning with units
  if props.bottom then
    if type(props.bottom) == "string" then
      local value, unit = Element._Units.parse(props.bottom)
      self.units.bottom = { value = value, unit = unit }
      self.bottom = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
    else
      self.bottom = props.bottom
      self.units.bottom = { value = props.bottom, unit = "px" }
    end
  else
    self.bottom = nil
    self.units.bottom = nil
  end

  -- Handle left positioning with units
  if props.left then
    if type(props.left) == "string" then
      local value, unit = Element._Units.parse(props.left)
      self.units.left = { value = value, unit = unit }
      self.left = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
    else
      self.left = props.left
      self.units.left = { value = props.left, unit = "px" }
    end
  else
    self.left = nil
    self.units.left = nil
  end

  if self.positioning == Element._utils.enums.Positioning.FLEX then
    -- Validate enum properties
    if props.flexDirection then
      Element._utils.validateEnum(props.flexDirection, Element._utils.enums.FlexDirection, "flexDirection")
    end
    if props.flexWrap then
      Element._utils.validateEnum(props.flexWrap, Element._utils.enums.FlexWrap, "flexWrap")
    end
    if props.justifyContent then
      Element._utils.validateEnum(props.justifyContent, Element._utils.enums.JustifyContent, "justifyContent")
    end
    if props.alignItems then
      Element._utils.validateEnum(props.alignItems, Element._utils.enums.AlignItems, "alignItems")
    end
    if props.alignContent then
      Element._utils.validateEnum(props.alignContent, Element._utils.enums.AlignContent, "alignContent")
    end
    if props.justifySelf then
      Element._utils.validateEnum(props.justifySelf, Element._utils.enums.JustifySelf, "justifySelf")
    end

    self.flexDirection = props.flexDirection or Element._utils.enums.FlexDirection.HORIZONTAL
    self.flexWrap = props.flexWrap or Element._utils.enums.FlexWrap.NOWRAP
    self.justifyContent = props.justifyContent or Element._utils.enums.JustifyContent.FLEX_START
    self.alignItems = props.alignItems or Element._utils.enums.AlignItems.STRETCH
    self.alignContent = props.alignContent or Element._utils.enums.AlignContent.STRETCH
    self.justifySelf = props.justifySelf or Element._utils.enums.JustifySelf.AUTO
  end

  -- Grid container properties
  if self.positioning == Element._utils.enums.Positioning.GRID then
    self.gridRows = props.gridRows or 1
    self.gridColumns = props.gridColumns or 1
    self.alignItems = props.alignItems or Element._utils.enums.AlignItems.STRETCH

    -- Handle columnGap and rowGap
    if props.columnGap then
      if type(props.columnGap) == "string" then
        local value, unit = Element._Units.parse(props.columnGap)
        self.columnGap = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, self.width)
      else
        self.columnGap = props.columnGap
      end
    else
      self.columnGap = 0
    end

    if props.rowGap then
      if type(props.rowGap) == "string" then
        local value, unit = Element._Units.parse(props.rowGap)
        self.rowGap = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, self.height)
      else
        self.rowGap = props.rowGap
      end
    else
      self.rowGap = 0
    end
  end

  self.alignSelf = props.alignSelf or Element._utils.enums.AlignSelf.AUTO

  -- Update the LayoutEngine with actual layout properties
  -- (it was initialized early with defaults for auto-sizing calculations)
  self._layoutEngine.positioning = self.positioning
  if self.flexDirection then
    self._layoutEngine.flexDirection = self.flexDirection
  end
  if self.flexWrap then
    self._layoutEngine.flexWrap = self.flexWrap
  end
  if self.justifyContent then
    self._layoutEngine.justifyContent = self.justifyContent
  end
  if self.alignItems then
    self._layoutEngine.alignItems = self.alignItems
  end
  if self.alignContent then
    self._layoutEngine.alignContent = self.alignContent
  end
  if self.gap then
    self._layoutEngine.gap = self.gap
  end
  if self.gridRows then
    self._layoutEngine.gridRows = self.gridRows
  end
  if self.gridColumns then
    self._layoutEngine.gridColumns = self.gridColumns
  end
  if self.columnGap then
    self._layoutEngine.columnGap = self.columnGap
  end
  if self.rowGap then
    self._layoutEngine.rowGap = self.rowGap
  end

  -- transform is already set at line 424 (props.transform or nil)
  -- Don't overwrite it here
  self.transition = props.transition or {}

  if props.overflow or props.overflowX or props.overflowY then
    self._scrollManager = Element._ScrollManager.new({
      overflow = props.overflow,
      overflowX = props.overflowX,
      overflowY = props.overflowY,
      scrollbarWidth = props.scrollbarWidth,
      scrollbarColor = props.scrollbarColor,
      scrollbarTrackColor = props.scrollbarTrackColor,
      scrollbarRadius = props.scrollbarRadius,
      scrollbarPadding = props.scrollbarPadding,
      scrollSpeed = props.scrollSpeed,
      hideScrollbars = props.hideScrollbars,
      _scrollX = props._scrollX,
      _scrollY = props._scrollY,
    }, scrollManagerDeps)

    -- Expose ScrollManager properties for backward compatibility (Renderer access)
    self.overflow = self._scrollManager.overflow
    self.overflowX = self._scrollManager.overflowX
    self.overflowY = self._scrollManager.overflowY
    self.scrollbarWidth = self._scrollManager.scrollbarWidth
    self.scrollbarColor = self._scrollManager.scrollbarColor
    self.scrollbarTrackColor = self._scrollManager.scrollbarTrackColor
    self.scrollbarRadius = self._scrollManager.scrollbarRadius
    self.scrollbarPadding = self._scrollManager.scrollbarPadding
    self.scrollSpeed = self._scrollManager.scrollSpeed
    self.hideScrollbars = self._scrollManager.hideScrollbars

    -- Initialize state properties (will be synced from ScrollManager)
    self._overflowX = false
    self._overflowY = false
    self._contentWidth = 0
    self._contentHeight = 0
    self._scrollX = 0
    self._scrollY = 0
    self._maxScrollX = 0
    self._maxScrollY = 0
    self._scrollbarHoveredVertical = false
    self._scrollbarHoveredHorizontal = false
    self._scrollbarDragging = false
    self._hoveredScrollbar = nil
    self._scrollbarDragOffset = 0
  else
    self._scrollManager = nil
  end

  -- Register element in z-index tracking for immediate mode
  if Element._Context._immediateMode then
    Element._Context.registerElement(self)
  end

  return self
end

--- Retrieve the element's screen-space rectangle for collision detection and positioning calculations
--- Use this for custom layout logic, tooltips, or detecting overlaps between elements
---@return { x:number, y:number, width:number, height:number }
function Element:getBounds()
  return { x = self.x, y = self.y, width = self:getBorderBoxWidth(), height = self:getBorderBoxHeight() }
end

--- Test if a screen coordinate falls within the element's clickable area
--- Use this for custom hit detection or determining which element the mouse is over
--- @param x number
--- @param y number
--- @return boolean
function Element:contains(x, y)
  local bounds = self:getBounds()
  return bounds.x <= x and bounds.y <= y and bounds.x + bounds.width >= x and bounds.y + bounds.height >= y
end

--- Get the element's total width including padding for layout calculations
--- Use this when you need the full visual width rather than just content width
---@return number
function Element:getBorderBoxWidth()
  return self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
end

--- Get the element's total height including padding for layout calculations
--- Use this when you need the full visual height rather than just content height
---@return number
function Element:getBorderBoxHeight()
  return self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
end

--- Sync ScrollManager state to Element properties for backward compatibility
--- This ensures Renderer and StateManager can access scroll state from Element
function Element:_syncScrollManagerState()
  if not self._scrollManager then
    return
  end

  -- Sync state properties from ScrollManager
  self._overflowX = self._scrollManager._overflowX
  self._overflowY = self._scrollManager._overflowY
  self._contentWidth = self._scrollManager._contentWidth
  self._contentHeight = self._scrollManager._contentHeight
  self._scrollX = self._scrollManager._scrollX
  self._scrollY = self._scrollManager._scrollY
  self._maxScrollX = self._scrollManager._maxScrollX
  self._maxScrollY = self._scrollManager._maxScrollY
  self._scrollbarHoveredVertical = self._scrollManager._scrollbarHoveredVertical
  self._scrollbarHoveredHorizontal = self._scrollManager._scrollbarHoveredHorizontal
  self._scrollbarDragging = self._scrollManager._scrollbarDragging
  self._hoveredScrollbar = self._scrollManager._hoveredScrollbar
  self._scrollbarDragOffset = self._scrollManager._scrollbarDragOffset
end

--- Detect if content overflows container bounds (delegates to ScrollManager)
function Element:_detectOverflow()
  if self._scrollManager then
    self._scrollManager:detectOverflow(self)
    self:_syncScrollManagerState()
  end
end

--- Programmatically scroll content to any position for implementing "scroll to top" buttons or navigation anchors
--- Use this to create custom scrolling controls or jump to specific content sections
---@param x number? -- X scroll position (nil to keep current)
---@param y number? -- Y scroll position (nil to keep current)
function Element:setScrollPosition(x, y)
  if self._scrollManager then
    self._scrollManager:setScroll(x, y)
    self:_syncScrollManagerState()
  end
end

--- Calculate scrollbar dimensions and positions (delegates to ScrollManager)
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
function Element:_calculateScrollbarDimensions()
  if self._scrollManager then
    return self._scrollManager:calculateScrollbarDimensions(self)
  end
  -- Return empty result if no ScrollManager
  return {
    vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
    horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 },
  }
end

--- Draw scrollbars

--- Get scrollbar at mouse position (delegates to ScrollManager)
---@param mouseX number
---@param mouseY number
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
function Element:_getScrollbarAtPosition(mouseX, mouseY)
  if self._scrollManager then
    return self._scrollManager:getScrollbarAtPosition(self, mouseX, mouseY)
  end
  return nil
end

--- Handle scrollbar mouse press
---@param mouseX number
---@param mouseY number
---@param button number
---@return boolean -- True if event was consumed
function Element:_handleScrollbarPress(mouseX, mouseY, button)
  if self._scrollManager then
    local consumed = self._scrollManager:handleMousePress(self, mouseX, mouseY, button)
    self:_syncScrollManagerState()
    return consumed
  end
  return false
end

--- Handle scrollbar drag (delegates to ScrollManager)
---@param mouseX number
---@param mouseY number
---@return boolean -- True if event was consumed
function Element:_handleScrollbarDrag(mouseX, mouseY)
  if self._scrollManager then
    local consumed = self._scrollManager:handleMouseMove(self, mouseX, mouseY)
    self:_syncScrollManagerState()
    return consumed
  end
  return false
end

--- Handle scrollbar release (delegates to ScrollManager)
---@param button number
---@return boolean -- True if event was consumed
function Element:_handleScrollbarRelease(button)
  if self._scrollManager then
    local consumed = self._scrollManager:handleMouseRelease(button)
    self:_syncScrollManagerState()
    return consumed
  end
  return false
end

--- Handle mouse wheel scrolling (delegates to ScrollManager)
---@param x number -- Horizontal scroll amount
---@param y number -- Vertical scroll amount
---@return boolean -- True if scroll was handled
function Element:_handleWheelScroll(x, y)
  if self._scrollManager then
    local consumed = self._scrollManager:handleWheel(x, y)
    self:_syncScrollManagerState()
    return consumed
  end
  return false
end

--- Query how far content is scrolled to implement scroll-aware UI like "back to top" buttons
--- Use this to create scroll position indicators or trigger lazy-loading
---@return number scrollX, number scrollY
function Element:getScrollPosition()
  if self._scrollManager then
    return self._scrollManager:getScroll()
  end
  return 0, 0
end

--- Find the scroll limits for validation and scroll position clamping
--- Use this to determine if content is fully scrolled or calculate remaining scroll distance
---@return number maxScrollX, number maxScrollY
function Element:getMaxScroll()
  if self._scrollManager then
    return self._scrollManager:getMaxScroll()
  end
  return 0, 0
end

--- Get normalized scroll progress for scroll-based animations or position indicators
--- Use this to drive progress bars or parallax effects based on scroll position
---@return number percentX, number percentY
function Element:getScrollPercentage()
  if self._scrollManager then
    return self._scrollManager:getScrollPercentage()
  end
  return 0, 0
end

--- Determine if content extends beyond visible bounds to conditionally show scrollbars or overflow indicators
--- Use this to decide whether to display scroll hints or enable scroll interactions
---@return boolean hasOverflowX, boolean hasOverflowY
function Element:hasOverflow()
  if self._scrollManager then
    return self._scrollManager:hasOverflow()
  end
  return false, false
end

--- Measure total content size including overflowed areas for scroll calculations
--- Use this to understand how much content exists beyond the visible viewport
---@return number contentWidth, number contentHeight
function Element:getContentSize()
  if self._scrollManager then
    return self._scrollManager:getContentSize()
  end
  return 0, 0
end

--- Scroll content by a relative amount for smooth scrolling animations or gesture-based scrolling
--- Use this to implement custom scroll controls or smooth scroll transitions
---@param dx number? -- X delta (nil for no change)
---@param dy number? -- Y delta (nil for no change)
function Element:scrollBy(dx, dy)
  if self._scrollManager then
    self._scrollManager:scrollBy(dx, dy)
    self:_syncScrollManagerState()
  end
end

--- Jump to the beginning of scrollable content instantly
--- Use this for "back to top" buttons or resetting scroll position
function Element:scrollToTop()
  self:setScrollPosition(nil, 0)
end

--- Scroll to bottom
function Element:scrollToBottom()
  if self._scrollManager then
    local _, maxScrollY = self._scrollManager:getMaxScroll()
    self:setScrollPosition(nil, maxScrollY)
  end
end

--- Scroll to left
function Element:scrollToLeft()
  self:setScrollPosition(0, nil)
end

--- Jump to the rightmost position of horizontally scrollable content
--- Use this to navigate to the end of horizontal lists or carousels
function Element:scrollToRight()
  if self._scrollManager then
    local maxScrollX, _ = self._scrollManager:getMaxScroll()
    self:setScrollPosition(maxScrollX, nil)
  end
end

--- Get the current state's scaled content padding
--- Returns the contentPadding for the current theme state, scaled to the element's size
---@return table|nil -- {left, top, right, bottom} or nil if no contentPadding
function Element:getScaledContentPadding()
  local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
  local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
  return self._themeManager:getScaledContentPadding(borderBoxWidth, borderBoxHeight)
end

--- Get or create blur instance for this element
---@return table? -- Blur instance or nil if no blur configured
function Element:getBlurInstance()
  -- Determine quality from contentBlur or backdropBlur
  local quality = 5 -- Default quality
  if self.contentBlur and self.contentBlur.quality then
    quality = self.contentBlur.quality
  elseif self.backdropBlur and self.backdropBlur.quality then
    quality = self.backdropBlur.quality
  end

  -- Create blur instance if needed
  if not self._blurInstance or self._blurInstance.quality ~= quality then
    self._blurInstance = Element._Blur.new({ quality = quality })
  end

  return self._blurInstance
end

--- Get available content width for children (accounting for 9-patch content padding)
--- This is the width that children should use when calculating percentage widths
---@return number
function Element:getAvailableContentWidth()
  local availableWidth = self.width

  local scaledContentPadding = self:getScaledContentPadding()
  if scaledContentPadding then
    -- Check if the element is using the scaled 9-patch contentPadding as its padding
    -- Allow small floating point differences (within 0.1 pixels)
    local usingContentPaddingAsPadding = (
      math.abs(self.padding.left - scaledContentPadding.left) < 0.1 and math.abs(self.padding.right - scaledContentPadding.right) < 0.1
    )

    if not usingContentPaddingAsPadding then
      -- Element has explicit padding different from contentPadding
      -- Subtract scaled contentPadding to get the area children should use
      availableWidth = availableWidth - scaledContentPadding.left - scaledContentPadding.right
    end
  end

  return math.max(0, availableWidth)
end

--- Get available content height for children (accounting for 9-patch content padding)
--- This is the height that children should use when calculating percentage heights
---@return number
function Element:getAvailableContentHeight()
  local availableHeight = self.height

  local scaledContentPadding = self:getScaledContentPadding()
  if scaledContentPadding then
    -- Check if the element is using the scaled 9-patch contentPadding as its padding
    -- Allow small floating point differences (within 0.1 pixels)
    local usingContentPaddingAsPadding = (
      math.abs(self.padding.top - scaledContentPadding.top) < 0.1 and math.abs(self.padding.bottom - scaledContentPadding.bottom) < 0.1
    )

    if not usingContentPaddingAsPadding then
      -- Element has explicit padding different from contentPadding
      -- Subtract scaled contentPadding to get the area children should use
      availableHeight = availableHeight - scaledContentPadding.top - scaledContentPadding.bottom
    end
  end

  return math.max(0, availableHeight)
end

--- Dynamically insert a child element into the hierarchy for runtime UI construction
--- Use this to build interfaces procedurally or add elements based on application state
---@param child Element
function Element:addChild(child)
  child.parent = self

  -- Re-evaluate positioning now that we have a parent
  -- If child was created without explicit positioning, inherit from parent
  if child._originalPositioning == nil then
    -- No explicit positioning was set during construction
    if self.positioning == Element._utils.enums.Positioning.FLEX or self.positioning == Element._utils.enums.Positioning.GRID then
      child.positioning = Element._utils.enums.Positioning.ABSOLUTE -- They are positioned BY flex/grid, not AS flex/grid
      child._explicitlyAbsolute = false -- Participate in parent's layout
    else
      child.positioning = Element._utils.enums.Positioning.RELATIVE
      child._explicitlyAbsolute = false -- Default for relative/absolute containers
    end
  end
  -- If child._originalPositioning is set, it means explicit positioning was provided
  -- and _explicitlyAbsolute was already set correctly during construction

  table.insert(self.children, child)

  -- Only recalculate auto-sizing if the child participates in layout
  -- (CSS: absolutely positioned children don't affect parent auto-sizing)
  if not child._explicitlyAbsolute then
    local sizeChanged = false

    if self.autosizing.height then
      local oldHeight = self.height
      local contentHeight = self:calculateAutoHeight()
      -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
      self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
      self.height = contentHeight
      if oldHeight ~= self.height then
        sizeChanged = true
      end
    end
    if self.autosizing.width then
      local oldWidth = self.width
      local contentWidth = self:calculateAutoWidth()
      -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
      self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
      self.width = contentWidth
      if oldWidth ~= self.width then
        sizeChanged = true
      end
    end

    -- Propagate size change up the tree
    if sizeChanged and self.parent and (self.parent.autosizing.width or self.parent.autosizing.height) then
      -- Trigger parent to recalculate its size by re-adding this child's contribution
      -- This ensures grandparents are notified of size changes
      if self.parent.autosizing.height then
        local contentHeight = self.parent:calculateAutoHeight()
        self.parent._borderBoxHeight = contentHeight + self.parent.padding.top + self.parent.padding.bottom
        self.parent.height = contentHeight
      end
      if self.parent.autosizing.width then
        local contentWidth = self.parent:calculateAutoWidth()
        self.parent._borderBoxWidth = contentWidth + self.parent.padding.left + self.parent.padding.right
        self.parent.width = contentWidth
      end
    end
  end

  -- In immediate mode, defer layout until endFrame() when all elements are created
  -- This prevents premature overflow detection with incomplete children
  if not Element._Context._immediateMode then
    self:layoutChildren()
  end
end

--- Remove a child element from the hierarchy to dynamically update UIs
--- Use this to delete elements when they're no longer needed or respond to user actions
---@param child Element
function Element:removeChild(child)
  for i, c in ipairs(self.children) do
    if c == child then
      table.remove(self.children, i)
      child.parent = nil

      -- Recalculate auto-sizing if needed
      if self.autosizing.width or self.autosizing.height then
        if self.autosizing.width then
          local contentWidth = self:calculateAutoWidth()
          self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
          self.width = contentWidth
        end
        if self.autosizing.height then
          local contentHeight = self:calculateAutoHeight()
          self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
          self.height = contentHeight
        end
      end

      -- Re-layout children after removal
      if not Element._Context._immediateMode then
        self:layoutChildren()
      end

      break
    end
  end
end

--- Delete all child elements at once for resetting containers or clearing lists
--- Use this to efficiently empty containers when rebuilding UI from scratch
function Element:clearChildren()
  -- Clear parent references for all children
  for _, child in ipairs(self.children) do
    child.parent = nil
  end

  -- Clear the children table
  self.children = {}

  -- Recalculate auto-sizing if needed
  if self.autosizing.width or self.autosizing.height then
    if self.autosizing.width then
      local contentWidth = self:calculateAutoWidth()
      self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
      self.width = contentWidth
    end
    if self.autosizing.height then
      local contentHeight = self:calculateAutoHeight()
      self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
      self.height = contentHeight
    end
  end

  -- Re-layout (though there are no children now)
  if not Element._Context._immediateMode then
    self:layoutChildren()
  end
end

--- Get the number of children this element has
---@return number
function Element:getChildCount()
  return #self.children
end

--- Apply positioning offsets (top, right, bottom, left) to an element
-- @param element The element to apply offsets to
function Element:applyPositioningOffsets(element)
  -- Delegate to LayoutEngine
  self._layoutEngine:applyPositioningOffsets(element)
end

function Element:layoutChildren()
  -- Check performance warnings (only on root elements to avoid spam)
  if not self.parent then
    self:_checkPerformanceWarnings()
  end

  -- Delegate layout to LayoutEngine
  self._layoutEngine:layoutChildren()
end

--- Destroy element and its children
function Element:destroy()
  -- Remove from global elements list
  for i, win in ipairs(Element._Context.topElements) do
    if win == self then
      table.remove(Element._Context.topElements, i)
      break
    end
  end

  if self.parent then
    for i, child in ipairs(self.parent.children) do
      if child == self then
        table.remove(self.parent.children, i)
        break
      end
    end
    self.parent = nil
  end

  -- Destroy all children
  for _, child in ipairs(self.children) do
    child:destroy()
  end

  -- Clear children table
  self.children = {}

  -- Clear parent reference
  if self.parent then
    self.parent = nil
  end

  -- Clear animation reference
  self.animation = nil

  -- Clear onEvent to prevent closure leaks
  self.onEvent = nil
end

--- Draw element and its children
function Element:draw(backdropCanvas)
  -- Early exit if element is invisible (optimization)
  if self.opacity <= 0 then
    return
  end

  -- Handle opacity during animation
  local drawBackgroundColor = self.backgroundColor
  if self.animation then
    local anim = self.animation:interpolate()
    if anim.opacity then
      drawBackgroundColor = Element._Color.new(self.backgroundColor.r, self.backgroundColor.g, self.backgroundColor.b, anim.opacity)
    end
  end

  -- Cache border box dimensions for this draw call (optimization)
  local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
  local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)

  -- LAYERS 0.5-3: Delegate visual rendering (backdrop blur, background, image, theme, borders) to Renderer module
  self._renderer:draw(self, backdropCanvas)

  -- LAYER 4: Delegate text rendering (text, cursor, selection, placeholder, password masking) to Renderer module
  self._renderer:drawText(self)

  -- Draw visual feedback when element is pressed (if it has an onEvent handler and highlight is not disabled)
  if self.onEvent and not self.disableHighlight and self._eventHandler then
    -- Check if any button is pressed
    local anyPressed = false
    local pressedState = self._eventHandler:getState()._pressed or {}
    for _, pressed in pairs(pressedState) do
      if pressed then
        anyPressed = true
        break
      end
    end
    if anyPressed then
      -- BORDER-BOX MODEL: Use stored border-box dimensions for drawing
      local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
      local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
      self._renderer:drawPressedState(self.x, self.y, borderBoxWidth, borderBoxHeight)
    end
  end

  -- Sort children by z-index before drawing
  local sortedChildren = {}
  for _, child in ipairs(self.children) do
    table.insert(sortedChildren, child)
  end
  table.sort(sortedChildren, function(a, b)
    return a.z < b.z
  end)

  -- Check if we need to clip children to rounded corners
  local hasRoundedCorners = false
  if self.cornerRadius then
    if type(self.cornerRadius) == "number" then
      hasRoundedCorners = self.cornerRadius > 0
    else
      hasRoundedCorners = self.cornerRadius.topLeft > 0
        or self.cornerRadius.topRight > 0
        or self.cornerRadius.bottomLeft > 0
        or self.cornerRadius.bottomRight > 0
    end
  end

  -- Helper function to draw children (with or without clipping)
  local function drawChildren()
    -- Determine overflow behavior per axis (matches HTML/CSS behavior)
    -- Priority: axis-specific (overflowX/Y) > general (overflow) > default (hidden)
    local overflowX = self.overflowX or self.overflow
    local overflowY = self.overflowY or self.overflow
    local needsOverflowClipping = (overflowX ~= "visible" or overflowY ~= "visible") and (overflowX ~= nil or overflowY ~= nil)

    -- Apply scroll offset if overflow is not visible
    local hasScrollOffset = needsOverflowClipping and (self._scrollX ~= 0 or self._scrollY ~= 0)

    if hasRoundedCorners and #sortedChildren > 0 then
      -- Use stencil to clip children to rounded rectangle
      -- BORDER-BOX MODEL: Use stored border-box dimensions for clipping
      local borderBoxWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
      local borderBoxHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)
      local stencilFunc = Element._RoundedRect.stencilFunction(self.x, self.y, borderBoxWidth, borderBoxHeight, self.cornerRadius)

      -- Temporarily disable canvas for stencil operation (LÖVE 11.5 workaround)
      local currentCanvas = love.graphics.getCanvas()
      love.graphics.setCanvas()
      love.graphics.stencil(stencilFunc, "replace", 1)
      love.graphics.setCanvas(currentCanvas)

      love.graphics.setStencilTest("greater", 0)

      -- Apply scroll offset AFTER clipping is set
      if hasScrollOffset then
        love.graphics.push()
        love.graphics.translate(-self._scrollX, -self._scrollY)
      end

      for _, child in ipairs(sortedChildren) do
        child:draw(backdropCanvas)
      end

      if hasScrollOffset then
        love.graphics.pop()
      end

      love.graphics.setStencilTest()
    elseif needsOverflowClipping and #sortedChildren > 0 then
      -- Clip content for overflow hidden/scroll/auto without rounded corners
      local contentX = self.x + self.padding.left
      local contentY = self.y + self.padding.top
      local contentWidth = self.width
      local contentHeight = self.height

      love.graphics.setScissor(contentX, contentY, contentWidth, contentHeight)

      -- Apply scroll offset AFTER clipping is set
      if hasScrollOffset then
        love.graphics.push()
        love.graphics.translate(-self._scrollX, -self._scrollY)
      end

      for _, child in ipairs(sortedChildren) do
        child:draw(backdropCanvas)
      end

      if hasScrollOffset then
        love.graphics.pop()
      end

      love.graphics.setScissor()
    else
      -- No clipping needed
      for _, child in ipairs(sortedChildren) do
        child:draw(backdropCanvas)
      end
    end
  end

  -- Apply content blur if configured
  if self.contentBlur and self.contentBlur.radius > 0 and #sortedChildren > 0 then
    local blurInstance = self:getBlurInstance()
    if blurInstance then
      Element._Blur.applyToRegion(blurInstance, self.contentBlur.radius, self.x, self.y, borderBoxWidth, borderBoxHeight, drawChildren)
    else
      drawChildren()
    end
  else
    drawChildren()
  end

  -- Draw scrollbars if overflow is scroll or auto
  -- IMPORTANT: Scrollbars must be drawn without parent clipping
  local overflowX = self.overflowX or self.overflow
  local overflowY = self.overflowY or self.overflow
  if overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto" then
    local scrollbarDims = self:_calculateScrollbarDimensions()
    if scrollbarDims.vertical.visible or scrollbarDims.horizontal.visible then
      -- Clear any parent scissor clipping before drawing scrollbars
      love.graphics.setScissor()
      -- Delegate scrollbar rendering to Renderer module
      self._renderer:drawScrollbars(self, self.x, self.y, self.width, self.height, scrollbarDims)
    end
  end
end

--- Update element (propagate to children)
---@param dt number
function Element:update(dt)
  -- Track active animations for performance warnings (only on root elements)
  if not self.parent then
    self:_trackActiveAnimations()
  end

  -- Restore scrollbar state from StateManager in immediate mode
  if self._stateId and Element._Context._immediateMode then
    local state = Element._StateManager.getState(self._stateId)
    if state then
      self._scrollbarHoveredVertical = state.scrollbarHoveredVertical or false
      self._scrollbarHoveredHorizontal = state.scrollbarHoveredHorizontal or false
      self._scrollbarDragging = state.scrollbarDragging or false
      self._hoveredScrollbar = state.hoveredScrollbar
      self._scrollbarDragOffset = state.scrollbarDragOffset or 0

      if self._scrollManager then
        self._scrollManager._scrollbarHoveredVertical = self._scrollbarHoveredVertical
        self._scrollManager._scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal
        self._scrollManager._scrollbarDragging = self._scrollbarDragging
        self._scrollManager._hoveredScrollbar = self._hoveredScrollbar
        self._scrollManager._scrollbarDragOffset = self._scrollbarDragOffset
      end
    end
  end

  for _, child in ipairs(self.children) do
    child:update(dt)
  end

  -- Update text editor cursor blink
  if self._textEditor then
    self._textEditor:update(self, dt)
  end

  -- Update animation if exists
  if self.animation then
    -- Ensure animation has Color module reference for color interpolation
    if Element._Animation and not Element._Animation._ColorModule and Element._Color then
      Element._Animation._ColorModule = Element._Color
    end

    -- Ensure animation has Transform module reference for transform interpolation
    if Element._Animation and not Element._Animation._TransformModule and Element._Transform then
      Element._Animation._TransformModule = Element._Transform
    end

    local finished = self.animation:update(dt, self)
    if finished then
      -- Animation:update() already called onComplete callback
      -- Check for chained animation
      if self.animation._next then
        self.animation = self.animation._next
      elseif self.animation._nextFactory and type(self.animation._nextFactory) == "function" then
        local success, nextAnim = pcall(self.animation._nextFactory, self)
        if success and nextAnim then
          self.animation = nextAnim
        else
          self.animation = nil
        end
      else
        self.animation = nil
      end
    else
      -- Apply animation interpolation during update
      local anim = self.animation:interpolate()

      -- Apply numeric properties
      self.width = anim.width or self.width
      self.height = anim.height or self.height
      self.opacity = anim.opacity or self.opacity
      self.x = anim.x or self.x
      self.y = anim.y or self.y
      self.gap = anim.gap or self.gap
      self.imageOpacity = anim.imageOpacity or self.imageOpacity
      self.scrollbarWidth = anim.scrollbarWidth or self.scrollbarWidth
      self.borderWidth = anim.borderWidth or self.borderWidth
      self.fontSize = anim.fontSize or self.fontSize
      self.lineHeight = anim.lineHeight or self.lineHeight

      -- Apply color properties
      if anim.backgroundColor then
        self.backgroundColor = anim.backgroundColor
      end
      if anim.borderColor then
        self.borderColor = anim.borderColor
      end
      if anim.textColor then
        self.textColor = anim.textColor
      end
      if anim.scrollbarColor then
        self.scrollbarColor = anim.scrollbarColor
      end
      if anim.scrollbarBackgroundColor then
        self.scrollbarBackgroundColor = anim.scrollbarBackgroundColor
      end
      if anim.imageTint then
        self.imageTint = anim.imageTint
      end

      -- Apply table properties
      if anim.padding then
        self.padding = anim.padding
      end
      if anim.margin then
        self.margin = anim.margin
      end
      if anim.cornerRadius then
        self.cornerRadius = anim.cornerRadius
      end

      -- Apply transform property
      if anim.transform then
        self.transform = anim.transform
      end

      -- Backward compatibility: Update background color with interpolated opacity
      if anim.opacity and not anim.backgroundColor then
        self.backgroundColor.a = anim.opacity
      end
    end
  end

  local mx, my = love.mouse.getPosition()

  if self._scrollManager then
    self._scrollManager:updateHoverState(self, mx, my)
    self:_syncScrollManagerState()
  end

  if self._stateId and Element._Context._immediateMode then
    Element._StateManager.updateState(self._stateId, {
      scrollbarHoveredVertical = self._scrollbarHoveredVertical,
      scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal,
      scrollbarDragging = self._scrollbarDragging,
      hoveredScrollbar = self._hoveredScrollbar,
    })
  end

  if self._scrollbarDragging and love.mouse.isDown(1) then
    self:_handleScrollbarDrag(mx, my)
  elseif self._scrollbarDragging then
    if self._scrollManager then
      self._scrollManager:handleMouseRelease(1)
      self:_syncScrollManagerState()
    end

    if self._stateId and Element._Context._immediateMode then
      Element._StateManager.updateState(self._stateId, {
        scrollbarDragging = false,
      })
    end
  end

  -- Handle scrollbar click/press (independent of onEvent)
  -- Check if we should handle scrollbar press for elements with overflow
  local overflowX = self.overflowX or self.overflow
  local overflowY = self.overflowY or self.overflow
  local hasScrollableOverflow = (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto")

  if hasScrollableOverflow and not self._scrollbarDragging then
    -- Check for scrollbar press on left mouse button
    if love.mouse.isDown(1) and not self._scrollbarPressHandled then
      local scrollbarPressed = self:_handleScrollbarPress(mx, my, 1)
      if scrollbarPressed then
        self._scrollbarPressHandled = true
      end
    elseif not love.mouse.isDown(1) then
      -- Reset press handled flag when button is released
      self._scrollbarPressHandled = false
    end
  end

  if self.onEvent or self.themeComponent or self.editable then
    -- Clickable area is the border box (x, y already includes padding)
    -- BORDER-BOX MODEL: Use stored border-box dimensions for hit detection
    local bx = self.x
    local by = self.y
    local bw = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right)
    local bh = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom)

    -- Account for scroll offsets from parent containers
    -- Walk up the parent chain and accumulate scroll offsets
    local scrollOffsetX = 0
    local scrollOffsetY = 0
    local current = self.parent
    while current do
      local overflowX = current.overflowX or current.overflow
      local overflowY = current.overflowY or current.overflow
      local hasScrollableOverflow = (
        overflowX == "scroll"
        or overflowX == "auto"
        or overflowY == "scroll"
        or overflowY == "auto"
        or overflowX == "hidden"
        or overflowY == "hidden"
      )
      if hasScrollableOverflow then
        scrollOffsetX = scrollOffsetX + (current._scrollX or 0)
        scrollOffsetY = scrollOffsetY + (current._scrollY or 0)
      end
      current = current.parent
    end

    -- Adjust mouse position by accumulated scroll offset for hit testing
    local adjustedMx = mx + scrollOffsetX
    local adjustedMy = my + scrollOffsetY
    local isHovering = adjustedMx >= bx and adjustedMx <= bx + bw and adjustedMy >= by and adjustedMy <= by + bh

    -- Check if this is the topmost element at the mouse position (z-index ordering)
    -- This prevents blocked elements from receiving interactions or visual feedback
    local isActiveElement
    if Element._Context._immediateMode then
      -- In immediate mode, use z-index occlusion detection
      local topElement = Element._Context.getTopElementAt(mx, my)
      isActiveElement = (topElement == self or topElement == nil)
    else
      -- In retained mode, use the old _activeEventElement mechanism
      isActiveElement = (Element._Context._activeEventElement == nil or Element._Context._activeEventElement == self)
    end

    -- Reset scrollbar press flag at start of each frame
    self._eventHandler:resetScrollbarPressFlag()

    -- Process mouse events through EventHandler FIRST
    -- This ensures pressed states are updated before theme state is calculated
    self._eventHandler:processMouseEvents(self, mx, my, isHovering, isActiveElement)

    -- In immediate mode, save EventHandler state to StateManager after processing events
    if self._stateId and Element._Context._immediateMode and self._stateId ~= "" then
      local eventHandlerState = self._eventHandler:getState()
      Element._StateManager.updateState(self._stateId, {
        _pressed = eventHandlerState._pressed,
        _lastClickTime = eventHandlerState._lastClickTime,
        _lastClickButton = eventHandlerState._lastClickButton,
        _clickCount = eventHandlerState._clickCount,
        _dragStartX = eventHandlerState._dragStartX,
        _dragStartY = eventHandlerState._dragStartY,
        _lastMouseX = eventHandlerState._lastMouseX,
        _lastMouseY = eventHandlerState._lastMouseY,
        _hovered = eventHandlerState._hovered,
      })
    end

    -- Update theme state based on interaction
    if self.themeComponent then
      -- Check if any button is pressed via EventHandler
      local anyPressed = self._eventHandler:isAnyButtonPressed()

      -- Update theme state via ThemeManager
      local newThemeState = self._themeManager:updateState(isHovering and isActiveElement, anyPressed, self._focused, self.disabled)

      -- Update state (in StateManager if in immediate mode, otherwise locally)
      if self._stateId and Element._Context._immediateMode then
        -- Update in StateManager for immediate mode
        local hover = newThemeState == "hover"
        local pressed = newThemeState == "pressed"
        local focused = newThemeState == "active" or self._focused

        Element._StateManager.updateState(self._stateId, {
          hover = hover,
          pressed = pressed,
          focused = focused,
          disabled = self.disabled,
          active = self.active,
        })
      end

      -- Always update local state for backward compatibility
      self._themeState = newThemeState
      -- Sync theme state with Renderer module
      if self._renderer then
        self._renderer:setThemeState(newThemeState)
      end
    end

    -- Process touch events through EventHandler
    self._eventHandler:processTouchEvents(self)
  end
end

---@param newViewportWidth number
---@param newViewportHeight number
function Element:recalculateUnits(newViewportWidth, newViewportHeight)
  self._layoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
end

--- Resize element and its children based on game window size change
---@param newGameWidth number
---@param newGameHeight number
function Element:resize(newGameWidth, newGameHeight)
  self:recalculateUnits(newGameWidth, newGameHeight)

  -- For non-auto-sized elements with viewport/percentage units, update content dimensions from border-box
  if not self.autosizing.width and self._borderBoxWidth and self.units.width.unit ~= "px" then
    self.width = math.max(0, self._borderBoxWidth - self.padding.left - self.padding.right)
  end
  if not self.autosizing.height and self._borderBoxHeight and self.units.height.unit ~= "px" then
    self.height = math.max(0, self._borderBoxHeight - self.padding.top - self.padding.bottom)
  end

  -- Update children
  for _, child in ipairs(self.children) do
    child:resize(newGameWidth, newGameHeight)
  end

  -- Recalculate auto-sized dimensions after children are resized
  if self.autosizing.width then
    local contentWidth = self:calculateAutoWidth()
    -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
    self._borderBoxWidth = contentWidth + self.padding.left + self.padding.right
    self.width = contentWidth
  end
  if self.autosizing.height then
    local contentHeight = self:calculateAutoHeight()
    -- BORDER-BOX MODEL: Add padding to get border-box, then subtract to get content
    self._borderBoxHeight = contentHeight + self.padding.top + self.padding.bottom
    self.height = contentHeight
  end

  -- Re-resolve ew/eh textSize units after all dimensions are finalized
  -- This ensures textSize updates based on current width/height (whether calculated or manually set)
  if self.units.textSize.value then
    local unit = self.units.textSize.unit
    local value = self.units.textSize.value
    local _, scaleY = Element._Context.getScaleFactors()

    if unit == "ew" then
      -- Element width relative (use current width)
      self.textSize = (value / 100) * self.width

      -- Apply min/max constraints
      local minSize = self.minTextSize and (Element._Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize)
      local maxSize = self.maxTextSize and (Element._Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize)
      if minSize and self.textSize < minSize then
        self.textSize = minSize
      end
      if maxSize and self.textSize > maxSize then
        self.textSize = maxSize
      end
      if self.textSize < 1 then
        self.textSize = 1
      end
    elseif unit == "eh" then
      -- Element height relative (use current height)
      self.textSize = (value / 100) * self.height

      -- Apply min/max constraints
      local minSize = self.minTextSize and (Element._Context.baseScale and (self.minTextSize * scaleY) or self.minTextSize)
      local maxSize = self.maxTextSize and (Element._Context.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize)
      if minSize and self.textSize < minSize then
        self.textSize = minSize
      end
      if maxSize and self.textSize > maxSize then
        self.textSize = maxSize
      end
      if self.textSize < 1 then
        self.textSize = 1
      end
    end
  end

  self:layoutChildren()
  self.prevGameSize.width = newGameWidth
  self.prevGameSize.height = newGameHeight
end

--- Calculate text width for button
---@return number
function Element:calculateTextWidth()
  if self.text == nil then
    return 0
  end

  local font = Element._utils.getFont(self.textSize, self.fontFamily, self.themeComponent, self._themeManager)
  local width = font:getWidth(self.text)
  return Element._utils.applyContentMultiplier(width, self.contentAutoSizingMultiplier, "width")
end

---@return number
function Element:calculateTextHeight()
  if self.text == nil then
    return 0
  end

  local font = Element._utils.getFont(self.textSize, self.fontFamily, self.themeComponent, self._themeManager)
  local height = font:getHeight()

  if self.textWrap and (self.textWrap == "word" or self.textWrap == "char" or self.textWrap == true) then
    local availableWidth = self.width

    if (not availableWidth or availableWidth <= 0) and self.parent then
      availableWidth = self.parent.width
    end

    if availableWidth and availableWidth > 0 then
      local wrappedWidth, wrappedLines = font:getWrap(self.text, availableWidth)
      height = height * #wrappedLines
    end
  end

  return Element._utils.applyContentMultiplier(height, self.contentAutoSizingMultiplier, "height")
end

function Element:calculateAutoWidth()
  return self._layoutEngine:calculateAutoWidth()
end

--- Calculate auto height based on children
function Element:calculateAutoHeight()
  return self._layoutEngine:calculateAutoHeight()
end

---@param newText string
---@param autoresize boolean? --default: false
function Element:updateText(newText, autoresize)
  self.text = newText or self.text
  if autoresize then
    self.width = self:calculateTextWidth()
    self.height = self:calculateTextHeight()
  end
end

---@param newOpacity number
function Element:updateOpacity(newOpacity)
  self.opacity = newOpacity
  for _, child in ipairs(self.children) do
    child:updateOpacity(newOpacity)
  end
end

--- same as calling updateOpacity(0)
function Element:hide()
  self:updateOpacity(0)
end

--- same as calling updateOpacity(1)
function Element:show()
  self:updateOpacity(1)
end

-- ====================
-- Input Handling - Cursor Management
-- ====================

--- Set cursor position
---@param position number -- Character index (0-based)
function Element:setCursorPosition(position)
  if self._textEditor then
    self._textEditor:setCursorPosition(self, position)
  end
end

--- Get cursor position
---@return number -- Character index (0-based)
function Element:getCursorPosition()
  if self._textEditor then
    return self._textEditor:getCursorPosition()
  end
  return 0
end

--- Move cursor by delta characters
---@param delta number -- Number of characters to move (positive or negative)
function Element:moveCursorBy(delta)
  if self._textEditor then
    self._textEditor:moveCursorBy(self, delta)
  end
end

--- Move cursor to start of text
function Element:moveCursorToStart()
  if self._textEditor then
    self._textEditor:moveCursorToStart(self)
  end
end

--- Move cursor to end of text
function Element:moveCursorToEnd()
  if self._textEditor then
    self._textEditor:moveCursorToEnd(self)
  end
end

--- Move cursor to start of current line
function Element:moveCursorToLineStart()
  if self._textEditor then
    self._textEditor:moveCursorToLineStart(self)
  end
end

--- Move cursor to end of current line
function Element:moveCursorToLineEnd()
  if self._textEditor then
    self._textEditor:moveCursorToLineEnd(self)
  end
end

--- Move cursor to start of previous word
function Element:moveCursorToPreviousWord()
  if self._textEditor then
    self._textEditor:moveCursorToPreviousWord(self)
  end
end

--- Move cursor to start of next word
function Element:moveCursorToNextWord()
  if self._textEditor then
    self._textEditor:moveCursorToNextWord(self)
  end
end

-- ====================
-- Input Handling - Selection Management
-- ====================

--- Set selection range
---@param startPos number -- Start position (inclusive)
---@param endPos number -- End position (inclusive)
function Element:setSelection(startPos, endPos)
  if self._textEditor then
    self._textEditor:setSelection(self, startPos, endPos)
  end
end

--- Get selection range
---@return number?, number? -- Start and end positions, or nil if no selection
function Element:getSelection()
  if self._textEditor then
    return self._textEditor:getSelection()
  end
  return nil, nil
end

--- Check if there is an active selection
---@return boolean
function Element:hasSelection()
  if self._textEditor then
    return self._textEditor:hasSelection()
  end
  return false
end

--- Clear selection
function Element:clearSelection()
  if self._textEditor then
    self._textEditor:clearSelection(self)
  end
end

--- Select all text
function Element:selectAll()
  if self._textEditor then
    self._textEditor:selectAll(self)
  end
end

--- Get selected text
---@return string? -- Selected text or nil if no selection
function Element:getSelectedText()
  if self._textEditor then
    return self._textEditor:getSelectedText()
  end
  return nil
end

--- Delete selected text
---@return boolean -- True if text was deleted
function Element:deleteSelection()
  if self._textEditor then
    local result = self._textEditor:deleteSelection(self)
    if result then
      self.text = self._textEditor:getText() -- Sync display text
      self._textEditor:updateAutoGrowHeight(self)
    end
    return result
  end
  return false
end

-- ====================
-- Input Handling - Focus Management
-- ====================

--- Give this element keyboard focus to enable text input or keyboard navigation
--- Use this to automatically focus text fields when showing forms or dialogs
function Element:focus()
  if self._textEditor then
    self._textEditor:focus(self)
  end
end

--- Remove keyboard focus to stop capturing input events
--- Use this when closing popups or switching focus to other elements
function Element:blur()
  if self._textEditor then
    self._textEditor:blur(self)
  end
end

--- Query focus state to conditionally render focus indicators or handle keyboard input
--- Use this to style focused elements or determine which element receives keyboard events
---@return boolean
function Element:isFocused()
  if self._textEditor then
    return self._textEditor:isFocused()
  end
  return false
end

-- ====================
-- Input Handling - Text Buffer Management
-- ====================

--- Retrieve the element's current text content for processing or validation
--- Use this to read user input from text fields or get display text
---@return string
function Element:getText()
  if self._textEditor then
    return self._textEditor:getText()
  end
  return self.text or ""
end

--- Update the element's text content programmatically for dynamic labels or resetting inputs
--- Use this to change text without user input, like clearing fields or updating status messages
---@param text string
function Element:setText(text)
  if self._textEditor then
    self._textEditor:setText(self, text)
    self.text = self._textEditor:getText() -- Sync display text
    self._textEditor:updateAutoGrowHeight(self)
    return
  end
  self.text = text
end

--- Programmatically insert text at any position for autocomplete or text manipulation
--- Use this to implement suggestions, templates, or text snippets
---@param text string -- Text to insert
---@param position number? -- Position to insert at (default: cursor position)
function Element:insertText(text, position)
  if self._textEditor then
    self._textEditor:insertText(self, text, position)
    self.text = self._textEditor:getText() -- Sync display text
    self._textEditor:updateAutoGrowHeight(self)
  end
end

---@param startPos number -- Start position (inclusive)
---@param endPos number -- End position (inclusive)
function Element:deleteText(startPos, endPos)
  if self._textEditor then
    self._textEditor:deleteText(self, startPos, endPos)
    self.text = self._textEditor:getText() -- Sync display text
    self._textEditor:updateAutoGrowHeight(self)
  end
end

--- Replace text in range
---@param startPos number -- Start position (inclusive)
---@param endPos number -- End position (inclusive)
---@param newText string -- Replacement text
function Element:replaceText(startPos, endPos, newText)
  if self._textEditor then
    self._textEditor:replaceText(self, startPos, endPos, newText)
    self.text = self._textEditor:getText() -- Sync display text
    self._textEditor:updateAutoGrowHeight(self)
  end
end

--- Wrap a single line of text
---@param line string -- Line to wrap
---@param maxWidth number -- Maximum width in pixels
---@return table -- Array of wrapped line parts
function Element:_wrapLine(line, maxWidth)
  return self._renderer:wrapLine(self, line, maxWidth)
end

---@return love.Font
function Element:_getFont()
  return self._renderer:getFont(self)
end

-- ====================
-- Input Handling - Mouse Selection
-- ====================

--- Handle mouse click on text (set cursor position or start selection)
---@param mouseX number -- Mouse X coordinate
---@param mouseY number -- Mouse Y coordinate
---@param clickCount number -- Number of clicks (1=single, 2=double, 3=triple)
function Element:_handleTextClick(mouseX, mouseY, clickCount)
  if self._textEditor then
    self._textEditor:handleTextClick(self, mouseX, mouseY, clickCount)
    -- Store mouse down position on element for drag tracking
    if clickCount == 1 then
      self._mouseDownPosition = self._textEditor:mouseToTextPosition(self, mouseX, mouseY)
    end
  end
end

--- Handle mouse drag for text selection
---@param mouseX number -- Mouse X coordinate
---@param mouseY number -- Mouse Y coordinate
function Element:_handleTextDrag(mouseX, mouseY)
  if self._textEditor then
    self._textEditor:handleTextDrag(self, mouseX, mouseY)
    self._textDragOccurred = self._textEditor._textDragOccurred
  end
end

-- ====================
-- Input Handling - Keyboard Input
-- ====================

--- Handle text input (character input)
---@param text string -- Character(s) to insert
function Element:textinput(text)
  if self._textEditor then
    self._textEditor:handleTextInput(self, text)
    self.text = self._textEditor:getText() -- Sync display text
    self._textEditor:updateAutoGrowHeight(self)
  end
end

--- Handle key press (special keys)
---@param key string -- Key name
---@param scancode string -- Scancode
---@param isrepeat boolean -- Whether this is a key repeat
function Element:keypressed(key, scancode, isrepeat)
  if self._textEditor then
    self._textEditor:handleKeyPress(self, key, scancode, isrepeat)
    self.text = self._textEditor:getText() -- Sync display text
    self._textEditor:updateAutoGrowHeight(self)
  end
end

-- ====================
-- Performance Monitoring
-- ====================

--- Get hierarchy depth of this element
---@return number depth Depth in the element tree (0 for root)
function Element:getHierarchyDepth()
  local depth = 0
  local current = self.parent
  while current do
    depth = depth + 1
    current = current.parent
  end
  return depth
end

--- Count total elements in this tree
---@return number count Total number of elements including this one and all descendants
function Element:countElements()
  local count = 1 -- Count self
  for _, child in ipairs(self.children) do
    count = count + child:countElements()
  end
  return count
end

function Element:_checkPerformanceWarnings()
  if not Element._Performance or not Element._Performance.warningsEnabled then
    return
  end

  -- Check hierarchy depth
  local depth = self:getHierarchyDepth()
  if depth >= 15 then
    Element._Performance:logWarning(
      string.format("hierarchy_depth_%s", self.id),
      "Element",
      string.format("Element hierarchy depth is %d levels for element '%s'", depth, self.id or "unnamed"),
      { depth = depth, elementId = self.id or "unnamed" },
      "Deep nesting can impact performance. Consider flattening the structure or using absolute positioning"
    )
  end

  -- Check total element count (only for root elements)
  if not self.parent then
    local totalElements = self:countElements()
    if totalElements >= 1000 then
      Element._Performance:logWarning(
        "element_count_high",
        "Element",
        string.format("UI contains %d+ elements", totalElements),
        { elementCount = totalElements },
        "Large element counts may impact performance. Consider virtualization for long lists or pagination for large datasets"
      )
    end
  end
end

--- Count active animations in tree
---@return number count Number of active animations
function Element:_countActiveAnimations()
  local count = self.animation and 1 or 0
  for _, child in ipairs(self.children) do
    count = count + child:_countActiveAnimations()
  end
  return count
end

--- Track active animations and warn if too many
function Element:_trackActiveAnimations()
  -- Get Performance instance from deps if available
  if not Element._Performance or not Element._Performance.warningsEnabled then
    return
  end

  local animCount = self:_countActiveAnimations()
  if animCount >= 50 then
    Element._Performance:logWarning(
      "animation_count_high",
      "Element",
      string.format("%d+ animations running simultaneously", animCount),
      { animationCount = animCount },
      "High animation counts may impact frame rate. Consider reducing concurrent animations or using CSS-style transitions"
    )
  end
end

--- Change the tint color of an image element dynamically for hover effects or state indication
--- Use this to recolor images without replacing the asset, like highlighting selected items
---@param color Color Color to tint the image
function Element:setImageTint(color)
  self.imageTint = color
  if self._renderer then
    self._renderer.imageTint = color
  end
end

--- Adjust image transparency independently from the element for fade effects
--- Use this to create image-specific fade animations or disabled states
---@param opacity number Opacity 0-1
function Element:setImageOpacity(opacity)
  if opacity ~= nil then
    Element._utils.validateRange(opacity, 0, 1, "imageOpacity")
  end
  self.imageOpacity = opacity
  if self._renderer then
    self._renderer.imageOpacity = opacity
  end
end

--- Set image repeat mode
---@param repeatMode string Repeat mode: "no-repeat", "repeat", "repeat-x", "repeat-y", "space", "round"
function Element:setImageRepeat(repeatMode)
  local validImageRepeat = {
    ["no-repeat"] = "no-repeat",
    ["repeat"] = "repeat",
    ["repeat-x"] = "repeat-x",
    ["repeat-y"] = "repeat-y",
    space = "space",
    round = "round",
  }
  Element._utils.validateEnum(repeatMode, validImageRepeat, "imageRepeat")
  self.imageRepeat = repeatMode
  if self._renderer then
    self._renderer.imageRepeat = repeatMode
  end
end

--- Apply rotation transform to create spinning animations or rotated layouts
--- Use this for loading spinners, compass needles, or angled UI elements
---@param angle number Angle in radians
function Element:rotate(angle)
  if not self.transform then
    self.transform = Element._Transform.new({})
  end
  self.transform.rotate = angle
end

--- Resize element visually using scale transforms for zoom effects
--- Use this for hover magnification, shrinking animations, or responsive scaling
---@param scaleX number X-axis scale
---@param scaleY number? Y-axis scale (defaults to scaleX)
function Element:scale(scaleX, scaleY)
  if not self.transform then
    self.transform = Element._Transform.new({})
  end
  self.transform.scaleX = scaleX
  self.transform.scaleY = scaleY or scaleX
end

--- Offset element position using transforms for smooth movement without layout recalculation
--- Use this for parallax effects, draggable elements, or position animations
---@param x number X translation
---@param y number Y translation
function Element:translate(x, y)
  if not self.transform then
    self.transform = Element._Transform.new({})
  end
  self.transform.translateX = x
  self.transform.translateY = y
end

--- Define the pivot point for rotation and scaling transforms
--- Use this to rotate around corners, edges, or custom points rather than the center
---@param originX number X origin (0-1, where 0.5 is center)
---@param originY number Y origin (0-1, where 0.5 is center)
function Element:setTransformOrigin(originX, originY)
  if not self.transform then
    self.transform = Element._Transform.new({})
  end
  self.transform.originX = originX
  self.transform.originY = originY
end

--- Set transition configuration for a property
---@param property string Property name or "all" for all properties
---@param config table Transition config {duration, easing, delay, onComplete}
function Element:setTransition(property, config)
  if not self.transitions then
    self.transitions = {}
  end

  if type(config) ~= "table" then
    Element._ErrorHandler:warn("Element", "ELEM_003")
    config = {}
  end

  -- Validate config
  if config.duration and (type(config.duration) ~= "number" or config.duration < 0) then
    Element._ErrorHandler:warn("Element", "ELEM_004", {
      value = tostring(config.duration),
    })
    config.duration = 0.3
  end

  self.transitions[property] = {
    duration = config.duration or 0.3,
    easing = config.easing or "easeOutQuad",
    delay = config.delay or 0,
    onComplete = config.onComplete,
  }
end

--- Set transition configuration for multiple properties
---@param groupName string Name for this transition group
---@param config table Transition config {duration, easing, delay, onComplete}
---@param properties table Array of property names
function Element:setTransitionGroup(groupName, config, properties)
  if type(properties) ~= "table" then
    Element._ErrorHandler:warn("Element", "ELEM_005")
    return
  end

  for _, prop in ipairs(properties) do
    self:setTransition(prop, config)
  end
end

--- Remove transition configuration for a property
---@param property string Property name or "all" to remove all
function Element:removeTransition(property)
  if not self.transitions then
    return
  end

  if property == "all" then
    self.transitions = {}
  else
    self.transitions[property] = nil
  end
end

--- Set property with automatic transition
---@param property string Property name
---@param value any New value
function Element:setProperty(property, value)
  -- Check if transitions are enabled for this property
  local shouldTransition = false
  local transitionConfig = nil

  if self.transitions then
    transitionConfig = self.transitions[property] or self.transitions["all"]
    shouldTransition = transitionConfig ~= nil
  end

  -- Don't transition if value is the same
  if self[property] == value then
    return
  end

  if shouldTransition and transitionConfig then
    local currentValue = self[property]

    -- Only transition if we have a valid current value
    if currentValue ~= nil then
      -- Create animation for the property change
      local Animation = require("modules.Animation")
      local anim = Animation.new({
        duration = transitionConfig.duration,
        start = { [property] = currentValue },
        final = { [property] = value },
        easing = transitionConfig.easing,
        onComplete = transitionConfig.onComplete,
      })

      anim:apply(self)
    else
      self[property] = value
    end
  else
    self[property] = value
  end
end

-- ====================
-- State Persistence
-- ====================

--- Save all element state for immediate mode persistence
--- Collects state from all sub-modules and returns consolidated state
---@return ElementStateData state Complete state snapshot
function Element:saveState()
  local state = {}
  if self._eventHandler then
    state.eventHandler = self._eventHandler:getState()
  end
  if self._textEditor then
    state.textEditor = self._textEditor:getState()
  end
  if self._scrollManager then
    state.scrollManager = self._scrollManager:getState()
  end
  if self.backdropBlur or self.contentBlur then
    state.blur = {
      _blurX = self.x,
      _blurY = self.y,
      _blurWidth = self._borderBoxWidth or (self.width + self.padding.left + self.padding.right),
      _blurHeight = self._borderBoxHeight or (self.height + self.padding.top + self.padding.bottom),
    }

    if self.backdropBlur then
      state.blur._backdropBlurRadius = self.backdropBlur.radius
      state.blur._backdropBlurQuality = self.backdropBlur.quality or 5
    end

    if self.contentBlur then
      state.blur._contentBlurRadius = self.contentBlur.radius
      state.blur._contentBlurQuality = self.contentBlur.quality or 5
    end
  end

  return state
end

--- Restore all element state from StateManager
--- Distributes state to all sub-modules
---@param state ElementStateData State to restore
function Element:restoreState(state)
  if not state then
    return
  end

  -- Restore EventHandler state (if exists)
  if self._eventHandler and state.eventHandler then
    self._eventHandler:setState(state.eventHandler)
  end

  -- Restore TextEditor state (if exists)
  if self._textEditor and state.textEditor then
    self._textEditor:setState(state.textEditor, self)
    -- Sync TextEditor's focus state to Element for theme management
    self._focused = self._textEditor._focused
    self._cursorPosition = self._textEditor._cursorPosition
    self._selectionStart = self._textEditor._selectionStart
    self._selectionEnd = self._textEditor._selectionEnd
    self._textBuffer = self._textEditor._textBuffer
  end

  -- Restore ScrollManager state (if exists)
  if self._scrollManager and state.scrollManager then
    self._scrollManager:setState(state.scrollManager)
  end

  -- Note: Blur cache data is used for invalidation, not restoration
end

--- Check if blur cache should be invalidated based on state changes
---@param oldState ElementStateData? Previous state
---@param newState ElementStateData Current state
---@return boolean shouldInvalidate True if blur cache should be cleared
function Element:shouldInvalidateBlurCache(oldState, newState)
  if not oldState or not oldState.blur or not newState.blur then
    return false
  end

  local old = oldState.blur
  local new = newState.blur

  -- Check if any blur-related property changed
  return old._blurX ~= new._blurX
    or old._blurY ~= new._blurY
    or old._blurWidth ~= new._blurWidth
    or old._blurHeight ~= new._blurHeight
    or old._backdropBlurRadius ~= new._backdropBlurRadius
    or old._backdropBlurQuality ~= new._backdropBlurQuality
    or old._contentBlurRadius ~= new._contentBlurRadius
    or old._contentBlurQuality ~= new._contentBlurQuality
end

--- Cleanup method to break circular references (for immediate mode)
--- Note: Cleans internal module state but keeps structure for inspection
function Element:_cleanup()
  -- Clear event callbacks (may hold closures)
  self.onEvent = nil
  self.onFocus = nil
  self.onBlur = nil
  self.onTextInput = nil
  self.onTextChange = nil
  self.onEnter = nil
  self.onImageLoad = nil
  self.onImageError = nil
end

return Element
