# Tree Select

> Hierarchical multi-select with checkbox cascading, mixed (partial) parent state, recursive search that auto-expands matched ancestors, and tag overflow.

- Available in: `@sisyphos-ui/react`, `@sisyphos-ui/vue`, `@sisyphos-ui/angular`
- Docs: https://sisyphosui.com/docs/components/tree-select

## Installation

Pick the framework binding that matches your stack:

```bash
pnpm add @sisyphos-ui/react   # React 18+
pnpm add @sisyphos-ui/vue     # Vue 3+
pnpm add @sisyphos-ui/angular # Angular 17+
```

## Import

```tsx
import "@sisyphos-ui/react/styles.css";
import { TreeSelect } from "@sisyphos-ui/react";
```

## Framework usage

### React 18+

```tsx
import { useState } from "react";
import { TreeSelect } from "@sisyphos-ui/react";

const tree = [
  { id: "1", label: "Engineering", children: [
    { id: "1-1", label: "Web" },
    { id: "1-2", label: "Mobile" },
  ]},
  { id: "2", label: "Design" },
];

export const Departments = () => {
  const [ids, setIds] = useState<string[]>([]);
  return <TreeSelect label="Departments" data={tree} value={ids} onChange={setIds} />;
};
```

### Vue 3+

```vue
<script setup lang="ts">
import { ref } from "vue";
import { TreeSelect } from "@sisyphos-ui/vue";
const tree = [
  { id: "1", label: "Engineering", children: [
    { id: "1-1", label: "Web" }, { id: "1-2", label: "Mobile" },
  ]},
  { id: "2", label: "Design" },
];
const ids = ref<string[]>([]);
</script>

<template>
  <TreeSelect label="Departments" :data="tree" v-model="ids" />
</template>
```

### Angular 17+

```ts
import { Component, signal } from "@angular/core";
import { TreeSelect } from "@sisyphos-ui/angular";

@Component({
  selector: "app-departments",
  standalone: true,
  imports: [TreeSelect],
  template: `<sui-tree-select label="Departments" [data]="tree" [(value)]="ids" />`,
})
export class DepartmentsComponent {
  tree = [
    { id: "1", label: "Engineering", children: [
      { id: "1-1", label: "Web" }, { id: "1-2", label: "Mobile" },
    ]},
    { id: "2", label: "Design" },
  ];
  ids = signal<string[]>([]);
}
```

## Examples

### Default

Hierarchical picker with cascading selection.

```tsx
import { useState } from "react";
import { TreeSelect } from "@sisyphos-ui/react";

const TREE_NODES = [
  {
    id: "engineering",
    label: "Engineering",
    children: [
      { id: "frontend", label: "Frontend" },
      { id: "backend",  label: "Backend" },
      { id: "platform", label: "Platform" },
    ],
  },
  {
    id: "design",
    label: "Design",
    children: [
      { id: "product", label: "Product" },
      { id: "brand",   label: "Brand" },
    ],
  },
];

export function Example() {
  const [selected, setSelected] = useState<(string | number)[]>([]);

  return (
    <div className="w-full max-w-sm">
      <TreeSelect
        label="Team"
        placeholder="Pick a team"
        nodes={TREE_NODES}
        value={selected}
        onChange={setSelected}
      />
    </div>
  );
}
```

### Searchable

Add `searchable` and `clearable` for dense trees.

```tsx
import { useState } from "react";
import { TreeSelect } from "@sisyphos-ui/react";

const TREE_NODES = [
  {
    id: "engineering",
    label: "Engineering",
    children: [
      { id: "frontend", label: "Frontend" },
      { id: "backend",  label: "Backend" },
    ],
  },
  {
    id: "design",
    label: "Design",
    children: [
      { id: "product", label: "Product" },
      { id: "brand",   label: "Brand" },
    ],
  },
];

export function Example() {
  const [selected, setSelected] = useState<(string | number)[]>([]);

  return (
    <TreeSelect
      label="Team"
      placeholder="Search people and teams…"
      searchable
      clearable
      nodes={TREE_NODES}
      value={selected}
      onChange={setSelected}
    />
  );
}
```

