# Tree

A nested tree view for hierarchical data: expandable nodes with roving-tabindex navigation, single or multi selection, and typeahead.

A nested `role="tree"` → `treeitem` → `group` → `treeitem` widget (file explorers, nav trees, category pickers). Roving-tabindex focus management (DOM focus rides the `treeitem`), full keyboard interaction, RTL arrow mirroring, and `aria-level` / `aria-setsize` / `aria-posinset` wiring.

Selection and expansion are two independent models: `value` (selected nodes) and `expanded` (open nodes). Expansion is always multi; only `value` honours `multiple`.

Both models are `readonly T[]` over the node value type, which `ForTree<T = string>` infers from `[(value)]` / `[(expanded)]` — the same shape `ForTable` uses for its selected and open rows. Node identity is resolved by `compareWith`, which defaults to `===`; bind `[compareWith]="(a, b) => a.id === b.id"` when your nodes are objects you re-create (a `descendantsOf` that maps fresh objects is the case that needs it — under `===` a fully-checked subtree reports `aria-checked="false"`).

## Anatomy

```html
<ul forTree [(value)]="selected" [(expanded)]="expanded" aria-label="Files">
  <li forTreeItem value="src">
    <div forTreeItemLabel>
      <span forTreeItemToggle>▸</span>
      src
    </div>

    <!-- rendered only while the node is expanded -->
    <ul forTreeGroup>
      <li forTreeItem value="main.ts">
        <div forTreeItemLabel>main.ts</div>
      </li>
    </ul>
  </li>
</ul>
```

In `selectionMode="checkbox"`, place a checkbox surface inside the label:

```html
<div forTreeItemLabel>
  <span forTreeItemToggle>▸</span>
  <span forTreeItemCheckbox>
    <span forTreeItemCheckboxIndicator>✓</span>
  </span>
  src
</div>
```

## Examples

Trees are recursive, and the idiomatic Angular shape is a small **recursive component** for the node. This keeps dependency injection correct at every depth: each node component nests its element injector under its enclosing `[forTreeGroup]`, so `[forTreeItem]` resolves the right level / container automatically.

```ts
import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';
import {
  ForTree,
  ForTreeGroup,
  ForTreeItem,
  ForTreeItemLabel,
  ForTreeItemToggle,
} from 'forty-cdk/tree';

interface Node {
  id: string;
  name: string;
  children?: Node[];
}

@Component({
  selector: 'app-tree-node',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [ForTreeItem, ForTreeItemLabel, ForTreeItemToggle, ForTreeGroup, TreeNode],
  // `display: contents` keeps this wrapper out of layout and the a11y tree, so
  // the <li role="treeitem"> stays a direct child of <ul role="group">.
  host: { style: 'display: contents' },
  template: `
    <li forTreeItem class="tree-item" [value]="node().id">
      <div forTreeItemLabel>
        @if (node().children?.length) {
          <span forTreeItemToggle class="tree-toggle">▸</span>
        }
        {{ node().name }}
      </div>

      @if (node().children?.length && expanded().includes(node().id)) {
        <ul forTreeGroup>
          @for (child of node().children ?? []; track child.id) {
            <app-tree-node [node]="child" [expanded]="expanded()" />
          }
        </ul>
      }
    </li>
  `,
})
export class TreeNode {
  readonly node = input.required<Node>();
  readonly expanded = input.required<readonly string[]>();
}

@Component({
  selector: 'app-files',
  imports: [ForTree, TreeNode],
  template: `
    <ul forTree [(value)]="selected" [(expanded)]="expanded" aria-label="File system">
      @for (n of roots; track n.id) {
        <app-tree-node [node]="n" [expanded]="expanded()" />
      }
    </ul>
  `,
})
export class Files {
  readonly selected = signal<readonly string[]>([]);
  readonly expanded = signal<readonly string[]>([]);
  readonly roots: Node[] = [
    {
      id: 'documents',
      name: 'Documents',
      children: [
        { id: 'resume', name: 'Resume' },
        { id: 'projects', name: 'Projects', children: [{ id: 'alpha', name: 'Alpha' }] },
      ],
    },
    { id: 'readme', name: 'Readme' },
  ];
}
```

> **Why not `ngTemplateOutlet`?** A single recursive `<ng-template>` instantiated with `[ngTemplateOutlet]` resolves dependency injection from where the template is **declared**, not where it is inserted — so a nested `[forTreeItem]` would inject the root tree as its container instead of its enclosing `[forTreeGroup]`, breaking `aria-level` and visible-order navigation. The recursive component above avoids this. If you must use `ngTemplateOutlet`, pass an explicit `[ngTemplateOutletInjector]` captured at each insertion point.

