Editor/SearchFilter.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Text.Json;
using Sandbox;
using Sandbox.Internal;
using System.Threading;
using System.Threading.Tasks;

namespace Editor;

/// <summary>
/// Search Filter – dockable inspector search panel that lets you scan and edit
/// properties across the selected GameObject and its children.
///
/// Live mode updates as you type and when selection changes.
/// Safe mode only updates when the user explicitly confirms a search.
/// </summary>
[Dock( "Editor", "Search Filter", "search" )]
public sealed class InspectorSearchTool : Widget
{
      private LineEdit searchBox;
      private IconButton clearButton;
      private IconButton progressIcon;
      private Label matchLabel;
      private Label safeBaseLabel;
      private Label scanStatusLabel;
      private Label scanTimeLabel;
      private Widget listContainer;
      private Label manualModeHintLabel;
      private Label selectedPrefixLabel;
      private Label selectedNameLabel;
      private Label selectedCountsLabel;
      private Label typeBoolLabel;
      private Label typeFloatLabel;
      private Label typeStringLabel;
      private Label typeEnumLabel;
      private Label typeComplexLabel;
      private Label typeErrorsLabel;
      // Top progress visuals removed (legacy) — keep references null and unused.
      private Widget noResultsContainer;
      private Label noResultsLabel;

      // When true, deselecting the current object clears the panel and shows the
      // "no selection" hint. This can be turned off from the cog menu.
      private bool clearOnDeselect = false;

      // When true, Safe mode keeps existing results when you switch selection;
      // when false, selecting a new object shows only the selection hint.
      private bool keepResultsOnSelectionChange = true;

      // When true, Safe mode will still auto-populate the list when selection changes
      // (but keeps manual search behaviour). When false, selection does not rebuild.
      private bool autoPopulateOnSelectionInSafe;

      // When true, component properties are only reflected and materialised when
      // the user explicitly loads a component (lazy mode). When false, all
      // properties are built eagerly on each rebuild.
      private bool lazyLoadProperties;

      // When true, nested/hidden boolean members discovered inside a small set of
      // known option structs (for example RenderOptions) are exposed as separate
      // rows (e.g. Light Color.IsSdr). When false, those flattened helpers are
      // omitted to keep results tidier and faster.
      private bool showHiddenNestedBools = true;
      private const int MaxSerializedChildRows = 12;
      private const int MaxSerializedChildDepth = 1;

      // When true, property type names (e.g. SoundEvent, Soundscape) are included
      // in the "name" search scope so searching for "sound" will match these even
      // if the property is just called "Event".
      private bool includeTypeNamesInNameSearch = true;

      // When true, simple list-like containers (e.g. hide body groups lists) are
      // flattened into individual rows so their entries are searchable.
      private bool flattenLists = true;

      // Experimental deep nested-bool mode (all complex types) has been disabled
      // for now – it was too slow in large scenes. Left here commented for
      // potential future use.
      // private bool showDeepNestedBools;

      // When true, Modifinder writes simple timing/debug information to the log
      // whenever rows are rebuilt or filters are applied.
      private bool debugLogging;

      // When debug logging is enabled, we keep track of which complex container
      // types we have inspected for nested options so we can log them once per
      // rebuild without flooding the console.
      private readonly HashSet<string> debugNestedTypeNames = new();

      // When true, only matching property rows are of interest and hierarchy
      // styling is suppressed (folders/components are not emphasised). This is
      // a compact "field only" mode for power searching.
      private bool leanMode = false;

      // Guard to prevent the clear (X) button from re-triggering live search logic
      // when we are only interested in clearing the text field.
      private bool suppressSearchChanged;

      private GameObject target;

      // Filters
      private bool includeChildren = true;
      private bool showBools = true;
      private bool showNumeric = true;
      private bool showText = true;
      private bool showEnums = true;
      private bool showComplex = true;
      private bool filterErrorsOnly;

      private enum MatchTargetMode
      {
              ComponentsOnly,
              NamesOnly,
              ComponentsAndNames
      }

      private MatchTargetMode matchTargetMode = MatchTargetMode.NamesOnly;

      private enum ValueSearchMode
      {
              NamesOnly,
              ValuesOnly,
              NamesAndValues
      }

      private ValueSearchMode valueSearchMode = ValueSearchMode.NamesAndValues;

      private enum SearchTriggerMode
      {
              Live,
              Safe
      }

      private SearchTriggerMode searchTriggerMode = SearchTriggerMode.Safe;

      private enum ValueFilterMode
      {
              All,
              SetOnly,
              UnsetOnly
      }

      private ValueFilterMode valueFilterMode = ValueFilterMode.All;

      private readonly List<PropertyRow> rows = new();
      private readonly List<RowModel> rowModels = new();
      private readonly Dictionary<int, RowModel> rowModelLookup = new();
      private readonly Dictionary<int, PropertyRow> rowLookup = new();
      private readonly List<ComponentGroup> componentGroups = new();
      private readonly List<GameObjectGroup> gameObjectGroups = new();
      private readonly ComponentBuildQueue componentBuildQueue = new();
      private readonly PropertyMetadataCache propertyMetadataCache = new();
      private CancellationTokenSource rebuildCancellation;
      private long rebuildSequence;
      private readonly Dictionary<SelectionSignature, ScanJobResult> scanCache = new();
      private readonly Queue<SelectionSignature> scanCacheOrder = new();
      private const int MaxScanCacheEntries = 4;
      private readonly Dictionary<string, RowFilterState> filterSnapshotCache = new();
      private readonly Queue<string> filterSnapshotOrder = new();
      private const int MaxFilterSnapshots = 6;
      private readonly Dictionary<string, HashSet<int>> nameIndex = new(StringComparer.OrdinalIgnoreCase);
      private readonly Dictionary<string, HashSet<int>> typeIndex = new(StringComparer.OrdinalIgnoreCase);
      private readonly Dictionary<string, HashSet<int>> valueIndex = new(StringComparer.OrdinalIgnoreCase);
      private readonly Dictionary<string, HashSet<int>> componentIndex = new(StringComparer.OrdinalIgnoreCase);
      private readonly Dictionary<string, HashSet<int>> objectIndex = new(StringComparer.OrdinalIgnoreCase);
      private bool rowsDirty;
      private int nextRowId;
      private string lastSearch = string.Empty;
      private string pendingFilterText = string.Empty;
      private bool filterDebouncePending;
      private readonly Stopwatch filterDebounceStopwatch = new();
      private const int FilterDebounceMs = 180;
      private const int SafeBaseFilterDebounceMs = 320;
      private bool filterInProgress;
      private const int HeavyRowCountThreshold = 1200;
      private bool safeBaseActive;
      private string safeBaseSearchText;
      private readonly HashSet<int> safeBaseRowIds = new();
      private string[] activeIncludeTerms = Array.Empty<string>();
      private string[] activeExcludeTerms = Array.Empty<string>();
      private QueryFilters activeQueryFilters = new(
              Array.Empty<string>(),
              Array.Empty<string>(),
              Array.Empty<string>(),
              Array.Empty<string>(),
              Array.Empty<string>() );
      private bool pendingRebuild;
      private bool isDisposed;
      private int totalComponentCount;
      private int builtComponentCount;
      private float lastProgress01;
private bool gradualResults = false;
      private bool showHeaderStats = true;
      private bool showModeMessages = true;
      private bool shorthandLabels;
      private bool safeQueryDirty;
      private CancellationTokenSource filterCancellation;
      private long filterSequence;

      private string BuildSearchFilterSummary()
      {
              string scope = includeChildren ? "scope:children" : "scope:selected";

              string match = matchTargetMode switch
              {
                      MatchTargetMode.ComponentsOnly => "match:comps",
                      MatchTargetMode.NamesOnly => "match:names",
                      MatchTargetMode.ComponentsAndNames => "match:comps+names",
                      _ => "match:comps+names"
              };

              string searchScope = valueSearchMode switch
              {
                      ValueSearchMode.NamesOnly => "search:names",
                      ValueSearchMode.ValuesOnly => "search:values",
                      ValueSearchMode.NamesAndValues => "search:names+values",
                      _ => "search:names+values"
              };

              string values = valueFilterMode switch
              {
                      ValueFilterMode.All => "values:all",
                      ValueFilterMode.SetOnly => "values:set",
                      ValueFilterMode.UnsetOnly => "values:unset",
                      _ => "values:all"
              };

              string types = "";
              if ( showBools ) types += "bool,";
              if ( showNumeric ) types += "float,";
              if ( showText ) types += "string,";
              if ( showEnums ) types += "enum,";
              if ( showComplex ) types += "cx,";
              if ( filterErrorsOnly ) types += "errors,";

              var allTypesOn = showBools && showNumeric && showText && showEnums && showComplex;
              if ( allTypesOn )
              {
                      types = filterErrorsOnly ? "all,error," : "all,";
              }

              if ( types.EndsWith( "," ) ) types = types.Substring( 0, types.Length - 1 );

              return $"{scope}, {match}, {searchScope}, {values}, types:{types}";
      }

      private static class SearchTokens
      {
              private static readonly char[] SplitChars = { ' ', '\t', '\r', '\n', '.', ',', ';', ':', '/', '\\', '-', '_', '+', '`', '(', ')', '[', ']', '{', '}', '<', '>', '@', '#', '!', '?', '"', '\'' };

              public static string[] Tokenize( string value )
              {
                      if ( string.IsNullOrEmpty( value ) )
                              return Array.Empty<string>();

                      var tokens = new List<string>(16);
                      var split = value.Split( SplitChars, StringSplitOptions.RemoveEmptyEntries );
                      foreach ( var s in split )
                      {
                              var lowered = s.ToLowerInvariant();
                              if ( !string.IsNullOrEmpty( lowered ) )
                                      tokens.Add( lowered );
                      }

                      return tokens.Count == 0 ? Array.Empty<string>() : tokens.ToArray();
              }

              public static bool Contains( string[] tokens, string term )
              {
                      if ( tokens == null || tokens.Length == 0 || string.IsNullOrEmpty( term ) )
                              return false;

                      var t = term.ToLowerInvariant();
                      return tokens.Any( tok => FuzzyContains( tok, t ) );
              }

              public static bool FuzzyContainsRaw( string haystack, string term )
              {
                      if ( string.IsNullOrEmpty( haystack ) || string.IsNullOrEmpty( term ) )
                              return false;
                      return FuzzyContains( haystack, term );
              }

              // Match terms without getting tripped by punctuation (see sbox-public NodeQuery).
              private static bool FuzzyContains( string str, string value )
              {
                      return CultureInfo.InvariantCulture.CompareInfo.IndexOf(
                              str, value, CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols ) >= 0;
              }
      }

      private static string HighlightMatches( string text, string[] terms )
      {
              if ( string.IsNullOrEmpty( text ) || terms == null || terms.Length == 0 )
                      return text ?? string.Empty;

              var matches = new List<(int Start, int Length)>();
              foreach ( var term in terms.OrderByDescending( t => t?.Length ?? 0 ) )
              {
                      if ( string.IsNullOrEmpty( term ) )
                              continue;

                      var pattern = Regex.Escape( term );
                      foreach ( Match match in Regex.Matches( text, pattern, RegexOptions.IgnoreCase ) )
                      {
                              if ( match.Length == 0 )
                                      continue;

                              var overlaps = matches.Any( m =>
                                      match.Index < m.Start + m.Length && m.Start < match.Index + match.Length );
                              if ( overlaps )
                                      continue;

                              matches.Add( (match.Index, match.Length) );
                      }
              }

              if ( matches.Count == 0 )
                      return text;

              matches.Sort( (a, b) => a.Start.CompareTo( b.Start ) );
              var sb = new StringBuilder( text.Length + matches.Count * 8 );
              var lastIndex = 0;
              foreach ( var match in matches )
              {
                      if ( match.Start > lastIndex )
                      {
                              sb.Append( text.Substring( lastIndex, match.Start - lastIndex ) );
                      }

                      var matchedText = text.Substring( match.Start, match.Length );
                      sb.Append( "[" );
                      sb.Append( matchedText );
                      sb.Append( "]" );
                      lastIndex = match.Start + match.Length;
              }

              if ( lastIndex < text.Length )
              {
                      sb.Append( text.Substring( lastIndex ) );
              }

              return sb.ToString();
      }

      private enum FilterModifier
      {
              None,
              Not
      }

      private readonly record struct FilterPart( FilterModifier Modifier, string Value )
      {
              private static readonly Regex Pattern =
                      new( @"(?<modifier>-?)(?<value>(?:[^\s.,;\\\/""_-]|\\[\\""]|""(?:(?:[^\\""]|\\[\\""])+)"")+)" );

              private static FilterModifier ParseModifier( string value )
              {
                      return value switch
                      {
                              "-" => FilterModifier.Not,
                              _ => FilterModifier.None
                      };
              }

              private static readonly Regex EscapedChars = new( @"\\[\\""]|""" );

              private static string FormatValue( string value )
              {
                      return EscapedChars.Replace( value, x => x.Value switch
                      {
                              "\\\\" => "\\",
                              "\\\"" => "\"",
                              _ => ""
                      } ).Trim();
              }

              public static IReadOnlyList<FilterPart> Parse( string value )
              {
                      if ( string.IsNullOrEmpty( value ) )
                      {
                              return Array.Empty<FilterPart>();
                      }

                      return Pattern.Matches( value )
                              .Select( x => new FilterPart(
                                      ParseModifier( x.Groups["modifier"].Value ),
                                      FormatValue( x.Groups["value"].Value ) ) )
                              .Where( x => !string.IsNullOrEmpty( x.Value ) )
                              .ToArray();
              }
      }

      private readonly record struct QueryFilters(
              string[] TypeTerms,
              string[] ComponentTerms,
              string[] ObjectTerms,
              string[] NameTerms,
              string[] ValueTerms );

      private static readonly Regex KeyValuePattern = new( @"\b(?<key>\w+):(?<value>\S+)" );

      private static void ParseSearchTerms( string rawText, out string[] includeTerms, out string[] excludeTerms, out QueryFilters filters )
      {
              rawText ??= string.Empty;
              var typeTerms = new List<string>();
              var componentTerms = new List<string>();
              var objectTerms = new List<string>();
              var nameTerms = new List<string>();
              var valueTerms = new List<string>();

              var kvMatches = KeyValuePattern.Matches( rawText );
              if ( kvMatches.Count > 0 )
              {
                      foreach ( Match match in kvMatches )
                      {
                              var key = match.Groups["key"].Value.ToLowerInvariant();
                              var value = match.Groups["value"].Value.Trim().Trim( '"' ).ToLowerInvariant();
                              if ( string.IsNullOrEmpty( value ) )
                                      continue;

                              switch ( key )
                              {
                                      case "type":
                                              typeTerms.Add( value );
                                              break;
                                      case "component":
                                      case "comp":
                                              componentTerms.Add( value );
                                              break;
                                      case "object":
                                      case "obj":
                                              objectTerms.Add( value );
                                              break;
                                      case "name":
                                              nameTerms.Add( value );
                                              break;
                                      case "value":
                                              valueTerms.Add( value );
                                              break;
                              }
                      }

                      rawText = KeyValuePattern.Replace( rawText, string.Empty ).Trim();
              }

              var parts = FilterPart.Parse( rawText );
              if ( parts.Count == 0 )
              {
                      includeTerms = Array.Empty<string>();
                      excludeTerms = Array.Empty<string>();
                      filters = new QueryFilters(
                              typeTerms.ToArray(),
                              componentTerms.ToArray(),
                              objectTerms.ToArray(),
                              nameTerms.ToArray(),
                              valueTerms.ToArray() );
                      return;
              }

              var include = new List<string>( parts.Count );
              var exclude = new List<string>();
              foreach ( var part in parts )
              {
                      var term = part.Value?.Trim();
                      if ( string.IsNullOrEmpty( term ) )
                              continue;

                      term = term.ToLowerInvariant();
                      if ( part.Modifier == FilterModifier.Not )
                              exclude.Add( term );
                      else
                              include.Add( term );
              }

              includeTerms = include.Count > 0 ? include.ToArray() : Array.Empty<string>();
              excludeTerms = exclude.Count > 0 ? exclude.ToArray() : Array.Empty<string>();
              filters = new QueryFilters(
                      typeTerms.ToArray(),
                      componentTerms.ToArray(),
                      objectTerms.ToArray(),
                      nameTerms.ToArray(),
                      valueTerms.ToArray() );
      }

      private static readonly Lazy<Dictionary<string, string[]>> ApiTypeTokenMap =
              new( BuildApiTypeTokenMap );

      private static Dictionary<string, string[]> BuildApiTypeTokenMap()
      {
              try
              {
                      var path = Path.Combine( Directory.GetCurrentDirectory(), "sbox_api.json" );
                      if ( !File.Exists( path ) )
                              return new Dictionary<string, string[]>( StringComparer.OrdinalIgnoreCase );

                      using var stream = File.OpenRead( path );
                      using var doc = JsonDocument.Parse( stream );
                      if ( !doc.RootElement.TryGetProperty( "Types", out var types ) || types.ValueKind != JsonValueKind.Array )
                              return new Dictionary<string, string[]>( StringComparer.OrdinalIgnoreCase );

                      var map = new Dictionary<string, string[]>( StringComparer.OrdinalIgnoreCase );
                      foreach ( var entry in types.EnumerateArray() )
                      {
                              if ( !entry.TryGetProperty( "Name", out var nameProp ) )
                                      continue;
                              var name = nameProp.GetString();
                              if ( string.IsNullOrEmpty( name ) )
                                      continue;

                              if ( !map.ContainsKey( name ) )
                                      map[name] = SplitPascalCaseTokens( name );

                              if ( entry.TryGetProperty( "FullName", out var fullProp ) )
                              {
                                      var fullName = fullProp.GetString();
                                      if ( !string.IsNullOrEmpty( fullName ) && !map.ContainsKey( fullName ) )
                                              map[fullName] = SplitPascalCaseTokens( fullName );
                              }
                      }

                      return map;
              }
              catch
              {
                      return new Dictionary<string, string[]>( StringComparer.OrdinalIgnoreCase );
              }
      }

      private static string[] SplitPascalCaseTokens( string value )
      {
              if ( string.IsNullOrEmpty( value ) )
                      return Array.Empty<string>();

              var matches = Regex.Matches( value, @"([A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|[0-9]+)" );
              if ( matches.Count == 0 )
                      return Array.Empty<string>();

              var list = new List<string>( matches.Count + 1 );
              foreach ( Match match in matches )
              {
                      var token = match.Value.ToLowerInvariant();
                      if ( !string.IsNullOrEmpty( token ) )
                              list.Add( token );
              }
              var full = value.ToLowerInvariant();
              if ( !string.IsNullOrEmpty( full ) )
                      list.Add( full );

              return list.Distinct().ToArray();
      }

      private static string[] MergeTokens( string[] baseTokens, string[] extraTokens )
      {
              if ( (baseTokens == null || baseTokens.Length == 0) && (extraTokens == null || extraTokens.Length == 0) )
                      return Array.Empty<string>();
              if ( baseTokens == null || baseTokens.Length == 0 )
                      return extraTokens ?? Array.Empty<string>();
              if ( extraTokens == null || extraTokens.Length == 0 )
                      return baseTokens;

              var set = new HashSet<string>( baseTokens );
              foreach ( var token in extraTokens )
                      set.Add( token );
              return set.ToArray();
      }

      private void UpdateSearchPlaceholder()
      {
              if ( searchBox == null )
                      return;

              var summary = BuildSearchFilterSummary();
              if ( safeBaseActive && string.IsNullOrEmpty( searchBox.Text ) )
              {
                      searchBox.PlaceholderText = $"Find in results… ({summary})";
              }
              else
              {
                      searchBox.PlaceholderText = $"Search… ({summary})";
              }
      }

      private string BuildParameterAccessorSummary( SkinnedModelRenderer.ParameterAccessor accessor, AnimationGraph graph, out List<(string Name, string Value)> entries )
      {
              entries = new List<(string Name, string Value)>();
              if ( accessor == null )
                      return string.Empty;

              if ( graph == null || !graph.IsValid || graph.ParamCount <= 0 )
                      return string.Empty;

              for ( int i = 0; i < graph.ParamCount; i++ )
              {
                      var name = graph.GetParameterName( i );
                      if ( string.IsNullOrEmpty( name ) )
                              continue;

                      var type = graph.GetParameterType( i );
                      string valueText;

                      if ( type == typeof( bool ) )
                      {
                              valueText = accessor.GetBool( name ).ToString();
                      }
                      else if ( type == typeof( float ) )
                      {
                              valueText = accessor.GetFloat( name ).ToString();
                      }
                      else if ( type == typeof( int ) )
                      {
                              valueText = accessor.GetInt( name ).ToString();
                      }
                      else if ( type == typeof( Vector3 ) )
                      {
                              valueText = accessor.GetVector( name ).ToString();
                      }
                      else if ( type == typeof( Rotation ) )
                      {
                              valueText = accessor.GetRotation( name ).ToString();
                      }
                      else
                      {
                              // Fallback for enums/other types – we can't read the raw enum
                              // value here, but we can at least surface whether it is set.
                              valueText = accessor.Contains( name ) ? "set" : "unset";
                      }

                      entries.Add( (name, valueText) );
              }

              return entries.Count > 0 ? string.Join( "; ", entries.Select( e => $"{e.Name}={e.Value}" ) ) : string.Empty;
      }

