{
+ internalRef.current = element;
+ if (typeof ref === "function") {
+ ref(element);
+ } else if (ref) {
+ ref.current = element;
+ }
+ }}
+ role="treeitem"
+ aria-label={ariaLabelledBy ? undefined : (ariaLabel ?? rowTextValue)}
+ aria-labelledby={ariaLabelledBy}
+ aria-level={hierarchy.level}
+ aria-selected={isSelected}
+ aria-disabled={disabled || undefined}
+ aria-expanded={expanded}
+ tabIndex={tabIndex}
+ className={cx(
+ "ui-tree-item",
+ isSticky && "ui-tree-item--sticky",
+ isFocusedRow && "ui-tree-item--focused",
+ className,
+ )}
+ style={{ "--ui-tree-level": hierarchy.level, ...style } as CSSProperties}
+ onFocus={(event) => {
+ if (!eventBelongsToRow(event, rowRef.current)) {
+ return;
+ }
+ onFocus?.(event);
+ if (!event.defaultPrevented && event.target === event.currentTarget) {
+ store.onItemFocus(itemId);
+ }
+ }}
+ onClick={(event) => {
+ if (!eventBelongsToRow(event, rowRef.current)) {
+ return;
+ }
+ onClick?.(event);
+ if (
+ event.defaultPrevented ||
+ disabled ||
+ isNestedInteractiveTarget(event)
+ ) {
+ return;
+ }
+ // Twistie clicks toggle without moving selection, like the native tree.
+ const onTwistie =
+ isBranch &&
+ event.target instanceof Node &&
+ chevronRef.current?.contains(event.target) === true;
+ if (!onTwistie) {
+ store.requestSelection(itemId, {
+ toggle: event.ctrlKey || event.metaKey,
+ range: event.shiftKey,
+ });
+ }
+ setExpanded?.(!expanded);
+ }}
+ >
+
+
+ {hierarchy.pathItemIds.map((pathItemId) => (
+
+ ))}
+
+
+ {isBranch ? (
+
+ ) : null}
+
+
+ {icon ? : null}
+ {typeof label === "string" ? {label} : label}
+
+ {action ? {action} : null}
+
+ {expanded && children ? (
+
+
+ {children}
+
+
+ ) : null}
+
+ );
+}
diff --git a/packages/ui/src/components/Tree/TreeStore.ts b/packages/ui/src/components/Tree/TreeStore.ts
new file mode 100644
index 000000000..1c631029e
--- /dev/null
+++ b/packages/ui/src/components/Tree/TreeStore.ts
@@ -0,0 +1,589 @@
+import type { KeyboardEvent } from "react";
+
+export const INTERACTIVE_SELECTOR = [
+ "a[href]",
+ "button",
+ "input",
+ "select",
+ "textarea",
+ "[contenteditable]:not([contenteditable='false'])",
+ "[role='button']",
+ "[role='checkbox']",
+ "[role='combobox']",
+ "[role='link']",
+ "[role='menuitem']",
+ "[role='option']",
+ "[role='radio']",
+ "[role='slider']",
+ "[role='spinbutton']",
+ "[role='switch']",
+ "[role='textbox']",
+ "[tabindex]:not([tabindex='-1'])",
+].join(",");
+
+/** Matching this gives visibility and tree order without mirroring the DOM. */
+const VISIBLE_ROW_SELECTOR =
+ '[role="treeitem"]:not([hidden]):not([hidden] *):not([aria-disabled="true"])';
+
+const TYPE_AHEAD_TIMEOUT_MS = 500;
+
+/** Repeating one character cycles matches instead of growing the query. */
+function nextTypeAheadQuery(buffer: string, character: string): string {
+ const isRepeat =
+ buffer.length > 0 && [...buffer].every((value) => value === character);
+ return isRepeat ? character : `${buffer}${character}`;
+}
+
+function findTypeAheadMatch(
+ items: readonly TreeStoreItem[],
+ fromIndex: number,
+ query: string,
+): TreeStoreItem | undefined {
+ for (let offset = 0; offset < items.length; offset++) {
+ const item = items[(fromIndex + offset) % items.length];
+ if (item?.textValue.toLocaleLowerCase().startsWith(query)) {
+ return item;
+ }
+ }
+ return undefined;
+}
+
+function isDisabled(element: HTMLElement): boolean {
+ return element.getAttribute("aria-disabled") === "true";
+}
+
+/** null on leaves. */
+function readExpanded(element: HTMLElement): boolean | null {
+ const expanded = element.getAttribute("aria-expanded");
+ return expanded === null ? null : expanded === "true";
+}
+
+/** Only what the DOM cannot answer; parentId outlives the row's removal. */
+export interface TreeStoreItem {
+ readonly id: string;
+ readonly element: HTMLElement;
+ textValue: string;
+ parentId?: string;
+ setExpanded?: (expanded: boolean) => void;
+}
+
+export type TreeStoreItemUpdate = Omit