{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"file-browser","type":"registry:ui","title":"FileBrowser","description":"Remote-capable file\/folder browser + directory picker \u2014 lazy async provider or streamed JSON snapshots, controlled selection, breadcrumb + editable path, ARIA tree keyboard nav.","package":"react-fancy","dependencies":["lucide-react"],"registryDependencies":[],"files":[{"path":"components\/fancy\/file-browser\/FileBrowser.context.ts","content":"import { createContext, useContext } from \"react\";\nimport type { FileBrowserContextValue } from \".\/FileBrowser.types\";\n\nexport const FileBrowserContext = createContext<FileBrowserContextValue | null>(null);\n\nexport function useFileBrowser(): FileBrowserContextValue {\n  const ctx = useContext(FileBrowserContext);\n  if (!ctx) {\n    throw new Error(\"useFileBrowser must be used within a <FileBrowser> component\");\n  }\n  return ctx;\n}\n","type":"registry:ui","target":"components\/fancy\/file-browser\/FileBrowser.context.ts"},{"path":"components\/fancy\/file-browser\/FileBrowser.tsx","content":"import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { useControllableState } from \"..\/..\/hooks\/use-controllable-state\";\nimport { FileBrowserContext } from \".\/FileBrowser.context\";\nimport { FileBrowserPathBar } from \".\/FileBrowserPathBar\";\nimport { FileBrowserToolbar } from \".\/FileBrowserToolbar\";\nimport { FileBrowserTree } from \".\/FileBrowserTree\";\nimport { FileBrowserNode } from \".\/FileBrowserNode\";\nimport {\n  compareFileEntries,\n  entryMatchesFilter,\n  isEntryExpandable,\n  parentPath,\n} from \".\/FileBrowser.utils\";\nimport type {\n  FileBrowserContextValue,\n  FileBrowserProps,\n  FileBrowserRow,\n  FileEntry,\n  FileLoadStatus,\n  FileSnapshotNode,\n  FileSort,\n} from \".\/FileBrowser.types\";\n\nconst DEFAULT_SORT: FileSort = { by: \"name\", direction: \"asc\" };\n\nfunction snapshotNodeToEntry(node: FileSnapshotNode): FileEntry {\n  const { children: _children, ...entry } = node;\n  return entry;\n}\n\n\/**\n * Index a snapshot tree into a `parent path -> children` map. Roots group\n * under their own parent path (usually `\"\/\"`); any node with a materialized\n * `children` array defines that folder's listing. Dirs with `children`\n * undefined stay unknown \u2014 a provider fills them lazily in hybrid mode.\n *\/\nfunction buildSnapshotMap(snapshot: FileSnapshotNode[] | undefined): Map<string, FileEntry[]> {\n  const map = new Map<string, FileEntry[]>();\n  if (!snapshot || snapshot.length === 0) return map;\n\n  for (const root of snapshot) {\n    const parent = parentPath(root.path);\n    const list = map.get(parent);\n    if (list) list.push(snapshotNodeToEntry(root));\n    else map.set(parent, [snapshotNodeToEntry(root)]);\n  }\n\n  const walk = (nodes: FileSnapshotNode[]) => {\n    for (const node of nodes) {\n      if (node.children) {\n        map.set(node.path, node.children.map(snapshotNodeToEntry));\n        walk(node.children);\n      }\n    }\n  };\n  walk(snapshot);\n\n  return map;\n}\n\n\/**\n * FileBrowser \u2014 remote-capable file\/folder browser + directory picker.\n *\n * Browses any file tree the host can describe \u2014 local FS, HTTP, SSH, or a\n * remote machine streaming snapshots \u2014 via two feeding modes that may be\n * combined:\n *\n * - **Provider mode (lazy pull):** pass `provider.loadChildren(path)`. Folders\n *   load on first expand with per-node loading + error states; the tree is\n *   never walked eagerly.\n * - **Snapshot mode (streamed push):** pass a JSON-friendly `snapshot` tree\n *   and replace\/patch it as chunks arrive (relay \/ WebSocket \/ MCP). The\n *   snapshot is the source of truth for every path it covers; with a provider\n *   also present, unknown-depth folders stay lazily loadable (hybrid).\n *\n * Fully controlled per the Human+ component contract (`value`\/`onChange`,\n * `path`\/`onPathChange`, `expandedPaths`\/`onExpandedChange`, plus\n * `sort`\/`filter`), with uncontrolled `defaultX` fallbacks. Every row carries\n * a `data-path` attribute \u2014 paths are the stable handles agents target, never\n * indexes or generated ids.\n *\n * Agent bridge sketch (ships later in `@particle-academy\/agent-integrations`):\n * `registerFilesBridge(server, { adapter })` will expose MCP tools over this\n * surface \u2014 `files_list(path)`, `files_expand(path)` \/ `files_collapse(path)`,\n * `files_select(paths)`, `files_navigate(path)`, and\n * `files_request_snapshot(path, depth)` \u2014 with each mutation emitting an\n * `AgentActivity` event so presence, undo, and coaching layers compose. The\n * adapter maps those tools onto the same controlled props\n * (`value`\/`path`\/`expandedPaths`) and provider\/snapshot contract; no DOM\n * scraping required.\n *\n * Read-only in v1: no content preview (pair with fancy-code's `FileViewer`)\n * and no write operations \u2014 rename\/delete\/upload arrive later behind a\n * `pendingMode`-gated iteration.\n *\/\nfunction FileBrowserRoot({\n  provider,\n  snapshot,\n  select = \"file\",\n  multiple = false,\n  value,\n  defaultValue,\n  onChange,\n  path,\n  defaultPath = \"\/\",\n  onPathChange,\n  expandedPaths,\n  defaultExpandedPaths,\n  onExpandedChange,\n  sort,\n  defaultSort,\n  onSortChange,\n  filter,\n  defaultFilter,\n  onFilterChange,\n  onError,\n  indentSize = 16,\n  showIcons = true,\n  className,\n  children,\n}: FileBrowserProps) {\n  \/\/ ---------------------------------------------------------------------------\n  \/\/ Controlled \/ uncontrolled state\n  \/\/ ---------------------------------------------------------------------------\n\n  const [currentPath, setCurrentPath] = useControllableState(path, defaultPath, onPathChange);\n  const [expanded, setExpanded] = useControllableState(\n    expandedPaths,\n    defaultExpandedPaths ?? [],\n    onExpandedChange,\n  );\n  const [sortState, setSortState] = useControllableState(sort, defaultSort ?? DEFAULT_SORT, onSortChange);\n  const [filterState, setFilterState] = useControllableState(filter, defaultFilter ?? \"\", onFilterChange);\n\n  \/\/ Selection is managed directly (not via useControllableState) because\n  \/\/ onChange also receives the matching FileEntry objects.\n  const [internalValue, setInternalValue] = useState<string | string[] | null>(\n    () => defaultValue ?? (multiple ? [] : null),\n  );\n  const selection = value !== undefined ? value : internalValue;\n  const selectedPaths = useMemo(\n    () => (selection == null ? [] : Array.isArray(selection) ? selection : [selection]),\n    [selection],\n  );\n\n  \/\/ ---------------------------------------------------------------------------\n  \/\/ Data layer \u2014 snapshot map + lazy provider cache\n  \/\/ ---------------------------------------------------------------------------\n\n  const snapshotMap = useMemo(() => buildSnapshotMap(snapshot), [snapshot]);\n\n  const [loadedChildren, setLoadedChildren] = useState<Record<string, FileEntry[]>>({});\n  const [loadStatus, setLoadStatus] = useState<Record<string, FileLoadStatus>>({});\n  const [loadErrors, setLoadErrors] = useState<Record<string, string>>({});\n\n  const seqCounter = useRef(0);\n  const requestSeq = useRef<Record<string, number>>({});\n\n  \/\/ Latest-state refs so `loadPath` stays referentially stable.\n  const stateRef = useRef({ snapshotMap, loadedChildren, loadStatus });\n  stateRef.current = { snapshotMap, loadedChildren, loadStatus };\n  const providerRef = useRef(provider);\n  providerRef.current = provider;\n  const onErrorRef = useRef(onError);\n  onErrorRef.current = onError;\n  const onChangeRef = useRef(onChange);\n  onChangeRef.current = onChange;\n\n  const loadPath = useCallback((targetPath: string, options?: { reload?: boolean }) => {\n    const prov = providerRef.current;\n    if (!prov) return;\n    const { snapshotMap: snap, loadedChildren: loaded, loadStatus: statuses } = stateRef.current;\n    const status = statuses[targetPath] ?? \"idle\";\n    if (status === \"loading\") return;\n    const known = snap.get(targetPath) ?? loaded[targetPath];\n    if (!options?.reload && (known !== undefined || status === \"error\")) return;\n\n    const requestId = ++seqCounter.current;\n    requestSeq.current[targetPath] = requestId;\n    setLoadStatus((prev) => ({ ...prev, [targetPath]: \"loading\" }));\n\n    prov.loadChildren(targetPath).then(\n      (entries) => {\n        if (requestSeq.current[targetPath] !== requestId) return;\n        setLoadedChildren((prev) => ({ ...prev, [targetPath]: entries }));\n        setLoadStatus((prev) => ({ ...prev, [targetPath]: \"loaded\" }));\n        setLoadErrors((prev) => {\n          if (!(targetPath in prev)) return prev;\n          const next = { ...prev };\n          delete next[targetPath];\n          return next;\n        });\n      },\n      (error: unknown) => {\n        if (requestSeq.current[targetPath] !== requestId) return;\n        setLoadStatus((prev) => ({ ...prev, [targetPath]: \"error\" }));\n        setLoadErrors((prev) => ({\n          ...prev,\n          [targetPath]: error instanceof Error ? error.message : String(error),\n        }));\n        onErrorRef.current?.(targetPath, error);\n      },\n    );\n  }, []);\n\n  const entriesFor = useCallback(\n    (p: string) => snapshotMap.get(p) ?? loadedChildren[p],\n    [snapshotMap, loadedChildren],\n  );\n\n  const statusFor = useCallback(\n    (p: string): FileLoadStatus => {\n      if (snapshotMap.has(p)) return \"loaded\";\n      const status = loadStatus[p];\n      if (status) return status;\n      return loadedChildren[p] !== undefined ? \"loaded\" : \"idle\";\n    },\n    [snapshotMap, loadStatus, loadedChildren],\n  );\n\n  const errorFor = useCallback((p: string) => loadErrors[p], [loadErrors]);\n\n  \/\/ Lazy loading \u2014 only the current directory and explicitly expanded folders\n  \/\/ (including expansions driven from outside via the controlled\n  \/\/ `expandedPaths` prop). Never an eager walk; errored paths wait for an\n  \/\/ explicit retry.\n  useEffect(() => {\n    if (!provider) return;\n    loadPath(currentPath);\n    for (const p of expanded) loadPath(p);\n  }, [provider, currentPath, expanded, snapshotMap, loadPath]);\n\n  \/\/ ---------------------------------------------------------------------------\n  \/\/ Filter + sort + flattened visible rows\n  \/\/ ---------------------------------------------------------------------------\n\n  const filterLower = filterState.trim().toLowerCase();\n\n  const visibleChildrenFor = useCallback(\n    (p: string): FileEntry[] => {\n      const entries = entriesFor(p);\n      if (!entries) return [];\n      const filtered = filterLower\n        ? entries.filter((entry) => entryMatchesFilter(entry, filterLower, entriesFor))\n        : entries.slice();\n      return filtered.sort((a, b) => compareFileEntries(a, b, sortState));\n    },\n    [entriesFor, filterLower, sortState],\n  );\n\n  const visibleRows = useMemo(() => {\n    const rows: FileBrowserRow[] = [];\n    const visited = new Set<string>([currentPath]);\n    const walk = (p: string, depth: number, parent: string | null) => {\n      for (const entry of visibleChildrenFor(p)) {\n        const expandable = isEntryExpandable(entry, entriesFor(entry.path), !!provider);\n        const isOpen = expandable && expanded.includes(entry.path);\n        rows.push({ entry, depth, expandable, expanded: isOpen, parentPath: parent });\n        if (isOpen && !visited.has(entry.path)) {\n          visited.add(entry.path);\n          walk(entry.path, depth + 1, entry.path);\n        }\n      }\n    };\n    walk(currentPath, 0, null);\n    return rows;\n  }, [visibleChildrenFor, entriesFor, provider, expanded, currentPath]);\n\n  \/\/ ---------------------------------------------------------------------------\n  \/\/ Selection\n  \/\/ ---------------------------------------------------------------------------\n\n  const entryIndex = useMemo(() => {\n    const map = new Map<string, FileEntry>();\n    for (const list of snapshotMap.values()) {\n      for (const entry of list) map.set(entry.path, entry);\n    }\n    for (const list of Object.values(loadedChildren)) {\n      for (const entry of list) {\n        if (!map.has(entry.path)) map.set(entry.path, entry);\n      }\n    }\n    return map;\n  }, [snapshotMap, loadedChildren]);\n\n  const isSelectable = useCallback(\n    (entry: FileEntry) =>\n      !entry.disabled &&\n      (select === \"both\" || (select === \"file\" ? entry.kind === \"file\" : entry.kind === \"dir\")),\n    [select],\n  );\n\n  const isSelected = useCallback((p: string) => selectedPaths.includes(p), [selectedPaths]);\n\n  const commitSelection = useCallback(\n    (paths: string[]) => {\n      const nextValue = multiple ? paths : (paths[0] ?? null);\n      if (value === undefined) setInternalValue(nextValue);\n      onChangeRef.current?.(\n        nextValue,\n        paths\n          .map((p) => entryIndex.get(p))\n          .filter((entry): entry is FileEntry => entry !== undefined),\n      );\n    },\n    [multiple, value, entryIndex],\n  );\n\n  const selectEntry = useCallback(\n    (entry: FileEntry) => {\n      if (!isSelectable(entry)) return;\n      if (multiple) {\n        commitSelection(\n          selectedPaths.includes(entry.path)\n            ? selectedPaths.filter((p) => p !== entry.path)\n            : [...selectedPaths, entry.path],\n        );\n      } else {\n        commitSelection([entry.path]);\n      }\n    },\n    [isSelectable, multiple, selectedPaths, commitSelection],\n  );\n\n  \/\/ ---------------------------------------------------------------------------\n  \/\/ Expansion, navigation, roving focus\n  \/\/ ---------------------------------------------------------------------------\n\n  const toggleExpanded = useCallback(\n    (p: string) => {\n      setExpanded((prev) => (prev.includes(p) ? prev.filter((x) => x !== p) : [...prev, p]));\n    },\n    [setExpanded],\n  );\n\n  const [focusedPath, setFocusedPath] = useState<string | null>(null);\n\n  const navigate = useCallback(\n    (p: string) => {\n      setCurrentPath(p);\n      setFilterState(\"\");\n      setFocusedPath(null);\n    },\n    [setCurrentPath, setFilterState],\n  );\n\n  const rowRefs = useRef(new Map<string, HTMLElement>());\n\n  const registerRow = useCallback((p: string, el: HTMLElement | null) => {\n    if (el) rowRefs.current.set(p, el);\n    else rowRefs.current.delete(p);\n  }, []);\n\n  const focusRow = useCallback((p: string) => {\n    rowRefs.current.get(p)?.focus();\n  }, []);\n\n  const tabFocusPath = useMemo(() => {\n    if (focusedPath && visibleRows.some((row) => row.entry.path === focusedPath)) {\n      return focusedPath;\n    }\n    return visibleRows[0]?.entry.path ?? null;\n  }, [focusedPath, visibleRows]);\n\n  \/\/ ---------------------------------------------------------------------------\n  \/\/ Context + render\n  \/\/ ---------------------------------------------------------------------------\n\n  const ctx = useMemo<FileBrowserContextValue>(\n    () => ({\n      entriesFor,\n      visibleChildrenFor,\n      statusFor,\n      errorFor,\n      loadPath,\n      hasProvider: !!provider,\n      path: currentPath,\n      navigate,\n      expandedPaths: expanded,\n      toggleExpanded,\n      select,\n      multiple,\n      selectedPaths,\n      isSelected,\n      isSelectable,\n      selectEntry,\n      sort: sortState,\n      setSort: setSortState,\n      filter: filterState,\n      setFilter: setFilterState,\n      visibleRows,\n      focusedPath,\n      setFocusedPath,\n      tabFocusPath,\n      focusRow,\n      registerRow,\n      indentSize,\n      showIcons,\n    }),\n    [\n      entriesFor, visibleChildrenFor, statusFor, errorFor, loadPath, provider,\n      currentPath, navigate, expanded, toggleExpanded, select, multiple,\n      selectedPaths, isSelected, isSelectable, selectEntry, sortState,\n      setSortState, filterState, setFilterState, visibleRows, focusedPath,\n      tabFocusPath, focusRow, registerRow, indentSize, showIcons,\n    ],\n  );\n\n  return (\n    <FileBrowserContext.Provider value={ctx}>\n      <div\n        data-react-fancy-file-browser=\"\"\n        className={cn(\n          \"flex flex-col overflow-hidden rounded-lg border border-zinc-200 bg-white text-sm dark:border-zinc-700 dark:bg-zinc-900\",\n          className,\n        )}\n      >\n        {children ?? (\n          <>\n            <FileBrowserPathBar \/>\n            <FileBrowserToolbar \/>\n            <FileBrowserTree \/>\n          <\/>\n        )}\n      <\/div>\n    <\/FileBrowserContext.Provider>\n  );\n}\n\nFileBrowserRoot.displayName = \"FileBrowser\";\n\nexport const FileBrowser = Object.assign(FileBrowserRoot, {\n  PathBar: FileBrowserPathBar,\n  Toolbar: FileBrowserToolbar,\n  Tree: FileBrowserTree,\n  Node: FileBrowserNode,\n});\n","type":"registry:ui","target":"components\/fancy\/file-browser\/FileBrowser.tsx"},{"path":"components\/fancy\/file-browser\/FileBrowser.types.ts","content":"import type { ReactNode } from \"react\";\n\n\/** Entry kind \u2014 regular file or directory. *\/\nexport type FileKind = \"file\" | \"dir\";\n\n\/**\n * A single file-system entry. JSON-friendly by design so agents and remote\n * hosts can emit entries directly (over MCP, a relay, or a WebSocket).\n * `path` is the stable identity \u2014 POSIX-style, never an index.\n *\/\nexport interface FileEntry {\n  \/** Stable identity \u2014 POSIX-style path (e.g. `\"\/src\/App.tsx\"`). *\/\n  path: string;\n  \/** Display name (usually the last path segment). *\/\n  name: string;\n  \/** `\"file\"` or `\"dir\"`. *\/\n  kind: FileKind;\n  \/** Size in bytes (optional; shown for files and used by size sorting). *\/\n  size?: number;\n  \/** Last-modified timestamp, ISO 8601 (optional; used by mtime sorting). *\/\n  mtime?: string;\n  \/**\n   * Dirs only: `false` = known-empty (no expand affordance), `true` = has\n   * children, `undefined` = unknown (expandable when a provider is present).\n   *\/\n  hasChildren?: boolean;\n  \/** Disabled entries render dimmed and cannot be selected, expanded, or navigated into. *\/\n  disabled?: boolean;\n}\n\n\/**\n * JSON-friendly snapshot node \u2014 a {@link FileEntry} plus optionally\n * materialized children. `children: undefined` on a dir = unknown depth (a\n * provider fills it lazily in hybrid mode); `children: []` = known-empty.\n *\/\nexport interface FileSnapshotNode extends FileEntry {\n  children?: FileSnapshotNode[];\n}\n\n\/** Async data source for provider mode. Works against local FS, HTTP, MCP bridges, SSH adapters \u2014 anything that resolves a listing. *\/\nexport interface FileBrowserProvider {\n  \/**\n   * Load the direct children of `path`. Called lazily \u2014 only for the current\n   * directory and explicitly expanded folders. The component never walks the\n   * tree eagerly.\n   *\/\n  loadChildren: (path: string) => Promise<FileEntry[]>;\n}\n\n\/** Which entry kinds are selectable. *\/\nexport type FileSelectMode = \"file\" | \"directory\" | \"both\";\n\nexport type FileSortField = \"name\" | \"size\" | \"mtime\";\nexport type FileSortDirection = \"asc\" | \"desc\";\n\n\/** Sort order for directory listings. Directories always sort before files. *\/\nexport interface FileSort {\n  by: FileSortField;\n  direction: FileSortDirection;\n}\n\n\/** Per-path load lifecycle for provider mode. *\/\nexport type FileLoadStatus = \"idle\" | \"loading\" | \"loaded\" | \"error\";\n\nexport interface FileBrowserProps {\n  \/** Async data source (provider mode). Folders load on first expand \u2014 never an eager walk. *\/\n  provider?: FileBrowserProvider;\n  \/**\n   * JSON-friendly tree value (snapshot mode). Replace or patch it from outside\n   * as stream chunks land \u2014 the component treats it as the source of truth for\n   * every path it covers. May be combined with `provider` (hybrid): the\n   * snapshot seeds, the provider fills unknown-depth folders.\n   *\/\n  snapshot?: FileSnapshotNode[];\n  \/** Which entry kinds are selectable (default `\"file\"`). Non-selectable entries stay browsable. *\/\n  select?: FileSelectMode;\n  \/** Allow selecting multiple entries; `value` becomes `string[]` (default `false`). *\/\n  multiple?: boolean;\n  \/** Controlled selection \u2014 a path, an array of paths (`multiple`), or `null`. *\/\n  value?: string | string[] | null;\n  \/** Initial selection (uncontrolled). *\/\n  defaultValue?: string | string[] | null;\n  \/** Called with the next selection and the matching known entries. *\/\n  onChange?: (value: string | string[] | null, entries: FileEntry[]) => void;\n  \/** Controlled current directory. *\/\n  path?: string;\n  \/** Initial current directory (uncontrolled, default `\"\/\"`). *\/\n  defaultPath?: string;\n  \/** Called when the current directory changes (breadcrumb click, path input, double-clicked folder). *\/\n  onPathChange?: (path: string) => void;\n  \/** Controlled expanded folder paths. *\/\n  expandedPaths?: string[];\n  \/** Initially expanded folder paths (uncontrolled). *\/\n  defaultExpandedPaths?: string[];\n  \/** Called when the expanded set changes. *\/\n  onExpandedChange?: (paths: string[]) => void;\n  \/** Controlled sort order. *\/\n  sort?: FileSort;\n  \/** Initial sort order (uncontrolled, default `{ by: \"name\", direction: \"asc\" }`). *\/\n  defaultSort?: FileSort;\n  \/** Called when the sort order changes. *\/\n  onSortChange?: (sort: FileSort) => void;\n  \/** Controlled name filter \u2014 a client-side substring match over loaded nodes. *\/\n  filter?: string;\n  \/** Initial name filter (uncontrolled). *\/\n  defaultFilter?: string;\n  \/** Called when the name filter changes. *\/\n  onFilterChange?: (filter: string) => void;\n  \/** Called when a provider load rejects; the failed folder shows an inline error with a retry. *\/\n  onError?: (path: string, error: unknown) => void;\n  \/** Indent per nesting level in px (default `16`). *\/\n  indentSize?: number;\n  \/** Show file\/folder icons (default `true`). *\/\n  showIcons?: boolean;\n  \/** Custom className for the outer shell. *\/\n  className?: string;\n  \/**\n   * Custom layout. When omitted, renders the default\n   * `<FileBrowser.PathBar \/>` + `<FileBrowser.Toolbar \/>` + `<FileBrowser.Tree \/>`.\n   *\/\n  children?: ReactNode;\n}\n\n\/** A flattened, visible row \u2014 the keyboard-navigation order of the tree pane. *\/\nexport interface FileBrowserRow {\n  entry: FileEntry;\n  depth: number;\n  \/** Whether the row shows an expand affordance. *\/\n  expandable: boolean;\n  \/** Whether the row is currently expanded. *\/\n  expanded: boolean;\n  \/** Path of the parent row, or `null` for top-level rows of the current directory. *\/\n  parentPath: string | null;\n}\n\nexport interface FileBrowserContextValue {\n  \/\/ Data\n  \/** Known children of a path (snapshot first, then provider cache), or `undefined` when not yet loaded. *\/\n  entriesFor: (path: string) => FileEntry[] | undefined;\n  \/** Children of a path after the name filter + sort are applied (empty when unknown). *\/\n  visibleChildrenFor: (path: string) => FileEntry[];\n  statusFor: (path: string) => FileLoadStatus;\n  errorFor: (path: string) => string | undefined;\n  \/** Request a lazy load of a path's children (no-op without a provider or when already known\/loading). *\/\n  loadPath: (path: string, options?: { reload?: boolean }) => void;\n  hasProvider: boolean;\n  \/\/ Navigation\n  path: string;\n  \/** Change the current directory (also clears the name filter and resets roving focus). *\/\n  navigate: (path: string) => void;\n  \/\/ Expansion\n  expandedPaths: string[];\n  toggleExpanded: (path: string) => void;\n  \/\/ Selection\n  select: FileSelectMode;\n  multiple: boolean;\n  selectedPaths: string[];\n  isSelected: (path: string) => boolean;\n  isSelectable: (entry: FileEntry) => boolean;\n  selectEntry: (entry: FileEntry) => void;\n  \/\/ Sort + filter\n  sort: FileSort;\n  setSort: (sort: FileSort) => void;\n  filter: string;\n  setFilter: (filter: string) => void;\n  \/\/ Keyboard \/ roving focus\n  visibleRows: FileBrowserRow[];\n  focusedPath: string | null;\n  setFocusedPath: (path: string | null) => void;\n  \/** The row that currently owns `tabIndex={0}`. *\/\n  tabFocusPath: string | null;\n  focusRow: (path: string) => void;\n  registerRow: (path: string, el: HTMLElement | null) => void;\n  \/\/ Presentation\n  indentSize: number;\n  showIcons: boolean;\n}\n\nexport interface FileBrowserPathBarProps {\n  \/** Allow switching to the editable path input (default `true`). *\/\n  editable?: boolean;\n  \/** Placeholder for the path input. *\/\n  placeholder?: string;\n  className?: string;\n}\n\nexport interface FileBrowserToolbarProps {\n  \/** Placeholder for the name filter input (default `\"Filter\"`). *\/\n  filterPlaceholder?: string;\n  className?: string;\n}\n\nexport interface FileBrowserTreeProps {\n  \/** Accessible label for the tree (default `\"Files\"`). *\/\n  ariaLabel?: string;\n  className?: string;\n}\n\nexport interface FileBrowserNodeProps {\n  entry: FileEntry;\n  depth: number;\n}\n","type":"registry:ui","target":"components\/fancy\/file-browser\/FileBrowser.types.ts"},{"path":"components\/fancy\/file-browser\/FileBrowser.utils.ts","content":"import type { FileEntry, FileSort } from \".\/FileBrowser.types\";\n\n\/**\n * Normalize free-typed path input: forward slashes, collapsed separators,\n * resolved `.` \/ `..` segments, no trailing slash. Absolute paths stay\n * absolute; relative paths stay relative (entry paths are identity and are\n * never rewritten \u2014 this is only for the editable path input).\n *\/\nexport function normalizePath(input: string): string {\n  const raw = input.trim().replace(\/\\\\\/g, \"\/\");\n  if (raw === \"\") return \"\/\";\n  const absolute = raw.startsWith(\"\/\");\n  const out: string[] = [];\n  for (const segment of raw.split(\"\/\")) {\n    if (segment === \"\" || segment === \".\") continue;\n    if (segment === \"..\") {\n      out.pop();\n      continue;\n    }\n    out.push(segment);\n  }\n  if (out.length === 0) return \"\/\";\n  return (absolute ? \"\/\" : \"\") + out.join(\"\/\");\n}\n\n\/** Parent of a path. Paths without a separator group under `\"\/\"`. *\/\nexport function parentPath(path: string): string {\n  const i = path.lastIndexOf(\"\/\");\n  if (i === -1) return \"\/\";\n  if (i === 0) return \"\/\";\n  return path.slice(0, i);\n}\n\nexport interface PathSegment {\n  label: string;\n  path: string;\n}\n\n\/** Cumulative breadcrumb segments for a path (root anchor excluded). *\/\nexport function pathSegments(path: string): PathSegment[] {\n  const absolute = path.startsWith(\"\/\");\n  const parts = path.split(\"\/\").filter(Boolean);\n  const segments: PathSegment[] = [];\n  let acc = \"\";\n  parts.forEach((part, i) => {\n    acc = i === 0 && !absolute ? part : `${acc}\/${part}`;\n    segments.push({ label: part, path: acc });\n  });\n  return segments;\n}\n\n\/**\n * Deterministic, numeric-aware name comparison. The locale is pinned to \"en\"\n * so server and client renders order identically (SSR safety).\n *\/\nfunction compareNames(a: string, b: string): number {\n  const cmp = a.localeCompare(b, \"en\", { numeric: true, sensitivity: \"base\" });\n  if (cmp !== 0) return cmp;\n  return a < b ? -1 : a > b ? 1 : 0;\n}\n\n\/** Dirs-first, then the active sort field, with a stable ascending name tie-break. *\/\nexport function compareFileEntries(a: FileEntry, b: FileEntry, sort: FileSort): number {\n  if (a.kind !== b.kind) return a.kind === \"dir\" ? -1 : 1;\n  const direction = sort.direction === \"desc\" ? -1 : 1;\n  let cmp: number;\n  switch (sort.by) {\n    case \"size\":\n      cmp = (a.size ?? -1) - (b.size ?? -1);\n      break;\n    case \"mtime\": {\n      const am = a.mtime ?? \"\";\n      const bm = b.mtime ?? \"\";\n      cmp = am < bm ? -1 : am > bm ? 1 : 0;\n      break;\n    }\n    default:\n      cmp = compareNames(a.name, b.name);\n  }\n  if (cmp !== 0) return direction * cmp;\n  return compareNames(a.name, b.name);\n}\n\n\/**\n * Whether a dir shows an expand affordance: known-empty dirs never do\n * (`hasChildren: false`, or a loaded\/snapshot listing of zero entries);\n * unknown-depth dirs only do when a provider can load them.\n *\/\nexport function isEntryExpandable(\n  entry: FileEntry,\n  children: FileEntry[] | undefined,\n  hasProvider: boolean,\n): boolean {\n  if (entry.kind !== \"dir\" || entry.hasChildren === false) return false;\n  if (children !== undefined) return children.length > 0;\n  return hasProvider;\n}\n\n\/**\n * Client-side name filter over loaded nodes only: an entry matches when its\n * name contains the query, or (for dirs) when any already-loaded descendant\n * matches. Never triggers loads.\n *\/\nexport function entryMatchesFilter(\n  entry: FileEntry,\n  query: string,\n  entriesFor: (path: string) => FileEntry[] | undefined,\n  visited: Set<string> = new Set(),\n): boolean {\n  if (entry.name.toLowerCase().includes(query)) return true;\n  if (entry.kind !== \"dir\" || visited.has(entry.path)) return false;\n  visited.add(entry.path);\n  const children = entriesFor(entry.path);\n  return children?.some((child) => entryMatchesFilter(child, query, entriesFor, visited)) ?? false;\n}\n\n\/** Compact, deterministic byte formatting (\"1.4 KB\", \"12 MB\"). *\/\nexport function formatFileSize(bytes: number): string {\n  if (!Number.isFinite(bytes) || bytes < 0) return \"\";\n  if (bytes < 1024) return `${bytes} B`;\n  const units = [\"KB\", \"MB\", \"GB\", \"TB\"];\n  let value = bytes;\n  let unit = -1;\n  do {\n    value \/= 1024;\n    unit++;\n  } while (value >= 1024 && unit < units.length - 1);\n  const rounded = value >= 100 ? Math.round(value) : Math.round(value * 10) \/ 10;\n  return `${rounded} ${units[unit]}`;\n}\n","type":"registry:ui","target":"components\/fancy\/file-browser\/FileBrowser.utils.ts"},{"path":"components\/fancy\/file-browser\/FileBrowserNode.tsx","content":"import { ChevronRight, CircleAlert, Loader2, RotateCw } from \"lucide-react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { useFileBrowser } from \".\/FileBrowser.context\";\nimport { formatFileSize, isEntryExpandable } from \".\/FileBrowser.utils\";\nimport type { FileBrowserNodeProps } from \".\/FileBrowser.types\";\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Icons \u2014 mirror TreeNav's file\/folder icons so both tree surfaces look the\n\/\/ same (TreeNav keeps them internal; duplicating ~40 lines beats widening its\n\/\/ public API).\n\/\/ ---------------------------------------------------------------------------\n\nconst EXT_COLORS: Record<string, string> = {\n  ts: \"#3178c6\",\n  tsx: \"#3178c6\",\n  js: \"#f7df1e\",\n  jsx: \"#f7df1e\",\n  php: \"#777bb4\",\n  html: \"#e34c26\",\n  htm: \"#e34c26\",\n  css: \"#264de4\",\n  json: \"#a1a1aa\",\n  md: \"#71717a\",\n  yaml: \"#cb171e\",\n  yml: \"#cb171e\",\n};\n\nfunction FileIcon({ ext }: { ext?: string }) {\n  const color = (ext && EXT_COLORS[ext.toLowerCase()]) || \"#71717a\";\n  return (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" className=\"shrink-0\">\n      <path d=\"M4 1h5.5L13 4.5V14a1 1 0 01-1 1H4a1 1 0 01-1-1V2a1 1 0 011-1z\" stroke={color} strokeWidth=\"1.2\" \/>\n      <path d=\"M9 1v4h4\" stroke={color} strokeWidth=\"1.2\" \/>\n    <\/svg>\n  );\n}\n\nfunction FolderIcon({ open }: { open: boolean }) {\n  if (open) {\n    return (\n      <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" className=\"shrink-0\">\n        <path d=\"M1.5 3.5a1 1 0 011-1h3l1.5 1.5H13a1 1 0 011 1V5H2.5V3.5z\" fill=\"#fbbf24\" \/>\n        <path d=\"M1 6h13l-1.5 7.5H2.5L1 6z\" fill=\"#fbbf24\" opacity=\"0.7\" \/>\n      <\/svg>\n    );\n  }\n  return (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" className=\"shrink-0\">\n      <path d=\"M1.5 3a1 1 0 011-1h3l1.5 1.5H13a1 1 0 011 1v8a1 1 0 01-1 1H2.5a1 1 0 01-1-1V3z\" fill=\"#fbbf24\" \/>\n    <\/svg>\n  );\n}\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Status rows \u2014 loading \/ error \/ empty placeholders inside the tree. Not\n\/\/ treeitems (they never receive roving focus); the owning folder carries\n\/\/ `aria-busy` while its children load.\n\/\/ ---------------------------------------------------------------------------\n\nexport interface FileBrowserStatusRowProps {\n  depth: number;\n  kind: \"loading\" | \"error\" | \"empty\" | \"no-matches\" | \"unknown\";\n  message?: string;\n  onRetry?: () => void;\n}\n\nexport function FileBrowserStatusRow({ depth, kind, message, onRetry }: FileBrowserStatusRowProps) {\n  const { indentSize } = useFileBrowser();\n  const paddingLeft = depth * indentSize + 4 + 18;\n\n  if (kind === \"error\") {\n    return (\n      <div\n        role=\"none\"\n        data-react-fancy-file-browser-status=\"error\"\n        className=\"flex items-center gap-1.5 py-0.5 pr-2 text-xs text-red-600 dark:text-red-400\"\n        style={{ paddingLeft }}\n      >\n        <CircleAlert size={12} className=\"shrink-0\" \/>\n        <span className=\"min-w-0 flex-1 truncate\">{message || \"Failed to load\"}<\/span>\n        {onRetry && (\n          <button\n            type=\"button\"\n            onClick={onRetry}\n            className=\"flex shrink-0 items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200\"\n          >\n            <RotateCw size={11} \/>\n            Retry\n          <\/button>\n        )}\n      <\/div>\n    );\n  }\n\n  return (\n    <div\n      role=\"none\"\n      data-react-fancy-file-browser-status={kind}\n      className=\"flex items-center gap-1.5 py-0.5 text-xs text-zinc-400 italic dark:text-zinc-500\"\n      style={{ paddingLeft }}\n    >\n      {kind === \"loading\" && <Loader2 size={12} className=\"shrink-0 animate-spin\" aria-hidden=\"true\" \/>}\n      <span className=\"truncate\">\n        {kind === \"loading\" ? \"Loading\u2026\" : kind === \"empty\" ? \"Empty\" : kind === \"no-matches\" ? \"No matches\" : \"No entries\"}\n      <\/span>\n    <\/div>\n  );\n}\n\nFileBrowserStatusRow.displayName = \"FileBrowserStatusRow\";\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ Node\n\/\/ ---------------------------------------------------------------------------\n\nexport function FileBrowserNode({ entry, depth }: FileBrowserNodeProps) {\n  const {\n    entriesFor, visibleChildrenFor, statusFor, errorFor, loadPath, hasProvider,\n    expandedPaths, toggleExpanded, navigate,\n    isSelected, isSelectable, selectEntry,\n    tabFocusPath, setFocusedPath, focusRow, registerRow,\n    indentSize, showIcons,\n  } = useFileBrowser();\n\n  const rawChildren = entriesFor(entry.path);\n  const childrenKnown = rawChildren !== undefined;\n  const expandable = isEntryExpandable(entry, rawChildren, hasProvider);\n  const expanded = expandable && expandedPaths.includes(entry.path);\n  const status = statusFor(entry.path);\n  const selectable = isSelectable(entry);\n  const selected = isSelected(entry.path);\n  const loading = expanded && !childrenKnown && status === \"loading\";\n  const paddingLeft = depth * indentSize + 4;\n  const ext = entry.name.includes(\".\") ? entry.name.split(\".\").pop() : undefined;\n\n  const handleClick = () => {\n    if (entry.disabled) return;\n    setFocusedPath(entry.path);\n    focusRow(entry.path);\n    if (selectable) selectEntry(entry);\n    if (expandable) toggleExpanded(entry.path);\n  };\n\n  const handleDoubleClick = () => {\n    if (entry.disabled || entry.kind !== \"dir\") return;\n    navigate(entry.path);\n  };\n\n  const handleChevronClick = (e: React.MouseEvent) => {\n    e.stopPropagation();\n    if (!entry.disabled && expandable) toggleExpanded(entry.path);\n  };\n\n  const visibleChildren = expanded && childrenKnown ? visibleChildrenFor(entry.path) : [];\n\n  return (\n    <div\n      role=\"treeitem\"\n      data-react-fancy-file-browser-node=\"\"\n      data-path={entry.path}\n      data-kind={entry.kind}\n      aria-level={depth + 1}\n      aria-expanded={expandable ? expanded : undefined}\n      aria-selected={selectable ? selected : undefined}\n      aria-disabled={entry.disabled || undefined}\n      aria-busy={loading || undefined}\n      tabIndex={tabFocusPath === entry.path ? 0 : -1}\n      ref={(el) => registerRow(entry.path, el)}\n      onFocus={(e) => {\n        if (e.target === e.currentTarget) setFocusedPath(entry.path);\n      }}\n      className=\"group outline-none\"\n    >\n      <div\n        data-react-fancy-file-browser-row=\"\"\n        onClick={handleClick}\n        onDoubleClick={handleDoubleClick}\n        className={cn(\n          \"flex w-full cursor-pointer items-center gap-1 rounded-md py-0.5 pr-2 text-left text-[13px] transition-colors select-none\",\n          selected\n            ? \"bg-blue-500\/15 text-blue-600 dark:text-blue-400\"\n            : \"text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800\",\n          entry.disabled && \"pointer-events-none opacity-40\",\n          \"group-focus-visible:ring-2 group-focus-visible:ring-blue-500\/40 group-focus-visible:ring-inset\",\n        )}\n        style={{ paddingLeft }}\n      >\n        {expandable ? (\n          loading ? (\n            <Loader2 size={14} className=\"shrink-0 animate-spin text-zinc-400\" aria-hidden=\"true\" \/>\n          ) : (\n            <button\n              type=\"button\"\n              tabIndex={-1}\n              aria-hidden=\"true\"\n              onClick={handleChevronClick}\n              className=\"flex shrink-0 items-center justify-center text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300\"\n            >\n              <ChevronRight\n                size={14}\n                className={cn(\"transition-transform duration-150\", expanded && \"rotate-90\")}\n              \/>\n            <\/button>\n          )\n        ) : (\n          <span className=\"w-3.5 shrink-0\" \/>\n        )}\n        {showIcons &&\n          (entry.kind === \"dir\" ? <FolderIcon open={expanded} \/> : <FileIcon ext={ext} \/>)}\n        <span className=\"min-w-0 flex-1 truncate\">{entry.name}<\/span>\n        {entry.kind === \"file\" && entry.size !== undefined && (\n          <span className=\"shrink-0 text-[11px] text-zinc-400 tabular-nums dark:text-zinc-500\">\n            {formatFileSize(entry.size)}\n          <\/span>\n        )}\n      <\/div>\n\n      {expanded && (\n        <div role=\"group\" data-react-fancy-file-browser-node-children=\"\">\n          {\/* An expanded-but-unknown folder is only reachable with a provider;\n              \"idle\" means its lazy load is about to start, so render it as\n              loading (keeps SSR output and the first client frame identical). *\/}\n          {!childrenKnown && (status === \"loading\" || status === \"idle\") && (\n            <FileBrowserStatusRow depth={depth + 1} kind=\"loading\" \/>\n          )}\n          {!childrenKnown && status === \"error\" && (\n            <FileBrowserStatusRow\n              depth={depth + 1}\n              kind=\"error\"\n              message={errorFor(entry.path)}\n              onRetry={() => loadPath(entry.path, { reload: true })}\n            \/>\n          )}\n          {childrenKnown &&\n            (visibleChildren.length > 0 ? (\n              visibleChildren.map((child) => (\n                <FileBrowserNode key={child.path} entry={child} depth={depth + 1} \/>\n              ))\n            ) : (\n              <FileBrowserStatusRow\n                depth={depth + 1}\n                kind={rawChildren!.length === 0 ? \"empty\" : \"no-matches\"}\n              \/>\n            ))}\n        <\/div>\n      )}\n    <\/div>\n  );\n}\n\nFileBrowserNode.displayName = \"FileBrowserNode\";\n","type":"registry:ui","target":"components\/fancy\/file-browser\/FileBrowserNode.tsx"},{"path":"components\/fancy\/file-browser\/FileBrowserPathBar.tsx","content":"import { Fragment, useState } from \"react\";\nimport { ChevronRight, Pencil } from \"lucide-react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { useFileBrowser } from \".\/FileBrowser.context\";\nimport { normalizePath, pathSegments } from \".\/FileBrowser.utils\";\nimport type { FileBrowserPathBarProps } from \".\/FileBrowser.types\";\n\n\/\/ Segment styling mirrors Breadcrumbs.Item (the Breadcrumbs component itself\n\/\/ is href-only and collapses to a dropdown on mobile, so the path bar renders\n\/\/ its own always-visible, click-to-navigate trail).\nconst SEGMENT_CLASS =\n  \"shrink-0 rounded text-[13px] text-zinc-500 transition-colors hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300\";\nconst ACTIVE_SEGMENT_CLASS = \"shrink-0 truncate text-[13px] font-medium text-zinc-900 dark:text-white\";\n\n\/**\n * Breadcrumb trail over the current directory plus an editable path input \u2014\n * type a POSIX-style path and press Enter to navigate (lazy-loading it in\n * provider mode). Escape or blur cancels the edit.\n *\/\nexport function FileBrowserPathBar({\n  editable = true,\n  placeholder = \"\/path\/to\/folder\",\n  className,\n}: FileBrowserPathBarProps) {\n  const { path, navigate } = useFileBrowser();\n  const [editing, setEditing] = useState(false);\n  const [draft, setDraft] = useState(\"\");\n\n  const startEdit = () => {\n    setDraft(path);\n    setEditing(true);\n  };\n\n  const commit = () => {\n    navigate(normalizePath(draft));\n    setEditing(false);\n  };\n\n  const segments = pathSegments(path);\n  const atRoot = segments.length === 0;\n\n  return (\n    <div\n      data-react-fancy-file-browser-path=\"\"\n      className={cn(\n        \"flex items-center gap-1 border-b border-zinc-200 px-2 py-1.5 dark:border-zinc-700\",\n        className,\n      )}\n    >\n      {editing ? (\n        <input\n          data-react-fancy-file-browser-path-input=\"\"\n          type=\"text\"\n          value={draft}\n          onChange={(e) => setDraft(e.target.value)}\n          onKeyDown={(e) => {\n            if (e.key === \"Enter\") {\n              e.preventDefault();\n              commit();\n            } else if (e.key === \"Escape\") {\n              e.preventDefault();\n              setEditing(false);\n            }\n          }}\n          onBlur={() => setEditing(false)}\n          autoFocus\n          spellCheck={false}\n          placeholder={placeholder}\n          aria-label=\"Path\"\n          className=\"min-w-0 flex-1 rounded-md border border-zinc-300 bg-transparent px-2 py-0.5 font-mono text-xs text-zinc-700 outline-none placeholder:text-zinc-400 focus:border-blue-400 dark:border-zinc-600 dark:text-zinc-300\"\n        \/>\n      ) : (\n        <nav\n          aria-label=\"Path\"\n          onDoubleClick={editable ? startEdit : undefined}\n          className=\"flex min-w-0 flex-1 items-center gap-1 overflow-x-auto\"\n        >\n          {atRoot ? (\n            <span data-react-fancy-file-browser-path-segment=\"\" aria-current=\"location\" className={ACTIVE_SEGMENT_CLASS}>\n              \/\n            <\/span>\n          ) : (\n            <button\n              type=\"button\"\n              data-react-fancy-file-browser-path-segment=\"\"\n              onClick={() => navigate(\"\/\")}\n              className={SEGMENT_CLASS}\n            >\n              \/\n            <\/button>\n          )}\n          {segments.map((segment, i) => {\n            const last = i === segments.length - 1;\n            return (\n              <Fragment key={segment.path}>\n                {i > 0 && (\n                  <ChevronRight size={12} aria-hidden=\"true\" className=\"shrink-0 text-zinc-400\" \/>\n                )}\n                {last ? (\n                  <span\n                    data-react-fancy-file-browser-path-segment=\"\"\n                    aria-current=\"location\"\n                    className={ACTIVE_SEGMENT_CLASS}\n                  >\n                    {segment.label}\n                  <\/span>\n                ) : (\n                  <button\n                    type=\"button\"\n                    data-react-fancy-file-browser-path-segment=\"\"\n                    onClick={() => navigate(segment.path)}\n                    className={SEGMENT_CLASS}\n                  >\n                    {segment.label}\n                  <\/button>\n                )}\n              <\/Fragment>\n            );\n          })}\n        <\/nav>\n      )}\n      {editable && !editing && (\n        <button\n          type=\"button\"\n          aria-label=\"Edit path\"\n          onClick={startEdit}\n          className=\"shrink-0 rounded-md p-1 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300\"\n        >\n          <Pencil size={13} \/>\n        <\/button>\n      )}\n    <\/div>\n  );\n}\n\nFileBrowserPathBar.displayName = \"FileBrowserPathBar\";\n","type":"registry:ui","target":"components\/fancy\/file-browser\/FileBrowserPathBar.tsx"},{"path":"components\/fancy\/file-browser\/FileBrowserToolbar.tsx","content":"import { ArrowDown, ArrowUp, Search, X } from \"lucide-react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport { useFileBrowser } from \".\/FileBrowser.context\";\nimport type { FileBrowserToolbarProps, FileSortField } from \".\/FileBrowser.types\";\n\nconst SORT_FIELDS: { field: FileSortField; label: string }[] = [\n  { field: \"name\", label: \"Name\" },\n  { field: \"size\", label: \"Size\" },\n  { field: \"mtime\", label: \"Modified\" },\n];\n\n\/**\n * Toolbar: client-side name filter (over loaded nodes only \u2014 never triggers\n * loads) plus a dirs-first sort control. Clicking the active sort field flips\n * its direction.\n *\/\nexport function FileBrowserToolbar({ filterPlaceholder = \"Filter\", className }: FileBrowserToolbarProps) {\n  const { filter, setFilter, sort, setSort } = useFileBrowser();\n\n  return (\n    <div\n      data-react-fancy-file-browser-toolbar=\"\"\n      className={cn(\n        \"flex items-center gap-2 border-b border-zinc-200 px-2 py-1.5 dark:border-zinc-700\",\n        className,\n      )}\n    >\n      <div\n        data-react-fancy-file-browser-filter=\"\"\n        className=\"flex min-w-0 flex-1 items-center gap-1.5 rounded-md border border-zinc-200 px-2 dark:border-zinc-700\"\n      >\n        <Search size={13} aria-hidden=\"true\" className=\"shrink-0 text-zinc-400\" \/>\n        <input\n          type=\"text\"\n          value={filter}\n          onChange={(e) => setFilter(e.target.value)}\n          placeholder={filterPlaceholder}\n          aria-label=\"Filter by name\"\n          spellCheck={false}\n          className=\"min-w-0 flex-1 bg-transparent py-1 text-xs text-zinc-700 outline-none placeholder:text-zinc-400 dark:text-zinc-300\"\n        \/>\n        {filter !== \"\" && (\n          <button\n            type=\"button\"\n            aria-label=\"Clear filter\"\n            onClick={() => setFilter(\"\")}\n            className=\"shrink-0 text-zinc-400 transition-colors hover:text-zinc-600 dark:hover:text-zinc-300\"\n          >\n            <X size={12} \/>\n          <\/button>\n        )}\n      <\/div>\n      <div\n        data-react-fancy-file-browser-sort=\"\"\n        role=\"group\"\n        aria-label=\"Sort\"\n        className=\"flex shrink-0 items-center gap-0.5\"\n      >\n        {SORT_FIELDS.map(({ field, label }) => {\n          const active = sort.by === field;\n          return (\n            <button\n              key={field}\n              type=\"button\"\n              data-sort-field={field}\n              aria-pressed={active}\n              onClick={() =>\n                setSort(\n                  active\n                    ? { by: field, direction: sort.direction === \"asc\" ? \"desc\" : \"asc\" }\n                    : { by: field, direction: \"asc\" },\n                )\n              }\n              className={cn(\n                \"flex items-center gap-0.5 rounded-md px-1.5 py-0.5 text-xs transition-colors\",\n                active\n                  ? \"bg-zinc-100 font-medium text-zinc-900 dark:bg-zinc-800 dark:text-white\"\n                  : \"text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300\",\n              )}\n            >\n              {label}\n              {active &&\n                (sort.direction === \"asc\" ? (\n                  <ArrowUp size={11} aria-hidden=\"true\" \/>\n                ) : (\n                  <ArrowDown size={11} aria-hidden=\"true\" \/>\n                ))}\n            <\/button>\n          );\n        })}\n      <\/div>\n    <\/div>\n  );\n}\n\nFileBrowserToolbar.displayName = \"FileBrowserToolbar\";\n","type":"registry:ui","target":"components\/fancy\/file-browser\/FileBrowserToolbar.tsx"},{"path":"components\/fancy\/file-browser\/FileBrowserTree.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\nimport { useFileBrowser } from \".\/FileBrowser.context\";\nimport { FileBrowserNode, FileBrowserStatusRow } from \".\/FileBrowserNode\";\nimport type { FileBrowserRow, FileBrowserTreeProps } from \".\/FileBrowser.types\";\n\n\/**\n * The tree pane: ARIA `tree` semantics, roving tabindex, and full keyboard\n * navigation (arrows \/ Enter \/ Space \/ Home \/ End) over the flattened row\n * order. Lazy loads fire on expand only.\n *\/\nexport function FileBrowserTree({ ariaLabel = \"Files\", className }: FileBrowserTreeProps) {\n  const ctx = useFileBrowser();\n  const {\n    path, entriesFor, visibleChildrenFor, statusFor, errorFor, loadPath, hasProvider,\n    visibleRows, tabFocusPath, setFocusedPath, focusRow,\n    toggleExpanded, isSelectable, selectEntry, navigate, multiple,\n  } = ctx;\n\n  const rootEntries = entriesFor(path);\n  const rootStatus = statusFor(path);\n  const rootChildren = rootEntries !== undefined ? visibleChildrenFor(path) : [];\n\n  const moveTo = (row: FileBrowserRow) => {\n    setFocusedPath(row.entry.path);\n    focusRow(row.entry.path);\n  };\n\n  const activate = (row: FileBrowserRow) => {\n    if (row.entry.disabled) return;\n    if (isSelectable(row.entry)) selectEntry(row.entry);\n    if (row.expandable) toggleExpanded(row.entry.path);\n    else if (row.entry.kind === \"dir\" && !isSelectable(row.entry)) navigate(row.entry.path);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    const rows = visibleRows;\n    if (rows.length === 0) return;\n    const index = rows.findIndex((row) => row.entry.path === tabFocusPath);\n    const current = index >= 0 ? rows[index] : rows[0];\n\n    switch (e.key) {\n      case \"ArrowDown\":\n        e.preventDefault();\n        if (index < rows.length - 1) moveTo(rows[index + 1]);\n        break;\n      case \"ArrowUp\":\n        e.preventDefault();\n        if (index > 0) moveTo(rows[index - 1]);\n        break;\n      case \"Home\":\n        e.preventDefault();\n        moveTo(rows[0]);\n        break;\n      case \"End\":\n        e.preventDefault();\n        moveTo(rows[rows.length - 1]);\n        break;\n      case \"ArrowRight\":\n        e.preventDefault();\n        if (current.entry.disabled) break;\n        if (current.expandable && !current.expanded) {\n          toggleExpanded(current.entry.path);\n        } else if (current.expanded) {\n          const firstChild = rows[index + 1];\n          if (firstChild && firstChild.parentPath === current.entry.path) moveTo(firstChild);\n        }\n        break;\n      case \"ArrowLeft\":\n        e.preventDefault();\n        if (current.expanded && !current.entry.disabled) {\n          toggleExpanded(current.entry.path);\n        } else if (current.parentPath) {\n          const parent = rows.find((row) => row.entry.path === current.parentPath);\n          if (parent) moveTo(parent);\n        }\n        break;\n      case \"Enter\":\n        e.preventDefault();\n        activate(current);\n        break;\n      case \" \":\n        e.preventDefault();\n        if (!current.entry.disabled) selectEntry(current.entry);\n        break;\n    }\n  };\n\n  return (\n    <div\n      role=\"tree\"\n      aria-label={ariaLabel}\n      aria-multiselectable={multiple || undefined}\n      data-react-fancy-file-browser-tree=\"\"\n      className={cn(\"min-h-0 flex-1 overflow-auto p-1\", className)}\n      onKeyDown={handleKeyDown}\n    >\n      {\/* \"idle\" with a provider renders as loading too: loads fire in an\n          effect, so this is what the server (and first client frame) shows\n          right before the request starts \u2014 keeps SSR + hydration identical. *\/}\n      {rootEntries === undefined && hasProvider && (rootStatus === \"loading\" || rootStatus === \"idle\") && (\n        <FileBrowserStatusRow depth={0} kind=\"loading\" \/>\n      )}\n      {rootEntries === undefined && rootStatus === \"error\" && (\n        <FileBrowserStatusRow\n          depth={0}\n          kind=\"error\"\n          message={errorFor(path)}\n          onRetry={() => loadPath(path, { reload: true })}\n        \/>\n      )}\n      {rootEntries === undefined && rootStatus === \"idle\" && !hasProvider && (\n        <FileBrowserStatusRow depth={0} kind=\"unknown\" \/>\n      )}\n      {rootEntries !== undefined &&\n        (rootChildren.length > 0 ? (\n          rootChildren.map((entry) => (\n            <FileBrowserNode key={entry.path} entry={entry} depth={0} \/>\n          ))\n        ) : (\n          <FileBrowserStatusRow depth={0} kind={rootEntries.length === 0 ? \"empty\" : \"no-matches\"} \/>\n        ))}\n    <\/div>\n  );\n}\n\nFileBrowserTree.displayName = \"FileBrowserTree\";\n","type":"registry:ui","target":"components\/fancy\/file-browser\/FileBrowserTree.tsx"},{"path":"components\/fancy\/file-browser\/index.ts","content":"export { FileBrowser } from \".\/FileBrowser\";\nexport { useFileBrowser } from \".\/FileBrowser.context\";\nexport type {\n  FileEntry,\n  FileKind,\n  FileSnapshotNode,\n  FileBrowserProvider,\n  FileSelectMode,\n  FileSort,\n  FileSortField,\n  FileSortDirection,\n  FileLoadStatus,\n  FileBrowserProps,\n  FileBrowserRow,\n  FileBrowserContextValue,\n  FileBrowserPathBarProps,\n  FileBrowserToolbarProps,\n  FileBrowserTreeProps,\n  FileBrowserNodeProps,\n} from \".\/FileBrowser.types\";\n","type":"registry:ui","target":"components\/fancy\/file-browser\/index.ts"}]}