Mounting is the consumer's responsibility: wrap `[forTreeGroup]` in `@if (expanded().includes(node.id))` so a collapsed parent drops its subtree. A node is treated as a **parent** (and emits `aria-expanded` / `data-state`) only when a `[forTreeItemToggle]` is registered inside it — leaves render no toggle and emit neither, matching the APG "end nodes lack `aria-expanded`" rule.

## Multi select

```html
<ul forTree multiple [(value)]="selected" [(expanded)]="expanded" aria-label="Files">
  ...
</ul>
```

In multi mode `Space` toggles the focused node; `Shift+ArrowUp/Down` extends; `Shift+Space` selects the contiguous range from the anchor; `Ctrl/Cmd+A` selects every visible enabled node (or clears when all are already selected).

## Checkbox selection

`selectionMode="checkbox"` switches each `treeitem` to `aria-checked` (instead of `aria-selected`) and makes every node toggle independently — `multiple` is not required. Place `[forTreeItemCheckbox]` and `[forTreeItemCheckboxIndicator]` inside the label for a visible checkbox surface.

```ts
import {
  ForTree,
  ForTreeGroup,
  ForTreeItem,
  ForTreeItemCheckbox,
  ForTreeItemCheckboxIndicator,
  ForTreeItemLabel,
  ForTreeItemToggle,
} from 'forty-cdk/tree';

@Component({
  selector: 'app-tree-node',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    ForTreeItem,
    ForTreeItemLabel,
    ForTreeItemToggle,
    ForTreeGroup,
    ForTreeItemCheckbox,
    ForTreeItemCheckboxIndicator,
    TreeNode,
  ],
  host: { style: 'display: contents' },
  template: `
    <li forTreeItem [value]="node().id">
      <div forTreeItemLabel>
        @if (node().children?.length) {
          <span forTreeItemToggle>▸</span>
        }
        <span forTreeItemCheckbox>
          <span forTreeItemCheckboxIndicator>✓</span>
        </span>
        {{ node().name }}
      </div>

      @if (node().children?.length && expanded().includes(node().id)) {
        <ul forTreeGroup>
          @for (child of node().children ?? []; track child.id) {
            <app-tree-node [node]="child" [expanded]="expanded()" />
          }
        </ul>
      }
    </li>
  `,
})
export class TreeNode {
  readonly node = input.required<Node>();
  readonly expanded = input.required<readonly string[]>();
}

@Component({
  selector: 'app-categories',
  imports: [ForTree, TreeNode],
  template: `
    <ul
      forTree
      selectionMode="checkbox"
      [(value)]="selected"
      [(expanded)]="expanded"
      aria-label="Categories"
    >
      @for (n of roots; track n.id) {
        <app-tree-node [node]="n" [expanded]="expanded()" />
      }
    </ul>
  `,
})
export class Categories {
  readonly selected = signal<readonly string[]>([]);
  readonly expanded = signal<readonly string[]>([]);
  readonly roots: Node[] = [
    { id: 'a', name: 'Alpha' },
    { id: 'b', name: 'Beta', children: [{ id: 'b1', name: 'Beta 1' }] },
  ];
}
```

### Cascade selection

Add `cascade` and `[descendantsOf]` to enable tri-state propagation. Checking a parent selects it and all its descendants atomically (including collapsed / unmounted ones), and a parent derives `aria-checked="mixed"` / `data-checked="mixed"` when only some descendants are checked. The `descendantsOf` function must return every selectable descendant id of the given node (not just direct children).

```ts
@Component({
  selector: 'app-categories',
  imports: [ForTree, TreeNode],
  template: `
    <ul
      forTree
      selectionMode="checkbox"
      cascade
      [descendantsOf]="descendantsFn"
      [(value)]="selected"
      [(expanded)]="expanded"
      aria-label="Categories"
    >
      @for (n of roots; track n.id) {
        <app-tree-node [node]="n" [expanded]="expanded()" />
      }
    </ul>
  `,
})
export class Categories {
  readonly selected = signal<readonly string[]>([]);
  readonly expanded = signal<readonly string[]>([]);
  readonly roots: Node[] = [
    {
      id: 'fruits',
      name: 'Fruits',
      children: [
        { id: 'apple', name: 'Apple' },
        { id: 'pear', name: 'Pear' },
      ],
    },
  ];

  readonly descendantsFn = (id: string): readonly string[] => {
    const flatten = (nodes: Node[]): string[] =>
      nodes.flatMap((n) => [n.id, ...flatten(n.children ?? [])]);
    const find = (nodes: Node[]): Node | undefined =>
      nodes.find((n) => n.id === id) ?? nodes.flatMap((n) => find(n.children ?? [])).find(Boolean);
    const node = find(this.roots);
    return node?.children ? flatten(node.children) : [];
  };
}
```

