{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "reui-cascader",
  "title": "Cascader",
  "description": "A multi-level combobox with drill-down, column and tree modes, breadcrumbs, search, async levels and a virtualized list.",
  "dependencies": [
    "@base-ui/react",
    "@radix-ui/react-slot",
    "@tanstack/react-virtual",
    "lucide-react"
  ],
  "registryDependencies": [
    "scroll-area",
    "spinner"
  ],
  "files": [
    {
      "path": "registry/tealmac/reui-cascader/cascader-async.tsx",
      "content": "/**\n * cascader-async - adapted for tealmac UI from ReUI \"cascader/cascader-async\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\n\"use client\"\n\nimport * as React from \"react\"\n\nimport { useCascaderState } from \"@/components/reui/cascader/cascader-context\"\nimport {\n  CASCADER_ROOT_KEY,\n  isCascaderMoreNode,\n} from \"@/components/reui/cascader/cascader-lib\"\nimport type {\n  CascaderIndex,\n  CascaderLoadContext,\n  CascaderLoadReason,\n  CascaderLoadResult,\n  CascaderLoadState,\n  CascaderNode,\n  CascaderSearchContext,\n} from \"@/components/reui/cascader/cascader-types\"\n\n/**\n * Async data for the cascader. Loaded pages live in their own store and are\n * MERGED onto the index built from `items`, never folded into that build:\n * `items` changes identity on any parent re-render, and folding would discard\n * every level the user drilled into. Map membership is the load discriminator:\n * no `states` entry means never fetched, while an entry with no `loading`, no\n * `error` and no children means empty for real.\n */\n\n/* -------------------------------------------------------------------------- */\n/*                                    Types                                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Fetches one level. `node` is `null` for the root level, and only\n * `{ value, label: value }`, no `data`, for a level on screen whose node has\n * not loaded: a deep link before the level above loads, or a page-loaded\n * level after a `loadKey` change.\n */\nexport type CascaderGetChildren<T = unknown> = (\n  node: CascaderNode<T> | null,\n  context: CascaderLoadContext\n) =>\n  | CascaderNode<T>[]\n  | CascaderLoadResult<T>\n  | Promise<CascaderNode<T>[] | CascaderLoadResult<T>>\n\n/** Server-side search, replacing the local index scan while the query is set. */\nexport type CascaderOnSearch<T = unknown> = (\n  query: string,\n  context: CascaderSearchContext\n) =>\n  | CascaderNode<T>[]\n  | CascaderLoadResult<T>\n  | Promise<CascaderNode<T>[] | CascaderLoadResult<T>>\n\n/** Resolves a selected value to its ancestor chain, root first, node last. */\nexport type CascaderResolveValue<T = unknown> = (\n  value: string,\n  context: CascaderLoadContext\n) => CascaderNode<T>[] | Promise<CascaderNode<T>[]>\n\nexport interface CascaderLoaderStore<T = unknown> {\n  /** Keyed by LEVEL: a parent's value, or `CASCADER_ROOT_KEY` for the root. */\n  pages: Map<string, CascaderNode<T>[]>\n  states: Map<string, CascaderLoadState>\n  /** Keyed by node value: search hits and resolved selections, level-less. */\n  detached: Map<string, CascaderNode<T>>\n  /**\n   * A resolved chain's parent links, node value to level key. Kept apart from\n   * `pages`: a level's first real page replaced a chain stub there, and a node\n   * that page left out lost its parent, and the trigger its path.\n   */\n  links: Map<string, string>\n}\n\nexport interface UseCascaderLoaderOptions<T = unknown> {\n  /** The index built from `items`, before any pages are merged in. */\n  base: CascaderIndex<T>\n  getChildren?: CascaderGetChildren<T>\n  onSearch?: CascaderOnSearch<T>\n  resolveValue?: CascaderResolveValue<T>\n  /** Milliseconds of quiet before `onSearch` fires. */\n  searchDebounce?: number\n  /** Changing this drops every cached page, state and detached node. */\n  loadKey?: unknown\n  /** Speculatively fetch a branch's children when it is highlighted. */\n  prefetch?: boolean\n  /** Called when a request fails. Never for an aborted or superseded one. */\n  onLoadError?: (\n    error: unknown,\n    context: { parent: string | null; reason: string }\n  ) => void\n  /**\n   * Whether the panel is live: the popup is open, or the cascader is inline.\n   * Gates levels, paging and search. `resolveValue` runs regardless, and\n   * retries a failed value when this turns true.\n   */\n  enabled: boolean\n  query: string\n  /** Level keys that are currently on screen. Root is `CASCADER_ROOT_KEY`. */\n  levels: string[]\n  /** Handed to `onSearch` as its scope and read for nothing else: the\n   * navigation path, or `[]` when the search covers the whole tree. */\n  path: string[]\n  /** Current selection, for `resolveValue`. */\n  values: string[]\n}\n\nexport interface CascaderLoader<T = unknown> {\n  /** Whether a `getChildren` loader is configured at all. */\n  active: boolean\n  store: CascaderLoaderStore<T>\n  states: ReadonlyMap<string, CascaderLoadState>\n  /**\n   * Async search hits, `null` when no `onSearch` is running. EMPTY while the\n   * first request is in flight, so the level behind is not shown as the answer.\n   */\n  searchResults: CascaderNode<T>[] | null\n  searchState: CascaderLoadState | null\n  /**\n   * Fetches a level's FIRST page. No-ops on a `states` entry (in flight,\n   * loaded or failed) or when `items` fills the level. The level effect fires\n   * only for on-screen levels, so a branch merely PRESSED is asked for by hand.\n   */\n  ensureLevel: (parentKey: string, reason: CascaderLoadReason) => void\n  /** Fetches the next page of a level. No-ops unless one is available. */\n  loadMore: (parentKey: string) => void\n  /** Refires a failed level. No-ops unless that level is in an error state. */\n  retryLevel: (parentKey: string) => void\n  /** Refires the server search for the current query. No-ops without one. */\n  retrySearch: () => void\n  /** Schedules a speculative fetch. Safe to call on every highlight move. */\n  prefetchNode: (node: CascaderNode<T> | null | undefined) => void\n  /**\n   * Evicts ONE level: aborts its request, drops its `states`/`pages` entries\n   * and paging latch, so the level effect refetches it. `null` = root level.\n   */\n  invalidateLevel: (value: string | null) => void\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Constants                                 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Highlight dwell before `prefetch` fetches: long enough that holding ArrowDown\n * does not fire a request per row, short enough to beat the ArrowRight press.\n */\nconst PREFETCH_DELAY = 150\n\n/** Request keys for the two non-level requests. Never collide with a value. */\nconst SEARCH_KEY = \"\\u0000search\"\nconst RESOLVE_PREFIX = \"\\u0000resolve:\"\n\nconst NO_STATE: CascaderLoadState = {\n  loading: false,\n  error: false,\n  hasMore: false,\n}\n\n/** Stable empty result, so an idle search never churns the state context. */\nconst NO_RESULTS: CascaderNode<never>[] = []\n\nfunction createStore<T>(): CascaderLoaderStore<T> {\n  return {\n    pages: new Map(),\n    states: new Map(),\n    detached: new Map(),\n    links: new Map(),\n  }\n}\n\nfunction sameLoadState(a: CascaderLoadState, b: CascaderLoadState): boolean {\n  return (\n    a.loading === b.loading &&\n    a.error === b.error &&\n    a.hasMore === b.hasMore &&\n    a.cursor === b.cursor\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              Store transitions                             */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Copy-on-write, and a NO-OP when nothing changed: the merged index is memoised\n * on store identity, so a fresh object for an unchanged state would rebuild it.\n */\nfunction withLoadState<T>(\n  store: CascaderLoaderStore<T>,\n  key: string,\n  update: (state: CascaderLoadState) => CascaderLoadState\n): CascaderLoaderStore<T> {\n  const current = store.states.get(key) ?? NO_STATE\n  const next = update(current)\n  if (store.states.has(key) && sameLoadState(current, next)) return store\n  const states = new Map(store.states)\n  states.set(key, next)\n  return { ...store, states }\n}\n\nfunction withPage<T>(\n  store: CascaderLoaderStore<T>,\n  key: string,\n  items: readonly CascaderNode<T>[],\n  options: { append: boolean; hasMore: boolean; cursor?: string }\n): CascaderLoaderStore<T> {\n  const previous = options.append ? (store.pages.get(key) ?? []) : []\n  const seen = new Set(previous.map((node) => node.value))\n  const merged = previous.slice()\n  for (const item of items) {\n    if (seen.has(item.value)) continue\n    seen.add(item.value)\n    merged.push(item)\n  }\n\n  const pages = new Map(store.pages)\n  pages.set(key, merged)\n  const states = new Map(store.states)\n  states.set(key, {\n    loading: false,\n    error: false,\n    hasMore: options.hasMore,\n    cursor: options.cursor,\n  })\n  return { ...store, pages, states }\n}\n\nfunction withDetached<T>(\n  store: CascaderLoaderStore<T>,\n  items: readonly CascaderNode<T>[]\n): CascaderLoaderStore<T> {\n  let detached: Map<string, CascaderNode<T>> | null = null\n  for (const item of items) {\n    if (store.detached.get(item.value) === item) continue\n    detached = detached ?? new Map(store.detached)\n    detached.set(item.value, item)\n  }\n  if (!detached) return store\n  return { ...store, detached }\n}\n\n/**\n * Records a resolved ancestor chain, root first, as `detached` nodes plus\n * `links`, never as rows in `pages`: a stub there made an unloaded level read\n * as loaded. Writes no `states`, so every level on it still fetches for real.\n */\nfunction withChain<T>(\n  store: CascaderLoaderStore<T>,\n  chain: readonly CascaderNode<T>[]\n): CascaderLoaderStore<T> {\n  if (chain.length === 0) return store\n  const detached = new Map(store.detached)\n  const links = new Map(store.links)\n  let parentKey = CASCADER_ROOT_KEY\n\n  for (const node of chain) {\n    detached.set(node.value, node)\n    links.set(node.value, parentKey)\n    parentKey = node.value\n  }\n\n  return { ...store, detached, links }\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Hook                                    */\n/* -------------------------------------------------------------------------- */\n\ninterface CascaderLoaderLatest<T> {\n  base: CascaderIndex<T>\n  store: CascaderLoaderStore<T>\n  getChildren?: CascaderGetChildren<T>\n  onSearch?: CascaderOnSearch<T>\n  resolveValue?: CascaderResolveValue<T>\n  onLoadError?: (\n    error: unknown,\n    context: { parent: string | null; reason: string }\n  ) => void\n  prefetch: boolean\n  path: string[]\n  query: string\n}\n\ninterface CascaderSearchSlice<T> {\n  query: string\n  results: CascaderNode<T>[]\n  loading: boolean\n  error: boolean\n}\n\n/**\n * The loader. A SIBLING of the `buildCascaderIndex` memo, never inside it: the\n * build stays pure in `items`, the merge pure in that build plus this store.\n */\nexport function useCascaderLoader<T = unknown>({\n  base,\n  getChildren,\n  onSearch,\n  resolveValue,\n  searchDebounce = 250,\n  loadKey,\n  prefetch = false,\n  onLoadError,\n  enabled,\n  query,\n  levels,\n  path,\n  values,\n}: UseCascaderLoaderOptions<T>): CascaderLoader<T> {\n  const [store, setStore] = React.useState<CascaderLoaderStore<T>>(createStore)\n  const [search, setSearch] = React.useState<CascaderSearchSlice<T> | null>(\n    null\n  )\n\n  /**\n   * Latest callbacks, WRITTEN IN AN EFFECT: `getChildren` is inline in most\n   * consumers, so closing over it would refire every in-flight request per\n   * re-render. The ref is what keeps the request machinery `[]`-dep. Declared\n   * FIRST, since effects run in declaration order, so the level effect below\n   * already sees the current commit.\n   */\n  const latest = React.useRef<CascaderLoaderLatest<T>>({\n    base,\n    store,\n    getChildren,\n    onSearch,\n    resolveValue,\n    onLoadError,\n    prefetch,\n    path,\n    query,\n  })\n\n  React.useEffect(() => {\n    latest.current = {\n      base,\n      store,\n      getChildren,\n      onSearch,\n      resolveValue,\n      onLoadError,\n      prefetch,\n      path,\n      query,\n    }\n  })\n\n  /** One AbortController PER KEY: columns mode runs several levels at once. */\n  const controllers = React.useRef(new Map<string, AbortController>())\n  /** Monotonic per key. The stale guard for out-of-order responses. */\n  const requestIds = React.useRef(new Map<string, number>())\n  /** In-flight `(level, cursor)` signatures, so a duplicate ask is free. */\n  const inflight = React.useRef(new Map<string, string>())\n  /**\n   * The `(child count, cursor)` signature at the last paging fire, per level.\n   * Guards what `hasMore` cannot: a page of zero new items while the server\n   * still says `hasMore`. Cursor is IN the signature because an all-duplicates\n   * page advances it while the count stands still - real progress, which a\n   * count-only latch would brick forever.\n   */\n  const moreLatch = React.useRef(new Map<string, string>())\n  /** Values `resolveValue` has already been asked about, so it asks once. */\n  const attempted = React.useRef(new Set<string>())\n  /**\n   * Values whose resolve failed. They stay `attempted` until the next open\n   * rather than the next values pass: with inline `items` every parent render\n   * is one, and an `onLoadError` that set parent state looped at network speed.\n   */\n  const failed = React.useRef(new Set<string>())\n  /** Every node the loader has seen, so a level key can name its own node. */\n  const known = React.useRef(new Map<string, CascaderNode<T>>())\n  const timers = React.useRef<{\n    prefetch: ReturnType<typeof setTimeout> | null\n  }>({ prefetch: null })\n  /** Bumped by a `loadKey` change, so responses from before it are dropped. */\n  const epoch = React.useRef(0)\n\n  const remember = React.useCallback((nodes: readonly CascaderNode<T>[]) => {\n    const map = known.current\n    const walk = (list: readonly CascaderNode<T>[]) => {\n      for (const node of list) {\n        map.set(node.value, node)\n        if (node.children?.length) walk(node.children)\n      }\n    }\n    walk(nodes)\n  }, [])\n\n  const abortKey = React.useCallback((key: string) => {\n    const controller = controllers.current.get(key)\n    if (!controller) return\n    controllers.current.delete(key)\n    inflight.current.delete(key)\n    controller.abort()\n  }, [])\n\n  /* ------------------------------- level load ------------------------------ */\n\n  const runLoad = React.useCallback(\n    (\n      key: string,\n      reason: CascaderLoadReason,\n      cursor: string | undefined,\n      append: boolean\n    ) => {\n      const { getChildren: loader, base: currentBase } = latest.current\n      if (!loader) return\n\n      const signature = `${append ? \"1\" : \"0\"}:${cursor ?? \"\"}`\n      if (inflight.current.get(key) === signature) return\n\n      abortKey(key)\n      const controller = new AbortController()\n      controllers.current.set(key, controller)\n      inflight.current.set(key, signature)\n\n      const requestId = (requestIds.current.get(key) ?? 0) + 1\n      requestIds.current.set(key, requestId)\n      const startEpoch = epoch.current\n\n      // A deep link (`path`, `expanded`) can put a level on screen before the\n      // level above it loads, and a `loadKey` reset forgets every node. `null`\n      // is the ROOT's argument, and a loader answered it with the root rows,\n      // so the level gets its value instead.\n      const node =\n        key === CASCADER_ROOT_KEY\n          ? null\n          : (currentBase.byValue.get(key) ??\n            known.current.get(key) ?? { value: key, label: key })\n\n      setStore((prev) =>\n        withLoadState(prev, key, (state) => ({\n          ...state,\n          loading: true,\n          error: false,\n        }))\n      )\n\n      const settle = () => {\n        if (inflight.current.get(key) === signature)\n          inflight.current.delete(key)\n        if (controllers.current.get(key) === controller) {\n          controllers.current.delete(key)\n        }\n      }\n\n      const stale = () =>\n        controller.signal.aborted ||\n        startEpoch !== epoch.current ||\n        requestId !== requestIds.current.get(key)\n\n      // `Promise.resolve().then(...)`, not a direct call: it normalises a\n      // SYNCHRONOUS throw into a rejection instead of taking the render down.\n      Promise.resolve()\n        .then(() => loader(node, { signal: controller.signal, cursor, reason }))\n        .then((result) => {\n          settle()\n          if (stale()) return\n          const items = Array.isArray(result) ? result : result.items\n          const nextCursor = Array.isArray(result)\n            ? undefined\n            : result.nextCursor\n          const hasMore = Array.isArray(result)\n            ? false\n            : (result.hasMore ?? nextCursor != null)\n\n          if (!append) moreLatch.current.delete(key)\n          remember(items)\n          setStore((prev) =>\n            withPage(prev, key, items, { append, hasMore, cursor: nextCursor })\n          )\n        })\n        .catch((error: unknown) => {\n          settle()\n          if (stale()) return\n          setStore((prev) =>\n            withLoadState(prev, key, (state) => ({\n              ...state,\n              loading: false,\n              error: true,\n            }))\n          )\n          // Behind the stale guard: an abort is navigation, not a failure.\n          latest.current.onLoadError?.(error, {\n            parent: key === CASCADER_ROOT_KEY ? null : key,\n            reason,\n          })\n        })\n    },\n    [abortKey, remember]\n  )\n\n  const ensureLevel = React.useCallback(\n    (key: string, reason: CascaderLoadReason) => {\n      const {\n        getChildren: loader,\n        store: current,\n        base: index,\n      } = latest.current\n      if (!loader) return\n      if (current.states.has(key)) return\n      // A level `items` already fills is not the loader's business: that is how\n      // a static root plus `getChildren` for the branches works with no flag.\n      if (index.childrenOf.has(key)) return\n      runLoad(key, reason, undefined, false)\n    },\n    [runLoad]\n  )\n\n  const loadMore = React.useCallback(\n    (key: string) => {\n      const { getChildren: loader, store: current } = latest.current\n      if (!loader) return\n      const state = current.states.get(key)\n      if (!state || state.loading || !state.hasMore) return\n\n      const loaded = current.pages.get(key)?.length ?? 0\n      const signature = `${loaded}:${state.cursor ?? \"\"}`\n      if (moreLatch.current.get(key) === signature) return\n      moreLatch.current.set(key, signature)\n\n      runLoad(key, \"more\", state.cursor, true)\n    },\n    [runLoad]\n  )\n\n  const retryLevel = React.useCallback(\n    (key: string) => {\n      const { getChildren: loader, store: current } = latest.current\n      if (!loader) return\n      const state = current.states.get(key)\n      if (!state?.error) return\n\n      const loaded = current.pages.get(key)?.length ?? 0\n      // A retry must be able to re-fire the page the latch just blocked.\n      moreLatch.current.delete(key)\n      runLoad(key, \"retry\", loaded > 0 ? state.cursor : undefined, loaded > 0)\n    },\n    [runLoad]\n  )\n\n  /**\n   * `detached` and `links` are deliberately untouched: chains and search hits\n   * belong to no level, and the trigger needs their path while the new page is\n   * in flight.\n   */\n  const invalidateLevel = React.useCallback(\n    (value: string | null) => {\n      const key = value ?? CASCADER_ROOT_KEY\n      abortKey(key)\n      // Bump the id too: a response past its signal check must still be stale.\n      requestIds.current.set(key, (requestIds.current.get(key) ?? 0) + 1)\n      moreLatch.current.delete(key)\n      setStore((prev) => {\n        if (!prev.states.has(key) && !prev.pages.has(key)) return prev\n        const states = new Map(prev.states)\n        states.delete(key)\n        const pages = new Map(prev.pages)\n        pages.delete(key)\n        return { ...prev, pages, states }\n      })\n    },\n    [abortKey]\n  )\n\n  const prefetchNode = React.useCallback(\n    (node: CascaderNode<T> | null | undefined) => {\n      const {\n        prefetch: on,\n        getChildren: loader,\n        store: current,\n      } = latest.current\n      if (!on || !loader || !node) return\n      if (isCascaderMoreNode(node)) return\n      if (!node.hasChildren) return\n      if (current.states.has(node.value)) return\n\n      const holder = timers.current\n      if (holder.prefetch) clearTimeout(holder.prefetch)\n      // A TIMEOUT, not a direct call: `onItemHighlighted` fires from a layout\n      // effect, where a synchronous setState is a render-phase cascade.\n      holder.prefetch = setTimeout(() => {\n        holder.prefetch = null\n        ensureLevel(node.value, \"prefetch\")\n      }, PREFETCH_DELAY)\n    },\n    [ensureLevel]\n  )\n\n  /* --------------------------------- search -------------------------------- */\n\n  const runSearch = React.useCallback(\n    (text: string) => {\n      const { onSearch: searcher, path: currentPath } = latest.current\n      if (!searcher) return\n\n      abortKey(SEARCH_KEY)\n      const controller = new AbortController()\n      controllers.current.set(SEARCH_KEY, controller)\n      const requestId = (requestIds.current.get(SEARCH_KEY) ?? 0) + 1\n      requestIds.current.set(SEARCH_KEY, requestId)\n      const startEpoch = epoch.current\n\n      setSearch((prev) => ({\n        query: text,\n        results: prev?.results ?? [],\n        loading: true,\n        error: false,\n      }))\n\n      const settle = () => {\n        if (controllers.current.get(SEARCH_KEY) === controller) {\n          controllers.current.delete(SEARCH_KEY)\n        }\n      }\n      const stale = () =>\n        controller.signal.aborted ||\n        startEpoch !== epoch.current ||\n        requestId !== requestIds.current.get(SEARCH_KEY)\n\n      Promise.resolve()\n        .then(() =>\n          searcher(text, { signal: controller.signal, path: currentPath })\n        )\n        .then((result) => {\n          settle()\n          if (stale()) return\n          const items = Array.isArray(result) ? result : result.items\n          remember(items)\n          // Search hits go to `detached`, never a level: a hit lives anywhere in\n          // the tree, and filing it under the open level would misplace it.\n          setStore((prev) => withDetached(prev, items))\n          setSearch({\n            query: text,\n            results: items,\n            loading: false,\n            error: false,\n          })\n        })\n        .catch((error: unknown) => {\n          settle()\n          if (stale()) return\n          setSearch((prev) => ({\n            query: text,\n            results: prev?.results ?? [],\n            loading: false,\n            error: true,\n          }))\n          latest.current.onLoadError?.(error, {\n            parent: null,\n            reason: \"search\",\n          })\n        })\n    },\n    [abortKey, remember]\n  )\n\n  const retrySearch = React.useCallback(() => {\n    const text = latest.current.query.trim()\n    if (text) runSearch(text)\n  }, [runSearch])\n\n  /* --------------------------------- resolve ------------------------------- */\n\n  const runResolve = React.useCallback(\n    (value: string) => {\n      const { resolveValue: resolver } = latest.current\n      if (!resolver) return\n\n      const key = `${RESOLVE_PREFIX}${value}`\n      abortKey(key)\n      const controller = new AbortController()\n      controllers.current.set(key, controller)\n      const startEpoch = epoch.current\n\n      const settle = () => {\n        if (controllers.current.get(key) === controller) {\n          controllers.current.delete(key)\n        }\n      }\n\n      Promise.resolve()\n        .then(() =>\n          resolver(value, { signal: controller.signal, reason: \"resolve\" })\n        )\n        .then((chain) => {\n          settle()\n          if (controller.signal.aborted || startEpoch !== epoch.current) return\n          if (!chain?.length) return\n          remember(chain)\n          setStore((prev) => withChain(prev, chain))\n        })\n        .catch((error: unknown) => {\n          // Read BEFORE `settle`. A newer ask or a reset that replaced this\n          // request owns the value's retry, so only a live failure is parked.\n          const live = controllers.current.get(key) === controller\n          settle()\n          if (live) failed.current.add(value)\n          if (controller.signal.aborted || startEpoch !== epoch.current) return\n          latest.current.onLoadError?.(error, {\n            parent: null,\n            reason: \"resolve\",\n          })\n        })\n    },\n    [abortKey, remember]\n  )\n\n  /* --------------------------------- resets -------------------------------- */\n\n  // Aborts what the PANEL asked for. A resolve keeps running: it feeds the\n  // trigger, which shows with the popup closed.\n  const cancelPanel = React.useCallback(() => {\n    const keys = Array.from(controllers.current.keys()).filter(\n      (key) => !key.startsWith(RESOLVE_PREFIX)\n    )\n    for (const key of keys) {\n      controllers.current.get(key)?.abort()\n      controllers.current.delete(key)\n    }\n    inflight.current.clear()\n\n    const holder = timers.current\n    if (holder.prefetch) {\n      clearTimeout(holder.prefetch)\n      holder.prefetch = null\n    }\n    if (keys.length === 0) return\n\n    setStore((prev) => {\n      let states: Map<string, CascaderLoadState> | null = null\n      for (const key of keys) {\n        const state = prev.states.get(key)\n        if (!state?.loading) continue\n        states = states ?? new Map(prev.states)\n        if ((prev.pages.get(key)?.length ?? 0) > 0) {\n          states.set(key, { ...state, loading: false })\n        } else {\n          // No entry AT ALL: membership is what says \"loaded\", so a stranded\n          // `loading: true` would read as loaded-and-empty forever.\n          states.delete(key)\n        }\n      }\n      return states ? { ...prev, states } : prev\n    })\n    setSearch((prev) => (prev?.loading ? { ...prev, loading: false } : prev))\n  }, [])\n\n  // A `loadKey` change drops everything. Declared BEFORE the level effect, so\n  // a reset always lands before the levels are asked for again.\n  const loadKeyRef = React.useRef<{ key: unknown } | null>(null)\n  React.useEffect(() => {\n    const previous = loadKeyRef.current\n    loadKeyRef.current = { key: loadKey }\n    if (!previous || Object.is(previous.key, loadKey)) return\n\n    epoch.current += 1\n    for (const controller of controllers.current.values()) controller.abort()\n    controllers.current.clear()\n    inflight.current.clear()\n    // `requestIds` is deliberately NOT cleared: the ids are stale-response\n    // guards, not cache. Reusing an aborted predecessor's id would leave only\n    // the epoch bump and the abort to reject the old response; monotonic per\n    // key keeps all three guards independent. The unmount cleanup clears them.\n    moreLatch.current.clear()\n    attempted.current.clear()\n    failed.current.clear()\n    known.current.clear()\n\n    const holder = timers.current\n    if (holder.prefetch) {\n      clearTimeout(holder.prefetch)\n      holder.prefetch = null\n    }\n\n    setStore(createStore)\n    setSearch(null)\n  }, [loadKey])\n\n  // Closing the popup aborts the panel's requests but does NOT drop the cache:\n  // reopening onto an already-loaded level is the point of keeping it.\n  React.useEffect(() => {\n    if (enabled) return\n    if (controllers.current.size === 0) return\n    cancelPanel()\n  }, [enabled, cancelPanel])\n\n  React.useEffect(() => {\n    const active = controllers.current\n    const holder = timers.current\n    const pending = inflight.current\n    const ids = requestIds.current\n    const tried = attempted.current\n    const parked = failed.current\n    return () => {\n      for (const controller of active.values()) controller.abort()\n      active.clear()\n      // Cleared WITH the controllers, for StrictMode's dev remount: a stale\n      // inflight signature would make `runLoad` skip the refetch forever, and\n      // a value still marked as asked would never be resolved again. The\n      // aborted promises are stale-guarded, so emptying the ids is safe.\n      pending.clear()\n      ids.clear()\n      tried.clear()\n      parked.clear()\n      if (holder.prefetch) clearTimeout(holder.prefetch)\n    }\n  }, [])\n\n  /* ------------------------------- the effects ----------------------------- */\n\n  const hasLoader = typeof getChildren === \"function\"\n  const hasSearch = typeof onSearch === \"function\"\n  const hasResolve = typeof resolveValue === \"function\"\n  const trimmed = query.trim()\n\n  // Serialised so the effects key on CONTENT, not on the array identity.\n  const levelsKey = JSON.stringify(levels)\n  const valuesKey = JSON.stringify(values)\n\n  // THE load trigger: one declarative effect on the levels on screen, not a\n  // call from `pushLevel`/`navigate`, which a controlled `path` never touches.\n  React.useEffect(() => {\n    if (!enabled || !hasLoader) return\n    for (const key of levels) ensureLevel(key, \"level\")\n    // `levels` enters through `levelsKey`; `store` re-runs after a load lands.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [enabled, hasLoader, levelsKey, store, ensureLevel])\n\n  React.useEffect(() => {\n    if (!hasSearch) return undefined\n    if (!enabled || !trimmed) {\n      abortKey(SEARCH_KEY)\n      setSearch((prev) => (prev === null ? prev : null))\n      return undefined\n    }\n    const timer = setTimeout(() => runSearch(trimmed), searchDebounce)\n    return () => {\n      clearTimeout(timer)\n      abortKey(SEARCH_KEY)\n    }\n  }, [hasSearch, enabled, trimmed, searchDebounce, abortKey, runSearch])\n\n  // Opening is the retry point for a failed resolve: un-ask it here, and the\n  // values pass below, which also re-runs on `enabled`, asks again.\n  React.useEffect(() => {\n    if (!enabled || failed.current.size === 0) return\n    for (const value of failed.current) attempted.current.delete(value)\n    failed.current.clear()\n  }, [enabled])\n\n  // NOT gated on `enabled`: the trigger shows the selection with the popup\n  // closed, so a saved value is pathed on load, not on the first open.\n  // `loadKey` re-runs it, because the reset dropped every resolved chain.\n  React.useEffect(() => {\n    if (!hasResolve) return\n    for (const value of values) {\n      if (!value) continue\n      if (attempted.current.has(value)) continue\n      if (base.byValue.has(value) || known.current.has(value)) continue\n      attempted.current.add(value)\n      runResolve(value)\n    }\n    // `values` is depended on through `valuesKey`.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [enabled, hasResolve, valuesKey, base, runResolve, loadKey])\n\n  /* -------------------------------- the value ------------------------------ */\n\n  const searchResults = React.useMemo(() => {\n    if (!hasSearch || !trimmed) return null\n    if (!search || search.query !== trimmed) {\n      return NO_RESULTS as CascaderNode<T>[]\n    }\n    return search.results\n  }, [hasSearch, trimmed, search])\n\n  const searchState = React.useMemo<CascaderLoadState | null>(() => {\n    if (!hasSearch || !trimmed) return null\n    const settled = search?.query === trimmed\n    return {\n      loading: !settled || !!search?.loading,\n      error: settled && !!search?.error,\n      hasMore: false,\n    }\n  }, [hasSearch, trimmed, search])\n\n  return React.useMemo(\n    () => ({\n      active: hasLoader,\n      store,\n      states: store.states,\n      searchResults,\n      searchState,\n      ensureLevel,\n      loadMore,\n      retryLevel,\n      retrySearch,\n      invalidateLevel,\n      prefetchNode,\n    }),\n    [\n      hasLoader,\n      store,\n      searchResults,\n      searchState,\n      ensureLevel,\n      loadMore,\n      retryLevel,\n      retrySearch,\n      invalidateLevel,\n      prefetchNode,\n    ]\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Consumers                                 */\n/* -------------------------------------------------------------------------- */\n\n/** One level's load state, `null` when never fetched. Omit for the root. */\nexport function useCascaderLoadState(\n  parent?: string | null\n): CascaderLoadState | null {\n  const { loadStates } = useCascaderState()\n  return loadStates.get(parent ?? CASCADER_ROOT_KEY) ?? null\n}\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-async.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader-columns.tsx",
      "content": "/**\n * cascader-columns - adapted for tealmac UI from ReUI \"cascader/cascader-columns\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\n\"use client\"\n\nimport * as React from \"react\"\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\"\nimport { LoaderCircleIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\nimport {\n  useCascaderActions,\n  useCascaderState,\n} from \"@/components/reui/cascader/cascader-context\"\nimport type { CascaderColumn } from \"@/components/reui/cascader/cascader-context\"\nimport {\n  CascaderItem,\n  getCascaderMoreProps,\n} from \"@/components/reui/cascader/cascader-item\"\nimport {\n  CASCADER_LIST_HEIGHT_CLASS,\n  CASCADER_LIST_PAD_CLASS,\n  CASCADER_ROOT_KEY,\n  CASCADER_ROWS_CLASS,\n  CASCADER_SCROLL_CLASS,\n  focusCascaderHome,\n  warnCascaderOnce,\n} from \"@/components/reui/cascader/cascader-lib\"\n\nexport interface CascaderColumnsProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"children\"\n> {\n  /** Width of each column. */\n  columnWidth?: number | string\n  /** Height CAP per column. Falls back to the root `maxHeight`, then 24rem. */\n  maxHeight?: number | string\n  /** Replaces the default panel; the seam a windowed column plugs into. */\n  children?: (column: CascaderColumn) => React.ReactNode\n}\n\n/**\n * Miller columns: the open trail side by side, one panel per level. Only the\n * DEEPEST column is a real listbox (Base UI owns exactly one list); the trail\n * behind is plain buttons, which keeps one state machine instead of a second,\n * 2D one. `CascaderInput` moves between columns with ArrowLeft/ArrowRight.\n */\nfunction CascaderColumns({\n  className,\n  columnWidth = 220,\n  maxHeight: maxHeightProp,\n  children,\n  ...props\n}: CascaderColumnsProps) {\n  const { maxHeight, mode, labels } = useCascaderActions()\n  const { columns } = useCascaderState()\n\n  // Before the early return, so the hook count is the same in both modes.\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return\n    if (mode === \"columns\") return\n    warnCascaderOnce(\n      `columns-outside-columns-mode:${mode}`,\n      `\\`CascaderColumns\\` renders nothing in \\`mode=\"${mode}\"\\`, so \\`columnWidth\\` and everything else on it does nothing. Set \\`mode=\"columns\"\\` on the root, or render \\`CascaderList\\` instead.`\n    )\n  }, [mode])\n\n  if (mode !== \"columns\") return null\n\n  const height = maxHeightProp ?? maxHeight\n  const toCss = (value: number | string) =>\n    typeof value === \"number\" ? `${value}px` : value\n\n  return (\n    <div\n      data-slot=\"cascader-columns\"\n      role=\"group\"\n      aria-label={labels.columnsLabel}\n      style={\n        {\n          \"--cascader-column-width\": toCss(columnWidth),\n          /* Set only from an EXPLICIT cap: unset means \"24rem or what the\n             viewport leaves\", via the `min()` fallback on the panel. A `?? 280`\n             default here ignored short viewports and wasted tall ones. */\n          ...(height != null\n            ? { \"--cascader-max-height\": toCss(height) }\n            : null),\n        } as React.CSSProperties\n      }\n      className={cn(\n        /* `max-h-full` with `min-h-0`: the trail is the panel's shrinking\n           child, and the columns inside it size against this box. */\n        \"flex max-h-full min-h-0 items-stretch overflow-x-auto overscroll-x-contain\",\n        CASCADER_LIST_PAD_CLASS,\n        className\n      )}\n      {...props}\n    >\n      {columns.map((column) =>\n        children ? (\n          <React.Fragment key={column.depth}>{children(column)}</React.Fragment>\n        ) : (\n          <CascaderColumnPanel key={column.depth} column={column} />\n        )\n      )}\n    </div>\n  )\n}\n\n/**\n * One column's box. `columnWidth` is the width of the LIST, not the box: under\n * `border-box` the 1px inline-start divider on every column but the first would\n * come out of the rows, so bordered columns are widened by that pixel. The box\n * is also the BOUND, the same `min(--available-height, cap)` the single list\n * uses, and the `ScrollArea` inside scrolls, so every column shows a thumb.\n */\nconst PANEL_CLASS = `flex w-(--cascader-column-width) shrink-0 flex-col overscroll-contain not-first:w-[calc(var(--cascader-column-width)_+_1px)] not-first:border-border/60 not-first:border-s ${CASCADER_LIST_HEIGHT_CLASS}`\n\nexport interface CascaderColumnPanelProps {\n  column: CascaderColumn\n  /** Replaces the panel's rows; the empty state still wins on an empty column. */\n  children?: React.ReactNode\n  /** Containing block for the windowed column's absolutely positioned rows. */\n  virtualized?: boolean\n}\n\nfunction CascaderColumnPanel({\n  column,\n  children,\n  virtualized,\n}: CascaderColumnPanelProps) {\n  const {\n    labels,\n    baseId,\n    isBranch,\n    isSelectable,\n    isSelected,\n    isIndeterminate,\n    retryLevel,\n    searchScope,\n  } = useCascaderActions()\n  const { loadStates, deepResults } = useCascaderState()\n\n  // Keyed per level, not one global flag: columns load and land independently.\n  const columnKey = column.parent?.value ?? CASCADER_ROOT_KEY\n  const loadState = loadStates.get(columnKey)\n\n  let emptyBody: React.ReactNode = labels.empty\n  if (loadState?.error) {\n    emptyBody = (\n      <>\n        {labels.error}{\" \"}\n        <button\n          type=\"button\"\n          data-slot=\"cascader-retry\"\n          onClick={(event) => {\n            // Same hand-off as the panel's Retry: a trail column, or an\n            // inline one, can hold focus, and the loading state unmounts this.\n            focusCascaderHome(event.currentTarget)\n            retryLevel(columnKey)\n          }}\n          className=\"text-foreground hover:bg-accent focus-visible:ring-ring/50 rounded-md px-1 font-medium outline-hidden transition-colors focus-visible:ring-2\"\n        >\n          {labels.retry}\n        </button>\n      </>\n    )\n  } else if (loadState?.loading) {\n    emptyBody = (\n      <span className=\"flex items-center gap-1.5\">\n        <LoaderCircleIcon className=\"size-3.5 animate-spin\" aria-hidden />\n        {labels.loading}\n      </span>\n    )\n  }\n\n  const rows =\n    column.items.length === 0 ? (\n      <p\n        data-slot=\"cascader-column-empty\"\n        data-state={\n          loadState?.error ? \"error\" : loadState?.loading ? \"loading\" : \"empty\"\n        }\n        className=\"text-muted-foreground px-2 py-1.5 text-sm\"\n      >\n        {emptyBody}\n      </p>\n    ) : (\n      (children ??\n      column.items.map((node, i) => {\n        const open = node.value === column.activeValue\n        return (\n          <CascaderItem\n            key={node.value}\n            node={node}\n            /* A trail row must not compete for `aria-activedescendant`. */\n            as={column.active ? \"option\" : \"button\"}\n            depth={column.depth}\n            /* Answered here, not in the row: the trail rows are memoised too. */\n            branch={isBranch(node)}\n            selectable={isSelectable(node)}\n            selected={isSelected(node)}\n            indeterminate={isIndeterminate(node)}\n            {...getCascaderMoreProps(node, loadStates)}\n            data-open={open || undefined}\n            className={open ? \"bg-accent/60 text-accent-foreground\" : undefined}\n            /* Set metadata is option-only; a trail row is a `role=\"button\"`. */\n            {...(column.active\n              ? {\n                  \"aria-setsize\": column.items.length,\n                  \"aria-posinset\": i + 1,\n                }\n              : null)}\n            {...(!column.active && open\n              ? {\n                  \"aria-expanded\": true,\n                  \"aria-controls\": `${baseId}-column-${column.depth + 1}`,\n                }\n              : null)}\n          />\n        )\n      }))\n    )\n\n  const shared = {\n    \"data-slot\": \"cascader-column\",\n    \"data-active\": column.active || undefined,\n    \"data-depth\": column.depth,\n    // Addressable so the opening trail row can point `aria-controls` here, and\n    // named even at the root, which has no parent label to borrow.\n    id: `${baseId}-column-${column.depth}`,\n    // Global hits come from the whole tree, so the parent would understate them.\n    \"aria-label\":\n      column.active && searchScope === \"global\" && deepResults\n        ? labels.rootLevel\n        : (column.parent?.label ?? labels.rootLevel),\n    // Conditional spread, never an explicit `undefined`: the active column is a\n    // Base UI element, and its `mergeProps` iterates own keys.\n    ...(virtualized ? { \"data-virtualized\": true } : null),\n  }\n\n  // A windowed row is absolutely positioned, so the ROWS' box is the containing\n  // block, not the scrollport: it carries the padding the geometry is measured\n  // against.\n  const rowsClass = cn(CASCADER_ROWS_CLASS, virtualized && \"relative\")\n\n  // The active column IS the Combobox list: only rows inside `Combobox.List`\n  // reach the CompositeList, arrow-key navigation and `aria-activedescendant`.\n  const body = column.active ? (\n    <ComboboxPrimitive.List {...shared} className={rowsClass}>\n      {rows}\n    </ComboboxPrimitive.List>\n  ) : (\n    // A named `group`, not a second listbox competing with the active column.\n    <div {...shared} role=\"group\" className={rowsClass}>\n      {rows}\n    </div>\n  )\n\n  return (\n    <div\n      data-slot=\"cascader-column-bounds\"\n      /* Repeated from `shared`: this box, not the semantic element, owns the\n         width, the divider and the height, so style hooks must reach it, and\n         `:first-child` on the semantic element no longer means \"first column\"\n         (it is its own scrollport's only child). */\n      data-active={column.active || undefined}\n      data-depth={column.depth}\n      className={PANEL_CLASS}\n    >\n      <ScrollArea className={CASCADER_SCROLL_CLASS}>{body}</ScrollArea>\n    </div>\n  )\n}\n\nexport { CascaderColumnPanel, CascaderColumns }\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-columns.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader-context.tsx",
      "content": "/**\n * cascader-context - adapted for tealmac UI from ReUI \"cascader/cascader-context\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\n\"use client\"\n\nimport * as React from \"react\"\n\nimport type {\n  CascaderActionItem,\n  CascaderChangeReason,\n  CascaderFlatNode,\n  CascaderIndex,\n  CascaderLabels,\n  CascaderLoadState,\n  CascaderMode,\n  CascaderNode,\n  CascaderSearchScope,\n} from \"@/components/reui/cascader/cascader-types\"\n\n/**\n * Four contexts, not one: actions (config and callbacks, near-stable), state\n * (every keystroke), render (`renderItem` identity) and highlight (every arrow\n * key and pointer move). One combined context re-rendered every row on every\n * keystroke, which is what made `React.memo` on the row worth nothing.\n */\n\n/* -------------------------------------------------------------------------- */\n/*                                Shared types                                */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderColumn<T = unknown> {\n  parent: CascaderNode<T> | null\n  items: CascaderNode<T>[]\n  depth: number\n  /** The node in this column that is drilled into, if any. */\n  activeValue: string | null\n  /** Whether this is the deepest column, the one Base UI owns. */\n  active: boolean\n}\n\nexport interface CascaderItemState<T = unknown> {\n  branch: boolean\n  selected: boolean\n  /** The node's own flag or a disabled ancestor's, as `isCascaderDisabled`. */\n  disabled: boolean\n  depth: number\n  count: number\n  /** Ancestor chain, root first. Populated for deep and global search rows. */\n  path: CascaderNode<T>[]\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    State                                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Everything derived from the query, the path and the selection. One keystroke\n * rebuilds most of it, so never subscribe to it from a row.\n */\nexport interface CascaderStateContextValue<T = unknown> {\n  /** Same `useMemo` identity as the actions context's `index`. */\n  index: CascaderIndex<T>\n  path: string[]\n  expanded: ReadonlySet<string>\n  query: string\n  currentParent: CascaderNode<T> | null\n  /** Rows for the current level, already filtered. */\n  levelItems: CascaderNode<T>[]\n  deepResults: CascaderNode<T>[] | null\n  /** What `Combobox.Root` is currently rendering, in render order. */\n  renderedItems: CascaderNode<T>[]\n  columns: CascaderColumn<T>[]\n  treeRows: CascaderFlatNode<T>[]\n  selectedValues: string[]\n  /** Selected nodes below each value, at any depth. Absent means zero. */\n  selectedDescendants: ReadonlyMap<string, number>\n  /**\n   * Per level, keyed by parent value or `CASCADER_ROOT_KEY`. MEMBERSHIP is the\n   * discriminator: no entry means never fetched, an entry with no `loading`, no\n   * `error` and no children means fetched and genuinely empty.\n   */\n  loadStates: ReadonlyMap<string, CascaderLoadState>\n  /** Async search, `null` when idle. Separate: a search belongs to no level. */\n  searchState: CascaderLoadState | null\n  announcement: string\n}\n\nconst CascaderStateContext = React.createContext<\n  CascaderStateContextValue | undefined\n>(undefined)\n\n/**\n * One provider serves every `T`, so the context holds an erased `unknown` value\n * and this cast restores it. The primitive never inspects the payload.\n */\nexport function useCascaderState<T = unknown>(): CascaderStateContextValue<T> {\n  const context = React.useContext(CascaderStateContext)\n  if (!context) {\n    throw new Error(\"useCascaderState must be used within a Cascader\")\n  }\n  return context as unknown as CascaderStateContextValue<T>\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Actions                                  */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Config and callbacks, slow enough that a memoised row can subscribe: the\n * mutators are `[]`-dep callbacks over a latest-props ref. The predicates are\n * the exception, read DURING RENDER where a ref written in an effect would\n * return the previous commit's answer, so each is memoised on its own input.\n */\nexport interface CascaderActionsContextValue<T = unknown> {\n  index: CascaderIndex<T>\n  mode: CascaderMode\n  multiple: boolean\n  /** Multi-select only: a commit propagates over the LOADED subtree. */\n  cascade: boolean\n  /** Whether a BRANCH is committable. Per-LIST: the check gutter is a COLUMN. */\n  branchesSelectable: boolean\n  /** Draws the SINGLE-SELECT check and its gutter. Ignored in multi-select. */\n  indicator: boolean\n  expandTrigger?: \"click\" | \"hover\"\n  actions: CascaderActionItem[]\n  searchScope: CascaderSearchScope\n  maxHeight?: number | string\n  inline: boolean\n  invalid: boolean\n  /**\n   * Id prefix; columns are `${baseId}-column-${depth}`. The SCHEME is contract:\n   * the filters primitive's `FilterMenuPinKeeper` restores its highlight across\n   * a live re-pin through `${baseId}-column-0` and the `cascader-item` slot.\n   */\n  baseId: string\n  labels: CascaderLabels\n\n  /**\n   * Whether rows are WINDOWED. Also handed to `Combobox.Root`, which is what\n   * makes an explicit row `index` legal: forwarding one while this is false\n   * makes `aria-activedescendant` resolve to nothing. Latched per level.\n   */\n  virtualized: boolean\n  /** Mounts a windowing renderer, returns its unregister. LAYOUT effect only. */\n  registerVirtualRenderer: () => () => void\n  /** The root's `virtualize` prop. `undefined` means \"decide by count\". */\n  virtualize?: boolean\n  virtualizeThreshold: number\n  estimateRowSize: number\n  overscan: number\n\n  /** Tells \"this level is empty\" from \"not fetched yet\" before a load state. */\n  hasLoader: boolean\n  /** Next page of a level. Latched: a page with nothing new is not re-asked. */\n  loadMore: (parentKey: string) => void\n  retryLevel: (parentKey: string) => void\n  /** Re-runs a failed server search (`onSearch`) for the current query. */\n  retrySearch: () => void\n  /**\n   * Evicts one level's async cache, `null` for the root, so membership reads\n   * never-loaded. A level that is on screen when evicted refetches at once.\n   */\n  invalidateLevel: (value: string | null) => void\n\n  /** Index as of the last commit. Use in the stable callbacks, not `index`. */\n  getIndex: () => CascaderIndex<T>\n  getState: () => CascaderStateContextValue<T>\n  /** Highlighted row or null. A getter: the highlight moves per arrow key. */\n  getHighlighted: () => CascaderNode<T> | null\n\n  setPath: (next: string[] | ((prev: string[]) => string[])) => void\n  pushLevel: (value: string) => void\n  popLevel: () => void\n  goToDepth: (depth: number) => void\n  toggleExpanded: (value: string) => void\n  /**\n   * Registers a footer submenu as open or closed. `Combobox` has no\n   * `FloatingTree`, so one Escape would dismiss the flyout AND the cascader;\n   * the root's `onOpenChange` guard cancels the close while any is open, from a\n   * ref so it reads as of that event without re-rendering the root.\n   */\n  setFlyoutOpen: (key: string, open: boolean) => void\n  hasOpenFlyout: () => boolean\n  setQuery: (next: string) => void\n  /**\n   * Replaces the selection. `onValueChange` diffs it against the current one\n   * for its node and reason; pass `reason` only when the caller knows better.\n   */\n  setSelection: (values: string[], reason?: CascaderChangeReason) => void\n  /** Commits a node, for rows outside the listbox such as ancestor columns. */\n  commit: (node: CascaderNode<T>) => void\n  navigate: (node: CascaderNode<T>) => void\n  /** Into `node` as a child of `depth`, replacing anything deeper. */\n  navigateAt: (node: CascaderNode<T>, depth: number) => void\n  /** Never undefined: falls back to a remembered label, then a synthetic node. */\n  resolveNode: (value: string) => CascaderNode<T>\n  isBranch: (node: CascaderNode<T>) => boolean\n  isSelectable: (node: CascaderNode<T>) => boolean\n  /**\n   * The node or an ancestor is `disabled`. Rows render it disabled; row\n   * presses, keys, `commit`, `navigate`, `navigateAt` and a held navigation\n   * refuse it. `pushLevel`, `setPath`, `toggleExpanded` and `setSelection` are\n   * raw setters and do not ask.\n   */\n  isDisabled: (node: CascaderNode<T>) => boolean\n  isSelected: (node: CascaderNode<T>) => boolean\n  /** Always `false` without `cascade`: partial selection needs propagation. */\n  isIndeterminate: (node: CascaderNode<T>) => boolean\n  /**\n   * O(1) read of `selectedDescendants`; a memoised row may not subscribe.\n   * Unlike `isIndeterminate` this answers in every mode.\n   */\n  selectedDescendantCount: (node: CascaderNode<T>) => number\n}\n\nconst CascaderActionsContext = React.createContext<\n  CascaderActionsContextValue | undefined\n>(undefined)\n\nexport function useCascaderActions<\n  T = unknown,\n>(): CascaderActionsContextValue<T> {\n  const context = React.useContext(CascaderActionsContext)\n  if (!context) {\n    throw new Error(\"useCascaderActions must be used within a Cascader\")\n  }\n  return context as unknown as CascaderActionsContextValue<T>\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                Render props                                */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Their own context, republished every render. They cannot ride on the actions\n * context: an inline closure read off a memoised object is whichever closure\n * that object captured, so the row would call a stale prop over stale state.\n */\nexport interface CascaderRenderContextValue<T = unknown> {\n  renderItem?: (\n    node: CascaderNode<T>,\n    state: CascaderItemState<T>\n  ) => React.ReactNode\n  renderLabel?: (\n    node: CascaderNode<T>,\n    state: CascaderItemState<T>\n  ) => React.ReactNode\n}\n\nconst CascaderRenderContext = React.createContext<CascaderRenderContextValue>(\n  {}\n)\n\nexport function useCascaderRender<\n  T = unknown,\n>(): CascaderRenderContextValue<T> {\n  return React.useContext(\n    CascaderRenderContext\n  ) as CascaderRenderContextValue<T>\n}\n\n/* -------------------------------------------------------------------------- */\n/*                               Highlight store                              */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderHighlight {\n  index: number\n  value: string | null\n}\n\n/**\n * An external store, deliberately NOT React state: `onItemHighlighted` fires on\n * every arrow key AND every pointer move over the list, so `setState` would\n * re-render the whole root at mousemove rate. A store re-renders subscribers\n * only, which inside the primitive is the virtualizer.\n */\nexport interface CascaderHighlightStore {\n  subscribe: (onStoreChange: () => void) => () => void\n  getSnapshot: () => CascaderHighlight\n  set: (next: CascaderHighlight) => void\n}\n\nconst NO_HIGHLIGHT: CascaderHighlight = { index: -1, value: null }\n\nexport function createCascaderHighlightStore(): CascaderHighlightStore {\n  let snapshot: CascaderHighlight = NO_HIGHLIGHT\n  const listeners = new Set<() => void>()\n\n  return {\n    subscribe(onStoreChange) {\n      listeners.add(onStoreChange)\n      return () => {\n        listeners.delete(onStoreChange)\n      }\n    },\n    // The SAME object until something changes; `useSyncExternalStore` needs it.\n    getSnapshot() {\n      return snapshot\n    },\n    set(next) {\n      if (next.index === snapshot.index && next.value === snapshot.value) return\n      snapshot = next\n      for (const listener of listeners) listener()\n    },\n  }\n}\n\n/** Shared and permanently empty, so the hook degrades outside a `Cascader`. */\nconst FALLBACK_HIGHLIGHT_STORE = createCascaderHighlightStore()\n\nconst CascaderHighlightContext = React.createContext<CascaderHighlightStore>(\n  FALLBACK_HIGHLIGHT_STORE\n)\n\n/** Subscribes to the highlight. Re-renders ONLY the calling component. */\nexport function useCascaderHighlight(): CascaderHighlight {\n  const store = React.useContext(CascaderHighlightContext)\n  return React.useSyncExternalStore(\n    store.subscribe,\n    store.getSnapshot,\n    store.getSnapshot\n  )\n}\n\nexport {\n  CascaderActionsContext,\n  CascaderHighlightContext,\n  CascaderRenderContext,\n  CascaderStateContext,\n}\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-context.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader-footer.tsx",
      "content": "/**\n * cascader-footer - adapted for tealmac UI from ReUI \"cascader/cascader-footer\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\n/* eslint-disable jsx-a11y/no-noninteractive-element-interactions -- keyboard handling is on the focusable parent (arrow keys, Enter, Escape); the flagged elements are pointer-only affordances or key-event forwarders, as in the ReUI original. */\n\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive } from \"@base-ui/react\"\nimport { useDirection } from \"@base-ui/react/direction-provider\"\nimport { ChevronRightIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { useCascaderActions } from \"@/components/reui/cascader/cascader-context\"\nimport {\n  CASCADER_ACTION_CLASS,\n  CascaderGroup,\n  CascaderLabel,\n} from \"@/components/reui/cascader/cascader-item\"\nimport {\n  CASCADER_LIST_PAD_CLASS,\n  getCascaderFooterStops,\n  isCascaderRtl,\n} from \"@/components/reui/cascader/cascader-lib\"\nimport type { CascaderActionItem } from \"@/components/reui/cascader/cascader-types\"\n\n/**\n * The pinned footer, and the side-anchored flyout a footer row can open. These\n * are COMMANDS: nothing here joins the selection, the filter set or the\n * highlight. The flyout is a Base UI `Popover` rendered as a REACT CHILD of\n * `Combobox.Popup` with its OWN `Portal` and NO `container`: a nested portal\n * resolves to the parent portal node, so it is a DOM sibling of the combobox\n * popup (not clipped, not `aria-hidden`) but a React descendant, which is what\n * the outside-press and focus-out whitelists read. `Combobox` builds no\n * `FloatingTree`, so the flyout is not consulted first and one Escape would\n * dismiss both; hence `CascaderSubmenu` registering with the root to turn one\n * Escape into two. `Combobox.List` clicks its highlighted row on Enter, hence\n * the footer sitting outside `CascaderList`. And a `Positioner` throws without\n * its `Portal`, while `modal` stays `false` on the `Root` so the combobox\n * keeps its own dismissal behaviour.\n */\n\n/* -------------------------------------------------------------------------- */\n/*                                   Footer                                   */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Keys the option list acts on, swallowed at the footer boundary. Escape and\n * Tab are absent on purpose: Escape must reach the root, Tab must keep moving.\n */\nconst FOOTER_SWALLOWED_KEYS = new Set([\n  \"Enter\",\n  \" \",\n  \"ArrowUp\",\n  \"ArrowDown\",\n  \"Home\",\n  \"End\",\n  \"PageUp\",\n  \"PageDown\",\n])\n\nexport type CascaderFooterProps = React.ComponentProps<\"div\">\n\n/**\n * Actions pinned below the list, a SIBLING of `CascaderList`. Children win\n * over the root's `actions` prop; with neither it renders nothing.\n */\nfunction CascaderFooter({\n  className,\n  children,\n  onKeyDown,\n  ...props\n}: CascaderFooterProps) {\n  const { actions, labels } = useCascaderActions()\n  const hasChildren = React.Children.count(children) > 0\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      onKeyDown?.(event)\n      if (event.defaultPrevented) return\n      if (!FOOTER_SWALLOWED_KEYS.has(event.key)) return\n      event.stopPropagation()\n\n      // The strip's own vertical movement, and the way back from the list's\n      // hand-off: either end returns focus to the search field, from which\n      // Base UI's empty highlight resumes the list. Down wraps to the FIELD,\n      // not a command (traps the arrows) or a row (no imperative highlight).\n      if (event.key !== \"ArrowDown\" && event.key !== \"ArrowUp\") return\n      const footer = event.currentTarget\n      const stops = getCascaderFooterStops(footer)\n      const active = document.activeElement as HTMLElement | null\n      const index = active ? stops.indexOf(active) : -1\n      if (index === -1) return\n      event.preventDefault()\n      const next =\n        event.key === \"ArrowDown\" ? stops[index + 1] : stops[index - 1]\n      if (next) {\n        next.focus()\n        return\n      }\n      if (event.key === \"ArrowUp\" && index > 0) return\n      footer\n        .closest<HTMLElement>('[data-slot=\"cascader-panel\"]')\n        ?.querySelector<HTMLElement>('[data-slot=\"cascader-input\"]')\n        ?.focus()\n    },\n    [onKeyDown]\n  )\n\n  if (!hasChildren && actions.length === 0) return null\n\n  return (\n    <div\n      data-slot=\"cascader-footer\"\n      /* Named, not a bare div: without it a screen reader reaches the actions\n         with nothing to say they are not more options. */\n      role=\"group\"\n      aria-label={labels.actionsLabel}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"border-border/60 flex shrink-0 flex-col gap-0.5 border-t\",\n        /* The LIST's padding, not a flat `p-1`: with a padding of its own the\n           two columns of text were 2px out in luma and sera and 4px out in\n           lyra. It also gives a separator in here a number to cancel. */\n        CASCADER_LIST_PAD_CLASS,\n        \"p-(--cascader-list-pad,4px)\",\n        className\n      )}\n      {...props}\n    >\n      {hasChildren ? children : <CascaderFooterActions actions={actions} />}\n    </div>\n  )\n}\n\nfunction CascaderFooterActions({ actions }: { actions: CascaderActionItem[] }) {\n  return (\n    <>\n      {actions.map((action, i) =>\n        action.items?.length ? (\n          <CascaderSubmenu key={actionKey(action, i)}>\n            <CascaderSubmenuTrigger\n              icon={action.icon}\n              disabled={action.disabled}\n            >\n              {action.label}\n            </CascaderSubmenuTrigger>\n            <CascaderSubmenuContent>\n              <CascaderActionList items={action.items} />\n            </CascaderSubmenuContent>\n          </CascaderSubmenu>\n        ) : (\n          <CascaderAction\n            key={actionKey(action, i)}\n            icon={action.icon}\n            disabled={action.disabled}\n            onSelect={action.onSelect}\n          >\n            {action.label}\n          </CascaderAction>\n        )\n      )}\n    </>\n  )\n}\n\nfunction actionKey(action: CascaderActionItem, index: number): string {\n  if (action.value != null) return action.value\n  if (typeof action.label === \"string\") return action.label\n  return String(index)\n}\n\n/**\n * Consecutive entries sharing a `group`, as runs not buckets: two separated\n * runs with the same name stay two, so the author's order survives.\n */\nfunction groupActionRuns(\n  items: CascaderActionItem[]\n): { group?: string; items: CascaderActionItem[] }[] {\n  const runs: { group?: string; items: CascaderActionItem[] }[] = []\n  for (const item of items) {\n    const last = runs[runs.length - 1]\n    if (last && last.group === item.group) last.items.push(item)\n    else runs.push({ group: item.group, items: [item] })\n  }\n  return runs\n}\n\n/**\n * Flyout body for a data-driven submenu. A named run becomes a real\n * `CascaderGroup`; unnamed runs stay unwrapped, as an unnamed group is noise.\n */\nfunction CascaderActionList({ items }: { items: CascaderActionItem[] }) {\n  const { close } = useCascaderSubmenu()\n  const runs = React.useMemo(() => groupActionRuns(items), [items])\n\n  const renderAction = (item: CascaderActionItem, i: number) => (\n    <CascaderAction\n      key={actionKey(item, i)}\n      icon={item.icon}\n      disabled={item.disabled}\n      onSelect={() => {\n        item.onSelect?.()\n        /* Closes behind the command, or the entries would read as toggles. */\n        close()\n      }}\n    >\n      {item.label}\n    </CascaderAction>\n  )\n\n  return (\n    <>\n      {runs.map((run, runIndex) =>\n        run.group ? (\n          <CascaderGroup key={`${run.group}-${runIndex}`} className=\"gap-0.5\">\n            <CascaderLabel>{run.group}</CascaderLabel>\n            {run.items.map(renderAction)}\n          </CascaderGroup>\n        ) : (\n          <React.Fragment key={`run-${runIndex}`}>\n            {run.items.map(renderAction)}\n          </React.Fragment>\n        )\n      )}\n    </>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Action                                   */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderActionProps extends Omit<\n  React.ComponentProps<\"button\">,\n  \"onSelect\"\n> {\n  icon?: React.ReactNode\n  /** Fires on press, after `onClick`, and not at all when disabled. */\n  onSelect?: () => void\n}\n\n/**\n * The cascader popup's panel per style, spelled with `style-<name>:` variants\n * rather than ReUI theme CSS so an installed footer needs only Tailwind.\n */\nconst FLYOUT_SURFACE_CLASS =\n  \"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-72 overflow-hidden ring-1 duration-100 style-vega:ring-foreground/10 style-vega:rounded-md style-vega:shadow-md style-nova:ring-foreground/10 style-nova:shadow-md style-nova:rounded-lg style-maia:ring-foreground/5 style-maia:rounded-2xl style-maia:shadow-2xl style-lyra:ring-foreground/10 style-lyra:shadow-md style-lyra:rounded-none style-mira:ring-foreground/10 style-mira:shadow-md style-mira:rounded-lg style-mira:dark:bg-popover style-luma:ring-foreground/5 style-luma:dark:ring-foreground/10 style-luma:rounded-3xl style-luma:shadow-lg style-rhea:ring-foreground/5 style-rhea:rounded-2xl style-rhea:dark:ring-foreground/10 style-rhea:shadow-lg style-sera:ring-foreground/10 style-sera:shadow-md style-sera:rounded-none\"\n\n/**\n * One footer command, shaped like a row and deliberately NOT one. A real\n * `<button>`, never a `Combobox.Item`: an item would join the arrow-key ring,\n * appear in `filteredItems`, and vanish the moment a query matched nothing -\n * exactly when \"Create new attribute\" is most useful. Disabled is\n * `aria-disabled`, never the native attribute: that one is not a tab stop and\n * the panel's Tab order reads `button:not([disabled])`, so a footer whose ONLY\n * row is a disabled command had no stop after the search field (measured on\n * `c-cascader-8`). Staying focusable costs the guards below, plus the\n * greyed-out look: `CASCADER_ACTION_CLASS` keys that off `aria-disabled`\n * instead of `:disabled`.\n */\nfunction CascaderAction({\n  className,\n  icon,\n  children,\n  onSelect,\n  onClick,\n  onKeyDown,\n  disabled,\n  ...props\n}: CascaderActionProps) {\n  // Set by `CascaderSubmenuContent`: a plain button in the footer's Tab ring,\n  // a roving-focus `menuitem` inside a flyout, never both.\n  const inMenu = React.useContext(CascaderMenuContext)\n\n  const handleClick = React.useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      // Before the consumer's handler: a disabled command runs none of them.\n      if (disabled) {\n        event.preventDefault()\n        return\n      }\n      onClick?.(event)\n      if (event.defaultPrevented) return\n      onSelect?.()\n    },\n    [disabled, onClick, onSelect]\n  )\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      if (disabled) {\n        // The two keys a `<button>` activates on, and only those.\n        if (event.key === \"Enter\" || event.key === \" \") event.preventDefault()\n        return\n      }\n      onKeyDown?.(event)\n    },\n    [disabled, onKeyDown]\n  )\n\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"cascader-action\"\n      /* Conditional spread: `false` would publish `aria-disabled=\"false\"`. */\n      {...(disabled ? { \"aria-disabled\": true, \"data-disabled\": \"\" } : null)}\n      onClick={handleClick}\n      onKeyDown={handleKeyDown}\n      /* Conditional spread, so a consumer's own `role` or `tabIndex` wins. */\n      {...(inMenu ? { role: \"menuitem\" as const, tabIndex: -1 } : null)}\n      className={cn(CASCADER_ACTION_CLASS, className)}\n      {...props}\n    >\n      {icon ? (\n        <span\n          data-slot=\"cascader-action-icon\"\n          className=\"text-muted-foreground flex shrink-0 items-center justify-center\"\n        >\n          {icon}\n        </span>\n      ) : null}\n      <span className=\"min-w-0 flex-1 truncate text-start\">{children}</span>\n    </button>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Submenu                                   */\n/* -------------------------------------------------------------------------- */\n\ninterface CascaderSubmenuContextValue {\n  rowRef: React.RefObject<HTMLButtonElement | null>\n  open: boolean\n  setOpen: (open: boolean) => void\n  close: () => void\n  /** Names the flyout: a menu is labelled by the control that opens it. */\n  triggerId: string\n  /**\n   * Whether the pending open came from the KEYBOARD. Base UI's own `openType`\n   * calls the opening arrow a POINTER open, because the trigger intercepts it\n   * and calls `setOpen` (measured: focus landed on the popup, not the first\n   * entry). A ref, so reading it in the focus phase cannot render.\n   */\n  keyboardRef: React.RefObject<boolean>\n}\n\n/** Marks the subtree INSIDE a flyout. See `inMenu` in `CascaderAction`. */\nconst CascaderMenuContext = React.createContext(false)\n\n/**\n * Every entry a menu's roving focus may land on, in DOM order, read from the\n * DOM because the entries are whatever the consumer composed. A disabled\n * `CascaderAction` is INCLUDED: it carries `aria-disabled`, not the native\n * attribute, so `:not([disabled])` excludes only a consumer's own natively\n * disabled `menuitem`, which cannot take focus.\n */\nfunction menuItems(popup: HTMLElement | null): HTMLElement[] {\n  if (!popup) return []\n  return Array.from(\n    popup.querySelectorAll<HTMLElement>('[role=\"menuitem\"]:not([disabled])')\n  )\n}\n\nconst CascaderSubmenuContext = React.createContext<\n  CascaderSubmenuContextValue | undefined\n>(undefined)\n\n/** The flyout's own state. `close()` is the one a custom entry usually wants. */\nexport function useCascaderSubmenu(): CascaderSubmenuContextValue {\n  const context = React.useContext(CascaderSubmenuContext)\n  if (!context) {\n    throw new Error(\"useCascaderSubmenu must be used within a CascaderSubmenu\")\n  }\n  return context\n}\n\nexport interface CascaderSubmenuProps {\n  open?: boolean\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  children?: React.ReactNode\n}\n\n/**\n * A footer row plus the flyout it opens. Registers with the cascader root\n * while open, which turns one Escape into two. Cleared in an EFFECT, so the\n * flyout still reads as open during the event that closed it.\n */\nfunction CascaderSubmenu({\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  children,\n}: CascaderSubmenuProps) {\n  const { setFlyoutOpen } = useCascaderActions()\n  const key = React.useId()\n  const triggerId = React.useId()\n  const rowRef = React.useRef<HTMLButtonElement | null>(null)\n  const keyboardRef = React.useRef(false)\n  const [uncontrolled, setUncontrolled] = React.useState(defaultOpen)\n  const open = openProp ?? uncontrolled\n\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (openProp == null) setUncontrolled(next)\n      onOpenChange?.(next)\n    },\n    [openProp, onOpenChange]\n  )\n\n  const close = React.useCallback(() => setOpen(false), [setOpen])\n\n  React.useEffect(() => {\n    setFlyoutOpen(key, open)\n    return () => setFlyoutOpen(key, false)\n  }, [setFlyoutOpen, key, open])\n\n  const context = React.useMemo<CascaderSubmenuContextValue>(\n    () => ({ rowRef, open, setOpen, close, triggerId, keyboardRef }),\n    [open, setOpen, close, triggerId]\n  )\n\n  return (\n    <CascaderSubmenuContext.Provider value={context}>\n      <PopoverPrimitive.Root\n        open={open}\n        onOpenChange={setOpen}\n        /* NEVER modal: it would disable the combobox that owns it. */\n        modal={false}\n      >\n        {children}\n      </PopoverPrimitive.Root>\n    </CascaderSubmenuContext.Provider>\n  )\n}\n\nexport interface CascaderSubmenuTriggerProps extends Omit<\n  React.ComponentProps<\"button\">,\n  \"onSelect\"\n> {\n  icon?: React.ReactNode\n}\n\n/** Carries the handler-veto hook, derived so a Base UI bump cannot drift. */\ntype CascaderSubmenuTriggerClickEvent = Parameters<\n  NonNullable<PopoverPrimitive.Trigger.Props[\"onClick\"]>\n>[0]\n\n/**\n * The footer row that opens the flyout, and its anchor. `aria-haspopup=\"menu\"`\n * rather than the `dialog` Base UI would announce: what opens is a list of\n * commands with roving focus. `disabled` is intercepted, not forwarded:\n * `Popover.Trigger` runs it through `useButton`, which writes the NATIVE\n * attribute for a native `<button>` and takes the row out of the panel's Tab\n * ring, the defect `CascaderAction` documents, and this component exposes no\n * `focusableWhenDisabled` to opt out. Published as `aria-disabled`, with the\n * arrow keys, Enter and Space, and the click closed by hand (`useClick`\n * ignores `defaultPrevented`).\n */\nfunction CascaderSubmenuTrigger({\n  className,\n  icon,\n  children,\n  onKeyDown,\n  onClick,\n  disabled,\n  ...props\n}: CascaderSubmenuTriggerProps) {\n  const { labels } = useCascaderActions()\n  const { rowRef, setOpen, triggerId, keyboardRef } = useCascaderSubmenu()\n  const direction = useDirection()\n\n  const handleClick = React.useCallback(\n    (event: CascaderSubmenuTriggerClickEvent) => {\n      if (disabled) {\n        // `mergeProps` runs right to left, so this drops Base UI's own handler.\n        event.preventDefault()\n        event.preventBaseUIHandler()\n        return\n      }\n      onClick?.(event)\n    },\n    [disabled, onClick]\n  )\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      if (disabled) {\n        if (event.key === \"Enter\" || event.key === \" \") event.preventDefault()\n        return\n      }\n\n      onKeyDown?.(event)\n      if (event.defaultPrevented) return\n\n      // The opening key points AT the flyout, so it flips with the writing\n      // direction; ArrowDown stays \"next command\". Resolved per keydown, never\n      // `useDirection()` alone: with no provider that answers \"ltr\" in RTL.\n      const openKey = isCascaderRtl(event.currentTarget, direction)\n        ? \"ArrowLeft\"\n        : \"ArrowRight\"\n\n      // Enter and Space open through the button's own click, so only FLAGGED.\n      if (event.key === \"Enter\" || event.key === \" \") {\n        keyboardRef.current = true\n        return\n      }\n\n      if (event.key !== openKey) return\n\n      event.preventDefault()\n      keyboardRef.current = true\n      setOpen(true)\n    },\n    [disabled, onKeyDown, direction, setOpen, keyboardRef]\n  )\n\n  return (\n    <PopoverPrimitive.Trigger\n      ref={rowRef}\n      id={triggerId}\n      data-slot=\"cascader-submenu-trigger\"\n      aria-haspopup=\"menu\"\n      /* NOT `disabled={disabled}`: Base UI writes the native attribute. */\n      {...(disabled ? { \"aria-disabled\": true, \"data-disabled\": \"\" } : null)}\n      onClick={handleClick}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        CASCADER_ACTION_CLASS,\n        /* Painted while the flyout is open, as shadcn paints a SubTrigger.\n           Keyed off `aria-expanded`, not shadcn's `data-[state=open]`: the\n           popover trigger carries `aria-expanded` in both states. */\n        \"aria-expanded:bg-accent aria-expanded:text-accent-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {icon ? (\n        <span\n          data-slot=\"cascader-action-icon\"\n          className=\"text-muted-foreground flex shrink-0 items-center justify-center\"\n        >\n          {icon}\n        </span>\n      ) : null}\n      <span className=\"min-w-0 flex-1 truncate text-start\">{children}</span>\n      {/* Nothing else says the row opens a MENU, not another tree level. */}\n      <span className=\"sr-only\">, {labels.submenuAffordance}</span>\n      <ChevronRightIcon\n        aria-hidden=\"true\"\n        /* Static: a rotating chevron is the DISCLOSURE convention, and this\n           one opens to the SIDE and keeps pointing there the whole time. */\n        className=\"text-muted-foreground -me-0.5 size-4 shrink-0 rtl:-scale-x-100\"\n      />\n    </PopoverPrimitive.Trigger>\n  )\n}\n\nexport interface CascaderSubmenuContentProps\n  extends\n    PopoverPrimitive.Popup.Props,\n    Pick<\n      PopoverPrimitive.Positioner.Props,\n      \"side\" | \"align\" | \"sideOffset\" | \"alignOffset\"\n    > {}\n\n/** Carries Base UI's handler-veto hook, so a plain React event will not do. */\ntype CascaderSubmenuKeyEvent = Parameters<\n  NonNullable<PopoverPrimitive.Popup.Props[\"onKeyDown\"]>\n>[0]\n\n/**\n * The flyout itself. `Portal` with NO `container`: a nested portal resolves to\n * the parent portal node, the mechanism the file header describes.\n */\nfunction CascaderSubmenuContent({\n  className,\n  children,\n  onKeyDown,\n  side = \"inline-end\",\n  align = \"end\",\n  sideOffset = 8,\n  alignOffset = 0,\n  ...props\n}: CascaderSubmenuContentProps) {\n  const { rowRef, close, triggerId, keyboardRef } = useCascaderSubmenu()\n  const direction = useDirection()\n  const popupRef = React.useRef<HTMLDivElement | null>(null)\n  const typeaheadRef = React.useRef({ buffer: \"\", at: 0 })\n\n  /**\n   * Keyboard opens land on the first entry; a pointer open falls through to\n   * Base UI's default, the popup, so a click paints no focus ring. Not an\n   * effect of our own: Base UI's focus manager runs on open and wins the race.\n   */\n  const initialFocus = React.useCallback(() => {\n    const byKeyboard = keyboardRef.current\n    keyboardRef.current = false\n    if (!byKeyboard) return true\n    return menuItems(popupRef.current)[0] ?? true\n  }, [keyboardRef])\n\n  const closeAndReturn = React.useCallback(() => {\n    close()\n    rowRef.current?.focus()\n  }, [close, rowRef])\n\n  const handleKeyDown = React.useCallback(\n    (event: CascaderSubmenuKeyEvent) => {\n      onKeyDown?.(event)\n      // A DOM sibling of the combobox popup but a REACT descendant, so keys in\n      // here reach its handlers unless stopped. Enter would commit a row.\n      event.stopPropagation()\n      if (event.defaultPrevented) return\n\n      const popup = popupRef.current\n      const items = menuItems(popup)\n      if (items.length === 0) return\n\n      const active = document.activeElement as HTMLElement | null\n      const index = active ? items.indexOf(active) : -1\n      const move = (next: number) => {\n        event.preventDefault()\n        items[(next + items.length) % items.length]?.focus()\n      }\n\n      // Mirrors the open key on the SAME per-keydown answer as the trigger.\n      const closeKey = isCascaderRtl(event.currentTarget, direction)\n        ? \"ArrowRight\"\n        : \"ArrowLeft\"\n\n      switch (event.key) {\n        case \"ArrowDown\":\n          return move(index + 1)\n        case \"ArrowUp\":\n          // From the popup itself (a pointer open) Up means the LAST entry.\n          return move(index === -1 ? items.length - 1 : index - 1)\n        case \"Home\":\n          return move(0)\n        case \"End\":\n          return move(items.length - 1)\n        case closeKey:\n          event.preventDefault()\n          return closeAndReturn()\n        case \"Tab\":\n          // A menu never holds Tab: it closes and focus carries on from its row.\n          close()\n          rowRef.current?.focus()\n          return\n        default:\n          break\n      }\n\n      // Typeahead, after the switch so it can never swallow a navigation key.\n      if (\n        event.key.length !== 1 ||\n        event.metaKey ||\n        event.ctrlKey ||\n        event.altKey\n      )\n        return\n      const now = event.timeStamp\n      const state = typeaheadRef.current\n      state.buffer = now - state.at > 500 ? event.key : state.buffer + event.key\n      state.at = now\n      const prefix = state.buffer.toLowerCase()\n      const from = index === -1 ? 0 : index\n      // Starts AFTER the current entry so one letter cycles rather than sticks.\n      const ordered = [\n        ...items.slice(state.buffer.length > 1 ? from : from + 1),\n        ...items.slice(0, state.buffer.length > 1 ? from : from + 1),\n      ]\n      const hit = ordered.find((item) =>\n        (item.textContent ?? \"\").trim().toLowerCase().startsWith(prefix)\n      )\n      if (hit) {\n        event.preventDefault()\n        hit.focus()\n      }\n    },\n    [onKeyDown, direction, close, closeAndReturn, rowRef]\n  )\n\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Positioner\n        /* The ROW, not whatever Base UI last treated as the trigger. */\n        anchor={rowRef}\n        side={side}\n        align={align}\n        sideOffset={sideOffset}\n        alignOffset={alignOffset}\n        className=\"isolate z-50\"\n      >\n        <PopoverPrimitive.Popup\n          ref={popupRef}\n          initialFocus={initialFocus}\n          data-slot=\"cascader-submenu-content\"\n          /* Roving focus, named by the row that opens it. `tabIndex={-1}` lets\n             a pointer open park focus here without adding a Tab stop. */\n          role=\"menu\"\n          aria-orientation=\"vertical\"\n          aria-labelledby={triggerId}\n          tabIndex={-1}\n          onKeyDown={handleKeyDown}\n          /* Marks a menu surface for the docs design-system picker, which\n             repaints menus by walking the DOM. An attribute, not a class. */\n          data-menu-target=\"\"\n          className={cn(\n            FLYOUT_SURFACE_CLASS,\n            \"flex max-w-(--available-width) min-w-48 flex-col gap-0.5 outline-hidden\",\n            CASCADER_LIST_PAD_CLASS,\n            \"p-(--cascader-list-pad,4px)\",\n            className\n          )}\n          {...props}\n        >\n          <CascaderMenuContext.Provider value={true}>\n            {children}\n          </CascaderMenuContext.Provider>\n        </PopoverPrimitive.Popup>\n      </PopoverPrimitive.Positioner>\n    </PopoverPrimitive.Portal>\n  )\n}\n\n/**\n * Whether the footer would render anything, for a wrapper (a separator, a grid\n * row) that has to make the same call `CascaderFooter` already makes.\n */\nexport function useCascaderHasActions(): boolean {\n  const { actions } = useCascaderActions()\n  return actions.length > 0\n}\n\nexport {\n  CascaderAction,\n  CascaderFooter,\n  CascaderSubmenu,\n  CascaderSubmenuContent,\n  CascaderSubmenuTrigger,\n}\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-footer.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader-i18n.tsx",
      "content": "/**\n * cascader-i18n - adapted for tealmac UI from ReUI \"cascader/cascader-i18n\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\nimport type {\n  CascaderLabels,\n  CascaderMode,\n} from \"@/components/reui/cascader/cascader-types\"\n\n/** Name of the root level. Hoisted so the root announcement can reuse it. */\nconst ROOT_LEVEL = \"Top level\"\n\n/** Hoisted for the same reason: several defaults end in an item count. */\nconst itemCount = (count: number) =>\n  `${count} ${count === 1 ? \"item\" : \"items\"}`\n\n/**\n * English defaults. Every string the primitive can render lives here, so a\n * consumer can translate the whole surface by passing `labels`.\n */\nexport const CASCADER_LABELS: CascaderLabels = {\n  // Deliberately NOT lowercased. `toLowerCase()` is locale-hostile - it maps\n  // Turkish \"İ\" to a two-code-point sequence and German \"İstanbul\" style\n  // proper nouns lose their casing - and a label is already written the way\n  // its author wants it read.\n  search: (parentLabel) =>\n    parentLabel ? `Search ${parentLabel}...` : \"Search...\",\n  back: \"Back\",\n  loading: \"Loading...\",\n  loadingMore: \"Loading more...\",\n  loadMore: \"Load more\",\n  error: \"Could not load items.\",\n  retry: \"Retry\",\n  empty: \"No results found.\",\n  selectedCount: (count) => `${count} selected`,\n  breadcrumbLabel: \"Breadcrumb\",\n  chipsLabel: \"Selected items\",\n  removeChip: (label) => `Remove ${label}`,\n  pathSeparator: \"/\",\n  rootLevel: ROOT_LEVEL,\n  itemCount,\n  branchAffordance: \"submenu\",\n  selectedState: \"selected\",\n  partiallySelectedState: \"partially selected\",\n  columnsLabel: \"Levels\",\n  actionsLabel: \"Actions\",\n  submenuAffordance: \"opens a menu\",\n  panelLabel: \"Options\",\n  keyboardHint: (mode: CascaderMode, dir: \"ltr\" | \"rtl\") => {\n    // \"Deeper\" is the direction the text runs, so the level keys mirror in\n    // RTL and the hint has to name the mirrored pair there - an LTR-worded\n    // hint would teach exactly the wrong keys.\n    const open = dir === \"rtl\" ? \"Left\" : \"Right\"\n    const back = dir === \"rtl\" ? \"Right\" : \"Left\"\n    if (mode === \"tree\") {\n      return `Use the ${open} Arrow key to expand and the ${back} Arrow key to collapse.`\n    }\n    if (mode === \"columns\") {\n      return `Use the ${open} Arrow key to open the next column and the ${back} Arrow key to go back.`\n    }\n    return `Use the ${open} Arrow key to open a branch and the ${back} Arrow key to go back.`\n  },\n  rootAnnouncement: (count) => `${ROOT_LEVEL}, ${itemCount(count)}`,\n  expandedAnnouncement: (label, count) =>\n    `${label} expanded, ${itemCount(count)}`,\n  collapsedAnnouncement: (label) => `${label} collapsed`,\n  levelAnnouncement: (parentLabel, depth, count) =>\n    `${parentLabel}, level ${depth}, ${itemCount(count)}`,\n  resultsAnnouncement: (count) =>\n    count === 1 ? \"1 result\" : `${count} results`,\n  maxReachedAnnouncement: (max) => `Selection limit of ${max} reached`,\n  cascadeAnnouncement: (label, count, selecting) =>\n    `${label} ${selecting ? \"selected\" : \"deselected\"}, ${itemCount(count)} followed`,\n  searchingAnnouncement: \"Searching...\",\n}\n\n/**\n * Shallow-merges consumer overrides over the defaults, so `labels` can carry a\n * single key without restating the rest.\n */\nexport function resolveCascaderLabels(\n  labels?: Partial<CascaderLabels>\n): CascaderLabels {\n  if (!labels) return CASCADER_LABELS\n  return { ...CASCADER_LABELS, ...labels }\n}\n\n/** Resolves the search placeholder, which may be a string or a function. */\nexport function resolveCascaderSearchLabel(\n  labels: CascaderLabels,\n  parentLabel?: string\n): string {\n  return typeof labels.search === \"function\"\n    ? labels.search(parentLabel)\n    : labels.search\n}\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-i18n.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader-item.tsx",
      "content": "/**\n * cascader-item - adapted for tealmac UI from ReUI \"cascader/cascader-item\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\n/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- keyboard handling is on the focusable parent (arrow keys, Enter, Escape); the flagged elements are pointer-only affordances or key-event forwarders, as in the ReUI original. */\n\"use client\"\n\nimport * as React from \"react\"\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\"\nimport { mergeProps } from \"@base-ui/react/merge-props\"\nimport { useRender } from \"@base-ui/react/use-render\"\nimport {\n  CheckIcon,\n  ChevronRightIcon,\n  LoaderCircleIcon,\n  MinusIcon,\n  RotateCwIcon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport {\n  useCascaderActions,\n  useCascaderRender,\n  useCascaderState,\n} from \"@/components/reui/cascader/cascader-context\"\nimport {\n  getCascaderCount,\n  getCascaderMoreParent,\n  getCascaderPath,\n  isCascaderMoreNode,\n} from \"@/components/reui/cascader/cascader-lib\"\nimport type {\n  CascaderLoadState,\n  CascaderNode,\n} from \"@/components/reui/cascader/cascader-types\"\n\n// Base UI extends its synthetic events with a handler-veto escape hatch.\ntype VetoableEvent = { preventBaseUIHandler?: () => void }\n\n/** A row press. The veto hook is optional: an `as=\"button\"` row is outside Base\n *  UI's listbox and gets a plain React event. */\nexport type CascaderRowEvent = React.MouseEvent<HTMLElement> & VetoableEvent\n\n// Each style's inline inset, as a variable: a row and its absolutely positioned\n// check have to land on the same number, in LOGICAL properties. These eight\n// numbers MIRROR the shared style sheets; keep them in step with\n// `registry/styles/style-*.css`.\nconst ROW_INSET_CLASS =\n  \"style-vega:[--cascader-row-inset:8px] style-nova:[--cascader-row-inset:6px] style-maia:[--cascader-row-inset:12px] style-lyra:[--cascader-row-inset:8px] style-mira:[--cascader-row-inset:8px] style-luma:[--cascader-row-inset:12px] style-rhea:[--cascader-row-inset:8px] style-sera:[--cascader-row-inset:12px]\"\n\n// Both insets equal, for a row with no check. `!` because a consumer may not\n// import the sheets into `layer(base)`, where the class would win on order.\nconst ROW_FLUSH_CLASS =\n  \"ps-[var(--cascader-row-inset,8px)]! pe-[var(--cascader-row-inset,8px)]!\"\n\n// The same start inset plus room for the check. A flat `pr-8` is wrong in five\n// of the eight styles: 4px of slack in nova, 8px short in maia, luma and sera,\n// and no gutter at all in mira, whose check then overlaps its own label.\nconst ROW_GUTTER_CLASS =\n  \"ps-[var(--cascader-row-inset,8px)]! pe-[calc(var(--cascader-row-inset,8px)*2_+_16px)]!\"\n\n// The check's box, inset to match the row's. A physical `right-2` neither\n// mirrors under RTL nor tracks the style's inset; `right-auto` comes with the\n// logical `end-*`, because width + `left` + `right` is over-constrained.\nconst INDICATOR_CLASS =\n  \"pointer-events-none absolute flex items-center justify-center style-vega:size-4 style-nova:size-4 style-maia:size-4 style-lyra:size-4 style-mira:size-3.5 style-luma:size-4 style-rhea:size-4 style-sera:size-4 end-[var(--cascader-row-inset,8px)]! rtl:right-auto!\"\n\n// The row's type, gap, radius and icon size per style. No horizontal padding:\n// every row pairs this with `ROW_FLUSH_CLASS` or `ROW_GUTTER_CLASS`.\nconst ROW_THEME_CLASS =\n  \"data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground style-vega:rounded-sm style-vega:py-1.5 style-vega:[&_svg:not([class*='size-'])]:size-4 style-nova:[&_svg:not([class*='size-'])]:size-4 style-nova:rounded-md style-nova:py-1 style-maia:[&_svg:not([class*='size-'])]:size-4 style-maia:rounded-xl style-maia:py-2 style-lyra:[&_svg:not([class*='size-'])]:size-4 style-lyra:py-2 style-lyra:rounded-none style-mira:rounded-md style-mira:py-1 style-mira:min-h-7 style-mira:[&_svg:not([class*='size-'])]:size-3.5 style-luma:[&_svg:not([class*='size-'])]:size-4 style-luma:py-2 style-luma:rounded-2xl style-luma:font-medium style-rhea:py-1.5 style-rhea:[&_svg:not([class*='size-'])]:size-4 style-rhea:rounded-xl style-rhea:min-h-7 style-sera:py-2 style-sera:rounded-none style-sera:[&_svg:not([class*='size-'])]:size-3.5\"\n\n/**\n * The row's gap and type, per style, held apart from the rest of the theme.\n *\n * A row that pins its own gap or text size takes `ROW_SHELL_CLASS` and leaves\n * these out, because a `style-<name>:` utility is emitted AFTER the plain one\n * it would have to lose to. Not receiving the token beats overriding it with\n * `!`, which a consumer then has to fight in turn.\n */\nconst ROW_GAP_CLASS =\n  \"style-vega:gap-2 style-nova:gap-2 style-maia:gap-2.5 style-lyra:gap-2 style-mira:gap-2 style-luma:gap-2.5 style-rhea:gap-2 style-sera:gap-2.5\"\n\nconst ROW_TEXT_CLASS =\n  \"style-vega:text-sm style-nova:text-sm style-maia:text-sm style-lyra:text-xs style-mira:text-xs/relaxed style-luma:text-sm style-rhea:text-sm style-sera:text-sm\"\n\nconst ROW_SHELL_CLASS = `${ROW_THEME_CLASS} relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 ${ROW_INSET_CLASS}`\n\nconst ROW_CLASS = `${ROW_SHELL_CLASS} ${ROW_GAP_CLASS} ${ROW_TEXT_CLASS}`\n\nconst CASCADER_ACTION_INSET_CLASS = `${ROW_INSET_CLASS} ${ROW_FLUSH_CLASS}`\n\n/** A footer COMMAND: the row theme, so it cannot drift from the list, plus the\n *  hover and `focus-visible` fills a plain button never gets. Fills, never a\n *  ring: a ring under rows that all paint with a fill read as a different\n *  widget bolted on. `focus-visible`, not `focus`, so a pointer press does not\n *  leave a command painted as if it were still hovered. Disabled keys off\n *  `aria-disabled`, not `:disabled`, to keep it FOCUSABLE: a footer whose only\n *  row is disabled has no tab stop. */\nexport const CASCADER_ACTION_CLASS = `${ROW_SHELL_CLASS} ${ROW_TEXT_CLASS} ${ROW_FLUSH_CLASS} hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground cursor-pointer gap-2 transition-colors aria-disabled:pointer-events-none aria-disabled:opacity-50`\n\n// The box around the affordances that are not the row. 20px around a 16px icon,\n// the outer 2px cancelled at the call site; 24px would set the row height.\nconst AFFORDANCE_BOX_CLASS =\n  \"flex size-5 shrink-0 items-center justify-center rounded-sm\"\n\n// What makes an affordance read as pressable on an already highlighted row. Not\n// `hover:bg-accent`: the row is `bg-accent` under the pointer, so the chip would\n// be invisible. `!` and `**:` for the `CHECKBOX_MARK_CLASS` reason.\nconst AFFORDANCE_HOVER_CLASS =\n  \"transition-[background-color,box-shadow,color] hover:bg-background hover:text-foreground! hover:**:text-foreground! hover:shadow-xs\"\n\n// The selection MARK's colour, pinned so nothing can repaint it: every row\n// carries a DESCENDANT highlight rule, and a mark is STATE. Pinned TWICE\n// because icon sets draw with `fill`/`stroke=\"currentColor\"`, which the\n// `<path>` inherits and resolves against the PATH's own `color`. Both halves\n// are asserted in `cascader-behavior.test.tsx`.\nconst CHECKBOX_MARK_CLASS =\n  \"text-primary-foreground! **:text-primary-foreground!\"\nconst INDICATOR_MARK_CLASS = \"text-foreground! **:text-foreground!\"\n\n// Milliseconds the pointer must rest on a branch row before\n// `expandTrigger=\"hover\"` drills in: long enough that a pointer crossing the\n// column on its way elsewhere opens nothing, short enough to read as immediate.\nconst CASCADER_HOVER_EXPAND_DELAY = 150\n\nexport interface CascaderItemProps extends Omit<\n  ComboboxPrimitive.Item.Props,\n  \"value\" | \"children\" | \"className\" | \"style\" | \"onClick\" | \"onMouseUp\"\n> {\n  /** No state callback: the row merges `className` and `style` itself. */\n  className?: string\n  style?: React.CSSProperties\n  onClick?: (event: CascaderRowEvent) => void\n  onMouseUp?: (event: CascaderRowEvent) => void\n  node: CascaderNode\n  /** Explicit render index. Required while virtualized and DELIBERATELY IGNORED\n   *  otherwise: it self-registers the row and breaks `aria-activedescendant`. */\n  index?: number\n  /** Indentation depth. Drives `--cascader-indent` in tree mode. */\n  depth?: number\n  /** Renders the ancestor chain under the label, for deep and global search. */\n  showPath?: boolean\n  /** `option` renders a real `Combobox.Item`; `button` renders identical markup\n   *  outside the listbox, for the ancestor columns in columns mode. */\n  as?: \"option\" | \"button\"\n  /** Tree mode: this row is an expanded branch. */\n  expanded?: boolean\n  /** Indent each depth by this many pixels. Tree mode only. */\n  indent?: number\n  /** Whether the node has children. Defaults to the cascader's own answer, as do\n   *  the three below: callers pass them because they already hold the level,\n   *  which keeps a memoised row off the volatile context. */\n  branch?: boolean\n  /** Whether the node may be committed. Defaults to the cascader's own answer. */\n  selectable?: boolean\n  /** Whether the node is currently selected. Defaults to the cascader's answer. */\n  selected?: boolean\n  /** Some but not all of the loaded subtree selected. `cascade` only. */\n  indeterminate?: boolean\n  /** Selected nodes below this one, at any depth. Drives the trailing count. */\n  selectedCount?: number\n  /** Paging row only: a request for this level is in flight. */\n  loading?: boolean\n  /** Paging row only: this level's last request failed. */\n  error?: boolean\n  /** Branch row only: this node's OWN children are being fetched. The chevron\n   *  (or, in tree mode, the expander) becomes a spinner IN PLACE, in the same\n   *  16px box: no skeleton row appears below, nothing reflows. */\n  childrenLoading?: boolean\n  /** Branch row only: the last fetch of this node's children failed. The same\n   *  box becomes the retry. The press path is unchanged: it goes through\n   *  `navigate`, which refires the failed level rather than moving. */\n  childrenError?: boolean\n  children?: React.ReactNode\n}\n\n/** One row. Branch presses navigate instead of selecting, vetoed here with\n *  `preventBaseUIHandler()` on `onClick` (Enter too) and on `onMouseUp`. */\nconst CascaderItem = React.memo(function CascaderItem({\n  node,\n  index,\n  depth,\n  showPath = false,\n  as = \"option\",\n  expanded,\n  indent = 16,\n  branch: branchProp,\n  selectable: selectableProp,\n  selected: selectedProp,\n  indeterminate: indeterminateProp,\n  selectedCount: selectedCountProp,\n  loading,\n  error,\n  childrenLoading = false,\n  childrenError = false,\n  className,\n  children,\n  onClick,\n  onMouseUp,\n  style,\n  ...props\n}: CascaderItemProps) {\n  // The ACTIONS context only: the state context republishes on every keystroke.\n  const {\n    index: treeIndex,\n    mode,\n    labels,\n    isBranch,\n    isSelectable,\n    isDisabled,\n    isSelected,\n    isIndeterminate,\n    selectedDescendantCount,\n    multiple,\n    branchesSelectable,\n    indicator: indicatorEnabled,\n    expandTrigger = \"click\",\n    navigate,\n    navigateAt,\n    toggleExpanded,\n    commit,\n    getState,\n    virtualized,\n    loadMore,\n    retryLevel,\n  } = useCascaderActions()\n  // Render props come from an always-current context, or a closure goes stale.\n  const { renderItem, renderLabel } = useCascaderRender()\n\n  const branch = branchProp ?? isBranch(node)\n  const selectable = selectableProp ?? isSelectable(node)\n  // An ancestor's flag counts: a search hit or a revealed path can list a row\n  // inside a branch nobody may open. Asked here, off the near-stable context,\n  // so no new prop and only a row that re-renders pays the ancestor walk.\n  const disabled = isDisabled(node)\n  const selected = selectedProp ?? isSelected(node)\n  // A selected node is never also partially selected (a dash in a filled box).\n  const indeterminate =\n    !selected && (indeterminateProp ?? isIndeterminate(node))\n  const count = branch ? getCascaderCount(treeIndex, node) : 0\n  const nodeDepth = depth ?? treeIndex.depthOf.get(node.value) ?? 0\n\n  // O(1): the cascader publishes a map of selected-descendant counts, because a\n  // row walking its own subtree is `rows x descendants`, paid every keystroke.\n  const selectedDescendants = branch\n    ? (selectedCountProp ?? selectedDescendantCount(node))\n    : 0\n  // `multiple` only: with a single selection the trigger, the breadcrumb and\n  // the leaf's own check already say where it is, so swapping a branch's real\n  // child total (\"24\") for a permanent \"1\" would trade a useful number away.\n  const showsSelectedCount = multiple && selectedDescendants > 0\n  const trailingCount = showsSelectedCount ? selectedDescendants : count\n  // Shown selected or not: suppressing it under a checked branch read as data\n  // loss, and the row's TWO trailing columns mean the count never has to share\n  // a slot with the check.\n  const showCount = trailingCount > 0\n\n  // Where a multi-select tree puts its checkbox: at the HEAD of the row, since a\n  // trailing box does not follow the label stagger. Per-LIST, never per-row, or\n  // the labels below an unselectable row step back out.\n  const leadingCheckbox = mode === \"tree\" && multiple\n\n  // Whether this row draws a MARK, and so whether it reserves the gutter one\n  // needs. `|| multiple` is the scope of the opt-out: a checkbox is the\n  // selection CONTROL, so `indicator={false}` is a no-op in multi-select.\n  const showsIndicator = indicatorEnabled || multiple\n\n  const veto = React.useCallback((event: VetoableEvent) => {\n    event.preventBaseUIHandler?.()\n  }, [])\n\n  const handleClick = React.useCallback(\n    (event: CascaderRowEvent) => {\n      onClick?.(event)\n      if (disabled) return\n      if (branch && !selectable) {\n        veto(event)\n        navigate(node)\n      }\n    },\n    [onClick, node, disabled, branch, selectable, veto, navigate]\n  )\n\n  const handleMouseUp = React.useCallback(\n    (event: CascaderRowEvent) => {\n      onMouseUp?.(event)\n      if (disabled) return\n      if (branch && !selectable) veto(event)\n    },\n    [onMouseUp, disabled, branch, selectable, veto]\n  )\n\n  // The tree expander's own press. With `selectable=\"any\"` the row press\n  // commits, and under `cascade` that selected a whole subtree while the row\n  // stayed collapsed, so expanding needs its own target. `stopPropagation`\n  // before the veto, so Base UI's handler never runs, and no role: nesting\n  // one in `role=\"option\"` fails axe. `navigate` fetches BEFORE it opens.\n  // A disabled row may still CLOSE what `expanded` opened, never open: a row\n  // only a query holds open is not in `expanded`, and the toggle would add it.\n  const handleExpanderClick = React.useCallback(\n    (event: CascaderRowEvent) => {\n      event.stopPropagation()\n      veto(event)\n      if (expanded) {\n        if (!disabled || getState().expanded.has(node.value)) {\n          toggleExpanded(node.value)\n        }\n      } else if (!disabled) navigate(node)\n    },\n    [veto, node, disabled, expanded, getState, toggleExpanded, navigate]\n  )\n\n  const handleExpanderMouseUp = React.useCallback(\n    (event: CascaderRowEvent) => {\n      // Base UI treats a drag-select mouseup as a commit; this is not one.\n      event.stopPropagation()\n      veto(event)\n    },\n    [veto]\n  )\n\n  const handleChevronClick = React.useCallback(\n    (event: CascaderRowEvent) => {\n      event.stopPropagation()\n      veto(event)\n      if (disabled) return\n      // `navigate` falls through to `pushLevel`, which APPENDS: right in the\n      // deepest column, but from an ancestor column it duplicated the level and\n      // corrupted the path. `navigateAt` rebuilds the trail from this depth.\n      if (as === \"button\") navigateAt(node, depth ?? 0)\n      else navigate(node)\n    },\n    [veto, node, disabled, as, navigateAt, depth, navigate]\n  )\n\n  const handleChevronMouseUp = React.useCallback(\n    (event: CascaderRowEvent) => veto(event),\n    [veto]\n  )\n\n  // Keeps focus in the search field: Base UI prevents the mousedown default for\n  // option rows, and focusing a `tabindex=\"-1\"` trail row kills every key.\n  const handleButtonMouseDown = React.useCallback(\n    (event: React.MouseEvent<HTMLElement>) => {\n      event.preventDefault()\n    },\n    []\n  )\n\n  const handleButtonClick = React.useCallback(\n    (event: CascaderRowEvent) => {\n      onClick?.(event)\n      if (disabled) return\n      if (branch && !selectable) {\n        // Button rows only exist in the columns trail, where `depth` is the\n        // column's own depth, so navigating replaces the trail from here.\n        navigateAt(node, depth ?? 0)\n        return\n      }\n      if (selectable) commit(node)\n    },\n    [onClick, node, disabled, branch, selectable, navigateAt, depth, commit]\n  )\n\n  // The paging row's press, never a selection, whatever `selectable` says.\n  const handleMoreClick = React.useCallback(\n    (event: CascaderRowEvent) => {\n      onClick?.(event)\n      veto(event)\n      const parent = getCascaderMoreParent(node)\n      if (parent == null) return\n      if (error) retryLevel(parent)\n      else if (!loading) loadMore(parent)\n    },\n    [onClick, veto, node, error, loading, retryLevel, loadMore]\n  )\n\n  // `expandTrigger=\"hover\"`: resting on a BRANCH row in the ACTIVE column drills\n  // in after a delay. The deepest column of columns mode only, and it never\n  // commits, so a pointer crossing the panel cannot change the value.\n  const hoverNavigates =\n    expandTrigger === \"hover\" &&\n    mode === \"columns\" &&\n    as === \"option\" &&\n    branch &&\n    !disabled\n  const hoverTimerRef = React.useRef<number | null>(null)\n\n  const cancelHoverNavigate = React.useCallback(() => {\n    if (hoverTimerRef.current == null) return\n    window.clearTimeout(hoverTimerRef.current)\n    hoverTimerRef.current = null\n  }, [])\n\n  const handleHoverPointerEnter = React.useCallback(() => {\n    cancelHoverNavigate()\n    hoverTimerRef.current = window.setTimeout(() => {\n      hoverTimerRef.current = null\n      navigate(node)\n    }, CASCADER_HOVER_EXPAND_DELAY)\n  }, [cancelHoverNavigate, navigate, node])\n\n  // Drilling in demotes this column to the trail WITHOUT unmounting the row, so\n  // a pending timer has to die or it fires `navigate` from an inactive column.\n  React.useEffect(() => {\n    if (!hoverNavigates) cancelHoverNavigate()\n  }, [hoverNavigates, cancelHoverNavigate])\n  React.useEffect(() => cancelHoverNavigate, [cancelHoverNavigate])\n\n  /* ------------------------------- paging row ------------------------------ */\n\n  if (isCascaderMoreNode(node)) {\n    const text = error\n      ? labels.error\n      : loading\n        ? node.count\n          ? labels.loadingMore\n          : labels.loading\n        : labels.loadMore\n\n    const moreStyle =\n      mode === \"tree\" && nodeDepth > 0\n        ? ({\n            ...style,\n            \"--cascader-indent\": `${nodeDepth * indent}px`,\n          } as React.CSSProperties)\n        : style\n\n    const moreShared = {\n      \"data-slot\": \"cascader-item\",\n      \"data-more\": \"\",\n      \"data-state\": error ? \"error\" : loading ? \"loading\" : \"idle\",\n      \"data-depth\": nodeDepth,\n      style: moreStyle,\n      className: cn(\n        ROW_SHELL_CLASS,\n        ROW_FLUSH_CLASS,\n        \"text-muted-foreground justify-center gap-1.5 text-xs\",\n        !error && !loading && \"hover:text-foreground\",\n        mode === \"tree\" &&\n          \"ps-[calc(var(--cascader-row-inset,8px)_+_var(--cascader-indent,0px))]!\",\n        className\n      ),\n    }\n\n    const moreBody = children ?? (\n      <>\n        {loading ? (\n          <LoaderCircleIcon className=\"size-3.5 animate-spin\" aria-hidden />\n        ) : null}\n        <span>{text}</span>\n        {/* TEXT, not a button: a focusable element inside a `role=\"option\"`\n            row is a `nested-interactive` violation. */}\n        {error ? (\n          <span className=\"text-foreground font-medium\">{labels.retry}</span>\n        ) : null}\n      </>\n    )\n\n    if (as === \"button\") {\n      const buttonProps = { ...(props as React.ComponentProps<\"button\">) }\n      delete buttonProps[\"aria-setsize\"]\n      delete buttonProps[\"aria-posinset\"]\n      delete buttonProps[\"aria-level\"]\n\n      return (\n        <button\n          type=\"button\"\n          {...moreShared}\n          tabIndex={-1}\n          onClick={handleMoreClick}\n          {...buttonProps}\n        >\n          {moreBody}\n        </button>\n      )\n    }\n\n    return (\n      <ComboboxPrimitive.Item\n        {...moreShared}\n        value={node}\n        {...(virtualized && index != null ? { index } : null)}\n        {...(mode === \"tree\"\n          ? { role: \"treeitem\" as const, \"aria-level\": nodeDepth + 1 }\n          : null)}\n        onClick={handleMoreClick}\n        onMouseUp={veto}\n        {...props}\n      >\n        {moreBody}\n      </ComboboxPrimitive.Item>\n    )\n  }\n\n  const itemState = {\n    branch,\n    selected,\n    disabled,\n    depth: nodeDepth,\n    count,\n    path: showPath ? getCascaderPath(treeIndex, node.value).slice(0, -1) : [],\n  }\n\n  const custom = renderItem?.(node, itemState)\n  const customLabel = renderLabel?.(node, itemState)\n\n  // What the markup cannot say: a branch would announce \"Person 24\", a naked\n  // number. Trail rows are outside the listbox, so they need words for state.\n  const srDetails = [\n    branch ? labels.itemCount(count) : null,\n    showsSelectedCount ? labels.selectedCount(selectedDescendants) : null,\n    branch && mode !== \"tree\" ? labels.branchAffordance : null,\n    as === \"button\" && selected ? labels.selectedState : null,\n    // Option rows say this with `aria-checked=\"mixed\"`; `role=\"button\"` allows\n    // neither that nor `aria-selected`, so a trail row says it in words.\n    as === \"button\" && indeterminate ? labels.partiallySelectedState : null,\n  ]\n    .filter(Boolean)\n    .map((detail) => `, ${detail}`)\n    .join(\"\")\n\n  // Extracted because tree mode renders it INLINE at the head of the row. It\n  // reads `data-selected` off the ROW, so it also works on the trail rows.\n  const checkbox = (\n    <span\n      data-slot=\"cascader-item-checkbox\"\n      className=\"border-input in-data-[selected]:bg-primary in-data-[selected]:border-primary in-data-[selected]:text-primary-foreground in-data-[indeterminate]:bg-primary in-data-[indeterminate]:border-primary in-data-[indeterminate]:text-primary-foreground flex size-4 shrink-0 items-center justify-center rounded-[4px] border transition-colors\"\n    >\n      {/* Colour declared ON THE ICON with `!`; see `CHECKBOX_MARK_CLASS`. */}\n      {indeterminate ? (\n        <MinusIcon\n          data-slot=\"cascader-item-dash\"\n          className={cn(CHECKBOX_MARK_CLASS, \"size-3\")}\n        />\n      ) : (\n        <CheckIcon\n          data-slot=\"cascader-item-tick\"\n          className={cn(\n            CHECKBOX_MARK_CLASS,\n            \"size-3 opacity-0 in-data-[selected]:opacity-100\"\n          )}\n        />\n      )}\n    </span>\n  )\n\n  const indicator = (\n    <>\n      {multiple ? (\n        checkbox\n      ) : (\n        <CheckIcon\n          data-slot=\"cascader-item-check\"\n          className={cn(\"pointer-events-none\", INDICATOR_MARK_CLASS)}\n        />\n      )}\n    </>\n  )\n\n  const body = custom ?? children ?? (\n    <>\n      {/* Tree mode expands in place, so the branch marker leads the row. */}\n      {mode === \"tree\" && branch ? (\n        <span\n          data-slot=\"cascader-item-expander\"\n          /* Pure affordance: the row itself carries `aria-expanded`. */\n          aria-hidden=\"true\"\n          data-state={\n            childrenError ? \"error\" : childrenLoading ? \"loading\" : \"idle\"\n          }\n          onClick={handleExpanderClick}\n          onMouseUp={handleExpanderMouseUp}\n          className={cn(\n            \"-ms-0.5\",\n            childrenError ? \"text-destructive\" : \"text-muted-foreground\",\n            AFFORDANCE_BOX_CLASS,\n            AFFORDANCE_HOVER_CLASS,\n            /* A disabled row is `pointer-events-none`; only while open does its\n               expander take the pointer back, to close what `expanded` opened. */\n            disabled && expanded && \"pointer-events-auto\",\n            childrenLoading && \"pointer-events-none\"\n          )}\n        >\n          {childrenLoading ? (\n            <Spinner aria-hidden=\"true\" className=\"size-4\" />\n          ) : childrenError ? (\n            <RotateCwIcon className=\"size-4\" />\n          ) : (\n            <ChevronRightIcon\n              className={cn(\n                \"size-4 transition-transform\",\n                /* Collapsed it points INTO the level, so it mirrors; expanded\n                   it points DOWN, the same in both writing modes, so the mirror\n                   has to come off or `scaleX(-1)` composed with `rotate(90deg)`\n                   lands it pointing up. `rtl:rotate-90` beside `rotate-90` was\n                   a no-op that only looked like it handled this. */\n                expanded ? \"rotate-90\" : \"rtl:-scale-x-100\"\n              )}\n            />\n          )}\n        </span>\n      ) : mode === \"tree\" ? (\n        // A LEAF's expander slot, reserved and empty: without it a leaf's label\n        // started 28px off its sibling branches' and read as one level up.\n        <span\n          data-slot=\"cascader-item-expander-spacer\"\n          aria-hidden=\"true\"\n          className={cn(\"-ms-0.5\", AFFORDANCE_BOX_CLASS)}\n        />\n      ) : null}\n\n      {/* Tree + multi-select: the box leads the row, and an unselectable row\n          still reserves its width. */}\n      {leadingCheckbox ? (\n        selectable ? (\n          checkbox\n        ) : (\n          <span\n            data-slot=\"cascader-item-checkbox-spacer\"\n            aria-hidden=\"true\"\n            className=\"size-4 shrink-0\"\n          />\n        )\n      ) : null}\n\n      {node.icon ? (\n        <span\n          data-slot=\"cascader-item-icon\"\n          className=\"text-muted-foreground flex shrink-0 items-center justify-center\"\n        >\n          {node.icon}\n        </span>\n      ) : null}\n\n      {/* The row box is sized for pieces side by side; this one stacks, so it\n          restates the gap: 8px reads as a paragraph break, not a subtitle. */}\n      <span className=\"flex min-w-0 flex-1 flex-col items-start gap-0.5\">\n        {customLabel ?? (\n          <>\n            <span className=\"w-full truncate text-start\">{node.label}</span>\n            {node.description ? (\n              <span\n                data-slot=\"cascader-item-description\"\n                className=\"text-muted-foreground w-full truncate text-start text-xs\"\n              >\n                {node.description}\n              </span>\n            ) : null}\n          </>\n        )}\n        {showPath ? <CascaderItemPath node={node} /> : null}\n      </span>\n\n      {/* The count is a bare number, and nothing else says a branch opens. */}\n      {srDetails ? (\n        <span data-slot=\"cascader-item-details\" className=\"sr-only\">\n          {srDetails}\n        </span>\n      ) : null}\n\n      {/* Tree mode expands in place, so it has no drill-in chevron. */}\n      {branch && mode !== \"tree\" ? (\n        <span\n          data-slot=\"cascader-item-trailing\"\n          aria-hidden=\"true\"\n          className=\"text-muted-foreground ms-auto flex shrink-0 items-center gap-1 text-xs tabular-nums\"\n        >\n          {showCount ? (\n            <span\n              data-slot=\"cascader-item-count\"\n              /* The marker a consumer (and a test) reads to tell the two\n                 numbers apart without matching on a colour class. */\n              {...(showsSelectedCount ? { \"data-selected-count\": \"\" } : null)}\n              className={cn(showsSelectedCount && \"text-primary!\")}\n            >\n              {trailingCount}\n            </span>\n          ) : null}\n          {/* ONLY the chevron is the drill target: sharing a hit area with\n              the count made a number pressable. */}\n          <span\n            data-slot=\"cascader-item-chevron\"\n            data-state={\n              childrenError ? \"error\" : childrenLoading ? \"loading\" : \"idle\"\n            }\n            onClick={selectable ? handleChevronClick : undefined}\n            onMouseUp={selectable ? handleChevronMouseUp : undefined}\n            onMouseDown={as === \"button\" ? handleButtonMouseDown : undefined}\n            className={cn(\n              /* Cancels the box's outer slack, so the icon keeps its column. */\n              \"-me-0.5\",\n              AFFORDANCE_BOX_CLASS,\n              selectable && AFFORDANCE_HOVER_CLASS,\n              childrenError && \"text-destructive\",\n              childrenLoading && \"pointer-events-none\"\n            )}\n          >\n            {childrenLoading ? (\n              <Spinner aria-hidden=\"true\" className=\"size-4\" />\n            ) : childrenError ? (\n              <RotateCwIcon className=\"size-4\" />\n            ) : (\n              <ChevronRightIcon className=\"size-4 rtl:-scale-x-100\" />\n            )}\n          </span>\n        </span>\n      ) : null}\n\n      {branch && mode === \"tree\" && showCount ? (\n        <span\n          data-slot=\"cascader-item-count\"\n          {...(showsSelectedCount ? { \"data-selected-count\": \"\" } : null)}\n          /* The count is spoken by the sr-only details span instead. */\n          aria-hidden=\"true\"\n          className={cn(\n            \"text-muted-foreground ms-auto shrink-0 text-xs tabular-nums\",\n            showsSelectedCount && \"text-primary!\"\n          )}\n        >\n          {trailingCount}\n        </span>\n      ) : null}\n\n      {/* No trailing indicator when a tree's box led the row, or when\n          `indicator={false}` opted out. See `showsIndicator`. */}\n      {selectable && !leadingCheckbox && showsIndicator ? (\n        as === \"button\" ? (\n          // No ItemIndicator outside Base UI: `data-selected` drives it.\n          (multiple || selected) && (\n            <span\n              data-slot=\"cascader-item-indicator\"\n              className={INDICATOR_CLASS}\n            >\n              {indicator}\n            </span>\n          )\n        ) : (\n          <ComboboxPrimitive.ItemIndicator\n            /* Kept mounted for a partial selection too: the row is not\n               selected, so Base UI would unmount the box the dash lives in. */\n            keepMounted={multiple || indeterminate}\n            render={\n              <span\n                data-slot=\"cascader-item-indicator\"\n                className={INDICATOR_CLASS}\n              />\n            }\n          >\n            {indicator}\n          </ComboboxPrimitive.ItemIndicator>\n        )\n      ) : null}\n    </>\n  )\n\n  const rowStyle =\n    mode === \"tree\" && nodeDepth > 0\n      ? ({\n          ...style,\n          \"--cascader-indent\": `${nodeDepth * indent}px`,\n        } as React.CSSProperties)\n      : style\n\n  const shared = {\n    \"data-slot\": \"cascader-item\",\n    \"data-branch\": branch || undefined,\n    \"data-selected\": selected || undefined,\n    \"data-indeterminate\": indeterminate || undefined,\n    \"data-depth\": nodeDepth,\n    \"data-expanded\": expanded || undefined,\n    // No `aria-level`: `role=\"option\"` allows only `aria-selected`,\n    // `aria-checked`, `aria-posinset` and `aria-setsize`, and a `role=\"button\"`\n    // trail row allows none of the four. Only tree mode has a role that takes a\n    // level, and it adds one below.\n    style: rowStyle,\n    className: cn(\n      ROW_CLASS,\n      /* Three separate reasons to hand the check gutter back. A multi-select\n         tree's box leads the row, so the gutter would be 24px of dead space;\n         `indicator={false}` drops the check, and keeping the gutter would park\n         the count and the chevron one gutter in from an edge nothing sits on;\n         and for branches the question is per-LIST, not per-row, because the old\n         `branch && !selectable` put neighbouring chevrons on 24px and 6px in\n         nova. One `pe-*` is emitted either way, so nothing here depends on\n         class-merge order. */\n      leadingCheckbox || !showsIndicator || (branch && !branchesSelectable)\n        ? ROW_FLUSH_CLASS\n        : ROW_GUTTER_CLASS,\n      /* ADDED to the style's own inset, never substituted: replacing it put\n         depth 0 at 12px and depth 1 at 16px in maia, luma and sera, a 4px step.\n         `_+_` because Tailwind's parser splits on whitespace. */\n      mode === \"tree\" &&\n        \"ps-[calc(var(--cascader-row-inset,8px)_+_var(--cascader-indent,0px))]!\",\n      className\n    ),\n  }\n\n  if (as === \"button\") {\n    const buttonProps = { ...(props as React.ComponentProps<\"button\">) }\n    // `role=\"button\"` allows none of these, so they are dropped, not passed.\n    delete buttonProps[\"aria-setsize\"]\n    delete buttonProps[\"aria-posinset\"]\n    delete buttonProps[\"aria-level\"]\n\n    return (\n      <button\n        type=\"button\"\n        {...shared}\n        /* Focus stays in the search input; the trail uses ArrowLeft. */\n        tabIndex={-1}\n        disabled={disabled}\n        aria-disabled={disabled || undefined}\n        onClick={handleButtonClick}\n        onMouseDown={handleButtonMouseDown}\n        {...buttonProps}\n      >\n        {body}\n      </button>\n    )\n  }\n\n  return (\n    <ComboboxPrimitive.Item\n      {...shared}\n      value={node}\n      /* THE one gate for the explicit index: supply one outside virtualized mode\n         and the first arrow key leaves `aria-activedescendant` pointing at\n         nothing. */\n      {...(virtualized && index != null ? { index } : null)}\n      disabled={disabled}\n      /* Conditional spread, never `role={... : undefined}`: `mergeProps`\n         iterates own keys, so `undefined` would delete `role=\"option\"`. */\n      {...(mode === \"tree\"\n        ? {\n            role: \"treeitem\" as const,\n            \"aria-level\": nodeDepth + 1,\n            ...(branch ? { \"aria-expanded\": !!expanded } : null),\n          }\n        : branch\n          ? { \"aria-haspopup\": \"listbox\" as const }\n          : null)}\n      /* Only ever `mixed`: a selected row already says so with `aria-selected`,\n         and two selection attributes on one row is noise. */\n      {...(indeterminate ? { \"aria-checked\": \"mixed\" as const } : null)}\n      {...(hoverNavigates\n        ? {\n            onPointerEnter: handleHoverPointerEnter,\n            onPointerLeave: cancelHoverNavigate,\n          }\n        : null)}\n      onClick={handleClick}\n      onMouseUp={handleMouseUp}\n      {...props}\n    >\n      {body}\n    </ComboboxPrimitive.Item>\n  )\n})\n\n/** Ancestor chain under a deep or global hit: \"Name\" alone is ambiguous. */\nfunction CascaderItemPath({ node }: { node: CascaderNode }) {\n  const { index, labels } = useCascaderActions()\n  const ancestors = getCascaderPath(index, node.value).slice(0, -1)\n  if (!ancestors.length) return null\n\n  return (\n    <span\n      data-slot=\"cascader-item-path\"\n      className=\"text-muted-foreground flex w-full items-center gap-0.5 truncate text-start text-xs\"\n    >\n      {ancestors.map((ancestor, i) => (\n        <React.Fragment key={ancestor.value}>\n          {i > 0 ? (\n            <span aria-hidden=\"true\">{labels.pathSeparator}</span>\n          ) : null}\n          <span className=\"truncate\">{ancestor.label}</span>\n        </React.Fragment>\n      ))}\n    </span>\n  )\n}\n\n/** Every load-derived prop a row needs. A PAGING row reports the level it\n *  belongs to, a BRANCH row the level it OWNS; mutually exclusive, so one call\n *  answers both. Exported because a memoised row cannot read them itself. */\nexport function getCascaderMoreProps(\n  node: CascaderNode,\n  loadStates: ReadonlyMap<string, CascaderLoadState>\n): {\n  loading: boolean\n  error: boolean\n  childrenLoading: boolean\n  childrenError: boolean\n} {\n  const parent = getCascaderMoreParent(node)\n  if (parent == null) {\n    const own = loadStates.get(node.value)\n    return {\n      loading: false,\n      error: false,\n      childrenLoading: !!own?.loading,\n      childrenError: !!own?.error,\n    }\n  }\n  const state = loadStates.get(parent)\n  return {\n    loading: !!state?.loading,\n    error: !!state?.error,\n    childrenLoading: false,\n    childrenError: false,\n  }\n}\n\nexport interface CascaderItemsProps {\n  /** Replaces the default row for every item. */\n  children?: (node: CascaderNode, index: number) => React.ReactNode\n}\n\n/** Whatever the active view is: the current level, the deepest column, or the\n *  flattened tree rows. Lives here so `cascader.tsx` never imports a row\n *  component, which would cycle with the context. `index` is deliberately not\n *  passed; see `CascaderItemProps`. */\nfunction CascaderItems({ children }: CascaderItemsProps) {\n  const { mode, isBranch, isSelectable, isSelected, isIndeterminate } =\n    useCascaderActions()\n  const { renderedItems, deepResults, treeRows, loadStates } =\n    useCascaderState()\n  const showPath = deepResults !== null\n\n  // Answered once here, as plain booleans, so `React.memo` can skip a row.\n  if (mode === \"tree\") {\n    return (\n      <>\n        {treeRows.map((row, i) =>\n          children ? (\n            <React.Fragment key={row.node.value}>\n              {children(row.node, i)}\n            </React.Fragment>\n          ) : (\n            <CascaderItem\n              key={row.node.value}\n              node={row.node}\n              depth={row.depth}\n              expanded={row.expanded}\n              branch={row.branch}\n              selectable={isSelectable(row.node)}\n              selected={isSelected(row.node)}\n              indeterminate={isIndeterminate(row.node)}\n              {...getCascaderMoreProps(row.node, loadStates)}\n              aria-setsize={row.setSize}\n              aria-posinset={row.posInSet}\n            />\n          )\n        )}\n      </>\n    )\n  }\n\n  return (\n    <>\n      {renderedItems.map((node, i) =>\n        children ? (\n          <React.Fragment key={node.value}>{children(node, i)}</React.Fragment>\n        ) : (\n          <CascaderItem\n            key={node.value}\n            node={node}\n            showPath={showPath}\n            branch={isBranch(node)}\n            selectable={isSelectable(node)}\n            selected={isSelected(node)}\n            indeterminate={isIndeterminate(node)}\n            {...getCascaderMoreProps(node, loadStates)}\n            aria-setsize={renderedItems.length}\n            aria-posinset={i + 1}\n          />\n        )\n      )}\n    </>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                          Group / Label / Separator                         */\n/* -------------------------------------------------------------------------- */\n\n// Whether a `CascaderLabel` has a group to name: Base UI's `Combobox.GroupLabel`\n// THROWS when there is none, and a heading is useful outside a group.\nconst CascaderGroupContext = React.createContext(false)\n\n// A heading over a run of rows. All eight styles are spelled out because the\n// type is not uniform (sera is uppercase), and the inset is the row's own.\nconst CASCADER_LABEL_CLASS = `text-muted-foreground text-xs style-vega:py-1.5 style-nova:py-1.5 style-maia:py-2.5 style-lyra:py-2 style-mira:py-1.5 style-luma:py-2.5 style-rhea:py-1.5 style-sera:py-2 style-sera:uppercase style-sera:tracking-wider style-sera:font-semibold ${CASCADER_ACTION_INSET_CLASS} block truncate`\n\n// A rule between two runs. The inline margin is not the combobox separator's:\n// that cancels the container's padding with a per-style number, wrong in a\n// footer and 4px too much in lyra. `--cascader-list-pad` reaches the edge.\nconst CASCADER_SEPARATOR_CLASS =\n  \"h-px style-vega:bg-border style-vega:my-1 style-nova:bg-border style-nova:my-1 style-maia:my-1 style-maia:bg-border/50 style-lyra:bg-border style-mira:my-1 style-mira:bg-border/50 style-luma:bg-border style-luma:my-1.5 style-rhea:bg-border style-rhea:my-1 style-sera:bg-border/50 style-sera:my-1.5 mx-[calc(var(--cascader-list-pad,4px)*-1)]! shrink-0\"\n\nexport interface CascaderGroupProps extends Omit<\n  ComboboxPrimitive.Group.Props,\n  \"className\"\n> {\n  /** Base UI also accepts a state callback here; the group has no state. */\n  className?: string\n}\n\n/** A run of related rows, named by the `CascaderLabel` inside it: the group is\n *  `role=\"group\"` and Base UI points its `aria-labelledby` at the label, where a\n *  heading alone inside a listbox is dropped from the accessibility tree.\n *  Grouping is composition, not data: `CascaderItems` renders one flat run per\n *  level, so its indices line up with `listRef`. */\nfunction CascaderGroup({ className, ...props }: CascaderGroupProps) {\n  return (\n    <CascaderGroupContext.Provider value={true}>\n      <ComboboxPrimitive.Group\n        data-slot=\"cascader-group\"\n        className={cn(\"flex flex-col\", className)}\n        {...props}\n      />\n    </CascaderGroupContext.Provider>\n  )\n}\n\nexport interface CascaderLabelProps extends Omit<\n  ComboboxPrimitive.GroupLabel.Props,\n  \"className\" | \"style\"\n> {\n  /** No state callback; outside a group this is not a Base UI part at all. */\n  className?: string\n  style?: React.CSSProperties\n}\n\n/** Inside a group: the real thing, with the id association Base UI wires up. */\nfunction CascaderGroupLabel({ className, ...props }: CascaderLabelProps) {\n  return (\n    <ComboboxPrimitive.GroupLabel\n      data-slot=\"cascader-label\"\n      className={cn(CASCADER_LABEL_CLASS, className)}\n      {...props}\n    />\n  )\n}\n\n// Outside a group: the same heading as a plain element. A separate component\n// rather than a branch, so the hook count cannot change when a label moves in.\nfunction CascaderPlainLabel({ className, ...props }: CascaderLabelProps) {\n  const defaultProps = {\n    \"data-slot\": \"cascader-label\",\n    className: cn(CASCADER_LABEL_CLASS, className),\n  }\n\n  return useRender({\n    defaultTagName: \"div\",\n    render: props.render,\n    props: mergeProps<\"div\">(defaultProps, props),\n  })\n}\n\n/** A heading. Inside `CascaderGroup` it is the group's accessible name; outside\n *  one (the footer flyout is a menu, not a listbox) it is decoration only. */\nfunction CascaderLabel(props: CascaderLabelProps) {\n  const grouped = React.useContext(CascaderGroupContext)\n  return grouped ? (\n    <CascaderGroupLabel {...props} />\n  ) : (\n    <CascaderPlainLabel {...props} />\n  )\n}\n\nexport interface CascaderSeparatorProps extends Omit<\n  ComboboxPrimitive.Separator.Props,\n  \"className\"\n> {\n  /** Base UI also accepts a state callback here; only the orientation varies. */\n  className?: string\n}\n\n/** Decorative on purpose: Base UI's separator is `role=\"separator\"`, which a\n *  `listbox` may not own, and a run that needs separating for a screen reader\n *  needs a `CascaderGroup`, not a line. A consumer can pass the role back. */\nfunction CascaderSeparator({ className, ...props }: CascaderSeparatorProps) {\n  return (\n    <ComboboxPrimitive.Separator\n      data-slot=\"cascader-separator\"\n      role=\"presentation\"\n      aria-hidden=\"true\"\n      className={cn(CASCADER_SEPARATOR_CLASS, className)}\n      {...props}\n    />\n  )\n}\n\nexport {\n  CascaderGroup,\n  CascaderItem,\n  CascaderItems,\n  CascaderLabel,\n  CascaderSeparator,\n}\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-item.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader-lib.tsx",
      "content": "/**\n * cascader-lib - adapted for tealmac UI from ReUI \"cascader/cascader-lib\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\nimport type {\n  CascaderCollapse,\n  CascaderFlatNode,\n  CascaderIndex,\n  CascaderNode,\n  CascaderPathSegment,\n  CascaderSelectable,\n} from \"@/components/reui/cascader/cascader-types\"\n\n/** Root level key in `childrenOf`. NUL-prefixed so no real value collides. */\nexport const CASCADER_ROOT_KEY = \"\\u0000root\"\n\n/**\n * Paging pseudo-node prefix. Written as an escape sequence, not a raw 0x00\n * byte: a literal NUL makes this module read as binary and `grep -I` skips it.\n */\nexport const CASCADER_MORE_PREFIX = \"\\u0000more:\"\n\n/* -------------------------------------------------------------------------- */\n/*                                Scroll layout                               */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The four scroll classes. In the lib because `cascader-columns.tsx` needs them\n * too and must not import `cascader.tsx`. Shell > bound > scrollport > rows.\n */\n\n/**\n * The bound. `min()` over an undefined custom property is an INVALID\n * declaration that drops the whole max-height, hence the `100vh` fallback: an\n * inline panel has no positioner, so no `--available-height`. `24rem` is the\n * default cap, so the common case sets no variable at all.\n */\nexport const CASCADER_LIST_HEIGHT_CLASS =\n  \"max-h-[min(var(--available-height,100vh),var(--cascader-max-height,24rem))]\"\n\n/**\n * Each style's list padding: rows take it as `padding`, the scrollport as\n * `scroll-padding` floored at 4px so lyra's `0` does not strand a row.\n * Mirrored per style from `registry/styles/style-*.css`; keep them in step.\n */\nexport const CASCADER_LIST_PAD_CLASS =\n  \"style-vega:[--cascader-list-pad:4px] style-nova:[--cascader-list-pad:4px] style-maia:[--cascader-list-pad:4px] style-lyra:[--cascader-list-pad:0px] style-mira:[--cascader-list-pad:4px] style-luma:[--cascader-list-pad:6px] style-rhea:[--cascader-list-pad:4px] style-sera:[--cascader-list-pad:6px]\"\n\n/** The scrollport: a `max-h-*` on the ScrollArea ROOT bounds nothing. */\nexport const CASCADER_SCROLL_CLASS =\n  \"size-full min-h-0 **:data-[slot=scroll-area-viewport]:h-full **:data-[slot=scroll-area-viewport]:overscroll-contain **:data-[slot=scroll-area-viewport]:scroll-py-[max(var(--cascader-list-pad,4px),4px)]\"\n\n/** The rows' box; `data-empty:p-0` keeps an empty state from double inset. */\nexport const CASCADER_ROWS_CLASS = \"p-(--cascader-list-pad,4px) data-empty:p-0\"\n\n/* -------------------------------------------------------------------------- */\n/*                                  Tab order                                 */\n/* -------------------------------------------------------------------------- */\n\n/**\n * What counts as a keyboard stop. Base UI's ScrollArea viewport makes ITSELF\n * tabbable when content overflows (`hiddenState.x && hiddenState.y ? -1 : 0`),\n * an unnamed stop per level that `ui/scroll-area.tsx` will not let us suppress.\n */\nconst CASCADER_TAB_STOP_SELECTOR =\n  'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex=\"-1\"])'\n\n/** The stop to step over: nothing here focuses the scrollport on purpose. */\nconst CASCADER_TAB_SKIP_SELECTOR = '[data-slot=\"scroll-area-viewport\"]'\n\n/** Out of the tab order whatever they contain; layout is never consulted. */\nconst CASCADER_TAB_HIDDEN_SELECTOR = \"[hidden],[inert],[aria-hidden='true']\"\n\n/** The panel's real keyboard stops, in DOM order. */\nfunction getCascaderTabStops(panel: HTMLElement): HTMLElement[] {\n  const stops = Array.from(\n    panel.querySelectorAll<HTMLElement>(CASCADER_TAB_STOP_SELECTOR)\n  )\n  return stops.filter(\n    (stop) =>\n      // Option and trail rows are real `<button>`s with `tabindex=\"-1\"`.\n      stop.getAttribute(\"tabindex\") !== \"-1\" &&\n      !stop.matches(CASCADER_TAB_SKIP_SELECTOR) &&\n      !stop.closest(CASCADER_TAB_HIDDEN_SELECTOR)\n  )\n}\n\n/** The footer's own stops; an `aria-disabled` command still counts as one. */\nexport function getCascaderFooterStops(scope: HTMLElement): HTMLElement[] {\n  const footer = scope.matches('[data-slot=\"cascader-footer\"]')\n    ? scope\n    : scope.querySelector<HTMLElement>('[data-slot=\"cascader-footer\"]')\n  if (!footer) return []\n  return getCascaderTabStops(footer)\n}\n\n/**\n * Hands focus to the panel's search field, or the popup without one. A Retry\n * button is replaced by the loading state as the retry starts, and a focused\n * node that unmounts drops focus to the body.\n */\nexport function focusCascaderHome(from: HTMLElement): void {\n  const panel = from.closest<HTMLElement>('[data-slot=\"cascader-panel\"]')\n  const home =\n    panel?.querySelector<HTMLElement>('[data-slot=\"cascader-input\"]') ??\n    from.closest<HTMLElement>('[data-slot=\"cascader-content\"]') ??\n    panel\n  // An inline panel with no search field is the last home left: make it\n  // focusable for this, never a tab stop.\n  if (home && home === panel && !home.hasAttribute(\"tabindex\")) {\n    home.setAttribute(\"tabindex\", \"-1\")\n  }\n  home?.focus()\n}\n\n/** Where Tab lands, or `null` to let the browser out: focus is never trapped. */\nexport function getCascaderTabTarget(\n  panel: HTMLElement,\n  from: Element | null,\n  backwards: boolean\n): HTMLElement | null {\n  const stops = getCascaderTabStops(panel)\n  if (stops.length === 0 || !from) return null\n\n  const index = stops.indexOf(from as HTMLElement)\n  if (index !== -1) return stops[index + (backwards ? -1 : 1)] ?? null\n\n  if (backwards) {\n    for (let i = stops.length - 1; i >= 0; i -= 1) {\n      const position = from.compareDocumentPosition(stops[i])\n      if (position & Node.DOCUMENT_POSITION_PRECEDING) return stops[i]\n    }\n    return null\n  }\n\n  for (const stop of stops) {\n    const position = from.compareDocumentPosition(stop)\n    if (position & Node.DOCUMENT_POSITION_FOLLOWING) return stop\n  }\n  return null\n}\n\n/**\n * The writing direction at `element`. `DirectionProvider` may only vote yes:\n * unmounted it answers `\"ltr\"`, which would override a real `<html dir=\"rtl\">`.\n */\nexport function isCascaderRtl(element: Element, provided: string): boolean {\n  if (provided === \"rtl\") return true\n  const explicit = element.closest(\"[dir]\")?.getAttribute(\"dir\")?.toLowerCase()\n  if (explicit === \"rtl\") return true\n  if (explicit === \"ltr\") return false\n  return (\n    element.ownerDocument?.defaultView?.getComputedStyle(element).direction ===\n    \"rtl\"\n  )\n}\n\n/**\n * The paging row as a REAL node, not a DOM-only row after the list: every index\n * Base UI hands out indexes the RENDERED array, so a DOM-only row shifts them.\n */\nexport function createCascaderMoreNode<T = unknown>(\n  parentKey: string,\n  loadedCount = 0\n): CascaderNode<T> {\n  return {\n    value: `${CASCADER_MORE_PREFIX}${parentKey}`,\n    // Empty on purpose: a typeahead letter must never reach the paging row.\n    label: \"\",\n    // Feeds the row's \"Loading more...\" wording, with no second prop to thread.\n    count: loadedCount,\n  }\n}\n\nexport function isCascaderMoreNode<T>(node: CascaderNode<T>): boolean {\n  return node.value.startsWith(CASCADER_MORE_PREFIX)\n}\n\n/** The level key a paging pseudo-node belongs to, or `null` for a real node. */\nexport function getCascaderMoreParent<T>(node: CascaderNode<T>): string | null {\n  if (!isCascaderMoreNode(node)) return null\n  return node.value.slice(CASCADER_MORE_PREFIX.length)\n}\n\n/** Either input shape into one index. Cycle-guarded depths; first wins. */\nexport function buildCascaderIndex<T = unknown>(\n  items: readonly CascaderNode<T>[] | undefined,\n  getParent?: (node: CascaderNode<T>) => string | null | undefined\n): CascaderIndex<T> {\n  const byValue = new Map<string, CascaderNode<T>>()\n  const childrenOf = new Map<string, CascaderNode<T>[]>()\n  const parentOf = new Map<string, string | null>()\n  const depthOf = new Map<string, number>()\n  const all: CascaderNode<T>[] = []\n\n  const push = (parentKey: string, node: CascaderNode<T>) => {\n    const bucket = childrenOf.get(parentKey)\n    if (bucket) {\n      bucket.push(node)\n    } else {\n      childrenOf.set(parentKey, [node])\n    }\n  }\n\n  if (getParent) {\n    for (const node of items ?? []) {\n      // A nullish entry is a data bug: skip it like a duplicate, say so in dev.\n      if (node == null) {\n        if (process.env.NODE_ENV !== \"production\") {\n          warnCascaderOnce(\n            \"nullish-entry\",\n            \"Ignored a null or undefined entry in `items` or `children`. Check the arrays you pass in.\"\n          )\n        }\n        continue\n      }\n      if (byValue.has(node.value)) continue\n      byValue.set(node.value, node)\n      all.push(node)\n    }\n\n    for (const node of all) {\n      const rawParent = getParent(node)\n      // An unknown parent makes the node a root rather than dropping the row.\n      const parent =\n        rawParent != null && byValue.has(rawParent) ? rawParent : null\n      parentOf.set(node.value, parent)\n      push(parent ?? CASCADER_ROOT_KEY, node)\n    }\n\n    for (const node of all) {\n      let depth = 0\n      let cursor = parentOf.get(node.value) ?? null\n      const seen = new Set<string>([node.value])\n      while (cursor != null && !seen.has(cursor)) {\n        seen.add(cursor)\n        depth += 1\n        cursor = parentOf.get(cursor) ?? null\n      }\n      depthOf.set(node.value, depth)\n    }\n  } else {\n    const walk = (\n      nodes: readonly CascaderNode<T>[] | undefined,\n      parent: string | null,\n      depth: number\n    ) => {\n      for (const node of nodes ?? []) {\n        // Same degradation as the flat path above.\n        if (node == null) {\n          if (process.env.NODE_ENV !== \"production\") {\n            warnCascaderOnce(\n              \"nullish-entry\",\n              \"Ignored a null or undefined entry in `items` or `children`. Check the arrays you pass in.\"\n            )\n          }\n          continue\n        }\n        if (byValue.has(node.value)) continue\n        byValue.set(node.value, node)\n        parentOf.set(node.value, parent)\n        depthOf.set(node.value, depth)\n        all.push(node)\n        push(parent ?? CASCADER_ROOT_KEY, node)\n        if (node.children?.length) walk(node.children, node.value, depth + 1)\n      }\n    }\n    walk(items, null, 0)\n  }\n\n  return {\n    byValue,\n    childrenOf,\n    parentOf,\n    depthOf,\n    roots: childrenOf.get(CASCADER_ROOT_KEY) ?? [],\n    all,\n  }\n}\n\n/** One branch-flagged copy per node, so a re-merge keeps row identity. */\nconst BRANCH_COPIES = new WeakMap<object, object>()\n\nfunction asCascaderBranch<T>(node: CascaderNode<T>): CascaderNode<T> {\n  if (node.hasChildren) return node\n  let copy = BRANCH_COPIES.get(node) as CascaderNode<T> | undefined\n  if (!copy) {\n    copy = { ...node, hasChildren: true }\n    BRANCH_COPIES.set(node, copy)\n  }\n  return copy\n}\n\n/**\n * Folds loaded pages into an index built from `items`, so a re-render with a\n * new `items` array keeps what the user drilled into. Static `items` wins;\n * `detached` is `byValue` ONLY, or deep search doubles it and `childrenOf` lies.\n *\n * A resolved chain node in `links` that no page placed still gets its parent,\n * so the trigger keeps its path. While its parent's level is unloaded it is\n * searchable but no row; while that level has pages left (`paging`) it is\n * LISTED, last, and in `pinned`; once the level loaded completely without it\n * (moved, deleted) it is `hidden`: no row, hit or cascade child. A node some\n * link names as a parent reads as a branch, whatever chain wrote it last.\n */\nexport function mergeCascaderIndex<T = unknown>(\n  base: CascaderIndex<T>,\n  pages: ReadonlyMap<string, readonly CascaderNode<T>[]>,\n  detached?: ReadonlyMap<string, CascaderNode<T>>,\n  links?: ReadonlyMap<string, string>,\n  paging?: ReadonlySet<string>\n): CascaderIndex<T> {\n  // Identity stability: every downstream `useMemo` is keyed on this index.\n  if (pages.size === 0 && !detached?.size) return base\n\n  const byValue = new Map(base.byValue)\n  const childrenOf = new Map(base.childrenOf)\n  const parentOf = new Map(base.parentOf)\n  const depthOf = new Map(base.depthOf)\n\n  // Copy-on-write per bucket: only levels that got a page pay for a new array.\n  const copied = new Set<string>()\n  const append = (parentKey: string, node: CascaderNode<T>) => {\n    let bucket = childrenOf.get(parentKey)\n    if (!copied.has(parentKey)) {\n      bucket = bucket ? bucket.slice() : []\n      childrenOf.set(parentKey, bucket)\n      copied.add(parentKey)\n    }\n    bucket!.push(node)\n  }\n\n  const insert = (parentKey: string, nodes: readonly CascaderNode<T>[]) => {\n    for (const node of nodes) {\n      if (byValue.has(node.value)) continue\n      byValue.set(node.value, node)\n      parentOf.set(\n        node.value,\n        parentKey === CASCADER_ROOT_KEY ? null : parentKey\n      )\n      append(parentKey, node)\n      if (node.children?.length) insert(node.value, node.children)\n    }\n  }\n\n  for (const [parentKey, nodes] of pages) insert(parentKey, nodes)\n\n  const pinned = new Set<string>()\n  const hidden = new Set<string>()\n  const linkParents = new Set(links?.values())\n  for (const [value, parentKey] of links ?? []) {\n    const linked = detached?.get(value)\n    if (!linked || byValue.has(value)) continue\n    const node = linkParents.has(value) ? asCascaderBranch(linked) : linked\n    byValue.set(value, node)\n    parentOf.set(value, parentKey === CASCADER_ROOT_KEY ? null : parentKey)\n    if (paging?.has(parentKey)) {\n      append(parentKey, node)\n      pinned.add(value)\n    } else if (pages.has(parentKey) || base.childrenOf.has(parentKey)) {\n      hidden.add(value)\n    }\n  }\n\n  for (const value of byValue.keys()) {\n    if (depthOf.has(value)) continue\n    let depth = 0\n    let cursor = parentOf.get(value) ?? null\n    const seen = new Set<string>([value])\n    while (cursor != null && !seen.has(cursor)) {\n      seen.add(cursor)\n      depth += 1\n      cursor = parentOf.get(cursor) ?? null\n    }\n    depthOf.set(value, depth)\n  }\n\n  const roots = childrenOf.get(CASCADER_ROOT_KEY) ?? []\n\n  // Depth first over the MERGED tree, so `all` keeps document order.\n  const all: CascaderNode<T>[] = []\n  const visited = new Set<string>()\n  const walk = (nodes: readonly CascaderNode<T>[]) => {\n    for (const node of nodes) {\n      if (visited.has(node.value)) continue\n      visited.add(node.value)\n      all.push(node)\n      const children = childrenOf.get(node.value)\n      if (children?.length) walk(children)\n    }\n  }\n  walk(roots)\n\n  // A page whose parent never arrived, or a linked node whose level has not\n  // loaded, stays searchable rather than vanishing.\n  for (const [value, node] of byValue) {\n    if (visited.has(value) || hidden.has(value)) continue\n    visited.add(value)\n    all.push(node)\n  }\n\n  if (detached) {\n    for (const [value, node] of detached) {\n      if (byValue.has(value)) continue\n      byValue.set(value, node)\n    }\n  }\n\n  return { byValue, childrenOf, parentOf, depthOf, roots, all, pinned, hidden }\n}\n\n/** Per index, so a query does not rebuild it. */\nconst LISTED_VALUES = new WeakMap<object, ReadonlySet<string>>()\n\n/**\n * Values reachable from the roots through `childrenOf`: the nodes a level can\n * show. A tree query keeps only these, or a resolved node no level lists left\n * its ancestors expanded over nothing.\n */\nexport function getCascaderListedValues<T>(\n  index: CascaderIndex<T>\n): ReadonlySet<string> {\n  let listed = LISTED_VALUES.get(index)\n  if (listed) return listed\n  const seen = new Set<string>()\n  const walk = (nodes: readonly CascaderNode<T>[]) => {\n    for (const node of nodes) {\n      if (seen.has(node.value)) continue\n      seen.add(node.value)\n      const children = index.childrenOf.get(node.value)\n      if (children?.length) walk(children)\n    }\n  }\n  walk(index.roots)\n  listed = seen\n  LISTED_VALUES.set(index, listed)\n  return listed\n}\n\n/**\n * A level's rows plus its paging row, placed BEFORE the `pinned` rows, which\n * sit last: the first row a page appends then takes the paging row's index,\n * so the highlight Base UI keeps by index lands on it.\n */\nexport function withCascaderMoreRow<T>(\n  rows: readonly CascaderNode<T>[],\n  more: CascaderNode<T>,\n  pinned?: ReadonlySet<string>\n): CascaderNode<T>[] {\n  const at = pinned?.size ? rows.findIndex((row) => pinned.has(row.value)) : -1\n  if (at === -1) return [...rows, more]\n  return [...rows.slice(0, at), more, ...rows.slice(at)]\n}\n\n/** Children of `parent`, or the root level when `parent` is nullish. */\nexport function getCascaderChildren<T>(\n  index: CascaderIndex<T>,\n  parent?: string | null\n): CascaderNode<T>[] {\n  return index.childrenOf.get(parent ?? CASCADER_ROOT_KEY) ?? []\n}\n\n/** A branch has known children, or `hasChildren` for an unfetched level. */\nexport function isCascaderBranch<T>(\n  index: CascaderIndex<T>,\n  node: CascaderNode<T>\n): boolean {\n  if (node.hasChildren) return true\n  return (index.childrenOf.get(node.value)?.length ?? 0) > 0\n}\n\n/** Trailing count for the default row. Explicit `count` wins over the tree. */\nexport function getCascaderCount<T>(\n  index: CascaderIndex<T>,\n  node: CascaderNode<T>\n): number {\n  if (typeof node.count === \"number\") return node.count\n  return index.childrenOf.get(node.value)?.length ?? 0\n}\n\n/**\n * Whether a node is disabled: its own flag, or any ancestor's, since a disabled\n * branch keeps its whole subtree shut. The index's copy counts as its own, so a\n * server hit that omits the flag `items` sets is still caught. Ancestry is\n * `parentOf`: a detached hit the index has never placed answers for itself.\n */\nexport function isCascaderDisabled<T>(\n  index: CascaderIndex<T>,\n  node: CascaderNode<T>\n): boolean {\n  if (node.disabled || index.byValue.get(node.value)?.disabled) return true\n  const seen = new Set<string>([node.value])\n  let cursor = index.parentOf.get(node.value) ?? null\n  while (cursor != null && !seen.has(cursor)) {\n    seen.add(cursor)\n    if (index.byValue.get(cursor)?.disabled) return true\n    cursor = index.parentOf.get(cursor) ?? null\n  }\n  return false\n}\n\n/** Whether a node may be committed as a selection. */\nexport function isCascaderSelectable<T>(\n  index: CascaderIndex<T>,\n  node: CascaderNode<T>,\n  selectable: CascaderSelectable<T>\n): boolean {\n  // Before the branches a consumer controls: `selectable=\"any\"` says yes to\n  // every node, and committing the paging row would select a level's name.\n  if (isCascaderMoreNode(node)) return false\n  // Rows render this answer and their presses, keys, `commit`, `navigate` and\n  // `navigateAt` check it, so a search lists a disabled subtree none of them\n  // opens. The raw setters (`pushLevel`, `setPath`, `toggleExpanded`,\n  // `setSelection`) do not ask.\n  if (isCascaderDisabled(index, node)) return false\n  if (typeof selectable === \"function\") return selectable(node)\n  if (selectable === \"any\") return true\n  return !isCascaderBranch(index, node)\n}\n\n/** Ancestor chain for `value`, root first. Empty while async data loads. */\nexport function getCascaderPath<T>(\n  index: CascaderIndex<T>,\n  value: string | null | undefined\n): CascaderNode<T>[] {\n  if (value == null) return []\n  const chain: CascaderNode<T>[] = []\n  const seen = new Set<string>()\n  let cursor: string | null | undefined = value\n  while (cursor != null && !seen.has(cursor)) {\n    seen.add(cursor)\n    const node = index.byValue.get(cursor)\n    if (!node) break\n    chain.push(node)\n    cursor = index.parentOf.get(cursor) ?? null\n  }\n  return chain.reverse()\n}\n\n/**\n * `toLocaleLowerCase`, not `toLowerCase`: the invariant mapping turns Turkish\n * \"I\" into \"i\" not \"ı\", so \"ışık\" would never match \"IŞIK\". Both sides fold here.\n */\nexport function foldCascaderText(text: string): string {\n  return text.toLocaleLowerCase()\n}\n\n/** Folds once so callers can hoist the cost out of a per-node loop. */\nexport function normalizeCascaderQuery(query: string): string {\n  return foldCascaderText(query.trim())\n}\n\n/** Case-insensitive substring over label and keywords; pre-fold the query. */\nexport function matchesCascaderQuery<T>(\n  node: CascaderNode<T>,\n  normalized: string\n): boolean {\n  if (!normalized) return true\n  // Coerced, not trusted: a label-less node is malformed data, not a crash.\n  if (foldCascaderText(node.label ?? \"\").includes(normalized)) return true\n  if (node.keywords) {\n    for (const keyword of node.keywords) {\n      if (foldCascaderText(keyword).includes(normalized)) return true\n    }\n  }\n  return false\n}\n\n/** Filters one level in place-order. Returns the input when the query is empty. */\nexport function filterCascaderLevel<T>(\n  nodes: readonly CascaderNode<T>[],\n  query: string,\n  matches: (\n    node: CascaderNode<T>,\n    normalized: string\n  ) => boolean = matchesCascaderQuery\n): CascaderNode<T>[] {\n  const normalized = normalizeCascaderQuery(query)\n  if (!normalized) return nodes as CascaderNode<T>[]\n  return nodes.filter((node) => matches(node, normalized))\n}\n\n/** Searches every node, optionally under `within`, in one pass over `all`. */\nexport function searchCascaderDeep<T>(\n  index: CascaderIndex<T>,\n  query: string,\n  options: {\n    within?: string | null\n    limit?: number\n    matches?: (node: CascaderNode<T>, normalized: string) => boolean\n  } = {}\n): CascaderNode<T>[] {\n  const normalized = normalizeCascaderQuery(query)\n  if (!normalized) return []\n\n  const { within, limit = 200, matches = matchesCascaderQuery } = options\n  const results: CascaderNode<T>[] = []\n\n  // One memoised ancestry map per query. The provisional `false` written on the\n  // way up doubles as the cycle guard; the trail is promoted once `within` hits.\n  const ancestry = within == null ? null : new Map<string, boolean>()\n  const isWithin = (node: CascaderNode<T>) => {\n    if (within == null || !ancestry) return true\n    const trail: string[] = []\n    let answer = false\n    let cursor: string | null | undefined = index.parentOf.get(node.value)\n    while (cursor != null) {\n      if (cursor === within) {\n        answer = true\n        break\n      }\n      const cached = ancestry.get(cursor)\n      if (cached !== undefined) {\n        answer = cached\n        break\n      }\n      ancestry.set(cursor, false)\n      trail.push(cursor)\n      cursor = index.parentOf.get(cursor) ?? null\n    }\n    if (answer) for (const value of trail) ancestry.set(value, true)\n    return answer\n  }\n\n  for (const node of index.all) {\n    if (results.length >= limit) break\n    if (!matches(node, normalized)) continue\n    if (!isWithin(node)) continue\n    results.push(node)\n  }\n\n  return results\n}\n\n/**\n * Flattens the tree into tree mode's visible rows. A `sentinels` paging row\n * COUNTS as a sibling in `aria-setsize`: it holds an index of its own.\n */\nexport function flattenCascaderTree<T>(\n  index: CascaderIndex<T>,\n  expanded: ReadonlySet<string>,\n  sentinels?: ReadonlySet<string>\n): CascaderFlatNode<T>[] {\n  const rows: CascaderFlatNode<T>[] = []\n\n  const walk = (\n    nodes: readonly CascaderNode<T>[],\n    parentKey: string,\n    depth: number\n  ) => {\n    const items = sentinels?.has(parentKey)\n      ? withCascaderMoreRow(\n          nodes,\n          createCascaderMoreNode<T>(parentKey, nodes.length),\n          index.pinned\n        )\n      : nodes\n    const setSize = items.length\n    for (let i = 0; i < items.length; i += 1) {\n      const node = items[i]\n      if (isCascaderMoreNode(node)) {\n        rows.push({\n          node,\n          depth,\n          branch: false,\n          expanded: false,\n          setSize,\n          posInSet: i + 1,\n        })\n        continue\n      }\n      const branch = isCascaderBranch(index, node)\n      const isExpanded = branch && expanded.has(node.value)\n      rows.push({\n        node,\n        depth,\n        branch,\n        expanded: isExpanded,\n        setSize,\n        posInSet: i + 1,\n      })\n      if (isExpanded) {\n        walk(index.childrenOf.get(node.value) ?? [], node.value, depth + 1)\n      }\n    }\n  }\n\n  walk(index.roots, CASCADER_ROOT_KEY, 0)\n  return rows\n}\n\n/** Shortens a path; the ellipsis segment still carries the hidden nodes. */\nexport function collapseCascaderPath<T>(\n  path: readonly CascaderNode<T>[],\n  options: { maxSegments?: number; collapse?: CascaderCollapse } = {}\n): CascaderPathSegment<T>[] {\n  const { maxSegments = 3, collapse = \"middle\" } = options\n\n  const asNodes = (\n    nodes: readonly CascaderNode<T>[]\n  ): CascaderPathSegment<T>[] =>\n    nodes.map((node) => ({ type: \"node\" as const, node }))\n\n  if (collapse === \"none\" || maxSegments <= 0 || path.length <= maxSegments) {\n    return asNodes(path)\n  }\n\n  if (collapse === \"start\") {\n    const tail = path.slice(path.length - maxSegments)\n    return [\n      { type: \"ellipsis\", hidden: path.slice(0, path.length - maxSegments) },\n      ...asNodes(tail),\n    ]\n  }\n\n  // \"middle\": keep the root for orientation and as much of the tail as fits.\n  const head = path.slice(0, 1)\n  const tailCount = maxSegments - 1\n  const tail = path.slice(path.length - tailCount)\n  const hidden = path.slice(1, path.length - tailCount)\n\n  if (hidden.length === 0) return asNodes(path)\n\n  return [...asNodes(head), { type: \"ellipsis\", hidden }, ...asNodes(tail)]\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              Cascade selection                             */\n/* -------------------------------------------------------------------------- */\n\n/** Per index, so a press does not rebuild it. */\nconst SELECTION_CHILDREN = new WeakMap<object, Map<string, unknown[]>>()\n\n/**\n * Children by parent, built from `parentOf`. Unlike `childrenOf` it also holds\n * a resolved chain node whose level has not loaded, which the selection walks\n * must see: unchecking an ancestor has to take it along. A `hidden` one is left\n * out, since its level loaded without it.\n */\nfunction getSelectionChildren<T>(\n  index: CascaderIndex<T>\n): Map<string, CascaderNode<T>[]> {\n  let map = SELECTION_CHILDREN.get(index) as\n    Map<string, CascaderNode<T>[]> | undefined\n  if (map) return map\n  map = new Map()\n  for (const [value, parent] of index.parentOf) {\n    const node = index.byValue.get(value)\n    if (!node || index.hidden?.has(value)) continue\n    const key = parent ?? CASCADER_ROOT_KEY\n    const bucket = map.get(key)\n    if (bucket) bucket.push(node)\n    else map.set(key, [node])\n  }\n  SELECTION_CHILDREN.set(index, map)\n  return map\n}\n\n/**\n * Every KNOWN node under `value`, root first: loaded, or on a resolved chain.\n * Reaching an unfetched descendant would mean a network request per level of\n * the subtree on every selection.\n */\nexport function collectCascaderSubtree<T>(\n  index: CascaderIndex<T>,\n  value: string\n): CascaderNode<T>[] {\n  const root = index.byValue.get(value)\n  if (!root) return []\n\n  const childrenOf = getSelectionChildren(index)\n  const out: CascaderNode<T>[] = []\n  const seen = new Set<string>()\n  const walk = (node: CascaderNode<T>) => {\n    if (seen.has(node.value)) return\n    seen.add(node.value)\n    out.push(node)\n    const children = childrenOf.get(node.value)\n    if (children) for (const child of children) walk(child)\n  }\n  walk(root)\n  return out\n}\n\n/**\n * Toggles `value` with its known subtree (loaded, or on a resolved chain), then\n * reconciles ancestors. One flat array, no hidden \"checked\" state: a branch is\n * in the selection exactly when every selectable known child of it is, which\n * keeps a row's checked state an O(1) lookup. Nodes `isSelectable` rejects are skipped down AND ignored up,\n * the pressed node excepted.\n */\nexport function applyCascadeSelection<T>(\n  index: CascaderIndex<T>,\n  selected: readonly string[],\n  value: string,\n  select: boolean,\n  isSelectable: (node: CascaderNode<T>) => boolean = () => true\n): string[] {\n  const next = new Set(selected)\n  const subtree = collectCascaderSubtree(index, value)\n\n  // An unknown value still toggles itself, so an early async selection lands.\n  if (subtree.length === 0) {\n    if (select) next.add(value)\n    else next.delete(value)\n  }\n\n  for (const node of subtree) {\n    // The pressed node is exempt: it was committed, so it is selectable.\n    if (node.value !== value && !isSelectable(node)) continue\n    if (select) next.add(node.value)\n    else next.delete(node.value)\n  }\n\n  // Bottom up: a parent can only answer once its children have.\n  const seen = new Set<string>([value])\n  let cursor = index.parentOf.get(value) ?? null\n  while (cursor != null && !seen.has(cursor)) {\n    seen.add(cursor)\n    const parent = index.byValue.get(cursor)\n    const children = getSelectionChildren(index).get(cursor) ?? []\n    const selectable = children.filter((child) => isSelectable(child))\n    const full =\n      selectable.length > 0 &&\n      selectable.every((child) => next.has(child.value)) &&\n      // Never promote a node the consumer said may not be committed. Under\n      // `selectable=\"leaf\"` no branch qualifies, hence the `cascade` warning.\n      !!parent &&\n      isSelectable(parent)\n    if (full) next.add(cursor)\n    else next.delete(cursor)\n    cursor = index.parentOf.get(cursor) ?? null\n  }\n\n  return Array.from(next)\n}\n\n/**\n * How many selected nodes each value has BELOW it, itself excluded. Walks UP\n * from each selection: a per-row subtree scan would be quadratic and re-paid.\n */\nexport function getCascaderSelectedDescendants<T>(\n  index: CascaderIndex<T>,\n  selected: readonly string[]\n): Map<string, number> {\n  const counts = new Map<string, number>()\n\n  for (const value of selected) {\n    const seen = new Set<string>([value])\n    let cursor = index.parentOf.get(value) ?? null\n    while (cursor != null && !seen.has(cursor)) {\n      seen.add(cursor)\n      counts.set(cursor, (counts.get(cursor) ?? 0) + 1)\n      cursor = index.parentOf.get(cursor) ?? null\n    }\n  }\n\n  return counts\n}\n\n/** The PARTIALLY selected values, read off counts the caller already holds. */\nexport function getCascaderIndeterminateFrom(\n  counts: ReadonlyMap<string, number>,\n  selected: readonly string[]\n): Set<string> {\n  const selectedSet = new Set(selected)\n  const partial = new Set<string>()\n\n  for (const [value, count] of counts) {\n    if (count > 0 && !selectedSet.has(value)) partial.add(value)\n  }\n\n  return partial\n}\n\n/** The one-call form; the root uses the two halves above and its own counts. */\nexport function getCascaderIndeterminate<T>(\n  index: CascaderIndex<T>,\n  selected: readonly string[]\n): Set<string> {\n  return getCascaderIndeterminateFrom(\n    getCascaderSelectedDescendants(index, selected),\n    selected\n  )\n}\n\n/** How `getCascaderCheckedValues` condenses a full-closure selection. */\nexport type CascaderCheckedStrategy = \"all\" | \"parent\" | \"child\"\n\n/**\n * The cascade selection under a reporting strategy. DERIVED OUTPUT ONLY: the\n * STORED value stays the full closure, which keeps a checked state an O(1)\n * lookup. `\"all\"` is the closure; `\"parent\"` drops a value whose parent is\n * selected; `\"child\"` drops one with a selected child.\n */\nexport function getCascaderCheckedValues<T>(\n  index: CascaderIndex<T>,\n  selected: readonly string[],\n  strategy: CascaderCheckedStrategy\n): readonly string[] {\n  if (strategy === \"all\") return selected\n\n  const set = new Set(selected)\n  if (strategy === \"parent\") {\n    return selected.filter((value) => {\n      const parent = index.parentOf.get(value)\n      return parent == null || !set.has(parent)\n    })\n  }\n\n  // Parents of a selection, from `parentOf`: it holds the resolved chain\n  // nodes no loaded level lists, which `childrenOf` misses.\n  const withSelectedChild = new Set<string>()\n  for (const value of selected) {\n    if (index.hidden?.has(value)) continue\n    const parent = index.parentOf.get(value)\n    if (parent != null) withSelectedChild.add(parent)\n  }\n  return selected.filter((value) => !withSelectedChild.has(value))\n}\n\n/** Values in `nodes` whose label collides, so the chip must show its path. */\nexport function findAmbiguousCascaderLabels<T>(\n  nodes: readonly CascaderNode<T>[]\n): Set<string> {\n  const firstByLabel = new Map<string, string>()\n  const ambiguous = new Set<string>()\n\n  for (const node of nodes) {\n    const first = firstByLabel.get(node.label)\n    if (first === undefined) {\n      firstByLabel.set(node.label, node.value)\n      continue\n    }\n    ambiguous.add(node.value)\n    ambiguous.add(first)\n  }\n\n  return ambiguous\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              Development only                              */\n/* -------------------------------------------------------------------------- */\n\n/** Warnings already emitted. Module scoped, or they repeat once per render. */\nconst CASCADER_WARNED = new Set<string>()\n\n/** Warns once per `key`, never in production, never by throwing. */\nexport function warnCascaderOnce(key: string, message: string): void {\n  if (process.env.NODE_ENV === \"production\") return\n  if (CASCADER_WARNED.has(key)) return\n  CASCADER_WARNED.add(key)\n  console.warn(`[Cascader] ${message}`)\n}\n\n/** Empties the ledger. Tests only; a warning is meant to be seen once. */\nexport function resetCascaderWarnings(): void {\n  CASCADER_WARNED.clear()\n}\n\n/** What a dev-time scan of the consumer's `items` found wrong with it. */\nexport interface CascaderDataIssues {\n  /** Values appearing more than once. First wins, so a duplicate drops a row. */\n  duplicates: string[]\n  /** Values on a `getParent` cycle. Depth is clamped, so the tree reads wrong. */\n  cycles: string[]\n}\n\n/** Dev-time scan, kept out of `buildCascaderIndex` so the build stays hot. */\nexport function findCascaderDataIssues<T>(\n  items: readonly CascaderNode<T>[] | undefined,\n  getParent?: (node: CascaderNode<T>) => string | null | undefined\n): CascaderDataIssues {\n  const seen = new Set<string>()\n  const duplicates = new Set<string>()\n  const flat: CascaderNode<T>[] = []\n\n  const visit = (nodes: readonly CascaderNode<T>[] | undefined) => {\n    for (const node of nodes ?? []) {\n      // The build skips a nullish entry, and this runs BEFORE its warning.\n      if (node == null) continue\n      if (seen.has(node.value)) duplicates.add(node.value)\n      else seen.add(node.value)\n      flat.push(node)\n      // Flat mode walks them too: BOTH `children` and `getParent` is a bug.\n      if (node.children?.length) visit(node.children)\n    }\n  }\n  visit(items)\n\n  const cycles: string[] = []\n  if (getParent) {\n    const parentOf = new Map<string, string | null>()\n    for (const node of flat) {\n      if (parentOf.has(node.value)) continue\n      const raw = getParent(node)\n      parentOf.set(node.value, raw != null && seen.has(raw) ? raw : null)\n    }\n    for (const node of flat) {\n      const walked = new Set<string>([node.value])\n      let cursor = parentOf.get(node.value) ?? null\n      while (cursor != null) {\n        if (walked.has(cursor)) {\n          cycles.push(node.value)\n          break\n        }\n        walked.add(cursor)\n        cursor = parentOf.get(cursor) ?? null\n      }\n    }\n  }\n\n  return { duplicates: Array.from(duplicates), cycles }\n}\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-lib.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader-nav.tsx",
      "content": "/**\n * cascader-nav - adapted for tealmac UI from ReUI \"cascader/cascader-nav\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\n\"use client\"\n\nimport * as React from \"react\"\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\"\nimport { useDirection } from \"@base-ui/react/direction-provider\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  useCascaderActions,\n  useCascaderState,\n} from \"@/components/reui/cascader/cascader-context\"\nimport { resolveCascaderSearchLabel } from \"@/components/reui/cascader/cascader-i18n\"\nimport {\n  collapseCascaderPath,\n  getCascaderFooterStops,\n  getCascaderPath,\n  isCascaderRtl,\n} from \"@/components/reui/cascader/cascader-lib\"\nimport type {\n  CascaderCollapse,\n  CascaderNode,\n  CascaderValueDisplay,\n} from \"@/components/reui/cascader/cascader-types\"\n\n/* -------------------------------------------------------------------------- */\n/*                                  Separator                                 */\n/* -------------------------------------------------------------------------- */\n\nfunction PathChevron() {\n  return (\n    <ChevronRightIcon className=\"size-3 shrink-0 opacity-50 rtl:-scale-x-100\" />\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                            asChild for built parts                         */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The child element `Slot` should be handed, with this part's own content\n * already cloned inside it, REPLACING that element's own children.\n *\n * `CascaderBreadcrumb` and `CascaderValue` BUILD their children, so handing\n * `Slot` that content directly deleted the part: Radix clones its child to\n * merge props onto it, and a Fragment or an array rejects them. Answers `null`\n * for anything that is not a single element, so the caller falls back to its\n * default tag rather than vanishing.\n *\n * The base twin ships no counterpart: Base UI's `useRender` already splits the\n * element (`render`) from the content (the merged `children`), and this is the\n * same split expressed the Radix way.\n */\nfunction cascaderSlotChild(\n  child: React.ReactNode,\n  content: React.ReactNode\n): React.ReactElement | null {\n  if (!React.isValidElement(child)) return null\n  return React.cloneElement(\n    child as React.ReactElement<{ children?: React.ReactNode }>,\n    undefined,\n    content\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                     Nav                                    */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderNavProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean\n}\n\n/**\n * Header: back control, search input, and the separator under them. The\n * breadcrumb belongs with the list below: it describes the rows, not the field.\n */\nfunction CascaderNav({\n  className,\n  asChild = false,\n  children,\n  ...props\n}: CascaderNavProps) {\n  const Comp = asChild ? Slot : \"div\"\n\n  return (\n    <Comp\n      data-slot=\"cascader-nav\"\n      className={cn(\n        /* `py-1`, not `py-1.5`: the row inside sets its own per-style height,\n           so even padding read too tall. 6px beside the field, 4px above and\n           below. */\n        \"border-border/60 flex shrink-0 flex-col gap-1 border-b px-1.5 py-1\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </Comp>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Back                                    */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderBackProps extends Omit<\n  React.ComponentProps<\"button\">,\n  \"children\"\n> {\n  asChild?: boolean\n  children?: React.ReactNode\n}\n\n/** Pops one level. Renders nothing at the root, so no dead header space. */\nfunction CascaderBack({\n  className,\n  asChild = false,\n  children,\n  ...props\n}: CascaderBackProps) {\n  const { popLevel, labels, mode } = useCascaderActions()\n  const { path } = useCascaderState()\n\n  // Returning early is safe only because the context read above is this\n  // component's last hook: one below this line would change the hook count\n  // between the root level and any deeper one.\n  const hidden = mode !== \"drill\" || path.length === 0\n  if (hidden) return null\n\n  const Comp = asChild ? Slot : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"cascader-back\"\n      type=\"button\"\n      aria-label={labels.back}\n      onClick={() => popLevel()}\n      className={cn(\n        \"text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 flex shrink-0 items-center justify-center rounded-md outline-hidden transition-colors focus-visible:ring-2\",\n        /* A notch under the row height: an affordance, not a second field. */\n        \"style-vega:size-6 style-nova:size-6 style-maia:size-7 style-lyra:size-6 style-mira:size-5 style-luma:size-6 style-rhea:size-6 style-sera:size-6\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? <ChevronLeftIcon className=\"size-4 rtl:-scale-x-100\" />}\n    </Comp>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                 Breadcrumb                                 */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderBreadcrumbProps extends Omit<\n  React.ComponentProps<\"nav\">,\n  \"children\"\n> {\n  /** Maximum visible node segments before the middle collapses. */\n  maxSegments?: number\n  collapse?: CascaderCollapse\n  /** Clicking a segment navigates back to that level. Defaults to true. */\n  interactive?: boolean\n  asChild?: boolean\n  /**\n   * With `asChild`, the single element the trail renders into: the trail is\n   * built here and becomes that element's children. Ignored without `asChild`.\n   */\n  children?: React.ReactElement\n}\n\n/**\n * Compact trail of the current level AND its ancestors, collapsed by the same\n * helper as `CascaderValue` so panel and trigger cannot disagree. The current\n * level IS the last crumb: dropping it read as a rendering bug, a trail naming\n * only the places you are not.\n */\nfunction CascaderBreadcrumb({\n  className,\n  maxSegments = 3,\n  collapse = \"middle\",\n  interactive = true,\n  asChild = false,\n  children,\n  ...props\n}: CascaderBreadcrumbProps) {\n  const { goToDepth, mode, labels } = useCascaderActions()\n  const { path, index } = useCascaderState()\n\n  // The WHOLE path, current level included; unresolvable entries drop out.\n  const nodes = path\n    .map((value) => index.byValue.get(value))\n    .filter(Boolean) as CascaderNode[]\n  const segments = collapseCascaderPath(nodes, { maxSegments, collapse })\n  // `collapseCascaderPath` never collapses the last node away, so this is\n  // always the final segment - derived rather than assumed, so a `collapse`\n  // mode that ever changes that cannot mark an ancestor as the current page.\n  const currentValue = nodes.length ? nodes[nodes.length - 1].value : null\n\n  // See `CascaderBack`: no hook may run below this early return.\n  const hidden = mode !== \"drill\" || nodes.length === 0\n  if (hidden) return null\n\n  const goTo = (node: CascaderNode) => {\n    const depth = path.indexOf(node.value)\n    if (depth < 0) return\n    // `goToDepth`, not a bare `setPath`: it reports `reason: \"breadcrumb\"`,\n    // clears the query and drops any pending navigation.\n    goToDepth(depth + 1)\n  }\n\n  const trail = segments.map((segment, i) => (\n    <React.Fragment\n      key={segment.type === \"node\" ? segment.node.value : `gap-${i}`}\n    >\n      {i > 0 ? <PathChevron /> : null}\n      {segment.type === \"ellipsis\" ? (\n        <span\n          data-slot=\"cascader-breadcrumb-ellipsis\"\n          title={segment.hidden\n            .map((n) => n.label)\n            .join(` ${labels.pathSeparator} `)}\n          className=\"shrink-0\"\n        >\n          &hellip;\n        </span>\n      ) : interactive && segment.node.value !== currentValue ? (\n        <button\n          type=\"button\"\n          data-slot=\"cascader-breadcrumb-item\"\n          onClick={() => goTo(segment.node)}\n          className=\"hover:text-foreground focus-visible:ring-ring/50 max-w-32 truncate rounded-sm outline-hidden transition-colors focus-visible:ring-2\"\n        >\n          {segment.node.label}\n        </button>\n      ) : (\n        <span\n          data-slot=\"cascader-breadcrumb-item\"\n          /* The level on screen: a span because it goes nowhere, plus\n             `aria-current=\"page\"` so it is not read as one of a flat list. */\n          {...(segment.node.value === currentValue\n            ? { \"aria-current\": \"page\" as const }\n            : null)}\n          className={cn(\n            \"max-w-32 truncate\",\n            segment.node.value === currentValue && \"text-foreground font-medium\"\n          )}\n        >\n          {segment.node.label}\n        </span>\n      )}\n    </React.Fragment>\n  ))\n\n  // See `cascaderSlotChild`: the trail is built here, so `asChild` works only\n  // if the consumer's element receives it.\n  const slotChild = asChild ? cascaderSlotChild(children, trail) : null\n\n  const shared = {\n    \"data-slot\": \"cascader-breadcrumb\",\n    \"aria-label\": labels.breadcrumbLabel,\n    className: cn(\n      \"text-muted-foreground flex min-w-0 shrink-0 items-center gap-0.5 pt-1.5 pb-0.5 text-xs\",\n      // Lines up with the ROWS, not the header: each style's list padding plus\n      // its row inset. vega/mira/rhea `p-1`+`pl-2`, nova `p-1`+`pl-1.5`, maia\n      // `p-1`+`pl-3`, lyra `pl-2`, luma/sera `p-1.5`+`pl-3`, per style-*.css.\n      \"style-vega:px-3 style-nova:px-2.5 style-maia:px-4 style-lyra:px-2 style-mira:px-3 style-luma:px-4.5 style-rhea:px-3 style-sera:px-4.5\",\n      className\n    ),\n    ...props,\n  }\n\n  return slotChild ? (\n    <Slot {...shared}>{slotChild}</Slot>\n  ) : (\n    <nav {...shared}>{trail}</nav>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Input                                   */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderInputProps extends ComboboxPrimitive.Input.Props {\n  /** Renders the back control inline, before the field. Defaults to true. */\n  showBack?: boolean\n}\n\n/** Base UI hands its input handlers an event carrying the veto hook. */\ntype CascaderInputKeyEvent = Parameters<\n  NonNullable<ComboboxPrimitive.Input.Props[\"onKeyDown\"]>\n>[0]\n\n/**\n * Search field. MUST render inside the positioner: only there does Base UI skip\n * the refill from the committed selection that would fight every level swap.\n */\nfunction CascaderInput({\n  className,\n  showBack = true,\n  placeholder,\n  onKeyDown,\n  \"aria-describedby\": ariaDescribedBy,\n  ...props\n}: CascaderInputProps) {\n  const {\n    labels,\n    popLevel,\n    mode,\n    getHighlighted,\n    isBranch,\n    isDisabled,\n    navigate,\n    index,\n    toggleExpanded,\n    inline,\n    invalid,\n    baseId,\n    searchScope,\n  } = useCascaderActions()\n  const direction = useDirection()\n  const { currentParent, query, path, expanded, renderedItems, treeRows } =\n    useCascaderState()\n\n  // Root wording under a global search: naming the level on screen would\n  // promise a narrower search than the one that runs.\n  const resolvedPlaceholder =\n    placeholder ??\n    resolveCascaderSearchLabel(\n      labels,\n      searchScope === \"global\" ? undefined : currentParent?.label\n    )\n\n  // Not `showBack`: `CascaderBack` renders nothing at the root or outside\n  // drill mode, where the field's own leading `px-1.5` is wanted. Beside the\n  // button it stacks with the row's `gap-1` into a 10px hole, hence `ps-0`.\n  const backVisible = showBack && mode === \"drill\" && path.length > 0\n\n  const hintId = `${baseId}-hint`\n  // Base UI names no list while inline; columns mode moves the real listbox to\n  // the deepest panel, which is the `path.length` one.\n  const listId = `${baseId}-column-${mode === \"columns\" ? path.length : 0}`\n\n  // The direction the HINT is worded for. `handleKeyDown` re-reads\n  // `isCascaderRtl` per keystroke; this is text on screen before any key, so\n  // the same check runs once against the mounted DOM (on the hint span, so no\n  // second ref into Base UI's input). SSR renders the \"ltr\" default and\n  // corrects on hydration. `direction` is the only dep: a `dir` attribute and\n  // the stylesheet are declarations, not state, so nothing to resubscribe to.\n  const hintRef = React.useRef<HTMLSpanElement>(null)\n  const [hintDir, setHintDir] = React.useState<\"ltr\" | \"rtl\">(\"ltr\")\n  React.useLayoutEffect(() => {\n    const hint = hintRef.current\n    if (!hint) return\n    setHintDir(isCascaderRtl(hint, direction) ? \"rtl\" : \"ltr\")\n  }, [direction])\n\n  /**\n   * Moves the highlight to `targetIndex` in `treeRows`. Base UI has no\n   * imperative setter (`actionsRef` is `{ unmount }`), so the move is arrow\n   * presses, sound only because `useListNavigation` fires `onItemHighlighted`\n   * SYNCHRONOUSLY per keydown. A press that fails to close the gap ends it.\n   */\n  const moveHighlightTo = (field: HTMLInputElement, targetIndex: number) => {\n    const rowIndex = () => {\n      const highlighted = getHighlighted()\n      if (!highlighted) return -1\n      return treeRows.findIndex((row) => row.node.value === highlighted.value)\n    }\n\n    let current = rowIndex()\n    for (let step = 0; step < treeRows.length; step += 1) {\n      if (current === -1 || current === targetIndex) return\n      const key = current > targetIndex ? \"ArrowUp\" : \"ArrowDown\"\n      field.dispatchEvent(\n        new KeyboardEvent(\"keydown\", { key, bubbles: true, cancelable: true })\n      )\n      const next = rowIndex()\n      const progressed =\n        key === \"ArrowUp\"\n          ? next < current && next >= targetIndex\n          : next > current && next <= targetIndex\n      if (next === -1 || !progressed) return\n      current = next\n    }\n  }\n\n  /**\n   * The tree pattern's two level keys; Base UI leaves both alone (list\n   * navigation is vertical-only, the chip handler no-ops without\n   * `Combobox.Chips`). Passed in rather than hardcoded because RTL swaps them:\n   * APG defines them as \"toward the children\", not as a physical arrow.\n   */\n  const handleTreeKeyDown = (\n    event: CascaderInputKeyEvent,\n    field: HTMLInputElement,\n    caretAtStart: boolean,\n    caretAtEnd: boolean,\n    forwardKey: \"ArrowLeft\" | \"ArrowRight\",\n    backKey: \"ArrowLeft\" | \"ArrowRight\"\n  ) => {\n    const forward = event.key === forwardKey\n    if (!forward && event.key !== backKey) return\n    if (forward ? !caretAtEnd : !caretAtStart) return\n\n    const highlighted = getHighlighted()\n    if (!highlighted) return\n    const rowIndex = treeRows.findIndex(\n      (row) => row.node.value === highlighted.value\n    )\n    if (rowIndex < 0) return\n    const row = treeRows[rowIndex]\n\n    if (forward) {\n      if (!row.branch) return\n      event.preventDefault()\n      if (!row.expanded) {\n        // `navigate`, not `toggleExpanded`: it waits for an unloaded branch's\n        // children, so the keyboard path in matches the expander.\n        navigate(row.node)\n        return\n      }\n      // `flattenCascaderTree` emits children right after their parent, so the\n      // first child is the next row. The depth check covers an empty branch.\n      const child = treeRows[rowIndex + 1]\n      if (child && child.depth === row.depth + 1) {\n        moveHighlightTo(field, rowIndex + 1)\n      }\n      return\n    }\n\n    // A query holds every hit's ancestors open without adding them to\n    // `expanded`, so the toggle would ADD such a row: it would open a disabled\n    // one for good. That row moves to its parent instead, as a closed one does.\n    const heldByQuery = isDisabled(row.node) && !expanded.has(row.node.value)\n    if (row.branch && row.expanded && !heldByQuery) {\n      event.preventDefault()\n      toggleExpanded(row.node.value)\n      return\n    }\n\n    const parentValue = index.parentOf.get(row.node.value)\n    if (!parentValue) return\n    const parentIndex = treeRows.findIndex(\n      (entry) => entry.node.value === parentValue\n    )\n    if (parentIndex < 0) return\n    event.preventDefault()\n    moveHighlightTo(field, parentIndex)\n  }\n\n  const handleKeyDown = (event: CascaderInputKeyEvent) => {\n    onKeyDown?.(event)\n    if (event.defaultPrevented) return\n\n    const field = event.currentTarget\n    const caretAtStart = field.selectionStart === 0 && field.selectionEnd === 0\n    const caretAtEnd =\n      field.selectionStart === query.length &&\n      field.selectionEnd === query.length\n\n    // ArrowDown at the END of the list hands real focus to the pinned footer,\n    // the key a combobox is actually navigated with; Tab still works and still\n    // skips the scroll area. An EMPTY list hands off at once, and the footer\n    // owns the way back. Counted from state, so a windowed list still answers.\n    if (event.key === \"ArrowDown\" && !event.altKey) {\n      const rowCount = mode === \"tree\" ? treeRows.length : renderedItems.length\n      const lastValue =\n        mode === \"tree\"\n          ? treeRows[rowCount - 1]?.node.value\n          : renderedItems[rowCount - 1]?.value\n      const highlighted = getHighlighted()\n      const atEnd =\n        rowCount === 0 ||\n        (highlighted != null && highlighted.value === lastValue)\n      if (atEnd) {\n        const panel = field.closest<HTMLElement>('[data-slot=\"cascader-panel\"]')\n        const stop = panel ? getCascaderFooterStops(panel)[0] : undefined\n        if (stop) {\n          event.preventDefault()\n          // Base UI's handler is deliberately NOT vetoed: its wrap's first\n          // press CLEARS the highlight, so no stale row stays active behind the\n          // focused command, and the emptied highlight completes the ring.\n          stop.focus()\n          return\n        }\n      }\n    }\n\n    // The level keys are LOGICAL, not physical: in RTL, ArrowLeft opens a\n    // branch and ArrowRight goes back. They act only at the caret edge, and\n    // those guards do NOT mirror: `selectionStart === 0` is the logical start.\n    const rtl = isCascaderRtl(field, direction)\n    const forwardKey = rtl ? \"ArrowLeft\" : \"ArrowRight\"\n    const backKey = rtl ? \"ArrowRight\" : \"ArrowLeft\"\n\n    if (mode === \"tree\") {\n      handleTreeKeyDown(\n        event,\n        field,\n        caretAtStart,\n        caretAtEnd,\n        forwardKey,\n        backKey\n      )\n      return\n    }\n\n    if (event.key === forwardKey && caretAtEnd) {\n      const highlighted = getHighlighted()\n      if (highlighted && isBranch(highlighted)) {\n        event.preventDefault()\n        navigate(highlighted)\n        return\n      }\n    }\n\n    // Backspace on an empty query pops a level too, so the keyboard way out of\n    // a level is symmetric with typing into it.\n    if (\n      (event.key === backKey && caretAtStart) ||\n      (event.key === \"Backspace\" && query.length === 0)\n    ) {\n      if (path.length > 0) {\n        event.preventDefault()\n        popLevel()\n      }\n    }\n  }\n\n  return (\n    <div\n      data-slot=\"cascader-input-row\"\n      className={cn(\n        \"flex shrink-0 items-center gap-1\",\n        /* Each style sizes a combobox search field only as a DIRECT child of\n           the popup; this row is nested, so the ladder is mirrored here rather\n           than hardcoding one height for all eight styles. */\n        \"style-vega:h-8 style-nova:h-8 style-maia:h-9 style-lyra:h-8 style-mira:h-7 style-luma:h-8 style-rhea:h-8 style-sera:h-8\"\n      )}\n    >\n      {showBack ? <CascaderBack /> : null}\n      <ComboboxPrimitive.Input\n        data-slot=\"cascader-input\"\n        placeholder={resolvedPlaceholder}\n        onKeyDown={handleKeyDown}\n        /* Nothing on screen says the level keys exist. */\n        aria-describedby={[ariaDescribedBy, hintId].filter(Boolean).join(\" \")}\n        /* Conditional: an explicit `undefined` deletes Base UI's own value. */\n        {...(mode === \"tree\" ? { \"aria-haspopup\": \"tree\" as const } : null)}\n        /* Base UI omits BOTH `aria-expanded` and `aria-controls` while inline,\n           and an embedded panel is permanently expanded, so supply both. */\n        {...(inline\n          ? { \"aria-expanded\": true, \"aria-controls\": listId }\n          : null)}\n        /* No trigger to carry the invalid state in the `inline` case. */\n        {...(invalid ? { \"aria-invalid\": true, \"data-invalid\": \"\" } : null)}\n        className={cn(\n          \"placeholder:text-muted-foreground h-full w-full min-w-0 flex-1 bg-transparent px-1.5 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\",\n          backVisible && \"ps-0\",\n          className\n        )}\n        {...props}\n      />\n      <span id={hintId} ref={hintRef} className=\"sr-only\">\n        {/* `hintDir`, not `direction`: the context alone misses an RTL app that\n            uses a `dir` attribute instead of `DirectionProvider`. */}\n        {labels.keyboardHint(mode, hintDir)}\n      </span>\n    </div>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Value                                   */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderValueProps extends Omit<\n  React.ComponentProps<\"span\">,\n  \"children\"\n> {\n  /** `path` shows the full trail, `leaf` only the node, `count` only a total. */\n  display?: CascaderValueDisplay\n  maxSegments?: number\n  collapse?: CascaderCollapse\n  separator?: React.ReactNode\n  /** Renders the selected node's icon before the trail. */\n  showIcon?: boolean\n  placeholder?: React.ReactNode\n  asChild?: boolean\n  /**\n   * A function replaces the whole rendering, receiving the resolved selection\n   * and path; with `asChild`, the element the display renders into instead.\n   */\n  children?:\n    | ((selected: CascaderNode[], path: CascaderNode[]) => React.ReactNode)\n    | React.ReactElement\n}\n\n/**\n * Trigger display. Defaults to the collapsed selection path rather than a bare\n * leaf label, which in a nested picker is frequently ambiguous.\n */\nfunction CascaderValue({\n  className,\n  display = \"path\",\n  maxSegments = 3,\n  collapse = \"middle\",\n  separator,\n  showIcon = true,\n  placeholder,\n  asChild = false,\n  children,\n  ...props\n}: CascaderValueProps) {\n  const { labels, multiple, resolveNode } = useCascaderActions()\n  const { index, selectedValues } = useCascaderState()\n\n  // `resolveNode`, not `index.byValue`: a selection missing from `items` (async\n  // children, a removed item) must still render a label, not an empty trigger.\n  const selected = selectedValues.map(resolveNode)\n\n  const resolvedPath =\n    selectedValues.length === 1 ? getCascaderPath(index, selectedValues[0]) : []\n  // No ancestor chain: fall back to the node so `display=\"path\"` shows a leaf.\n  const path =\n    resolvedPath.length > 0\n      ? resolvedPath\n      : selected.length === 1\n        ? selected\n        : []\n\n  // Split on the type, not on `asChild`, so passing the wrong one is a type\n  // error at the call site instead of a silent no-op.\n  const renderContent = typeof children === \"function\" ? children : undefined\n  const slotElement = typeof children === \"function\" ? undefined : children\n\n  let content: React.ReactNode\n\n  if (renderContent) {\n    content = renderContent(selected, path)\n  } else if (selectedValues.length === 0) {\n    content = (\n      <span className=\"text-muted-foreground truncate\">{placeholder}</span>\n    )\n  } else if (display === \"count\" || (multiple && selectedValues.length > 1)) {\n    content = labels.selectedCount(selectedValues.length)\n  } else {\n    const leaf = path[path.length - 1] ?? selected[0]\n    const segments =\n      display === \"leaf\"\n        ? [{ type: \"node\" as const, node: leaf }]\n        : collapseCascaderPath(path, { maxSegments, collapse })\n\n    content = (\n      <>\n        {showIcon && leaf?.icon ? (\n          <span\n            data-slot=\"cascader-value-icon\"\n            className=\"text-muted-foreground flex shrink-0 items-center\"\n          >\n            {leaf.icon}\n          </span>\n        ) : null}\n        {segments.map((segment, i) => (\n          <React.Fragment\n            key={segment.type === \"node\" ? segment.node.value : `gap-${i}`}\n          >\n            {i > 0 ? (separator ?? <PathChevron />) : null}\n            {segment.type === \"ellipsis\" ? (\n              <span\n                data-slot=\"cascader-value-ellipsis\"\n                title={segment.hidden\n                  .map((n) => n.label)\n                  .join(` ${labels.pathSeparator} `)}\n                className=\"text-muted-foreground shrink-0\"\n              >\n                &hellip;\n              </span>\n            ) : (\n              <span\n                className={cn(\n                  \"truncate\",\n                  i < segments.length - 1 && \"text-muted-foreground\"\n                )}\n              >\n                {segment.node?.label}\n              </span>\n            )}\n          </React.Fragment>\n        ))}\n      </>\n    )\n  }\n\n  // See `cascaderSlotChild`: `Slot` cannot clone the Fragment the default\n  // branch produces, so the consumer's element has to receive the content.\n  const slotChild = asChild ? cascaderSlotChild(slotElement, content) : null\n\n  const shared = {\n    \"data-slot\": \"cascader-value\",\n    className: cn(\"flex min-w-0 items-center gap-1 truncate\", className),\n    ...props,\n  }\n\n  return slotChild ? (\n    <Slot {...shared}>{slotChild}</Slot>\n  ) : (\n    <span {...shared}>{content}</span>\n  )\n}\n\nexport {\n  CascaderNav,\n  CascaderBack,\n  CascaderBreadcrumb,\n  CascaderInput,\n  CascaderValue,\n}\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-nav.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader-types.tsx",
      "content": "/**\n * cascader-types - adapted for tealmac UI from ReUI \"cascader/cascader-types\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\n// Title: Cascader\n// Description: Nested multi-level combobox with drill-down, columns and tree modes.\n\nimport type * as React from \"react\"\n\n/**\n * A node in the cascader tree. Two input shapes normalize to one internal\n * index: nested (nodes carry `children`), or flat adjacency plus the root's\n * `getParent`, which skips the re-nesting step for large datasets.\n */\nexport interface CascaderNode<T = unknown> {\n  /** Stable id. Also the committed selection value. */\n  value: string\n  label: string\n  icon?: React.ReactNode\n  description?: string\n  children?: CascaderNode<T>[]\n  /** Declares a branch before load; async nodes read as leaves without it. */\n  hasChildren?: boolean\n  /** Trailing count. Defaults to known children; set it for async nodes. */\n  count?: number\n  /** Listed, never selected or opened; on a branch, its whole subtree too. */\n  disabled?: boolean\n  keywords?: string[]\n  /**\n   * Only read on a node nested inside a `getChildren` result. For the level a\n   * node OWNS the authoritative signal is that level's `CascaderLoadResult`.\n   */\n  hasMore?: boolean\n  data?: T\n}\n\n/**\n * One footer ACTION. Deliberately not a `CascaderNode`, so a command can never\n * be passed where an option is expected and join the ring, filter or selection.\n */\nexport interface CascaderActionItem {\n  /** Stable key. Falls back to the label when it is a string, then the index. */\n  value?: string\n  label: React.ReactNode\n  icon?: React.ReactNode\n  disabled?: boolean\n  /** Ignored when `items` is present - a flyout opens instead. */\n  onSelect?: () => void\n  /** Turns the row into a submenu trigger. One level deep on purpose. */\n  items?: CascaderActionItem[]\n  /** Consecutive entries sharing a heading are drawn under one. */\n  group?: string\n}\n\n/** Panel layout. See the docs for the keyboard map of each. */\nexport type CascaderMode = \"drill\" | \"columns\" | \"tree\"\n\nexport type CascaderSearchScope = \"level\" | \"deep\" | \"global\"\n\n/**\n * Which nodes may be committed. The predicate arm is generic over the payload\n * rather than quantified per call: a per-call `<T>` would force every predicate\n * to accept EVERY payload, so `(node: CascaderNode<Member>) => boolean` could\n * never satisfy it.\n */\nexport type CascaderSelectable<T = unknown> =\n  \"leaf\" | \"any\" | ((node: CascaderNode<T>) => boolean)\n\nexport type CascaderCollapse = \"middle\" | \"start\" | \"none\"\n\nexport type CascaderValueDisplay = \"path\" | \"leaf\" | \"count\"\n\nexport type CascaderChangeReason = \"select\" | \"deselect\" | \"clear\"\n\n/** Second argument to `onValueChange`: resolved nodes, so no re-lookup. */\nexport interface CascaderChangeDetails<T = unknown> {\n  /** The node committed or toggled. Null when the selection was cleared. */\n  node: CascaderNode<T> | null\n  /** Ancestor chain of `node`, root first, node last. */\n  path: CascaderNode<T>[]\n  nodes: CascaderNode<T>[]\n  reason: CascaderChangeReason\n}\n\n/** Normalized tree, built once per `items` identity, shared by every mode. */\nexport interface CascaderIndex<T = unknown> {\n  byValue: Map<string, CascaderNode<T>>\n  /** Children by parent value. Root children are keyed by `ROOT_KEY`. */\n  childrenOf: Map<string, CascaderNode<T>[]>\n  parentOf: Map<string, string | null>\n  /** Zero-based depth by node value. */\n  depthOf: Map<string, number>\n  /** Top level nodes, in input order. */\n  roots: CascaderNode<T>[]\n  /** Every node in a stable, depth-first order. Used by deep search. */\n  all: CascaderNode<T>[]\n  /** Resolved chain nodes listed last in a level that may still return them. */\n  pinned?: ReadonlySet<string>\n  /** Resolved chain nodes whose level loaded completely without them: they\n   * keep their path for the trigger, and are no row, hit or cascade child. */\n  hidden?: ReadonlySet<string>\n}\n\nexport interface CascaderFlatNode<T = unknown> {\n  node: CascaderNode<T>\n  depth: number\n  /** Known children, or a declared `hasChildren`. */\n  branch: boolean\n  expanded: boolean\n  /** Sibling count, plus one when the level has a paging row. */\n  setSize: number\n  /** One-based index among siblings. */\n  posInSet: number\n}\n\n/** One segment of a rendered path, after collapsing. */\nexport type CascaderPathSegment<T = unknown> =\n  | { type: \"node\"; node: CascaderNode<T> }\n  | { type: \"ellipsis\"; hidden: CascaderNode<T>[] }\n\n/**\n * Async load state for one node's children. Deliberately WITHOUT a `status`\n * field: \"declared a branch, never fetched\" and \"fetched and genuinely empty\"\n * are told apart by MAP MEMBERSHIP, and a second source of that truth would\n * eventually disagree with the first.\n */\nexport interface CascaderLoadState {\n  loading: boolean\n  error: boolean\n  hasMore: boolean\n  /** Opaque cursor handed back to `getChildren` for the next page. */\n  cursor?: string\n}\n\n/** Why a level was fetched. `resolve`: `resolveValue` hit an unloaded node. */\nexport type CascaderLoadReason =\n  \"level\" | \"more\" | \"prefetch\" | \"retry\" | \"resolve\"\n\n/** Argument handed to `getChildren` and `resolveValue`. */\nexport interface CascaderLoadContext {\n  /**\n   * Aborted when the request is superseded, `loadKey` changes or the cascader\n   * unmounts, and for a `getChildren` load also when the popup closes.\n   */\n  signal: AbortSignal\n  cursor?: string\n  reason?: CascaderLoadReason\n}\n\n/** Value returned by `getChildren`. A bare array is also accepted. */\nexport interface CascaderLoadResult<T = unknown> {\n  items: CascaderNode<T>[]\n  nextCursor?: string\n  /** Defaults to whether `nextCursor` was supplied. */\n  hasMore?: boolean\n}\n\n/** Argument handed to `onSearch`. */\nexport interface CascaderSearchContext {\n  signal: AbortSignal\n  /** The path the user is searching within, deepest last. Empty under a\n   * global search, whose scope is the whole tree. */\n  path: string[]\n}\n\n/**\n * Every user facing string, so the primitive ships no hardcoded copy. The\n * callbacks take plain labels, not nodes: a `CascaderNode<T>` parameter would\n * force this object to carry the item generic. `*Announcement` is live-region.\n */\nexport interface CascaderLabels {\n  /** `parentLabel` is absent at the root and under a global search. */\n  search: string | ((parentLabel?: string) => string)\n  back: string\n  /** A level's FIRST page. `loadingMore` is the next, `loadMore` its idle row. */\n  loading: string\n  loadingMore: string\n  loadMore: string\n  error: string\n  retry: string\n  empty: string\n  /** Rendered by `CascaderValue` when `display=\"count\"`. */\n  selectedCount: (count: number) => string\n  breadcrumbLabel: string\n  chipsLabel: string\n  removeChip: (label: string) => string\n  /** Trail separator. Not every locale writes one with a slash. */\n  pathSeparator: string\n  /** Names the root level wherever there is no parent node to name it. */\n  rootLevel: string\n  itemCount: (count: number) => string\n  /** Appended to a branch row's name outside tree mode: it opens another list. */\n  branchAffordance: string\n  /** Columns-trail rows are plain buttons, so they carry no `aria-selected`. */\n  selectedState: string\n  /** Same for the mixed state, which a `role=\"button\"` row may not carry. */\n  partiallySelectedState: string\n  columnsLabel: string\n  actionsLabel: string\n  submenuAffordance: string\n  panelLabel: string\n  /** Read per mode AND per direction: the level keys mirror in RTL. */\n  keyboardHint: (mode: CascaderMode, dir: \"ltr\" | \"rtl\") => string\n  rootAnnouncement: (count: number) => string\n  expandedAnnouncement: (label: string, count: number) => string\n  collapsedAnnouncement: (label: string) => string\n  levelAnnouncement: (\n    parentLabel: string,\n    depth: number,\n    count: number\n  ) => string\n  resultsAnnouncement: (count: number) => string\n  maxReachedAnnouncement: (max: number) => string\n  /** `count` is the descendants swept along; `selecting` is the direction. */\n  cascadeAnnouncement: (\n    label: string,\n    count: number,\n    selecting: boolean\n  ) => string\n  searchingAnnouncement: string\n}\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-types.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader-virtual.tsx",
      "content": "/**\n * cascader-virtual - adapted for tealmac UI from ReUI \"cascader/cascader-virtual\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\n\"use client\"\n\nimport * as React from \"react\"\nimport {\n  defaultRangeExtractor,\n  useVirtualizer,\n  type Range,\n  type Virtualizer,\n} from \"@tanstack/react-virtual\"\n\nimport { CascaderColumnPanel } from \"@/components/reui/cascader/cascader-columns\"\nimport {\n  useCascaderActions,\n  useCascaderHighlight,\n  useCascaderState,\n} from \"@/components/reui/cascader/cascader-context\"\nimport type { CascaderColumn } from \"@/components/reui/cascader/cascader-context\"\nimport {\n  CascaderItem,\n  CascaderItems,\n  getCascaderMoreProps,\n} from \"@/components/reui/cascader/cascader-item\"\n\n/**\n * Windowing for the cascader, in its own file so the primitive's own install\n * never pulls `@tanstack/react-virtual` in. Base UI forces three rules:\n *\n * 1. A windowed row MUST carry an explicit `index`; the fallback\n *    `findItemIndex` is O(n) per row and returns `-1` on the frame a level swap\n *    renders empty, dangling `aria-activedescendant`.\n * 2. That index is DESTRUCTIVE while unvirtualized (`useCompositeListItem`\n *    skips registration, `CompositeList` truncates `elementsRef` to zero and\n *    the first arrow key highlights nothing), so rows stay plain until the root\n *    flips `virtualized`, gated inside `CascaderItem`.\n * 3. No `Combobox.Collection` and no FUNCTION CHILD on `Combobox.List`, which\n *    implicitly wraps one in a Collection: a Collection renders every filtered\n *    item.\n */\n\n/* -------------------------------------------------------------------------- */\n/*                                 Virtualizer                                */\n/* -------------------------------------------------------------------------- */\n\nexport interface UseCascaderVirtualizerOptions {\n  count: number\n  getScrollElement: () => HTMLElement | null\n  estimateSize?: number\n  overscan?: number\n  /** Key rows by node value, never by index: a tree expand shifts every index. */\n  getItemKey: (index: number) => string | number\n  /** Render index of the highlighted row, or `-1`. Pinned into the window. */\n  activeIndex?: number\n}\n\nexport type CascaderVirtualizer = Virtualizer<HTMLElement, HTMLElement>\n\n/**\n * The cascader's virtualizer: TanStack plus the highlight pinning and\n * scroll-into-view a combobox list needs. `activeIndex` is clamped here, not by\n * the caller: emitted from a layout effect, it can be one commit ahead of a\n * level that just shrank and would otherwise scroll to a row that is gone.\n */\nexport function useCascaderVirtualizer({\n  count,\n  getScrollElement,\n  estimateSize = 32,\n  overscan = 8,\n  getItemKey,\n  activeIndex = -1,\n}: UseCascaderVirtualizerOptions): CascaderVirtualizer {\n  const active = activeIndex >= 0 && activeIndex < count ? activeIndex : -1\n\n  // Keeps the highlighted row mounted so `aria-activedescendant` never dangles.\n  const rangeExtractor = React.useCallback(\n    (range: Range) => {\n      const indices = new Set(defaultRangeExtractor(range))\n      if (active !== -1) indices.add(active)\n      return Array.from(indices).sort((a, b) => a - b)\n    },\n    [active]\n  )\n\n  const measureEstimate = React.useCallback(() => estimateSize, [estimateSize])\n\n  // React Compiler bails on `useVirtualizer` HERE, and only here - the bail does not\n  // propagate to the components that call this hook. They opt out themselves.\n  // eslint-disable-next-line react-hooks/incompatible-library\n  const virtualizer = useVirtualizer<HTMLElement, HTMLElement>({\n    count,\n    getScrollElement,\n    estimateSize: measureEstimate,\n    overscan,\n    getItemKey,\n    rangeExtractor,\n  })\n\n  // Base UI's scroll-into-view no-ops on a windowed-out row (empty `listRef`).\n  React.useEffect(() => {\n    if (active === -1) return\n    virtualizer.scrollToIndex(active, { align: \"auto\" })\n  }, [active, count, virtualizer])\n\n  return virtualizer\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Geometry                                  */\n/* -------------------------------------------------------------------------- */\n\ninterface CascaderVirtualGutter {\n  block: number\n  inline: number\n}\n\nconst NO_GUTTER: CascaderVirtualGutter = { block: 0, inline: 0 }\n\n/**\n * The ROWS' BOX's own padding, never the scrollport's (separate element, reads\n * a flat zero). Measured because every style sets its own: a windowed row is\n * positioned against this box's PADDING box while the scroll-height spacer is\n * an in-flow child of its CONTENT box, so without it rows sit flush.\n */\nfunction useCascaderVirtualGutter(\n  contentElement: HTMLElement | null\n): CascaderVirtualGutter {\n  const [gutter, setGutter] = React.useState<CascaderVirtualGutter>(NO_GUTTER)\n\n  React.useLayoutEffect(() => {\n    if (!contentElement) return\n    const styles = getComputedStyle(contentElement)\n    const block = Number.parseFloat(styles.paddingTop) || 0\n    const inline =\n      Number.parseFloat(styles.paddingInlineStart || styles.paddingLeft) || 0\n    setGutter((previous) =>\n      previous.block === block && previous.inline === inline\n        ? previous\n        : { block, inline }\n    )\n  }, [contentElement])\n\n  return gutter\n}\n\n/** Logical insets, not `left` / `width`, so RTL needs no second code path. */\nfunction cascaderVirtualRowStyle(\n  start: number,\n  gutter: CascaderVirtualGutter\n): React.CSSProperties {\n  return {\n    position: \"absolute\",\n    top: 0,\n    insetInlineStart: gutter.inline,\n    insetInlineEnd: gutter.inline,\n    transform: `translateY(${start + gutter.block}px)`,\n  }\n}\n\n/**\n * The element that actually SCROLLS: the `ScrollArea` viewport above the rows'\n * box, not the rows' box itself, which never overflows, so a virtualizer aimed\n * at it would read a full `clientHeight` and render every row. `closest` rather\n * than `parentElement`, plus a fallback to the content element, keeps a\n * hand-written `overflow-y-auto` container working.\n */\nfunction cascaderScrollElement(\n  content: HTMLElement | null\n): HTMLElement | null {\n  if (!content) return null\n  return (\n    content.closest<HTMLElement>('[data-slot=\"scroll-area-viewport\"]') ??\n    content\n  )\n}\n\n/**\n * `role=\"presentation\"` keeps the listbox owning nothing but options, and this\n * node's parent IS the rows' box, saving a ref threaded through `CascaderList`.\n */\nfunction CascaderVirtualSpacer({\n  height,\n  onContentElement,\n}: {\n  height: number\n  onContentElement: (element: HTMLElement | null) => void\n}) {\n  return (\n    <div\n      ref={(node) => onContentElement(node?.parentElement ?? null)}\n      role=\"presentation\"\n      data-slot=\"cascader-virtual-spacer\"\n      style={{ height }}\n    />\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Rows                                    */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderVirtualItemsProps {\n  /** Row height before measurement. Defaults to the root `estimateRowSize`. */\n  estimateSize?: number\n  /** Rows rendered beyond each edge. Defaults to the root `overscan`. */\n  overscan?: number\n}\n\n/**\n * Windowed replacement for `CascaderItems`, dropped inside `CascaderList`. The\n * registration is a LAYOUT effect because the root derives `virtualized` from\n * it and the flip has to land before the browser paints. Until the root has\n * seen it, this renders exactly what `CascaderItems` does, so no frame carries\n * indexed rows while Base UI still owns the composite list.\n */\nfunction CascaderVirtualItems(props: CascaderVirtualItemsProps) {\n  const { virtualized, registerVirtualRenderer } = useCascaderActions()\n\n  React.useLayoutEffect(\n    () => registerVirtualRenderer(),\n    [registerVirtualRenderer]\n  )\n\n  if (!virtualized) return <CascaderItems />\n\n  return <CascaderVirtualRows {...props} />\n}\n\nfunction CascaderVirtualRows({\n  estimateSize,\n  overscan,\n}: CascaderVirtualItemsProps) {\n  /* The `useCascaderVirtualizer` bail does NOT reach here: the compiler caches\n     `getVirtualItems()` on the virtualizer, whose identity never changes, so the\n     window freezes on scroll. Inert where no compiler runs. */\n  \"use no memo\"\n\n  const {\n    estimateRowSize,\n    overscan: rootOverscan,\n    mode,\n    isBranch,\n    isSelectable,\n    isSelected,\n    isIndeterminate,\n  } = useCascaderActions()\n  const { renderedItems, treeRows, deepResults, loadStates } =\n    useCascaderState()\n  // Re-renders on every highlight change, and has to: the pinned row and the\n  // scroll target are both derived from it.\n  const highlight = useCascaderHighlight()\n\n  const [contentElement, setContentElement] =\n    React.useState<HTMLElement | null>(null)\n  const gutter = useCascaderVirtualGutter(contentElement)\n\n  const tree = mode === \"tree\"\n  const count = tree ? treeRows.length : renderedItems.length\n  const showPath = !tree && deepResults !== null\n\n  const getItemKey = React.useCallback(\n    (index: number) =>\n      (tree ? treeRows[index]?.node.value : renderedItems[index]?.value) ??\n      index,\n    [tree, treeRows, renderedItems]\n  )\n\n  const virtualizer = useCascaderVirtualizer({\n    count,\n    getScrollElement: () => cascaderScrollElement(contentElement),\n    estimateSize: estimateSize ?? estimateRowSize,\n    overscan: overscan ?? rootOverscan,\n    getItemKey,\n    activeIndex: highlight.index,\n  })\n\n  return (\n    <>\n      <CascaderVirtualSpacer\n        height={virtualizer.getTotalSize()}\n        onContentElement={setContentElement}\n      />\n      {virtualizer.getVirtualItems().map((row) => {\n        const flat = tree ? treeRows[row.index] : undefined\n        const node = tree ? flat?.node : renderedItems[row.index]\n        // For one commit after a query narrows the level, the window can name\n        // a row it no longer has; the count recomputes on the same tick.\n        if (!node) return null\n\n        return (\n          <CascaderItem\n            key={row.key}\n            /* Measured, not estimated: rows are two lines with a `description`\n               and three in deep or global search, and row height is per\n               style. An estimate alone would make `scrollToIndex` land on the\n               wrong row. */\n            ref={virtualizer.measureElement}\n            data-index={row.index}\n            style={cascaderVirtualRowStyle(row.start, gutter)}\n            node={node}\n            index={row.index}\n            depth={flat?.depth}\n            expanded={flat?.expanded}\n            branch={flat ? flat.branch : isBranch(node)}\n            selectable={isSelectable(node)}\n            selected={isSelected(node)}\n            indeterminate={isIndeterminate(node)}\n            {...getCascaderMoreProps(node, loadStates)}\n            showPath={showPath}\n            aria-setsize={flat ? flat.setSize : count}\n            aria-posinset={flat ? flat.posInSet : row.index + 1}\n          />\n        )\n      })}\n    </>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Column                                   */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderVirtualColumnProps extends CascaderVirtualItemsProps {\n  column: CascaderColumn\n}\n\n/**\n * Windowed replacement for one Miller column, passed through the\n * `CascaderColumns` render slot. One virtualizer PER COLUMN, since columns\n * scroll independently, and only the deepest is Base UI's listbox: the trail\n * behind is `as=\"button\"` rows outside Base UI, so it windows on its own row\n * count rather than waiting for the root to flip.\n */\nfunction CascaderVirtualColumn({\n  column,\n  estimateSize,\n  overscan,\n}: CascaderVirtualColumnProps) {\n  const {\n    virtualized,\n    registerVirtualRenderer,\n    virtualize,\n    virtualizeThreshold,\n  } = useCascaderActions()\n\n  React.useLayoutEffect(\n    () => registerVirtualRenderer(),\n    [registerVirtualRenderer]\n  )\n\n  const windowed = column.active\n    ? virtualized\n    : (virtualize ?? column.items.length >= virtualizeThreshold)\n\n  if (!windowed) return <CascaderColumnPanel column={column} />\n\n  return (\n    <CascaderColumnPanel column={column} virtualized>\n      {column.active ? (\n        <CascaderVirtualActiveColumnRows\n          column={column}\n          estimateSize={estimateSize}\n          overscan={overscan}\n        />\n      ) : (\n        <CascaderVirtualColumnRows\n          column={column}\n          estimateSize={estimateSize}\n          overscan={overscan}\n          activeIndex={-1}\n        />\n      )}\n    </CascaderColumnPanel>\n  )\n}\n\n/** Only the active column subscribes to the highlight; a trail column must not. */\nfunction CascaderVirtualActiveColumnRows(props: CascaderVirtualColumnProps) {\n  const highlight = useCascaderHighlight()\n  return <CascaderVirtualColumnRows {...props} activeIndex={highlight.index} />\n}\n\nfunction CascaderVirtualColumnRows({\n  column,\n  estimateSize,\n  overscan,\n  activeIndex,\n}: CascaderVirtualColumnProps & { activeIndex: number }) {\n  /* The `useCascaderVirtualizer` bail does NOT reach here: the compiler caches\n     `getVirtualItems()` on the virtualizer, whose identity never changes, so the\n     window freezes on scroll. Inert where no compiler runs. */\n  \"use no memo\"\n\n  const {\n    estimateRowSize,\n    overscan: rootOverscan,\n    baseId,\n    isBranch,\n    isSelectable,\n    isSelected,\n    isIndeterminate,\n  } = useCascaderActions()\n  const { loadStates } = useCascaderState()\n\n  const [contentElement, setContentElement] =\n    React.useState<HTMLElement | null>(null)\n  const gutter = useCascaderVirtualGutter(contentElement)\n\n  const items = column.items\n  const getItemKey = React.useCallback(\n    (index: number) => items[index]?.value ?? index,\n    [items]\n  )\n\n  const virtualizer = useCascaderVirtualizer({\n    count: items.length,\n    getScrollElement: () => cascaderScrollElement(contentElement),\n    estimateSize: estimateSize ?? estimateRowSize,\n    overscan: overscan ?? rootOverscan,\n    getItemKey,\n    activeIndex,\n  })\n\n  return (\n    <>\n      <CascaderVirtualSpacer\n        height={virtualizer.getTotalSize()}\n        onContentElement={setContentElement}\n      />\n      {virtualizer.getVirtualItems().map((row) => {\n        const node = items[row.index]\n        if (!node) return null\n        const open = node.value === column.activeValue\n\n        return (\n          <CascaderItem\n            key={row.key}\n            ref={virtualizer.measureElement}\n            data-index={row.index}\n            style={cascaderVirtualRowStyle(row.start, gutter)}\n            node={node}\n            as={column.active ? \"option\" : \"button\"}\n            depth={column.depth}\n            branch={isBranch(node)}\n            selectable={isSelectable(node)}\n            selected={isSelected(node)}\n            indeterminate={isIndeterminate(node)}\n            {...getCascaderMoreProps(node, loadStates)}\n            data-open={open || undefined}\n            className={open ? \"bg-accent/60 text-accent-foreground\" : undefined}\n            /* Only the listbox column may carry an index or listbox metadata.\n               A trail row is a `role=\"button\"`, which allows neither. */\n            {...(column.active\n              ? {\n                  index: row.index,\n                  \"aria-setsize\": items.length,\n                  \"aria-posinset\": row.index + 1,\n                }\n              : null)}\n            {...(!column.active && open\n              ? {\n                  \"aria-expanded\": true,\n                  \"aria-controls\": `${baseId}-column-${column.depth + 1}`,\n                }\n              : null)}\n          />\n        )\n      })}\n    </>\n  )\n}\n\nexport { CascaderVirtualColumn, CascaderVirtualItems }\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader-virtual.tsx"
    },
    {
      "path": "registry/tealmac/reui-cascader/cascader.tsx",
      "content": "/**\n * cascader - adapted for tealmac UI from ReUI \"cascader/cascader\"\n * (https://github.com/keenthemes/reui), MIT License, Copyright (c) 2025\n * Keenthemes Inc. The full licence text is in THIRD_PARTY_NOTICES.md.\n * Changes: tealmac UI import paths, lucide icons instead of the icon-library\n * placeholder, @radix-ui/react-slot, and theme tokens.\n */\n\"use client\"\n\nimport * as React from \"react\"\nimport { Combobox as ComboboxPrimitive } from \"@base-ui/react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { ChevronDownIcon, XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\nimport {\n  useCascaderLoader,\n  useCascaderLoadState,\n} from \"@/components/reui/cascader/cascader-async\"\nimport type {\n  CascaderGetChildren,\n  CascaderOnSearch,\n  CascaderResolveValue,\n} from \"@/components/reui/cascader/cascader-async\"\nimport {\n  CascaderActionsContext,\n  CascaderHighlightContext,\n  CascaderRenderContext,\n  CascaderStateContext,\n  createCascaderHighlightStore,\n  useCascaderActions,\n  useCascaderHighlight,\n  useCascaderRender,\n  useCascaderState,\n} from \"@/components/reui/cascader/cascader-context\"\nimport type {\n  CascaderActionsContextValue,\n  CascaderColumn,\n  CascaderHighlight,\n  CascaderHighlightStore,\n  CascaderItemState,\n  CascaderRenderContextValue,\n  CascaderStateContextValue,\n} from \"@/components/reui/cascader/cascader-context\"\nimport { resolveCascaderLabels } from \"@/components/reui/cascader/cascader-i18n\"\nimport {\n  applyCascadeSelection,\n  buildCascaderIndex,\n  CASCADER_LIST_HEIGHT_CLASS,\n  CASCADER_LIST_PAD_CLASS,\n  CASCADER_ROOT_KEY,\n  CASCADER_ROWS_CLASS,\n  CASCADER_SCROLL_CLASS,\n  collapseCascaderPath,\n  createCascaderMoreNode,\n  filterCascaderLevel,\n  findAmbiguousCascaderLabels,\n  findCascaderDataIssues,\n  flattenCascaderTree,\n  focusCascaderHome,\n  getCascaderCheckedValues,\n  getCascaderChildren,\n  getCascaderCount,\n  getCascaderIndeterminateFrom,\n  getCascaderListedValues,\n  getCascaderPath,\n  getCascaderSelectedDescendants,\n  getCascaderTabTarget,\n  isCascaderBranch,\n  isCascaderDisabled,\n  isCascaderMoreNode,\n  isCascaderSelectable,\n  matchesCascaderQuery,\n  mergeCascaderIndex,\n  normalizeCascaderQuery,\n  searchCascaderDeep,\n  warnCascaderOnce,\n  withCascaderMoreRow,\n} from \"@/components/reui/cascader/cascader-lib\"\nimport type { CascaderCheckedStrategy } from \"@/components/reui/cascader/cascader-lib\"\nimport type {\n  CascaderActionItem,\n  CascaderChangeDetails,\n  CascaderChangeReason,\n  CascaderFlatNode,\n  CascaderIndex,\n  CascaderLabels,\n  CascaderLoadState,\n  CascaderMode,\n  CascaderNode,\n  CascaderSearchScope,\n  CascaderSelectable,\n} from \"@/components/reui/cascader/cascader-types\"\n\n/** Stable `filteredItems` for a level swap; a fixed identity cannot loop. */\nconst EMPTY: CascaderNode<never>[] = []\n\n/** Stable empty array for the `actions` prop, so a cascader with no footer\n * does not republish the actions context on every render. */\nconst EMPTY_ACTIONS: CascaderActionItem[] = []\n\n/** The `onSearch` scope of a global search: no path, so the whole tree. */\nconst GLOBAL_SEARCH_PATH: string[] = []\n\n// Word joiner. A polite region reads only MUTATIONS, so alternate it.\nconst ANNOUNCE_MARKER = \"\\u2060\"\n\n// Delay before a query result-count reaches the live region: polite\n// announcements queue, and re-announcing per keystroke plays back stale\n// counts. Only the count defers; every event-shaped one stays immediate.\nconst ANNOUNCE_DEBOUNCE = 150\n\n/* -------------------------------------------------------------------------- */\n/*                                   State                                    */\n/* -------------------------------------------------------------------------- */\n\n// Minimal controlled/uncontrolled resolver, hand-rolled because the repo has\n// no shared `useControllableState`; the root needs it five times.\nfunction useControllable<V>(\n  controlled: V | undefined,\n  defaultValue: V,\n  onChange?: (value: V) => void,\n  /** Prop name, used only by the development-time switching warning. */\n  devName?: string\n): [V, (next: V | ((prev: V) => V)) => void] {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue)\n  const isControlled = controlled !== undefined\n  const value = isControlled ? controlled : uncontrolled\n\n  // Switching controlled/uncontrolled mid-life strands half the state.\n  const wasControlled = React.useRef(isControlled)\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return\n    if (wasControlled.current === isControlled || !devName) return\n    const from = wasControlled.current ? \"controlled\" : \"uncontrolled\"\n    const to = isControlled ? \"controlled\" : \"uncontrolled\"\n    wasControlled.current = isControlled\n    warnCascaderOnce(\n      `controlled-switch:${devName}`,\n      `\\`${devName}\\` switched from ${from} to ${to}. Decide once: pass \\`${devName}\\` for the whole life of the component, or pass only the default and let the cascader own it.`\n    )\n  }, [isControlled, devName])\n\n  // Written in an EFFECT: a discarded render would still have mutated a ref.\n  const latest = React.useRef({ value, isControlled, onChange })\n  React.useEffect(() => {\n    latest.current = { value, isControlled, onChange }\n  })\n\n  // The last PROP seen: it differs from the optimistic advance exactly when a\n  // controlled parent declines a change and re-renders nothing.\n  const lastProp = React.useRef(controlled)\n  React.useEffect(() => {\n    lastProp.current = controlled\n  })\n\n  const set = React.useCallback((next: V | ((prev: V) => V)) => {\n    const current = latest.current.value\n    const resolved =\n      typeof next === \"function\" ? (next as (prev: V) => V)(current) : next\n    if (Object.is(resolved, current)) {\n      // Dedup against the last PROP, not the optimistic advance: a parent that\n      // declines re-renders nothing, so deduping on the advance left the popup\n      // un-dismissable from the second Escape on.\n      if (\n        !latest.current.isControlled ||\n        Object.is(resolved, lastProp.current)\n      ) {\n        return\n      }\n    }\n\n    // Advance now so two updates in ONE batch compose, rather than the second\n    // reading a stale value and its `Object.is` dedup dropping it.\n    latest.current = { ...latest.current, value: resolved }\n\n    if (!latest.current.isControlled) setUncontrolled(resolved)\n    latest.current.onChange?.(resolved)\n  }, [])\n\n  return [value, set]\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              Shallow stability                             */\n/* -------------------------------------------------------------------------- */\n\nfunction shallowEqualRecords(a: object, b: object): boolean {\n  const left = a as Record<string, unknown>\n  const right = b as Record<string, unknown>\n  const keys = Object.keys(left)\n  if (keys.length !== Object.keys(right).length) return false\n  for (const key of keys) {\n    if (!Object.is(left[key], right[key])) return false\n  }\n  return true\n}\n\nfunction shallowEqualItemLists(\n  a: readonly object[],\n  b: readonly object[]\n): boolean {\n  if (a.length !== b.length) return false\n  for (let i = 0; i < a.length; i += 1) {\n    if (a[i] !== b[i] && !shallowEqualRecords(a[i], b[i])) return false\n  }\n  return true\n}\n\n// Reuses the previous value while the next merely says the same thing: an\n// inline `labels`/`actions` otherwise republishes the actions context, and\n// with it every row. Adjust-state-during-render, not a ref.\nfunction useShallowStable<V>(\n  value: V,\n  equal: (a: V & object, b: V & object) => boolean\n): V {\n  const [stable, setStable] = React.useState(value)\n  if (Object.is(value, stable)) return stable\n  const comparable =\n    typeof value === \"object\" &&\n    value !== null &&\n    typeof stable === \"object\" &&\n    stable !== null\n  if (comparable && equal(value as V & object, stable as V & object)) {\n    return stable\n  }\n  setStable(value)\n  return value\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                  Context                                   */\n/* -------------------------------------------------------------------------- */\n\n/** The pre-split shape, merged, so `useCascader()` returns what it always did. */\nexport interface CascaderContextValue<T = unknown>\n  extends CascaderActionsContextValue<T>, CascaderStateContextValue<T> {}\n\n/**\n * The cascader's internals, typed for the caller's own item payload.\n * @deprecated Reads BOTH halves, so a caller re-renders on every keystroke.\n * Prefer `useCascaderActions()` or `useCascaderState()`. Kept for compat.\n */\nexport function useCascader<T = unknown>(): CascaderContextValue<T> {\n  const actions = useCascaderActions<T>()\n  const state = useCascaderState<T>()\n\n  return React.useMemo(() => ({ ...actions, ...state }), [actions, state])\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              Headless selection                            */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderSelection<T = unknown> {\n  /** The selected nodes, resolved. Empty when nothing is selected. */\n  selected: CascaderNode<T>[]\n  /** Ancestor chain per selected node, root first, node last. */\n  paths: CascaderNode<T>[][]\n  /** The single selection, or the first one in `multiple` mode. */\n  first: CascaderNode<T> | null\n  /** Ancestor chain of `first`. */\n  firstPath: CascaderNode<T>[]\n  count: number\n  isEmpty: boolean\n  multiple: boolean\n  remove: (value: string) => void\n  clear: () => void\n}\n\n/** Headless access to the selection, for a trigger that is not\n * `CascaderValue`. Pass the payload explicitly to get `node.data` typed. */\nexport function useCascaderSelection<T = unknown>(): CascaderSelection<T> {\n  const { multiple, setSelection } = useCascaderActions<T>()\n  const { index, selectedValues } = useCascaderState<T>()\n\n  return React.useMemo(() => {\n    const selected = selectedValues\n      .map((value) => index.byValue.get(value))\n      .filter(Boolean) as CascaderNode<T>[]\n    const paths = selectedValues.map((value) => getCascaderPath(index, value))\n\n    return {\n      selected,\n      paths,\n      first: selected[0] ?? null,\n      firstPath: paths[0] ?? [],\n      count: selectedValues.length,\n      isEmpty: selectedValues.length === 0,\n      multiple,\n      remove: (value: string) =>\n        setSelection(selectedValues.filter((entry) => entry !== value)),\n      // Explicit reason: emptying on purpose is not \"deselect the last node\".\n      clear: () => setSelection([], \"clear\"),\n    }\n  }, [index, selectedValues, multiple, setSelection])\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Root                                    */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Why the navigation path changed.\n * - `\"drill\"`: a branch was entered (row press, level arrow, search hit).\n * - `\"back\"`: one level up (back control, Backspace on an empty query).\n * - `\"breadcrumb\"`: a jump to an exact depth in the trail.\n * - `\"reveal\"`: the reopen navigated to the level holding the selection.\n * - `\"external\"`: `setPath` called from outside the primitive's own flows.\n */\nexport type CascaderPathChangeReason =\n  \"drill\" | \"back\" | \"breadcrumb\" | \"reveal\" | \"external\"\n\n/** Everything independent of the selection mode; the rest is on the two arms. */\nexport interface CascaderBaseProps<T = unknown> {\n  /** Nested tree, or a flat list when `getParent` is supplied. */\n  items: CascaderNode<T>[]\n  /** Opt in to flat adjacency input by returning each node's parent value. */\n  getParent?: (node: CascaderNode<T>) => string | null | undefined\n\n  /**\n   * Fetches one level on demand; `node` is `null` for the root, and only\n   * `{ value, label: value }` for a level on screen whose node has not loaded\n   * (a deep link, or a page-loaded level after a `loadKey` change). Return an\n   * array, or a `CascaderLoadResult` when the level pages. Pages cache apart\n   * from `items`; mark unfetched branches `hasChildren` or they read as leaves.\n   */\n  getChildren?: CascaderGetChildren<T>\n  /**\n   * Server-side search, debounced by `searchDebounce`; replaces the local scan\n   * while the query is set. Results are detached: resolvable by value, in no\n   * level. Ignored in `mode=\"tree\"`, where a hit belongs to no visible branch.\n   */\n  onSearch?: CascaderOnSearch<T>\n  /** Milliseconds of quiet before `onSearch` fires. */\n  searchDebounce?: number\n  /**\n   * Resolves a value the loader has never seen into its ancestor chain, root\n   * first, so the trigger can path a server-supplied selection. Runs with the\n   * popup closed too. Does NOT mark those levels loaded: opening one still\n   * fetches, and lists the node after its rows while it has pages left.\n   */\n  resolveValue?: CascaderResolveValue<T>\n  /** Cache key. Changing it drops every page and aborts anything in flight. */\n  loadKey?: unknown\n  /** Prefetch a highlighted branch's children after a pause. Off by default. */\n  prefetch?: boolean\n  /**\n   * Called when a load fails. `context.parent` is the level (`null` for root);\n   * `context.reason` is `\"level\"`, `\"more\"`, `\"retry\"`, `\"prefetch\"`,\n   * `\"search\"` or `\"resolve\"`. Never for an aborted or superseded request.\n   */\n  onLoadError?: (\n    error: unknown,\n    context: { parent: string | null; reason: string }\n  ) => void\n\n  /** Which nodes may be committed. Defaults to leaves only. */\n  selectable?: CascaderSelectable<T>\n\n  /**\n   * Whether the built-in SINGLE-SELECT check is drawn. Defaults to true.\n   * `false` also gives back the inline-end gutter every style reserves for it;\n   * `data-selected` and `aria-selected` stay. A no-op under `multiple`, where\n   * the checkbox is the selection CONTROL rather than a decoration.\n   */\n  indicator?: boolean\n\n  /**\n   * Footer ACTIONS: commands pinned below the list, never rows in it, and never\n   * touching the data tree. A childless `<CascaderFooter />` draws these; an\n   * action with `items` opens a side-anchored flyout rather than firing.\n   */\n  actions?: CascaderActionItem[]\n\n  /**\n   * Parent/child cascading. Multi-select only, off by default, and it needs\n   * committable branches (`selectable=\"any\"` or a predicate). A commit selects\n   * or deselects every SELECTABLE node in the KNOWN subtree (loaded, or on a\n   * resolved chain) and reconciles ancestors. KNOWN is the semantic, not a\n   * hedge: a commit must not fan out into a request per level, so children\n   * arriving later arrive unchecked.\n   */\n  cascade?: boolean\n\n  mode?: CascaderMode\n\n  /** How a branch opens from the pointer in columns mode's ACTIVE column.\n   * `\"click\"` is the default, and neither value ever commits a selection. */\n  expandTrigger?: \"click\" | \"hover\"\n\n  path?: string[]\n  defaultPath?: string[]\n  /** The second argument says WHY. An existing `(path) => void` still works. */\n  onPathChange?: (\n    path: string[],\n    details: { reason: CascaderPathChangeReason }\n  ) => void\n\n  expanded?: string[]\n  defaultExpanded?: string[]\n  onExpandedChange?: (expanded: string[]) => void\n\n  open?: boolean\n  defaultOpen?: boolean\n  /** Forwards Base UI's reason; a leaf commit's close says `\"item-press\"`. */\n  onOpenChange?: (open: boolean, details: { reason: string }) => void\n\n  /**\n   * Whether a SINGLE-SELECT leaf commit closes the popup. Defaults to true.\n   * `false` is for a compare-and-repick flow; Escape, outside press and the\n   * trigger still close. Ignored under `multiple`, which never closes.\n   */\n  closeOnSelect?: boolean\n\n  inputValue?: string\n  defaultInputValue?: string\n  onInputValueChange?: (value: string) => void\n\n  /**\n   * `\"level\"` (default) filters the level on screen, `\"deep\"` also searches\n   * below it, `\"global\"` searches the whole tree from any level.\n   */\n  searchScope?: CascaderSearchScope\n  /** Custom matcher, replacing label + keywords substring matching. */\n  filter?: (node: CascaderNode<T>, normalizedQuery: string) => boolean\n\n  /** On open, navigate to the level holding the selection. Default true.\n   * Not in `mode=\"tree\"`, which has no levels to navigate. */\n  revealSelected?: boolean\n\n  maxHeight?: number | string\n\n  /**\n   * Window the rendered rows. `undefined` decides per level from\n   * `virtualizeThreshold`; `true`/`false` force it. Also opt-in at the markup\n   * level, so the base install pulls in no virtualization library.\n   */\n  virtualize?: boolean\n  /** Row count at which windowing turns itself on. */\n  virtualizeThreshold?: number\n  /** Row height handed to the virtualizer before a row has been measured. */\n  estimateRowSize?: number\n  /** Rows rendered beyond each edge of the viewport. */\n  overscan?: number\n\n  labels?: Partial<CascaderLabels>\n  /** Replaces the entire row, affordances included. */\n  renderItem?: (\n    node: CascaderNode<T>,\n    state: CascaderItemState<T>\n  ) => React.ReactNode\n  /** Replaces only the label block; icon, count, chevron and check stay. */\n  renderLabel?: (\n    node: CascaderNode<T>,\n    state: CascaderItemState<T>\n  ) => React.ReactNode\n\n  disabled?: boolean\n  /** Native form field name. Submits the selected value(s). */\n  name?: string\n  /** Id of the owning form, for a cascader rendered outside it. */\n  form?: string\n  /** Id of the control. Pairs the field with a `<label htmlFor>`. */\n  id?: string\n  /** Marks the field required for native form validation. */\n  required?: boolean\n  /** The value can be read and submitted, but not changed. */\n  readOnly?: boolean\n  /** Marks the field invalid: `aria-invalid` and `data-invalid` on the trigger,\n   * the chips and the input, which every style's error treatment reads. */\n  invalid?: boolean\n  /** Ref to the hidden input Base UI submits with, for form-library focus. */\n  inputRef?: React.Ref<HTMLInputElement>\n  /** Renders without a floating popup, for an embedded `CascaderPanel`. */\n  inline?: boolean\n\n  children?: React.ReactNode\n}\n\nexport interface CascaderSingleProps<T = unknown> extends CascaderBaseProps<T> {\n  multiple?: false\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string, details: CascaderChangeDetails<T>) => void\n  /** `never` so passing it is a compile error, not a silent no-op. */\n  max?: never\n}\n\n/** Multi-select props. Enables checkbox rows and chip display. */\nexport interface CascaderMultipleProps<\n  T = unknown,\n> extends CascaderBaseProps<T> {\n  multiple: true\n  value?: string[]\n  defaultValue?: string[]\n  onValueChange?: (value: string[], details: CascaderChangeDetails<T>) => void\n  /** Cap on selections. Further picks are refused, the existing ones survive. */\n  max?: number\n}\n\n/** `multiple` discriminates: `value`, `onValueChange` and `max` narrow with it. */\nexport type CascaderProps<T = unknown> =\n  CascaderSingleProps<T> | CascaderMultipleProps<T>\n\n/** Normalizes to an array. `\"\"` is \"no selection\", not a node: it is the\n * uncontrolled default, and resolving it would render a blank chip. */\nfunction toArray(value: string | string[] | undefined): string[] {\n  if (value == null) return []\n  if (Array.isArray(value)) return value.filter((entry) => entry !== \"\")\n  return value === \"\" ? [] : [value]\n}\n\n/* -------------------------------------------------------------------------- */\n/*                            Development warnings                            */\n/* -------------------------------------------------------------------------- */\n\ninterface CascaderDevOptions<T> {\n  items: CascaderNode<T>[]\n  getParent?: (node: CascaderNode<T>) => string | null | undefined\n  mode: CascaderMode\n  multiple: boolean\n  cascade: boolean\n  indicator: boolean\n  selectable: CascaderSelectable<T>\n  searchScope: CascaderSearchScope\n  max: number | undefined\n  value: string | string[] | undefined\n  defaultValue: string | string[] | undefined\n  hasExpanded: boolean\n  hasPath: boolean\n  hasOnSearch: boolean\n}\n\n/** Misconfigurations that are SILENT by design. Once each, never in prod. */\nfunction useCascaderDevWarnings<T>(options: CascaderDevOptions<T>) {\n  const {\n    items,\n    getParent,\n    mode,\n    multiple,\n    cascade,\n    indicator,\n    selectable,\n    searchScope,\n    max,\n    value,\n    defaultValue,\n    hasExpanded,\n    hasPath,\n    hasOnSearch,\n  } = options\n\n  // O(n) over the raw input, so the memo is skipped outright in production.\n  const issues = React.useMemo(\n    () =>\n      process.env.NODE_ENV === \"production\"\n        ? null\n        : findCascaderDataIssues(items, getParent),\n    [items, getParent]\n  )\n\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return\n\n    for (const duplicate of issues?.duplicates ?? []) {\n      warnCascaderOnce(\n        `duplicate-value:${duplicate}`,\n        `Duplicate node value ${JSON.stringify(duplicate)}. \\`value\\` is the selection key, so only the first occurrence is indexed and the rest never render.`\n      )\n    }\n\n    for (const cycle of issues?.cycles ?? []) {\n      warnCascaderOnce(\n        `cycle:${cycle}`,\n        `\\`getParent\\` puts ${JSON.stringify(cycle)} on a cycle. The depth walk is cycle-guarded so nothing hangs, but every depth on that chain is clamped rather than derived.`\n      )\n    }\n  }, [issues])\n\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return\n\n    for (const [prop, current] of [\n      [\"value\", value],\n      [\"defaultValue\", defaultValue],\n    ] as const) {\n      if (current == null) continue\n      if (multiple && !Array.isArray(current)) {\n        warnCascaderOnce(\n          `value-shape:multiple:${prop}`,\n          `\\`multiple\\` is set but \\`${prop}\\` is a string. Multi-select reads and writes a \\`string[]\\`; a string is treated as no selection at all.`\n        )\n      } else if (!multiple && Array.isArray(current)) {\n        warnCascaderOnce(\n          `value-shape:single:${prop}`,\n          `\\`${prop}\\` is an array but \\`multiple\\` is not set. Single-select reads and writes a \\`string\\`; only the first entry would survive a commit.`\n        )\n      }\n    }\n\n    if (cascade && !multiple) {\n      warnCascaderOnce(\n        \"cascade-without-multiple\",\n        \"`cascade` does nothing without `multiple`: a single selection has no subtree to propagate over.\"\n      )\n    }\n\n    if (cascade && selectable === \"leaf\") {\n      warnCascaderOnce(\n        \"cascade-with-leaf-selectable\",\n        '`cascade` does nothing while `selectable=\"leaf\"`: no branch can be committed, so no commit ever has a subtree under it. Pass `selectable=\"any\"` or a predicate that accepts branches.'\n      )\n    }\n\n    if (!indicator && multiple) {\n      warnCascaderOnce(\n        \"indicator-false-with-multiple\",\n        \"`indicator={false}` does nothing with `multiple`: the checkbox is the selection control rather than a decoration, so it and its gutter stay. Mark selection your own way from `data-selected` on the row, and drop the prop.\"\n      )\n    }\n\n    if (max != null && !multiple) {\n      warnCascaderOnce(\n        \"max-without-multiple\",\n        \"`max` does nothing without `multiple`: there is only ever one selection to cap.\"\n      )\n    }\n\n    if (hasExpanded && mode !== \"tree\") {\n      warnCascaderOnce(\n        `expanded-outside-tree:${mode}`,\n        `\\`expanded\\` only does something in \\`mode=\"tree\"\\`, and this cascader is in \\`mode=\"${mode}\"\\`. Drill and columns navigate with \\`path\\`.`\n      )\n    }\n\n    if (hasPath && mode === \"tree\") {\n      warnCascaderOnce(\n        \"path-in-tree\",\n        '`path` does nothing in `mode=\"tree\"`: branches expand in place rather than replacing the level. Use `expanded`.'\n      )\n    }\n\n    if (searchScope === \"deep\" && mode === \"tree\") {\n      warnCascaderOnce(\n        \"deep-search-in-tree\",\n        '`searchScope=\"deep\"` does nothing in `mode=\"tree\"`: a tree query already matches at any depth and auto-expands the ancestors of every hit.'\n      )\n    }\n\n    if (searchScope === \"global\" && mode === \"tree\") {\n      warnCascaderOnce(\n        \"global-search-in-tree\",\n        '`searchScope=\"global\"` does nothing in `mode=\"tree\"`: a tree query already searches the whole tree and auto-expands the ancestors of every hit.'\n      )\n    }\n\n    if (hasOnSearch && mode === \"tree\") {\n      warnCascaderOnce(\n        \"onsearch-in-tree\",\n        '`onSearch` does nothing in `mode=\"tree\"`: a tree query filters the loaded tree in place, and a server hit belongs to no visible branch, so its results would never render. The request is not fired. Filter the tree locally, or use drill or columns for server search.'\n      )\n    }\n  }, [\n    mode,\n    multiple,\n    cascade,\n    indicator,\n    selectable,\n    searchScope,\n    max,\n    value,\n    defaultValue,\n    hasExpanded,\n    hasPath,\n    hasOnSearch,\n  ])\n}\n\n/** What the `[]`-dep callbacks would otherwise close over. One ref, one\n * effect, so \"the last committed render\" is defined in exactly one place. */\ninterface CascaderLatest<T> {\n  index: CascaderIndex<T>\n  state: CascaderStateContextValue<T>\n  mode: CascaderMode\n  multiple: boolean\n  cascade: boolean\n  max: number | undefined\n  labels: CascaderLabels\n  closeOnSelect: boolean\n  selectedValues: string[]\n  deepResults: CascaderNode<T>[] | null\n  onValueChange:\n    | ((value: string | string[], details: CascaderChangeDetails<T>) => void)\n    | undefined\n  resolveNode: (value: string) => CascaderNode<T>\n  isSelectable: (node: CascaderNode<T>) => boolean\n  needsChildren: (node: CascaderNode<T>) => boolean\n  expanded: ReadonlySet<string>\n}\n\n/** A navigation waiting on a fetch. The `kind`s differ: a drill appends, a\n * columns press replaces the trail from `depth`, a search hit swaps in its own\n * `trail`, a tree press expands. */\ntype CascaderPendingNavigation =\n  | {\n      value: string\n      kind: \"push\" | \"at\" | \"expand\"\n      /** `kind: \"at\"` only. The depth the pressed row belongs to. */\n      depth: number\n    }\n  | {\n      value: string\n      kind: \"trail\"\n      /** The hit's ancestor chain, root first, the hit itself last. */\n      trail: string[]\n    }\n\n/** No default on `T`: inferred from `items`, so `node.data` arrives typed. */\nfunction Cascader<T>({\n  items,\n  getParent,\n  getChildren,\n  onSearch,\n  searchDebounce,\n  resolveValue,\n  loadKey,\n  prefetch,\n  onLoadError,\n  value: valueProp,\n  defaultValue,\n  onValueChange: onValueChangeProp,\n  multiple = false,\n  selectable = \"leaf\",\n  indicator = true,\n  actions,\n  cascade = false,\n  max,\n  mode = \"drill\",\n  expandTrigger,\n  path: pathProp,\n  defaultPath,\n  onPathChange,\n  expanded: expandedProp,\n  defaultExpanded,\n  onExpandedChange,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  closeOnSelect = true,\n  inputValue: inputValueProp,\n  defaultInputValue = \"\",\n  onInputValueChange,\n  searchScope = \"level\",\n  filter,\n  revealSelected = true,\n  maxHeight,\n  virtualize,\n  virtualizeThreshold = 100,\n  estimateRowSize = 32,\n  overscan = 8,\n  labels: labelsProp,\n  renderItem,\n  renderLabel,\n  disabled,\n  name,\n  form,\n  id,\n  required,\n  readOnly,\n  invalid,\n  inputRef,\n  inline,\n  children,\n}: CascaderProps<T>) {\n  const baseIndex = React.useMemo(\n    () => buildCascaderIndex(items, getParent),\n    // `getParent` is stable by design: an inline accessor must not rebuild a\n    // 50k-node index.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [items]\n  )\n\n  const baseId = React.useId()\n\n  // Shallow-stabilized first: an inline `labels` would republish the context.\n  const stableLabelsProp = useShallowStable(labelsProp, shallowEqualRecords)\n\n  const labels = React.useMemo(\n    () => resolveCascaderLabels(stableLabelsProp),\n    [stableLabelsProp]\n  )\n\n  useCascaderDevWarnings({\n    items,\n    getParent,\n    mode,\n    multiple,\n    cascade,\n    indicator,\n    selectable,\n    searchScope,\n    max,\n    value: valueProp,\n    defaultValue,\n    hasExpanded:\n      expandedProp !== undefined ||\n      defaultExpanded !== undefined ||\n      onExpandedChange !== undefined,\n    hasPath:\n      pathProp !== undefined ||\n      defaultPath !== undefined ||\n      onPathChange !== undefined,\n    hasOnSearch: onSearch !== undefined,\n  })\n\n  // Public props are discriminated, internals are not: one assertion.\n  const onValueChange = onValueChangeProp as\n    | ((value: string | string[], details: CascaderChangeDetails<T>) => void)\n    | undefined\n\n  // No `onChange`: `onValueChange` takes a second argument, so `emitSelection`\n  // invokes it rather than the state hook, which only knows the value.\n  const [value, setValue] = useControllable<string | string[]>(\n    valueProp,\n    defaultValue ?? (multiple ? [] : \"\"),\n    undefined,\n    \"value\"\n  )\n  // Why the current path (or open) change is happening. Refs, not arguments:\n  // `useControllable` only passes a value, and its onChange runs synchronously\n  // inside the setter, so a ref written just before is exact. The path ref\n  // resets to `\"external\"`, which is what the consumer's `setPath` reports.\n  const pathReasonRef = React.useRef<CascaderPathChangeReason>(\"external\")\n  const openReasonRef = React.useRef<string>(\"none\")\n\n  const [path, setPath] = useControllable<string[]>(\n    pathProp,\n    defaultPath ?? [],\n    (next) => onPathChange?.(next, { reason: pathReasonRef.current }),\n    \"path\"\n  )\n  const [expandedList, setExpandedList] = useControllable<string[]>(\n    expandedProp,\n    defaultExpanded ?? [],\n    onExpandedChange,\n    \"expanded\"\n  )\n  const [open, setOpen] = useControllable<boolean>(\n    openProp,\n    defaultOpen,\n    (next) => onOpenChange?.(next, { reason: openReasonRef.current }),\n    \"open\"\n  )\n\n  /** The internal `setPath`: names its reason, then hands back to \"external\". */\n  const setPathWithReason = React.useCallback(\n    (\n      next: string[] | ((prev: string[]) => string[]),\n      reason: CascaderPathChangeReason\n    ) => {\n      pathReasonRef.current = reason\n      try {\n        setPath(next)\n      } finally {\n        pathReasonRef.current = \"external\"\n      }\n    },\n    [setPath]\n  )\n  const [query, setQuery] = useControllable<string>(\n    inputValueProp,\n    defaultInputValue,\n    onInputValueChange,\n    \"inputValue\"\n  )\n\n  const expanded = React.useMemo(() => new Set(expandedList), [expandedList])\n  const selectedValues = React.useMemo(() => toArray(value), [value])\n\n  /* --------------------------------- async -------------------------------- */\n\n  // On-screen levels, so the ones worth fetching. Columns shows the whole\n  // trail at once, which is why the loader is per-level, not one flag.\n  const levels = React.useMemo(() => {\n    if (mode === \"tree\") return [CASCADER_ROOT_KEY, ...expandedList]\n    if (mode === \"columns\") return [CASCADER_ROOT_KEY, ...path]\n    return [path.length ? path[path.length - 1] : CASCADER_ROOT_KEY]\n  }, [mode, path, expandedList])\n\n  // A SIBLING of the index build, never a step inside it: the build stays pure\n  // in `items`, the loader owns the pages, the merge is pure in both, and that\n  // is what makes loaded data survive an `items` change.\n  const loader = useCascaderLoader<T>({\n    base: baseIndex,\n    getChildren,\n    // Gated, not merely unrendered: a tree hit belongs to no visible branch.\n    onSearch: mode === \"tree\" ? undefined : onSearch,\n    resolveValue,\n    searchDebounce,\n    loadKey,\n    prefetch,\n    onLoadError,\n    // An inline cascader is never \"open\", but its panel is live.\n    enabled: open || !!inline,\n    query,\n    levels,\n    path: searchScope === \"global\" ? GLOBAL_SEARCH_PATH : path,\n    values: selectedValues,\n  })\n\n  const loadStates = loader.states\n\n  // Destructured here, not at the actions memo, because `navigate` sits above\n  // it. All three are `[]`-dep inside the loader, so identity is stable.\n  const { ensureLevel, retryLevel: retryLoaderLevel, invalidateLevel } = loader\n\n  /**\n   * Whether pressing this branch has to FETCH first: in flight, failed, or\n   * never asked. `baseIndex`, as `ensureLevel` asks it: the two must agree or a\n   * press parks a navigation nothing will ever settle.\n   */\n  const needsChildren = React.useCallback(\n    (node: CascaderNode<T>) => {\n      if (!loader.active) return false\n      const state = loadStates.get(node.value)\n      if (state) return state.loading || state.error\n      return !baseIndex.childrenOf.has(node.value)\n    },\n    [loader.active, loadStates, baseIndex]\n  )\n\n  // Levels with pages left, the only ones a resolved chain node is listed in.\n  // A string first, so a loading flip that leaves `hasMore` alone keeps the\n  // set, and the index, as they were.\n  const pagingKey = JSON.stringify(\n    Array.from(loadStates)\n      .filter(([, state]) => state.hasMore)\n      .map(([key]) => key)\n  )\n  const paging = React.useMemo(\n    () => new Set<string>(JSON.parse(pagingKey) as string[]),\n    [pagingKey]\n  )\n\n  // Keyed on the maps the merge reads, NOT the store: `withLoadState`\n  // returns a fresh store over the SAME maps, so keying on it rebuilt the\n  // whole index on every loading flip that changed no data.\n  const { pages, detached, links } = loader.store\n  const index = React.useMemo(\n    () => mergeCascaderIndex(baseIndex, pages, detached, links, paging),\n    [baseIndex, pages, detached, links, paging]\n  )\n\n  /** Whether a level needs the paging pseudo-row. A never-fetched level gets\n   * nothing; that is the empty state's job. Tree has no per-level empty state,\n   * so there the row is also the branch's loading and error surface. */\n  const needsMoreRow = React.useCallback(\n    (key: string, childCount: number) => {\n      const state = loadStates.get(key)\n      if (!state) return false\n      if (childCount > 0) return state.hasMore || state.loading || state.error\n      return mode === \"tree\" && (state.loading || state.error)\n    },\n    [loadStates, mode]\n  )\n\n  // Remembers every selected node, so the trigger keeps a label after it\n  // leaves the loaded set (or was never in it).\n  const labelCacheRef = React.useRef(new Map<string, CascaderNode<T>>())\n\n  // Populated during render, since the trigger reads it while rendering.\n  // Idempotent and prop-derived, so a discarded render is harmless.\n  for (const selected of selectedValues) {\n    const node = index.byValue.get(selected)\n    if (node) labelCacheRef.current.set(selected, node)\n  }\n\n  const resolveNode = React.useCallback(\n    (nodeValue: string): CascaderNode<T> =>\n      index.byValue.get(nodeValue) ??\n      labelCacheRef.current.get(nodeValue) ?? {\n        value: nodeValue,\n        label: nodeValue,\n      },\n    [index]\n  )\n\n  /* ------------------------------ derived view ----------------------------- */\n\n  const currentParentValue = path.length ? path[path.length - 1] : null\n  const currentParent = currentParentValue\n    ? (index.byValue.get(currentParentValue) ?? null)\n    : null\n\n  const currentLevelKey = currentParentValue ?? CASCADER_ROOT_KEY\n\n  const isDeepSearching =\n    (searchScope === \"deep\" || searchScope === \"global\") &&\n    query.trim().length > 0 &&\n    mode !== \"tree\"\n  // Global is the same scan from the root, whatever level is on screen.\n  const deepSearchWithin = searchScope === \"global\" ? null : currentParentValue\n\n  const localDeepResults = React.useMemo(() => {\n    if (!isDeepSearching) return null\n    return searchCascaderDeep(index, query, {\n      within: deepSearchWithin,\n      matches: filter,\n    })\n  }, [isDeepSearching, index, query, deepSearchWithin, filter])\n\n  // Server search wins over the local scan, or each hit shows up twice.\n  const deepResults = loader.searchResults ?? localDeepResults\n\n  const levelItems = React.useMemo(() => {\n    const level = getCascaderChildren(index, currentParentValue)\n    return filterCascaderLevel(level, query, filter)\n  }, [index, currentParentValue, query, filter])\n\n  /** The active level's rows plus the paging pseudo-row, appended HERE because\n   * every index Base UI hands out indexes this array: a DOM row missing from\n   * it shifts the highlight and every windowed row by one. */\n  const levelRendered = React.useMemo(() => {\n    if (deepResults) return deepResults\n    if (!needsMoreRow(currentLevelKey, levelItems.length)) return levelItems\n    return withCascaderMoreRow(\n      levelItems,\n      createCascaderMoreNode<T>(currentLevelKey, levelItems.length),\n      index.pinned\n    )\n  }, [deepResults, levelItems, currentLevelKey, needsMoreRow, index.pinned])\n\n  // Columns renders the whole trail, but only the deepest column is\n  // interactive through Base UI, so `renderedItems` stays what drill uses.\n  const columns = React.useMemo<CascaderColumn<T>[]>(() => {\n    if (mode !== \"columns\") return []\n    const trail: CascaderColumn<T>[] = []\n    for (let depth = 0; depth <= path.length; depth += 1) {\n      const parentValue = depth === 0 ? null : path[depth - 1]\n      const parent = parentValue\n        ? (index.byValue.get(parentValue) ?? null)\n        : null\n      const isActive = depth === path.length\n      let columnItems: CascaderNode<T>[]\n      if (isActive) {\n        columnItems = levelRendered\n      } else {\n        // A trail column pages on its own, without losing the columns right of it.\n        const key = parentValue ?? CASCADER_ROOT_KEY\n        const children = getCascaderChildren(index, parentValue)\n        columnItems = needsMoreRow(key, children.length)\n          ? withCascaderMoreRow(\n              children,\n              createCascaderMoreNode<T>(key, children.length),\n              index.pinned\n            )\n          : children\n      }\n      trail.push({\n        parent,\n        depth,\n        activeValue: path[depth] ?? null,\n        active: isActive,\n        // Only the active column is filtered: a query narrows what you type\n        // into, never the trail behind it.\n        items: columnItems,\n      })\n    }\n    return trail\n  }, [mode, path, index, levelRendered, needsMoreRow])\n\n  // Tree level keys needing a paging pseudo-row. With no per-level empty\n  // state, this is also where a still-loading branch says so.\n  const treeSentinels = React.useMemo(() => {\n    if (mode !== \"tree\") return undefined\n    const keys = new Set<string>()\n    const consider = (key: string) => {\n      if (needsMoreRow(key, index.childrenOf.get(key)?.length ?? 0)) {\n        keys.add(key)\n      }\n    }\n    consider(CASCADER_ROOT_KEY)\n    for (const value of expanded) consider(value)\n    return keys.size ? keys : undefined\n  }, [mode, index, expanded, needsMoreRow])\n\n  const treeRows = React.useMemo<CascaderFlatNode<T>[]>(() => {\n    if (mode !== \"tree\") return []\n    const rows = flattenCascaderTree(index, expanded, treeSentinels)\n    // Same normalizer as the level filter, so the two cannot disagree.\n    const normalized = normalizeCascaderQuery(query)\n    if (!normalized) return rows\n    // Matches every node, not just visible rows, and keeps the ancestors, or a\n    // hit inside a collapsed branch disappears. Same matcher as every other\n    // path, so keyword-only hits do not vanish in tree mode alone.\n    const keep = new Set<string>()\n    // Only a hit some level shows: a resolved node no loaded level lists kept\n    // its ancestors expanded over nothing, while the status said no results.\n    const listed = getCascaderListedValues(index)\n    for (const node of index.all) {\n      if (!listed.has(node.value)) continue\n      if (\n        !(filter\n          ? filter(node, normalized)\n          : matchesCascaderQuery(node, normalized))\n      ) {\n        continue\n      }\n      for (const ancestor of getCascaderPath(index, node.value)) {\n        keep.add(ancestor.value)\n      }\n    }\n    // No sentinels while filtering: that page would not have the query applied.\n    return flattenCascaderTree(index, new Set([...expanded, ...keep])).filter(\n      (row) => keep.has(row.node.value)\n    )\n  }, [mode, index, expanded, treeSentinels, query, filter])\n\n  // What `Combobox.Root` receives. Tree flattens to the same linear list, so\n  // selection and highlighting need no per-mode case.\n  const renderedItems = React.useMemo(\n    () => (mode === \"tree\" ? treeRows.map((row) => row.node) : levelRendered),\n    [mode, treeRows, levelRendered]\n  )\n\n  /* ---------------------------- level swap reset --------------------------- */\n\n  /** Base UI never resets the highlighted index on an `items` change and has\n   * no imperative setter; its one automatic reset is an out-of-range clamp, so\n   * a single empty frame trips it. The frame rides `filteredItems`, so the\n   * `items` identity stays put. */\n  const levelKey = currentParentValue ?? \"\"\n\n  // STATE, not a ref: a replayed render would see a ref advanced and skip it.\n  const [swap, setSwap] = React.useState({ key: levelKey, active: false })\n\n  if (swap.key !== levelKey) {\n    setSwap({ key: levelKey, active: true })\n  }\n  const swapping = swap.active && swap.key === levelKey\n\n  React.useLayoutEffect(() => {\n    if (swap.active) setSwap((prev) => ({ ...prev, active: false }))\n  }, [swap.active])\n\n  // The same frame on demand, for a navigation that lands on the level already\n  // on screen: the key never moves, so the check above never fires.\n  const resetHighlight = React.useCallback(() => {\n    setSwap((prev) => (prev.active ? prev : { ...prev, active: true }))\n  }, [])\n\n  /* ----------------------------- virtualization ---------------------------- */\n\n  // A COUNT: columns mounts one per column, and StrictMode remounts effects.\n  const [virtualRenderers, setVirtualRenderers] = React.useState(0)\n\n  const registerVirtualRenderer = React.useCallback(() => {\n    setVirtualRenderers((count) => count + 1)\n    return () => setVirtualRenderers((count) => Math.max(0, count - 1))\n  }, [])\n\n  const hasVirtualRenderer = virtualRenderers > 0\n  const wantsVirtual = virtualize ?? renderedItems.length >= virtualizeThreshold\n\n  // LATCHED per level: otherwise a query narrowing a 5,000 row level past the\n  // threshold tears the virtualizer down and rebuilds it on backspace, losing\n  // every measurement. State, not a ref, like `swap`.\n  const [virtualLatch, setVirtualLatch] = React.useState({\n    key: levelKey,\n    on: false,\n  })\n\n  const virtualized =\n    hasVirtualRenderer &&\n    (virtualLatch.key === levelKey\n      ? virtualLatch.on || wantsVirtual\n      : wantsVirtual)\n\n  if (!hasVirtualRenderer) {\n    // Nothing is windowing, so nothing may stay latched.\n    if (virtualLatch.on || virtualLatch.key !== levelKey) {\n      setVirtualLatch({ key: levelKey, on: false })\n    }\n  } else if (virtualLatch.key !== levelKey) {\n    setVirtualLatch({ key: levelKey, on: virtualized })\n  } else if (virtualized && !virtualLatch.on) {\n    setVirtualLatch({ key: levelKey, on: true })\n  }\n\n  /**\n   * One forced re-render, one commit AFTER windowing turns on. Base UI sizes\n   * `listRef` from `filteredItems` in a layout effect keyed on that array's\n   * identity, but `virtualized` reaches the list a commit later and the\n   * `CompositeList` teardown REPLACES `listRef` with an empty array nothing\n   * re-sizes, so ArrowDown stops at the last WINDOWED row. A passive effect is\n   * late enough: the teardown is scheduled from a layout effect.\n   */\n  const [virtualSyncs, setVirtualSyncs] = React.useState(0)\n\n  React.useEffect(() => {\n    if (!virtualized) return\n    setVirtualSyncs((count) => count + 1)\n  }, [virtualized])\n\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return undefined\n    if (virtualize !== true || hasVirtualRenderer) return undefined\n    // Deferred a tick: the renderer registers from a layout effect, so a\n    // synchronous check would report every correctly wired cascader.\n    const timeout = setTimeout(() => {\n      console.error(\n        \"[Cascader] `virtualize` is true but no windowing renderer is mounted. \" +\n          \"Render <CascaderVirtualItems /> inside <CascaderList>, or \" +\n          \"<CascaderVirtualColumn /> through the <CascaderColumns> slot.\"\n      )\n    }, 0)\n    return () => clearTimeout(timeout)\n  }, [virtualize, hasVirtualRenderer])\n\n  /* ------------------------------ navigation ------------------------------ */\n\n  /** One signal per navigation, feeding the live region, bumped EXACTLY once\n   * per action. `seq` alternates the invisible marker so two identical\n   * announcements are still two mutations; `toggled` names the tree branch. */\n  const [announceSignal, setAnnounceSignal] = React.useState<{\n    seq: number\n    toggled: string | null\n    /** A one-off verbatim message: a `max` refusal, a cascade fan-out. STATE,\n     * so an unrelated render cannot clear it mid-read. */\n    notice: string | null\n  }>({ seq: 0, toggled: null, notice: null })\n\n  const bumpAnnouncement = React.useCallback((toggled: string | null) => {\n    setAnnounceSignal((prev) => ({ seq: prev.seq + 1, toggled, notice: null }))\n  }, [])\n\n  /** Speaks for interactions that change nothing on screen: a refusal returns\n   * before any state moves, a cascade sweep is invisible from the row pressed.\n   * The seq bump keeps two identical refusals two mutations. */\n  const announceNotice = React.useCallback((notice: string) => {\n    setAnnounceSignal((prev) => ({ seq: prev.seq + 1, toggled: null, notice }))\n  }, [])\n\n  /**\n   * The navigation a press asked for and a fetch has not answered yet. Load\n   * BEFORE you move: drilling in first threw away the list the user was\n   * reading and, on failure, stranded them on an error screen for a level they\n   * had never seen. Holding here keeps the old rows and puts the progress on\n   * the pressed row, whose chevron `CascaderItem` swaps for a spinner.\n   */\n  const [pendingNavigation, setPendingNavigation] =\n    React.useState<CascaderPendingNavigation | null>(null)\n\n  /** Asks for a branch's children and parks the navigation until they land.\n   * BOTH entry points are called unconditionally: `retryLevel` no-ops unless\n   * the level errored, `ensureLevel` no-ops if it has state, so a repeat press\n   * is free and the readiness decision stays inside the loader. */\n  const requestChildren = React.useCallback(\n    (node: CascaderNode<T>, pending: CascaderPendingNavigation) => {\n      setPendingNavigation(pending)\n      retryLoaderLevel(node.value)\n      ensureLevel(node.value, \"level\")\n      // Otherwise the panel is silent from the press until the level swaps.\n      bumpAnnouncement(null)\n    },\n    [retryLoaderLevel, ensureLevel, bumpAnnouncement]\n  )\n\n  const pushLevel = React.useCallback(\n    (nodeValue: string) => {\n      setPathWithReason((prev) => [...prev, nodeValue], \"drill\")\n      setQuery(\"\")\n      bumpAnnouncement(null)\n    },\n    [setPathWithReason, setQuery, bumpAnnouncement]\n  )\n\n  const popLevel = React.useCallback(() => {\n    setPathWithReason(\n      (prev) => (prev.length ? prev.slice(0, -1) : prev),\n      \"back\"\n    )\n    setQuery(\"\")\n    // Going BACK abandons a held navigation, or an in-flight branch drills in\n    // the moment it lands, long after the user moved on.\n    setPendingNavigation(null)\n    bumpAnnouncement(null)\n  }, [setPathWithReason, setQuery, bumpAnnouncement])\n\n  const goToDepth = React.useCallback(\n    (depth: number) => {\n      setPathWithReason(\n        (prev) => prev.slice(0, Math.max(0, depth)),\n        \"breadcrumb\"\n      )\n      setQuery(\"\")\n      setPendingNavigation(null)\n      bumpAnnouncement(null)\n    },\n    [setPathWithReason, setQuery, bumpAnnouncement]\n  )\n\n  const toggleExpanded = React.useCallback(\n    (nodeValue: string) => {\n      setExpandedList((prev) =>\n        prev.includes(nodeValue)\n          ? prev.filter((v) => v !== nodeValue)\n          : [...prev, nodeValue]\n      )\n      bumpAnnouncement(nodeValue)\n    },\n    [setExpandedList, bumpAnnouncement]\n  )\n\n  const isBranch = React.useCallback(\n    (node: CascaderNode<T>) => isCascaderBranch(index, node),\n    [index]\n  )\n\n  const isSelectable = React.useCallback(\n    (node: CascaderNode<T>) => isCascaderSelectable(index, node, selectable),\n    [index, selectable]\n  )\n\n  const isDisabled = React.useCallback(\n    (node: CascaderNode<T>) => isCascaderDisabled(index, node),\n    [index]\n  )\n\n  // Whether a branch can be committed AT ALL here. `\"leaf\"` is the only\n  // setting that answers no in advance; a predicate is opaque, so the check\n  // gutter is reserved uniformly rather than per row.\n  const branchesSelectable = selectable !== \"leaf\"\n\n  // Open flyouts, by key. A ref, not state: `handleOpenChange` needs the\n  // answer as of the Escape being handled, and opening a footer menu must not\n  // re-render the root.\n  const openFlyoutsRef = React.useRef<Set<string>>(new Set())\n\n  const setFlyoutOpen = React.useCallback((key: string, open: boolean) => {\n    if (open) openFlyoutsRef.current.add(key)\n    else openFlyoutsRef.current.delete(key)\n  }, [])\n\n  const hasOpenFlyout = React.useCallback(\n    () => openFlyoutsRef.current.size > 0,\n    []\n  )\n\n  // Shallow-stabilized like `labels`: an inline array republished the context.\n  const stableActions = useShallowStable(actions, shallowEqualItemLists)\n\n  const resolvedActions = React.useMemo<CascaderActionItem[]>(\n    () => stableActions ?? EMPTY_ACTIONS,\n    [stableActions]\n  )\n\n  // A Set, not an `includes` scan: `isSelected` runs once per rendered row.\n  const selectedSet = React.useMemo(\n    () => new Set(selectedValues),\n    [selectedValues]\n  )\n\n  const isSelected = React.useCallback(\n    (node: CascaderNode<T>) => selectedSet.has(node.value),\n    [selectedSet]\n  )\n\n  /** Selected-descendant counts, walked ONCE per selection change and never\n   * stored, so the flat value array stays the only source of truth. The walk\n   * is `selections x depth`, up from each selection rather than down from each\n   * row, which keeps the count and `isIndeterminate` O(1). */\n  const selectedDescendants = React.useMemo(\n    () => getCascaderSelectedDescendants(index, selectedValues),\n    [index, selectedValues]\n  )\n\n  const selectedDescendantCount = React.useCallback(\n    (node: CascaderNode<T>) => selectedDescendants.get(node.value) ?? 0,\n    [selectedDescendants]\n  )\n\n  // Partial selections read off the SAME map, so the dash and the number are\n  // two readings of one traversal and cannot drift apart.\n  const indeterminateSet = React.useMemo(() => {\n    if (!cascade || !multiple) return null\n    return getCascaderIndeterminateFrom(selectedDescendants, selectedValues)\n  }, [cascade, multiple, selectedDescendants, selectedValues])\n\n  const isIndeterminate = React.useCallback(\n    (node: CascaderNode<T>) => indeterminateSet?.has(node.value) ?? false,\n    [indeterminateSet]\n  )\n\n  // Base UI owns the highlight; mirrored so handlers can read it unsubscribed.\n  const highlightedRef = React.useRef<CascaderNode<T> | null>(null)\n  const getHighlighted = React.useCallback(() => highlightedRef.current, [])\n\n  // The same highlight as an external store, for the consumer that must\n  // RE-RENDER on it. `useState`, not `useMemo`: React may drop a memo, and a\n  // second store would silently orphan every subscriber.\n  const [highlightStore] = React.useState<CascaderHighlightStore>(\n    createCascaderHighlightStore\n  )\n\n  /* ------------------------------ announcement ---------------------------- */\n\n  const currentLoadState = React.useMemo<CascaderLoadState | null>(\n    () => loadStates.get(currentLevelKey) ?? null,\n    [loadStates, currentLevelKey]\n  )\n  const searchLoadState = loader.searchState\n\n  /** What the query announcement COUNTS: matches, not rows, since the list\n   * also carries a tree query's ancestor context and the Load-more row. Server\n   * hits count as-is; re-judging them locally would drop what it matched on\n   * data the client cannot see. */\n  const announcedMatches = React.useMemo(() => {\n    if (deepResults) return deepResults.length\n    const normalized = normalizeCascaderQuery(query)\n    if (!normalized) return 0\n    const matches = filter ?? matchesCascaderQuery\n    return renderedItems.filter(\n      (node) => !isCascaderMoreNode(node) && matches(node, normalized)\n    ).length\n  }, [deepResults, renderedItems, query, filter])\n\n  // Real rows only: a tree's first load renders its loading row, and counting\n  // that one called the first fetch \"Loading more\".\n  const hasRows = renderedItems.some((node) => !isCascaderMoreNode(node))\n\n  // The live-region text plus whether it may be DEFERRED; see\n  // `ANNOUNCE_DEBOUNCE`. The committed string is state, set below.\n  const announcementTarget = React.useMemo(() => {\n    // An INLINE cascader is never \"open\", so `open` alone silenced its tree.\n    if (!open && !inline) return { text: \"\", defer: false }\n\n    const total = renderedItems.length\n    const marker = ANNOUNCE_MARKER.repeat(announceSignal.seq % 2)\n\n    // A notice outranks everything below: it exists because nothing else on\n    // screen changed. It rides the signal, so the next navigation replaces it.\n    if (announceSignal.notice) {\n      return { text: `${announceSignal.notice}${marker}`, defer: false }\n    }\n\n    // A tree toggle changes neither level nor query, so it rides the signal.\n    const toggled =\n      mode === \"tree\" && announceSignal.toggled\n        ? (index.byValue.get(announceSignal.toggled) ?? null)\n        : null\n\n    // Async states outrank everything below, or a fetching level announces \"0\n    // results\" and contradicts itself. A HELD navigation outranks even those:\n    // nothing on screen has moved yet, so \"loading\" is the only truth.\n    const pendingState = pendingNavigation\n      ? (loadStates.get(pendingNavigation.value) ?? null)\n      : null\n    const levelState = query.trim() ? searchLoadState : currentLoadState\n    let text: string\n    let defer = false\n    if (pendingState?.error) {\n      text = labels.error\n    } else if (pendingNavigation) {\n      // Always the FIRST-page string: the rows on screen belong to the level\n      // being left behind, so their count says nothing about this request.\n      text = labels.loading\n    } else if (levelState?.error) {\n      text = labels.error\n    } else if (levelState?.loading) {\n      // With a query set the in-flight load IS the server search, and calling\n      // that \"Loading more\" misreads what is being waited on.\n      text = query.trim()\n        ? labels.searchingAnnouncement\n        : hasRows\n          ? labels.loadingMore\n          : labels.loading\n    } else if (query.trim()) {\n      // An empty result set is the more useful thing to say than \"0 results\".\n      text =\n        announcedMatches === 0\n          ? labels.empty\n          : labels.resultsAnnouncement(announcedMatches)\n      // The ONE deferred branch: only a stream of keystrokes needs coalescing.\n      defer = true\n    } else if (toggled) {\n      text = expanded.has(toggled.value)\n        ? labels.expandedAnnouncement(\n            toggled.label,\n            getCascaderCount(index, toggled)\n          )\n        : labels.collapsedAnnouncement(toggled.label)\n    } else if (currentParent) {\n      // `path.length + 1` matches tree mode's `aria-level`: the rows are\n      // `currentParent`'s CHILDREN, one level below it.\n      text = labels.levelAnnouncement(\n        currentParent.label,\n        path.length + 1,\n        total\n      )\n    } else {\n      // Returning to the root would otherwise announce nothing at all.\n      text = labels.rootAnnouncement(total)\n    }\n\n    return { text: `${text}${marker}`, defer }\n  }, [\n    open,\n    inline,\n    mode,\n    index,\n    expanded,\n    announceSignal,\n    query,\n    labels,\n    renderedItems.length,\n    hasRows,\n    announcedMatches,\n    currentParent,\n    path.length,\n    currentLoadState,\n    searchLoadState,\n    pendingNavigation,\n    loadStates,\n  ])\n\n  /** The COMMITTED announcement. Immediate texts commit during render (the\n   * pattern `swap` uses), so an async settle announces in the very commit that\n   * shows the rows. Only the deferred query count uses the timer below. */\n  const [announced, setAnnounced] = React.useState(announcementTarget)\n\n  if (!announcementTarget.defer && announced.text !== announcementTarget.text) {\n    // Terminates: after the set the condition is false on the re-render.\n    setAnnounced(announcementTarget)\n  }\n\n  React.useEffect(() => {\n    if (!announcementTarget.defer) return undefined\n    const timer = setTimeout(\n      () => setAnnounced(announcementTarget),\n      ANNOUNCE_DEBOUNCE\n    )\n    return () => clearTimeout(timer)\n  }, [announcementTarget])\n\n  const announcement = announced.text\n\n  /* --------------------------------- state -------------------------------- */\n\n  // The volatile half: rebuilt by a keystroke, hence split from the actions.\n  const stateValue = React.useMemo<CascaderStateContextValue<T>>(\n    () => ({\n      index,\n      path,\n      expanded,\n      query,\n      currentParent,\n      levelItems,\n      deepResults,\n      renderedItems,\n      columns,\n      treeRows,\n      selectedValues,\n      selectedDescendants,\n      loadStates,\n      searchState: searchLoadState,\n      announcement,\n    }),\n    [\n      index,\n      path,\n      expanded,\n      query,\n      currentParent,\n      levelItems,\n      deepResults,\n      renderedItems,\n      columns,\n      treeRows,\n      selectedValues,\n      selectedDescendants,\n      loadStates,\n      searchLoadState,\n      announcement,\n    ]\n  )\n\n  /* ------------------------------ latest props ---------------------------- */\n\n  /**\n   * Latest committed props and derived state, WRITTEN IN AN EFFECT. That is\n   * what keeps the callbacks below `[]`-dep and stable for the life of the\n   * cascader; writing during render is unsafe under concurrent rendering. The\n   * cost: a read sees the PREVIOUS commit until the effect runs, so ONLY event\n   * handlers may read it. Anything used during render is memoised instead.\n   */\n  const latest = React.useRef<CascaderLatest<T>>({\n    index,\n    state: stateValue,\n    mode,\n    multiple,\n    cascade,\n    max,\n    labels,\n    closeOnSelect,\n    selectedValues,\n    deepResults,\n    onValueChange,\n    resolveNode,\n    isSelectable,\n    needsChildren,\n    expanded,\n  })\n\n  React.useEffect(() => {\n    latest.current = {\n      index,\n      state: stateValue,\n      mode,\n      multiple,\n      cascade,\n      max,\n      labels,\n      closeOnSelect,\n      selectedValues,\n      deepResults,\n      onValueChange,\n      resolveNode,\n      isSelectable,\n      needsChildren,\n      expanded,\n    }\n  })\n\n  const getIndex = React.useCallback(() => latest.current.index, [])\n  const getState = React.useCallback(() => latest.current.state, [])\n\n  const goToLevelAt = React.useCallback(\n    (nodeValue: string, depth: number) => {\n      setPathWithReason(\n        (prev) => [...prev.slice(0, Math.max(0, depth)), nodeValue],\n        \"drill\"\n      )\n      setQuery(\"\")\n      bumpAnnouncement(null)\n    },\n    [setPathWithReason, setQuery, bumpAnnouncement]\n  )\n\n  const goToTrail = React.useCallback(\n    (trail: string[]) => {\n      // A global hit can BE the level on screen. Then nothing moved, so no path\n      // change is reported, and no level swap clears the highlight the hit\n      // list left at its index, where Enter would commit some other row.\n      const { path: current } = latest.current.state\n      const stays =\n        trail.length === current.length &&\n        trail.every((entry, i) => entry === current[i])\n      if (stays) resetHighlight()\n      else setPathWithReason(trail, \"drill\")\n      setQuery(\"\")\n      bumpAnnouncement(null)\n    },\n    [setPathWithReason, resetHighlight, setQuery, bumpAnnouncement]\n  )\n\n  const navigateAt = React.useCallback(\n    (node: CascaderNode<T>, depth: number) => {\n      if (isCascaderDisabled(latest.current.index, node)) return\n      if (latest.current.mode === \"tree\") {\n        toggleExpanded(node.value)\n        return\n      }\n      if (latest.current.needsChildren(node)) {\n        requestChildren(node, { value: node.value, kind: \"at\", depth })\n        return\n      }\n      goToLevelAt(node.value, depth)\n    },\n    [toggleExpanded, goToLevelAt, requestChildren]\n  )\n\n  const navigate = React.useCallback(\n    (node: CascaderNode<T>) => {\n      const {\n        mode: currentMode,\n        deepResults: currentDeepResults,\n        index: currentIndex,\n        expanded: currentExpanded,\n        needsChildren: pending,\n      } = latest.current\n\n      // Every drill and expand passes here, from a search hit, a key or a hover\n      // timer alike, so this is where a disabled subtree stays shut.\n      if (isCascaderDisabled(currentIndex, node)) return\n\n      if (currentMode === \"tree\") {\n        // Collapsing never needs data. Opening waits WITHOUT expanding, so\n        // the branch stays shut with a spinner rather than opening onto no rows.\n        if (!currentExpanded.has(node.value) && pending(node)) {\n          requestChildren(node, {\n            value: node.value,\n            kind: \"expand\",\n            depth: 0,\n          })\n          return\n        }\n        toggleExpanded(node.value)\n        return\n      }\n\n      // A search hit sits at any depth, and under a global search outside the\n      // current path, so drilling in swaps in the hit's own trail. A held drill\n      // carries it too: splicing the hit into the current path at its depth\n      // kept the wrong ancestors for anything below a direct child.\n      if (currentDeepResults) {\n        const trail = getCascaderPath(currentIndex, node.value).map(\n          (entry) => entry.value\n        )\n        if (pending(node)) {\n          requestChildren(node, { value: node.value, kind: \"trail\", trail })\n          return\n        }\n        goToTrail(trail)\n        return\n      }\n\n      if (pending(node)) {\n        requestChildren(node, { value: node.value, kind: \"push\", depth: 0 })\n        return\n      }\n      pushLevel(node.value)\n    },\n    [toggleExpanded, goToTrail, pushLevel, requestChildren]\n  )\n\n  /**\n   * The other half of `requestChildren`: the fetch settled, so the held\n   * navigation happens or is dropped. Reads the load STATE, not a promise:\n   * only the loader knows a request was superseded, aborted or retried, and no\n   * entry at all means it has not reached the store yet. On failure nothing\n   * moves and the intent is KEPT, which is what makes the row's retry\n   * affordance mean something; only `popLevel`, `goToDepth` and a close drop it.\n   */\n  React.useEffect(() => {\n    if (!pendingNavigation) return\n    const state = loadStates.get(pendingNavigation.value)\n    if (!state || state.loading || state.error) return\n\n    setPendingNavigation(null)\n\n    // Asked again against the index as it is NOW: a new `items` or a landed\n    // page can disable the target while its fetch was in flight.\n    const target = index.byValue.get(pendingNavigation.value)\n    if (target && isDisabled(target)) return\n\n    if (pendingNavigation.kind === \"expand\") {\n      toggleExpanded(pendingNavigation.value)\n      return\n    }\n    if (pendingNavigation.kind === \"trail\") {\n      goToTrail(pendingNavigation.trail)\n      return\n    }\n    if (pendingNavigation.kind === \"at\") {\n      goToLevelAt(pendingNavigation.value, pendingNavigation.depth)\n      return\n    }\n    pushLevel(pendingNavigation.value)\n  }, [\n    pendingNavigation,\n    loadStates,\n    index,\n    isDisabled,\n    toggleExpanded,\n    goToTrail,\n    goToLevelAt,\n    pushLevel,\n  ])\n\n  // A popup that closes mid-flight must not drill in when the answer arrives;\n  // the loader aborts the request, this drops the intent.\n  React.useEffect(() => {\n    if (!open && !inline) setPendingNavigation(null)\n  }, [open, inline])\n\n  /* ------------------------------- selection ------------------------------ */\n\n  // The one place the selection changes, so `onValueChange` reports the same\n  // shape whatever caused it: a row press, drag-select, or the headless hooks.\n  const emitSelection = React.useCallback(\n    (\n      nextValues: string[],\n      node: CascaderNode<T> | null,\n      reason: CascaderChangeReason\n    ) => {\n      const {\n        selectedValues: current,\n        multiple: currentMultiple,\n        onValueChange: emit,\n        index: currentIndex,\n        resolveNode: resolve,\n      } = latest.current\n\n      // Nothing changed. The dedup lives here now, not in `useControllable`.\n      const unchanged =\n        nextValues.length === current.length &&\n        nextValues.every((entry, i) => entry === current[i])\n      if (unchanged) return\n\n      const next = currentMultiple ? nextValues : (nextValues[0] ?? \"\")\n      setValue(next)\n\n      if (!emit) return\n\n      const chain = node ? getCascaderPath(currentIndex, node.value) : []\n      emit(next, {\n        node,\n        // A node absent from `items` has no chain, so fall back to the node\n        // rather than report an empty path for a change that had one.\n        path: node ? (chain.length ? chain : [node]) : [],\n        // `resolveNode`, not `index.byValue`: a selected value missing from\n        // `items` must still come back rather than be dropped.\n        nodes: nextValues.map(resolve),\n        reason,\n      })\n    },\n    [setValue]\n  )\n\n  const setSelection = React.useCallback(\n    (values: string[], reason?: CascaderChangeReason) => {\n      const { selectedValues: current, resolveNode: resolve } = latest.current\n      const before = new Set(current)\n      const after = new Set(values)\n      const added = values.find((entry) => !before.has(entry))\n      const removed = current.find((entry) => !after.has(entry))\n\n      if (reason === \"clear\") {\n        emitSelection(values, null, \"clear\")\n        return\n      }\n      if (added) {\n        emitSelection(values, resolve(added), reason ?? \"select\")\n        return\n      }\n      if (removed) {\n        emitSelection(values, resolve(removed), reason ?? \"deselect\")\n        return\n      }\n      // Same set: a no-op, but routed so an explicit `reason` still applies.\n      emitSelection(values, null, reason ?? \"select\")\n    },\n    [emitSelection]\n  )\n\n  const commit = React.useCallback(\n    (node: CascaderNode<T>) => {\n      const {\n        multiple: currentMultiple,\n        cascade: currentCascade,\n        selectedValues: current,\n        max: currentMax,\n        index: currentIndex,\n        isSelectable: currentIsSelectable,\n        labels: currentLabels,\n        closeOnSelect: currentCloseOnSelect,\n      } = latest.current\n\n      // Refuses a SELECT, never a removal: a disabled value set from outside\n      // must stay removable, and a cascade chip's removal arrives here.\n      const selecting = !currentMultiple || !current.includes(node.value)\n      if (selecting && isCascaderDisabled(currentIndex, node)) return\n\n      if (!currentMultiple) {\n        emitSelection([node.value], node, \"select\")\n        if (currentCloseOnSelect) {\n          openReasonRef.current = \"item-press\"\n          setOpen(false)\n        } else {\n          // The popup stays up, so clear the query the way a multiple commit\n          // does; the next pick then starts from the full level.\n          setQuery(\"\")\n        }\n        return\n      }\n\n      if (currentCascade) {\n        const next = applyCascadeSelection(\n          currentIndex,\n          current,\n          node.value,\n          selecting,\n          currentIsSelectable\n        )\n        // `max` caps the whole gesture rather than truncating it: an arbitrary\n        // prefix of a subtree is a selection nobody asked for. The refusal\n        // changes nothing on screen, so the live region is its only witness.\n        if (\n          selecting &&\n          currentMax != null &&\n          next.length > currentMax &&\n          next.length > current.length\n        ) {\n          announceNotice(currentLabels.maxReachedAnnouncement(currentMax))\n          return\n        }\n        emitSelection(next, node, selecting ? \"select\" : \"deselect\")\n        // How many values the closure swept along is invisible from the one\n        // row pressed, so the sweep says its number out loud, itself excluded.\n        const before = new Set(current)\n        const after = new Set(next)\n        let swept = 0\n        for (const entry of selecting ? next : current) {\n          if (entry === node.value) continue\n          if (selecting ? !before.has(entry) : !after.has(entry)) swept += 1\n        }\n        if (swept > 0) {\n          announceNotice(\n            currentLabels.cascadeAnnouncement(\n              node.label ?? node.value,\n              swept,\n              selecting\n            )\n          )\n        }\n        setQuery(\"\")\n        return\n      }\n\n      if (current.includes(node.value)) {\n        emitSelection(\n          current.filter((v) => v !== node.value),\n          node,\n          \"deselect\"\n        )\n      } else if (currentMax == null || current.length < currentMax) {\n        emitSelection([...current, node.value], node, \"select\")\n      } else {\n        // The plain-multiple twin of the cascade refusal above: just as silent.\n        announceNotice(currentLabels.maxReachedAnnouncement(currentMax))\n      }\n      setQuery(\"\")\n    },\n    [emitSelection, setOpen, setQuery, announceNotice]\n  )\n\n  /** Safety net for paths that bypass the row-level veto (the drag-select\n   * mouseup). `details.cancel()` suppresses the commit and the close together:\n   * Base UI shares one event-details object between them. */\n  const handleComboboxValueChange = React.useCallback(\n    (\n      next: CascaderNode<T> | CascaderNode<T>[] | null,\n      details: ComboboxPrimitive.Root.ChangeEventDetails\n    ) => {\n      if (!multiple) {\n        const node = Array.isArray(next) ? (next[0] ?? null) : next\n        // The paging row is a real option, so Base UI reports it as a\n        // selection. Same safety net branches get, for paths a row cannot veto.\n        if (node && isCascaderMoreNode(node)) {\n          details.cancel()\n          return\n        }\n        // The row is Base UI `disabled` already, so only a path around it gets\n        // here, a custom row re-enabling it for one: refused, never navigated.\n        if (node && isDisabled(node)) {\n          details.cancel()\n          return\n        }\n        if (node && !isSelectable(node) && isBranch(node)) {\n          details.cancel()\n          navigate(node)\n          return\n        }\n        if (node) {\n          // `closeOnSelect={false}` must also stop Base UI's OWN close; one\n          // shared details object, so cancelling suppresses both, and our value\n          // is controlled, so Base UI's half had nothing to keep.\n          if (!closeOnSelect) details.cancel()\n          commit(node)\n        }\n        return\n      }\n\n      // Take Base UI's whole array as the answer and inspect only what was\n      // ADDED. Deriving the toggle from the tail works for an add but on a\n      // remove drops the wrong node, leaving the last selection unclearable.\n      const nextNodes = Array.isArray(next) ? next : []\n      const before = new Set(selectedValues)\n      const added = nextNodes.find((node) => !before.has(node.value))\n\n      if (added && (isCascaderMoreNode(added) || isDisabled(added))) {\n        details.cancel()\n        return\n      }\n\n      if (added && !isSelectable(added) && isBranch(added)) {\n        details.cancel()\n        navigate(added)\n        return\n      }\n\n      // Cascading reads Base UI's array only as the REPORT of which row was\n      // pressed: it differs by exactly one value either way, and the real\n      // commit covers a whole subtree.\n      if (cascade) {\n        const nextValues = new Set(nextNodes.map((node) => node.value))\n        const removed = selectedValues.find((entry) => !nextValues.has(entry))\n        const toggled = added ?? (removed != null ? resolveNode(removed) : null)\n        if (toggled) commit(toggled)\n        return\n      }\n\n      if (\n        max != null &&\n        nextNodes.length > selectedValues.length &&\n        nextNodes.length > max\n      ) {\n        details.cancel()\n        // The third silent refusal site: cancelling reverts the press with no\n        // state change, so the live region is the only witness.\n        announceNotice(labels.maxReachedAnnouncement(max))\n        return\n      }\n\n      setSelection(nextNodes.map((node) => node.value))\n      setQuery(\"\")\n    },\n    [\n      multiple,\n      selectedValues,\n      max,\n      cascade,\n      closeOnSelect,\n      labels,\n      announceNotice,\n      isSelectable,\n      isDisabled,\n      isBranch,\n      navigate,\n      commit,\n      resolveNode,\n      setSelection,\n      setQuery,\n    ]\n  )\n\n  const handleOpenChange = React.useCallback(\n    (nextOpen: boolean, details: ComboboxPrimitive.Root.ChangeEventDetails) => {\n      if (details.isCanceled) return\n\n      // Escape closes ONE popup at a time. `Combobox` builds no `FloatingTree`,\n      // so one Escape reaches the flyout and the cascader; the flyout clears\n      // its registration in an effect, so the set holds for exactly this event.\n      if (!nextOpen && details.reason === \"escape-key\" && hasOpenFlyout()) {\n        details.cancel()\n        return\n      }\n\n      // Captured for the wrapped `onOpenChange`, which runs synchronously\n      // inside `setOpen`, so a ref written just before the call is exact.\n      openReasonRef.current = details.reason ?? \"none\"\n      setOpen(nextOpen)\n\n      if (!nextOpen) {\n        setQuery(\"\")\n        return\n      }\n\n      // Opening onto the level holding the selection is the difference\n      // between \"edit this\" and \"start over\". Not in a tree, where `path`\n      // does nothing and the live region announced a level nobody could see.\n      if (revealSelected) {\n        setQuery(\"\")\n        const first = toArray(value)[0]\n        if (mode !== \"tree\") {\n          setPathWithReason(\n            first\n              ? getCascaderPath(index, first)\n                  .slice(0, -1)\n                  .map((node) => node.value)\n              : [],\n            \"reveal\"\n          )\n        }\n      }\n    },\n    [\n      setOpen,\n      setQuery,\n      revealSelected,\n      value,\n      index,\n      mode,\n      setPathWithReason,\n      hasOpenFlyout,\n    ]\n  )\n\n  /* -------------------------------- context ------------------------------- */\n\n  // NOT memoised further: render props must be the current closure, and only\n  // rows consume this. See `CascaderRenderContextValue`.\n  const renderContext = React.useMemo(\n    () => ({ renderItem, renderLabel }),\n    [renderItem, renderLabel]\n  )\n\n  /** The stable half: nothing in these deps moves on a keystroke, a level\n   * change or a highlight, which is what makes `React.memo` on `CascaderItem`\n   * hold while the user types. Destructured off `loader` so the memo depends\n   * on its `[]`-dep callbacks, not the object it republishes per load. */\n  const {\n    active: hasLoader,\n    loadMore,\n    retryLevel,\n    retrySearch,\n    prefetchNode,\n  } = loader\n\n  const actionsValue = React.useMemo<CascaderActionsContextValue<T>>(\n    () => ({\n      index,\n      mode,\n      multiple,\n      cascade,\n      branchesSelectable,\n      indicator,\n      expandTrigger,\n      actions: resolvedActions,\n      searchScope,\n      maxHeight,\n      inline: !!inline,\n      invalid: !!invalid,\n      baseId,\n      labels,\n      virtualized,\n      registerVirtualRenderer,\n      virtualize,\n      virtualizeThreshold,\n      estimateRowSize,\n      overscan,\n      hasLoader,\n      loadMore,\n      retryLevel,\n      retrySearch,\n      invalidateLevel,\n      getIndex,\n      getState,\n      getHighlighted,\n      setPath,\n      pushLevel,\n      popLevel,\n      goToDepth,\n      toggleExpanded,\n      setFlyoutOpen,\n      hasOpenFlyout,\n      setQuery,\n      setSelection,\n      commit,\n      navigate,\n      navigateAt,\n      resolveNode,\n      isBranch,\n      isSelectable,\n      isDisabled,\n      isSelected,\n      isIndeterminate,\n      selectedDescendantCount,\n    }),\n    [\n      index,\n      mode,\n      multiple,\n      cascade,\n      branchesSelectable,\n      indicator,\n      expandTrigger,\n      resolvedActions,\n      searchScope,\n      maxHeight,\n      inline,\n      invalid,\n      baseId,\n      labels,\n      virtualized,\n      registerVirtualRenderer,\n      virtualize,\n      virtualizeThreshold,\n      estimateRowSize,\n      overscan,\n      hasLoader,\n      loadMore,\n      retryLevel,\n      retrySearch,\n      invalidateLevel,\n      getIndex,\n      getState,\n      getHighlighted,\n      setPath,\n      pushLevel,\n      popLevel,\n      goToDepth,\n      toggleExpanded,\n      setFlyoutOpen,\n      hasOpenFlyout,\n      setQuery,\n      setSelection,\n      commit,\n      navigate,\n      navigateAt,\n      resolveNode,\n      isBranch,\n      isSelectable,\n      isDisabled,\n      isSelected,\n      isIndeterminate,\n      selectedDescendantCount,\n    ]\n  )\n\n  // A fresh array identity each time windowing turns on, and once more after:\n  // that identity is the ONLY thing that re-runs Base UI's `listRef` sizing\n  // effect. See `virtualSyncs`.\n  const comboboxItems = React.useMemo(\n    () => (virtualized ? renderedItems.slice() : renderedItems),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [renderedItems, virtualized, virtualSyncs]\n  )\n\n  const comboboxValue = React.useMemo(() => {\n    if (multiple) return selectedValues.map(resolveNode)\n    return selectedValues.length ? resolveNode(selectedValues[0]) : null\n  }, [multiple, selectedValues, resolveNode])\n\n  return (\n    // Providers store values with the item generic erased; the hooks re-apply\n    // the caller's own `T` on the way back out.\n    <CascaderActionsContext.Provider\n      value={actionsValue as unknown as CascaderActionsContextValue}\n    >\n      <CascaderStateContext.Provider\n        value={stateValue as unknown as CascaderStateContextValue}\n      >\n        <CascaderHighlightContext.Provider value={highlightStore}>\n          <CascaderRenderContext.Provider\n            value={renderContext as unknown as CascaderRenderContextValue}\n          >\n            <ComboboxPrimitive.Root\n              items={renderedItems}\n              /* The empty frame that resets the highlight. See the comment above. */\n              filteredItems={swapping ? EMPTY : comboboxItems}\n              /* Filtering is ours: the level, deep and global scopes cannot\n                 be expressed through Base UI's single matcher. */\n              filter={null}\n              value={comboboxValue}\n              onValueChange={handleComboboxValueChange}\n              open={open}\n              onOpenChange={handleOpenChange}\n              inputValue={query}\n              onInputValueChange={(next: string) => setQuery(next)}\n              multiple={multiple}\n              disabled={disabled}\n              /* Form wiring, straight to Base UI's hidden input. Conditional\n                 spreads: an explicit `undefined` on a controlled prop reads as\n                 \"deleted\" rather than \"not supplied\". */\n              name={name}\n              {...(form != null ? { form } : null)}\n              {...(id != null ? { id } : null)}\n              {...(required != null ? { required } : null)}\n              {...(readOnly != null ? { readOnly } : null)}\n              {...(inputRef ? { inputRef } : null)}\n              inline={inline}\n              /* Stops Base UI rendering its composite list, which is what makes\n                 an explicit row `index` legal. `items`/`filteredItems` stay full:\n                 they size `listRef`, or ArrowDown stops at the last RENDERED row. */\n              virtualized={virtualized}\n              itemToStringValue={(item: CascaderNode<T>) => item?.value ?? \"\"}\n              itemToStringLabel={(item: CascaderNode<T>) => item?.label ?? \"\"}\n              isItemEqualToValue={(a: CascaderNode<T>, b: CascaderNode<T>) =>\n                a?.value === b?.value\n              }\n              // Fires on every arrow key AND every pointer move, so it must\n              // never call setState. The ref serves keyboard handlers, the\n              // de-duping store anything that has to re-render.\n              onItemHighlighted={(\n                item: CascaderNode<T> | undefined,\n                details: ComboboxPrimitive.Root.HighlightEventDetails\n              ) => {\n                highlightedRef.current = item ?? null\n                highlightStore.set({\n                  index: details.index,\n                  value: item?.value ?? null,\n                })\n                // Schedules a TIMEOUT and returns: inside a layout effect, a\n                // synchronous setState would cascade before paint.\n                prefetchNode(item)\n              }}\n            >\n              {children}\n            </ComboboxPrimitive.Root>\n          </CascaderRenderContext.Provider>\n        </CascaderHighlightContext.Provider>\n      </CascaderStateContext.Provider>\n    </CascaderActionsContext.Provider>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Trigger                                  */\n/* -------------------------------------------------------------------------- */\n\n/** `Trigger.Props` stops short of `ref`, so it is intersected in, as the\n * shadcn combobox wrapper also does: a form library needs a ref here, and\n * `onBlur`, the \"touched\" signal, lands here too. */\nexport interface CascaderTriggerProps\n  extends\n    ComboboxPrimitive.Trigger.Props,\n    Pick<React.ComponentPropsWithRef<\"button\">, \"ref\"> {\n  /** Hides the trailing chevron, for a trigger that supplies its own. */\n  showIcon?: boolean\n}\n\nfunction CascaderTrigger({\n  className,\n  children,\n  showIcon = true,\n  ref,\n  ...props\n}: CascaderTriggerProps) {\n  const { invalid } = useCascaderActions()\n\n  // `ref` is the one prop `mergeProps` does not merge, so chain it by hand.\n  const triggerRef = React.useRef<HTMLButtonElement | null>(null)\n  const setTrigger = React.useCallback(\n    (node: HTMLButtonElement | null) => {\n      triggerRef.current = node\n      if (typeof ref === \"function\") ref(node)\n      else if (ref) ref.current = node\n    },\n    [ref]\n  )\n\n  /** 18 of 19 audited examples shipped an unnamed combobox: the trigger's\n   * contents are the field's VALUE, so a screen reader never hears its NAME.\n   * Checked in the DOM, since the name may arrive as a `<label for>`. */\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return\n    const element = triggerRef.current\n    if (!element) return\n    const named =\n      element.hasAttribute(\"aria-label\") ||\n      element.hasAttribute(\"aria-labelledby\") ||\n      element.closest(\"label\") !== null ||\n      (element.id !== \"\" &&\n        element.ownerDocument.querySelector(\n          `label[for=\"${CSS.escape(element.id)}\"]`\n        ) !== null)\n    if (named) return\n    warnCascaderOnce(\n      \"trigger-unnamed\",\n      \"`CascaderTrigger` has no accessible name. Its contents are the field's VALUE - they change with the selection - so a screen reader hears what is picked but never what the field is for. Pass `aria-label` or `aria-labelledby`, or reference the trigger's `id` from a `<label>`.\"\n    )\n  })\n\n  return (\n    <ComboboxPrimitive.Trigger\n      ref={setTrigger}\n      data-slot=\"cascader-trigger\"\n      /* Conditional spread: an explicit `undefined` would DELETE whatever a\n         `Field` wrapper had already put here. */\n      {...(invalid ? { \"aria-invalid\": true, \"data-invalid\": \"\" } : null)}\n      className={cn(TRIGGER_ICON_FALLBACK_CLASS, className)}\n      {...props}\n    >\n      {children}\n      {showIcon ? (\n        <ChevronDownIcon\n          className={`${TRIGGER_ICON_CLASS} pointer-events-none shrink-0`}\n        />\n      ) : null}\n    </ComboboxPrimitive.Trigger>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              Resolved theme                                */\n/* -------------------------------------------------------------------------- */\n\n/* The eight registry styles each state a combobox surface differently, so the\n * classes below spell every one out with `style-<name>:` variants. Nothing\n * depends on ReUI's theme CSS; `build-registry` flattens one in on install. */\n\n/** Icon size for a combobox surface: 14px in mira and sera, 16px elsewhere. */\nconst TRIGGER_ICON_CLASS =\n  \"text-muted-foreground style-vega:size-4 style-nova:size-4 style-maia:size-4 style-lyra:size-4 style-mira:size-3.5 style-luma:size-4 style-rhea:size-4 style-sera:size-3.5\"\n\n/** The same ladder, applied only to trigger children that carry no size. */\nconst TRIGGER_ICON_FALLBACK_CLASS =\n  \"style-vega:[&_svg:not([class*='size-'])]:size-4 style-nova:[&_svg:not([class*='size-'])]:size-4 style-maia:[&_svg:not([class*='size-'])]:size-4 style-lyra:[&_svg:not([class*='size-'])]:size-4 style-mira:[&_svg:not([class*='size-'])]:size-3.5 style-luma:[&_svg:not([class*='size-'])]:size-4 style-rhea:[&_svg:not([class*='size-'])]:size-4 style-sera:[&_svg:not([class*='size-'])]:size-3.5\"\n\n/** The chips field: a form-control surface, so it carries the invalid states. */\nconst CHIPS_CLASS =\n  \"flex flex-wrap items-center border bg-clip-padding style-vega:dark:bg-input/30 style-vega:border-input style-vega:focus-within:border-ring style-vega:focus-within:ring-ring/50 style-vega:has-aria-invalid:ring-destructive/20 style-vega:dark:has-aria-invalid:ring-destructive/40 style-vega:has-aria-invalid:border-destructive style-vega:dark:has-aria-invalid:border-destructive/50 style-vega:min-h-9 style-vega:gap-1.5 style-vega:rounded-md style-vega:bg-transparent style-vega:px-2.5 style-vega:py-1.5 style-vega:text-sm style-vega:shadow-xs style-vega:transition-[color,box-shadow] style-vega:focus-within:ring-3 style-vega:has-aria-invalid:ring-3 style-vega:has-data-[slot=combobox-chip]:px-1.5 style-nova:dark:bg-input/30 style-nova:border-input style-nova:focus-within:border-ring style-nova:focus-within:ring-ring/50 style-nova:has-aria-invalid:ring-destructive/20 style-nova:dark:has-aria-invalid:ring-destructive/40 style-nova:has-aria-invalid:border-destructive style-nova:dark:has-aria-invalid:border-destructive/50 style-nova:bg-transparent style-nova:px-2.5 style-nova:text-sm style-nova:focus-within:ring-3 style-nova:has-aria-invalid:ring-3 style-nova:min-h-8 style-nova:gap-1 style-nova:rounded-lg style-nova:py-1 style-nova:transition-colors style-nova:has-data-[slot=combobox-chip]:px-1 style-maia:border-input style-maia:focus-within:border-ring style-maia:focus-within:ring-ring/50 style-maia:has-aria-invalid:ring-destructive/20 style-maia:dark:has-aria-invalid:ring-destructive/40 style-maia:has-aria-invalid:border-destructive style-maia:dark:has-aria-invalid:border-destructive/50 style-maia:min-h-9 style-maia:gap-1.5 style-maia:px-2.5 style-maia:py-1.5 style-maia:text-sm style-maia:has-data-[slot=combobox-chip]:px-1.5 style-maia:transition-colors style-maia:bg-input/30 style-maia:rounded-4xl style-maia:focus-within:ring-[3px] style-maia:has-aria-invalid:ring-[3px] style-lyra:dark:bg-input/30 style-lyra:border-input style-lyra:focus-within:border-ring style-lyra:focus-within:ring-ring/50 style-lyra:has-aria-invalid:ring-destructive/20 style-lyra:dark:has-aria-invalid:ring-destructive/40 style-lyra:has-aria-invalid:border-destructive style-lyra:dark:has-aria-invalid:border-destructive/50 style-lyra:bg-transparent style-lyra:px-2.5 style-lyra:min-h-8 style-lyra:gap-1 style-lyra:py-1 style-lyra:transition-colors style-lyra:has-data-[slot=combobox-chip]:px-1 style-lyra:rounded-none style-lyra:text-xs style-lyra:focus-within:ring-1 style-lyra:has-aria-invalid:ring-1 style-mira:dark:bg-input/30 style-mira:border-input style-mira:focus-within:border-ring style-mira:has-aria-invalid:ring-destructive/20 style-mira:dark:has-aria-invalid:ring-destructive/40 style-mira:has-aria-invalid:border-destructive style-mira:dark:has-aria-invalid:border-destructive/50 style-mira:rounded-md style-mira:gap-1 style-mira:transition-colors style-mira:has-data-[slot=combobox-chip]:px-1 style-mira:bg-input/20 style-mira:focus-within:ring-ring/30 style-mira:min-h-7 style-mira:px-2 style-mira:py-0.5 style-mira:text-xs/relaxed style-mira:focus-within:ring-2 style-mira:has-aria-invalid:ring-2 style-luma:focus-within:border-ring style-luma:has-aria-invalid:ring-destructive/20 style-luma:dark:has-aria-invalid:ring-destructive/40 style-luma:has-aria-invalid:border-destructive style-luma:dark:has-aria-invalid:border-destructive/50 style-luma:min-h-9 style-luma:gap-1.5 style-luma:py-1.5 style-luma:text-sm style-luma:focus-within:ring-3 style-luma:has-aria-invalid:ring-3 style-luma:has-data-[slot=combobox-chip]:px-1.5 style-luma:focus-within:ring-ring/30 style-luma:bg-input/50 style-luma:border-transparent style-luma:rounded-3xl style-luma:px-3 style-luma:transition-[color,box-shadow,background-color] style-rhea:focus-within:border-ring style-rhea:has-aria-invalid:ring-destructive/20 style-rhea:dark:has-aria-invalid:ring-destructive/40 style-rhea:has-aria-invalid:border-destructive style-rhea:dark:has-aria-invalid:border-destructive/50 style-rhea:px-2.5 style-rhea:text-sm style-rhea:transition-[color,box-shadow] style-rhea:focus-within:ring-3 style-rhea:has-aria-invalid:ring-3 style-rhea:min-h-8 style-rhea:gap-1 style-rhea:py-1 style-rhea:has-data-[slot=combobox-chip]:px-1 style-rhea:focus-within:ring-ring/30 style-rhea:bg-input/50 style-rhea:border-transparent style-rhea:rounded-2xl style-rhea:duration-200 style-sera:gap-1.5 style-sera:bg-transparent style-sera:py-1.5 style-sera:text-sm style-sera:rounded-none style-sera:border-transparent style-sera:border-b-input style-sera:focus-within:border-b-ring style-sera:has-aria-invalid:border-b-destructive style-sera:dark:has-aria-invalid:border-b-destructive/50 style-sera:min-h-10 style-sera:px-0 style-sera:transition-[color,border-color] style-sera:has-data-[slot=combobox-chip]:px-0\"\n\nconst CHIP_CLASS =\n  \"text-foreground flex w-fit items-center justify-center gap-1 font-medium whitespace-nowrap style-vega:bg-muted style-vega:h-[calc(--spacing(5.5))] style-vega:rounded-sm style-vega:px-1.5 style-vega:text-xs style-vega:has-data-[slot=combobox-chip-remove]:pr-0 style-nova:bg-muted style-nova:rounded-sm style-nova:px-1.5 style-nova:text-xs style-nova:has-data-[slot=combobox-chip-remove]:pr-0 style-nova:h-[calc(--spacing(5.25))] style-maia:h-[calc(--spacing(5.5))] style-maia:text-xs style-maia:has-data-[slot=combobox-chip-remove]:pr-0 style-maia:bg-muted-foreground/10 style-maia:rounded-4xl style-maia:px-2 style-lyra:bg-muted style-lyra:px-1.5 style-lyra:text-xs style-lyra:has-data-[slot=combobox-chip-remove]:pr-0 style-lyra:h-[calc(--spacing(5.25))] style-lyra:rounded-none style-mira:px-1.5 style-mira:has-data-[slot=combobox-chip-remove]:pr-0 style-mira:bg-muted-foreground/10 style-mira:h-[calc(--spacing(4.75))] style-mira:rounded-[calc(var(--radius-sm)-2px)] style-mira:text-xs/relaxed style-luma:h-[calc(--spacing(5.5))] style-luma:text-xs style-luma:has-data-[slot=combobox-chip-remove]:pr-0 style-luma:px-2 style-luma:bg-input style-luma:rounded-3xl style-luma:dark:bg-input/60 style-rhea:px-1.5 style-rhea:text-xs style-rhea:h-[calc(--spacing(5.25))] style-rhea:bg-input style-rhea:dark:bg-input/60 style-rhea:rounded-2xl style-rhea:has-data-[slot=combobox-chip-remove]:pr-0.5 style-sera:bg-muted style-sera:h-[calc(--spacing(5.5))] style-sera:text-xs style-sera:has-data-[slot=combobox-chip-remove]:pr-0 style-sera:px-2 style-sera:rounded-none\"\n\nconst CHIP_REMOVE_CLASS =\n  \"opacity-50 hover:opacity-100 style-vega:-ml-1 style-nova:-ml-1 style-maia:-ml-1 style-lyra:-ml-1 style-mira:-ml-1 style-luma:-ml-1 style-rhea:-ml-0.5 style-rhea:size-4.5 style-rhea:aria-disabled:pointer-events-none style-sera:-ml-1\"\n\n/** The floating panel's surface. The shared popup's `InputGroup` sizing is\n * left out (mirrored in `cascader-nav.tsx`), as are its `max-h-72` and\n * `min-w-*`: `CascaderContent` sets both from the positioner's variables. */\nconst CONTENT_SURFACE_CLASS =\n  \"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 overflow-hidden ring-1 duration-100 style-vega:ring-foreground/10 style-vega:rounded-md style-vega:shadow-md style-nova:ring-foreground/10 style-nova:shadow-md style-nova:rounded-lg style-maia:ring-foreground/5 style-maia:rounded-2xl style-maia:shadow-2xl style-lyra:ring-foreground/10 style-lyra:shadow-md style-lyra:rounded-none style-mira:ring-foreground/10 style-mira:shadow-md style-mira:rounded-lg style-mira:dark:bg-popover style-luma:ring-foreground/5 style-luma:dark:ring-foreground/10 style-luma:rounded-3xl style-luma:shadow-lg style-rhea:ring-foreground/5 style-rhea:rounded-2xl style-rhea:dark:ring-foreground/10 style-rhea:shadow-lg style-sera:ring-foreground/10 style-sera:shadow-md style-sera:rounded-none data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2\"\n\nconst EMPTY_CLASS =\n  \"text-muted-foreground hidden w-full justify-center py-2 text-center group-data-empty/combobox-content:flex style-vega:text-sm style-nova:text-sm style-maia:text-sm style-lyra:text-xs style-mira:text-xs/relaxed style-luma:text-sm style-rhea:text-sm style-sera:text-sm\"\n\n/* -------------------------------------------------------------------------- */\n/*                                    Chips                                   */\n/* -------------------------------------------------------------------------- */\n\n/** Ref for the chips container: they replace the trigger, so the popup has to\n * be anchored to them. Hand it to `CascaderContent`'s `anchor`. */\nexport function useCascaderAnchor() {\n  return React.useRef<HTMLDivElement | null>(null)\n}\n\nexport interface CascaderChipsProps\n  extends\n    Omit<ComboboxPrimitive.Chips.Props, \"children\">,\n    Pick<React.ComponentPropsWithRef<\"div\">, \"ref\"> {\n  /** Shown in place of the chips when nothing is selected. */\n  placeholder?: React.ReactNode\n  /**\n   * How a `cascade` selection is condensed into chips. DISPLAY ONLY: the\n   * stored value stays the full closure, which keeps a row's checked state an\n   * O(1) lookup. `\"all\"` (default) is one chip per value, `\"parent\"` collapses\n   * a fully selected branch, `\"child\"` chips only the deepest frontier.\n   * Removing a condensed chip removes its whole closure and reconciles.\n   */\n  strategy?: CascaderCheckedStrategy\n  /** Replaces the chip list, receiving the resolved selection IN SELECTION\n   * ORDER. Keep that order: Base UI removes a chip by sibling position. */\n  children?: React.ReactNode | ((nodes: CascaderNode[]) => React.ReactNode)\n}\n\n/**\n * The multi-select trigger surface: one removable chip per selection, where\n * `CascaderValue` would collapse to \"3 selected\". A component rather than a\n * recipe because of three easy misses: the popup anchored to the chips, an\n * accessible name on every remove button, and labels that disambiguate when\n * two branches use the same one.\n *\n * ```tsx\n * const anchor = useCascaderAnchor()\n * <Cascader multiple items={items}>\n *   <CascaderChips ref={anchor} placeholder=\"Select attributes\" />\n *   <CascaderContent anchor={anchor}>...</CascaderContent>\n * </Cascader>\n * ```\n */\nfunction CascaderChips({\n  className,\n  children,\n  placeholder,\n  strategy = \"all\",\n  ...props\n}: CascaderChipsProps) {\n  const {\n    labels,\n    multiple,\n    invalid,\n    resolveNode,\n    index,\n    isSelectable,\n    setSelection,\n  } = useCascaderActions()\n  const { selectedValues } = useCascaderState()\n\n  React.useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return\n    if (multiple) return\n    warnCascaderOnce(\n      \"chips-without-multiple\",\n      \"`CascaderChips` renders one chip per selection and there is only ever one without `multiple`. Use `CascaderValue` for a single-select trigger.\"\n    )\n  }, [multiple])\n\n  // DERIVED display: the selection stays the full closure, the chips condense.\n  const displayedValues =\n    strategy === \"all\"\n      ? selectedValues\n      : getCascaderCheckedValues(index, selectedValues, strategy)\n\n  // Not memoised: one entry per SELECTION, not per row, so the pass is short.\n  const nodes = displayedValues.map(resolveNode)\n  const ambiguous = findAmbiguousCascaderLabels(nodes)\n\n  // Removal for a CONDENSED chip: Base UI's `ChipRemove` maps a press onto the\n  // STORED array, which is wrong once the chips are fewer, so this removes the\n  // chip's whole subtree closure instead.\n  const removeClosure = (node: CascaderNode) => {\n    setSelection(\n      applyCascadeSelection(\n        index,\n        selectedValues,\n        node.value,\n        false,\n        isSelectable\n      ),\n      \"deselect\"\n    )\n  }\n\n  const content = typeof children === \"function\" ? children(nodes) : children\n\n  return (\n    <ComboboxPrimitive.Chips\n      data-slot=\"cascader-chips\"\n      /* An unnamed `role=\"toolbar\"` announces as just \"toolbar\". */\n      aria-label={labels.chipsLabel}\n      /* Conditional spread: the error treatment is keyed on\n         `has-aria-invalid:`, so the attribute must be absent, not empty. */\n      {...(invalid ? { \"aria-invalid\": true, \"data-invalid\": \"\" } : null)}\n      className={cn(CHIPS_CLASS, className)}\n      {...props}\n    >\n      {content ??\n        (nodes.length === 0 ? (\n          <span\n            data-slot=\"cascader-chips-placeholder\"\n            className=\"text-muted-foreground truncate\"\n          >\n            {placeholder}\n          </span>\n        ) : (\n          nodes.map((node) => (\n            <CascaderChip\n              key={node.value}\n              node={node}\n              showPath={ambiguous.has(node.value)}\n              /* Only a condensed chip needs the remover; the default shape\n                 keeps Base UI's positional removal. */\n              {...(strategy !== \"all\"\n                ? { onRemove: () => removeClosure(node) }\n                : null)}\n            />\n          ))\n        ))}\n    </ComboboxPrimitive.Chips>\n  )\n}\n\nexport interface CascaderChipProps extends Omit<\n  ComboboxPrimitive.Chip.Props,\n  \"children\"\n> {\n  node: CascaderNode\n  /** Prefix the label with its ancestor trail. */\n  showPath?: boolean\n  /** Trail segments to show, the node itself included. */\n  maxSegments?: number\n  /** Hides the remove button, for a read-only chip. */\n  showRemove?: boolean\n  /** Replaces Base UI's positional removal for this chip (remove button and\n   * Backspace/Delete). Supplied for a `strategy`-condensed chip, whose\n   * position no longer maps onto the stored array. */\n  onRemove?: () => void\n  /** Replaces the chip's label. The remove button is still rendered. */\n  children?: React.ReactNode\n}\n\n/** Base UI hands its chip handlers events carrying the veto hook. */\ntype CascaderChipKeyEvent = Parameters<\n  NonNullable<ComboboxPrimitive.Chip.Props[\"onKeyDown\"]>\n>[0]\n\ntype CascaderChipRemoveClickEvent = Parameters<\n  NonNullable<ComboboxPrimitive.ChipRemove.Props[\"onClick\"]>\n>[0]\n\n/** One chip. Removal goes through Base UI's `ChipRemove`, which reports the\n * shortened selection to the root, so it lands in the same `setSelection` as\n * every other deselection and `cascade` applies to it. */\nfunction CascaderChip({\n  className,\n  node,\n  showPath = false,\n  maxSegments = 2,\n  showRemove = true,\n  onRemove,\n  children,\n  ...props\n}: CascaderChipProps) {\n  const { labels, index } = useCascaderActions()\n\n  const chain = showPath ? getCascaderPath(index, node.value) : []\n  const segments = collapseCascaderPath(chain, {\n    maxSegments,\n    collapse: \"start\",\n  })\n\n  const label = segments.length\n    ? segments\n        .map((segment) => (segment.type === \"node\" ? segment.node.label : \"…\"))\n        .join(` ${labels.pathSeparator} `)\n    : node.label\n\n  return (\n    <ComboboxPrimitive.Chip\n      data-slot=\"cascader-chip\"\n      /* Keyboard half of `onRemove`: Base UI's chip removes ITSELF by position\n         on Backspace/Delete, the arithmetic a condensed chip must not use. The\n         veto runs first, so the closure removal replaces it. */\n      {...(onRemove\n        ? {\n            onKeyDown: (event: CascaderChipKeyEvent) => {\n              if (event.key !== \"Backspace\" && event.key !== \"Delete\") return\n              event.preventBaseUIHandler()\n              onRemove()\n            },\n          }\n        : null)}\n      className={cn(\n        CHIP_CLASS,\n        \"has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? <span className=\"truncate\">{label}</span>}\n      {showRemove ? (\n        <ComboboxPrimitive.ChipRemove\n          data-slot=\"cascader-chip-remove\"\n          /* Named after what the chip DISPLAYS, not the bare node label, or\n             two disambiguated chips both announce \"Remove Created at\". */\n          aria-label={labels.removeChip(label)}\n          /* The pointer half of `onRemove`, same veto, same reason. */\n          {...(onRemove\n            ? {\n                onClick: (event: CascaderChipRemoveClickEvent) => {\n                  event.preventBaseUIHandler()\n                  onRemove()\n                },\n              }\n            : null)}\n          className={CHIP_REMOVE_CLASS}\n        >\n          <XIcon className=\"pointer-events-none\" />\n        </ComboboxPrimitive.ChipRemove>\n      ) : null}\n    </ComboboxPrimitive.Chip>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                   Content                                  */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderContentProps\n  extends\n    ComboboxPrimitive.Popup.Props,\n    /** Positioner surface forwarded as-is, so an unusual anchor does not force\n     * a rebuild of the content stack. No `trackAnchor`: Base UI 1.5.0 has\n     * none. */\n    Pick<\n      ComboboxPrimitive.Positioner.Props,\n      | \"side\"\n      | \"align\"\n      | \"sideOffset\"\n      | \"alignOffset\"\n      | \"anchor\"\n      | \"collisionBoundary\"\n      | \"collisionPadding\"\n      | \"sticky\"\n      | \"positionMethod\"\n    >,\n    /** Where the portal mounts, for a shadow root or a scoped stacking context. */\n    Pick<ComboboxPrimitive.Portal.Props, \"container\"> {}\n\n/** Portal + Positioner + floating panel. Unlike shadcn's `ComboboxContent` it\n * does NOT clamp the popup to the anchor width (a cascader panel is routinely\n * wider), but it keeps `group/combobox-content`, which `CascaderEmpty` needs. */\nfunction CascaderContent({\n  className,\n  side = \"bottom\",\n  sideOffset = 6,\n  align = \"start\",\n  alignOffset = 0,\n  anchor,\n  collisionBoundary,\n  collisionPadding,\n  sticky,\n  positionMethod,\n  container,\n  ref,\n  ...props\n}: CascaderContentProps) {\n  const { labels } = useCascaderActions()\n  const popupRef = React.useRef<HTMLDivElement | null>(null)\n\n  // `initialFocus` needs the popup element, and `ref` is the one prop\n  // `mergeProps` does NOT merge, so it is chained by hand rather than spread.\n  const setPopup = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      popupRef.current = node\n      if (typeof ref === \"function\") ref(node)\n      else if (ref) ref.current = node\n    },\n    [ref]\n  )\n\n  /**\n   * Focus goes to the search field, where the level keys and\n   * `aria-activedescendant` live. Base UI's own default agrees but is COMPUTED\n   * FROM THE FIRST RENDER, when the field has not mounted, and collapses to\n   * \"do not move focus\": measured, a KEYBOARD open left focus on the trigger\n   * and cost two Tabs to reach the footer where a pointer open cost one.\n   *\n   * `initialFocus` rather than an effect, because anything scheduled alongside\n   * Base UI's focus manager is a race; the field is resolved INSIDE the\n   * callback, which runs after the layout effects. Touch is handed back to\n   * Base UI on purpose - focusing the popup keeps the Android keyboard shut.\n   */\n  const initialFocus = React.useCallback((openType: string) => {\n    const popup = popupRef.current\n    if (!popup) return true\n    if (openType === \"touch\") return popup\n    return (\n      popup.querySelector<HTMLElement>('[data-slot=\"cascader-input\"]') ?? true\n    )\n  }, [])\n\n  return (\n    // Conditional spreads throughout: `mergeProps` iterates own keys, so an\n    // explicit `undefined` reads as \"deleted\" rather than \"not supplied\".\n    <ComboboxPrimitive.Portal\n      {...(container !== undefined ? { container } : null)}\n    >\n      <ComboboxPrimitive.Positioner\n        side={side}\n        sideOffset={sideOffset}\n        align={align}\n        alignOffset={alignOffset}\n        anchor={anchor}\n        {...(collisionBoundary !== undefined ? { collisionBoundary } : null)}\n        {...(collisionPadding !== undefined ? { collisionPadding } : null)}\n        {...(sticky !== undefined ? { sticky } : null)}\n        {...(positionMethod !== undefined ? { positionMethod } : null)}\n        className=\"isolate z-50\"\n      >\n        <ComboboxPrimitive.Popup\n          ref={setPopup}\n          data-slot=\"cascader-content\"\n          /* Marks a menu surface for the docs design-system picker. An\n             attribute, not a class, so the panel's look stays self-contained. */\n          data-menu-target=\"\"\n          /* The input inside makes this a `role=\"dialog\"`, unnamed by default. */\n          aria-label={labels.panelLabel}\n          initialFocus={initialFocus}\n          className={cn(\n            CONTENT_SURFACE_CLASS,\n            \"group/combobox-content relative flex max-h-(--available-height) max-w-(--available-width) min-w-(--anchor-width) origin-(--transform-origin) flex-col\",\n            className\n          )}\n          {...props}\n        />\n      </ComboboxPrimitive.Positioner>\n    </ComboboxPrimitive.Portal>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    Panel                                   */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderPanelProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean\n}\n\n/**\n * The nav + list(s) + footer container, with no positioning of its own: inside\n * `CascaderContent` for the popup case, alone for the embedded one.\n *\n * `min-h-0` and `max-h-full` are load-bearing. A flex child without `min-h-0`\n * refuses to shrink below its content, which is the usual reason a scroll area\n * never scrolls - worst embedded, where no positioner publishes\n * `--available-height`. It also owns the panel's TAB ORDER: the scroll area\n * makes itself a tab stop whenever the level overflows, stranding the footer,\n * and the panel is the only ancestor of both ends of that move in BOTH shapes.\n */\nfunction CascaderPanel({\n  className,\n  asChild = false,\n  children,\n  onKeyDown,\n  ...props\n}: CascaderPanelProps) {\n  const { mode } = useCascaderActions()\n  const Comp = asChild ? Slot : \"div\"\n\n  const handleKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      // Called FIRST, so a consumer's handler can drop this one with\n      // `preventDefault`; the base twin gets that ordering from `mergeProps`.\n      onKeyDown?.(event)\n      if (event.key !== \"Tab\" || event.defaultPrevented) return\n      // A browser-level move (Ctrl+Tab switches tabs) is never ours.\n      if (event.altKey || event.ctrlKey || event.metaKey) return\n\n      const panel = event.currentTarget\n      const target = event.target as HTMLElement | null\n      if (!target || !panel.contains(target)) return\n\n      const next = getCascaderTabTarget(panel, target, event.shiftKey)\n      // Off either end: left to the browser, since focus is never trapped.\n      if (!next) return\n\n      event.preventDefault()\n      next.focus()\n    },\n    [onKeyDown]\n  )\n\n  return (\n    <Comp\n      data-slot=\"cascader-panel\"\n      data-mode={mode}\n      onKeyDown={handleKeyDown}\n      className={cn(\"flex max-h-full min-h-0 w-full flex-col\", className)}\n      {...props}\n    >\n      {children}\n    </Comp>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                    List                                    */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderListProps extends Omit<\n  ComboboxPrimitive.List.Props,\n  \"style\"\n> {\n  /** Applied to the OUTER shell, which owns the height, so an inline\n   * `--cascader-max-height` lands where the bound can read it. Narrowed from\n   * Base UI's `style`: the shell is a plain `<div>` with no list state. */\n  style?: React.CSSProperties\n  /** Caps this level's height; the panel still shrinks to fit. Default 24rem. */\n  maxHeight?: number | string\n}\n\n/**\n * Scroll container for one level: pattern 3 of the house scroll reference,\n * viewport-bounded popup scroll. The height is `min()` of `--available-height`\n * (the positioner's space between anchor and viewport edge) and `maxHeight`,\n * so neither can be ignored; a fixed pixel cap alone still reserved 288px for\n * a panel opened 200px above the fold and spilled off screen, and wasted room\n * in a tall window. The four classes are shared with `CascaderColumns` and\n * live in `cascader-lib.tsx`.\n */\nfunction CascaderList({\n  className,\n  style,\n  maxHeight: maxHeightProp,\n  ...props\n}: CascaderListProps) {\n  const { maxHeight, mode, labels, baseId, virtualized, searchScope } =\n    useCascaderActions()\n  const { currentParent, deepResults } = useCascaderState()\n  const resolved = maxHeightProp ?? maxHeight\n  // Global hits come from the whole tree, so the parent would understate them.\n  const listName =\n    searchScope === \"global\" && deepResults\n      ? labels.rootLevel\n      : (currentParent?.label ?? labels.rootLevel)\n\n  return (\n    <div\n      data-slot=\"cascader-list-shell\"\n      style={\n        resolved != null\n          ? ({\n              ...style,\n              \"--cascader-max-height\":\n                typeof resolved === \"number\" ? `${resolved}px` : resolved,\n            } as React.CSSProperties)\n          : style\n      }\n      className={cn(\n        \"relative flex max-h-full min-h-0\",\n        CASCADER_LIST_PAD_CLASS\n      )}\n    >\n      <div\n        data-slot=\"cascader-list-bounds\"\n        className={cn(\n          \"flex w-full min-w-0 flex-col overscroll-contain\",\n          CASCADER_LIST_HEIGHT_CLASS\n        )}\n      >\n        <ScrollArea className={CASCADER_SCROLL_CLASS}>\n          <ComboboxPrimitive.List\n            data-slot=\"cascader-list\"\n            /* Base UI names the list nothing, so every mode shipped an unnamed\n               listbox. The level's parent is the name; the root borrows one. */\n            aria-label={listName}\n            /* Replaces Base UI's floating id. `aria-controls` reads the live\n               id, so the columns trail gets a predictable target. */\n            id={`${baseId}-column-0`}\n            /* NEVER a ternary ending in `undefined`: `mergeProps` iterates own\n               keys, so that DELETES Base UI's `role=\"listbox\"`. */\n            {...(mode === \"tree\" ? { role: \"tree\" as const } : null)}\n            {...(virtualized ? { \"data-virtualized\": true } : null)}\n            className={cn(\n              CASCADER_ROWS_CLASS,\n              /* A windowed row is absolutely positioned, so the ROWS' box is\n                 the containing block, not the scrollport: it carries the\n                 padding the geometry is measured against. */\n              virtualized && \"relative\",\n              className\n            )}\n            {...props}\n          />\n        </ScrollArea>\n      </div>\n    </div>\n  )\n}\n\n/* -------------------------------------------------------------------------- */\n/*                              Empty / Loading                               */\n/* -------------------------------------------------------------------------- */\n\nexport interface CascaderEmptyProps extends ComboboxPrimitive.Empty.Props {\n  /** @deprecated Ignored: the loading surface no longer draws skeletons. They\n   * promised a shape the level might not have and cost a second layout; the\n   * progress lives on the branch row instead. Kept so call sites compile. */\n  skeletonRows?: number\n}\n\n/** The empty, loading and error surface, in ONE element that never unmounts.\n * `Combobox.Empty` renders whenever `filteredItems.length === 0`, as true of a\n * fetching level as of an empty one, so swapping siblings would announce \"No\n * results found.\" over every async level. Swap the CHILDREN. */\nfunction CascaderEmpty({\n  className,\n  children,\n  // Accepted and ignored; destructured so it is never spread onto the DOM.\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  skeletonRows,\n  ...props\n}: CascaderEmptyProps) {\n  const { labels, retryLevel, retrySearch } = useCascaderActions()\n  const { query, path, searchState } = useCascaderState()\n  // The level whose emptiness is on screen; tree always shows the root.\n  const levelKey = path.length ? path[path.length - 1] : CASCADER_ROOT_KEY\n  const loadState = useCascaderLoadState(levelKey)\n  const state = query.trim() ? (searchState ?? loadState) : loadState\n\n  let body: React.ReactNode = children ?? labels.empty\n\n  if (state?.error) {\n    body = (\n      <span\n        data-slot=\"cascader-error\"\n        className=\"flex flex-col items-center gap-1.5\"\n      >\n        <span>{labels.error}</span>\n        <button\n          type=\"button\"\n          data-slot=\"cascader-retry\"\n          /* A real button because this lives OUTSIDE the listbox; a focusable\n             element inside a `role=\"option\"` is `nested-interactive`. */\n          onClick={(event) => {\n            focusCascaderHome(event.currentTarget)\n            // With a query set the error shown is the SEARCH's, so that is\n            // what to ask again; the level behind it never failed.\n            if (query.trim() && searchState?.error) retrySearch()\n            else retryLevel(levelKey)\n          }}\n          className=\"text-foreground hover:bg-accent focus-visible:ring-ring/50 rounded-md px-2 py-0.5 font-medium outline-hidden transition-colors focus-visible:ring-2\"\n        >\n          {labels.retry}\n        </button>\n      </span>\n    )\n  } else if (state?.loading) {\n    body = (\n      <span\n        data-slot=\"cascader-loading\"\n        className=\"flex w-full items-center justify-center\"\n      >\n        {labels.loading}\n      </span>\n    )\n  }\n\n  return (\n    <ComboboxPrimitive.Empty\n      data-slot=\"cascader-empty\"\n      data-state={state?.error ? \"error\" : state?.loading ? \"loading\" : \"empty\"}\n      className={cn(EMPTY_CLASS, className)}\n      /* The ONE place an explicit `undefined` is right: it DELETES Base UI's\n         live region, which is wanted. `CascaderStatus` is the single one, and\n         leaving this one spoke \"No results found.\" on every level swap, which\n         renders one deliberately empty frame to reset the highlight. */\n      role={undefined}\n      aria-live={undefined}\n      aria-atomic={undefined}\n      {...props}\n    >\n      {body}\n    </ComboboxPrimitive.Empty>\n  )\n}\n\nexport interface CascaderStatusProps extends ComboboxPrimitive.Status.Props {\n  asChild?: boolean\n}\n\n/**\n * Polite live region for the current level and result count, which drill-down\n * otherwise hides in a visual breadcrumb. Built on `Combobox.Status` for its\n * initial-mutation marker: Safari and VoiceOver need a text change ~200ms\n * after mount before reading a polite region at all.\n */\nfunction CascaderStatus({\n  className,\n  children,\n  asChild = false,\n  ...props\n}: CascaderStatusProps) {\n  const { announcement } = useCascaderState()\n\n  // `asChild` only engages when there IS an element to slot into: with a bare\n  // string, Radix's `Slot` answers `null` and deletes the live region.\n  const asSlot = asChild && React.isValidElement(children)\n\n  return (\n    <ComboboxPrimitive.Status\n      data-slot=\"cascader-status\"\n      className={cn(\"sr-only\", className)}\n      {...(asSlot ? { render: <Slot /> } : null)}\n      {...props}\n    >\n      {children ?? announcement}\n    </ComboboxPrimitive.Status>\n  )\n}\n\nexport {\n  Cascader,\n  CascaderTrigger,\n  CascaderChip,\n  CascaderChips,\n  CascaderContent,\n  CascaderPanel,\n  CascaderList,\n  CascaderEmpty,\n  CascaderStatus,\n}\n\n/** Re-exported from `cascader-context.tsx`: existing import paths keep\n * working, and the row component avoids an import cycle through this file. */\nexport {\n  createCascaderHighlightStore,\n  useCascaderActions,\n  useCascaderHighlight,\n  useCascaderRender,\n  useCascaderState,\n}\n\nexport type {\n  CascaderActionsContextValue,\n  CascaderColumn,\n  CascaderHighlight,\n  CascaderHighlightStore,\n  CascaderItemState,\n  CascaderRenderContextValue,\n  CascaderStateContextValue,\n}\n",
      "type": "registry:ui",
      "target": "components/reui/cascader/cascader.tsx"
    }
  ],
  "type": "registry:ui"
}