# Tree Node

Use `ui-tree-node` when you need one expandable branch or want fine-grained control over recursive rows. For full collections of root nodes, use `ui-tree` instead.

## Import
```ts
import { TreeNodeComponent, type TreeNode } from 'ui';
```

## Leaf and parent rows
```ts
import { Component } from '@angular/core';
import { TreeNodeComponent, type TreeNode } from 'ui';

@Component({
  selector: 'app-tree-node-basic-demo',
  standalone: true,
  imports: [TreeNodeComponent],
  template: `
    <div
      style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-start;width:100%;max-width:46rem"
    >
      <div
        style="flex:1 1 18rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Leaf row
        </p>
        <ui-tree-node [node]="leafNode" />
      </div>

      <div
        style="flex:1 1 18rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Parent row
        </p>
        <ui-tree-node [node]="parentNode" />
      </div>
    </div>
  `,
})
export class TreeNodeBasicDemoComponent {
  protected readonly leafNode: TreeNode = {
    id: 'leaf',
    label: 'Readme.md',
    icon: 'document',
    hasChildren: false,
  };

  protected readonly parentNode: TreeNode = {
    id: 'parent',
    label: 'Project files',
    icon: 'folder',
    hasChildren: true,
    expanded: true,
    children: [
      { id: 'brief', label: 'Brief.docx', icon: 'document', hasChildren: false },
      { id: 'notes', label: 'Notes.md', icon: 'document', hasChildren: false },
    ],
  };
}
```

## Interaction behavior
```ts
import { Component } from '@angular/core';
import { TreeNodeComponent, type TreeNode } from 'ui';

@Component({
  selector: 'app-tree-node-behavior-demo',
  standalone: true,
  imports: [TreeNodeComponent],
  template: `
    <div
      style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-start;width:100%;max-width:50rem"
    >
      <div
        style="flex:1 1 18rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Browser style
        </p>
        <ui-tree-node
          [node]="browserNode"
          chevronPosition="before"
          [showSelectionIndicator]="true"
          indicatorPosition="vertical"
        />
      </div>

      <div
        style="flex:1 1 18rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Button-like interaction
        </p>
        <ui-tree-node
          [node]="actionNode"
          chevronPosition="after"
          [showSelectionIndicator]="true"
          indicatorPosition="horizontal"
          [asButton]="true"
          [expandOnClick]="true"
          [selectOnClick]="true"
          variant="secondary"
        />
      </div>
    </div>
  `,
})
export class TreeNodeBehaviorDemoComponent {
  protected readonly browserNode: TreeNode = {
    id: 'browser',
    label: 'src',
    icon: 'folder',
    selected: true,
    hasChildren: true,
    expanded: true,
    children: [
      { id: 'app', label: 'app', icon: 'folder', hasChildren: false },
      { id: 'styles', label: 'styles.scss', icon: 'document', hasChildren: false },
    ],
  };

  protected readonly actionNode: TreeNode = {
    id: 'action',
    label: 'Workspace',
    icon: 'folder',
    selected: true,
    hasChildren: true,
    expanded: true,
    children: [
      { id: 'overview', label: 'Overview', icon: 'home', hasChildren: false },
      { id: 'activity', label: 'Activity', icon: 'pulse', hasChildren: false },
    ],
  };
}
```

## Common states
```ts
import { Component } from '@angular/core';
import { TreeNodeComponent, type TreeNode } from 'ui';

@Component({
  selector: 'app-tree-node-states-demo',
  standalone: true,
  imports: [TreeNodeComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1rem;width:100%;max-width:52rem"
    >
      <div
        style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Default
        </p>
        <ui-tree-node [node]="defaultNode" />
      </div>

      <div
        style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Selected
        </p>
        <ui-tree-node [node]="selectedNode" [showSelectionIndicator]="true" />
      </div>

      <div
        style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Disabled
        </p>
        <ui-tree-node [node]="disabledNode" />
      </div>
    </div>
  `,
})
export class TreeNodeStatesDemoComponent {
  protected readonly defaultNode: TreeNode = {
    id: 'default',
    label: 'Design tokens.json',
    icon: 'document',
    hasChildren: false,
  };

  protected readonly selectedNode: TreeNode = {
    id: 'selected',
    label: 'Brand assets',
    icon: 'folder',
    selected: true,
    hasChildren: true,
    expanded: true,
    children: [{ id: 'logo', label: 'Logo.svg', icon: 'image', hasChildren: false }],
  };