      /// <summary>
      /// Build the dock UI: selection summary, search controls and scrollable results.
      /// </summary>
      public InspectorSearchTool( Widget parent ) : base( parent )
      {
              MinimumHeight = 200;

              var rootLayout = Layout = Layout.Column();
              rootLayout.Margin = 4;
              rootLayout.Spacing = 4;

              // Selection summary row – always visible, even when nothing is selected.
              var selectionRow = rootLayout.AddRow();
              selectionRow.Spacing = 4;

              selectedPrefixLabel = new Label( "" )
              {
                      Alignment = TextFlag.LeftCenter
              };
              selectedPrefixLabel.SetStyles( "color: rgba(255,255,255,0.55);" );
              selectionRow.Add( selectedPrefixLabel, 0 );

              selectedNameLabel = new Label( "(none)" )
              {
                      Alignment = TextFlag.LeftCenter
              };
              selectedNameLabel.SetStyles( "font-weight: 700; color: rgba(255,255,255,0.85);" );
              selectedNameLabel.ToolTip = "Current selection name.";
              selectionRow.Add( selectedNameLabel, 0 );

              // Text status for the current scan/index – reports how many
              // fields/components have been indexed so far, next to the object name.
              scanStatusLabel = new Label( "" )
              {
                      Alignment = TextFlag.LeftCenter
              };
              scanStatusLabel.SetStyles( "color: rgba(255,255,255,0.45); font-size: 11px; margin-left: 6px;" );
              scanStatusLabel.ToolTip = "Scan/filter progress (built/total components or rows).";
              selectionRow.Add( scanStatusLabel, 0 );

              scanTimeLabel = new Label( "" )
              {
                      Alignment = TextFlag.LeftCenter
              };
              scanTimeLabel.SetStyles( "color: rgba(255,255,255,0.55); font-size: 11px; margin-left: 6px;" );
              scanTimeLabel.ToolTip = "Time taken for the last filter (ms).";
              selectionRow.Add( scanTimeLabel, 0 );

              selectionRow.AddStretchCell();

              selectedCountsLabel = new Label( "0 descendants · 0 components" )
              {
                      Alignment = TextFlag.RightCenter
              };
              selectedCountsLabel.SetStyles( "color: rgba(255,255,255,0.55);" );
              selectedCountsLabel.ToolTip = "Descendants and component counts for the current selection.";
              selectionRow.Add( selectedCountsLabel, 0 );

              // Search + filters row
              var searchRow = rootLayout.AddRow();
              searchRow.Spacing = 4;

              searchBox = new LineEdit( this )
              {
                      PlaceholderText = "Search…",
                      FixedHeight = Theme.RowHeight
              };
              searchBox.ToolTip = "Search: filter component properties by name and/or value.\nSafe mode: press Enter to apply; leave empty and press Enter to list all fields.";

              // Search trigger mode: live vs manual
              IconButton searchTriggerButton = null;
              void UpdateSearchTriggerButton()
              {
                      switch ( searchTriggerMode )
                      {
                              case SearchTriggerMode.Live:
                                      searchTriggerButton.Icon = "flash_on";
                                      searchTriggerButton.ToolTip = "Mode: Live – filters instantly as you type and when selection changes.\nClick to switch to Safe mode.";
                                      break;
                              case SearchTriggerMode.Safe:
                                      searchTriggerButton.Icon = "security";
                                      searchTriggerButton.ToolTip = "Mode: Safe – updates only when you press Enter or click refresh.\nClick to switch to Live mode.";
                                      break;
                      }
              }

              searchTriggerButton = new IconButton( "flash_on", () =>
              {
                      searchTriggerMode = searchTriggerMode == SearchTriggerMode.Live
                              ? SearchTriggerMode.Safe
                              : SearchTriggerMode.Live;
                      UpdateSearchTriggerButton();
                      UpdateSearchPlaceholder();

                      if ( searchTriggerMode == SearchTriggerMode.Live && pendingRebuild )
                      {
                              RebuildRows();
                      }
                      UpdateManualModeHint();
              } )
              {
                      FixedHeight = Theme.RowHeight,
                      FixedWidth = Theme.RowHeight
              };
              UpdateSearchTriggerButton();
              searchRow.Add( searchTriggerButton, 0 );

              safeBaseLabel = new Label( "" )
              {
                      Alignment = TextFlag.LeftCenter,
                      Visible = false
              };
              safeBaseLabel.SetStyles( "color: rgba(200,220,255,0.9); font-size: 11px; margin-left: 4px; padding: 2px 6px; background-color: rgba(40,60,110,0.7); border-radius: 6px;" );
              searchRow.Add( safeBaseLabel, 0 );

              searchRow.Add( searchBox, 1 );

              // Initial placeholder reflects default filter settings
              UpdateSearchPlaceholder();

              // In live mode, filter as the user types; in safe mode, filter only on Enter
              // (via ReturnPressed) or the refresh button, so focus changes (e.g. clicking
              // the console) never trigger a search.
              searchBox.TextEdited += OnSearchChanged;
              searchBox.ReturnPressed += () =>
              {
                      if ( searchTriggerMode == SearchTriggerMode.Safe )
                      {
                              safeQueryDirty = false;
                              ApplyFilterSafe( searchBox.Text );
                      }
              };

              clearButton = new IconButton( "close", ClearSearch )
              {
                      ToolTip = "Action: clear or interrupt search.\nSafe mode: first click clears text, second click clears results.",
                      FixedHeight = Theme.RowHeight,
                      FixedWidth = Theme.RowHeight
              };
              searchRow.Add( clearButton, 0 );

              matchLabel = new Label( "0" ) { Alignment = TextFlag.LeftCenter };
              matchLabel.SetStyles( "color: rgba(255,255,255,0.4);" );
              matchLabel.ToolTip = "Visible matches for the current search.";
              searchRow.Add( matchLabel, 0 );

              // We no longer show a dedicated cancel button; the clear (X) button
              // doubles as the interrupt. Keep a null spinner reference so
              // UpdateProgressIcon can safely no-op when asked to update it.
              progressIcon = null;

              // Scope toggle: Selected vs Children
              IconButton scopeButton = null;
              scopeButton = new IconButton( includeChildren ? "account_tree" : "person", () =>
              {
                      includeChildren = !includeChildren;
                      scopeButton.Icon = includeChildren ? "account_tree" : "person";
                      scopeButton.ToolTip = includeChildren
                              ? "Scope: selection + children – include the selected object and its descendants."
                              : "Scope: selection only – search just the selected object.";
                      UpdateSelectionSummary();
                      if ( searchTriggerMode == SearchTriggerMode.Live )
                      {
                              RebuildRows();
                      }
                      else
                      {
                              pendingRebuild = true;
                      }
                      UpdateSearchPlaceholder();
                      UpdateManualModeHint();
              } )
              {
                      ToolTip = includeChildren
                              ? "Scope: selection + children – include the selected object and its descendants."
                              : "Scope: selection only – search just the selected object.",
                      FixedHeight = Theme.RowHeight,
                      FixedWidth = Theme.RowHeight
              };
              searchRow.Add( scopeButton, 0 );

              // Match target toggle: components vs property names vs both (3-way)
              IconButton matchButton = null;
              void UpdateMatchButton()
              {
                      switch ( matchTargetMode )
                      {
                              case MatchTargetMode.ComponentsOnly:
                                      matchButton.Icon = "view_module";
                              matchButton.ToolTip = "Show properties in matching components.";
                              break;
                      case MatchTargetMode.NamesOnly:
                              matchButton.Icon = "title";
                              matchButton.ToolTip = "Scope: properties only – show properties that match.";
                              break;
                      case MatchTargetMode.ComponentsAndNames:
                              matchButton.Icon = "public";
                              matchButton.ToolTip = "Show components and properties that match.";
                              break;
                      }
              }

              matchButton = new IconButton( "public", () =>
              {
                      matchTargetMode = matchTargetMode switch
                      {
                              MatchTargetMode.ComponentsAndNames => MatchTargetMode.ComponentsOnly,
                              MatchTargetMode.ComponentsOnly => MatchTargetMode.NamesOnly,
                              MatchTargetMode.NamesOnly => MatchTargetMode.ComponentsAndNames,
                              _ => MatchTargetMode.ComponentsAndNames
                      };

                      UpdateMatchButton();
                      UpdateSearchPlaceholder();
                      if ( searchTriggerMode == SearchTriggerMode.Live )
                      {
                              ApplyFilter( lastSearch );
                      }
                      else
                      {
                              UpdateManualModeHint();
                      }
              } )
              {
                      FixedHeight = Theme.RowHeight,
                      FixedWidth = Theme.RowHeight
              };
              UpdateMatchButton();
              searchRow.Add( matchButton, 0 );

              // Value/name search mode toggle: Names, Values, Names & Values (3-way)
              IconButton valueTextButton = null;
              void UpdateValueSearchButton()
              {
                      switch ( valueSearchMode )
                      {
                              case ValueSearchMode.NamesOnly:
                                      valueTextButton.Icon = "title";
                                      valueTextButton.ToolTip = "Scope: names only – look only at property names.";
                                      break;
                              case ValueSearchMode.ValuesOnly:
                                      valueTextButton.Icon = "tag";
                                      valueTextButton.ToolTip = "Scope: values only – look only at property values.";
                                      break;
                              case ValueSearchMode.NamesAndValues:
                                      valueTextButton.Icon = "public";
                                      valueTextButton.ToolTip = "Scope: names + values – look at both property names and values.";
                                      break;
                      }
              }

              valueTextButton = new IconButton( "title", () =>
              {
                      valueSearchMode = valueSearchMode switch
                      {
                              ValueSearchMode.NamesAndValues => ValueSearchMode.NamesOnly,
                              ValueSearchMode.NamesOnly => ValueSearchMode.ValuesOnly,
                              ValueSearchMode.ValuesOnly => ValueSearchMode.NamesAndValues,
                              _ => ValueSearchMode.NamesAndValues
                      };

                      UpdateValueSearchButton();
                      UpdateSearchPlaceholder();
                      if ( searchTriggerMode == SearchTriggerMode.Live )
                      {
                              ApplyFilter( lastSearch );
                      }
                      else
                      {
                              UpdateManualModeHint();
                      }
              } )
              {
                      FixedHeight = Theme.RowHeight,
                      FixedWidth = Theme.RowHeight
              };
              UpdateValueSearchButton();
              searchRow.Add( valueTextButton, 0 );

              // Value filter toggle: All / Set / Unset (single icon button cycling three modes)
              IconButton valueFilterButton = null;
              void UpdateValueFilterButton()
              {
                      switch ( valueFilterMode )
                      {
                              case ValueFilterMode.All:
                                      valueFilterButton.Icon = "filter_alt_off";
                                      valueFilterButton.ToolTip = "Value state: all – show both set and unset values.";
                                      break;
                              case ValueFilterMode.SetOnly:
                                      valueFilterButton.Icon = "filter_alt";
                                      valueFilterButton.ToolTip = "Value state: set only – show only values that currently have a value.";
                                      break;
                              case ValueFilterMode.UnsetOnly:
                                      valueFilterButton.Icon = "filter_list";
                                      valueFilterButton.ToolTip = "Value state: unset only – show only values that are currently empty.";
                                      break;
                      }
              }

              valueFilterButton = new IconButton( "filter_alt_off", () =>
              {
                      valueFilterMode = valueFilterMode switch
                      {
                              ValueFilterMode.All => ValueFilterMode.SetOnly,
                              ValueFilterMode.SetOnly => ValueFilterMode.UnsetOnly,
                              ValueFilterMode.UnsetOnly => ValueFilterMode.All,
                              _ => ValueFilterMode.All
                      };

                      UpdateValueFilterButton();
                      UpdateSearchPlaceholder();
                      if ( searchTriggerMode == SearchTriggerMode.Live )
                      {
                              ApplyFilter( lastSearch );
                      }
                      else
                      {
                              UpdateManualModeHint();
                      }
              } )
              {
                      FixedHeight = Theme.RowHeight,
                      FixedWidth = Theme.RowHeight
              };
              UpdateValueFilterButton();
              searchRow.Add( valueFilterButton, 0 );

              var selectionCog = new IconButton( "settings", OpenOptionsMenu )
              {
                      ToolTip = "Options: selection behaviour, Safe mode, loading and search helpers.",
                      FixedWidth = Theme.RowHeight,
                      FixedHeight = Theme.RowHeight
              };
              searchRow.Add( selectionCog, 0 );

              // Type and extra filter toggles – shown on a second row as [checkbox icon] + colored label
              var filterRow = rootLayout.AddRow();
              filterRow.Spacing = 4;

              IconButton AddFilterToggle( string iconOn, Func<bool> getter, Action<bool> setter, string tooltip )
              {
                      IconButton btn = null;
                      btn = new IconButton( getter() ? iconOn : "check_box_outline_blank", () =>
                      {
                              var newVal = !getter();
                              setter( newVal );
                              btn.Icon = newVal ? iconOn : "check_box_outline_blank";
                              UpdateSearchPlaceholder();
                              if ( searchTriggerMode == SearchTriggerMode.Live )
                              {
                                      ApplyFilter( lastSearch );
                              }
                              else
                              {
                                      UpdateManualModeHint();
                              }
                      } )
                      {
                              ToolTip = tooltip,
                              FixedHeight = Theme.RowHeight,
                              FixedWidth = Theme.RowHeight
                      };
                      return btn;
              }

              Label AddFilterLabel( string text, string colorCss, out Label store )
              {
                      var label = new Label( text )
                      {
                              Alignment = TextFlag.LeftCenter
                      };
                      label.SetStyles( $"color: {colorCss};" );
                      store = label;
                      return label;
              }

              // Bool (orange), Float (lime), String (pink), Enum (teal), Complex (yellow)
              filterRow.Add( AddFilterToggle( "check_box", () => showBools, v => showBools = v, "Type filter: bool – include true/false flags." ), 0 );
              filterRow.Add( AddFilterLabel( "Bool", "rgba(255,190,120,0.95)", out typeBoolLabel ), 0 );

              filterRow.Add( AddFilterToggle( "check_box", () => showNumeric, v => showNumeric = v, "Type filter: numeric – include int/float-like values." ), 0 );
              filterRow.Add( AddFilterLabel( "Float", "rgba(120,255,120,0.9)", out typeFloatLabel ), 0 );

              filterRow.Add( AddFilterToggle( "check_box", () => showText, v => showText = v, "Type filter: text – include string fields (names, tags)." ), 0 );
              filterRow.Add( AddFilterLabel( "String", "rgba(255,160,220,0.9)", out typeStringLabel ), 0 );

              filterRow.Add( AddFilterToggle( "check_box", () => showEnums, v => showEnums = v, "Type filter: enum – include enum/flags values." ), 0 );
              filterRow.Add( AddFilterLabel( "Enum", "rgba(120,240,240,0.9)", out typeEnumLabel ), 0 );

              filterRow.Add( AddFilterToggle( "check_box", () => showComplex, v => showComplex = v, "Type filter: complex – include vectors, colours, transforms." ), 0 );
              filterRow.Add( AddFilterLabel( "Complex", "rgba(255,235,140,0.9)", out typeComplexLabel ), 0 );

              // Extra filter: errors-only (label in red)
              filterRow.Add( AddFilterToggle( "check_box", () => filterErrorsOnly, v => filterErrorsOnly = v,"Extra filter: errors only – show only fields with '<error>'." ), 0 );
              filterRow.Add( AddFilterLabel( "Errors", "rgba(255,120,120,0.95)", out typeErrorsLabel ), 0 );

              var safeRow = rootLayout.AddRow();
              safeRow.Spacing = 0;

              safeRow.AddStretchCell();
              manualModeHintLabel = new Label( this )
              {
                      Text = "Safe mode is on. Press Enter or click refresh to update results.",
                      Visible = false
              };
              manualModeHintLabel.SetStyles( "color: rgba(200,220,255,0.85); background-color: rgba(20,60,120,0.55); font-weight: 600; font-size: 12px; margin: 8px 0; padding: 8px 12px; border-radius: 6px; text-align:center; min-width: 260px;" );
              safeRow.Add( manualModeHintLabel, 0 );
              safeRow.AddStretchCell();

              // List area (scrollable)
              var scrollArea = new ScrollArea( this );
              scrollArea.Canvas = new Widget( scrollArea );
              scrollArea.Canvas.VerticalSizeMode = SizeMode.CanGrow;
              scrollArea.Canvas.Layout = Layout.Column();
              scrollArea.Canvas.Layout.Margin = 0;
              rootLayout.Add( scrollArea, 1 );

              listContainer = scrollArea.Canvas;

              // Initial build to show hint when nothing is selected
              UpdateSelectionSummary();
              RebuildRows( force: true );
              UpdateManualModeHint();
              ApplyHeaderStatsVisibility();
              ApplyShorthandLabels();
      }

      private void ApplyHeaderStatsVisibility()
      {
              var currentMargin = showHeaderStats ? 6 : 0;

              if ( selectedPrefixLabel != null && selectedPrefixLabel.IsValid() )
              {
                      selectedPrefixLabel.Visible = showHeaderStats;
              }

              if ( selectedNameLabel != null && selectedNameLabel.IsValid() )
              {
                      selectedNameLabel.Visible = showHeaderStats;
              }

              if ( selectedCountsLabel != null && selectedCountsLabel.IsValid() )
              {
                      selectedCountsLabel.Visible = showHeaderStats;
              }

              if ( scanStatusLabel != null && scanStatusLabel.IsValid() )
              {
                      scanStatusLabel.Visible = showHeaderStats;
              }

              if ( scanTimeLabel != null && scanTimeLabel.IsValid() )
              {
                      scanTimeLabel.Visible = showHeaderStats;
                      if ( !showHeaderStats )
                      {
                              scanTimeLabel.Text = string.Empty;
                      }

                      // When stats are hidden, trim extra gap on the timing label; when
                      // shown, give it a small breathing space.
                      scanTimeLabel.SetStyles( $"color: rgba(255,255,255,0.55); font-size: 11px; margin-left: {currentMargin}px;" );
              }

      }

      private void ApplyShorthandLabels()
      {
              // Selected prefix
              if ( selectedPrefixLabel != null && selectedPrefixLabel.IsValid() )
              {
                      selectedPrefixLabel.Text = shorthandLabels ? "" : "Selected:";
              }

              // Type filter labels
              if ( shorthandLabels )
              {
                      // Shorten only the ones that benefit most.
                      if ( typeBoolLabel != null && typeBoolLabel.IsValid() ) typeBoolLabel.Text = "B";
                      if ( typeFloatLabel != null && typeFloatLabel.IsValid() ) typeFloatLabel.Text = "F";
                      if ( typeStringLabel != null && typeStringLabel.IsValid() ) typeStringLabel.Text = "S";
                      if ( typeEnumLabel != null && typeEnumLabel.IsValid() ) typeEnumLabel.Text = "En";
                      if ( typeComplexLabel != null && typeComplexLabel.IsValid() ) typeComplexLabel.Text = "Cx";
                      if ( typeErrorsLabel != null && typeErrorsLabel.IsValid() ) typeErrorsLabel.Text = "E";
              }
              else
              {
                      if ( typeBoolLabel != null && typeBoolLabel.IsValid() ) typeBoolLabel.Text = "Bool";
                      if ( typeFloatLabel != null && typeFloatLabel.IsValid() ) typeFloatLabel.Text = "Float";
                      if ( typeStringLabel != null && typeStringLabel.IsValid() ) typeStringLabel.Text = "String";
                      if ( typeEnumLabel != null && typeEnumLabel.IsValid() ) typeEnumLabel.Text = "Enum";
                      if ( typeComplexLabel != null && typeComplexLabel.IsValid() ) typeComplexLabel.Text = "Complex";
                      if ( typeErrorsLabel != null && typeErrorsLabel.IsValid() ) typeErrorsLabel.Text = "Errors";
              }

              // Selection counts are updated by UpdateSelectionSummary, which already
              // respects shorthandLabels.
      }

      public void SetTarget( GameObject obj )
      {
              if ( isDisposed || !Visible )
                      return;

              if ( target == obj )
                      return;

              if ( debugLogging )
              {
                      var name = obj != null && obj.IsValid() ? obj.Name ?? "(unnamed)" : "(null)";
                      Log.Info( $"[SearchFilter] SetTarget -> {name}" );
              }

              target = obj;

              if ( target == null || !target.IsValid() )
              {
                      pendingRebuild = false;
                      UpdateSelectionSummary();

                      // Respect the \"Deselect clears results\" option. When disabled,
                      // keep the current results visible even if nothing is selected.
                      if ( clearOnDeselect )
                      {
                              ShowSelectedHint();
                      }

                      UpdateManualModeHint();
                      return;
              }

              pendingRebuild = true;
              UpdateSelectionSummary();

              if ( searchTriggerMode == SearchTriggerMode.Live ||
                   (searchTriggerMode == SearchTriggerMode.Safe && autoPopulateOnSelectionInSafe) )
              {
                      // Live mode (or Safe with override): selecting updates immediately.
                      RebuildRows();
              }
              else if ( searchTriggerMode == SearchTriggerMode.Safe )
              {
                      // In Safe mode, we normally keep existing results. If configured not
                      // to keep results, or if we have no rows yet (fresh open), replace
                      // the plonker text with the object-selected hint.
                      if ( !keepResultsOnSelectionChange || (rows.Count == 0 && string.IsNullOrEmpty( lastSearch )) )
                      {
                              ShowSelectedHint();
                      }
              }

              UpdateManualModeHint();
      }

      private void ShowSelectedHint()
      {
              if ( listContainer is null )
                      return;

              listContainer.Layout.Clear( true );
              noResultsContainer = null;
              noResultsLabel = null;
              if ( matchLabel != null && matchLabel.IsValid() )
              {
                      matchLabel.Text = "0";
                      matchLabel.SetStyles( "color: rgba(255,255,255,0.4);" );
              }

              // Reset progress/status when we are back to the empty selection hint.
              totalComponentCount = 0;
              builtComponentCount = 0;
              lastProgress01 = 0f;
              safeQueryDirty = false;
              UpdateSearchProgress( 0f );

              var emptyContainer = new Widget( listContainer );
              var emptyLayout = emptyContainer.Layout = Layout.Column();
              emptyLayout.AddStretchCell();

              var hasSelection = target != null && target.IsValid();
              var hintText = hasSelection
                      ? string.Format( "Selected: {0}\n\nPress Enter on empty search to list all fields.", target?.Name ?? "(unnamed)" )
                      : "You haven't selected anything, you plonker.";
              var hint = new Label( hintText )
              {
                      Alignment = TextFlag.Center
              };
              hint.SetStyles( "color: rgba(255,255,255,0.35); font-style: italic; margin-top: 10px; text-align:center; white-space: pre-wrap;" );
              emptyLayout.Add( hint, 0 );
              emptyLayout.AddStretchCell();
              emptyContainer.SetStyles( "min-height: 220px;" );
              listContainer.Layout.Add( emptyContainer, 0 );
      }

      private void ShowResultsClearedMessage()
      {
              if ( listContainer is null )
                      return;

              listContainer.Layout.Clear( true );

              // This message replaces any previous "no results" banner, so drop
              // references to those widgets to avoid talking to disposed labels.
              noResultsContainer = null;
              noResultsLabel = null;

              if ( matchLabel != null && matchLabel.IsValid() )
              {
                      matchLabel.Text = "0";
                      matchLabel.SetStyles( "color: rgba(255,255,255,0.4);" );
              }
              if ( scanStatusLabel != null && scanStatusLabel.IsValid() )
              {
                      scanStatusLabel.Text = string.Empty;
              }

              var container = new Widget( listContainer );
              var layout = container.Layout = Layout.Column();
              layout.AddStretchCell();

              var label = new Label( "Results cleared. Enter a search to repopulate." )
              {
                      Alignment = TextFlag.Center
              };
              label.SetStyles( "color: rgba(255,255,255,0.35); font-style: italic; margin-top: 10px; text-align:center;" );
              layout.Add( label, 0 );
              layout.AddStretchCell();
              container.SetStyles( "min-height: 220px;" );
              listContainer.Layout.Add( container, 0 );

              totalComponentCount = 0;
              builtComponentCount = 0;
              lastProgress01 = 0f;
              UpdateSearchProgress( 0f );
      }