## Filtering

forty-cdk ships no filtering machinery — matching stays consumer-owned. The library exports one pure helper, `expandToReveal`, that translates the matched set into the ancestor values you need to expand so every match becomes visible.

**Three-step recipe:**

1. **Filter your own data and re-render.** Derive a filtered node list with `computed()` and drive the tree's `@for` off that signal. The library adds no filtering engine, empty-state pieces, or snapshot logic.
2. **Expand ancestors with `expandToReveal`.** Call `expandToReveal(matches, ancestorsOf)` to get the unique ancestor values to merge into `[(expanded)]`. The helper is pure — it has no Angular reactivity, no DOM, and no side effects.
3. **Highlight matched text with consumer CSS.** Wrap matched text in a `<mark>` element or apply a `.match` class while rendering filtered labels. No new data attribute is emitted by the library.

```ts
import { linkedSignal } from '@angular/core';
import { expandToReveal } from 'forty-cdk/tree';

readonly query = signal('');
readonly filtered = computed(() => filterNodes(this.roots, this.query()));

// Two-way bindable via [(expanded)]: manual expand/collapse is preserved through
// `previous.value`, and a new query re-reveals every match by re-deriving from
// the matched set. `linkedSignal` is the idiomatic replacement for
// `effect(() => this.expanded.update(...))` — no state written inside an effect.
readonly expanded = linkedSignal<readonly string[], readonly string[]>({
  source: () => collectIds(this.filtered()),
  computation: (matches, previous) => [
    ...new Set([...(previous?.value ?? []), ...expandToReveal(matches, this.ancestorsOf)]),
  ],
});

// Returns a node's ancestor ids from the consumer's own hierarchy.
ancestorsOf = (id: string): readonly string[] => { /* walk roots, return the path */ };
```

`expandToReveal` accepts any `Iterable<T>` (array, `Set`, generator) of node values. Root-level matches contribute nothing — a root has no ancestors to expand.

## API

### `ForTree`

