{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "organisation-unit-tree",
  "type": "registry:ui",
  "title": "Organisation Unit Tree",
  "description": "A tree component for selecting organisation units with lazy loading support",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "input",
    "button",
    "badge",
    "card"
  ],
  "files": [
    {
      "path": "registry/default/ui/organisation-unit-tree.tsx",
      "content": "\"use client\";\r\n\r\nimport type React from \"react\";\r\n\r\nimport { useCallback, useEffect, useState, useMemo } from \"react\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Loader2, Search, Users, ChevronDown, ChevronRight, X } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n/**\r\n * Interface for tree node data structure\r\n */\r\nexport interface TreeNode {\r\n  /** Unique identifier for the node */\r\n  id: string;\r\n  /** Display name of the node */\r\n  name: string;\r\n  /** Hierarchical level (0 = root) */\r\n  level?: number;\r\n  /** Parent node ID, null for root nodes */\r\n  parentId?: string | null;\r\n  /** Child nodes array */\r\n  children?: TreeNode[];\r\n  /** Indicates if node has children (for lazy loading) */\r\n  hasChildren?: boolean;\r\n  /** Loading state for lazy loading */\r\n  isLoading?: boolean;\r\n  /** Whether children have been loaded */\r\n  isLoaded?: boolean;\r\n  /** Allow additional properties */\r\n  [key: string]: any;\r\n}\r\n\r\n/**\r\n * Props for the OrganizationTree component\r\n */\r\nexport interface OrganizationTreeProps {\r\n  /** Tree data array */\r\n  data: TreeNode[];\r\n  /** Callback when selection changes */\r\n  onSelectionChange?: (selectedNodes: TreeNode[], selectedIds: string[]) => void;\r\n  /** Callback when search term changes */\r\n  onSearchChange?: (searchTerm: string) => void;\r\n  /** Initially selected node IDs */\r\n  initialSelected?: string[];\r\n  /** Enable single selection mode */\r\n  singleSelection?: boolean;\r\n  /** Show search and controls */\r\n  showControls?: boolean;\r\n  /** Show selection statistics */\r\n  showStats?: boolean;\r\n  /** Array of enabled node IDs (others will be disabled) */\r\n  enabledNodeIds?: string[];\r\n  /** Restrict selection to specific level */\r\n  restrictSelectionToLevel?: number;\r\n  /** Enable lazy loading of children */\r\n  enableLazyLoading?: boolean;\r\n  /** Function to load children for a node */\r\n  onLoadChildren?: (nodeId: string) => Promise<TreeNode[]>;\r\n  /** Additional CSS classes */\r\n  className?: string;\r\n  /** Initially expanded node IDs */\r\n  initiallyExpanded?: string[];\r\n  /** Custom placeholder text for search */\r\n  searchPlaceholder?: string;\r\n}\r\n\r\n/**\r\n * Internal node state for rendering\r\n */\r\ninterface NodeState {\r\n  isExpanded: boolean;\r\n  isSelected: boolean;\r\n  isHighlighted: boolean;\r\n  isSearchMatch: boolean;\r\n  isDisabled: boolean;\r\n}\r\n\r\n/**\r\n * Search result information\r\n */\r\ninterface SearchResults {\r\n  matchCount: number;\r\n  matchingIds: string[];\r\n}\r\n\r\n/**\r\n * A highly reusable tree component for displaying hierarchical organization units\r\n * with search, selection, lazy loading, and filtering capabilities.\r\n *\r\n * @example\r\n * \\`\\`\\`tsx\r\n * const data = [\r\n *   {\r\n *     id: \"1\",\r\n *     name: \"Root Organization\",\r\n *     level: 0,\r\n *     children: [\r\n *       { id: \"2\", name: \"Department A\", level: 1, parentId: \"1\" },\r\n *       { id: \"3\", name: \"Department B\", level: 1, parentId: \"1\" }\r\n *     ]\r\n *   }\r\n * ];\r\n *\r\n * <OrganizationTree\r\n *   data={data}\r\n *   onSelectionChange={(nodes, ids) => console.log('Selected:', ids)}\r\n *   singleSelection={true}\r\n *   showControls={true}\r\n *   showStats={true}\r\n * />\r\n * \\`\\`\\`\r\n */\r\nexport function OrganizationTree({\r\n  data,\r\n  onSelectionChange,\r\n  onSearchChange,\r\n  initialSelected = [],\r\n  singleSelection = false,\r\n  showControls = true,\r\n  showStats = true,\r\n  enabledNodeIds,\r\n  restrictSelectionToLevel,\r\n  enableLazyLoading = false,\r\n  onLoadChildren,\r\n  className,\r\n  initiallyExpanded = [],\r\n  searchPlaceholder = \"Search organization units...\",\r\n}: OrganizationTreeProps) {\r\n  // State management\r\n  const [searchTerm, setSearchTerm] = useState(\"\");\r\n  const [selectedIds, setSelectedIds] = useState<string[]>(initialSelected);\r\n  const [expandedIds, setExpandedIds] = useState<string[]>(initiallyExpanded);\r\n  const [loadingIds, setLoadingIds] = useState<Set<string>>(new Set());\r\n  const [treeData, setTreeData] = useState<TreeNode[]>(data);\r\n\r\n  // Update tree data when prop changes\r\n  useEffect(() => {\r\n    setTreeData(data);\r\n  }, [data]);\r\n\r\n  /**\r\n   * Flattens tree structure for easier searching and processing\r\n   */\r\n  const flattenedNodes = useMemo(() => {\r\n    const flatten = (nodes: TreeNode[], level = 0): TreeNode[] => {\r\n      return nodes.reduce((acc, node) => {\r\n        const nodeWithLevel = { ...node, level: node.level ?? level };\r\n        acc.push(nodeWithLevel);\r\n        if (node.children && node.children.length > 0) {\r\n          acc.push(...flatten(node.children, level + 1));\r\n        }\r\n        return acc;\r\n      }, [] as TreeNode[]);\r\n    };\r\n    return flatten(treeData);\r\n  }, [treeData]);\r\n\r\n  /**\r\n   * Search functionality with highlighting\r\n   */\r\n  const searchResults = useMemo((): SearchResults => {\r\n    if (!searchTerm.trim()) {\r\n      return { matchCount: 0, matchingIds: [] };\r\n    }\r\n\r\n    const searchLower = searchTerm.toLowerCase();\r\n    const matchingNodes = flattenedNodes.filter(\r\n      (node) => node.name.toLowerCase().includes(searchLower) || node.id.toLowerCase().includes(searchLower),\r\n    );\r\n\r\n    return {\r\n      matchCount: matchingNodes.length,\r\n      matchingIds: matchingNodes.map((node) => node.id),\r\n    };\r\n  }, [searchTerm, flattenedNodes]);\r\n\r\n  /**\r\n   * Find node by ID in the tree\r\n   */\r\n  const findNodeById = useCallback(\r\n    (nodeId: string): TreeNode | null => {\r\n      return flattenedNodes.find((node) => node.id === nodeId) || null;\r\n    },\r\n    [flattenedNodes],\r\n  );\r\n\r\n  /**\r\n   * Get selected nodes from IDs\r\n   */\r\n  const selectedNodes = useMemo(() => {\r\n    return selectedIds.map((id) => findNodeById(id)).filter(Boolean) as TreeNode[];\r\n  }, [selectedIds, findNodeById]);\r\n\r\n  /**\r\n   * Handle node selection\r\n   */\r\n  const handleNodeSelect = useCallback(\r\n    (nodeId: string) => {\r\n      setSelectedIds((prev) => {\r\n        let newSelected: string[];\r\n\r\n        if (singleSelection) {\r\n          newSelected = prev.includes(nodeId) ? [] : [nodeId];\r\n        } else {\r\n          newSelected = prev.includes(nodeId) ? prev.filter((id) => id !== nodeId) : [...prev, nodeId];\r\n        }\r\n\r\n        // Return updated selection\r\n        return newSelected;\r\n      });\r\n    },\r\n    [singleSelection],\r\n  );\r\n\r\n  useEffect(() => {\r\n    if (onSelectionChange) {\r\n      const newSelectedNodes = selectedIds.map((id) => findNodeById(id)).filter(Boolean) as TreeNode[];\r\n      onSelectionChange(newSelectedNodes, selectedIds);\r\n    }\r\n  }, [selectedIds]); // Removed findNodeById and onSelectionChange from dependencies\r\n\r\n  /**\r\n   * Handle node expansion/collapse\r\n   */\r\n  const handleNodeToggle = useCallback(\r\n    async (nodeId: string) => {\r\n      const isExpanded = expandedIds.includes(nodeId);\r\n\r\n      if (isExpanded) {\r\n        // Collapse\r\n        setExpandedIds((prev) => prev.filter((id) => id !== nodeId));\r\n      } else {\r\n        // Expand\r\n        setExpandedIds((prev) => [...prev, nodeId]);\r\n\r\n        // Load children if lazy loading is enabled\r\n        if (enableLazyLoading && onLoadChildren) {\r\n          const node = findNodeById(nodeId);\r\n          if (node && !node.isLoaded && !loadingIds.has(nodeId)) {\r\n            setLoadingIds((prev) => new Set(prev).add(nodeId));\r\n\r\n            try {\r\n              const children = await onLoadChildren(nodeId);\r\n\r\n              // Update tree data with loaded children\r\n              setTreeData((prevData) => {\r\n                const updateNode = (nodes: TreeNode[]): TreeNode[] => {\r\n                  return nodes.map((node) => {\r\n                    if (node.id === nodeId) {\r\n                      return {\r\n                        ...node,\r\n                        children: children,\r\n                        isLoaded: true,\r\n                        isLoading: false,\r\n                      };\r\n                    }\r\n                    if (node.children) {\r\n                      return {\r\n                        ...node,\r\n                        children: updateNode(node.children),\r\n                      };\r\n                    }\r\n                    return node;\r\n                  });\r\n                };\r\n                return updateNode(prevData);\r\n              });\r\n            } catch (error) {\r\n              console.error(\"Failed to load children for node:\", nodeId, error);\r\n            } finally {\r\n              setLoadingIds((prev) => {\r\n                const next = new Set(prev);\r\n                next.delete(nodeId);\r\n                return next;\r\n              });\r\n            }\r\n          }\r\n        }\r\n      }\r\n    },\r\n    [expandedIds, enableLazyLoading, onLoadChildren, findNodeById, loadingIds],\r\n  );\r\n\r\n  /**\r\n   * Handle search input change\r\n   */\r\n  const handleSearchChange = useCallback(\r\n    (value: string) => {\r\n      setSearchTerm(value);\r\n      onSearchChange?.(value);\r\n    },\r\n    [onSearchChange],\r\n  );\r\n\r\n  /**\r\n   * Clear search\r\n   */\r\n  const clearSearch = useCallback(() => {\r\n    setSearchTerm(\"\");\r\n    onSearchChange?.(\"\");\r\n  }, [onSearchChange]);\r\n\r\n  /**\r\n   * Clear all selections\r\n   */\r\n  const clearSelection = useCallback(() => {\r\n    setSelectedIds([]);\r\n  }, []);\r\n\r\n  /**\r\n   * Check if node should be disabled\r\n   */\r\n  const isNodeDisabled = useCallback(\r\n    (node: TreeNode): boolean => {\r\n      // Check enabled IDs filter\r\n      if (enabledNodeIds && enabledNodeIds.length > 0) {\r\n        if (!enabledNodeIds.includes(node.id)) {\r\n          return true;\r\n        }\r\n      }\r\n\r\n      // Check level restriction\r\n      if (restrictSelectionToLevel !== undefined) {\r\n        if (node.level !== restrictSelectionToLevel) {\r\n          return true;\r\n        }\r\n      }\r\n\r\n      return false;\r\n    },\r\n    [enabledNodeIds, restrictSelectionToLevel],\r\n  );\r\n\r\n  /**\r\n   * Render individual tree node\r\n   */\r\n  const renderNode = useCallback(\r\n    (node: TreeNode, level = 0): React.ReactNode => {\r\n      const hasChildren = enableLazyLoading\r\n        ? node.hasChildren || (node.children && node.children.length > 0)\r\n        : node.children && node.children.length > 0;\r\n\r\n      const isExpanded = expandedIds.includes(node.id);\r\n      const isSelected = selectedIds.includes(node.id);\r\n      const isSearchMatch = searchResults.matchingIds.includes(node.id);\r\n      const isDisabled = isNodeDisabled(node);\r\n      const isLoading = loadingIds.has(node.id);\r\n\r\n      return (\r\n        <div key={node.id}>\r\n          <div\r\n            className={cn(\r\n              \"flex items-center py-2 px-2 rounded-md transition-colors\",\r\n              \"hover:bg-accent/50\",\r\n              isSelected && \"bg-primary/10 border-l-4 border-primary\",\r\n              isSearchMatch && \"bg-yellow-100 dark:bg-yellow-900/20\",\r\n              isDisabled && \"opacity-50 cursor-not-allowed\",\r\n              !isDisabled && \"cursor-pointer\",\r\n            )}\r\n            style={{ paddingLeft: `${level * 20 + 8}px` }}\r\n            onClick={() => !isDisabled && handleNodeSelect(node.id)}\r\n          >\r\n            {/* Expand/Collapse Button */}\r\n            <Button\r\n              variant=\"ghost\"\r\n              size=\"sm\"\r\n              className=\"p-1 h-6 w-6 mr-2\"\r\n              onClick={(e) => {\r\n                e.stopPropagation();\r\n                if (hasChildren) {\r\n                  handleNodeToggle(node.id);\r\n                }\r\n              }}\r\n              disabled={!hasChildren || isLoading}\r\n            >\r\n              {isLoading ? (\r\n                <Loader2 className=\"h-4 w-4 animate-spin\" />\r\n              ) : hasChildren ? (\r\n                isExpanded ? (\r\n                  <ChevronDown className=\"h-4 w-4\" />\r\n                ) : (\r\n                  <ChevronRight className=\"h-4 w-4\" />\r\n                )\r\n              ) : (\r\n                <div className=\"w-4 h-4\" />\r\n              )}\r\n            </Button>\r\n\r\n            {/* Node Content */}\r\n            <div className=\"flex items-center flex-1 min-w-0\">\r\n              <Users className=\"h-4 w-4 text-muted-foreground mr-2 flex-shrink-0\" />\r\n              <span className={cn(\"text-sm truncate\", isSearchMatch && \"font-semibold\", isSelected && \"font-medium\")}>\r\n                {node.name}\r\n              </span>\r\n            </div>\r\n          </div>\r\n\r\n          {/* Render Children */}\r\n          {isExpanded && node.children && node.children.length > 0 && (\r\n            <div>{node.children.map((child) => renderNode(child, level + 1))}</div>\r\n          )}\r\n        </div>\r\n      );\r\n    },\r\n    [\r\n      expandedIds,\r\n      selectedIds,\r\n      searchResults.matchingIds,\r\n      isNodeDisabled,\r\n      loadingIds,\r\n      enableLazyLoading,\r\n      handleNodeSelect,\r\n      handleNodeToggle,\r\n    ],\r\n  );\r\n\r\n  if (!data || data.length === 0) {\r\n    return (\r\n      <Card className={className}>\r\n        <CardContent className=\"flex items-center justify-center p-8\">\r\n          <div className=\"text-center text-muted-foreground\">\r\n            <Users className=\"h-8 w-8 mx-auto mb-2\" />\r\n            <p>No organization units available</p>\r\n          </div>\r\n        </CardContent>\r\n      </Card>\r\n    );\r\n  }\r\n\r\n  return (\r\n    <div className={cn(\"space-y-4\", className)}>\r\n      {/* Controls */}\r\n      {showControls && (\r\n        <div className=\"space-y-4\">\r\n          <div className=\"flex gap-2\">\r\n            <div className=\"relative flex-1\">\r\n              <Search className=\"absolute left-2 top-2.5 h-4 w-4 text-muted-foreground\" />\r\n              <Input\r\n                placeholder={searchPlaceholder}\r\n                value={searchTerm}\r\n                onChange={(e) => handleSearchChange(e.target.value)}\r\n                className=\"pl-8\"\r\n              />\r\n              {searchTerm && (\r\n                <Button variant=\"ghost\" size=\"sm\" className=\"absolute right-1 top-1 h-6 w-6 p-0\" onClick={clearSearch}>\r\n                  <X className=\"h-4 w-4\" />\r\n                </Button>\r\n              )}\r\n            </div>\r\n          </div>\r\n\r\n          {/* Statistics */}\r\n          {showStats && (\r\n            <div className=\"flex flex-wrap gap-2\">\r\n              {selectedIds.length > 0 && (\r\n                <div className=\"flex items-center gap-2\">\r\n                  <Badge variant=\"outline\">Selected: {selectedIds.length}</Badge>\r\n                  <Button variant=\"ghost\" size=\"sm\" onClick={clearSelection} className=\"h-6 px-2 text-xs\">\r\n                    Clear\r\n                  </Button>\r\n                </div>\r\n              )}\r\n\r\n              {searchTerm && searchResults.matchCount > 0 && (\r\n                <Badge variant=\"secondary\">Found: {searchResults.matchCount} matches</Badge>\r\n              )}\r\n            </div>\r\n          )}\r\n\r\n          {/* Selected Items */}\r\n          {showStats && selectedNodes.length > 0 && (\r\n            <div className=\"flex flex-wrap gap-1\">\r\n              {selectedNodes.map((node) => (\r\n                <Badge key={node.id} variant=\"secondary\" className=\"text-xs\">\r\n                  {node.name}\r\n                  <Button\r\n                    variant=\"ghost\"\r\n                    size=\"sm\"\r\n                    className=\"h-4 w-4 p-0 ml-1 hover:bg-destructive hover:text-destructive-foreground\"\r\n                    onClick={() => handleNodeSelect(node.id)}\r\n                  >\r\n                    <X className=\"h-3 w-3\" />\r\n                  </Button>\r\n                </Badge>\r\n              ))}\r\n            </div>\r\n          )}\r\n        </div>\r\n      )}\r\n\r\n      {/* Tree */}\r\n      <Card>\r\n        <CardContent className=\"p-4\">\r\n          <div className=\"space-y-1\">{treeData.map((node) => renderNode(node, 0))}</div>\r\n        </CardContent>\r\n      </Card>\r\n    </div>\r\n  );\r\n}\r\n\r\nexport default OrganizationTree;\r\n",
      "type": "registry:ui"
    }
  ]
}