      protected override void OnKeyPress( KeyEvent e )
      {
              base.OnKeyPress( e );

              // When the search box is empty and a base term is active, a Backspace
              // press should clear only the base tag without reordering or
              // refiltering the stack.
              if ( safeBaseActive && searchBox != null )
              {
                      // KeyEvent doesn't expose Backspace directly everywhere, but we can
                      // use the button code name which is already used in KeyBind.
                      var keyName = e.GetButtonCodeName();
                      if ( !string.IsNullOrEmpty( keyName ) &&
                           keyName.Equals( "Backspace", StringComparison.OrdinalIgnoreCase ) &&
                           string.IsNullOrEmpty( searchBox.Text ) )
                      {
                              ClearSafeBase();
                              e.Accepted = true;
                      }
              }
      }

      /// <summary>
      /// Open a small context menu with toggles for behaviour that is handy to
      /// tweak while iterating on the tool (Safe mode selection, clearing rules).
      /// </summary>
      private void OpenOptionsMenu()
      {
              var menu = new ContextMenu();

              // Selection behaviour
              var keepResultsLabel = keepResultsOnSelectionChange
                      ? "✓ Keep results when changing object"
                      : "Keep results when changing object";
              menu.AddOption( keepResultsLabel, "visibility", () =>
              {
                      keepResultsOnSelectionChange = !keepResultsOnSelectionChange;
              } );

              var clearOnDeselectLabel = clearOnDeselect
                      ? "✓ Deselect clears results"
                      : "Deselect clears results";
              menu.AddOption( clearOnDeselectLabel, "close", () =>
              {
                      clearOnDeselect = !clearOnDeselect;
              } );

              var autoPopulateLabel = autoPopulateOnSelectionInSafe
                      ? "✓ Safe mode: selection auto-populates list"
                      : "Safe mode: selection auto-populates list";
              menu.AddOption( autoPopulateLabel, "playlist_add_check", () =>
              {
                      autoPopulateOnSelectionInSafe = !autoPopulateOnSelectionInSafe;
              } );

              var includeTypesLabel = includeTypeNamesInNameSearch
                      ? "✓ Include type names in name search"
                      : "Include type names in name search";
              menu.AddOption( includeTypesLabel, "category", () =>
              {
                      includeTypeNamesInNameSearch = !includeTypeNamesInNameSearch;

                      // Clear cached name tokens so the next filter pass rebuilds them
                      // with the updated setting.
                      foreach ( var model in rowModels )
                      {
                              if ( model != null )
                              {
                                      model.NameTokens = null;
                              }
                      }

                      if ( !string.IsNullOrEmpty( lastSearch ) )
                      {
                              ApplyFilter( lastSearch );
                      }
                      else
                      {
                              UpdateManualModeHint();
                      }
              } );

              var flattenListsLabel = flattenLists
                      ? "✓ Flatten simple lists (e.g. hide body groups)"
                      : "Flatten simple lists (e.g. hide body groups)";
              menu.AddOption( flattenListsLabel, "format_list_bulleted", () =>
              {
                      flattenLists = !flattenLists;

                      // Force a rebuild so flattened list rows are rebuilt/removed.
                      pendingRebuild = true;
                      if ( searchTriggerMode == SearchTriggerMode.Live )
                      {
                              RebuildRows();
                      }
                      else
                      {
                              UpdateManualModeHint();
                      }
              } );

              // Loading / view
              var lazyLabel = lazyLoadProperties
                      ? "✓ Lazy-load component properties"
                      : "Lazy-load component properties";
              menu.AddOption( lazyLabel, "schedule", () =>
              {
                      lazyLoadProperties = !lazyLoadProperties;
                      pendingRebuild = true;
                      if ( searchTriggerMode == SearchTriggerMode.Live )
                      {
                              ApplyFilter( lastSearch );
                      }
                      else
                      {
                              UpdateManualModeHint();
                      }
              } );

              // Field-only mode has been removed; keeping logic internal-only.

              var headerStatsLabel = showHeaderStats
                      ? "✓ Show header stats"
                      : "Show header stats";
              menu.AddOption( headerStatsLabel, "insights", () =>
              {
                      showHeaderStats = !showHeaderStats;
                      ApplyHeaderStatsVisibility();
              } );

              var shorthandLabel = shorthandLabels
                      ? "✓ Shorthand labels"
                      : "Shorthand labels";
              menu.AddOption( shorthandLabel, "short_text", () =>
              {
                      shorthandLabels = !shorthandLabels;
                      UpdateSelectionSummary();
                      ApplyShorthandLabels();
              } );

              // Search helpers
              var nestedLabel = showHiddenNestedBools
                      ? "✓ Nested RenderOptions flags"
                      : "Nested RenderOptions flags";
              menu.AddOption( nestedLabel, "filter_alt", () =>
              {
                      showHiddenNestedBools = !showHiddenNestedBools;
                      // In Safe mode, simply mark that a rebuild is needed and let the
                      // blue banner remind the user to press Enter. In Live mode we can
                      // safely refresh immediately.
                      pendingRebuild = true;
                      if ( searchTriggerMode == SearchTriggerMode.Live )
                      {
                              ApplyFilter( lastSearch );
                      }
                      else
                      {
                              UpdateManualModeHint();
                      }
              } );

              // Deep nested-bool mode (all complex types) has been disabled for now;
              // it is kept here commented as an experimental option that proved too
              // slow in large scenes.
              // var deepNestedLabel = showDeepNestedBools
              //      ? "✓ Search: deep nested option flags (all, slower)"
              //      : "Search: deep nested option flags (all, slower)";
              // menu.AddOption( deepNestedLabel, "filter_list", () =>
              // {
              //      showDeepNestedBools = !showDeepNestedBools;
              //      // Same Safe/Live behaviour as the shallow nested option flags.
              //      pendingRebuild = true;
              //      if ( searchTriggerMode == SearchTriggerMode.Live )
              //      {
              //              ApplyFilter( lastSearch );
              //      }
              //      else
              //      {
              //              UpdateManualModeHint();
              //      }
              // } );

              // Debug
              var debugLabel = debugLogging
                      ? "✓ Debug: log timings to console"
                      : "Debug: log timings to console";
              menu.AddOption( debugLabel, "bug_report", () =>
              {
                      debugLogging = !debugLogging;
              } );

              var modeMsgLabel = showModeMessages
                      ? "✓ Show mode messages"
                      : "Show mode messages";
              menu.AddOption( modeMsgLabel, "info", () =>
              {
                      showModeMessages = !showModeMessages;
                      UpdateManualModeHint();
              } );

              menu.OpenAt( Editor.Application.CursorPosition );
      }

      private void UpdateManualModeHint()
      {
              if ( manualModeHintLabel is null )
                      return;

              if ( !showModeMessages )
              {
                      manualModeHintLabel.Visible = false;
                      return;
              }

              // Live mode uses a persistent yellow banner so it is obvious that the
              // list is updating as you type / select.
              if ( searchTriggerMode == SearchTriggerMode.Live )
              {
                      manualModeHintLabel.Text = "Live mode is on. Results update as you type and when selection changes.";
                      manualModeHintLabel.SetStyles( "color: rgba(255,220,220,0.95); background-color: rgba(150,40,40,0.75); font-weight: 600; font-size: 12px; margin: 8px 0; padding: 8px 12px; border-radius: 6px; text-align:center; min-width: 260px;" );
                      manualModeHintLabel.Visible = true;
                      return;
              }

              // Safe base active acts like a mini live mode for refinements.
              if ( searchTriggerMode == SearchTriggerMode.Safe && safeBaseActive )
              {
                      var term = string.IsNullOrEmpty( safeBaseSearchText ) ? "base term" : $"\"{safeBaseSearchText}\"";
                      manualModeHintLabel.Text = $"Searching within results for {term}. Refining updates per keystroke.";
                      manualModeHintLabel.SetStyles( "color: rgba(255,240,200,0.95); background-color: rgba(140,110,30,0.7); font-weight: 600; font-size: 12px; margin: 8px 0; padding: 8px 12px; border-radius: 6px; text-align:center; min-width: 260px;" );
                      manualModeHintLabel.Visible = true;
                      return;
              }

              // Safe mode banner appears when either a structural change is pending
              // (new scan required) or the current query text has changed and needs
              // to be applied.
              var showBanner = searchTriggerMode == SearchTriggerMode.Safe && (pendingRebuild || safeQueryDirty);

              if ( showBanner )
              {
                      manualModeHintLabel.Text = "Safe mode is on. Press Enter or click refresh to update results.";
                      manualModeHintLabel.SetStyles( "color: rgba(200,220,255,0.85); background-color: rgba(20,60,120,0.55); font-weight: 600; font-size: 12px; margin: 8px 0; padding: 8px 12px; border-radius: 6px; text-align:center; min-width: 260px;" );
                      manualModeHintLabel.Visible = true;
              }
              else
              {
                      manualModeHintLabel.Visible = false;
              }
      }

      private void UpdateSelectionSummary()
      {
              if ( selectedNameLabel is null || selectedCountsLabel is null )
                      return;

              if ( target == null || !target.IsValid() )
              {
                      selectedNameLabel.Text = "(none)";
                      selectedCountsLabel.Text = "0 descendants · 0 components";
                      return;
              }

              selectedNameLabel.Text = target.Name ?? "(unnamed)";

              var descendants = 0;
              var components = 0;

              if ( includeChildren )
              {
                      foreach ( var go in EnumerateWithChildren( target ) )
                      {
                              if ( go == null || !go.IsValid() )
                                      continue;

                              if ( go != target )
                                      descendants++;

                              components += go.Components.GetAll().Count( c => c != null && c.IsValid() );
                      }
              }
              else
              {
                      components = target.Components.GetAll().Count( c => c != null && c.IsValid() );
              }

              if ( shorthandLabels )
              {
                      selectedCountsLabel.Text = $"{descendants} d · {components} c";
              }
              else
              {
                      selectedCountsLabel.Text = $"{descendants} descendants · {components} components";
              }
      }

      public void EnsureTarget( GameObject obj )
      {
              if ( isDisposed || !Visible )
                      return;

              if ( target == obj )
                      return;

              SetTarget( obj );
      }

      /// <summary>
      /// Keep in sync with the active scene selection.
      /// </summary>
      [EditorEvent.Frame]
      public void Frame()
      {
              if ( isDisposed )
                      return;

              if ( !Visible )
                      return;

              var session = SceneEditorSession.Active;
              if ( session is null )
                      return;

              var go = session.Selection.OfType<GameObject>().FirstOrDefault();
              EnsureTarget( go );

              ProcessPendingComponentBuilds();

              if ( filterDebouncePending &&
                   !componentBuildQueue.HasPending )
              {
                      var debounceMs = FilterDebounceMs;
                      if ( searchTriggerMode == SearchTriggerMode.Safe && safeBaseActive )
                      {
                              debounceMs = SafeBaseFilterDebounceMs;
                      }

                      if ( filterDebounceStopwatch.ElapsedMilliseconds >= debounceMs )
                      {
                              filterDebouncePending = false;

                              if ( searchTriggerMode == SearchTriggerMode.Live )
                              {
                                      ApplyFilter( pendingFilterText );
                              }
                              else if ( searchTriggerMode == SearchTriggerMode.Safe && safeBaseActive )
                              {
                                      ApplyFilterSafe( pendingFilterText );
                              }
                      }
              }

              UpdateProgressIcon();
      }

      private void RebuildRows( bool applyFilter = true, bool force = false )
      {
              if ( isDisposed || ( !Visible && !force ) )
                      return;

              // Any new rebuild makes previous filter work obsolete.
              filterInProgress = false;
              filterSnapshotCache.Clear();
              filterSnapshotOrder.Clear();

              pendingRebuild = false;
              UpdateManualModeHint();

              CancelPendingScan();
              debugNestedTypeNames.Clear();

              if ( target == null || !target.IsValid() )
              {
                      ShowSelectedHint();
                      return;
              }

              PrepareForScan();

              var signature = SelectionSignature.Create( target, includeChildren, valueSearchMode, searchTriggerMode, lazyLoadProperties );

              if ( !force && scanCache.TryGetValue( signature, out var cachedResult ) && cachedResult != null)
              {
                      ApplyScanResult( cachedResult, applyFilter );
                      return;
              }

              var request = new ScanJobRequest
              {
                      Target = target,
                      IncludeChildren = includeChildren,
                      LazyLoad = lazyLoadProperties,
                      Signature = signature
              };

              StartBackgroundScan( request, applyFilter && !string.IsNullOrEmpty( lastSearch ) );
      }

      private void PrepareForScan()
      {
              rows.Clear();
              rowModels.Clear();
              rowModelLookup.Clear();
              rowLookup.Clear();
              nextRowId = 0;
              rowsDirty = true;
              componentGroups.Clear();
              gameObjectGroups.Clear();
              componentBuildQueue.Clear();
              ClearTokenIndexes();

              if ( listContainer == null )
                      return;

              listContainer.Layout.Clear( true );
              noResultsContainer = null;
              noResultsLabel = null;

              matchLabel.Text = "…";
              matchLabel.SetStyles( "color: rgba(255,255,255,0.4);" );

              var placeholder = new Widget( listContainer );
              var placeholderLayout = placeholder.Layout = Layout.Column();
              placeholderLayout.AddStretchCell();

              var hint = new Label( "Scanning selection…" )
              {
                      Alignment = TextFlag.Center
              };
              hint.SetStyles( "color: rgba(255,255,255,0.35); font-style: italic; margin-top: 10px; text-align:center;" );
              placeholderLayout.Add( hint, 0 );
              placeholderLayout.AddStretchCell();

              placeholder.SetStyles( "min-height: 220px;" );
              listContainer.Layout.Add( placeholder );

              // Reset progress at the start of a scan.
              totalComponentCount = 0;
              builtComponentCount = 0;
              lastProgress01 = 0f;
              UpdateSearchProgress( 0f );

              lastProgress01 = 0f;
              UpdateProgressIcon();
      }

      private void CancelPendingScan()
      {
              if ( rebuildCancellation != null )
              {
                      rebuildCancellation.Cancel();
                      rebuildCancellation.Dispose();
                      rebuildCancellation = null;
              }

              componentBuildQueue.Clear();
              filterDebouncePending = false;
              filterInProgress = false;
              UpdateProgressIcon();
      }

      private void StartBackgroundScan( ScanJobRequest request, bool applyFilter )
      {
              if ( request == null )
                      return;

              var cts = new CancellationTokenSource();
              rebuildCancellation = cts;
              var token = cts.Token;
              var sequence = ++rebuildSequence;
              var stopwatch = debugLogging ? Stopwatch.StartNew() : null;

              Task.Run( () =>
              {
                              ScanJobResult result = null;
                              try
                              {
                                      result = BuildScanResult( request, token );
                      }
                      catch ( Exception e )
                      {
                              Log.Warning( $"[SearchFilter] Background scan failed: {e.Message}" );
                      }

                      if ( result == null )
                              return;

                      if ( stopwatch != null )
                      {
                              stopwatch.Stop();
                              result.ScanMilliseconds = stopwatch.ElapsedMilliseconds;
                      }

                      MainThread.Queue( () =>
                      {
                              if ( token.IsCancellationRequested || sequence != rebuildSequence )
                                      return;

                              if ( rebuildCancellation != null )
                              {
                                      rebuildCancellation.Dispose();
                                      rebuildCancellation = null;
                              }

                              // Cache the scan result for this selection signature so that
                              // revisiting recent objects does not require a full rescan.
                              if ( result != null )
                              {
                                      if ( !scanCacheOrder.Contains( request.Signature ) )
                                      {
                                              scanCacheOrder.Enqueue( request.Signature );
                                      }

                                      while ( scanCacheOrder.Count > MaxScanCacheEntries )
                                      {
                                              var oldest = scanCacheOrder.Dequeue();
                                              scanCache.Remove( oldest );
                                      }

                                      scanCache[request.Signature] = result;
                              }

                              ApplyScanResult( result, applyFilter );
                      } );
              } );
      }

      private ScanJobResult BuildScanResult( ScanJobRequest request, CancellationToken token )
      {
              var result = new ScanJobResult
              {
                      LazyLoad = request.LazyLoad
              };

              if ( request.Target == null || !request.Target.IsValid() )
                      return result;

              foreach ( var go in EnumerateTargets( request.Target, request.IncludeChildren ) )
              {
                      if ( token.IsCancellationRequested )
                              return null;

                      if ( go == null || !go.IsValid() )
                              continue;

                      var goData = new ScanGameObjectResult
                      {
                              GameObject = go,
                              Depth = GetDepth( go ),
                              Path = GetHierarchyPath( go )
                      };

                      foreach ( var component in go.Components.GetAll() )
                      {
                              if ( token.IsCancellationRequested )
                                      return null;

                              if ( component == null || !component.IsValid() )
                                      continue;

                              var type = component.GetType();
                              PropertyInfo[] props;
                              try
                              {
                                      props = propertyMetadataCache.GetPropertiesForComponentType( type );
                              }
                              catch
                              {
                                      continue;
                              }

                              var comp = new ComponentScanResult
                              {
                                      Component = component,
                                      DisplayName = type.Name,
                                      Properties = props
                              };

                              goData.Components.Add( comp );
                      }

                      result.GameObjects.Add( goData );
              }

              return result;
      }

      private void ApplyScanResult( ScanJobResult result, bool applyFilter )
      {
              if ( listContainer == null )
                      return;

              rows.Clear();
              rowModels.Clear();
              rowModelLookup.Clear();
              rowLookup.Clear();
              nextRowId = 0;
              componentGroups.Clear();
              gameObjectGroups.Clear();
              componentBuildQueue.Clear();
              ClearTokenIndexes();
              listContainer.Layout.Clear( true );
              noResultsContainer = null;
              noResultsLabel = null;

              // Capture total component count for the current scan so we can render
              // a simple progress bar as properties are materialised.
              totalComponentCount = 0;
              builtComponentCount = 0;

              var useLazy = result?.LazyLoad ?? lazyLoadProperties;
              if ( string.IsNullOrWhiteSpace( lastSearch ) )
              {
                      useLazy = true;
              }

              if ( result == null || result.GameObjects.Count == 0 )
              {
                      ShowSelectedHint();
                      return;
              }

              foreach ( var goData in result.GameObjects )
              {
                      var go = goData.GameObject;
                      if ( go == null || !go.IsValid() )
                              continue;

                      var depth = goData.Depth;
                      var goRow = new Widget( listContainer );
                      var goRowLayout = goRow.Layout = Layout.Row();
                      goRowLayout.Spacing = 0;

                      var goIndent = new Widget( goRow );
                      var goIndentLayout = goIndent.Layout = Layout.Row();
                      goIndentLayout.Spacing = 0;

                      var folderHitLabel = new Label( "" )
                      {
                              Alignment = TextFlag.Center
                      };
                      folderHitLabel.SetStyles( "color: rgba(130,220,185,0.9); font-size: 12px; font-weight: 600; margin:0; padding:0;" );
                      folderHitLabel.Visible = false;
                      goIndentLayout.Add( folderHitLabel, 0 );

                      goIndent.FixedWidth = 20 + 14 * depth;
                      goRowLayout.Add( goIndent, 0 );

                      var goShade = depth % 2 == 0
                              ? "rgba(255,255,255,0.02)"
                              : "rgba(255,255,255,0.05)";
                      goRow.SetStyles( $"background-color: {goShade}; border-radius: 4px; margin: 2px 0; padding: 2px 2px 2px 0;" );

                      var goGroup = new ExpandGroup( goRow )
                      {
                              Title = go.Name,
                              Icon = "folder"
                      };
                      goGroup.SetOpenState( false );
                      goGroup.StateCookieName = $"SpudInspectorSearcher.GameObject.{go.Id}";

                      var goBody = new Widget( goGroup );
                      goBody.Layout = Layout.Column();
                      goGroup.SetWidget( goBody );
                      goRowLayout.Add( goGroup, 1 );
                      listContainer.Layout.Add( goRow );

                      // When a search term is active, defer showing the hierarchy rows
                      // themselves until after the first filtered ApplyFilter pass so we
                      // never flash an unfiltered tree while components are still
                      // streaming in.
                      if ( !string.IsNullOrEmpty( lastSearch ) )
                      {
                              goRow.Visible = false;
                      }

                      var goKey = go.Id.ToString();
                      var goInfo = new GameObjectGroup
                      {
                              Group = goGroup,
                              Name = goGroup.Title,
                              NameLower = (goGroup.Title ?? string.Empty).ToLowerInvariant(),
                              HitLabel = folderHitLabel,
                              Row = goRow,
                              Key = goKey
                      };
                      gameObjectGroups.Add( goInfo );

                      var pathRow = new Widget( goBody );
                      var pathLayout = pathRow.Layout = Layout.Row();
                      pathLayout.Spacing = 4;

                      var capturedGo = go;
                      var pathButton = new IconButton( "arrow_forward", () =>
                      {
                              if ( capturedGo != null && capturedGo.IsValid() )
                              {
                                      var session = SceneEditorSession.Active;
                                      try
                                      {
                                              session?.Selection?.Set( capturedGo );
                                      }
                                      catch
                                      {
                                      }
                              }
                      } )
                      {
                              FixedWidth = Theme.RowHeight,
                              FixedHeight = Theme.RowHeight,
                              ToolTip = "Select this object in the scene"
                      };
                      pathLayout.Add( pathButton, 0 );

                      var pathLabel = new Label( goData.Path )
                      {
                              Alignment = TextFlag.LeftCenter
                      };
                      pathLabel.SetStyles( "color: rgba(200,220,255,0.8);" );
                      pathLayout.Add( pathLabel, 1 );

                      pathRow.SetStyles( "padding: 2px 4px; margin: 2px 0 4px 0; background-color: rgba(255,255,255,0.02); border-radius: 4px;" );
                      goBody.Layout.Add( pathRow );

                      foreach ( var compData in goData.Components )
                      {
                              var component = compData.Component;
                              if ( component == null || !component.IsValid() )
                                      continue;

                              var compRow = new Widget( goBody );
                              var compRowLayout = compRow.Layout = Layout.Row();
                              compRowLayout.Spacing = 0;

                              var compIndent = new Widget( compRow );
                              compIndent.FixedWidth = 14;
                              compRowLayout.Add( compIndent, 0 );

                              var group = new ExpandGroup( compRow )
                              {
                                      Title = compData.DisplayName,
                                      Icon = "tune"
                              };
                              group.SetOpenState( false );
                              group.StateCookieName = $"SpudInspectorSearcher.{go.Id}.{compData.DisplayName}";

                              var groupBody = new Widget( group );
                              groupBody.Layout = Layout.Column();
                              group.SetWidget( groupBody );

                              compRowLayout.Add( group, 1 );
                              goBody.Layout.Add( compRow );

                              var groupInfo = new ComponentGroup
                              {
                                      Group = group,
                                      Name = group.Title,
                                      OwnerKey = goKey,
                                      MatchKey = $"{goKey}:{group.Title}:{component.GetHashCode()}",
                                      OwnerName = go.Name,
                                      Component = component,
                                      Properties = compData.Properties ?? Array.Empty<PropertyInfo>(),
                                      Body = groupBody,
                                      PropertiesBuilt = false
                              };
                              componentGroups.Add( groupInfo );
                              totalComponentCount++;

                              if ( useLazy )
                              {
                                      var placeholder = new Widget( groupBody );
                                      var phLayout = placeholder.Layout = Layout.Row();
                                      phLayout.Spacing = 4;

                                      var loadButton = new IconButton( "unfold_more", () =>
                                      {
                                              groupBody.Layout.Clear( true );
                                              BuildComponentRows( groupInfo );
                                              groupInfo.PropertiesBuilt = true;

                                              if ( !string.IsNullOrEmpty( lastSearch ) || pendingRebuild )
                                              {
                                                      ApplyFilter( lastSearch );
                                              }
                                      } )
                                      {
                                              FixedWidth = Theme.RowHeight,
                                              FixedHeight = Theme.RowHeight,
                                              ToolTip = "Load properties for this component (lazy mode)."
                                      };
                                      phLayout.Add( loadButton, 0 );

                                      var phLabel = new Label( "Load properties for this component (lazy mode)" )
                                      {
                                              Alignment = TextFlag.LeftCenter
                                      };
                                      phLabel.SetStyles( "color: rgba(255,255,255,0.6);" );
                                      phLayout.Add( phLabel, 1 );

                                      groupBody.Layout.Add( placeholder );
                              }
                              else
                              {
                                      componentBuildQueue.Enqueue( groupInfo );
                              }
                      }
              }

              var stateContainer = new Widget( listContainer );
              var stateLayout = stateContainer.Layout = Layout.Column();
              stateLayout.Spacing = 4;
              stateLayout.AddStretchCell();

              var stateLabel = new Label( "" )
              {
                      Alignment = TextFlag.Center
              };
              stateLabel.SetStyles( "color: rgba(255,255,255,0.35); font-style: italic; margin-top: 10px; text-align:center;" );
              stateLayout.Add( stateLabel, 0 );

              stateLayout.AddStretchCell();
              stateContainer.SetStyles( "min-height: 220px;" );
              listContainer.Layout.Add( stateContainer, 0 );

              noResultsContainer = stateContainer;
              noResultsLabel = stateLabel;

              var hasPendingRows = componentBuildQueue.HasPending;

              if ( rows.Count == 0 )
              {
                      noResultsContainer.Visible = true;

                      // When a search term is active and rows are still being built,
                      // signal that we are searching rather than claiming "No results".
                      if ( !string.IsNullOrEmpty( lastSearch ) && hasPendingRows )
                      {
                              var searchingTerm = lastSearch
                                      .ToLowerInvariant()
                                      .Split( ' ', StringSplitOptions.RemoveEmptyEntries )
                                      .FirstOrDefault() ?? lastSearch;
                              var selectedName = target != null && target.IsValid()
                                      ? target.Name ?? "(unnamed)"
                                      : "(no selection)";
                              noResultsLabel.Text = $"Searching \"{searchingTerm}\"\n\nin {selectedName} ({BuildSearchFilterSummary()})...";
                      }
                      else if ( string.IsNullOrEmpty( lastSearch ) )
                      {
                              noResultsLabel.Text = "Object selected. Enter a search to populate the results.";
                      }
                      else
                      {
                              noResultsLabel.Text = $"No results for \"{lastSearch}\" :(";
                      }
              }
              else
              {
                      noResultsContainer.Visible = false;
              }

              UpdateSearchProgress( totalComponentCount > 0 ? (float)builtComponentCount / totalComponentCount : 0f );

              listContainer.Layout.AddStretchCell();

              if ( debugLogging && result != null )
              {
                      Log.Info( $"[SearchFilter] RebuildRows async lazy={lazyLoadProperties} lean={leanMode} nestedBools={showHiddenNestedBools} objects={gameObjectGroups.Count} components={componentGroups.Count} rows={rows.Count} scanMs={result.ScanMilliseconds}" );
              }

              if ( applyFilter )
              {
                      ApplyFilter( lastSearch );
              }

              UpdateProgressIcon();
      }