| Property                | Type                                | Description                                                                                                                                                                                                                                                |
| ----------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value`                 | `model<readonly T[]>`               | Two-way bindable. Selected node values. Single mode keeps 0 or 1; multi any number.<br>**Default:** `[]`                                                                                                                                                   |
| `expanded`              | `model<readonly T[]>`               | Two-way bindable. Open (expanded) parent node values. Always multi.<br>**Default:** `[]`                                                                                                                                                                   |
| `selected`              | `Signal<T \| null>`                 | Read-only single-select convenience view of `value`: the sole selected value, or `null` when none / many are selected.<br>**Default:** —                                                                                                                   |
| `compareWith`           | `input<(a: T, b: T) => boolean>`    | Equality comparator for node values — selection and expansion membership, cascade descendants, the range anchor, and drag-drop resolution all route through it.<br>**Default:** `(a, b) => a === b`                                                        |
| `multiple`              | `input<boolean>`                    | When true, multiple nodes can be selected.<br>**Default:** `false`                                                                                                                                                                                         |
| `disabled`              | `input<boolean>`                    | Disables the whole tree. Reflected as `aria-disabled` / `data-disabled`.<br>**Default:** —                                                                                                                                                                 |
| `orientation`           | `input<'vertical' \| 'horizontal'>` | Navigation axis. `'vertical'` (ArrowUp/Down move; ArrowLeft/Right expand/collapse). Reflected as `aria-orientation` / `data-orientation`.<br>**Default:** `'vertical'`                                                                                     |
| `ariaLabel`             | `input<string \| null>`             | Reactive accessible name, reflected as `aria-label`. Prefer native `aria-labelledby` when a visible label exists.<br>**Default:** `null` (and empty) emits no attribute                                                                                    |
| `dir`                   | `input<'ltr' \| 'rtl' \| null>`     | Writing direction. `null` resolves the inherited ambient direction; an explicit value wins. Reflected to the host `dir` attribute and mirrors the expand/collapse arrows in RTL.<br>**Default:** `null`                                                    |
| `selectionFollowsFocus` | `input<boolean>`                    | Single-mode only. When true, arrow navigation also selects the focused node.<br>**Default:** from `provideForTreeDefaults`                                                                                                                                 |
| `selectionMode`         | `input<'highlight' \| 'checkbox'>`  | Selection presentation. `'highlight'` uses `aria-selected`; `'checkbox'` uses `aria-checked` and renders the checkbox anatomy (inherently multi-select).<br>**Default:** `'highlight'`                                                                     |
| `cascade`               | `input<boolean>`                    | Enables cascade selection in `selectionMode="checkbox"`: checking / unchecking a node propagates to all descendants, and a parent reports `aria-checked="mixed"` when only some descendants are checked. Requires `descendantsOf`.<br>**Default:** `false` |
| `descendantsOf`         | `input<(value: T) => readonly T[]>` | Returns the selectable descendant values of a node (excluding the node itself). Required when `cascade` is `true`; the tree throws a `[forty-cdk/tree]` error otherwise.<br>**Default:** —                                                                 |

| Data attribute     | Values                     |
| ------------------ | -------------------------- |
| `data-orientation` | `vertical` \| `horizontal` |
| `data-disabled`    | present \| absent          |

### `ForTreeItem`

| Property    | Type                | Description                                                                                                |
| ----------- | ------------------- | ---------------------------------------------------------------------------------------------------------- |
| `value`     | `input.required<T>` | The node's value. Must be unique within the tree.<br>**Default:** —                                        |
| `disabled`  | `input<boolean>`    | Disables this node: not selectable, skipped by keyboard navigation.<br>**Default:** —                      |
| `textValue` | `input<string>`     | Typeahead text override. Falls back to the `[forTreeItemLabel]` text content when empty.<br>**Default:** — |

| Data attribute     | Values                                                  |
| ------------------ | ------------------------------------------------------- |
| `data-state`       | `open` \| `closed` (parent items only)                  |
| `data-selected`    | present \| absent                                       |
| `data-highlighted` | present \| absent                                       |
| `data-disabled`    | present \| absent                                       |
| `data-checked`     | `"true"` \| `"false"` \| `"mixed"` (checkbox mode only) |

A `[forTreeItem]` emits `data-state` only when it is a parent (a `[forTreeItemToggle]` is registered inside it); leaves carry neither `data-state` nor `aria-expanded`. Expansion (`data-state`) and selection (`data-selected`) are independent hooks because a node can be both expandable and selected at once.

### `ForTreeItemToggle`

| Data attribute | Values             |
| -------------- | ------------------ |
| `data-state`   | `open` \| `closed` |

### `ForTreeItemCheckbox`

| Data attribute | Values                                      |
| -------------- | ------------------------------------------- |
| `data-state`   | `checked` \| `unchecked` \| `indeterminate` |

### `ForTreeItemCheckboxIndicator`

| Data attribute | Values                                      |
| -------------- | ------------------------------------------- |
| `data-state`   | `checked` \| `unchecked` \| `indeterminate` |

## Keyboard

Vertical, LTR (mirrored for `dir="rtl"`):

| Key                     | Behavior                                                                                      |
| ----------------------- | --------------------------------------------------------------------------------------------- |
| **ArrowDown / ArrowUp** | Move focus to the next / previous visible node (no wrap; collapsed subtrees are skipped).     |
| **ArrowRight**          | Closed parent → expand (focus stays); open parent → focus first child; leaf → no-op.          |
| **ArrowLeft**           | Open parent → collapse (focus stays); otherwise → focus the parent node; closed root → no-op. |
| **Home / End**          | First / last visible node.                                                                    |
| **Enter**               | Select / activate the focused node.                                                           |
| **Space**               | Single: select. Multi: toggle the focused node's selection.                                   |
| **\***                  | Expand every sibling parent at the focused node's level.                                      |
| **type a character**    | Typeahead: focus the next visible node whose label starts with the buffer.                    |
| **Shift+ArrowUp/Down**  | Multi: move focus and toggle the new node's selection.                                        |
| **Shift+Space**         | Multi: select the contiguous range from the anchor to the focused node.                       |
| **Ctrl/Cmd+A**          | Multi: select every visible enabled node (toggles off when all are already selected).         |

Under `dir="rtl"` the expand / collapse arrows swap: **ArrowLeft** expands and **ArrowRight** collapses.

## Scope defaults

```ts
import { provideForTreeDefaults } from 'forty-cdk/tree';