### Multi select

Pre-fill a selection via `value` — renders compact chip summary with `+N` overflow.

```tsx
import { useState } from "react";
import { TreeSelect } from "@sisyphos-ui/react";

const TREE_NODES = [
  {
    id: "engineering",
    label: "Engineering",
    children: [
      { id: "frontend", label: "Frontend" },
      { id: "backend",  label: "Backend" },
    ],
  },
  {
    id: "design",
    label: "Design",
    children: [
      { id: "product", label: "Product" },
      { id: "brand",   label: "Brand" },
    ],
  },
];

export function Example() {
  const [selected, setSelected] = useState<(string | number)[]>(["engineering"]);

  return (
    <TreeSelect
      label="Invitees"
      placeholder="Pick one or more groups"
      nodes={TREE_NODES}
      value={selected}
      onChange={setSelected}
      clearable
      searchable
    />
  );
}
```

### Error state

Inline validation mirrors the other form controls.

```tsx
import { TreeSelect } from "@sisyphos-ui/react";

const TREE_NODES = [
  {
    id: "engineering",
    label: "Engineering",
    children: [
      { id: "frontend", label: "Frontend" },
      { id: "backend",  label: "Backend" },
    ],
  },
  {
    id: "design",
    label: "Design",
    children: [
      { id: "product", label: "Product" },
      { id: "brand",   label: "Brand" },
    ],
  },
];

export function Example() {
  return (
    <TreeSelect
      label="Team"
      placeholder="Required"
      nodes={TREE_NODES}
      value={[]}
      error
      errorMessage="Pick at least one team to route this request."
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| **nodes** (required) | `TreeNode[]` | — | Hierarchical data with `id`, `label`, optional `children` and `disabled`. |
| value | `TreeNodeId[]` | — | Controlled selected ids. |
| defaultValue | `TreeNodeId[]` | — | Initial uncontrolled selection. |
| onChange | `(ids: TreeNodeId[]) => void` | — | Called with the next selection. |
| label | `string` | — | Field label rendered above the trigger. |
| placeholder | `string` | — | Trigger placeholder when nothing is selected. |
| triggerLabel | `string` | — | Override for the trigger's primary text. |
| searchable | `boolean` | `true` | Show a search input above the tree. Auto-expands matched ancestors while a term is active. |
| searchPlaceholder | `string` | `"Search…"` | Placeholder for the search input. |
| cascade | `boolean` | `true` | Toggle a parent node to also toggle its descendants. With `false`, only the node itself is selected. |
| maxTagCount | `number` | `3` | Show up to this many tags in the trigger; remaining count collapses into `+N`. |
| clearable | `boolean` | `false` | Render a `Clear all` button when there is a selection. |
| fullWidth | `boolean` | `false` | Stretch the trigger to its container. |
| size | `"sm" \| "md" \| "lg"` | `"md"` | Field height + typography scale. |
| placement | `Placement` | `"bottom-start"` | Initial popover placement (auto-flips when needed). |
| error | `boolean` | `false` | Marks the field as invalid for styling and ARIA. |
| errorMessage | `string` | — | Message shown below the field when `error` is true. |
| disabled | `boolean` | `false` | Disables interaction. |
| required | `boolean` | `false` | Adds the required indicator and ARIA flag. |

## Keyboard interactions

- **Tab** — Moves focus to and inside the picker.
- **Enter + Space** — Toggles the focused node.
- **Esc** — Closes the popover.

## Accessibility notes

- Each row is a `role="checkbox"` with `aria-checked` set to `"true"`, `"false"`, or `"mixed"` for partial parents.
- The dropdown uses `role="tree"`; expand chevrons expose `aria-expanded`.
- Trigger is wired with `role="combobox"` + `aria-expanded` and `aria-haspopup="tree"`.
- Search input is portal-mounted alongside the tree and receives focus on open.

<!-- exports: { "TreeSelect": "@sisyphos-ui/react" } -->