      private void ProcessPendingComponentBuilds()
      {
              var builtAny = componentBuildQueue.ProcessNextBatch( BuildComponentRows );
              if ( builtAny )
              {
                      // Track how many component groups have had their properties built
                      // so we can update the progress bar even while results are still
                      // streaming in.
                      builtComponentCount = componentGroups.Count( g => g.PropertiesBuilt );
                      var progress01 = totalComponentCount > 0 ? (float)builtComponentCount / totalComponentCount : 0f;

                      UpdateSearchProgress( progress01 );
                      UpdateProgressIcon();

                      // When a safe-mode base term is active, new rows can start
                      // matching it. Refresh the base set so refinements stay on the
                      // correct subset.
                      var safeModeActive = searchTriggerMode == SearchTriggerMode.Safe && safeBaseActive && !string.IsNullOrEmpty( safeBaseSearchText );
                      if ( safeModeActive )
                      {
                              RefreshSafeBaseMatches();
                      }

                      if ( gradualResults || !componentBuildQueue.HasPending )
                      {
                              if ( safeModeActive && !string.IsNullOrEmpty( lastSearch ) )
                              {
                                      var refineWithinBase = lastSearch != safeBaseSearchText;
                                      ApplyFilter( refineWithinBase ? lastSearch : safeBaseSearchText, refineWithinBase );
                              }
                              else
                              {
                                      ApplyFilter( lastSearch );
                              }

                              // When the queue has finished and the final filter has run,
                              // mark progress as complete so the bar reaches 100% only once
                              // results are actually ready.
                              if ( !componentBuildQueue.HasPending )
                              {
                                      UpdateSearchProgress( 1f );
                              }
                      }
              }
      }

      private void UpdateSearchProgress( float progress01 )
      {
              progress01 = Math.Clamp( progress01, 0f, 1f );
              if ( progress01 < lastProgress01 )
              {
                      progress01 = lastProgress01;
              }
              lastProgress01 = progress01;

              if ( scanStatusLabel != null && scanStatusLabel.IsValid() )
              {
                      if ( totalComponentCount > 0 )
                      {
                              scanStatusLabel.Text = $"{builtComponentCount}/{totalComponentCount}";
                      }
                      else
                      {
                              scanStatusLabel.Text = rows.Count > 0
                                      ? $"{rows.Count}"
                                      : string.Empty;
                      }
              }
      }

      private void OnSearchChanged( string text )
      {
              if ( suppressSearchChanged )
                      return;

              // Live mode and Safe-mode with an active base both behave like
              // debounced "live" search; plain Safe mode requires Enter.
              if ( searchTriggerMode == SearchTriggerMode.Live ||
                   (searchTriggerMode == SearchTriggerMode.Safe && safeBaseActive) )
              {
                      pendingFilterText = text ?? string.Empty;
                      filterDebouncePending = true;
                      filterDebounceStopwatch.Restart();

                      if ( searchTriggerMode == SearchTriggerMode.Safe )
                      {
                              safeQueryDirty = false;
                      }

                      UpdateSearchPlaceholder();
              }
              else
              {
                      // Safe mode: typing indicates that results are stale for the current
                      // query; the user needs to press Enter to re-apply the filter, but
                      // we do not need a new scan of the hierarchy.
                      safeQueryDirty = true;
                      UpdateManualModeHint();
                      UpdateSearchPlaceholder();
              }
      }

      private void ClearSearch()
      {
              if ( isDisposed )
                      return;

              // If a search/scan/filter is currently in progress, treat this as an
              // interrupt instead of a full clear: cancel work but keep the current
              // stack and counts visible.
              var isSearching = rebuildCancellation != null || componentBuildQueue.HasPending || filterDebouncePending || filterInProgress;
              if ( isSearching )
              {
                      filterDebouncePending = false;
                      filterInProgress = false;
                      CancelPendingScan();
                      // After an interrupt, keep whatever rows we have so far and apply
                      // the current filter to them so the user can see partial results.
                      if ( rows.Count > 0 && !string.IsNullOrEmpty( lastSearch ) )
                      {
                              ApplyFilter( lastSearch, refineWithinBase: safeBaseActive );
                      }

                      // Do not clear the base term here; the user can choose to clear it
                      // explicitly with another action.
                      if ( debugLogging )
                      {
                              Log.Info( "[SearchFilter] Interrupt: cancelled search, kept partial results." );
                      }
                      UpdateManualModeHint();
                      UpdateProgressIcon();
                      return;
              }

              // Safe mode: first click clears only the text and marks a pending rebuild.
              // Second click (with an active search but empty text) clears the results.
                      if ( searchTriggerMode == SearchTriggerMode.Safe )
                      {
                              if ( !string.IsNullOrEmpty( searchBox.Text ) )
                      {
                              // First clear: empty the field and show the Safe-mode banner, but
                              // keep current results until the user explicitly refreshes. Also
                              // drop any active base term.
                              suppressSearchChanged = true;
                              searchBox.Text = string.Empty;
                              suppressSearchChanged = false;
                              pendingRebuild = true;
                              ClearSafeBase();
                              UpdateManualModeHint();
                      }
                              else if ( !string.IsNullOrEmpty( lastSearch ) || rows.Count > 0 )
                              {
                                      // Second clear: drop the active search term and clear the results.
                                      CancelPendingScan();
                                      lastSearch = string.Empty;
                                      totalComponentCount = 0;
                                      builtComponentCount = 0;
                                      lastProgress01 = 0f;
                                      safeQueryDirty = false;
                                      UpdateSearchProgress( 0f );
                                      rows.Clear();
                                      rowModels.Clear();
                             rowLookup.Clear();
                              rowModelLookup.Clear();
                              nextRowId = 0;
                              rowsDirty = true;
                              componentGroups.Clear();
                              gameObjectGroups.Clear();
                              ClearTokenIndexes();
                              ShowResultsClearedMessage();
                              // Mark that the next search needs a fresh rebuild.
                              pendingRebuild = true;
                              safeQueryDirty = false;
                              ClearSafeBase();
                              UpdateSelectionSummary();
                              if ( scanTimeLabel != null && scanTimeLabel.IsValid() )
                              {
                                      scanTimeLabel.Text = string.Empty;
                              }
                              UpdateManualModeHint();
                      }

                      searchBox.Focus();
                      UpdateProgressIcon();
                      return;
              }

              // Live mode:
              // - First click: clear text and debounce filter (keep results visible).
              // - Second click (with empty text but an active search): clear results.
              if ( searchTriggerMode == SearchTriggerMode.Live )
              {
                      if ( !string.IsNullOrEmpty( searchBox.Text ) || string.IsNullOrEmpty( lastSearch ) )
                      {
                              // First clear: empty the field and debounce the filter so we
                              // don't immediately trigger a heavy pass if the user keeps typing.
                              suppressSearchChanged = true;
                              searchBox.Text = string.Empty;
                              suppressSearchChanged = false;

                              pendingFilterText = string.Empty;
                              filterDebouncePending = true;
                              filterDebounceStopwatch.Restart();

                              CancelPendingScan();

                              ClearSafeBase();

                              UpdateManualModeHint();
                              searchBox.Focus();
                              UpdateProgressIcon();
                              return;
                      }

                      if ( string.IsNullOrEmpty( searchBox.Text ) && (rows.Count > 0 || !string.IsNullOrEmpty( lastSearch )) )
                      {
                              // Second clear: drop the active search term and clear results.
                              CancelPendingScan();
                              lastSearch = string.Empty;
                              totalComponentCount = 0;
                              builtComponentCount = 0;
                              lastProgress01 = 0f;
                              UpdateSearchProgress( 0f );
                              rows.Clear();
                              rowModels.Clear();
                              rowLookup.Clear();
                              rowModelLookup.Clear();
                              nextRowId = 0;
                              rowsDirty = true;
                              componentGroups.Clear();
                              gameObjectGroups.Clear();
                              ClearTokenIndexes();
                              ShowResultsClearedMessage();
                              // Mark that the next search needs a fresh rebuild.
                              pendingRebuild = true;
                              ClearSafeBase();
                              UpdateSelectionSummary();
                              if ( scanTimeLabel != null && scanTimeLabel.IsValid() )
                              {
                                      scanTimeLabel.Text = string.Empty;
                              }
                              UpdateManualModeHint();
                              searchBox.Focus();
                              UpdateProgressIcon();
                              return;
                      }
              }
      }

      private void ApplyFilterSafe( string rawText )
      {
              // Safe mode helper – decide whether this Enter press should define a
              // new base search or refine within the existing base stack.
              if ( searchTriggerMode != SearchTriggerMode.Safe )
              {
                      ApplyFilter( rawText );
                      return;
              }

              rawText ??= string.Empty;
              ParseSearchTerms( rawText, out var includeTerms, out _, out _ );
              var term = includeTerms.Length > 0 ? includeTerms[0] : string.Empty;

              var hasBase = safeBaseActive && !string.IsNullOrEmpty( safeBaseSearchText );
              var refineWithinBase = false;

              // First non-empty Safe-mode Enter sets the base term immediately so the
              // chip appears before the search finishes populating results.
              if ( !hasBase && !string.IsNullOrEmpty( term ) )
              {
                      safeBaseActive = true;
                      safeBaseSearchText = rawText;
                      safeBaseRowIds.Clear();
                      UpdateSafeBaseLabel();
                      UpdateSearchPlaceholder();
                      refineWithinBase = false;

                      // Move the base term into the chip and clear the text field so we
                      // don't duplicate the word visually.
                      if ( searchBox != null )
                      {
                              suppressSearchChanged = true;
                              searchBox.Text = string.Empty;
                              suppressSearchChanged = false;
                      }
              }
              else if ( hasBase && string.IsNullOrEmpty( term ) )
              {
                      // Empty term with an active base should re-apply the base search
                      // rather than clearing it or treating it as a global empty search.
                      ApplyFilter( safeBaseSearchText, refineWithinBase: false );
                      UpdateSearchPlaceholder();
                      return;
              }
              else
              {
                      refineWithinBase = hasBase && !string.IsNullOrEmpty( term );
              }

              ApplyFilter( rawText, refineWithinBase );
      }

      private void UpdateSafeBaseLabel()
      {
              if ( safeBaseLabel == null )
                      return;

              if ( searchTriggerMode == SearchTriggerMode.Safe && safeBaseActive && !string.IsNullOrEmpty( safeBaseSearchText ) )
              {
                      safeBaseLabel.Visible = true;
                      safeBaseLabel.Text = $"\"{safeBaseSearchText}\":";
              }
              else
              {
                      safeBaseLabel.Visible = false;
                      safeBaseLabel.Text = string.Empty;
              }
      }

      private RowFilterState RefreshSafeBaseMatches()
      {
              if ( !safeBaseActive || string.IsNullOrEmpty( safeBaseSearchText ) )
                      return null;

              ParseSearchTerms( safeBaseSearchText, out var includeTerms, out var excludeTerms, out var filters );
              var term = includeTerms.Length > 0 ? includeTerms[0] : string.Empty;
              var termEmpty = string.IsNullOrEmpty( term );

              var filterEngine = new FilterEngine( this );
              var filterResult = filterEngine.Run( rowModels, includeTerms, excludeTerms, filters, termEmpty, CancellationToken.None );

              safeBaseRowIds.Clear();
              foreach ( var model in rowModels )
              {
                      if ( model == null )
                              continue;

                      if ( filterResult.RowStates.TryGetValue( model.Id, out var state ) && state.Match )
                      {
                              safeBaseRowIds.Add( model.Id );
                      }
              }

              safeBaseActive = safeBaseRowIds.Count > 0;
              UpdateSafeBaseLabel();
              return filterResult;
      }

      private void ClearSafeBase()
      {
              if ( !safeBaseActive )
                      return;

              safeBaseActive = false;
              safeBaseSearchText = string.Empty;
              safeBaseRowIds.Clear();
              UpdateSafeBaseLabel();
              UpdateSearchPlaceholder();

              // When the base is dropped while results are visible, keep the stack
              // unchanged. If there are no results at all, fall back to the standard
              // selection hint so the user knows how to search again.
              if ( rows.Count == 0 && string.IsNullOrEmpty( lastSearch ) )
              {
                      ShowSelectedHint();
              }
      }

      private void ApplyFilter( string rawText, bool refineWithinBase = false )
      {
              if ( isDisposed )
                      return;

              if ( rebuildCancellation != null && componentBuildQueue.HasPending )
                      return;

              filterInProgress = true;
              if ( progressIcon != null )
              {
                      progressIcon.Visible = true;
                      progressIcon.Enabled = true;
                      progressIcon.ToolTip = "Applying filter…";
              }

              var filterStopwatch = Stopwatch.StartNew();
              rawText ??= string.Empty;

              if ( rawText == lastSearch && !pendingRebuild && !rowsDirty && !string.IsNullOrEmpty( rawText ) )
              {
                      filterInProgress = false;
                      UpdateProgressIcon();
                      return;
              }

              lastSearch = rawText;

              if ( pendingRebuild )
              {
                      pendingRebuild = false;
                      RebuildRows( applyFilter: false );
              }

              ParseSearchTerms( lastSearch, out var includeTerms, out var excludeTerms, out var filters );
              activeIncludeTerms = includeTerms ?? Array.Empty<string>();
              activeExcludeTerms = excludeTerms ?? Array.Empty<string>();
              activeQueryFilters = filters;
              var termEmpty = includeTerms.Length == 0 || string.IsNullOrEmpty( includeTerms[0] );

              if ( !termEmpty && noResultsContainer != null && noResultsLabel != null )
              {
                      var searchingTerm = lastSearch
                              .ToLowerInvariant()
                              .Split( ' ', StringSplitOptions.RemoveEmptyEntries )
                              .FirstOrDefault() ?? lastSearch;
                      noResultsLabel.Text = $"Searching \"{searchingTerm}\"...";
                      noResultsContainer.Visible = true;
              }

              foreach ( var cg in componentGroups )
              {
                      cg.MatchCount = 0;
                      cg.Group.Title = cg.Name;
              }
              foreach ( var og in gameObjectGroups )
              {
                      og.MatchCount = 0;
                      og.Group.Title = og.Name;
                      og.NameMatch = false;
              }

              if ( termEmpty && noResultsContainer != null )
              {
                      noResultsContainer.Visible = false;
              }

              var snapshotKey = lastSearch ?? string.Empty;
              RowFilterState cachedState = null;
              var canUseSnapshot = !pendingRebuild && !refineWithinBase && filterSnapshotCache.TryGetValue( snapshotKey, out cachedState );

              var candidates = GetFilterCandidates( includeTerms, filters, termEmpty, refineWithinBase ).ToList();

              if ( canUseSnapshot )
              {
                      ApplyFilterResultToUi( cachedState, refineWithinBase, termEmpty, snapshotKey, includeTerms, filterStopwatch );
                      return;
              }

              StartFilterTask( candidates, includeTerms, excludeTerms, filters, termEmpty, filterStopwatch, refineWithinBase, snapshotKey );
      }

      private void StartFilterTask( List<RowModel> candidates, string[] includeTerms, string[] excludeTerms, QueryFilters filters, bool termEmpty, Stopwatch filterStopwatch, bool refineWithinBase, string snapshotKey )
      {
              filterCancellation?.Cancel();
              filterCancellation?.Dispose();

              var cts = new CancellationTokenSource();
              filterCancellation = cts;
              var token = cts.Token;
              var sequence = ++filterSequence;

              var filterEngine = new FilterEngine( this );

              Task.Run( () =>
              {
                      try
                      {
                              return filterEngine.Run( candidates, includeTerms, excludeTerms, filters, termEmpty, token );
                      }
                      catch ( OperationCanceledException )
                      {
                              return null;
                      }
                      catch ( Exception e )
                      {
                              Log.Warning( $"[SearchFilter] Filter task failed: {e.Message}" );
                              return null;
                      }
              }, token ).ContinueWith( t =>
              {
                      if ( token.IsCancellationRequested || sequence != filterSequence || isDisposed )
                              return;

                      var filterResult = t.Result;
                      if ( filterResult == null )
                              return;

                      MainThread.Queue( () =>
                      {
                              if ( token.IsCancellationRequested || sequence != filterSequence || isDisposed )
                                      return;

                              ApplyFilterResultToUi( filterResult, refineWithinBase, termEmpty, snapshotKey, includeTerms, filterStopwatch );
                      } );
              }, TaskScheduler.Default );
      }