// app config or a component's providers
providers: [provideForTreeDefaults({ selectionFollowsFocus: true })];
```

## Virtualization

For very large trees (thousands of nodes) bind `[totalCount]` to switch to an **activedescendant focus model** over a consumer-owned virtualized window.

### Opt-in API

#### `ForTree` additions

| API             | Type                                            | Description                                                                                                                      |
| --------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `totalCount`    | `input<number \| undefined>`                    | Total flattened node count. Setting this switches the tree to the activedescendant focus model. Leave unset for roving-tabindex. |
| `visibleRange`  | `input<readonly [number, number] \| undefined>` | Inclusive-exclusive `[start, end)` index range of the currently rendered nodes. Provided by `injectVirtualizer`.                 |
| `scrollToIndex` | `output<number>`                                | Emitted when keyboard navigation reaches a node outside the rendered window. Forward to `injectVirtualizer`'s `scrollToIndex`.   |

#### `ForTreeItem` additions (virtualized path only)

| API         | Type                    | Description                                                                                                                                            |
| ----------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `itemIndex` | `input<number \| null>` | Zero-based absolute index in the flattened node list. **Required** in the virtualized path. Leave unset (default `null`) outside the virtualized path. |
| `level`     | `input<number \| null>` | Tree depth of this node (1-based). Overrides the container-derived `aria-level` in the virtualized path.                                               |
| `setSize`   | `input<number \| null>` | Total siblings at this node's level. Overrides the container-derived `aria-setsize` in the virtualized path.                                           |
| `posInSet`  | `input<number \| null>` | 1-based position among siblings (matches `aria-posinset`). Overrides the container-derived value in the virtualized path.                              |

**Naming note:** `[posInSet]` is the per-level `aria-posinset` (position among siblings at this level, 1-based). It is **not** the absolute flat index — that is `[itemIndex]`. This matches the ARIA attribute name and is intentionally different from how some other APIs name it.

### Focus-model switch

| Mode                              | Tree host tabindex | Item tabindex   | Focus mechanism                     |
| --------------------------------- | ------------------ | --------------- | ----------------------------------- |
| Standard (no `totalCount`)        | none               | `0` on one item | DOM focus rides the item (roving)   |
| Virtualized (`totalCount` is set) | `0`                | `-1` always     | `aria-activedescendant` on the host |

### Navigation flow

1. Consumer flattens their visible tree into a flat list, computing `level`, `setSize`, `posInSet`, and `itemIndex` for each node (using the true sibling totals — off-window siblings contribute their real counts because the consumer knows them).
2. `injectVirtualizer({ count: flatCount, estimateSize, scrollElement })` drives the render window.
3. The tree host receives `(scrollToIndex)` when keyboard navigation needs a node outside the window; the consumer forwards the index to `v.scrollToIndex(idx, { align: 'auto' })`.
4. Once the target node mounts (carrying the requested `[itemIndex]`), the bridge effect resolves the pending activedescendant.

### Consumer example

```ts
import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  computed,
  signal,
  viewChild,
} from '@angular/core';
import { ForTree, ForTreeItem, ForTreeItemLabel, ForTreeItemToggle } from 'forty-cdk/tree';
import { injectVirtualizer } from 'forty-cdk/virtualization';

interface TreeNode {
  value: string;
  label: string;
  children?: TreeNode[];
}

interface FlatNode {
  value: string;
  label: string;
  level: number;
  setSize: number;
  posInSet: number;
  itemIndex: number;
  expandable: boolean;
}

function flatten(nodes: TreeNode[], expanded: ReadonlySet<string>, level = 1): FlatNode[] {
  const result: FlatNode[] = [];
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]!;
    result.push({
      value: node.value,
      label: node.label,
      level,
      setSize: nodes.length,
      posInSet: i + 1,
      itemIndex: result.length,
      expandable: !!node.children?.length,
    });
    if (node.children?.length && expanded.has(node.value)) {
      result.push(...flatten(node.children, expanded, level + 1));
    }
  }
  return result;
}

@Component({
  selector: 'app-virtual-tree',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [ForTree, ForTreeItem, ForTreeItemLabel, ForTreeItemToggle],
  template: `
    <ul
      forTree
      #scroll
      aria-label="Files"
      [(value)]="selected"
      [(expanded)]="expanded"
      [totalCount]="flat().length"
      [visibleRange]="v.range()"
      (scrollToIndex)="v.scrollToIndex($event, { align: 'auto' })"
      style="overflow: auto; max-height: 400px; position: relative;"
    >
      <div [style.height.px]="v.totalSize()" style="position: relative">
        @for (vi of v.virtualItems(); track vi.key) {
          <li
            forTreeItem
            [value]="flat()[vi.index]!.value"
            [level]="flat()[vi.index]!.level"
            [setSize]="flat()[vi.index]!.setSize"
            [posInSet]="flat()[vi.index]!.posInSet"
            [itemIndex]="vi.index"
            [style.transform]="'translateY(' + vi.start + 'px)'"
            style="position: absolute; left: 0; right: 0;"
          >
            @if (flat()[vi.index]!.expandable) {
              <span forTreeItemToggle>▸</span>
            }
            <div forTreeItemLabel>{{ flat()[vi.index]!.label }}</div>
          </li>
        }
      </div>
    </ul>
  `,
})
export class VirtualTree {
  readonly selected = signal<readonly string[]>([]);
  readonly expanded = signal<readonly string[]>([]);

