{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-virtual-tree",
  "type": "registry:component",
  "title": "Virtual trees",
  "description": "A hierarchy painted one window at a time",
  "author": "sudo-kw <https://github.com/sudo-kw>",
  "dependencies": [
    "@gpuix/native@0.11.0",
    "@gpuix/react@0.11.0",
    "@tanstack/virtual-core@3.19.6",
    "effect@4.1.2",
    "react@^19.2.4"
  ],
  "registryDependencies": [
    "https://gpuixui.sudokw.com/gpuix-components.json",
    "https://gpuixui.sudokw.com/gpuix-icons.json",
    "https://gpuixui.sudokw.com/gpuix-primitives.json",
    "https://gpuixui.sudokw.com/gpuix-tokens.json",
    "https://gpuixui.sudokw.com/gpuix-tw.json"
  ],
  "files": [
    {
      "path": "packages/data/src/internal/list.ts",
      "target": "@ui/gpuixui/data/internal/list.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as P from \"effect/Predicate\";\nimport * as Str from \"effect/String\";\n\nexport const listOf = <A>(\n  value: ReadonlyArray<A> | null | undefined,\n): ReadonlyArray<A> => (Arr.isArray(value) ? value : []);\n\nexport const recordsOf = <A extends { readonly key: string }>(\n  value: ReadonlyArray<A> | undefined,\n): ReadonlyArray<A> =>\n  Arr.filter(\n    listOf(value),\n    (entry) => P.isObject(entry) && P.isString(entry.key),\n  );\n\nexport const textsOf = (\n  value: ReadonlyArray<string> | undefined,\n): ReadonlyArray<string> =>\n  Arr.map(listOf(value), (entry) => (P.isString(entry) ? entry : \"\"));\n\nconst namesRow = (value: unknown): boolean =>\n  P.isString(value) && Str.isNonEmpty(Str.trim(value));\n\nexport const rowsOf = <A>(\n  value: ReadonlyArray<A> | null | undefined,\n  keyOf: (row: A) => string,\n): ReadonlyArray<A> =>\n  Arr.filter(listOf(value), (row) => P.isObject(row) && namesRow(keyOf(row)));\n"
    },
    {
      "path": "packages/data/src/internal/painted.ts",
      "target": "@ui/gpuixui/data/internal/painted.ts",
      "type": "registry:lib",
      "content": "import type { EventPayload, StyleDesc } from \"@gpuix/react\";\nimport * as Option from \"effect/Option\";\nimport { useCallback, useState } from \"react\";\nimport { paintableSize } from \"./size.js\";\n\nexport interface PaintedHeight {\n  readonly measured: Option.Option<number>;\n\n  readonly onLayout: (event: EventPayload) => void;\n}\n\nexport const usePaintedHeight = (\n  onLayout: ((event: EventPayload) => void) | undefined,\n): PaintedHeight => {\n  const [measured, setMeasured] = useState<Option.Option<number>>(\n    Option.none(),\n  );\n  const report = useCallback(\n    (event: EventPayload) => {\n      onLayout?.(event);\n      Option.match(paintableSize(event.height), {\n        onNone: () => {},\n        onSome: (height) =>\n          setMeasured((current) =>\n            Option.contains(current, height) ? current : Option.some(height),\n          ),\n      });\n    },\n    [onLayout],\n  );\n  return { measured, onLayout: report };\n};\n\nexport const paneFill: (height: Option.Option<number>) => StyleDesc =\n  Option.match({\n    onNone: () => ({\n      display: \"flex\",\n      flexDirection: \"column\",\n      flexGrow: 1,\n      flexShrink: 1,\n      flexBasis: 0,\n      minHeight: 0,\n    }),\n    onSome: () => ({}),\n  });\n"
    },
    {
      "path": "packages/data/src/internal/size.ts",
      "target": "@ui/gpuixui/data/internal/size.ts",
      "type": "registry:lib",
      "content": "import * as Option from \"effect/Option\";\n\nexport const paintableSize = (\n  size: number | undefined,\n): Option.Option<number> =>\n  Option.filter(\n    Option.fromNullishOr(size),\n    (value) => Number.isFinite(value) && value >= 0,\n  );\n"
    },
    {
      "path": "packages/data/src/recipe.ts",
      "target": "@ui/gpuixui/data/recipe.ts",
      "type": "registry:lib",
      "content": "import type { StyleDesc } from \"@gpuix/react\";\nimport { createRecipeCache } from \"../tw/index.js\";\nimport type { Theme } from \"./theme.js\";\n\nexport const recipeStyle: (recipe: string, theme: Theme) => StyleDesc =\n  createRecipeCache(256);\n"
    },
    {
      "path": "packages/data/src/selection/logic.ts",
      "target": "@ui/gpuixui/data/selection/logic.ts",
      "type": "registry:lib",
      "content": "import * as Arr from \"effect/Array\";\nimport * as HashSet from \"effect/HashSet\";\nimport * as Option from \"effect/Option\";\n\nexport type PressMode = \"replace\" | \"toggle\" | \"range\";\n\nexport interface PressModifiers {\n  readonly shift?: boolean;\n  readonly ctrl?: boolean;\n  readonly cmd?: boolean;\n}\n\nexport interface SelectionState {\n  readonly selected: HashSet.HashSet<string>;\n\n  readonly anchor: Option.Option<string>;\n}\n\nexport const emptySelection: SelectionState = {\n  selected: HashSet.empty(),\n  anchor: Option.none(),\n};\n\nexport const pressMode = (modifiers: PressModifiers = {}): PressMode =>\n  modifiers.shift === true\n    ? \"range\"\n    : modifiers.cmd === true || modifiers.ctrl === true\n      ? \"toggle\"\n      : \"replace\";\n\nexport const isSelected = (state: SelectionState, key: string): boolean =>\n  HashSet.has(state.selected, key);\n\nexport const selectionSize = (state: SelectionState): number =>\n  HashSet.size(state.selected);\n\nexport const allSelected = (\n  state: SelectionState,\n  visible: ReadonlyArray<string>,\n): boolean =>\n  visible.length > 0 &&\n  Arr.every(visible, (key) => HashSet.has(state.selected, key));\n\nexport const someSelected = (\n  state: SelectionState,\n  visible: ReadonlyArray<string>,\n): boolean =>\n  !allSelected(state, visible) &&\n  Arr.some(visible, (key) => HashSet.has(state.selected, key));\n\nconst indexIn = (\n  order: ReadonlyArray<string>,\n  key: string,\n): Option.Option<number> =>\n  Arr.findFirstIndex(order, (candidate) => candidate === key);\n\nexport const pressSelection = (\n  state: SelectionState,\n  key: string,\n  mode: PressMode,\n  order: ReadonlyArray<string>,\n): SelectionState => {\n  if (mode === \"toggle\") {\n    const selected = HashSet.has(state.selected, key)\n      ? HashSet.remove(state.selected, key)\n      : HashSet.add(state.selected, key);\n    return { selected, anchor: Option.some(key) };\n  }\n  if (mode === \"range\") {\n    const from = Option.flatMap(state.anchor, (anchor) =>\n      indexIn(order, anchor),\n    );\n    const to = indexIn(order, key);\n    if (Option.isNone(from) || Option.isNone(to)) {\n      return { selected: HashSet.make(key), anchor: Option.some(key) };\n    }\n    const start = Math.min(from.value, to.value);\n    const count = Math.abs(from.value - to.value) + 1;\n    const spanned = Arr.take(Arr.drop(order, start), count);\n    return {\n      selected: Arr.reduce(spanned, state.selected, (selected, key) =>\n        HashSet.add(selected, key),\n      ),\n\n      anchor: state.anchor,\n    };\n  }\n  return { selected: HashSet.make(key), anchor: Option.some(key) };\n};\n\nexport const selectAll = (\n  _state: SelectionState,\n  order: ReadonlyArray<string>,\n): SelectionState => ({\n  selected: HashSet.fromIterable(order),\n  anchor: Option.none(),\n});\n\nexport const clearSelection = (_state: SelectionState): SelectionState =>\n  emptySelection;\n\nexport const retainSelection = (\n  state: SelectionState,\n  visible: ReadonlyArray<string>,\n): SelectionState => {\n  const shown = HashSet.fromIterable(visible);\n  return {\n    selected: HashSet.filter(state.selected, (key) => HashSet.has(shown, key)),\n    anchor: Option.filter(state.anchor, (key) => HashSet.has(shown, key)),\n  };\n};\n\nexport const currentKey = (state: SelectionState): Option.Option<string> =>\n  Option.filter(state.anchor, (key) => HashSet.has(state.selected, key));\n\nexport const neighbourKey = (\n  order: ReadonlyArray<string>,\n  current: Option.Option<string>,\n  delta: number,\n): Option.Option<string> =>\n  Option.match(\n    Option.flatMap(current, (key) => indexIn(order, key)),\n    {\n      onNone: () => Option.fromNullishOr(order[0]),\n      onSome: (index) =>\n        Option.fromNullishOr(\n          order[Math.max(0, Math.min(order.length - 1, index + delta))],\n        ),\n    },\n  );\n\nexport const moveSelection = (\n  state: SelectionState,\n  order: ReadonlyArray<string>,\n  delta: number,\n): SelectionState =>\n  Option.match(neighbourKey(order, currentKey(state), delta), {\n    onNone: () => state,\n    onSome: (key) => ({\n      selected: HashSet.make(key),\n      anchor: Option.some(key),\n    }),\n  });\n"
    },
    {
      "path": "packages/data/src/theme.ts",
      "target": "@ui/gpuixui/data/theme.ts",
      "type": "registry:lib",
      "content": "import type { Theme } from \"../tokens/index.js\";\nimport { useTheme as useTokenTheme } from \"../tokens/react.js\";\n\nexport {\n  controlFocusRing,\n  dividerOn,\n  pointerWash,\n  textStyle,\n  themeColors,\n} from \"../tokens/index.js\";\nexport type { Theme };\n\nexport const useTheme = (): Theme => useTokenTheme();\n"
    },
    {
      "path": "packages/data/src/tree/logic.ts",
      "target": "@ui/gpuixui/data/tree/logic.ts",
      "type": "registry:lib",
      "content": "import { typeaheadMatchIndex } from \"../../components/typeahead.js\";\nimport { spokenText } from \"../../primitives/core/index.js\";\nimport { Match, pipe } from \"effect\";\nimport * as Arr from \"effect/Array\";\nimport * as HashSet from \"effect/HashSet\";\nimport * as MutableHashSet from \"effect/MutableHashSet\";\nimport * as Option from \"effect/Option\";\nimport * as P from \"effect/Predicate\";\nimport * as Schema from \"effect/Schema\";\nimport { listOf } from \"../internal/list.js\";\nimport { neighbourKey } from \"../selection/logic.js\";\n\nexport const TREE_INDENT = 16;\n\nexport interface TreeSpec<T> {\n  readonly keyOf: (node: T) => string;\n\n  readonly childrenOf: (node: T) => ReadonlyArray<T> | null | undefined;\n}\n\nexport interface TreeRow<T> {\n  readonly node: T;\n  readonly key: string;\n\n  readonly depth: number;\n\n  readonly parent: Option.Option<string>;\n\n  readonly hasChildren: boolean;\n\n  readonly expanded: boolean;\n}\n\nexport const flattenTree = <T>(\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n  expanded: HashSet.HashSet<string>,\n): ReadonlyArray<TreeRow<T>> => {\n  const rows: Array<TreeRow<T>> = [];\n\n  const painted = MutableHashSet.empty<string>();\n\n  const visit = (\n    node: T,\n    depth: number,\n    parent: Option.Option<string>,\n    ancestors: HashSet.HashSet<string>,\n  ): void => {\n    const key = spec.keyOf(node);\n    if (HashSet.has(ancestors, key) || MutableHashSet.has(painted, key)) return;\n    MutableHashSet.add(painted, key);\n    const children = listOf(spec.childrenOf(node));\n    const hasChildren = Arr.isReadonlyArrayNonEmpty(children);\n    const open = hasChildren && HashSet.has(expanded, key);\n    rows.push({ node, key, depth, parent, hasChildren, expanded: open });\n    if (!open) return;\n    const path = HashSet.add(ancestors, key);\n    Arr.forEach(children, (child) =>\n      visit(child, depth + 1, Option.some(key), path),\n    );\n  };\n\n  Arr.forEach(listOf(roots), (root) =>\n    visit(root, 0, Option.none(), HashSet.empty()),\n  );\n  return rows;\n};\n\nexport interface PaintedRow<T> {\n  readonly row: TreeRow<T>;\n\n  readonly leaving: boolean;\n}\n\nexport const paintedRows = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  leaving: ReadonlyArray<TreeRow<T>>,\n  before: ReadonlyArray<TreeRow<T>>,\n): ReadonlyArray<PaintedRow<T>> => {\n  const visible = HashSet.fromIterable(Arr.map(rows, (row) => row.key));\n  const anchorOf = (row: TreeRow<T>): Option.Option<string> =>\n    pipe(\n      Arr.findFirstIndex(before, (candidate) => candidate.key === row.key),\n      Option.flatMap((index) =>\n        Arr.findFirst(Arr.drop(before, index + 1), (candidate) =>\n          HashSet.has(visible, candidate.key),\n        ),\n      ),\n      Option.map((candidate) => candidate.key),\n    );\n  const anchoredBefore = (key: string): ReadonlyArray<TreeRow<T>> =>\n    Arr.filter(leaving, (row) => Option.contains(anchorOf(row), key));\n\n  return [\n    ...Arr.flatMap(rows, (row) => [\n      ...Arr.map(anchoredBefore(row.key), (gone) => ({\n        row: gone,\n        leaving: true,\n      })),\n      { row, leaving: false },\n    ]),\n    ...Arr.map(\n      Arr.filter(leaving, (row) => Option.isNone(anchorOf(row))),\n      (row) => ({ row, leaving: true }),\n    ),\n  ];\n};\n\nexport const branchKeys = <T>(\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n): ReadonlyArray<string> => {\n  const keys: Array<string> = [];\n  const seen = MutableHashSet.empty<string>();\n\n  const visit = (node: T, ancestors: HashSet.HashSet<string>): void => {\n    const key = spec.keyOf(node);\n    if (HashSet.has(ancestors, key) || MutableHashSet.has(seen, key)) return;\n    MutableHashSet.add(seen, key);\n    const children = listOf(spec.childrenOf(node));\n    if (Arr.isReadonlyArrayNonEmpty(children)) keys.push(key);\n    const path = HashSet.add(ancestors, key);\n    Arr.forEach(children, (child) => visit(child, path));\n  };\n\n  Arr.forEach(listOf(roots), (root) => visit(root, HashSet.empty()));\n  return keys;\n};\n\nexport const isExpanded = (\n  expanded: HashSet.HashSet<string>,\n  key: string,\n): boolean => HashSet.has(expanded, key);\n\nexport const setExpanded = (\n  expanded: HashSet.HashSet<string>,\n  key: string,\n  open: boolean,\n): HashSet.HashSet<string> =>\n  open ? HashSet.add(expanded, key) : HashSet.remove(expanded, key);\n\nexport const toggleExpanded = (\n  expanded: HashSet.HashSet<string>,\n  key: string,\n): HashSet.HashSet<string> =>\n  setExpanded(expanded, key, !isExpanded(expanded, key));\n\nexport const collapseAll = (): HashSet.HashSet<string> => HashSet.empty();\n\nexport const expandedSet = (value: unknown): HashSet.HashSet<string> =>\n  P.isIterable(value)\n    ? HashSet.fromIterable(Arr.filter(Arr.fromIterable(value), P.isString))\n    : HashSet.empty();\n\nexport const expandAll = <T>(\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n): HashSet.HashSet<string> => HashSet.fromIterable(branchKeys(roots, spec));\n\nexport const expandedPath = <T>(\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n  key: string,\n): Option.Option<ReadonlyArray<string>> => {\n  const walk = (\n    nodes: ReadonlyArray<T>,\n    path: ReadonlyArray<string>,\n  ): Option.Option<ReadonlyArray<string>> =>\n    Option.match(Arr.head(nodes), {\n      onNone: () => Option.none(),\n      onSome: (node) => {\n        const nodeKey = spec.keyOf(node);\n        if (Arr.contains(path, nodeKey)) {\n          return walk(Arr.drop(nodes, 1), path);\n        }\n        const next = [...path, nodeKey];\n        if (nodeKey === key) return Option.some(next);\n        const inside = walk(listOf(spec.childrenOf(node)), next);\n        return Option.isSome(inside) ? inside : walk(Arr.drop(nodes, 1), path);\n      },\n    });\n\n  return walk(listOf(roots), []);\n};\n\nexport const expandTo = <T>(\n  expanded: HashSet.HashSet<string>,\n  roots: ReadonlyArray<T> | null | undefined,\n  spec: TreeSpec<T>,\n  key: string,\n): HashSet.HashSet<string> =>\n  Option.match(expandedPath(roots, spec, key), {\n    onNone: () => expanded,\n    onSome: (path) =>\n      Arr.reduce(path, expanded, (set, ancestor) => HashSet.add(set, ancestor)),\n  });\n\nexport const treeKeys = [\n  \"up\",\n  \"down\",\n  \"home\",\n  \"end\",\n  \"right\",\n  \"left\",\n  \"enter\",\n  \"space\",\n] as const;\n\nexport const TreeKey = Schema.Literals(treeKeys);\n\nexport type TreeKey = typeof TreeKey.Type;\n\nexport const isTreeKey = Schema.is(TreeKey);\n\nexport interface TreeCursor {\n  readonly expanded: HashSet.HashSet<string>;\n  readonly active: Option.Option<string>;\n}\n\nexport const emptyCursor: TreeCursor = {\n  expanded: HashSet.empty(),\n  active: Option.none(),\n};\n\nexport const findRow = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  key: Option.Option<string>,\n): Option.Option<TreeRow<T>> =>\n  Option.flatMap(key, (wanted) =>\n    Arr.findFirst(rows, (row) => row.key === wanted),\n  );\n\nconst indexOf = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  key: Option.Option<string>,\n): Option.Option<number> =>\n  Option.flatMap(key, (wanted) =>\n    Arr.findFirstIndex(rows, (row) => row.key === wanted),\n  );\n\nconst at = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  index: number,\n): Option.Option<TreeRow<T>> => Option.fromNullishOr(rows[index]);\n\nexport const cursorRow = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  cursor: TreeCursor,\n): Option.Option<TreeRow<T>> =>\n  Option.orElse(findRow(rows, cursor.active), () => Arr.head(rows));\n\nexport const treeCursorMove = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  cursor: TreeCursor,\n  key: TreeKey,\n): TreeCursor => {\n  const order = Arr.map(rows, (row) => row.key);\n  const active = findRow(rows, cursor.active);\n\n  const focus = (next: Option.Option<string>): TreeCursor => ({\n    ...cursor,\n    active: next,\n  });\n\n  const opened = (row: TreeRow<T>, next: boolean): TreeCursor => ({\n    expanded: setExpanded(cursor.expanded, row.key, next),\n    active: Option.some(row.key),\n  });\n  return Match.value(key).pipe(\n    Match.when(\"up\", () => focus(neighbourKey(order, cursor.active, -1))),\n    Match.when(\"down\", () => focus(neighbourKey(order, cursor.active, 1))),\n    Match.when(\"home\", () => focus(Arr.head(order))),\n    Match.when(\"end\", () =>\n      focus(Option.fromNullishOr(order[order.length - 1])),\n    ),\n    Match.when(\"right\", () =>\n      Option.match(active, {\n        onNone: () => focus(Arr.head(order)),\n        onSome: (row) => {\n          const open = isExpanded(cursor.expanded, row.key);\n          if (!row.hasChildren) return focus(Option.some(row.key));\n          if (!open) return opened(row, true);\n          return Option.match(indexOf(rows, Option.some(row.key)), {\n            onNone: () => cursor,\n            onSome: (index) =>\n              Option.match(at(rows, index + 1), {\n                onNone: () => cursor,\n                onSome: (next) =>\n                  next.depth === row.depth + 1\n                    ? focus(Option.some(next.key))\n                    : cursor,\n              }),\n          });\n        },\n      }),\n    ),\n    Match.when(\"left\", () =>\n      Option.match(active, {\n        onNone: () => cursor,\n        onSome: (row) =>\n          row.hasChildren && isExpanded(cursor.expanded, row.key)\n            ? opened(row, false)\n            : Option.match(row.parent, {\n                onNone: () => cursor,\n                onSome: (parent) => focus(Option.some(parent)),\n              }),\n      }),\n    ),\n    Match.when(\"enter\", () =>\n      Option.match(active, {\n        onNone: () => cursor,\n        onSome: (row) =>\n          row.hasChildren\n            ? opened(row, !isExpanded(cursor.expanded, row.key))\n            : focus(Option.some(row.key)),\n      }),\n    ),\n    Match.when(\"space\", () =>\n      Option.match(active, {\n        onNone: () => cursor,\n        onSome: (row) =>\n          row.hasChildren\n            ? opened(row, !isExpanded(cursor.expanded, row.key))\n            : focus(Option.some(row.key)),\n      }),\n    ),\n    Match.exhaustive,\n  );\n};\n\nexport const treeName = (label: string, key: string): string =>\n  spokenText(Option.fromNullishOr(label), key);\n\nexport const typeAheadRow = <T>(\n  rows: ReadonlyArray<TreeRow<T>>,\n  active: Option.Option<string>,\n  prefix: string,\n  labelOf: (row: TreeRow<T>) => string,\n): Option.Option<string> =>\n  Option.flatMap(\n    typeaheadMatchIndex(\n      Arr.map(rows, (row) => ({\n        label: treeName(labelOf(row), row.key),\n        disabled: false,\n      })),\n      indexOf(rows, active),\n      prefix,\n    ),\n    (index) => Option.map(at(rows, index), (row) => row.key),\n  );\n"
    },
    {
      "path": "packages/data/src/tree/recipe.ts",
      "target": "@ui/gpuixui/data/tree/recipe.ts",
      "type": "registry:lib",
      "content": "export const TREE_ROOT = \"flex flex-col overflow-hidden rounded-md\";\n\nexport const TREE_ROW =\n  \"flex flex-row items-center shrink-0 pr-2 text-sm text-foreground hover:bg-hover cursor-pointer\";\n\nexport const TREE_ROW_ACTIVE =\n  \"flex flex-row items-center shrink-0 pr-2 text-sm bg-selected text-selected-foreground hover:bg-accent cursor-pointer\";\n\nexport const TREE_CHEVRON =\n  \"flex flex-row items-center justify-center shrink-0\";\n"
    },
    {
      "path": "packages/data/src/tree/row.tsx",
      "target": "@ui/gpuixui/data/tree/row.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { EventPayload, PublicInstance } from \"@gpuix/react\";\nimport { Icon } from \"../../icons/index.js\";\nimport {\n  iconSideOf,\n  rowHeightOf,\n  type DensitySource,\n} from \"../../primitives/core/index.js\";\nimport { cn } from \"../../tw/index.js\";\nimport type { ReactNode } from \"react\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { pointerWash, textStyle, themeColors, useTheme } from \"../theme.js\";\nimport { TREE_INDENT, type TreeRow } from \"./logic.js\";\nimport { TREE_CHEVRON, TREE_ROW, TREE_ROW_ACTIVE } from \"./recipe.js\";\n\nexport const TREE_INSET = 8;\n\nexport const treeRowHeight = (\n  height: number | undefined,\n  theme: DensitySource,\n): number => height ?? rowHeightOf(theme);\n\nexport interface TreeRowViewProps<T> {\n  readonly row: TreeRow<T>;\n\n  readonly label: string;\n\n  readonly active: boolean;\n\n  readonly tabbable: boolean;\n\n  readonly renderRow?: (row: TreeRow<T>) => ReactNode;\n\n  readonly height?: number;\n\n  readonly testId?: string;\n  readonly onPress: () => void;\n  readonly onKeyDown: (event: EventPayload) => void;\n  readonly attach: (instance: PublicInstance | null) => void;\n}\n\nconst Label = ({ active, label }: { active: boolean; label: string }) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  return (\n    <text\n      style={{\n        ...textStyle(theme, \"body\"),\n        color: active ? colors.selectedForeground : colors.foreground,\n      }}\n    >\n      {label}\n    </text>\n  );\n};\n\nexport const TreeRowView = <T,>({\n  row,\n  label,\n  active,\n  tabbable,\n  renderRow,\n  height,\n  testId,\n  onPress,\n  onKeyDown,\n  attach,\n}: TreeRowViewProps<T>) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n\n  const chevron = active ? colors.selectedForeground : colors.mutedForeground;\n  const hoverFill = active ? colors.accent : colors.hover;\n  const pressFill =\n    (active\n      ? pointerWash(theme, \"active\", \"accent\")\n      : pointerWash(theme, \"active\", \"hover\")) ?? colors.active;\n  return (\n    <div\n      ref={attach}\n      role=\"treeitem\"\n      aria-label={label}\n      aria-level={row.depth + 1}\n      aria-selected={active}\n      {...(row.hasChildren ? { \"aria-expanded\": row.expanded } : {})}\n      tabIndex={tabbable ? 0 : -1}\n      testId={testId}\n      onClick={onPress}\n      onKeyDown={onKeyDown}\n      style={{\n        ...recipeStyle(cn(active ? TREE_ROW_ACTIVE : TREE_ROW), theme),\n        height: treeRowHeight(height, theme),\n        paddingLeft: TREE_INSET + row.depth * TREE_INDENT,\n\n        hover: { backgroundColor: hoverFill },\n        active: { backgroundColor: pressFill },\n      }}\n    >\n      <div\n        testId={testId === undefined ? undefined : `${testId}-slot`}\n        style={{\n          ...recipeStyle(TREE_CHEVRON, theme),\n          width: iconSideOf(theme, \"md\"),\n          height: iconSideOf(theme, \"md\"),\n        }}\n      >\n        {row.hasChildren && chevron !== undefined ? (\n          <Icon\n            name={row.expanded ? \"chevron-down\" : \"chevron-right\"}\n            size=\"md\"\n            color={chevron}\n          />\n        ) : undefined}\n      </div>\n      {renderRow === undefined ? (\n        <Label active={active} label={label} />\n      ) : (\n        renderRow(row)\n      )}\n    </div>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/tree/use-tree.ts",
      "target": "@ui/gpuixui/data/tree/use-tree.ts",
      "type": "registry:lib",
      "content": "import { useGpuix, type EventPayload, type PublicInstance } from \"@gpuix/react\";\nimport { typedChar, useTypeahead } from \"../../components/typeahead.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { recordsOf } from \"../internal/list.js\";\nimport {\n  cursorRow,\n  emptyCursor,\n  isTreeKey,\n  treeCursorMove,\n  treeName,\n  type TreeCursor,\n  type TreeRow,\n} from \"./logic.js\";\n\nexport interface UseTreeOptions<T> {\n  readonly rows: ReadonlyArray<TreeRow<T>>;\n\n  readonly labelOf: (node: T) => string;\n\n  readonly cursor?: TreeCursor;\n\n  readonly onCursorChange?: (cursor: TreeCursor) => void;\n\n  readonly defaultCursor?: TreeCursor;\n\n  readonly onActivate?: (row: TreeRow<T>) => void;\n}\n\nexport interface TreeCommitOptions {\n  readonly focus?: boolean;\n}\n\nexport interface TreeController<T> {\n  readonly cursor: TreeCursor;\n\n  readonly tabKey: Option.Option<string>;\n\n  readonly commit: (cursor: TreeCursor, options?: TreeCommitOptions) => void;\n\n  readonly activate: (row: TreeRow<T>) => void;\n\n  readonly handleKey: (event: EventPayload) => void;\n\n  readonly attach: (key: string) => (instance: PublicInstance | null) => void;\n\n  readonly holdsFocus: () => boolean;\n}\n\nexport const useTree = <T>({\n  rows,\n  labelOf,\n  cursor,\n  onCursorChange,\n  defaultCursor,\n  onActivate,\n}: UseTreeOptions<T>): TreeController<T> => {\n  const renderer = useGpuix().renderer;\n\n  const visible = useMemo(() => recordsOf(rows), [rows]);\n  const [uncontrolled, setUncontrolled] = useState<TreeCursor>(\n    defaultCursor ?? emptyCursor,\n  );\n  const resolved = cursor ?? uncontrolled;\n  const elements = useRef(new Map<string, number>());\n  const focused = useRef<Option.Option<string>>(resolved.active);\n\n  const fromKeys = useRef(false);\n  const deferred = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n  useEffect(\n    () => () => {\n      if (deferred.current !== undefined) clearTimeout(deferred.current);\n    },\n    [],\n  );\n\n  const commit = useCallback(\n    (next: TreeCursor, options?: TreeCommitOptions) => {\n      fromKeys.current = options?.focus !== false;\n      if (options?.focus === false) focused.current = next.active;\n      if (cursor === undefined) setUncontrolled(next);\n      onCursorChange?.(next);\n    },\n    [cursor, onCursorChange],\n  );\n\n  const typeahead = useTypeahead(\n    Arr.map(visible, (row) => ({\n      key: row.key,\n\n      label: treeName(labelOf(row.node), row.key),\n      disabled: false,\n    })),\n    Option.flatMap(resolved.active, (key) =>\n      Arr.findFirstIndex(visible, (row) => row.key === key),\n    ),\n    (match) => commit({ ...resolved, active: Option.some(match.key) }),\n  );\n\n  useLayoutEffect(() => {\n    if (\n      Option.getOrUndefined(resolved.active) ===\n      Option.getOrUndefined(focused.current)\n    ) {\n      fromKeys.current = false;\n      return;\n    }\n    const ours = fromKeys.current;\n    fromKeys.current = false;\n    focused.current = resolved.active;\n    Option.match(resolved.active, {\n      onNone: () => {},\n      onSome: (key) => {\n        const focusRow = () => {\n          const id = elements.current.get(key);\n          if (id !== undefined) renderer?.focusElement?.(id);\n        };\n        focusRow();\n        if (ours) return;\n\n        if (deferred.current !== undefined) clearTimeout(deferred.current);\n        deferred.current = setTimeout(() => {\n          deferred.current = undefined;\n          focusRow();\n        }, 0);\n      },\n    });\n  });\n\n  const activate = useCallback(\n    (row: TreeRow<T>) => {\n      const onRow = { ...resolved, active: Option.some(row.key) };\n\n      commit(row.hasChildren ? treeCursorMove(visible, onRow, \"enter\") : onRow);\n      onActivate?.(row);\n    },\n    [commit, onActivate, resolved, visible],\n  );\n\n  const holdsFocus = useCallback((): boolean => {\n    const id = renderer?.getFocusedElementId?.();\n    if (id === undefined || id === null) return false;\n    return Arr.some(\n      Arr.fromIterable(elements.current.values()),\n      (value) => value === id,\n    );\n  }, [renderer]);\n\n  const handleKey = useCallback(\n    (event: EventPayload) => {\n      if (!holdsFocus()) return;\n      if (isTreeKey(event.key)) {\n        const next = treeCursorMove(visible, resolved, event.key);\n        commit(next);\n        if (event.key === \"enter\" || event.key === \"space\") {\n          if (onActivate !== undefined) {\n            Option.match(next.active, {\n              onNone: () => {},\n              onSome: (key) => {\n                Option.match(\n                  Arr.findFirst(visible, (candidate) => candidate.key === key),\n                  { onNone: () => {}, onSome: onActivate },\n                );\n              },\n            });\n          }\n        }\n        return;\n      }\n      Option.match(typedChar(event), {\n        onNone: () => {},\n        onSome: typeahead,\n      });\n    },\n    [commit, onActivate, resolved, visible, typeahead, holdsFocus],\n  );\n\n  const attach = useCallback(\n    (key: string) => (instance: PublicInstance | null) => {\n      if (instance === null) elements.current.delete(key);\n      else elements.current.set(key, instance.id);\n    },\n    [],\n  );\n\n  const tabKey = Option.match(cursorRow(visible, resolved), {\n    onNone: () => Option.none<string>(),\n    onSome: (row) => Option.some(row.key),\n  });\n\n  return {\n    cursor: resolved,\n    tabKey,\n    commit,\n    activate,\n    handleKey,\n    attach,\n    holdsFocus,\n  };\n};\n"
    },
    {
      "path": "packages/data/src/virtual/list.tsx",
      "target": "@ui/gpuixui/data/virtual/list.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport type { EventPayload, Props, StyleDesc } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { cn } from \"../../tw/index.js\";\nimport * as Option from \"effect/Option\";\nimport type { ReactNode } from \"react\";\nimport { recipeStyle } from \"../recipe.js\";\nimport { useTheme } from \"../theme.js\";\nimport { paintableSize } from \"../internal/size.js\";\nimport { rowCount, rowHeightOf, rowIndex, overdrawRows } from \"./window.js\";\n\nexport interface VisibleRange {\n  readonly first: number;\n\n  readonly last: number;\n}\n\nexport const visibleRange = (\n  event: EventPayload,\n): Option.Option<VisibleRange> =>\n  Option.flatMap(Option.fromNullishOr(event.startIndex), (first) =>\n    Option.map(\n      Option.fromNullishOr(event.endIndex),\n      (last): VisibleRange => ({ first, last }),\n    ),\n  );\n\ninterface VirtualListShared extends Omit<Props, \"children\" | \"onVisibleRange\"> {\n  readonly height?: number;\n\n  readonly followTail?: boolean;\n\n  readonly alignment?: \"top\" | \"bottom\";\n\n  readonly overdraw?: number;\n\n  readonly onVisibleRange?: (range: VisibleRange) => void;\n\n  readonly className?: string;\n  readonly children?: ReactNode;\n}\n\nexport type VirtualListProps =\n  | (VirtualListShared & {\n      readonly itemCount?: undefined;\n      readonly estimatedItemHeight?: number;\n      readonly windowStart?: undefined;\n    })\n  | (VirtualListShared & {\n      readonly itemCount: number;\n\n      readonly estimatedItemHeight: number;\n      readonly windowStart?: number;\n    });\n\nconst listBox = (height?: number): StyleDesc =>\n  height === undefined\n    ? {}\n    : { height, flexGrow: 0, flexShrink: 0, flexBasis: height };\n\nexport const VirtualList = (props: VirtualListProps) => {\n  const theme = useTheme();\n  const {\n    height,\n    followTail,\n    alignment,\n    overdraw,\n    onVisibleRange,\n    className,\n    children,\n    style,\n    ...rest\n  } = props;\n  const handleRange =\n    onVisibleRange === undefined\n      ? undefined\n      : (event: EventPayload) =>\n          Option.match(visibleRange(event), {\n            onNone: () => {},\n            onSome: onVisibleRange,\n          });\n  const shared = {\n    ...rest,\n    followTail,\n    alignment,\n    overdraw: overdraw === undefined ? undefined : overdrawRows(overdraw),\n    onVisibleRange: handleRange,\n    style: mergeStyles(\n      recipeStyle(cn(className ?? \"\"), theme),\n      mergeStyles(listBox(Option.getOrUndefined(paintableSize(height))), style),\n    ),\n  };\n  const windowed =\n    props.itemCount === undefined\n      ? undefined\n      : {\n          itemCount: rowCount(props.itemCount),\n\n          estimatedItemHeight: rowHeightOf(props.estimatedItemHeight),\n          windowStart:\n            props.windowStart === undefined\n              ? undefined\n              : rowIndex(props.windowStart),\n        };\n  return windowed === undefined ? (\n    <virtual-list {...shared}>{children}</virtual-list>\n  ) : (\n    <virtual-list\n      {...shared}\n      itemCount={windowed.itemCount}\n      estimatedItemHeight={windowed.estimatedItemHeight}\n      windowStart={windowed.windowStart}\n    >\n      {children}\n    </virtual-list>\n  );\n};\n"
    },
    {
      "path": "packages/data/src/virtual/tree.tsx",
      "target": "@ui/gpuixui/data/virtual/tree.tsx",
      "type": "registry:component",
      "content": "/** @jsxImportSource @gpuix/react */\nimport { useGpuix, type Props, type PublicInstance } from \"@gpuix/react\";\nimport { mergeStyles } from \"@gpuix/react/floating\";\nimport { rowHeightOf } from \"../../primitives/core/index.js\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\nimport {\n  Fragment,\n  useCallback,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  type ReactNode,\n} from \"react\";\nimport { themeColors, useTheme } from \"../theme.js\";\nimport { recordsOf } from \"../internal/list.js\";\nimport { paintableSize } from \"../internal/size.js\";\nimport { paneFill, usePaintedHeight } from \"../internal/painted.js\";\nimport { treeName, type TreeCursor, type TreeRow } from \"../tree/logic.js\";\nimport { TreeRowView } from \"../tree/row.js\";\nimport { useTree } from \"../tree/use-tree.js\";\nimport { VirtualList, type VisibleRange } from \"./list.js\";\nimport { useVirtualRows } from \"./use-virtual-rows.js\";\nimport { anchorOffset, rowHeightOf as windowRowHeight } from \"./window.js\";\n\nexport interface VirtualTreeProps<T> extends Omit<\n  Props,\n  \"children\" | \"onVisibleRange\"\n> {\n  readonly rows: ReadonlyArray<TreeRow<T>>;\n\n  readonly labelOf: (node: T) => string;\n\n  readonly cursor?: TreeCursor;\n\n  readonly onCursorChange?: (cursor: TreeCursor) => void;\n\n  readonly defaultCursor?: TreeCursor;\n\n  readonly onActivate?: (row: TreeRow<T>) => void;\n\n  readonly renderRow?: (row: TreeRow<T>) => ReactNode;\n\n  readonly height: number;\n\n  readonly rowHeight?: number;\n\n  readonly overdraw?: number;\n\n  readonly onVisibleRange?: (range: VisibleRange) => void;\n\n  readonly instanceRef?: (instance: PublicInstance | null) => void;\n  readonly className?: string;\n\n  readonly testId?: string;\n}\n\nexport const VirtualTree = <T,>({\n  rows: dataRows,\n  labelOf,\n  cursor,\n  onCursorChange,\n  defaultCursor,\n  onActivate,\n  renderRow,\n  height,\n  rowHeight: namedRowHeight,\n  overdraw,\n  onVisibleRange,\n  onLayout,\n  instanceRef,\n  className,\n  testId,\n  style,\n  ...rest\n}: VirtualTreeProps<T>) => {\n  const theme = useTheme();\n  const colors = themeColors(theme);\n  const renderer = useGpuix().renderer;\n\n  const bound = paintableSize(height);\n  const painted = usePaintedHeight(onLayout);\n\n  const rowHeight = windowRowHeight(namedRowHeight, rowHeightOf(theme));\n\n  const listHeight = Option.orElse(bound, () => painted.measured);\n\n  const rows = useMemo(() => recordsOf(dataRows), [dataRows]);\n  const window = useVirtualRows({\n    count: rows.length,\n    rowHeight,\n    viewportHeight: Option.getOrElse(listHeight, () => 0),\n    overdraw,\n  });\n  const tree = useTree({\n    rows,\n    labelOf,\n    cursor,\n    onCursorChange,\n    defaultCursor,\n    onActivate,\n  });\n  const instance = useRef<PublicInstance | null>(null);\n  const attach = useCallback(\n    (next: PublicInstance | null) => {\n      instance.current = next;\n      instanceRef?.(next);\n    },\n    [instanceRef],\n  );\n\n  const activeIndex = Option.flatMap(tree.cursor.active, (key) =>\n    Arr.findFirstIndex(rows, (row) => row.key === key),\n  );\n\n  Option.match(activeIndex, {\n    onNone: () => {},\n    onSome: (index) => {\n      if (!Arr.contains(window.mounted, index)) window.reveal(index);\n    },\n  });\n\n  useLayoutEffect(() => {\n    if (Option.isSome(activeIndex)) return;\n    if (rows.length === 0) return;\n    const index = Math.min(\n      rows.length - 1,\n      Math.max(0, Math.floor(window.scrollOffset / rowHeight)),\n    );\n    const row = rows[index];\n    if (row === undefined) return;\n    tree.commit(\n      { ...tree.cursor, active: Option.some(row.key) },\n      { focus: tree.holdsFocus() },\n    );\n  }, [activeIndex, rows, rowHeight, tree, window.scrollOffset]);\n\n  const handleRange = useCallback(\n    (range: VisibleRange) => {\n      const anchor = anchorOffset(\n        Option.fromNullishOr(\n          renderer?.getListScrollTop?.(instance.current?.id ?? -1),\n        ),\n        rowHeight,\n      );\n      const offset = Option.getOrElse(anchor, () => range.first * rowHeight);\n      window.setScrollOffset(offset);\n\n      Option.match(activeIndex, {\n        onNone: () => {},\n        onSome: (index) => {\n          if (Arr.contains(window.mountedAt(offset), index)) return;\n          const anchorRow =\n            rows[\n              Math.min(\n                rows.length - 1,\n                Math.max(0, Math.floor(offset / rowHeight)),\n              )\n            ];\n          if (anchorRow === undefined) return;\n          tree.commit(\n            { ...tree.cursor, active: Option.some(anchorRow.key) },\n            { focus: tree.holdsFocus() },\n          );\n        },\n      });\n      onVisibleRange?.(range);\n    },\n    [activeIndex, rows, onVisibleRange, renderer, rowHeight, tree, window],\n  );\n\n  useLayoutEffect(() => {\n    Option.match(window.scrollTarget, {\n      onNone: () => {},\n      onSome: (index) => {\n        const list = instance.current;\n        if (list !== null) renderer?.scrollToItem?.(list.id, index);\n        window.consumeScrollTarget();\n      },\n    });\n  }, [window.scrollTarget, window.consumeScrollTarget, renderer]);\n\n  const list = (\n    <VirtualList\n      {...rest}\n      ref={attach}\n      role=\"tree\"\n      testId={testId}\n      height={Option.getOrUndefined(listHeight)}\n      itemCount={rows.length}\n      estimatedItemHeight={rowHeight}\n      windowStart={window.windowStart}\n      onVisibleRange={handleRange}\n      className={className}\n      style={mergeStyles({ scrollbarColor: colors.scrollbar }, style)}\n    >\n      {Arr.map(window.mounted, (index) => {\n        const row = rows[index];\n        if (row === undefined) return null;\n        return (\n          <Fragment key={row.key}>\n            <TreeRowView\n              row={row}\n              active={Option.contains(tree.cursor.active, row.key)}\n              tabbable={Option.contains(tree.tabKey, row.key)}\n              label={treeName(labelOf(row.node), row.key)}\n              renderRow={renderRow}\n              height={rowHeight}\n              testId={\n                testId === undefined ? undefined : `${testId}-row-${row.key}`\n              }\n              onPress={() => tree.activate(row)}\n              onKeyDown={tree.handleKey}\n              attach={tree.attach(row.key)}\n            />\n          </Fragment>\n        );\n      })}\n    </VirtualList>\n  );\n\n  return Option.match(bound, {\n    onSome: () => list,\n    onNone: () => (\n      <div onLayout={painted.onLayout} style={paneFill(bound)}>\n        {list}\n      </div>\n    ),\n  });\n};\n"
    },
    {
      "path": "packages/data/src/virtual/use-virtual-rows.ts",
      "target": "@ui/gpuixui/data/virtual/use-virtual-rows.ts",
      "type": "registry:lib",
      "content": "import * as Option from \"effect/Option\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport {\n  clampScrollOffset,\n  createWindowCalculator,\n  rowCount,\n  rowHeightOf,\n  rowIndex,\n  type VirtualWindow,\n  type WindowSpec,\n} from \"./window.js\";\n\nexport interface VirtualReveal {\n  readonly index: number;\n}\n\nexport interface UseVirtualRowsOptions extends WindowSpec {\n  readonly initialOffset?: number;\n\n  readonly reveal?: VirtualReveal;\n}\n\nexport interface VirtualRowsController {\n  readonly window: VirtualWindow;\n\n  readonly windowStart: number;\n\n  readonly mounted: ReadonlyArray<number>;\n\n  readonly mountedAt: (offset: number) => ReadonlyArray<number>;\n\n  readonly scrollOffset: number;\n\n  readonly setScrollOffset: (offset: number) => void;\n\n  readonly reveal: (index: number) => VirtualWindow;\n\n  readonly scrollTarget: Option.Option<number>;\n  readonly consumeScrollTarget: () => void;\n}\n\nexport const useVirtualRows = ({\n  count,\n  rowHeight,\n  viewportHeight,\n  overdraw,\n  initialOffset,\n  reveal,\n}: UseVirtualRowsOptions): VirtualRowsController => {\n  const height = rowHeightOf(rowHeight);\n  const calculator = useMemo(\n    () =>\n      createWindowCalculator({\n        count,\n        rowHeight: height,\n        viewportHeight,\n        overdraw,\n      }),\n    [count, height, viewportHeight, overdraw],\n  );\n  const [scrollOffset, setScrollOffset] = useState(initialOffset ?? 0);\n  const [lastReveal, setLastReveal] = useState<VirtualReveal | undefined>(\n    undefined,\n  );\n  const [scrollTarget, setScrollTarget] = useState<Option.Option<number>>(\n    Option.none(),\n  );\n\n  const spec = { count, rowHeight: height, viewportHeight, overdraw };\n  const offset = clampScrollOffset(spec, scrollOffset);\n\n  if (offset !== scrollOffset) setScrollOffset(offset);\n\n  const window = calculator.at(offset);\n  const mounted = calculator.indicesAt(offset);\n\n  const revealTo = useCallback(\n    (index: number) => {\n      const target = calculator.around(index);\n\n      setScrollOffset(rowIndex(index) * height);\n      return target;\n    },\n    [calculator, height],\n  );\n\n  if (reveal !== undefined && reveal !== lastReveal) {\n    setLastReveal(reveal);\n    revealTo(reveal.index);\n\n    const row = Math.min(rowCount(count) - 1, rowIndex(reveal.index));\n    if (row >= 0) setScrollTarget(Option.some(row));\n  }\n\n  return {\n    window,\n    windowStart: window.start,\n    mounted,\n    mountedAt: calculator.indicesAt,\n    scrollOffset: offset,\n    setScrollOffset,\n    reveal: revealTo,\n    scrollTarget,\n    consumeScrollTarget: useCallback(() => setScrollTarget(Option.none()), []),\n  };\n};\n"
    },
    {
      "path": "packages/data/src/virtual/window.ts",
      "target": "@ui/gpuixui/data/virtual/window.ts",
      "type": "registry:lib",
      "content": "import { Virtualizer } from \"@tanstack/virtual-core\";\nimport * as Arr from \"effect/Array\";\nimport * as Option from \"effect/Option\";\n\nexport const DEFAULT_OVERDRAW = 4;\n\nexport const DEFAULT_ROW_HEIGHT = 37;\n\nexport interface VirtualWindow {\n  readonly start: number;\n  readonly end: number;\n}\n\nexport interface WindowSpec {\n  readonly count: number;\n\n  readonly rowHeight: number;\n\n  readonly viewportHeight: number;\n\n  readonly overdraw?: number;\n}\n\nexport interface WindowCalculator {\n  readonly indicesAt: (scrollOffset: number) => ReadonlyArray<number>;\n\n  readonly at: (scrollOffset: number) => VirtualWindow;\n\n  readonly around: (index: number) => VirtualWindow;\n}\n\nexport const rowHeightOf = (\n  height: number | undefined,\n  fallback: number = DEFAULT_ROW_HEIGHT,\n): number =>\n  height !== undefined && Number.isFinite(height) && height > 0\n    ? height\n    : fallback;\n\nconst viewportOf = (spec: WindowSpec): number =>\n  Number.isFinite(spec.viewportHeight) && spec.viewportHeight > 0\n    ? spec.viewportHeight\n    : rowHeightOf(spec.rowHeight);\n\nexport const overdrawRows = (overdraw: number | undefined): number =>\n  overdraw === undefined || !Number.isFinite(overdraw)\n    ? DEFAULT_OVERDRAW\n    : Math.max(0, Math.floor(overdraw));\n\nexport const rowCount = (count: number): number =>\n  Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0;\n\nexport const maxScrollOffset = (spec: WindowSpec): number =>\n  Math.max(\n    0,\n    rowCount(spec.count) * rowHeightOf(spec.rowHeight) - viewportOf(spec),\n  );\n\nexport const clampScrollOffset = (\n  spec: WindowSpec,\n  scrollOffset: number,\n): number => {\n  const offset = Number.isFinite(scrollOffset) ? Math.max(0, scrollOffset) : 0;\n  return Math.min(offset, maxScrollOffset(spec));\n};\n\nexport const createWindowCalculator = (spec: WindowSpec): WindowCalculator => {\n  const rowHeight = rowHeightOf(spec.rowHeight);\n  const virtualizer = new Virtualizer<never, never>({\n    count: rowCount(spec.count),\n    estimateSize: () => rowHeight,\n    getScrollElement: () => null,\n    observeElementRect: () => undefined,\n    observeElementOffset: () => undefined,\n    scrollToFn: () => undefined,\n    overscan: overdrawRows(spec.overdraw),\n  });\n\n  const indicesAt = (scrollOffset: number): ReadonlyArray<number> => {\n    virtualizer.scrollRect = { width: 0, height: viewportOf(spec) };\n    virtualizer.scrollOffset = clampScrollOffset(spec, scrollOffset);\n    return Arr.filter(\n      virtualizer.getVirtualIndexes(),\n      (index) => index >= 0 && index < rowCount(spec.count),\n    );\n  };\n\n  const at = (scrollOffset: number): VirtualWindow => {\n    const indices = indicesAt(scrollOffset);\n    const first = Option.getOrUndefined(Arr.head(indices));\n    const last = Option.getOrUndefined(Arr.last(indices));\n    return first === undefined || last === undefined\n      ? { start: 0, end: 0 }\n      : { start: first, end: last + 1 };\n  };\n\n  return {\n    indicesAt,\n    at,\n    around: (index) => at(rowIndex(index) * rowHeight),\n  };\n};\n\nexport const rowIndex = (index: number): number =>\n  Number.isFinite(index) ? Math.max(0, Math.floor(index)) : 0;\n\nexport const anchorOffset = (\n  anchor: Option.Option<ReadonlyArray<number>>,\n  rowHeight: number,\n): Option.Option<number> =>\n  Option.flatMap(anchor, (values) => {\n    const index = values[0];\n    const offsetInItem = values[1];\n    if (index === undefined || offsetInItem === undefined) return Option.none();\n    const into = Number.isFinite(offsetInItem) ? Math.max(0, offsetInItem) : 0;\n    return Option.some(rowIndex(index) * rowHeightOf(rowHeight) + into);\n  });\n"
    }
  ],
  "docs": "[Virtual trees](https://gpuixui.sudokw.com/data-display/virtual-tree) — These are GPUix native components: the files render through GPUix's reconciler against `@gpuix/react`, not the DOM, and need the per-platform `@gpuix/native` binding. The CLI installs the `@gpuixui/*` runtime the components import as registry items, so the copied tree resolves on its own; wrap it in `ThemeProvider` before it renders.",
  "meta": {
    "url": "/data-display/virtual-tree",
    "kind": "component",
    "runtime": "gpuix",
    "registry": "gpuixui"
  }
}