      private void ApplyFilterResultToUi( RowFilterState filterResult, bool refineWithinBase, bool termEmpty, string snapshotKey, string[] includeTerms, Stopwatch filterStopwatch )
      {
              if ( filterResult == null )
                      return;

              var term = includeTerms.Length > 0 ? includeTerms[0] : string.Empty;

              var objectsWithPropertyMatches = new HashSet<string>();
              foreach ( var model in rowModels )
              {
                      if ( model == null || string.IsNullOrEmpty( model.GameObjectKey ) )
                              continue;

                      if ( filterResult.RowStates.TryGetValue( model.Id, out var state ) && state.Match )
                      {
                              objectsWithPropertyMatches.Add( model.GameObjectKey );
                      }
              }

              int visibleMatchCount = 0;
              bool anyVisibleRows = false;

              foreach ( var row in rows )
              {
                      var inBase = !refineWithinBase || (safeBaseActive && safeBaseRowIds.Contains( row.RowId ));
                      RowMatchResult state;
                      if ( inBase && filterResult.RowStates.TryGetValue( row.RowId, out var s ) )
                      {
                              state = s;
                      }
                      else if ( refineWithinBase )
                      {
                              state = new RowMatchResult { PassesFilters = false };
                      }
                      else
                      {
                              state = new RowMatchResult { PassesFilters = true };
                      }

                      var passes = state.PassesFilters;
                      var match = !termEmpty && state.Match;
                      var nameMatch = !termEmpty && state.NameMatch;
                      var valueMatch = !termEmpty && state.ValueMatch;
                      var objectNameMatch = !termEmpty && state.ObjectNameMatch;

                      bool visible;
                      if ( !passes )
                      {
                              visible = false;
                      }
                      else if ( termEmpty )
                      {
                              if ( refineWithinBase && safeBaseActive && safeBaseRowIds.Count > 0 )
                              {
                                      visible = inBase;
                              }
                              else
                              {
                                      visible = true;
                              }
                      }
                      else if ( leanMode )
                      {
                              visible = match;
                      }
                      else
                      {
                              visible = match;
                      }

                      if ( row.DisplayName != null )
                      {
                              if ( termEmpty )
                              {
                                      row.NameLabel.Text = row.DisplayName;
                              }
                              else if ( nameMatch )
                              {
                                      row.NameLabel.Text = HighlightMatches( row.DisplayName, includeTerms );
                              }
                              else
                              {
                                      row.NameLabel.Text = row.DisplayName;
                              }
                      }

                      if ( row.Widget != null && visible != row.IsVisible )
                      {
                              row.Widget.Visible = visible;
                              row.IsVisible = visible;
                      }

                              var nameHighlight = !termEmpty && nameMatch;
                              if ( nameHighlight != row.NameHighlighted )
                              {
                                      row.SetHighlighted( nameHighlight );
                                      row.NameHighlighted = nameHighlight;
                              }

                              if ( row.NameMatchDot != null && nameMatch != row.NameDotOn )
                              {
                                      row.NameMatchDot.Text = nameMatch ? "●" : string.Empty;
                                      row.NameMatchDot.Visible = nameMatch;
                                      row.NameDotOn = nameMatch;
                              }

                      if ( row.ValueMatchDot != null && valueMatch != row.ValueDotOn )
                      {
                              row.ValueMatchDot.Text = valueMatch ? "●" : string.Empty;
                              row.ValueMatchDot.Visible = valueMatch;
                              row.ValueDotOn = valueMatch;
                      }

                      if ( row.HitLabel != null )
                      {
                              row.HitLabel.Text = match ? "●" : string.Empty;
                              row.HitLabel.Visible = match;
                      }

                              if ( row.EditorWidget != null )
                              {
                                      if ( valueMatch != row.ValueHighlighted )
                                      {
                                              var baseStyle = row.EditorBaseStyle ?? string.Empty;
                                              if ( valueMatch )
                                              {
                                                      row.EditorWidget.SetStyles( $"{baseStyle} color: rgba(140,220,185,1.0);" );
                                              }
                                              else
                                              {
                                                      row.EditorWidget.SetStyles( baseStyle );
                                              }

                                              row.ValueHighlighted = valueMatch;
                                      }

                                      if ( row.EditorWidget is Label valueLabel )
                                      {
                                              if ( termEmpty )
                                              {
                                                      valueLabel.Text = row.ValuePreview ?? string.Empty;
                                              }
                                              else if ( valueMatch )
                                              {
                                                      valueLabel.Text = HighlightMatches( row.ValuePreview ?? string.Empty, includeTerms );
                                              }
                                              else
                                              {
                                                      valueLabel.Text = row.ValuePreview ?? string.Empty;
                                              }
                                      }
                              }

                      if ( visible )
                      {
                              anyVisibleRows = true;
                              if ( match )
                              {
                                      visibleMatchCount++;
                              }
                      }
              }

              if ( noResultsContainer != null && noResultsLabel != null )
              {
                      if ( termEmpty )
                      {
                              noResultsContainer.Visible = false;
                      }
                      else if ( !anyVisibleRows )
                      {
                              var searchingTerm = lastSearch
                                      .ToLowerInvariant()
                                      .Split( ' ', StringSplitOptions.RemoveEmptyEntries )
                                      .FirstOrDefault() ?? lastSearch;
                              if ( filterInProgress || componentBuildQueue.HasPending || filterDebouncePending )
                              {
                              noResultsLabel.Text = $"Searching \"{searchingTerm}\"...";
                              }
                              else
                              {
                                      noResultsLabel.Text = $"No results for \"{searchingTerm}\" :(";
                              }
                              noResultsContainer.Visible = true;
                      }
                      else
                      {
                              noResultsContainer.Visible = false;
                      }
              }

              if ( !(searchTriggerMode == SearchTriggerMode.Live && rows.Count > HeavyRowCountThreshold) )
              {
                      var zebraIndex = 0;
                      foreach ( var row in rows.Where( r => r.Widget != null && r.Widget.Visible ) )
                      {
                              var baseShade = zebraIndex % 2 == 0 ? "rgba(255,255,255,0.02)" : "rgba(255,255,255,0.04)";
                              var highlightShade = (row.ValueHighlighted || row.NameHighlighted) ? "rgba(110,210,175,0.10)" : baseShade;
                              row.Widget.SetStyles( $"background-color: {highlightShade}; padding: 2px 4px;" );
                              zebraIndex++;
                      }
              }

              if ( !leanMode )
              {
                      foreach ( var cg in componentGroups )
                      {
                              cg.MatchCount = filterResult.ComponentMatches.TryGetValue( cg.MatchKey ?? cg.OwnerKey, out var cCount ) ? cCount : 0;

                              if ( cg.MatchCount > 0 )
                              {
                                      cg.Group.Title = $"{cg.Name} ({cg.MatchCount})";
                                      cg.Group.SetStyles( "color: rgba(255,255,255,1.0);" );
                                      cg.Group.Visible = true;
                                      if ( termEmpty )
                                      {
                                              cg.Group.SetOpenState( true );
                                      }
                                      else
                                      {
                                              cg.Group.SetOpenState( false );
                                      }
                                      cg.Group.SetHeight();
                              }
                              else
                              {
                                      cg.Group.Visible = termEmpty;
                              }
                      }

                      var isSearchingTerm = !termEmpty && (rebuildCancellation != null || componentBuildQueue.HasPending || filterDebouncePending || filterInProgress);
                      var selectedKey = target != null && target.IsValid() ? target.Id.ToString() : null;

                      foreach ( var og in gameObjectGroups )
                      {
                              og.MatchCount = filterResult.ObjectMatches.TryGetValue( og.Key, out var gCount ) ? gCount : 0;
                              og.NameMatch = !termEmpty && og.MatchCount > 0;

                              if ( og.MatchCount > 0 || termEmpty )
                              {
                                      var titleBase = og.MatchCount > 0
                                              ? $"{og.Name} ({og.MatchCount})"
                                              : og.Name;

                                      og.Group.Title = titleBase;

                                      var color = og.NameMatch ? "color: rgba(130,220,185,1.0);" : "color: rgba(255,255,255,1.0);";
                                      var weight = og.NameMatch ? "font-weight: 600;" : "font-weight: 400;";
                                      og.Group.SetStyles( $"{color} {weight}" );

                                      if ( og.HitLabel != null )
                                      {
                                              if ( isSearchingTerm && !string.IsNullOrEmpty( selectedKey ) && og.Key == selectedKey )
                                              {
                                                      og.HitLabel.Text = "⟳";
                                                      og.HitLabel.Visible = true;
                                              }
                                              else if ( og.NameMatch )
                                              {
                                                      og.HitLabel.Text = "●";
                                                      og.HitLabel.Visible = true;
                                              }
                                              else
                                              {
                                                      og.HitLabel.Text = string.Empty;
                                                      og.HitLabel.Visible = false;
                                              }
                                      }

                                      if ( og.Row != null && og.NameMatch )
                                      {
                                              og.Row.SetStyles( "background-color: rgba(110,210,175,0.08); border-radius: 4px; margin: 2px 0; padding: 2px 2px 2px 0;" );
                                      }

                                      if ( og.Row != null )
                                              og.Row.Visible = true;
                                      if ( termEmpty )
                                      {
                                              og.Group.SetOpenState( true );
                                      }
                                      else
                                      {
                                              og.Group.SetOpenState( false );
                                      }
                                      og.Group.SetHeight();
                              }
                              else
                              {
                                      og.Group.Title = $"{og.Name} (0)";
                                      og.Group.SetStyles( "color: rgba(255,255,255,0.4);" );
                                      if ( og.HitLabel != null )
                                      {
                                              og.HitLabel.Text = string.Empty;
                                              og.HitLabel.Visible = false;
                                      }
                                      if ( og.Row != null )
                                              og.Row.Visible = false;
                              }
                      }
              }
              else
              {
                      foreach ( var cg in componentGroups )
                      {
                              var hasVisibleRows = cg.Rows.Any( r => r.Widget != null && r.Widget.Visible );
                              if ( cg.Group != null )
                              {
                                      cg.Group.Visible = hasVisibleRows;
                                      if ( hasVisibleRows )
                                      {
                                              cg.Group.Title = cg.Name;
                                              cg.Group.Icon = string.Empty;
                                              cg.Group.SetOpenState( true );
                                              cg.Group.SetStyles( "color: rgba(255,255,255,0.9); font-weight: 500; font-size: 11px; margin: 0; padding: 0;" );
                                              cg.Group.SetHeight();
                                      }
                              }
                      }

                      foreach ( var og in gameObjectGroups )
                      {
                              var hasVisibleRows = componentGroups
                                      .Where( cg => cg.OwnerKey == og.Key )
                                      .Any( cg => cg.Rows.Any( r => r.Widget != null && r.Widget.Visible ) );

                              if ( og.Row != null )
                                      og.Row.Visible = hasVisibleRows;

                              if ( og.Group != null )
                              {
                                      og.Group.Visible = hasVisibleRows;
                                      if ( hasVisibleRows )
                                      {
                                              og.Group.Title = og.Name;
                                              og.Group.Icon = string.Empty;
                                              og.Group.SetOpenState( true );
                                              og.Group.SetStyles( "color: rgba(255,255,255,0.9); font-weight: 600; font-size: 11px; margin: 2px 0 0 0; padding: 0;" );
                                              og.Group.SetHeight();
                                      }
                              }

                              if ( og.HitLabel != null )
                              {
                                      og.HitLabel.Text = string.Empty;
                                      og.HitLabel.Visible = false;
                              }
                      }
              }

              if ( string.IsNullOrEmpty( term ) )
              {
                      matchLabel.Text = rows.Count.ToString();
                      matchLabel.SetStyles( "color: rgba(255,255,255,0.4);" );
              }
              else
              {
                      matchLabel.Text = visibleMatchCount.ToString();
                      matchLabel.SetStyles( visibleMatchCount > 0
                              ? "color: rgba(150,230,190,0.95); font-weight: 700;"
                              : "color: rgba(255,255,255,0.3);" );
              }

              if ( filterStopwatch != null )
              {
                      filterStopwatch.Stop();
                      if ( scanTimeLabel != null )
                      {
                              scanTimeLabel.Text = $"{filterStopwatch.ElapsedMilliseconds} ms";
                      }
              }

              if ( !termEmpty )
              {
                      if ( !anyVisibleRows && noResultsContainer != null )
                      {
                              noResultsContainer.Visible = true;
                              noResultsLabel.Text = "No results :(";
                      }
                      else if ( noResultsContainer != null )
                      {
                              noResultsContainer.Visible = false;
                      }
              }
              else
              {
                      if ( !anyVisibleRows && noResultsContainer != null )
                      {
                              noResultsContainer.Visible = true;
                              noResultsLabel.Text = "Object selected. Enter a search to populate results.";
                      }
                      else if ( noResultsContainer != null )
                      {
                              noResultsContainer.Visible = false;
                      }
              }

              if ( searchTriggerMode == SearchTriggerMode.Safe && !refineWithinBase )
              {
                      if ( !string.IsNullOrEmpty( lastSearch ) && anyVisibleRows )
                      {
                              safeBaseRowIds.Clear();
                              foreach ( var row in rows )
                              {
                                      if ( row.Widget != null && row.Widget.Visible )
                                      {
                                              safeBaseRowIds.Add( row.RowId );
                                      }
                              }

                              safeBaseActive = safeBaseRowIds.Count > 0;
                              safeBaseSearchText = safeBaseActive ? lastSearch : string.Empty;
                      }
                      else
                      {
                              safeBaseActive = false;
                              safeBaseSearchText = string.Empty;
                              safeBaseRowIds.Clear();
                      }

                      UpdateSafeBaseLabel();
              }

              if ( !refineWithinBase && !termEmpty && rows.Count > 0 )
              {
                      if ( !filterSnapshotOrder.Contains( snapshotKey ) )
                      {
                              filterSnapshotOrder.Enqueue( snapshotKey );
                      }

                      while ( filterSnapshotOrder.Count > MaxFilterSnapshots )
                      {
                              var oldest = filterSnapshotOrder.Dequeue();
                              filterSnapshotCache.Remove( oldest );
                      }

                      filterSnapshotCache[snapshotKey] = filterResult;
              }

              filterInProgress = false;
              UpdateProgressIcon();

              rowsDirty = false;
              UpdateManualModeHint();
      }

      private void UpdateProgressIcon()
      {
              var scanning = rebuildCancellation != null || componentBuildQueue.HasPending || filterDebouncePending || filterInProgress;

              // Clear button doubles as interrupt; change icon/tooltip while search
              // is running.
              if ( clearButton != null )
              {
                      clearButton.Icon = scanning ? "cancel" : "close";
                      clearButton.ToolTip = scanning
                              ? "Action: interrupt search."
                              : "Action: clear or interrupt search.\nSafe mode: first click clears text, second click clears results.";
              }
      }

      /// <summary>
      /// Enumerate target GameObjects to inspect: selected only, or selected + all descendants.
      /// </summary>
      private IEnumerable<GameObject> EnumerateTargets()
      {
              foreach ( var go in EnumerateTargets( target, includeChildren ) )
                      yield return go;
      }

      private IEnumerable<GameObject> EnumerateTargets( GameObject root, bool includeDescendants )
      {
              if ( root == null || !root.IsValid() )
                      yield break;

              if ( !includeDescendants )
              {
                      yield return root;
                      yield break;
              }

              foreach ( var go in EnumerateWithChildren( root ) )
              {
                      yield return go;
              }
      }

      private IEnumerable<GameObject> EnumerateWithChildren( GameObject root )
      {
              if ( root == null || !root.IsValid() )
                      yield break;

              yield return root;

              foreach ( var child in root.Children )
              {
                      foreach ( var c in EnumerateWithChildren( child ) )
                              yield return c;
              }
      }

      /// <summary>
      /// Get the depth of a GameObject in its parent/child hierarchy (0 = root).
      /// </summary>
      private int GetDepth( GameObject go )
      {
              var depth = 0;
              var current = go?.Parent;
              while ( current != null && current.IsValid() )
              {
                      depth++;
                      current = current.Parent;
              }
              return depth;
      }

      private string GetHierarchyPath( GameObject go )
      {
              if ( go == null || !go.IsValid() )
                      return string.Empty;

              var stack = new Stack<string>();
              var current = go;
              while ( current != null && current.IsValid() )
              {
                      stack.Push( current.Name );
                      current = current.Parent;
              }

              var segments = stack.ToArray();
              if ( segments.Length == 0 )
                      return string.Empty;

              return string.Join( " / ", segments );
      }

      private enum PropertyKind
      {
              Bool,
              Numeric,
              Text,
              Enum,
              Complex
      }

	      private static PropertyKind GetPropertyKind( Type t )
	      {
	              if ( t == null ) return PropertyKind.Complex;

	              // Treat Nullable<T> like its underlying type for kind checks so that
	              // bool? aligns with bool and numeric? aligns with numeric.
	              var underlying = Nullable.GetUnderlyingType( t ) ?? t;

	              if ( underlying == typeof( bool ) ) return PropertyKind.Bool;
	              if ( underlying == typeof( string ) ) return PropertyKind.Text;
	              if ( underlying.IsEnum ) return PropertyKind.Enum;
	
	              if ( underlying == typeof( int ) || underlying == typeof( float ) || underlying == typeof( double ) ||
	                   underlying == typeof( long ) || underlying == typeof( short ) ||
	                   underlying == typeof( uint ) || underlying == typeof( ulong ) )
	                      return PropertyKind.Numeric;
	
	                      return PropertyKind.Complex;
	      }

      private bool ShouldIncludeKind( PropertyKind kind )
      {
              return kind switch
              {
                      PropertyKind.Bool => showBools,
                      PropertyKind.Numeric => showNumeric,
                      PropertyKind.Text => showText,
                      PropertyKind.Enum => showEnums,
                      PropertyKind.Complex => showComplex,
                      _ => true
              };
      }

      /// <summary>
      /// Surface a small set of key option flags (currently Bloom/BloomLayer) as
      /// first-class rows so they are searchable even when the more expensive
      /// nested-bool flattening is disabled.
      /// </summary>
      private void TryAddBloomOptionRow( object container, string displayPrefix, ComponentGroup groupInfo, Component component, Widget groupBody, string componentTypeName )
      {
              if ( container == null || groupInfo == null || groupBody == null )
                      return;

              if ( string.IsNullOrWhiteSpace( lastSearch ) )
                      return;

              var nestedType = container.GetType();

              if ( debugLogging )
              {
                      var key = nestedType.FullName ?? nestedType.Name ?? "UnknownNestedType";
                      if ( debugNestedTypeNames.Add( key ) )
                      {
                              Log.Info( $"[SearchFilter] Nested options type seen: {key} (showHidden={showHiddenNestedBools})" );
                      }
              }

              // Look for a public bool called BloomLayer (as serialized in prefabs).
              var bloomProp = nestedType.GetProperty( "BloomLayer", BindingFlags.Instance | BindingFlags.Public );
              if ( bloomProp == null || bloomProp.PropertyType != typeof( bool ) )
                      return;

              bool bloomValue;
              try
              {
                      bloomValue = (bool)(bloomProp.GetValue( container ) ?? false);
              }
              catch
              {
                      return;
              }

              var memberName = "Bloom";
              var nestedDisplayName = string.IsNullOrEmpty( displayPrefix )
                      ? memberName
                      : $"{displayPrefix}.{memberName}";
              var nestedValueText = bloomValue ? "true" : "false";
              if ( !ShouldIncludeRowForSearch( nestedDisplayName, "bool", nestedValueText ) )
                      return;

              var nestedNameLabel = new Label( nestedDisplayName )
              {
                      Alignment = TextFlag.LeftCenter
              };

              var nestedRowWidget = new Widget( groupBody );
              var nestedRowLayout = nestedRowWidget.Layout = Layout.Row();
              nestedRowLayout.Spacing = 2;

              var nestedHitLabel = AddMatchIndicator( nestedRowWidget, nestedRowLayout );

              var nestedNameContainer = new Widget( nestedRowWidget );
              var nestedNameLayout = nestedNameContainer.Layout = Layout.Row();
              nestedNameLayout.Spacing = 0;
              nestedNameLabel.SetStyles( "margin:0; padding:0;" );
              nestedNameLayout.Add( nestedNameLabel, 0 );
              nestedNameContainer.FixedWidth = 180;
              nestedRowLayout.Add( nestedNameContainer, 0 );

              var nestedValueLabel = new Label( nestedValueText )
              {
                      FixedHeight = Theme.RowHeight * 0.9f,
                      Alignment = TextFlag.LeftCenter
              };
              nestedValueLabel.SetStyles( "background-color: rgba(255,190,120,0.08);" );
              nestedRowLayout.Add( nestedValueLabel, 1 );

              var nestedTooltip =
                      $"Type: bool\nComponent: {componentTypeName}\nProperty: {nestedDisplayName}\nValue: {nestedValueText}";
              nestedValueLabel.ToolTip = nestedTooltip;
              nestedNameLabel.ToolTip = nestedTooltip;

              nestedRowWidget.SetStyles( "margin: 2px 0; padding: 2px 4px; border-radius: 3px;" );
              groupBody.Layout.Add( nestedRowWidget );

              var nestedRow = new PropertyRow
              {
                      Widget = nestedRowWidget,
                      NameLabel = nestedNameLabel,
                      EditorWidget = nestedValueLabel,
                      Component = component,
                      PropertyInfo = null, // synthetic, read-only row
                      Name = memberName,
                      DisplayName = nestedDisplayName,
                      ComponentName = groupInfo.Name,
                      GameObjectName = groupInfo.OwnerName,
                      ValuePreview = nestedValueText,
                      HitLabel = nestedHitLabel,
                      IsSynthetic = true,
                      IsVisible = nestedRowWidget.Visible
              };

              RegisterRowModel( nestedRow, groupInfo, memberName, nestedDisplayName, nestedValueText, PropertyKind.Bool, isSynthetic: true );
              rows.Add( nestedRow );
              rowsDirty = true;
              groupInfo.Rows.Add( nestedRow );
      }