  readonly roots: TreeNode[] = [
    /* large tree data */
  ];

  readonly flat = computed(() => flatten(this.roots, new Set(this.expanded())));

  private readonly scrollRef = viewChild<ElementRef<HTMLElement>>('scroll');
  private readonly scrollElement = computed(() => this.scrollRef()?.nativeElement ?? null);

  readonly v = injectVirtualizer({
    count: computed(() => this.flat().length),
    estimateSize: () => 32,
    scrollElement: this.scrollElement,
  });
}
```

### Intentional limitations

The following behaviors are unavailable in the virtualized path and are documented intentional limitations (same as listbox/select virtualization):

- **Multi-select range modifiers** (Shift+ArrowUp/Down, Shift+Space, Ctrl/Cmd+A) are unsupported: pressing one on a virtualized `[multiple]` tree throws in dev mode (a no-op in production) rather than silently degrading. Range selection requires knowing the full list of enabled nodes in the range, which is not available when the list is partially unmounted. Use `selectionMode="checkbox"` (each node toggles independently, so no range is needed) for multi-select over large trees.
- **Cross-window typeahead** only matches within the currently rendered window. Typeahead over unmounted nodes is not supported.
- **`*` (expand-all-siblings)** is dropped. It requires knowing all siblings at the focused node's level, including those outside the window.

## Drag & drop

Add `[forTreeNodeDrag]` on the same element as `[forTree]` to enable pointer and keyboard drag reordering and re-parenting.

### Pieces

| Class                   | Selector                  | Description                                                                                          |
| ----------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------- |
| `ForTreeNodeDrag`       | `[forTreeNodeDrag]`       | Root coordinator. Apply on the same element as `[forTree]`.                                          |
| `ForTreeNodeDragHandle` | `[forTreeNodeDragHandle]` | Optional grab-area constraint inside an item. When present, pointer drags start only from within it. |

### Inputs / outputs

| API        | Type                                                 | Description                                                                                             |
| ---------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `disabled` | `input<boolean>`                                     | Disables all drag interactions. Default `false`.                                                        |
| `canDrop`  | `input<(event: ForTreeDragDropEvent<T>) => boolean>` | Optional veto callback. Return `false` to reject a specific move. When omitted, all drops are accepted. |
| `nodeDrop` | `output<ForTreeDragDropEvent<T>>`                    | Emitted once per committed move. Apply `moveTreeNode` in the handler to update your data.               |

### A non-string tree must bind `[canDrop]`

`ForTreeNodeDrag<T = string>` is generic over the same node value type as `ForTree`, but — unlike the root, which infers `T` from `[(value)]` / `[(expanded)]` — it has **no input that carries `T` on its own** except `[canDrop]`. So if your node values are not `string`, bind it, typed at the node value:

```ts
readonly canDrop = (event: ForTreeDragDropEvent<FileNode>): boolean => true;
```

```html
<ul forTree forTreeNodeDrag [(value)]="picked" [canDrop]="canDrop" (nodeDrop)="onDrop($event)"></ul>
```

A callback that vetoes nothing is enough — its only job here is to carry the inference.

**Read the diagnostic you get without it carefully, because the obvious fix is the wrong one.** With no `[canDrop]`, `T` stays at its `string` default, so `(nodeDrop)` reports `ForTreeDragDropEvent<string>` while the runtime hands you the node value you actually bound. A handler typed at your real node type fails to compile:

```
TS2345: Argument of type 'ForTreeDragDropEvent<string>' is not assignable to
        parameter of type 'ForTreeDragDropEvent<FileNode>'.
```

The error points at your handler, not at the missing input — and retyping the handler to `string` to satisfy it is what turns a compile error into a silent one: `moveTreeNode` then infers its own `V` as `string`, your `trackBy` returns a `string` id, and the comparison against the object the event really carries never matches, so the helper returns your `roots` unchanged. The drag completes, the announcement fires, and nothing moves.

Annotating a `viewChild` / `@ViewChild` reference (`ForTreeNodeDrag<FileNode>`) recovers `T` for reading `dropIndicator` from TypeScript, but it cannot retype a template binding — `[canDrop]` is the only channel that fixes `(nodeDrop)`.

### Keyboard interaction

| Key               | Behavior while **not** lifted | Behavior while **lifted**                                        |
| ----------------- | ----------------------------- | ---------------------------------------------------------------- |
| `Ctrl/Cmd+Space`  | Lifts the focused node.       | —                                                                |
| `ArrowDown`       | Normal tree navigation.       | Moves the insertion point one row down.                          |
| `ArrowUp`         | Normal tree navigation.       | Moves the insertion point one row up.                            |
| `ArrowRight`      | Normal expand / enter.        | Deepens the target level by 1 (LTR; reversed under RTL).         |
| `ArrowLeft`       | Normal collapse / leave.      | Shallows the target level by 1 (LTR; reversed under RTL).        |
| `Space` / `Enter` | Normal select / activate.     | Drops the node at the current resolved position.                 |
| `Escape`          | —                             | Cancels the drag; the node is returned to its original position. |
| `Tab`             | Normal focus leave.           | Cancels the drag.                                                |

### Minimal example

```ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
  ForTree,
  type ForTreeDragDropEvent,
  ForTreeGroup,
  ForTreeItem,
  ForTreeItemLabel,
  ForTreeItemToggle,
  ForTreeNodeDrag,
  ForTreeNodeDragHandle,
  moveTreeNode,
} from 'forty-cdk/tree';

