import { Children, type CSSProperties, isValidElement, type KeyboardEvent, type ReactElement, type ReactNode, useRef, useState, } from 'react'; import { ChevronRight, File as FileIcon, Folder as FolderIcon, FolderOpen, } from '@/components/icons'; import { styles } from './styles'; export interface TreeFolderProps { name: string; defaultOpen?: boolean; /** When false the folder renders statically (no toggle). Defaults to true. */ openable?: boolean; highlight?: boolean; children?: ReactNode; } export interface TreeFileProps { name: string; highlight?: boolean; } export interface TreeProps { children?: ReactNode; style?: CSSProperties; className?: string; } function elementText(node: ReactNode): string { if (typeof node === 'string' || typeof node === 'number') { return String(node); } if (Array.isArray(node)) { return node.map(elementText).join(''); } if (isValidElement<{ children?: ReactNode }>(node)) { return elementText(node.props.children); } return ''; } function isListElement(node: ReactNode): node is ReactElement<{ children?: ReactNode }> { return isValidElement(node) && (node.type === 'ul' || node.type === 'ol'); } /** Stable key for MDX-generated list wrappers (static content, never reordered). */ function listFingerprint(node: ReactNode): string { const text = elementText(node).replace(/\s+/g, ' ').trim().slice(0, 64); let hash = 0; for (let i = 0; i < text.length; i += 1) { hash = (hash * 31 + text.charCodeAt(i)) | 0; } return `mdx-list-${text.length}-${hash}`; } /** Split an MDX
  • into its label (inline content) and an optional nested list. */ function splitListItem(li: ReactElement<{ children?: ReactNode }>): { label: string; nested: ReactNode; } { let nested: ReactNode = null; const labelParts: ReactNode[] = []; Children.forEach(li.props.children, child => { if (isListElement(child)) { nested = child; } else { labelParts.push(child); } }); return { label: elementText(labelParts).trim(), nested }; } function ListItemsAsNodes({ items }: { items: ReactNode }): ReactNode { const nodes: ReactNode[] = []; let index = 0; Children.forEach(items, li => { if (!isValidElement<{ children?: ReactNode }>(li) || li.type !== 'li') { return; } const { label, nested } = splitListItem(li); if (!label && !nested) { return; } const name = label || 'untitled'; const isFolder = name.endsWith('/') || Boolean(nested); index += 1; if (isFolder) { nodes.push( {nested ? ( ).props.children} /> ) : null} , ); } else { nodes.push(); } }); return <>{nodes}; } function containsList(children: ReactNode): boolean { let found = false; Children.forEach(children, child => { if (isListElement(child)) { found = true; } }); return found; } export function TreeFolder({ name, defaultOpen = false, openable = true, highlight = false, children, }: TreeFolderProps) { const [open, setOpen] = useState(defaultOpen); const expanded = openable ? open : true; const Icon = expanded ? FolderOpen : FolderIcon; return ( // biome-ignore lint/a11y/useFocusableInteractive: keyboard focus is delegated to the row button inside this treeitem
  • {openable ? ( ) : ( {name} )} {children ? ( // biome-ignore lint/a11y/useSemanticElements: group is the required ARIA role for nested tree levels ) : null}
  • ); } export function TreeFile({ name, highlight = false }: TreeFileProps) { return ( // biome-ignore lint/a11y/useFocusableInteractive: file rows are arrow-key targets in the tree widget
  • {name}
  • ); } function visibleFocusables(root: HTMLElement): HTMLElement[] { return [...root.querySelectorAll('[data-tree-focus]')].filter( element => !element.closest('[hidden]'), ); } function TreeRoot({ children, style, className }: TreeProps) { const rootRef = useRef(null); const searchRef = useRef({ text: '', timer: 0 }); const onKeyDown = (event: KeyboardEvent) => { const root = rootRef.current; const target = event.target as HTMLElement; if (!root || !target.closest('[data-tree-focus]')) { return; } const items = visibleFocusables(root); const index = items.indexOf(target as HTMLElement); const focusAt = (next: number) => { event.preventDefault(); items[(next + items.length) % items.length]?.focus(); }; switch (event.key) { case 'ArrowDown': focusAt(index + 1); break; case 'ArrowUp': focusAt(index - 1); break; case 'Home': focusAt(0); break; case 'End': focusAt(items.length - 1); break; case 'ArrowRight': { const row = target.closest('[role="treeitem"]'); const toggle = row?.querySelector(':scope > [data-tree-focus]'); if ( toggle && toggle.tagName === 'BUTTON' && toggle.getAttribute('aria-expanded') === 'false' ) { event.preventDefault(); toggle.click(); } else { const firstChild = row?.querySelector(':scope > ul [data-tree-focus]'); if (firstChild) { event.preventDefault(); firstChild.focus(); } } break; } case 'ArrowLeft': { const row = target.closest('[role="treeitem"]'); const toggle = row?.querySelector(':scope > [data-tree-focus]'); if ( toggle && toggle.tagName === 'BUTTON' && toggle.getAttribute('aria-expanded') === 'true' ) { event.preventDefault(); toggle.click(); } else { const parent = row?.parentElement?.closest('[role="treeitem"]'); const parentFocus = parent?.querySelector(':scope > [data-tree-focus]'); if (parentFocus) { event.preventDefault(); parentFocus.focus(); } } break; } case '*': { // Expand all sibling folders at the current level. const group = target.closest('ul'); group ?.querySelectorAll( ':scope > [role="treeitem"] > button[aria-expanded="false"]', ) .forEach(button => { button.click(); }); break; } default: { if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) { const state = searchRef.current; window.clearTimeout(state.timer); state.text += event.key.toLowerCase(); state.timer = window.setTimeout(() => { state.text = ''; }, 500); const match = items.find(item => (item.textContent || '').trim().toLowerCase().startsWith(state.text), ); if (match) { event.preventDefault(); (match as HTMLElement).focus(); } } } } }; const content = containsList(children) ? Children.map(children, child => isListElement(child) ? ( ).props.children} /> ) : ( child ), ) : children; // Buttons (folders) stay in the tab order natively; file rows are reached // with arrow keys. return ( // biome-ignore lint/a11y/noStaticElementInteractions: key handling for the tree widget lives on its root
    {/* biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: tree is the required ARIA role for this widget */}
      {content}
    ); } export function Tree(props: TreeProps) { return ; } /** Alias: and are interchangeable, like Mintlify. */ export function FileTree(props: TreeProps) { return ; } Tree.Folder = TreeFolder; Tree.File = TreeFile; FileTree.Folder = TreeFolder; FileTree.File = TreeFile; export const FileTreeFolder = TreeFolder; export const FileTreeFile = TreeFile;