      /// <summary>
      /// Recursively flatten simple boolean members inside a complex container so that
      /// nested options like RenderOptions.Bloom or RenderOptions.BloomLayer can be
      /// searched directly. This only runs for a small allowlisted set of option
      /// structs (e.g. RenderOptions) and walks public members with a shallow depth
      /// limit to avoid runaway recursion and huge result counts.
      /// </summary>
      private void FlattenNestedBoolMembers( object container, string displayPrefix, ComponentGroup groupInfo, Component component, Widget groupBody, string componentTypeName, int depth = 0 )
      {
              if ( container == null || groupInfo == null || groupBody == null )
                      return;

              if ( string.IsNullOrWhiteSpace( lastSearch ) )
                      return;

              if ( !showHiddenNestedBools )
                      return;

              // Keep recursion shallow; most engine option structs are only 1–2 levels deep.
              if ( depth > 3 )
                      return;

              var nestedType = container.GetType();
              var nestedTypeName = nestedType.Name;
              var nestedFullName = nestedType.FullName ?? nestedTypeName ?? string.Empty;

              // Only flatten for a small set of known option/helper types so
              // we avoid exploding the row count on arbitrary complex fields.
              var allowNested =
                      nestedFullName.Contains( "RenderOptions", StringComparison.OrdinalIgnoreCase ) ||
                      nestedFullName.Contains( "SequenceAccessor", StringComparison.OrdinalIgnoreCase );
              if ( !allowNested )
                      return;

              // Skip primitives and strings – nothing nested to walk.
              if ( nestedType.IsPrimitive || nestedType == typeof( string ) )
                      return;

              void AddNestedBoolRow( string memberName, bool value )
              {
                      var nestedDisplayName = string.IsNullOrEmpty( displayPrefix )
                              ? memberName
                              : $"{displayPrefix}.{memberName}";
                      var nestedValueText = value ? "true" : "false";
                      if ( !ShouldIncludeRowForSearch( nestedDisplayName, "bool", nestedValueText ) )
                              return;

                      var nestedNameLabel = new Label( nestedDisplayName )
                      {
                              Alignment = TextFlag.LeftCenter
                      };

                      var nestedRowWidget = new Widget( groupBody );
                      var nestedRowLayout = nestedRowWidget.Layout = Layout.Row();
                      nestedRowLayout.Spacing = 2;

                      var nestedHitLabel = AddMatchIndicator( nestedRowWidget, nestedRowLayout );

                      var nestedNameContainer = new Widget( nestedRowWidget );
                      var nestedNameLayout = nestedNameContainer.Layout = Layout.Row();
                      nestedNameLayout.Spacing = 0;
                      nestedNameLabel.SetStyles( "margin:0; padding:0;" );
                      nestedNameLayout.Add( nestedNameLabel, 0 );
                      nestedNameContainer.FixedWidth = 180;
                      nestedRowLayout.Add( nestedNameContainer, 0 );

                      var nestedValueLabel = new Label( nestedValueText )
                      {
                              FixedHeight = Theme.RowHeight * 0.8f,
                              Alignment = TextFlag.LeftCenter
                      };
                      var nestedBaseStyle = "background-color: rgba(255,255,255,0.08); padding: 0 3px;";
                      nestedValueLabel.SetStyles( nestedBaseStyle );
                      nestedRowLayout.Add( nestedValueLabel, 1 );

                      var nestedTooltip =
                              $"Type: bool\nComponent: {componentTypeName}\nProperty: {nestedDisplayName}\nValue: {nestedValueText}";
                      nestedValueLabel.ToolTip = nestedTooltip;
                      nestedNameLabel.ToolTip = nestedTooltip;

                      nestedRowWidget.SetStyles( "margin: 2px 0; padding: 2px 4px; border-radius: 3px;" );
                      groupBody.Layout.Add( nestedRowWidget );

                      var nestedRow = new PropertyRow
                      {
                              Widget = nestedRowWidget,
                              NameLabel = nestedNameLabel,
                              EditorWidget = nestedValueLabel,
                              Component = component,
                              PropertyInfo = null, // nested bools are read-only here
                              Name = memberName,
                              DisplayName = nestedDisplayName,
                              ComponentName = groupInfo.Name,
                              GameObjectName = groupInfo.OwnerName,
                              ValuePreview = nestedValueText,
                              HitLabel = nestedHitLabel
                      };

              nestedRow.EditorBaseStyle = nestedBaseStyle;

                      RegisterRowModel( nestedRow, groupInfo, memberName, nestedDisplayName, nestedValueText, PropertyKind.Bool, isSynthetic: true );
                      rows.Add( nestedRow );
                      rowsDirty = true;
                      groupInfo.Rows.Add( nestedRow );
              }

              // Properties (public only – the prefab serializer works from these).
              foreach ( var nestedProp in nestedType.GetProperties( BindingFlags.Instance | BindingFlags.Public ) )
              {
                      var memberType = nestedProp.PropertyType;

                      object nestedObj;
                      try
                      {
                              nestedObj = nestedProp.GetValue( container );
                      }
                      catch
                      {
                              continue;
                      }

                      if ( memberType == typeof( bool ) )
                      {
                              var boolValue = nestedObj is bool b && b;
                              AddNestedBoolRow( nestedProp.Name, boolValue );
                      }
                      else if ( !memberType.IsPrimitive && memberType != typeof( string ) && nestedObj != null )
                      {
                              FlattenNestedBoolMembers( nestedObj, string.IsNullOrEmpty( displayPrefix ) ? nestedProp.Name : $"{displayPrefix}.{nestedProp.Name}", groupInfo, component, groupBody, componentTypeName, depth + 1 );
                      }
              }

              // Fields (public only – the prefab serializer works from these).
              foreach ( var nestedField in nestedType.GetFields( BindingFlags.Instance | BindingFlags.Public ) )
              {
                      var memberType = nestedField.FieldType;

                      object nestedObj;
                      try
                      {
                              nestedObj = nestedField.GetValue( container );
                      }
                      catch
                      {
                              continue;
                      }

                      if ( memberType == typeof( bool ) )
                      {
                              var boolValue = nestedObj is bool b && b;
                              AddNestedBoolRow( nestedField.Name, boolValue );
                      }
                      else if ( !memberType.IsPrimitive && memberType != typeof( string ) && nestedObj != null )
                      {
                              FlattenNestedBoolMembers( nestedObj, string.IsNullOrEmpty( displayPrefix ) ? nestedField.Name: $"{displayPrefix}.{nestedField.Name}", groupInfo, component, groupBody, componentTypeName, depth + 1 );
                      }
              }
      }

      /// <summary>
      /// Flatten simple list-like containers (e.g. List&lt;string&gt; or List&lt;enum&gt;) into
      /// individual rows so common patterns like \"Hide body groups\" can be searched
      /// by value (Head, Torso, ArmUpper, etc.).
      /// </summary>
      private void FlattenListEntries( object container, string displayPrefix, ComponentGroup groupInfo, Component component, Widget groupBody, string componentTypeName )
      {
              if ( string.IsNullOrWhiteSpace( lastSearch ) )
                      return;

              if ( !flattenLists )
                      return;

              if ( container is not IList list || list.Count == 0 || groupInfo == null || groupBody == null )
                      return;

              // Avoid exploding the UI on very large lists.
              const int MaxListEntries = 64;
              var count = Math.Min( list.Count, MaxListEntries );

              var listType = container.GetType();
              var elementType = listType.IsGenericType ? listType.GetGenericArguments()[0] : typeof( object );

              // Only flatten simple value-like entries (strings, enums, primitives and a couple of small structs).
              bool IsSimpleElementType( Type t )
              {
                      if ( t == null ) return false;
                      if ( t.IsEnum ) return true;
                      if ( t == typeof( string ) ) return true;
                      if ( t.IsPrimitive ) return true;
                      if ( t == typeof( Vector3 ) || t == typeof( Rotation ) ) return true;
                      return false;
              }

              if ( !IsSimpleElementType( elementType ) )
                      return;

              for ( int i = 0; i < count; i++ )
              {
                      var item = list[i];
                      if ( item == null )
                              continue;

                      var name = item.ToString() ?? string.Empty;
                      if ( string.IsNullOrEmpty( name ) )
                              continue;

                      var entryDisplay = string.IsNullOrEmpty( displayPrefix )
                              ? name
                              : $"{displayPrefix}.{name}";
                      var entryValue = name;
                      if ( !ShouldIncludeRowForSearch( entryDisplay, elementType?.Name ?? "List", entryValue ) )
                              continue;

              var entryRowWidget = new Widget( groupBody );
              var entryLayout = entryRowWidget.Layout = Layout.Row();
              entryLayout.Spacing = 2;

                      var entryHitLabel = AddMatchIndicator( entryRowWidget, entryLayout );

                      var entryNameContainer = new Widget( entryRowWidget );
                      var entryNameLayout = entryNameContainer.Layout = Layout.Row();
                      entryNameLayout.Spacing = 0;
                      var entryNameLabel = new Label( entryDisplay )
                      {
                              Alignment = TextFlag.LeftCenter
                      };
                      entryNameLabel.SetStyles( "margin:0; padding:0;" );
                      entryNameLayout.Add( entryNameLabel, 0 );
                      entryNameContainer.FixedWidth = 180;
                      entryLayout.Add( entryNameContainer, 0 );

                      var entryWidget = new Label( entryValue )
                      {
                              FixedHeight = Theme.RowHeight * 0.8f,
                              Alignment = TextFlag.LeftCenter
                      };
                      var entryBaseStyle = "background-color: rgba(255,255,255,0.08); padding: 0 3px;";
                      entryWidget.SetStyles( entryBaseStyle );
                      entryLayout.Add( entryWidget, 1 );

                      var entryTooltip =
                              $"Type: List item\nComponent: {componentTypeName}\nProperty: {entryDisplay}\nValue: {entryValue}";
                      entryWidget.ToolTip = entryTooltip;
                      entryNameLabel.ToolTip = entryTooltip;

                      entryRowWidget.SetStyles( "margin: 2px 0; padding: 2px 4px; border-radius: 3px;" );
                      groupBody.Layout.Add( entryRowWidget );

                      var row = new PropertyRow
                      {
                              Widget = entryRowWidget,
                              NameLabel = entryNameLabel,
                              EditorWidget = entryWidget,
                              Component = component,
                              PropertyInfo = null,
                              Name = entryDisplay,
                              DisplayName = entryDisplay,
                              ComponentName = groupInfo.Name,
                              GameObjectName = groupInfo.OwnerName,
                              ValuePreview = entryValue,
                              HitLabel = entryHitLabel,
                              EditorBaseStyle = entryBaseStyle,
                              IsSynthetic = true,
                              IsVisible = entryRowWidget.Visible
                      };

                      RegisterRowModel( row, groupInfo, row.Name, entryDisplay, entryValue, PropertyKind.Complex, isSynthetic: true );
                      rows.Add( row );
                      rowsDirty = true;
                      groupInfo.Rows.Add( row );
              }
      }

      /// <summary>
      /// Flatten simple option fields on small helper structs such as PhysicsLock so
      /// values like Pitch/Yaw can be searched directly without exploding row counts.
      /// </summary>
      private void FlattenPhysicsLockOptions( PhysicsLock physicsLock, string displayPrefix, ComponentGroup groupInfo, Component component, Widget groupBody, string componentTypeName )
      {
              if ( groupInfo == null || groupBody == null )
                      return;

              var type = physicsLock.GetType();

              bool IsSimple( Type t )
              {
                      if ( t == null ) return false;
                      if ( t.IsEnum ) return true;
                      if ( t == typeof( string ) ) return true;
                      if ( t.IsPrimitive ) return true;
                      if ( t == typeof( float ) || t == typeof( double ) ) return true;
                      return false;
              }

              void AddOptionRow( string memberName, string valueText )
              {
                      if ( string.IsNullOrEmpty( memberName ) )
                              return;

                      var entryDisplay = string.IsNullOrEmpty( displayPrefix )
                              ? memberName
                              : $"{displayPrefix}.{memberName}";
                      if ( !ShouldIncludeRowForSearch( entryDisplay, "PhysicsLock", valueText ) )
                              return;

                      var entryRowWidget = new Widget( groupBody );
                      var entryLayout = entryRowWidget.Layout = Layout.Row();
                      entryLayout.Spacing = 2;

                      var entryHitLabel = AddMatchIndicator( entryRowWidget, entryLayout );

                      var entryNameContainer = new Widget( entryRowWidget );
                      var entryNameLayout = entryNameContainer.Layout = Layout.Row();
                      entryNameLayout.Spacing = 0;
                      var entryNameLabel = new Label( entryDisplay )
                      {
                              Alignment = TextFlag.LeftCenter
                      };
                      entryNameLabel.SetStyles( "margin:0; padding:0;" );
                      entryNameLayout.Add( entryNameLabel, 0 );
                      entryNameContainer.FixedWidth = 180;
                      entryLayout.Add( entryNameContainer, 0 );

                      var entryWidget = new Label( valueText )
                      {
                              FixedHeight = Theme.RowHeight * 0.8f,
                              Alignment = TextFlag.LeftCenter
                      };
                      var entryBaseStyle = "background-color: rgba(255,255,255,0.08); padding: 0 3px;";
                      entryWidget.SetStyles( entryBaseStyle );
                      entryLayout.Add( entryWidget, 1 );

                      var entryTooltip =
                              $"Type: PhysicsLock\nComponent: {componentTypeName}\nProperty: {entryDisplay}\nValue: {valueText}";
                      entryWidget.ToolTip = entryTooltip;
                      entryNameLabel.ToolTip = entryTooltip;

                      entryRowWidget.SetStyles( "margin: 2px 0; padding: 2px 4px; border-radius: 3px;" );
                      groupBody.Layout.Add( entryRowWidget );

                      var row = new PropertyRow
                      {
                              Widget = entryRowWidget,
                              NameLabel = entryNameLabel,
                              EditorWidget = entryWidget,
                              Component = component,
                              PropertyInfo = null,
                              Name = entryDisplay,
                              DisplayName = entryDisplay,
                              ComponentName = groupInfo.Name,
                              GameObjectName = groupInfo.OwnerName,
                              ValuePreview = valueText,
                              HitLabel = entryHitLabel,
                              EditorBaseStyle = entryBaseStyle,
                              IsSynthetic = true,
                              IsVisible = entryRowWidget.Visible
                      };

                      RegisterRowModel( row, groupInfo, row.Name, entryDisplay, valueText, PropertyKind.Complex, isSynthetic: true );
                      rows.Add( row );
                      rowsDirty = true;
                      groupInfo.Rows.Add( row );
              }

              foreach ( var prop in type.GetProperties( BindingFlags.Instance | BindingFlags.Public ) )
              {
                      var t = prop.PropertyType;
                      if ( !IsSimple( t ) )
                              continue;

                      object v;
                      try
                      {
                              v = prop.GetValue( physicsLock );
                      }
                      catch
                      {
                              continue;
                      }

                      var valueText = v?.ToString() ?? string.Empty;
                      AddOptionRow( prop.Name, valueText );
              }

              foreach ( var field in type.GetFields( BindingFlags.Instance | BindingFlags.Public ) )
              {
                      var t = field.FieldType;
                      if ( !IsSimple( t ) )
                              continue;

                      object v;
                      try
                      {
                              v = field.GetValue( physicsLock );
                      }
                      catch
                      {
                              continue;
                      }

                      var valueText = v?.ToString() ?? string.Empty;
                      AddOptionRow( field.Name, valueText );
              }
      }

      private bool ShouldIncludeRowForSearch( string name, string typeName, string value )
      {
              if ( string.IsNullOrWhiteSpace( lastSearch ) )
                      return true;

              name ??= string.Empty;
              typeName ??= string.Empty;
              value ??= string.Empty;

              if ( activeExcludeTerms != null && activeExcludeTerms.Length > 0 )
              {
                      var blob = $"{name} {typeName} {value}";
                      for ( int i = 0; i < activeExcludeTerms.Length; i++ )
                      {
                              var term = activeExcludeTerms[i];
                              if ( string.IsNullOrEmpty( term ) )
                                      continue;
                              if ( SearchTokens.FuzzyContainsRaw( blob, term ) )
                                      return false;
                      }
              }

              if ( activeQueryFilters.NameTerms.Length > 0 &&
                   !activeQueryFilters.NameTerms.All( t => SearchTokens.FuzzyContainsRaw( name, t ) ) )
                      return false;

              if ( activeQueryFilters.TypeTerms.Length > 0 &&
                   !activeQueryFilters.TypeTerms.All( t => SearchTokens.FuzzyContainsRaw( typeName, t ) ) )
                      return false;

              if ( activeQueryFilters.ValueTerms.Length > 0 &&
                   !activeQueryFilters.ValueTerms.All( t => SearchTokens.FuzzyContainsRaw( value, t ) ) )
                      return false;

              if ( activeIncludeTerms == null || activeIncludeTerms.Length == 0 )
                      return true;

              var includeBlob = $"{name} {typeName} {value}";
              return activeIncludeTerms.All( t => SearchTokens.FuzzyContainsRaw( includeBlob, t ) );
      }

      private void FlattenSerializedPropertyChildren( SerializedProperty property, string displayPrefix, ComponentGroup groupInfo, Component component, Widget groupBody, string componentTypeName, int depth = 0 )
      {
              if ( !showHiddenNestedBools || property == null || groupInfo == null || groupBody == null )
                      return;

              if ( string.IsNullOrWhiteSpace( lastSearch ) )
                      return;

              if ( depth > MaxSerializedChildDepth )
                      return;

              SerializedObject nestedObject;
              try
              {
                      if ( !property.TryGetAsObject( out nestedObject ) )
                              return;
              }
              catch
              {
                      return;
              }

              var added = 0;
              foreach ( var childProp in nestedObject )
              {
                      if ( added >= MaxSerializedChildRows )
                              return;

                      if ( childProp.HasAttribute<HideAttribute>() )
                              continue;

                      var childType = childProp.PropertyType;
                      var childKind = GetPropertyKind( childType );
                      var childName = childProp.DisplayName ?? childProp.Name;
                      var childDisplay = string.IsNullOrEmpty( displayPrefix )
                              ? childName
                              : $"{displayPrefix}.{childName}";

                      if ( childKind == PropertyKind.Complex )
                      {
                              if ( depth < MaxSerializedChildDepth )
                              {
                                      FlattenSerializedPropertyChildren( childProp, childDisplay, groupInfo, component, groupBody, componentTypeName, depth + 1 );
                              }
                              continue;
                      }

                      string childValue = string.Empty;
                      try
                      {
                              var valueObj = childProp.GetValue<object>();
                              childValue = valueObj?.ToString() ?? string.Empty;
                      }
                      catch
                      {
                              childValue = string.Empty;
                      }
                      if ( !ShouldIncludeRowForSearch( childDisplay, childType?.Name ?? string.Empty, childValue ) )
                              continue;

                      var childRowWidget = new Widget( groupBody );
                      var childLayout = childRowWidget.Layout = Layout.Row();
                      childLayout.Spacing = 2;

                      var childHitLabel = AddMatchIndicator( childRowWidget, childLayout );

                      var childNameContainer = new Widget( childRowWidget );
                      var childNameLayout = childNameContainer.Layout = Layout.Row();
                      childNameLayout.Spacing = 0;
                      var childNameLabel = new Label( childDisplay )
                      {
                              Alignment = TextFlag.LeftCenter
                      };
                      childNameLabel.SetStyles( "margin:0; padding:0;" );
                      childNameLayout.Add( childNameLabel, 0 );
                      childNameContainer.FixedWidth = 180;
                      childLayout.Add( childNameContainer, 0 );

                      var childValueLabel = new Label( childValue )
                      {
                              FixedHeight = Theme.RowHeight * 0.8f,
                              Alignment = TextFlag.LeftCenter
                      };
                      var childBaseStyle = "background-color: rgba(255,255,255,0.08); padding: 0 3px;";
                      childValueLabel.SetStyles( childBaseStyle );
                      childLayout.Add( childValueLabel, 1 );

                      var childTooltip =
                              $"Type: {childType?.Name ?? "Unknown"}\nComponent: {componentTypeName}\nProperty: {childDisplay}\nValue: {childValue}";
                      childValueLabel.ToolTip = childTooltip;
                      childNameLabel.ToolTip = childTooltip;

                      childRowWidget.SetStyles( "margin: 2px 0; padding: 2px 4px; border-radius: 3px;" );
                      groupBody.Layout.Add( childRowWidget );

                      var row = new PropertyRow
                      {
                              Widget = childRowWidget,
                              NameLabel = childNameLabel,
                              EditorWidget = childValueLabel,
                              Component = component,
                              PropertyInfo = null,
                              Name = childDisplay,
                              DisplayName = childDisplay,
                              ComponentName = groupInfo.Name,
                              GameObjectName = groupInfo.OwnerName,
                              ValuePreview = childValue,
                              HitLabel = childHitLabel,
                              EditorBaseStyle = childBaseStyle,
                              IsSynthetic = true,
                              IsVisible = childRowWidget.Visible
                      };

                      RegisterRowModel( row, groupInfo, row.Name, childDisplay, childValue, childKind, isSynthetic: true );
                      rows.Add( row );
                      rowsDirty = true;
                      groupInfo.Rows.Add( row );
                      added++;
              }
      }

      private static string GetSimpleTypeName( Type type )
      {
              if ( type == null )
                      return null;

              var name = type.Name;
              var tick = name.IndexOf( '`' );
              if ( tick > 0 )
              {
                      name = name.Substring( 0, tick );
              }

              return name;
      }

      private static string[] BuildTypeTokens( Type type )
      {
              if ( type == null )
                      return Array.Empty<string>();

              var tokens = new HashSet<string>( StringComparer.OrdinalIgnoreCase );

              void AddTokens( string value )
              {
                      if ( string.IsNullOrEmpty( value ) )
                              return;

                      foreach ( var token in SearchTokens.Tokenize( value ) )
                      {
                              if ( string.IsNullOrEmpty( token ) )
                                      continue;

                              tokens.Add( token );
                      }
              }

              void AddType( Type t )
              {
                      if ( t == null )
                              return;

                      AddTokens( GetSimpleTypeName( t ) );
                      AddTokens( t.FullName );
              }

              AddType( type );

              var nullableType = Nullable.GetUnderlyingType( type );
              if ( nullableType != null && nullableType != type )
                      AddType( nullableType );

              if ( type.IsArray )
              {
                      var element = type.GetElementType();
                      if ( element != null )
                              AddType( element );
              }

              if ( type.IsGenericType )
              {
                      foreach ( var arg in type.GetGenericArguments() )
                      {
                              AddType( arg );
                      }
              }

              return tokens.Count == 0 ? Array.Empty<string>() : tokens.ToArray();
      }

      private void RegisterRowModel( PropertyRow row, ComponentGroup groupInfo, string propertyName, string displayName, string valueText, PropertyKind kind, bool isSynthetic )
      {
              if ( row == null || groupInfo == null )
                      return;

              var propertyType = row.PropertyInfo?.PropertyType;
              var propertyTypeName = GetSimpleTypeName( propertyType );
              var propertyTypeFullName = propertyType?.FullName ?? string.Empty;
              var propertyTypeFullNameLower = string.IsNullOrEmpty( propertyTypeFullName )
                      ? string.Empty
                      : propertyTypeFullName.ToLowerInvariant();

              var id = nextRowId++;
              row.RowId = id;

              var model = new RowModel
              {
                      Id = id,
                      GameObjectKey = groupInfo.OwnerKey,
                      OwnerKey = groupInfo.MatchKey ?? groupInfo.OwnerKey,
                      OwnerName = groupInfo.OwnerName,
                      GameObjectName = groupInfo.OwnerName,
                      ComponentName = groupInfo.Name,
                      PropertyName = propertyName,
                      DisplayName = displayName,
                      ValuePreview = valueText,
                      PropertyTypeName = propertyTypeName,
                      Kind = kind,
                      IsSynthetic = isSynthetic,
                      IsError = valueText == "<error>"
              };

              row.GameObjectKey = model.GameObjectKey;

              rowModels.Add( model );
              rowModelLookup[id] = model;
              rowLookup[id] = row;

              model.NameTokens = SearchTokens.Tokenize( $"{model.PropertyNameLower} {model.DisplayNameLower}" );
              model.TypeTokens = BuildTypeTokens( propertyType );
              if ( !string.IsNullOrEmpty( propertyTypeName ) && ApiTypeTokenMap.Value.TryGetValue( propertyTypeName, out var apiTokens ) )
              {
                      model.TypeTokens = MergeTokens( model.TypeTokens, apiTokens );
              }
              else if ( !string.IsNullOrEmpty( propertyTypeFullName ) && ApiTypeTokenMap.Value.TryGetValue( propertyTypeFullName, out var apiTokensFull ) )
              {
                      model.TypeTokens = MergeTokens( model.TypeTokens, apiTokensFull );
              }
              model.ComponentTokens = SearchTokens.Tokenize( model.ComponentNameLower );
              model.ObjectTokens = SearchTokens.Tokenize( model.GameObjectNameLower );
              model.ValueTokens = SearchTokens.Tokenize( model.ValuePreviewLower );
              model.SearchBlobLower = string.Join( " ", new[]
              {
                      model.PropertyNameLower,
                      model.DisplayNameLower,
                      model.PropertyTypeNameLower,
                      propertyTypeFullNameLower,
                      model.ComponentNameLower,
                      model.GameObjectNameLower,
                      model.ValuePreviewLower
              } );

              AddTokensToIndex( nameIndex, model.NameTokens, id );
              AddTokensToIndex( typeIndex, model.TypeTokens, id );
              AddTokensToIndex( componentIndex, model.ComponentTokens, id );
              AddTokensToIndex( objectIndex, model.ObjectTokens, id );
              AddTokensToIndex( valueIndex, model.ValueTokens, id );

              rowsDirty = true;
      }