interface Node {
  id: string;
  name: string;
  children?: Node[];
}

@Component({
  selector: 'app-tree-node',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    ForTreeItem,
    ForTreeItemLabel,
    ForTreeItemToggle,
    ForTreeGroup,
    ForTreeNodeDragHandle,
    TreeNode,
  ],
  host: { style: 'display: contents' },
  template: `
    <li forTreeItem [value]="node().id">
      <div forTreeItemLabel>
        <span forTreeNodeDragHandle aria-hidden="true">⠿</span>
        @if (node().children?.length) {
          <span forTreeItemToggle>▸</span>
        }
        {{ node().name }}
      </div>
      @if (node().children?.length && expanded().includes(node().id)) {
        <ul forTreeGroup>
          @for (child of node().children ?? []; track child.id) {
            <app-tree-node [node]="child" [expanded]="expanded()" />
          }
        </ul>
      }
    </li>
  `,
})
export class TreeNode {
  readonly node = input.required<Node>();
  readonly expanded = input.required<readonly string[]>();
}

@Component({
  selector: 'app-files',
  imports: [ForTree, ForTreeNodeDrag, TreeNode],
  template: `
    <ul
      forTree
      forTreeNodeDrag
      [(value)]="selected"
      [(expanded)]="expanded"
      [canDrop]="canDrop"
      (nodeDrop)="onDrop($event)"
      aria-label="File system"
    >
      @for (n of roots(); track n.id) {
        <app-tree-node [node]="n" [expanded]="expanded()" />
      }
    </ul>
  `,
})
export class Files {
  readonly selected = signal<readonly string[]>([]);
  readonly expanded = signal<readonly string[]>([]);
  readonly roots = signal<Node[]>([
    {
      id: 'documents',
      name: 'Documents',
      children: [
        { id: 'resume', name: 'Resume' },
        { id: 'projects', name: 'Projects', children: [{ id: 'alpha', name: 'Alpha' }] },
      ],
    },
    { id: 'readme', name: 'Readme' },
  ]);

  readonly canDrop = (event: ForTreeDragDropEvent): boolean => {
    return event.newParent !== event.node;
  };

  onDrop(event: ForTreeDragDropEvent): void {
    this.roots.update((r) =>
      moveTreeNode(r, {
        event,
        trackBy: (n) => n.id,
        children: (n) => n.children,
        withChildren: (n, children) => ({ ...n, children: children as Node[] }),
      }),
    );
  }
}
```

### Data attributes on `[forTreeNodeDrag]`

| Attribute               | Values       | When present                                  |
| ----------------------- | ------------ | --------------------------------------------- |
| `data-dragging`         | `""` (empty) | A drag session is live (pointer or keyboard). |
| `data-drop-target`      | `""` (empty) | A valid drop target has been resolved.        |
| `--for-tree-drop-level` | integer 1–N  | The resolved depth of the current target.     |

### Drop indicator

While a drag is live, the single `[forTreeItem]` the lifted node would land beside reflects **`data-drop-position`** so you can draw an indented insertion line. Exactly one visible item carries it at a time; it is absent on every other row and whenever the tree is idle. It tracks every pointer move and every Arrow step (in both LTR and RTL) and clears on drop / cancel / Escape / Tab.

| Piece           | Attribute            | Values                                                        |
| --------------- | -------------------- | ------------------------------------------------------------- |
| `[forTreeItem]` | `data-drop-position` | `"before"` \| `"after"` (the line sits above / below the row) |

Pair it with the root's `--for-tree-drop-level` (the resolved depth) to indent the line to the target level:

```css
[forTreeItem][data-drop-position]::after {
  content: '';
  position: absolute;
  left: calc(var(--for-tree-drop-level, 1) * 1rem);
  right: 0;
  height: 2px;
  background: var(--accent);
}
[forTreeItem][data-drop-position='before']::after {
  top: 0;
}
[forTreeItem][data-drop-position='after']::after {
  bottom: 0;
}
```

Advanced consumers can read the same resolved position programmatically: `[forTreeNodeDrag]` exposes a read-only `dropIndicator: Signal<ForTreeDropIndicator<T> | null>` (`{ anchor, position, level }`, `null` when idle). Injecting it through `ForTreeNodeDragContext` reads the same signal at `ForTreeDropIndicator<unknown>`, since a token cannot carry the node value type.

On lift the dragged node's subtree is collapsed (and restored on drop / cancel). This keeps the drop geometry tractable and structurally prevents dropping a node into its own descendant; `[canDrop]` adds consumer-defined vetoes on top.

### Localizing drag announcements

While a drag is in flight, `[forTreeNodeDrag]` announces lift / move / drop / cancel / invalid-drop through an off-screen live region. The phrasing is English by default; override it per injector scope with `provideForTreeDefaults` so screen readers speak the consumer's language. `position` / `total` are 1-based, and `parentLabel` is `null` when the node lands at the root — phrase the root-vs-parent distinction in your own language.

```ts
import { provideForTreeDefaults } from 'forty-cdk/tree';