  protected readonly disabledNode: TreeNode = {
    id: 'disabled',
    label: 'Archived release',
    icon: 'archive',
    disabled: true,
    hasChildren: false,
  };
}
```

## Custom content templates
```ts
import { CommonModule } from '@angular/common';
import { Component, TemplateRef, viewChild } from '@angular/core';
import { BadgeComponent, TreeNodeComponent, type TreeNode } from 'ui';

interface FileTreeNode extends TreeNode<FileTreeNode> {
  data?: {
    meta: string;
    status?: string;
  };
}

@Component({
  selector: 'app-tree-node-content-template-demo',
  standalone: true,
  imports: [CommonModule, BadgeComponent, TreeNodeComponent],
  template: `
    <div
      style="width:100%;max-width:28rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <ui-tree-node [node]="node" [contentTemplate]="contentTemplateRef() ?? null">
        <ng-template #content let-node>
          <div
            style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;width:100%;min-width:0"
          >
            <div style="min-width:0;display:flex;flex-direction:column;gap:0.125rem">
              <span
                style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600"
              >
                {{ node.label }}
              </span>
              <span style="font-size:0.75rem;color:var(--color-neutral-foreground2-rest)">
                {{ node.data?.meta }}
              </span>
            </div>
            @if (node.data?.status) {
              <ui-badge
                [text]="node.data.status"
                size="small"
                appearance="subtle"
                variant="secondary"
                shape="rounded"
              />
            }
          </div>
        </ng-template>
      </ui-tree-node>
    </div>
  `,
})
export class TreeNodeContentTemplateDemoComponent {
  protected readonly node: FileTreeNode = {
    id: 'content-template',
    label: 'Release notes',
    icon: 'document',
    hasChildren: true,
    expanded: true,
    data: {
      meta: 'Updated 2 hours ago',
      status: 'Synced',
    },
    children: [
      {
        id: 'content-template-child',
        label: 'Draft.md',
        icon: 'document',
        hasChildren: false,
        data: {
          meta: '3 collaborators',
          status: 'Draft',
        },
      },
    ],
  };

  protected contentTemplateRef = viewChild<TemplateRef<any>>('content');
}
```

## Quick actions
```ts
import { CommonModule } from '@angular/common';
import { Component, TemplateRef, signal, viewChild } from '@angular/core';
import { ButtonComponent, TreeNodeComponent, type TreeNode } from 'ui';

@Component({
  selector: 'app-tree-node-quick-actions-demo',
  standalone: true,
  imports: [CommonModule, ButtonComponent, TreeNodeComponent],
  template: `
    <div
      style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-start;width:100%;max-width:52rem"
    >
      <div
        style="flex:0 0 24rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-tree-node
          [node]="node"
          [showQuickActions]="true"
          [quickActionsTemplate]="quickActionsTemplateRef() ?? null"
        />
      </div>

      <div
        style="flex:1 1 16rem;padding:0.875rem 1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <p
          style="margin:0 0 0.5rem;font-size:0.75rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-neutral-foreground2-rest)"
        >
          Last action
        </p>
        <div style="font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground-rest)">
          {{ lastAction() || 'No quick action used yet.' }}
        </div>
      </div>
    </div>

    <ng-template #quickActions let-node>
      <div style="display:flex;gap:0.375rem">
        <ui-button
          appearance="tint"
          variant="secondary"
          icon="edit"
          (click)="onQuickAction('Rename', node); $event.stopPropagation()"
        />
        <ui-button
          appearance="tint"
          variant="danger"
          icon="delete"
          (click)="onQuickAction('Archive', node); $event.stopPropagation()"
        />
      </div>
    </ng-template>
  `,
})
export class TreeNodeQuickActionsDemoComponent {
  protected readonly node: TreeNode = {
    id: 'quick-actions',
    label: 'Q2 planning',
    icon: 'folder',
    hasChildren: true,
    expanded: true,
    children: [
      { id: 'timeline', label: 'Timeline.xlsx', icon: 'document', hasChildren: false },
      { id: 'risks', label: 'Risks.md', icon: 'document', hasChildren: false },
    ],
  };

  protected readonly lastAction = signal('');
  protected quickActionsTemplateRef = viewChild<TemplateRef<any>>('quickActions');

  protected onQuickAction(action: string, node: TreeNode): void {
    this.lastAction.set(`${action} on "${node.label}"`);
  }
}
```

## Recursive nesting
```ts
import { Component } from '@angular/core';
import { TreeNodeComponent, type TreeNode } from 'ui';