      private void AddTokensToIndex( Dictionary<string, HashSet<int>> index, string[] tokens, int rowId )
      {
              if ( tokens == null || tokens.Length == 0 )
                      return;

              foreach ( var token in tokens )
              {
                      if ( string.IsNullOrEmpty( token ) )
                              continue;

                      if ( !index.TryGetValue( token, out var set ) )
                      {
                              set = new HashSet<int>();
                              index[token] = set;
                      }

                      set.Add( rowId );
              }
      }

      private void ClearTokenIndexes()
      {
              nameIndex.Clear();
              typeIndex.Clear();
              valueIndex.Clear();
              componentIndex.Clear();
              objectIndex.Clear();
      }

      private HashSet<int> IntersectTerms( Dictionary<string, HashSet<int>> index, string[] terms )
      {
              HashSet<int> current = null;
              foreach ( var term in terms )
              {
                      if ( !index.TryGetValue( term, out var set ) || set == null || set.Count == 0 )
                      {
                              return new HashSet<int>();
                      }

                      if ( current == null )
                      {
                              current = new HashSet<int>( set );
                      }
                      else
                      {
                              current.IntersectWith( set );
                      }
              }

              return current ?? new HashSet<int>();
      }

      private IEnumerable<RowModel> GetFilterCandidates( string[] includeTerms, QueryFilters filters, bool termEmpty, bool refineWithinBase )
      {
              IEnumerable<RowModel> baseSet = refineWithinBase && safeBaseActive && safeBaseRowIds.Count > 0
                      ? rowModels.Where( m => m != null && safeBaseRowIds.Contains( m.Id ) )
                      : rowModels;

              HashSet<int> filterCandidates = null;

              void IntersectFilter( Dictionary<string, HashSet<int>> index, string[] terms )
              {
                      if ( terms == null || terms.Length == 0 )
                              return;

                      var termSet = IntersectTerms( index, terms );
                      if ( filterCandidates == null )
                              filterCandidates = termSet;
                      else
                              filterCandidates.IntersectWith( termSet );
              }

              IntersectFilter( typeIndex, filters.TypeTerms );
              IntersectFilter( componentIndex, filters.ComponentTerms );
              IntersectFilter( objectIndex, filters.ObjectTerms );
              IntersectFilter( nameIndex, filters.NameTerms );
              IntersectFilter( valueIndex, filters.ValueTerms );

              if ( refineWithinBase && safeBaseActive && safeBaseRowIds.Count > 0 && filterCandidates != null )
              {
                      filterCandidates.IntersectWith( safeBaseRowIds );
              }

              if ( termEmpty )
              {
                      if ( filterCandidates == null )
                              return baseSet;

                      return baseSet.Where( m => m != null && filterCandidates.Contains( m.Id ) );
              }

              var includeCandidateIds = new HashSet<int>();

              void Union( HashSet<int> source )
              {
                      if ( source == null || source.Count == 0 )
                              return;

                      includeCandidateIds.UnionWith( source );
              }

              var nameCandidates = IntersectTerms( nameIndex, includeTerms );
              Union( nameCandidates );

              if ( includeTypeNamesInNameSearch )
              {
                      var typeCandidates = IntersectTerms( typeIndex, includeTerms );
                      Union( typeCandidates );
              }

              if ( valueSearchMode != ValueSearchMode.NamesOnly )
              {
                      var valueCandidates = IntersectTerms( valueIndex, includeTerms );
                      Union( valueCandidates );
              }

              // Object/component matches still influence visibility/highlighting.
              Union( IntersectTerms( componentIndex, includeTerms ) );
              Union( IntersectTerms( objectIndex, includeTerms ) );

              // If no index hits, fall back to the full (or base) set to avoid missing rows.
              if ( includeCandidateIds.Count == 0 )
              {
                      if ( includeTerms == null || includeTerms.Length == 0 )
                              return baseSet;

                      if ( filterCandidates != null )
                      {
                              return baseSet.Where( m =>
                                      m != null
                                      && filterCandidates.Contains( m.Id )
                                      && includeTerms.All( t => SearchTokens.FuzzyContainsRaw( m.SearchBlobLower, t ) ) );
                      }

                      return baseSet.Where( m =>
                              m != null && includeTerms.All( t => SearchTokens.FuzzyContainsRaw( m.SearchBlobLower, t ) ) );
              }

              if ( filterCandidates != null )
              {
                      includeCandidateIds.IntersectWith( filterCandidates );
              }

              if ( refineWithinBase && safeBaseActive && safeBaseRowIds.Count > 0 )
              {
                      includeCandidateIds.IntersectWith( safeBaseRowIds );
              }

              var list = new List<RowModel>( includeCandidateIds.Count );
              foreach ( var id in includeCandidateIds )
              {
                      if ( rowModelLookup.TryGetValue( id, out var model ) && model != null )
                      {
                              list.Add( model );
                      }
              }

              return list;
      }

      /// <summary>
      /// Build all property rows for a given component group. This is used both in
      /// the default eager mode and when a lazy-loaded component is explicitly expanded.
      /// </summary>
      private void BuildComponentRows( ComponentGroup groupInfo )
      {
              if ( groupInfo == null || groupInfo.Component == null || !groupInfo.Component.IsValid() )
                      return;

              groupInfo.Rows.Clear();

              var component = groupInfo.Component;
              var groupBody = groupInfo.Body;
              var props = groupInfo.Properties ?? Array.Empty<PropertyInfo>();

              // Try to get a SerializedObject for this component so we can
              // use ControlWidget editors for complex/serialized fields.
              SerializedObject componentSO = null;
              try
              {
                      componentSO = EditorTypeLibrary.GetSerializedObject( component );
              }
              catch
              {
                      componentSO = null;
              }

              var type = component.GetType();

              // Additionally inspect public instance fields so complex option structs that
              // are exposed as fields (for example, some RenderOptions layouts) can still
              // contribute nested boolean search rows even when they are not surfaced as
              // [Property]-decorated properties.
              foreach ( var field in type.GetFields( BindingFlags.Instance | BindingFlags.Public ) )
              {
                      var fieldType = field.FieldType;
                      // Skip obvious non-container types
                      if ( fieldType.IsPrimitive || fieldType == typeof( string ) )
                              continue;

                      object fieldValue;
                      try
                      {
                              fieldValue = field.GetValue( component );
                      }
                      catch
                      {
                              continue;
                      }

                      if ( fieldValue == null )
                              continue;

                      var fieldDisplay = DisplayInfo.ForMember( field ).Name ?? field.Name;
                      FlattenNestedBoolMembers( fieldValue, fieldDisplay, groupInfo, component, groupBody, type.Name);
              }

              foreach ( var prop in props )
              {
                      // Use pretty display name like the built-in inspector ("Cast Shadows" instead of "CastShadows")
                      var displayInfo = DisplayInfo.ForMember( prop );
                      var displayName = displayInfo.Name ?? prop.Name;

                      object valueObj = null;
                      string valueText = string.Empty;
                      try
                      {
                              valueObj = prop.GetValue( component );
                              // Treat null as empty so it's easy to type a fresh value
                              valueText = valueObj?.ToString() ?? string.Empty;
                      }
                      catch
                      {
                              valueText = "<error>";
                      }

                      var includeMainRow = ShouldIncludeRowForSearch( displayName, prop.PropertyType?.Name ?? string.Empty, valueText );

                      Widget editorWidget = null;
                      var propType = prop.PropertyType;
                      var underlyingType = Nullable.GetUnderlyingType( propType ) ?? propType;
                      var compactSearchUi = !string.IsNullOrWhiteSpace( lastSearch );
                      var allowControlInCompact = propType == typeof( Model );
                      SerializedProperty serializedProp = null;
                      if ( componentSO != null )
                      {
                              try
                              {
                                      serializedProp = componentSO.GetProperty( prop.Name );
                              }
                              catch
                              {
                                      serializedProp = null;
                              }
                      }

                      // Resolve type/component names once per property so both the main
                      // row and any flattened nested rows can share them in their tooltips.
                      var typeName = propType != null ? propType.Name : "Unknown";
                      var componentTypeName = component != null ? component.GetType().Name : "Unknown";

                      List<(string Name, string Value)> parameterEntries = null;
                      if ( propType == typeof( SkinnedModelRenderer.ParameterAccessor ) )
                      {
                              try
                              {
                                      var accessor = valueObj as SkinnedModelRenderer.ParameterAccessor;
                                      if ( accessor == null && serializedProp != null && serializedProp.IsValid )
                                      {
                                              accessor = serializedProp.GetValue<SkinnedModelRenderer.ParameterAccessor>();
                                      }

                                      var graph = accessor?.Graph;
                                      if ( accessor != null && graph != null )
                                      {
                                              var summary = BuildParameterAccessorSummary( accessor, graph, out parameterEntries );
                                              if ( !string.IsNullOrEmpty( summary ) )
                                              {
                                                      valueText = summary;
                                              }
                                              valueObj = accessor;
                                      }
                              }
                              catch
                              {
                                      // Best-effort only; fall back to default value text.
                              }
                      }

                      var kind = GetPropertyKind( propType );
                      var includeKind = ShouldIncludeKind( kind );

                      // For complex container types, we still want to be able to flatten and
                      // expose simple nested fields (like RenderOptions.BloomLayer) even when
                      // the complex type itself is filtered out.
                      var allowComplexForNested = kind == PropertyKind.Complex && !includeKind;
                      if ( !includeKind && !allowComplexForNested )
                              continue;

                      var skipMainRow = false;
                      if ( allowComplexForNested )
                      {
                              skipMainRow = true;
                      }

                      var isSimpleType = underlyingType == typeof( string )
                              || underlyingType.IsEnum
                              || underlyingType == typeof( int )
                              || underlyingType == typeof( float )
                              || underlyingType == typeof( double )
                              || underlyingType == typeof( long )
                              || underlyingType == typeof( short )
                              || underlyingType == typeof( uint )
                              || underlyingType == typeof( ulong )
                              || underlyingType == typeof( bool );

                      if ( !includeMainRow && isSimpleType )
                              continue;

                      if ( !includeMainRow )
                              skipMainRow = true;

                      // Basic editable types: string, numeric, bool, enum
                      if ( underlyingType == typeof( string ) ||
                           underlyingType.IsEnum ||
                           underlyingType == typeof( int ) ||
                           underlyingType == typeof( float ) ||
                           underlyingType == typeof( double ) ||
                           underlyingType == typeof( long ) ||
                           underlyingType == typeof( short ) ||
                           underlyingType == typeof( uint ) ||
                           underlyingType == typeof( ulong ) )
                      {
                              var line = new LineEdit( groupBody )
                              {
                                      Text = valueText,
                                      FixedHeight = Theme.RowHeight * 0.8f
                              };
                              line.SetStyles( "margin:0; padding:0 3px;" );

                              // Commit on finish edit
                              var capturedComponent = component;
                              var capturedRowIndex = rows.Count;

                              line.EditingFinished += () =>
                              {
                                      if ( capturedComponent is null || !capturedComponent.IsValid() )
                                              return;

                                      if ( capturedRowIndex < 0 || capturedRowIndex >= rows.Count )
                                              return;

                                      var row = rows[capturedRowIndex];
                                      TryCommitValue( row, line.Text );
                              };

                              editorWidget = line;
                      }
                      else if ( underlyingType == typeof( bool ) )
                      {
                              // Toggle via checkbox-style icon button, left-aligned in the value column
                              IconButton boolToggle = null;
                              var capturedComponent = component;
                              var capturedProp = prop;
                              var capturedRowIndex = rows.Count;

                              bool GetCurrent()
                              {
                                      try
                                      {
                                              return (bool)(capturedProp.GetValue( capturedComponent ) ?? false);
                                      }
                                      catch
                                      {
                                              return false;
                                      }
                              }

                              void UpdateBoolIcon()
                              {
                                      var current = GetCurrent();
                                      boolToggle.Icon = current ? "check_box" : "check_box_outline_blank";
                              }

                              boolToggle = new IconButton( "check_box_outline_blank", () =>
                              {
                                      if ( capturedComponent is null || !capturedComponent.IsValid() )
                                              return;

                                      if ( capturedRowIndex < 0 || capturedRowIndex >= rows.Count )
                                              return;

                                      var current = GetCurrent();
                                      var newVal = !current;
                                      try
                                      {
                                              capturedProp.SetValue( capturedComponent, newVal );

                                              var row = rows[capturedRowIndex];
                                              row.ValuePreview = newVal ? "true" : "false";

                                              UpdateBoolIcon();
                                      }
                                      catch { }
                              } )
                              {
                                      FixedHeight = Theme.RowHeight * 0.8f,
                                      FixedWidth = Theme.RowHeight,
                                      ToolTip = "Toggle the bool value"
                              };

                              UpdateBoolIcon();

                              var boolContainer = new Widget( groupBody );
                              var boolLayout = boolContainer.Layout = Layout.Row();
                              boolLayout.Spacing = 0;
                              boolLayout.Add( boolToggle, 0 );

                              editorWidget = boolContainer;
                      }
                      else
                      {
                              // Complex type – prefer search-friendly representations over
                              // embedding heavy custom editors. For most types we still use
                              // ControlWidget when available; ParameterAccessor is handled
                              // specially via summary + flattened rows.
                              if ( includeMainRow && serializedProp != null && serializedProp.IsValid && includeKind )
                              {
                                      try
                                      {
                                              var spVal = serializedProp.GetValue<object>();
                                              if ( spVal != null )
                                              {
                                                      valueObj ??= spVal;
                                                      if ( string.IsNullOrEmpty( valueText ) || valueText == prop.PropertyType?.Name )
                                                      {
                                                              valueText = spVal.ToString();
                                                      }
                                              }
                                      }
                                      catch { }
                              }

                              if ( includeMainRow && includeKind )
                              {
                                      if ( (compactSearchUi && !allowControlInCompact) || serializedProp == null || !serializedProp.IsValid )
                                      {
                                              editorWidget = new Label( valueText )
                                              {
                                                      FixedHeight = Theme.RowHeight * 0.8f,
                                                      Alignment = TextFlag.LeftCenter
                                              };
                                      }
                                      else if ( propType == typeof( SkinnedModelRenderer.ParameterAccessor ) )
                                      {
                                              editorWidget = new Label( valueText )
                                              {
                                                      FixedHeight = Theme.RowHeight * 0.8f,
                                                      Alignment = TextFlag.LeftCenter
                                              };
                                      }
                                      else
                                      {
                                              var control = ControlWidget.Create( serializedProp );
                                              if ( control.IsValid() )
                                              {
                                                      editorWidget = control;
                                              }
                                              else
                                              {
                                                      editorWidget = new Label( valueText )
                                                      {
                                                              FixedHeight = Theme.RowHeight * 0.8f,
                                                              Alignment = TextFlag.LeftCenter
                                                      };
                                              }
                                      }
                              }

                              // Always make key option flags like Bloom/BloomLayer directly searchable,
                              // even when the broader nested-bool flattening is disabled.
                              if ( valueObj != null )
                              {
                                      TryAddBloomOptionRow( valueObj, displayName, groupInfo, component, groupBody, componentTypeName );

                                      // Flatten simple nested boolean fields inside complex structs so they
                                      // can be searched directly (e.g. RenderOptions.Bloom or RenderOptions.BloomLayer),
                                      // even when they live a couple of levels deep in an "Advanced" sub-struct.
                                      FlattenNestedBoolMembers( valueObj, displayName, groupInfo, component, groupBody, componentTypeName );

                                      // Flatten simple list-like containers (e.g. hide-body-groups lists) so
                                      // their entries can be searched by name.
                                      FlattenListEntries( valueObj, displayName, groupInfo, component, groupBody, componentTypeName );

                                      // Flatten simple PhysicsLock fields (e.g. Pitch/Yaw) for targeted
                                      // option structs without resorting to blanket struct flattening.
                                      if ( valueObj is PhysicsLock physicsLock )
                                      {
                                              FlattenPhysicsLockOptions( physicsLock, displayName, groupInfo, component, groupBody, componentTypeName );
                                      }
                              }

                              if ( serializedProp != null && serializedProp.IsValid )
                              {
                                      FlattenSerializedPropertyChildren( serializedProp, displayName, groupInfo, component, groupBody, componentTypeName );
                              }

                              // If this complex property itself isn't included by the current type
                              // filters, skip creating a main row for it – we only keep the nested
                              // flattened rows above.
                              if ( skipMainRow )
                                      continue;
                      }

                      // Name label for the property column
                      var nameLabel = new Label( displayName )
                      {
                              Alignment = TextFlag.LeftCenter
                      };

                      var rowWidget = new Widget( groupBody );
                      var rowLayout = rowWidget.Layout = Layout.Row();
                      rowLayout.Spacing = 2;
                      // When a search term is active, defer showing new rows until the
                      // next ApplyFilter pass so we never flash unfiltered content while
                      // components are still streaming in.
                      if ( !string.IsNullOrEmpty( lastSearch ) )
                      {
                              rowWidget.Visible = false;
                      }

                      // Match indicator cells to the left of the name and value fields.
                      var nameDotContainer = new Widget( rowWidget );
                      nameDotContainer.FixedWidth = 12;
                      var nameDotLayout = nameDotContainer.Layout = Layout.Row();
                      nameDotLayout.Spacing = 0;
                      var nameDotLabel = new Label( "" )
                      {
                              Alignment = TextFlag.Center
                      };
                      nameDotLabel.SetStyles( "color: rgba(110,210,175,0.95); font-size: 12px; font-weight: 700; margin:0; padding:0;" );
                      nameDotLabel.Visible = false;
                      nameDotLayout.Add( nameDotLabel, 1 );
                      rowLayout.Add( nameDotContainer, 0 );

                      // Fixed-width property column, flexible editor column (single label for name)
                      var nameContainer = new Widget( rowWidget );
                      var nameLayout = nameContainer.Layout = Layout.Row();
                      nameLayout.Spacing = 0;

                      nameLabel.SetStyles( "margin:0; padding:0;" );
                      nameLayout.Add( nameLabel, 0 );

                      nameContainer.FixedWidth = 180;
                      rowLayout.Add( nameContainer, 0 );

                      var valueDotContainer = new Widget( rowWidget );
                      valueDotContainer.FixedWidth = 12;
                      var valueDotLayout = valueDotContainer.Layout = Layout.Row();
                      valueDotLayout.Spacing = 0;
                      var valueDotLabel = new Label( "" )
                      {
                              Alignment = TextFlag.Center
                      };
                      valueDotLabel.SetStyles( "color: rgba(110,210,175,0.95); font-size: 12px; font-weight: 700; margin:0; padding:0;" );
                      valueDotLabel.Visible = false;
                      valueDotLayout.Add( valueDotLabel, 1 );
                      rowLayout.Add( valueDotContainer, 0 );

                      rowLayout.Add( editorWidget, 1 );

                      // Subtle background accent on the editor cell – neutral, off-white
                      // plastic so type differences are not overly colorful.
                      var kindForRow = kind;
                      var editorKindStyle = "background-color: rgba(255,255,255,0.08); padding: 0 3px;";
                      if ( !string.IsNullOrEmpty( editorKindStyle ) )
                      {
                              editorWidget.SetStyles( editorKindStyle + " padding: 0 3px;" );
                      }

                      // Per-row tooltip describing type/component/property/value (with simple emphasis)
                      var tooltipText =
                              $"Type: {typeName}\nComponent: {componentTypeName}\nProperty: {displayName}\nValue: {valueText}";
                      editorWidget.ToolTip = tooltipText;
                      nameLabel.ToolTip = tooltipText;

                      // Vertical spacing and subtle rounding for the entire row; zebra colors applied after filtering
                      rowWidget.SetStyles( "margin: 2px 0; padding: 2px 4px; border-radius: 3px;" );

                      groupBody.Layout.Add( rowWidget );

                      var row = new PropertyRow
                      {
                              Widget = rowWidget,
                              NameLabel = nameLabel,
                              EditorWidget = editorWidget,
                              Component = component,
                              PropertyInfo = prop,
                              Name = prop.Name,
                              DisplayName = displayName,
                              ComponentName = groupInfo.Name,
                              GameObjectName = groupInfo.OwnerName,
                              ValuePreview = valueText,
                              HitLabel = null,
                              NameMatchDot = nameDotLabel,
                              ValueMatchDot = valueDotLabel,
                              EditorBaseStyle = editorKindStyle,
                              IsVisible = rowWidget.Visible
                      };

                      RegisterRowModel( row, groupInfo, prop.Name, displayName, valueText, kindForRow, isSynthetic: false );
                      rows.Add( row );
                      rowsDirty = true;
                      groupInfo.Rows.Add( row );

                      // Expose ParameterAccessor entries as individual searchable rows.
                      if ( parameterEntries != null && parameterEntries.Count > 0 )
                      {
                              foreach ( var (entryName, entryValue) in parameterEntries )
                              {
                                      var entryDisplay = $"{displayName}.{entryName}";
                                      var entryWidget = new Label( entryValue )
                                      {
                                              FixedHeight = Theme.RowHeight * 0.8f,
                                              Alignment = TextFlag.LeftCenter
                                      };
                                      var entryBaseStyle = "background-color: rgba(255,255,255,0.08); padding: 0 3px;";
                                      entryWidget.SetStyles( entryBaseStyle );

                                      var entryRowWidget = new Widget( groupBody );
                                      var entryLayout = entryRowWidget.Layout = Layout.Row();
                                      entryLayout.Spacing = 2;

                                      var entryHitLabel = AddMatchIndicator( entryRowWidget, entryLayout );

                                      var entryNameContainer = new Widget( entryRowWidget );
                                      var entryNameLayout = entryNameContainer.Layout = Layout.Row();
                                      entryNameLayout.Spacing = 0;
                                      var entryNameLabel = new Label( entryDisplay )
                                      {
                                              Alignment = TextFlag.LeftCenter
                                      };
                                      entryNameLabel.SetStyles( "margin:0; padding:0;" );
                                      entryNameLayout.Add( entryNameLabel, 0 );
                                      entryNameContainer.FixedWidth = 180;
                                      entryLayout.Add( entryNameContainer, 0 );

                                      entryLayout.Add( entryWidget, 1 );

                                      var entryTooltip =
                                              $"Type: Parameter\nComponent: {componentTypeName}\nProperty: {entryDisplay}\nValue: {entryValue}";
                                      entryWidget.ToolTip = entryTooltip;
                                      entryNameLabel.ToolTip = entryTooltip;

                                      entryRowWidget.SetStyles( "margin: 2px 0; padding: 2px 4px; border-radius: 3px;" );
                                      groupBody.Layout.Add( entryRowWidget );

                                      var paramRow = new PropertyRow
                                      {
                                              Widget = entryRowWidget,
                                              NameLabel = entryNameLabel,
                                              EditorWidget = entryWidget,
                                              Component = component,
                                              PropertyInfo = null,
                                              Name = $"{prop.Name}.{entryName}",
                                              DisplayName = entryDisplay,
                                              ComponentName = groupInfo.Name,
                                              GameObjectName = groupInfo.OwnerName,
                                              ValuePreview = entryValue,
                                              HitLabel = entryHitLabel,
                                              EditorBaseStyle = entryBaseStyle,
                                              IsSynthetic = true,
                                              IsVisible = entryRowWidget.Visible
                                      };

                                      RegisterRowModel( paramRow, groupInfo, paramRow.Name, entryDisplay, entryValue, PropertyKind.Complex, isSynthetic: true );
                                      rows.Add( paramRow );
                                      rowsDirty = true;
                                      groupInfo.Rows.Add( paramRow );
                              }
                      }
              }
      }

