public/list/New-UiTree.ps1

function New-UiTree {
    <#
    .SYNOPSIS
        Creates a hierarchical tree view for displaying nested data.
    .DESCRIPTION
        Builds a WPF TreeView from nested hashtables, objects, or flat path-based data.
        For nested data, each item's display text comes from -DisplayProperty and children
        from -ChildrenProperty. For flat data like Get-ChildItem output, use -PathProperty
        to specify which property contains the hierarchical path (e.g., FullName).
         
        For parent-child relationships (like processes), use -IdProperty and -ParentIdProperty.
    .PARAMETER Variable
        Variable name for accessing this tree in button actions.
    .PARAMETER Items
        Array of tree items. Can be nested (with Children property) or flat with paths.
    .PARAMETER DisplayProperty
        Property name to display as the node text. Defaults to 'Name'.
    .PARAMETER ChildrenProperty
        Property name containing child items for nested data. Defaults to 'Children'.
    .PARAMETER PathProperty
        Property containing a hierarchical path (e.g., FullName for FileInfo objects).
        When specified, the tree builds hierarchy from path segments instead of nested data.
    .PARAMETER PathSeparator
        Separator character for path segments. Defaults to '\' for filesystem paths.
        Use ',' for AD Distinguished Names, '.' for namespaces.
    .PARAMETER ReversePath
        Reverse the path segment order. Use for AD Distinguished Names where leaf is first
        (CN=User,OU=Sales,DC=corp,DC=com becomes DC=com > DC=corp > OU=Sales > CN=User).
    .PARAMETER IdProperty
        Property containing unique ID for parent-child relationships (e.g., Id for processes).
    .PARAMETER ParentIdProperty
        Property containing parent's ID (e.g., ParentProcessId for processes).
    .PARAMETER Height
        Height of the tree control. Defaults to 200.
    .PARAMETER ExpandAll
        Expand all nodes on load.
    .PARAMETER WPFProperties
        Hashtable of additional WPF properties to set on the control.
    .EXAMPLE
        # Nested hashtable data
        $data = @(
            @{ Name = 'Root'; Children = @(
                @{ Name = 'Child 1' }
                @{ Name = 'Child 2'; Children = @(
                    @{ Name = 'Grandchild' }
                )}
            )}
        )
        New-UiTree -Variable 'tree' -Items $data
    .EXAMPLE
        # Filesystem
        Get-ChildItem C:\Temp -Recurse -Directory | New-UiTree -Variable 'folders' -PathProperty 'FullName'
    .EXAMPLE
        # Active Directory OUs - DN is reversed, comma-separated
        Get-ADOrganizationalUnit -Filter * | New-UiTree -Variable 'ous' -PathProperty 'DistinguishedName' -PathSeparator ',' -ReversePath
    .EXAMPLE
        # Process tree - parent/child by ID
        Get-Process | New-UiTree -Variable 'procs' -IdProperty 'Id' -ParentIdProperty 'Parent.Id' -DisplayProperty 'ProcessName'
    .EXAMPLE
        # .NET namespaces
        [AppDomain]::CurrentDomain.GetAssemblies().GetTypes() |
            Select -Unique FullName |
            New-UiTree -Variable 'types' -PathProperty 'FullName' -PathSeparator '.'
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Variable,

        [Parameter(ValueFromPipeline)]
        [object[]]$Items,

        [Parameter()]
        [string]$DisplayProperty = 'Name',

        [Parameter()]
        [string]$ChildrenProperty = 'Children',

        [Parameter()]
        [string]$PathProperty,

        [Parameter()]
        [string]$PathSeparator = '\',

        [switch]$ReversePath,

        [Parameter()]
        [string]$IdProperty,

        [Parameter()]
        [string]$ParentIdProperty,

        [int]$Height = 200,

        [switch]$ExpandAll,

        [Parameter()]
        [hashtable]$WPFProperties
    )

    begin {
        $collectedItems = [System.Collections.Generic.List[object]]::new()
    }

    process {
        if ($Items) {
            foreach ($item in $Items) { $collectedItems.Add($item) }
        }
    }

    end {
        $session   = Assert-UiSession -CallerName 'New-UiTree'
        $parent    = $session.CurrentParent
        $treeStyle = [System.Windows.Application]::Current.TryFindResource('ModernTreeViewStyle')

        # Create the tree control with base styling
        $tree = [System.Windows.Controls.TreeView]@{
            Height          = $Height
            BorderThickness = [System.Windows.Thickness]::new(1)
            Margin          = [System.Windows.Thickness]::new(4)
        }

        if ($treeStyle) { $tree.Style = $treeStyle }
        [PsUi.ThemeEngine]::RegisterElement($tree)

        # Use collected items from pipeline or direct parameter
        $allItems = if ($collectedItems.Count -gt 0) { $collectedItems } else { $Items }

        if ($IdProperty -and $ParentIdProperty -and $allItems) {
            # Build from parent-child ID relationships (process trees, org charts)
            $nodeMap = @{}
            
            # First pass: create nodes for each item
            foreach ($item in $allItems) {
                $id = $item.$IdProperty
                if ($null -eq $id) { continue }
                
                $displayText = if ($item.PSObject.Properties[$DisplayProperty]) { $item.$DisplayProperty } else { $id.ToString() }
                
                $node = [System.Windows.Controls.TreeViewItem]@{
                    Header = $displayText
                    Tag    = $item
                }
                if ($ExpandAll) { $node.IsExpanded = $true }
                
                $nodeMap[$id] = @{ Node = $node; Item = $item }
            }
            
            # Second pass: wire parent-child relatonships
            foreach ($id in $nodeMap.Keys) {
                $entry    = $nodeMap[$id]
                $item     = $entry.Item
                $node     = $entry.Node
                $parentId = $item.$ParentIdProperty
                
                if ($parentId -and $nodeMap.ContainsKey($parentId)) {
                    [void]$nodeMap[$parentId].Node.Items.Add($node)
                }
                else {
                    [void]$tree.Items.Add($node)
                }
            }
        }
        elseif ($PathProperty -and $allItems) {
            # Build hierarchy from path strings (filesystem, AD, registry)
            $nodeMap = @{}
            
            foreach ($item in $allItems | Sort-Object $PathProperty) {
                $path = $item.$PathProperty
                if (!$path) { continue }
                
                # Split path into segments and optionally reverse for DN-style paths
                $segments = $path.Split($PathSeparator, [System.StringSplitOptions]::RemoveEmptyEntries)
                if ($ReversePath) { [array]::Reverse($segments) }
                
                $currentPath = ''
                $parentNode  = $null
                
                for ($i = 0; $i -lt $segments.Count; $i++) {
                    $segment     = $segments[$i]
                    $currentPath = if ($currentPath) { "$currentPath$PathSeparator$segment" } else { $segment }
                    
                    # Reuse existing node or create new one
                    if ($nodeMap.ContainsKey($currentPath)) {
                        $parentNode = $nodeMap[$currentPath]
                    }
                    else {
                        $isLeaf  = ($i -eq $segments.Count - 1)
                        $tagData = if ($isLeaf) { $item } else { $null }
                        
                        $node = [System.Windows.Controls.TreeViewItem]@{
                            Header = $segment
                            Tag    = $tagData
                        }
                        
                        if ($ExpandAll) { $node.IsExpanded = $true }
                        
                        if (!$parentNode) {
                            [void]$tree.Items.Add($node)
                        }
                        else {
                            [void]$parentNode.Items.Add($node)
                        }
                        
                        $nodeMap[$currentPath] = $node
                        $parentNode = $node
                    }
                }
            }
        }
        elseif ($allItems) {
            # Walk nested data structure (hashtables with Children arrays)
            $buildNodes = {
                param($itemList, $parentNode)
                
                foreach ($item in $itemList) {
                    $displayText = $null
                    $children    = $null
                    
                    # Handle both hashtables and PSObjects
                    if ($item -is [hashtable]) {
                        $displayText = $item[$DisplayProperty]
                        $children    = $item[$ChildrenProperty]
                    }
                    elseif ($item.PSObject.Properties[$DisplayProperty]) {
                        $displayText = $item.$DisplayProperty
                        $children    = $item.$ChildrenProperty
                    }
                    else {
                        $displayText = $item.ToString()
                    }

                    $node = [System.Windows.Controls.TreeViewItem]@{
                        Header = $displayText
                        Tag    = $item
                    }

                    # Recurse into children if present
                    if ($children -and $children.Count -gt 0) {
                        & $buildNodes $children $node
                    }

                    if ($ExpandAll) { $node.IsExpanded = $true }

                    if (!$parentNode) {
                        [void]$tree.Items.Add($node)
                    }
                    else {
                        [void]$parentNode.Items.Add($node)
                    }
                }
            }
            
            & $buildNodes $allItems $null
        }

        # Register control for variable hydration
        $session.AddControlSafe($Variable, $tree)

        # Bubble scroll events to parent ScrollViewer so tree doesn't swallow them
        $tree.Add_PreviewMouseWheel({
            param($sender, $eventArgs)
            if (!$eventArgs.Handled) {
                $eventArgs.Handled = $true
                $newEvent = [System.Windows.Input.MouseWheelEventArgs]::new($eventArgs.MouseDevice, $eventArgs.Timestamp, $eventArgs.Delta)
                $newEvent.RoutedEvent = [System.Windows.UIElement]::MouseWheelEvent
                $newEvent.Source = $sender
                $parentElement = $sender.Parent -as [System.Windows.UIElement]
                if ($parentElement) { $parentElement.RaiseEvent($newEvent) }
            }
        })

        if ($WPFProperties) {
            Set-UiProperties -Control $tree -Properties $WPFProperties
        }

        # Attach to parent container
        if ($parent -is [System.Windows.Controls.Panel]) {
            [void]$parent.Children.Add($tree)
        }
        elseif ($parent -is [System.Windows.Controls.ItemsControl]) {
            [void]$parent.Items.Add($tree)
        }
        elseif ($parent -is [System.Windows.Controls.ContentControl]) {
            $parent.Content = $tree
        }
    }
}