provideForTreeDefaults({
  dragAnnounceLift: (label) =>
    `${label} levantado. Usa las flechas para mover, Espacio para soltar, Escape para cancelar.`,
  dragAnnounceMove: (label, parentLabel, position, total) =>
    `${label}: ${parentLabel ? `dentro de ${parentLabel}, ` : 'en la raíz, '}posición ${position} de ${total}.`,
  dragAnnounceDrop: (label, parentLabel, position, total) =>
    `${label} soltado ${parentLabel ? `dentro de ${parentLabel}, ` : 'en la raíz, '}posición ${position} de ${total}.`,
  dragAnnounceCancel: (label) => `Cancelado. ${label} vuelve a su posición original.`,
  dragAnnounceInvalid: (label) => `No se puede soltar ${label} aquí.`,
});
```

| Default               | Type                                                                                      | Description                                                        |
| --------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `dragAnnounceLift`    | `(label: string) => string`                                                               | Announced when a node is picked up for drag.                       |
| `dragAnnounceMove`    | `(label: string, parentLabel: string \| null, position: number, total: number) => string` | Announced on each intermediate move while a node is lifted.        |
| `dragAnnounceDrop`    | `(label: string, parentLabel: string \| null, position: number, total: number) => string` | Announced when a node is committed to its new position.            |
| `dragAnnounceCancel`  | `(label: string) => string`                                                               | Announced when a lift is cancelled and the node returns to origin. |
| `dragAnnounceInvalid` | `(label: string) => string`                                                               | Announced when a `canDrop` veto rejects the attempted drop.        |

## Accessibility

Implements the [WAI-ARIA Tree View pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) (APG Approach A — DOM focus rides the `treeitem`).

- **Label the tree** via the reactive `[ariaLabel]` input or a native `aria-labelledby` pointing at a visible heading.
- **`data-state="open" | "closed"`** is reflected on parent nodes only (and on the toggle); leaves carry neither, matching `aria-expanded`.
- **`data-selected`** (present / absent) reflects selection on every node — a node is simultaneously expandable and selectable, so expansion (`data-state`) and selection (`data-selected`) get separate hooks.
- **`data-highlighted=""`** marks the current roving-tabindex node, the same hook used across the listbox / menu / select primitives.
- **Exactly one node is tabbable** at a time (the selected node, or the first enabled node). `Tab` enters and leaves the whole tree in one stop.
- **In `selectionMode="checkbox"`** each `treeitem` emits `aria-checked` (`"true"` / `"false"`) and no `aria-selected`; the `[forTreeItemCheckbox]` and `[forTreeItemCheckboxIndicator]` are `aria-hidden` / decorative — the `treeitem` itself is the accessible checkbox. With `cascade`, a parent reports `aria-checked="mixed"` (and `data-checked="mixed"`) when only some of its descendants are checked; the cascade reaches collapsed / unmounted descendants through the `descendantsOf` descriptor, so the tri-state is always correct even when children are not yet mounted.

## Styling

forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed per piece in the [API](#api) section.

```css
.tree-toggle {
  display: inline-block;
  transition: transform 150ms;
}
.tree-toggle[data-state='open'] {
  transform: rotate(90deg);
}
.tree-item[data-highlighted] {
  outline: 2px solid Highlight;
}
```

## Wrapping in a design system

Subclassing the root is the supported pattern; the subclass must re-provide `FOR_TREE_CONTEXT` because Angular does not inherit a directive's `providers`, and every projected piece resolves its context through it. See [Wrapping non-form roots](../../../docs/wrapping-non-form-roots.md).