      private Label AddMatchIndicator( Widget rowWidget, Layout rowLayout )
      {
              var hitContainer = new Widget( rowWidget );
              hitContainer.FixedWidth = 20;

              var hitLayout = hitContainer.Layout = Layout.Row();
              hitLayout.Spacing = 0;

                      var hitLabel = new Label( "" )
                      {
                              Alignment = TextFlag.Center
                      };
                      hitLabel.SetStyles( "color: rgba(110,210,175,0.95); font-size: 12px; font-weight: 700; margin:0; padding:0;" );
              hitLabel.Visible = false;
              hitLayout.Add( hitLabel, 1 );

              rowLayout.Add( hitContainer, 0 );
              return hitLabel;
      }

      private void TryCommitValue( PropertyRow row, string text )
      {
              if ( isDisposed )
                      return;

              if ( row == null || row.Component is null || !row.Component.IsValid() || row.PropertyInfo is null )
                      return;

              var pType = row.PropertyInfo.PropertyType;

              object converted = null;

              try
              {
                      if ( pType == typeof( string ) )
                      {
                              converted = text;
                      }
                      else if ( pType.IsEnum )
                      {
                              converted = Enum.Parse( pType, text, ignoreCase: true );
                      }
                      else if ( pType == typeof( int ) )
                      {
                              if ( int.TryParse( text, out var v ) ) converted = v;
                      }
                      else if ( pType == typeof( float ) )
                      {
                              if ( float.TryParse( text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var v ) )
                                      converted = v;
                      }
                      else if ( pType == typeof( double ) )
                      {
                              if ( double.TryParse( text, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var v ) )
                                      converted = v;
                      }
                      else if ( pType == typeof( long ) )
                      {
                              if ( long.TryParse( text, out var v ) ) converted = v;
                      }
                      else if ( pType == typeof( short ) )
                      {
                              if ( short.TryParse( text, out var v ) ) converted = v;
                      }
                      else if ( pType == typeof( uint ) )
                      {
                              if ( uint.TryParse( text, out var v ) ) converted = v;
                      }
                      else if ( pType == typeof( ulong ) )
                      {
                              if ( ulong.TryParse( text, out var v ) ) converted = v;
                      }
              }
              catch
              {
                      converted = null;
              }

              if ( converted is null && pType != typeof( string ) )
                      return;

              try
              {
                      row.PropertyInfo.SetValue( row.Component, converted );
                      row.ValuePreview = text;
              }
              catch
              {
                      // Ignore bad writes; we don't want to throw in the UI.
              }
      }

      private sealed class ScanJobRequest
      {
              public GameObject Target;
              public bool IncludeChildren;
              public bool LazyLoad;
              public SelectionSignature Signature;
      }

      private sealed class ScanJobResult
      {
              public bool LazyLoad;
              public long ScanMilliseconds;
              public List<ScanGameObjectResult> GameObjects { get; } = new();
      }

      private sealed class ScanGameObjectResult
      {
              public GameObject GameObject;
              public int Depth;
              public string Path;
              public List<ComponentScanResult> Components { get; } = new();
      }

      private sealed class ComponentScanResult
      {
              public Component Component;
              public string DisplayName;
              public PropertyInfo[] Properties;
      }

      private sealed class RowModel
      {
              public int Id;
              public string GameObjectKey;
              public string OwnerKey;
              public string OwnerName;
              public string GameObjectName;
              public string ComponentName;
              public string PropertyName;
              public string DisplayName;
              public string ValuePreview;
              public string PropertyTypeName;
              public PropertyKind Kind;
              public bool IsSynthetic;
              public bool IsError;
              public string[] NameTokens;
              public string[] TypeTokens;
              public string[] ComponentTokens;
              public string[] ObjectTokens;
              public string[] ValueTokens;
              public string SearchBlobLower;

              public string GameObjectNameLower => GameObjectName?.ToLowerInvariant() ?? string.Empty;
              public string ComponentNameLower => ComponentName?.ToLowerInvariant() ?? string.Empty;
              public string PropertyNameLower => PropertyName?.ToLowerInvariant() ?? string.Empty;
              public string DisplayNameLower => DisplayName?.ToLowerInvariant() ?? string.Empty;
              public string ValuePreviewLower => ValuePreview?.ToLowerInvariant() ?? string.Empty;
              public string PropertyTypeNameLower => PropertyTypeName?.ToLowerInvariant() ?? string.Empty;
      }

      private sealed class PropertyRow
      {
              public Widget Widget;
              public Label NameLabel;
              public Label HitLabel;
              public Label NameMatchDot;
              public Label ValueMatchDot;
              public Widget EditorWidget;
              public Component Component;
              public PropertyInfo PropertyInfo;
              public int RowId;
              public string Name;
              public string DisplayName;
              public string ComponentName;
              public string GameObjectKey;
              public string GameObjectName;
              public string ValuePreview;
              public string EditorBaseStyle;
              public bool IsSynthetic;

              // Cached UI state so we can avoid redundant property and style updates
              // during filtering.
              public bool IsVisible;
              public bool NameHighlighted;
              public bool ValueHighlighted;
              public bool NameDotOn;
              public bool ValueDotOn;

              public string NameLower => Name?.ToLowerInvariant() ?? string.Empty;
              public string DisplayNameLower => DisplayName?.ToLowerInvariant() ?? string.Empty;
              public string ComponentNameLower => ComponentName?.ToLowerInvariant() ?? string.Empty;
              public string GameObjectNameLower => GameObjectName?.ToLowerInvariant() ?? string.Empty;
              public string ValuePreviewLower => ValuePreview?.ToLowerInvariant() ?? string.Empty;

      public void SetHighlighted( bool on )
      {
              var style = on
                      ? "color: rgb(140,220,185);"
                      : "color: rgba(255,255,255,0.9);";

              NameLabel?.SetStyles( style );
      }
      }

      private sealed class ComponentGroup
      {
              public ExpandGroup Group;
              public string Name;
              public string OwnerKey;
              public string MatchKey;
              public string OwnerName;
              public List<PropertyRow> Rows { get; } = new();
              public int MatchCount;
              public Component Component;
              public PropertyInfo[] Properties;
              public Widget Body;
              public bool PropertiesBuilt;
      }

      private sealed class GameObjectGroup
      {
              public ExpandGroup Group;
              public string Name;
              public string NameLower;
              public string Key;
              public Widget Row;
              public int MatchCount;
              public bool NameMatch;
              public Label HitLabel;
      }

      private readonly struct SelectionSignature : IEquatable<SelectionSignature>
      {
              private readonly long targetId;
              private readonly bool includeChildren;
              private readonly ValueSearchMode valueMode;
              private readonly SearchTriggerMode triggerMode;
              private readonly bool lazy;

              private SelectionSignature( long targetId, bool includeChildren, ValueSearchMode valueMode, SearchTriggerMode triggerMode, bool lazy )
              {
                      this.targetId = targetId;
                      this.includeChildren = includeChildren;
                      this.valueMode = valueMode;
                      this.triggerMode = triggerMode;
                      this.lazy = lazy;
              }

              public static SelectionSignature Create( GameObject target, bool includeChildren, ValueSearchMode valueMode, SearchTriggerMode triggerMode, bool lazy )
              {
                      long id = 0;
                      if ( target != null && target.IsValid() )
                      {
                              // GameObject.Id is a Guid; use its hash as a stable-ish identifier for caching.
                              id = target.Id.GetHashCode();
                      }

                      return new SelectionSignature( id, includeChildren, valueMode, triggerMode, lazy );
              }

              public bool Equals( SelectionSignature other )
              {
                      return targetId == other.targetId &&
                             includeChildren == other.includeChildren &&
                             valueMode == other.valueMode &&
                             triggerMode == other.triggerMode &&
                             lazy == other.lazy;
              }

              public override bool Equals( object obj )
              {
                      return obj is SelectionSignature other && Equals( other );
              }

              public override int GetHashCode()
              {
                      var hashCode = targetId.GetHashCode();
                      hashCode = (hashCode * 397) ^ includeChildren.GetHashCode();
                      hashCode = (hashCode * 397) ^ (int)valueMode;
                      hashCode = (hashCode * 397) ^ (int)triggerMode;
                      hashCode = (hashCode * 397) ^ lazy.GetHashCode();
                      return hashCode;
              }
      }

      private sealed class RowMatchResult
      {
              public bool PassesFilters;
              public bool Match;
              public bool ValueMatch;
              public bool NameMatch;
              public bool ObjectNameMatch;
      }

      private sealed class RowFilterState
      {
              public Dictionary<int, RowMatchResult> RowStates { get; } = new();
              public Dictionary<string, int> ComponentMatches { get; } = new();
              public Dictionary<string, int> ObjectMatches { get; } = new();
              public bool AnyObjectNameMatch;
              public int MatchCount;
      }

      private sealed class FilterEngine
      {
              private readonly InspectorSearchTool owner;

              public FilterEngine( InspectorSearchTool owner )
              {
                      this.owner = owner;
              }

              private const int ParallelThreshold = 2000;

              public RowFilterState Run( IEnumerable<RowModel> sourceRows, string[] includeTerms, string[] excludeTerms, QueryFilters filters, bool termEmpty, CancellationToken cancellationToken )
              {
                      var result = new RowFilterState();
                      if ( sourceRows is null )
                              return result;

                      if ( sourceRows is ICollection<RowModel> list && list.Count >= ParallelThreshold )
                      {
                              RunParallel( list, includeTerms, excludeTerms, filters, termEmpty, cancellationToken, result );
                              return result;
                      }

                      foreach ( var row in sourceRows )
                      {
                              cancellationToken.ThrowIfCancellationRequested();
                              if ( row == null )
                                      continue;

                      if ( !TryEvaluateRow( row, includeTerms, excludeTerms, filters, termEmpty, out var state, out var componentMatch ) )
                              continue;

                              UpdateAggregates( row, state, componentMatch, owner.matchTargetMode == MatchTargetMode.ComponentsOnly, result );
                      }

                      return result;
              }

              private void RunParallel(
                      ICollection<RowModel> rows,
                      string[] includeTerms,
                      string[] excludeTerms,
                      QueryFilters filters,
                      bool termEmpty,
                      CancellationToken token,
                      RowFilterState result )
              {
                      var rowStates = new System.Collections.Concurrent.ConcurrentDictionary<int, RowMatchResult>();
                      var componentMatches = new System.Collections.Concurrent.ConcurrentDictionary<string, int>();
                      var objectMatches = new System.Collections.Concurrent.ConcurrentDictionary<string, int>();
                      int matchCount = 0;
                      int anyObjectNameMatch = 0;

                      Parallel.ForEach( rows, ( row, state ) =>
                      {
                              if ( token.IsCancellationRequested || row == null )
                                      return;

                              if ( !TryEvaluateRow( row, includeTerms, excludeTerms, filters, termEmpty, out var rowState, out var componentMatch ) )
                                      return;

                              rowStates[row.Id] = rowState;
                              if ( rowState.Match )
                              {
                                      System.Threading.Interlocked.Increment( ref matchCount );
                                      if ( !string.IsNullOrEmpty( row.OwnerKey ) )
                                              componentMatches.AddOrUpdate( row.OwnerKey, 1, (_, v) => v + 1 );
                                      if ( !string.IsNullOrEmpty( row.GameObjectKey ) )
                                              objectMatches.AddOrUpdate( row.GameObjectKey, 1, (_, v) => v + 1 );
                              }

                              if ( owner.matchTargetMode == MatchTargetMode.ComponentsOnly && componentMatch )
                              {
                                      if ( !string.IsNullOrEmpty( row.OwnerKey ) )
                                              componentMatches.AddOrUpdate( row.OwnerKey, 1, (_, v) => Math.Max( 1, v ) );
                                      if ( !string.IsNullOrEmpty( row.GameObjectKey ) )
                                              objectMatches.AddOrUpdate( row.GameObjectKey, 1, (_, v) => Math.Max( 1, v ) );
                              }

                              if ( rowState.ObjectNameMatch )
                              {
                                      System.Threading.Interlocked.Exchange( ref anyObjectNameMatch, 1 );
                                      if ( !string.IsNullOrEmpty( row.GameObjectKey ) )
                                              objectMatches.AddOrUpdate( row.GameObjectKey, 1, (_, v) => Math.Max( 1, v ) );
                              }
                      } );

                      result.MatchCount = matchCount;
                      result.AnyObjectNameMatch = anyObjectNameMatch == 1;
                      foreach ( var pair in rowStates )
                              result.RowStates[pair.Key] = pair.Value;
                      foreach ( var pair in componentMatches )
                              result.ComponentMatches[pair.Key] = pair.Value;
                      foreach ( var pair in objectMatches )
                              result.ObjectMatches[pair.Key] = pair.Value;
              }

              private bool TryEvaluateRow(
                      RowModel row,
                      string[] includeTerms,
                      string[] excludeTerms,
                      QueryFilters filters,
                      bool termEmpty,
                      out RowMatchResult state,
                      out bool componentMatch )
              {
                      state = new RowMatchResult();
                      componentMatch = false;

                      // Type filter always applies
                      if ( !owner.ShouldIncludeKind( row.Kind ) )
                      {
                              state.PassesFilters = false;
                              return true;
                      }

                      // Value-state filters
                      if ( owner.valueFilterMode == ValueFilterMode.SetOnly && string.IsNullOrEmpty( row.ValuePreview ) )
                      {
                              state.PassesFilters = false;
                              return true;
                      }

                      if ( owner.valueFilterMode == ValueFilterMode.UnsetOnly && !string.IsNullOrEmpty( row.ValuePreview ) )
                      {
                              state.PassesFilters = false;
                              return true;
                      }

                      if ( owner.filterErrorsOnly && !row.IsError )
                      {
                              state.PassesFilters = false;
                              return true;
                      }

                      state.PassesFilters = true;

                      bool TokenContains( string[] tokens, string term ) => SearchTokens.Contains( tokens, term );
                      var includeTypeTokens = owner.includeTypeNamesInNameSearch && row.TypeTokens != null && row.TypeTokens.Length > 0;

                      if ( excludeTerms != null && excludeTerms.Length > 0 )
                      {
                              for ( int i = 0; i < excludeTerms.Length; i++ )
                              {
                                      var term = excludeTerms[i];
                                      if ( string.IsNullOrEmpty( term ) )
                                              continue;
                                      if ( TokenContains( row.ComponentTokens, term )
                                           || TokenContains( row.NameTokens, term )
                                           || (includeTypeTokens && TokenContains( row.TypeTokens, term ))
                                           || (row.ObjectTokens != null && TokenContains( row.ObjectTokens, term ))
                                           || (row.ValueTokens != null && TokenContains( row.ValueTokens, term )) )
                                      {
                                              state.PassesFilters = false;
                                              return true;
                                      }
                              }
                      }

                      if ( filters.TypeTerms.Length > 0 && !filters.TypeTerms.All( t => TokenContains( row.TypeTokens, t ) ) )
                      {
                              state.PassesFilters = false;
                              return true;
                      }
                      if ( filters.ComponentTerms.Length > 0 && !filters.ComponentTerms.All( t => TokenContains( row.ComponentTokens, t ) ) )
                      {
                              state.PassesFilters = false;
                              return true;
                      }
                      if ( filters.ObjectTerms.Length > 0 && !filters.ObjectTerms.All( t => TokenContains( row.ObjectTokens, t ) ) )
                      {
                              state.PassesFilters = false;
                              return true;
                      }
                      if ( filters.NameTerms.Length > 0 && !filters.NameTerms.All( t => TokenContains( row.NameTokens, t ) ) )
                      {
                              state.PassesFilters = false;
                              return true;
                      }
                      if ( filters.ValueTerms.Length > 0 && !filters.ValueTerms.All( t => TokenContains( row.ValueTokens, t ) ) )
                      {
                              state.PassesFilters = false;
                              return true;
                      }

                      if ( termEmpty )
                      {
                              return true;
                      }

                      componentMatch = includeTerms.All( t => TokenContains( row.ComponentTokens, t ) );
                      var nameMatch = includeTerms.All( t => TokenContains( row.NameTokens, t ) || (includeTypeTokens && TokenContains( row.TypeTokens, t )) );
                      var objectMatch = row.ObjectTokens != null && row.ObjectTokens.Length > 0 && includeTerms.All( t => TokenContains( row.ObjectTokens, t ) );
                      var valMatch = row.ValueTokens != null && row.ValueTokens.Length > 0 && includeTerms.All( t => TokenContains( row.ValueTokens, t ) );

                      state.ObjectNameMatch = objectMatch;

                      var propertyNameMatch = nameMatch;
                      var valueMatch = owner.valueSearchMode != ValueSearchMode.NamesOnly && valMatch;

                      if ( owner.valueSearchMode == ValueSearchMode.ValuesOnly )
                              propertyNameMatch = false;
                      if ( owner.valueSearchMode == ValueSearchMode.NamesOnly )
                              valueMatch = false;

                      if ( owner.matchTargetMode == MatchTargetMode.ComponentsOnly )
                      {
                              propertyNameMatch = false;
                              valueMatch = false;
                      }

                      var rowMatch = propertyNameMatch || valueMatch;
                      if ( owner.matchTargetMode == MatchTargetMode.ComponentsOnly )
                      {
                              rowMatch = componentMatch;
                      }

                      state.ValueMatch = valueMatch;
                      state.NameMatch = propertyNameMatch;
                      state.Match = rowMatch;
                      return true;
              }

              private static void UpdateAggregates( RowModel row, RowMatchResult state, bool componentMatch, bool componentsOnly, RowFilterState result )
              {
                      if ( state.Match )
                      {
                              result.MatchCount++;
                              if ( !string.IsNullOrEmpty( row.OwnerKey ) )
                              {
                                      result.ComponentMatches.TryGetValue( row.OwnerKey, out var cCount );
                                      result.ComponentMatches[row.OwnerKey] = cCount + 1;
                              }

                              if ( !string.IsNullOrEmpty( row.GameObjectKey ) )
                              {
                                      result.ObjectMatches.TryGetValue( row.GameObjectKey, out var gCount );
                                      result.ObjectMatches[row.GameObjectKey] = gCount + 1;
                              }
                      }

                      if ( componentsOnly && componentMatch )
                      {
                              if ( !string.IsNullOrEmpty( row.OwnerKey ) )
                              {
                                      result.ComponentMatches.TryGetValue( row.OwnerKey, out var cCount );
                                      result.ComponentMatches[row.OwnerKey] = Math.Max( 1, cCount );
                              }
                              if ( !string.IsNullOrEmpty( row.GameObjectKey ) )
                              {
                                      result.ObjectMatches.TryGetValue( row.GameObjectKey, out var gCount );
                                      result.ObjectMatches[row.GameObjectKey] = Math.Max( 1, gCount );
                              }
                      }

                      if ( state.ObjectNameMatch )
                      {
                              result.AnyObjectNameMatch = true;
                              if ( !string.IsNullOrEmpty( row.GameObjectKey ) )
                              {
                                      result.ObjectMatches.TryGetValue( row.GameObjectKey, out var gCount );
                                      result.ObjectMatches[row.GameObjectKey] = Math.Max( 1, gCount );
                              }
                      }

                      result.RowStates[row.Id] = state;
              }
      }

      private sealed class ComponentBuildQueue
      {
              private readonly Queue<ComponentGroup> queue = new();
              private const int MaxComponentsPerFrame = 2;

              public void Enqueue( ComponentGroup group )
              {
                      if ( group == null )
                              return;

                      queue.Enqueue( group );
              }

              public bool ProcessNextBatch( Action<ComponentGroup> builder )
              {
                      if ( builder == null || queue.Count == 0 )
                              return false;

                      var builtAny = false;
                      var processed = 0;

                      while ( queue.Count > 0 && processed < MaxComponentsPerFrame )
                      {
                              var group = queue.Dequeue();
                              if ( group == null || group.Body == null )
                                      continue;

                              group.Body.Layout?.Clear( true );
                              builder( group );
                              group.PropertiesBuilt = true;

                              builtAny = true;
                              processed++;
                      }

                      return builtAny;
              }

              public void Clear()
              {
                      queue.Clear();
              }

              public bool HasPending => queue.Count > 0;
      }

      private sealed class PropertyMetadataCache
      {
              private readonly System.Collections.Concurrent.ConcurrentDictionary<Type, PropertyInfo[]> propertyCache = new();

              public PropertyInfo[] GetPropertiesForComponentType( Type type )
              {
                      if ( type == null )
                              return Array.Empty<PropertyInfo>();

                      return propertyCache.GetOrAdd( type, t =>
                              t.GetProperties( BindingFlags.Instance | BindingFlags.Public )
                                      .Where( p => p.IsDefined( typeof( PropertyAttribute ), inherit: true ) )
                                      .ToArray() );
              }
      }

      public override void OnDestroyed()
      {
              var destroyStopwatch = debugLogging ? Stopwatch.StartNew() : null;

              Visible = false;
              isDisposed = true;
              pendingRebuild = false;
              CancelPendingScan();

              // Keep OnDestroyed as light as possible – the dock manager owns child
              // widgets and will dispose them. We only clear our managed lists so
              // subsequent callbacks become no-ops.
              var rowCount = rows.Count;
              var goCount = gameObjectGroups.Count;
              var compCount = componentGroups.Count;

              rows.Clear();
              rowModels.Clear();
              rowModelLookup.Clear();
              rowLookup.Clear();
              nextRowId = 0;
              componentGroups.Clear();
              gameObjectGroups.Clear();
              ClearTokenIndexes();
              target = null;

              if ( destroyStopwatch != null )
              {
                      destroyStopwatch.Stop();
                      Log.Info( $"[SearchFilter] OnDestroyed rows={rowCount} objects={goCount} components={compCount} ms={destroyStopwatch.ElapsedMilliseconds}" );
              }

              base.OnDestroyed();
      }
}