@Component({
  selector: 'app-tree-node-recursive-demo',
  standalone: true,
  imports: [TreeNodeComponent],
  template: `
    <div
      style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-start;width:100%;max-width:54rem"
    >
      <div
        style="flex:0 0 24rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-tree-node [node]="node" [showSelectionIndicator]="true" [expandOnClick]="true" />
      </div>

      <div
        style="flex:1 1 18rem;padding:0.875rem 1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <p
          style="margin:0 0 0.5rem;font-size:0.75rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-neutral-foreground2-rest)"
        >
          Recursive use
        </p>
        <div style="display:grid;gap:0.5rem;font-size:0.875rem;line-height:1.5">
          <div><code>ui-tree-node</code> can render nested children on its own.</div>
          <div>
            Use it directly when you need a single expandable branch, not a full tree container.
          </div>
          <div>For full collections of roots, prefer <code>ui-tree</code>.</div>
        </div>
      </div>
    </div>
  `,
})
export class TreeNodeRecursiveDemoComponent {
  protected readonly node: TreeNode = {
    id: 'root',
    label: 'Workspace',
    icon: 'folder',
    hasChildren: true,
    expanded: true,
    children: [
      {
        id: 'project-a',
        label: 'Project A',
        icon: 'folder',
        hasChildren: true,
        expanded: true,
        children: [
          { id: 'brief', label: 'Brief.docx', icon: 'document', hasChildren: false },
          { id: 'board', label: 'Board.url', icon: 'link', hasChildren: false, selected: true },
        ],
      },
      {
        id: 'project-b',
        label: 'Project B',
        icon: 'folder',
        hasChildren: true,
        children: [{ id: 'notes', label: 'Notes.md', icon: 'document', hasChildren: false }],
      },
    ],
  };
}
```

## Drop targets inside a branch
```ts
import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import { ButtonComponent, TreeNodeComponent, type TreeNode } from 'ui';

@Component({
  selector: 'app-tree-node-drag-drop-demo',
  standalone: true,
  imports: [CommonModule, ButtonComponent, TreeNodeComponent],
  template: `
    <div
      style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-start;width:100%;max-width:54rem"
    >
      <div
        style="flex:0 0 24rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-tree-node
          [node]="node()"
          [draggable]="true"
          [dropZone]="true"
          (drop)="onDrop($event)"
        />
      </div>

      <div
        style="flex:1 1 18rem;display:flex;flex-direction:column;gap:0.75rem;padding:0.875rem 1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <div style="font-size:0.875rem;line-height:1.5">
          Reorder child items inside one branch to preview drop zones on <code>ui-tree-node</code>.
        </div>

        <ui-button variant="secondary" appearance="outline" size="small" (click)="resetNode()">
          Reset branch
        </ui-button>

        <div style="font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground-rest)">
          {{ lastAction() || 'No move yet.' }}
        </div>
      </div>
    </div>
  `,
})
export class TreeNodeDragDropDemoComponent {
  protected readonly node = signal<TreeNode>(this.createNode());
  protected readonly lastAction = signal('');

  protected onDrop(event: { node: TreeNode; position: 'before' | 'after' | 'inside' }): void {
    this.lastAction.set(`Drop target: "${event.node.label}" (${event.position}).`);
  }

  protected resetNode(): void {
    this.node.set(this.createNode());
    this.lastAction.set('Branch reset to initial state.');
  }

  private createNode(): TreeNode {
    return {
      id: 'drop-root',
      label: 'Assets',
      icon: 'folder',
      hasChildren: true,
      expanded: true,
      children: [
        { id: 'cover', label: 'Cover.png', icon: 'image', hasChildren: false },
        { id: 'icons', label: 'Icons.svg', icon: 'image', hasChildren: false },
        { id: 'copy', label: 'Copy.txt', icon: 'document', hasChildren: false },
      ],
    };
  }
}
```

## Accessibility

### Tree-item semantics
`ui-tree-node` exposes `role="treeitem"` and reflects hierarchical state through attributes such as `aria-expanded`, `aria-selected`, `aria-disabled`, and `aria-level` when applicable.

### Keyboard behavior
Keyboard behavior follows the tree-item model.

| Key | Action |
| --- | --- |
| Enter / Space | Activates the focused node. |
| ArrowRight | Expands a collapsed parent node. |
| ArrowLeft | Collapses an expanded parent node. |
| * | Expands sibling branches at the same level when supported by the current context. |

### Custom content and quick actions
If you replace the default row content with a custom template, preserve a clear primary label. Quick actions and extra controls need their own accessible labels and should not make the main tree-item purpose ambiguous.
