{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "organization-switcher",
  "title": "Organization Switcher",
  "description": "Shows which organisation the user works in and lets them switch, with optional sub-organisations, search and favourites.",
  "dependencies": [
    "@base-ui/react@^1.8.0",
    "lucide-react@^1.48.0"
  ],
  "files": [
    {
      "path": "registry/rapid/ui/organization-switcher/organization-switcher.tsx",
      "content": "'use client'\n\nimport { Combobox as ComboboxPrimitive } from '@base-ui/react/combobox'\nimport { Select as SelectPrimitive } from '@base-ui/react/select'\nimport {\n\tBuilding2Icon,\n\tCheckIcon,\n\tChevronDownIcon,\n\tChevronRightIcon,\n\tChevronsUpDownIcon,\n\tSearchIcon,\n\tStarIcon,\n} from 'lucide-react'\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\n\ntype Organization = {\n\t/** Unique id; this is the value of the switcher */\n\tid: string\n\tname: string\n\t/** Makes this a sub-organisation of the organisation with that id */\n\tparentId?: string | null\n\t/** A short line under the name, e.g. the kind of organisation */\n\tdescription?: string\n\t/** An icon or image for the tile in the trigger. Without it the tile shows the first letter of the name. */\n\tlogo?: React.ReactNode\n\tdisabled?: boolean\n}\n\n/** An organisation with its place in the tree; what `OrganizationSwitcherList` hands to its children. */\ntype OrganizationNode = Organization & {\n\t/** 0 for a top-level organisation, 1 for its sub-organisations and so on */\n\tdepth: number\n\t/** The organisations above it, top level first */\n\tancestors: Organization[]\n\t/** The names of the organisations above it, e.g. \"Ekdalen Municipality / Building and Environment\" */\n\tpath: string\n\t/** Whether it is one of the `favorites` */\n\tfavorite: boolean\n\t/** Whether it has sub-organisations */\n\thasChildren: boolean\n\t/** Whether its sub-organisations are listed; false once the user has collapsed it */\n\texpanded: boolean\n}\n\nconst getPath = (organizations: Organization[]) => organizations.map((organization) => organization.name).join(' / ')\n\n/** Orders a flat list as a tree: every organisation is followed by its sub-organisations. */\nfunction getOrganizationNodes(\n\torganizations: readonly Organization[],\n\tfavorites: ReadonlySet<string>,\n\tcollapsed: ReadonlySet<string>,\n) {\n\tconst ids = new Set(organizations.map((organization) => organization.id))\n\tconst children = new Map<string | null, Organization[]>()\n\tfor (const organization of organizations) {\n\t\tconst { parentId } = organization\n\t\t// A sub-organisation whose parent is not in the list is shown at the top level\n\t\tconst parent = parentId != null && parentId !== organization.id && ids.has(parentId) ? parentId : null\n\t\tchildren.set(parent, [...(children.get(parent) ?? []), organization])\n\t}\n\n\tconst nodes: OrganizationNode[] = []\n\tfunction visit(organization: Organization, ancestors: Organization[]) {\n\t\tconst subOrganizations = children.get(organization.id) ?? []\n\t\tconst hasChildren = subOrganizations.length > 0\n\t\tnodes.push({\n\t\t\t...organization,\n\t\t\tdepth: ancestors.length,\n\t\t\tancestors,\n\t\t\tpath: getPath(ancestors),\n\t\t\tfavorite: favorites.has(organization.id),\n\t\t\thasChildren,\n\t\t\texpanded: hasChildren && !collapsed.has(organization.id),\n\t\t})\n\t\tfor (const child of subOrganizations) visit(child, [...ancestors, organization])\n\t}\n\tfor (const organization of children.get(null) ?? []) visit(organization, [])\n\treturn nodes\n}\n\ntype OrganizationSwitcherContextValue = {\n\t/**\n\t * What the parts are built on: a Base UI Select, a Combobox with a search field (`searchable`), or that Combobox\n\t * as a grid, where a row has more cells than the organisation: the favourite button (`favorites`) and the button\n\t * that collapses its sub-organisations (`collapsible`).\n\t */\n\tmode: 'select' | 'list' | 'grid'\n\tcollapsible: boolean\n\t/** How many buttons a row can have after the organisation; the popup makes room for them */\n\tactions: number\n\t/** True while there is text in the search field; the list is then flat instead of a tree */\n\tsearching: boolean\n\t/** The organisations to list: the tree without collapsed branches, or the matches while searching */\n\tmatches: OrganizationNode[]\n\t/** The favourites as they were when the list opened, so rows don't move while the user marks favourites */\n\tfavorites: OrganizationNode[]\n\tselected: OrganizationNode | undefined\n\ttoggleFavorite: (id: string) => void\n\ttoggleCollapsed: (id: string) => void\n}\n\nconst OrganizationSwitcherContext = React.createContext<OrganizationSwitcherContextValue | null>(null)\n\n// Rows outside the tree, such as favourites, are not indented and show their path instead\nconst OrganizationSwitcherFlatContext = React.createContext(false)\n\nfunction useOrganizationSwitcher() {\n\tconst context = React.useContext(OrganizationSwitcherContext)\n\tif (!context) {\n\t\tthrow new Error('OrganizationSwitcher parts must be used within an OrganizationSwitcher.')\n\t}\n\treturn context\n}\n\nconst NO_IDS: readonly string[] = []\n\n/**\n * Holds the organisations, the selected one and the favourites. Without `searchable` the list is a Base UI Select;\n * with it, a Base UI Combobox whose search field sits inside the popup.\n */\nfunction OrganizationSwitcher({\n\titems,\n\tvalue,\n\tdefaultValue = null,\n\tonValueChange,\n\tsearchable = false,\n\tfavorites,\n\tonFavoritesChange,\n\tcollapsible = false,\n\tcollapsed,\n\tdefaultCollapsed = NO_IDS,\n\tonCollapsedChange,\n\tdisabled = false,\n\topen,\n\tdefaultOpen,\n\tonOpenChange,\n\tchildren,\n}: Readonly<{\n\t/** A flat list. Give a sub-organisation the id of its organisation as `parentId`. */\n\titems: readonly Organization[]\n\t/** The id of the selected organisation or sub-organisation */\n\tvalue?: string | null\n\tdefaultValue?: string | null\n\tonValueChange?: (value: string, organization: Organization) => void\n\t/** Lets the user filter the list with `OrganizationSwitcherSearch`; use it for long lists */\n\tsearchable?: boolean\n\t/** The ids of the user's favourite organisations. Needs `searchable`. Save them in `onFavoritesChange`. */\n\tfavorites?: readonly string[]\n\tonFavoritesChange?: (favorites: string[], change: { id: string; favorite: boolean }) => void\n\t/** Lets the user collapse an organisation's sub-organisations with `OrganizationSwitcherExpander`. Needs `searchable`. */\n\tcollapsible?: boolean\n\t/** The ids of the collapsed organisations; everything else is expanded */\n\tcollapsed?: readonly string[]\n\tdefaultCollapsed?: readonly string[]\n\tonCollapsedChange?: (collapsed: string[], change: { id: string; collapsed: boolean }) => void\n\tdisabled?: boolean\n\topen?: boolean\n\tdefaultOpen?: boolean\n\tonOpenChange?: (open: boolean) => void\n\tchildren?: React.ReactNode\n}>) {\n\tconst [internalValue, setInternalValue] = React.useState(defaultValue)\n\tconst controlled = value !== undefined\n\tconst selectedValue = controlled ? value : internalValue\n\tconst [query, setQuery] = React.useState('')\n\tconst searching = searchable && query.trim() !== ''\n\tconst { contains } = ComboboxPrimitive.useFilter({ sensitivity: 'base' })\n\tconst searchableMode = favorites || collapsible ? 'grid' : 'list'\n\tconst mode = searchable ? searchableMode : 'select'\n\n\tconst favoriteIds = favorites ?? NO_IDS\n\tconst [favoritesAtOpen, setFavoritesAtOpen] = React.useState(favoriteIds)\n\tconst [internalCollapsed, setInternalCollapsed] = React.useState(defaultCollapsed)\n\tconst collapsedIds = collapsed ?? internalCollapsed\n\tconst nodes = React.useMemo(\n\t\t() => getOrganizationNodes(items, new Set(favoriteIds), new Set(collapsedIds)),\n\t\t[items, favoriteIds, collapsedIds],\n\t)\n\tconst selected = selectedValue == null ? undefined : nodes.find((node) => node.id === selectedValue)\n\tconst getName = (id: string) => nodes.find((node) => node.id === id)?.name ?? ''\n\n\tconst context = React.useMemo<OrganizationSwitcherContextValue>(() => {\n\t\tconst search = query.trim()\n\t\tconst canCollapse = mode === 'grid' && collapsible\n\t\t// A collapsed organisation hides everything below it; the search still looks through all of them\n\t\tconst tree = canCollapse\n\t\t\t? nodes.filter((node) => node.ancestors.every((ancestor) => !collapsedIds.includes(ancestor.id)))\n\t\t\t: nodes\n\t\treturn {\n\t\t\tmode,\n\t\t\tcollapsible: canCollapse,\n\t\t\tactions: (favorites ? 1 : 0) + (collapsible ? 1 : 0),\n\t\t\tsearching,\n\t\t\tmatches: searching ? nodes.filter((node) => contains(node.name, search)) : tree,\n\t\t\tfavorites: mode === 'grid' && !searching ? nodes.filter((node) => favoritesAtOpen.includes(node.id)) : [],\n\t\t\tselected,\n\t\t\ttoggleFavorite(id) {\n\t\t\t\tconst favorite = !favoriteIds.includes(id)\n\t\t\t\tconst next = favorite ? [...favoriteIds, id] : favoriteIds.filter((favoriteId) => favoriteId !== id)\n\t\t\t\tonFavoritesChange?.(next, { id, favorite })\n\t\t\t},\n\t\t\ttoggleCollapsed(id) {\n\t\t\t\tconst nowCollapsed = !collapsedIds.includes(id)\n\t\t\t\tconst next = nowCollapsed ? [...collapsedIds, id] : collapsedIds.filter((collapsedId) => collapsedId !== id)\n\t\t\t\tif (collapsed === undefined) setInternalCollapsed(next)\n\t\t\t\tonCollapsedChange?.(next, { id, collapsed: nowCollapsed })\n\t\t\t},\n\t\t}\n\t}, [\n\t\tmode,\n\t\tcollapsible,\n\t\tfavorites,\n\t\tsearching,\n\t\tquery,\n\t\tnodes,\n\t\tfavoritesAtOpen,\n\t\tselected,\n\t\tfavoriteIds,\n\t\tonFavoritesChange,\n\t\tcollapsed,\n\t\tcollapsedIds,\n\t\tonCollapsedChange,\n\t\tcontains,\n\t])\n\n\tfunction handleValueChange(next: string | null) {\n\t\tconst organization = next == null ? undefined : items.find((item) => item.id === next)\n\t\tif (!organization) return\n\t\tif (!controlled) setInternalValue(organization.id)\n\t\tonValueChange?.(organization.id, organization)\n\t}\n\n\tconst rootProps = {\n\t\tvalue: selectedValue,\n\t\tonValueChange: handleValueChange,\n\t\tdisabled,\n\t\topen,\n\t\tdefaultOpen,\n\t\tonOpenChange: (nextOpen: boolean) => {\n\t\t\tif (nextOpen) setFavoritesAtOpen(favoriteIds)\n\t\t\tonOpenChange?.(nextOpen)\n\t\t},\n\t}\n\n\treturn (\n\t\t<OrganizationSwitcherContext.Provider value={context}>\n\t\t\t{searchable ? (\n\t\t\t\t// The parts filter and render the rows themselves, so the combobox gets no `items`\n\t\t\t\t<ComboboxPrimitive.Root<string>\n\t\t\t\t\t{...rootProps}\n\t\t\t\t\tonInputValueChange={setQuery}\n\t\t\t\t\titemToStringLabel={getName}\n\t\t\t\t\tfilter={null}\n\t\t\t\t\tgrid={mode === 'grid'}\n\t\t\t\t\tautoHighlight\n\t\t\t\t>\n\t\t\t\t\t{children}\n\t\t\t\t</ComboboxPrimitive.Root>\n\t\t\t) : (\n\t\t\t\t<SelectPrimitive.Root<string> {...rootProps}>{children}</SelectPrimitive.Root>\n\t\t\t)}\n\t\t</OrganizationSwitcherContext.Provider>\n\t)\n}\n\n/**\n * The button that shows the current organisation and opens the list. Give it an `aria-label` such as \"Organisation\".\n * In a sidebar, pass `render={<SidebarMenuButton size=\"lg\" />}`.\n */\nfunction OrganizationSwitcherTrigger({\n\trender,\n\tchildren,\n\t...props\n}: Omit<React.ComponentProps<'button'>, 'value' | 'defaultValue'> & {\n\t/** Replaces the button; the default styles are left out so the element keeps its own look */\n\trender?: React.ReactElement\n}) {\n\tconst { mode } = useOrganizationSwitcher()\n\tconst triggerProps = {\n\t\t'data-slot': 'organization-switcher-trigger',\n\t\trender: render ?? (\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"flex h-12 w-full min-w-0 items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-3 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-accent data-popup-open:text-accent-foreground\"\n\t\t\t/>\n\t\t),\n\t\t...props,\n\t\tchildren: (\n\t\t\t<>\n\t\t\t\t{children}\n\t\t\t\t<ChevronsUpDownIcon aria-hidden=\"true\" className=\"ml-auto size-4 shrink-0\" />\n\t\t\t</>\n\t\t),\n\t}\n\n\treturn mode === 'select' ? (\n\t\t<SelectPrimitive.Trigger {...triggerProps} />\n\t) : (\n\t\t<ComboboxPrimitive.Trigger {...triggerProps} />\n\t)\n}\n\n/**\n * The selected organisation inside the trigger: a logo tile, the selected organisation's name and, under it, the\n * organisations above it or, for a top-level organisation, its description.\n */\nfunction OrganizationSwitcherValue({\n\tplaceholder,\n\tclassName,\n\t...props\n}: React.ComponentProps<'span'> & {\n\t/** Shown while nothing is selected, e.g. \"Select organisation\" */\n\tplaceholder?: React.ReactNode\n}) {\n\tconst { selected } = useOrganizationSwitcher()\n\tconst top = selected?.ancestors[0] ?? selected\n\t// The organisation the user works in is the main line; a sub-organisation names the ones above it underneath\n\tconst description = selected && selected.depth > 0 ? getPath(selected.ancestors) : selected?.description\n\t// A sub-organisation without a logo of its own shows its organisation's logo, but its own initial\n\tconst logo = selected?.logo ?? top?.logo ?? Array.from(selected?.name.trim() ?? '')[0]?.toUpperCase()\n\n\treturn (\n\t\t<>\n\t\t\t<span\n\t\t\t\tdata-slot=\"organization-switcher-logo\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t\tclassName=\"flex aspect-square size-8 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-primary text-sm font-semibold text-primary-foreground [&_img]:size-full [&_img]:object-cover [&_svg]:size-4\"\n\t\t\t>\n\t\t\t\t{logo ?? <Building2Icon />}\n\t\t\t</span>\n\t\t\t<span\n\t\t\t\tdata-slot=\"organization-switcher-value\"\n\t\t\t\tdata-placeholder={selected ? undefined : ''}\n\t\t\t\tclassName={cn('grid min-w-0 flex-1 text-left leading-tight', className)}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t<span className=\"truncate font-medium\">{selected?.name ?? placeholder}</span>\n\t\t\t\t{description ? <span className=\"truncate text-xs text-muted-foreground\">{description}</span> : null}\n\t\t\t</span>\n\t\t</>\n\t)\n}\n\nfunction OrganizationSwitcherContent({\n\tclassName,\n\tside = 'bottom',\n\talign = 'start',\n\tsideOffset = 4,\n\t...props\n}: Omit<React.ComponentProps<'div'>, 'ref'> &\n\tPick<ComboboxPrimitive.Positioner.Props, 'side' | 'align' | 'sideOffset'>) {\n\tconst { mode, actions } = useOrganizationSwitcher()\n\tconst searchable = mode !== 'select'\n\tconst positionerProps = { side, align, sideOffset, className: 'isolate z-50' }\n\tconst popupProps = {\n\t\t'data-slot': 'organization-switcher-content',\n\t\tclassName: cn(\n\t\t\t'max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-60 origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-border outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',\n\t\t\t!searchable && 'overflow-y-auto',\n\t\t\t// Room for the buttons next to the names\n\t\t\tmode === 'grid' && (actions > 1 ? 'min-w-80' : 'min-w-72'),\n\t\t\tclassName,\n\t\t),\n\t\t...props,\n\t}\n\n\treturn searchable ? (\n\t\t<ComboboxPrimitive.Portal>\n\t\t\t<ComboboxPrimitive.Positioner {...positionerProps}>\n\t\t\t\t<ComboboxPrimitive.Popup {...popupProps} />\n\t\t\t</ComboboxPrimitive.Positioner>\n\t\t</ComboboxPrimitive.Portal>\n\t) : (\n\t\t<SelectPrimitive.Portal>\n\t\t\t<SelectPrimitive.Positioner {...positionerProps} alignItemWithTrigger={false}>\n\t\t\t\t<SelectPrimitive.Popup {...popupProps} />\n\t\t\t</SelectPrimitive.Positioner>\n\t\t</SelectPrimitive.Portal>\n\t)\n}\n\n/** A heading above the list, e.g. \"Switch organisation\" */\nfunction OrganizationSwitcherLabel({ className, ...props }: React.ComponentProps<'div'>) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"organization-switcher-label\"\n\t\t\tclassName={cn('px-3 pt-2.5 pb-1 text-xs text-muted-foreground', className)}\n\t\t\t{...props}\n\t\t/>\n\t)\n}\n\n/** The search field. It needs `searchable` on `OrganizationSwitcher` and renders nothing without it. */\nfunction OrganizationSwitcherSearch({ className, placeholder, ...props }: ComboboxPrimitive.Input.Props) {\n\tconst { mode } = useOrganizationSwitcher()\n\tif (mode === 'select') return null\n\n\treturn (\n\t\t<div data-slot=\"organization-switcher-search\" className=\"flex h-10 items-center gap-2 border-b px-3\">\n\t\t\t<SearchIcon aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n\t\t\t<ComboboxPrimitive.Input\n\t\t\t\taria-label={typeof placeholder === 'string' ? placeholder : undefined}\n\t\t\t\tplaceholder={placeholder}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t'h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground',\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t/>\n\t\t</div>\n\t)\n}\n\n/** The message when the search has no matches. It renders nothing without `searchable`. */\nfunction OrganizationSwitcherEmpty({ className, children, ...props }: React.ComponentProps<'output'>) {\n\tconst { mode, matches } = useOrganizationSwitcher()\n\tif (mode === 'select') return null\n\n\treturn (\n\t\t// Stays in the page as a live region, so it only takes up space when it has text\n\t\t<output\n\t\t\tdata-slot=\"organization-switcher-empty\"\n\t\t\taria-live=\"polite\"\n\t\t\taria-atomic=\"true\"\n\t\t\tclassName={cn('block px-3 py-6 text-center text-sm text-muted-foreground empty:p-0', className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{matches.length === 0 ? children : null}\n\t\t</output>\n\t)\n}\n\ntype OrganizationSwitcherRenderItem = (organization: OrganizationNode, index: number) => React.ReactNode\n\n/** One row per organisation. In a grid the row holds the organisation and its buttons side by side. */\nfunction OrganizationSwitcherRows({\n\torganizations,\n\tchildren,\n}: {\n\torganizations: OrganizationNode[]\n\tchildren: OrganizationSwitcherRenderItem\n}) {\n\tconst { mode } = useOrganizationSwitcher()\n\n\treturn organizations.map((organization, index) =>\n\t\tmode === 'grid' ? (\n\t\t\t<ComboboxPrimitive.Row\n\t\t\t\tkey={organization.id}\n\t\t\t\tdata-slot=\"organization-switcher-row\"\n\t\t\t\tclassName=\"group/organization-switcher-row flex items-center gap-0.5\"\n\t\t\t>\n\t\t\t\t{children(organization, index)}\n\t\t\t</ComboboxPrimitive.Row>\n\t\t) : (\n\t\t\t<React.Fragment key={organization.id}>{children(organization, index)}</React.Fragment>\n\t\t),\n\t)\n}\n\n/**\n * The scrolling list. Give it a function to list every organisation, or `OrganizationSwitcherFavorites` and\n * `OrganizationSwitcherGroup` to list them in groups.\n */\nfunction OrganizationSwitcherList({\n\tclassName,\n\tchildren,\n\t...props\n}: Omit<React.ComponentProps<'div'>, 'children' | 'ref'> & {\n\tchildren: React.ReactNode | OrganizationSwitcherRenderItem\n}) {\n\tconst { mode, matches } = useOrganizationSwitcher()\n\tconst content =\n\t\ttypeof children === 'function' ? (\n\t\t\t<OrganizationSwitcherRows organizations={matches}>{children}</OrganizationSwitcherRows>\n\t\t) : (\n\t\t\tchildren\n\t\t)\n\n\tif (mode === 'select') {\n\t\treturn (\n\t\t\t<SelectPrimitive.List data-slot=\"organization-switcher-list\" className={cn('p-1', className)} {...props}>\n\t\t\t\t{content}\n\t\t\t</SelectPrimitive.List>\n\t\t)\n\t}\n\n\treturn (\n\t\t<ComboboxPrimitive.List\n\t\t\tdata-slot=\"organization-switcher-list\"\n\t\t\tclassName={cn(\n\t\t\t\t'max-h-[min(--spacing(72),calc(var(--available-height)---spacing(10)))] scroll-py-1 overflow-y-auto overscroll-contain p-1',\n\t\t\t\tmatches.length === 0 && 'p-0',\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{content}\n\t\t</ComboboxPrimitive.List>\n\t)\n}\n\nfunction OrganizationSwitcherGroupFrame({\n\tlabel,\n\tclassName,\n\tchildren,\n\t...props\n}: Omit<React.ComponentProps<'div'>, 'ref'> & { label?: React.ReactNode }) {\n\tconst { mode } = useOrganizationSwitcher()\n\tconst Group = mode === 'select' ? SelectPrimitive.Group : ComboboxPrimitive.Group\n\tconst GroupLabel = mode === 'select' ? SelectPrimitive.GroupLabel : ComboboxPrimitive.GroupLabel\n\n\treturn (\n\t\t<Group data-slot=\"organization-switcher-group\" className={cn('not-first:mt-1', className)} {...props}>\n\t\t\t{label ? (\n\t\t\t\t<GroupLabel\n\t\t\t\t\tdata-slot=\"organization-switcher-group-label\"\n\t\t\t\t\tclassName=\"flex items-center gap-1.5 px-2 py-1.5 text-xs text-muted-foreground [&_svg]:size-3.5\"\n\t\t\t\t>\n\t\t\t\t\t{label}\n\t\t\t\t</GroupLabel>\n\t\t\t) : null}\n\t\t\t{children}\n\t\t</Group>\n\t)\n}\n\n/** Every organisation, in tree order, under an optional `label`. While searching it lists the matches without it. */\nfunction OrganizationSwitcherGroup({\n\tlabel,\n\tchildren,\n\t...props\n}: Omit<React.ComponentProps<'div'>, 'children' | 'ref'> & {\n\t/** A heading for the group, e.g. \"All organisations\" */\n\tlabel?: React.ReactNode\n\tchildren: OrganizationSwitcherRenderItem\n}) {\n\tconst { searching, matches } = useOrganizationSwitcher()\n\tif (matches.length === 0) return null\n\n\treturn (\n\t\t<OrganizationSwitcherGroupFrame label={searching ? undefined : label} {...props}>\n\t\t\t<OrganizationSwitcherRows organizations={matches}>{children}</OrganizationSwitcherRows>\n\t\t</OrganizationSwitcherGroupFrame>\n\t)\n}\n\n/**\n * The user's favourite organisations, as a group above the others. It needs `favorites` and `searchable` on\n * `OrganizationSwitcher`, and renders nothing while searching or when there are no favourites.\n */\nfunction OrganizationSwitcherFavorites({\n\tlabel,\n\tchildren,\n\t...props\n}: Omit<React.ComponentProps<'div'>, 'children' | 'ref'> & {\n\t/** A heading for the group, e.g. \"Favourites\" */\n\tlabel?: React.ReactNode\n\tchildren: OrganizationSwitcherRenderItem\n}) {\n\tconst { favorites } = useOrganizationSwitcher()\n\tif (favorites.length === 0) return null\n\n\treturn (\n\t\t<OrganizationSwitcherFlatContext.Provider value={true}>\n\t\t\t<OrganizationSwitcherGroupFrame label={label} {...props}>\n\t\t\t\t<OrganizationSwitcherRows organizations={favorites}>{children}</OrganizationSwitcherRows>\n\t\t\t</OrganizationSwitcherGroupFrame>\n\t\t</OrganizationSwitcherFlatContext.Provider>\n\t)\n}\n\n/**\n * One organisation in the list; the children are its name. Sub-organisations are indented by `level` in the tree.\n * Search results and favourites are not indented, so `path` is shown under the name there instead of `description`.\n */\nfunction OrganizationSwitcherItem({\n\tvalue,\n\tlevel = 0,\n\tpath,\n\tdescription,\n\tdisabled,\n\tclassName,\n\tstyle,\n\tchildren,\n\t...props\n}: Omit<React.ComponentProps<'div'>, 'ref'> & {\n\t/** The id of the organisation */\n\tvalue: string\n\t/** How deep the organisation sits in the tree; `organization.depth` */\n\tlevel?: number\n\t/** The organisations above it; `organization.path` */\n\tpath?: string\n\t/** A second line under the name */\n\tdescription?: React.ReactNode\n\tdisabled?: boolean\n}) {\n\tconst { mode, searching } = useOrganizationSwitcher()\n\tconst flat = React.useContext(OrganizationSwitcherFlatContext) || searching\n\tconst details = flat ? path : description\n\tconst itemProps = {\n\t\tvalue,\n\t\tdisabled,\n\t\t'data-slot': 'organization-switcher-item',\n\t\tclassName: cn(\n\t\t\t'relative flex min-h-8 w-full min-w-0 flex-1 cursor-default items-center rounded-md py-1.5 pr-8 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground data-highlighted:**:text-accent-foreground',\n\t\t\tclassName,\n\t\t),\n\t\tstyle: { paddingInlineStart: `${0.5 + (flat ? 0 : level)}rem`, ...style },\n\t\t...props,\n\t}\n\tconst indicator = <span className=\"pointer-events-none absolute right-2 flex size-4 items-center justify-center\" />\n\tconst check = <CheckIcon aria-hidden=\"true\" className=\"size-4\" />\n\tconst text = (\n\t\t<>\n\t\t\t{details ? <span className=\"truncate text-xs text-muted-foreground\">{details}</span> : null}\n\t\t\t{/* The indentation is only visual, so the organisations above are given as text as well */}\n\t\t\t{!flat && path ? <span className=\"sr-only\">, {path}</span> : null}\n\t\t</>\n\t)\n\n\treturn mode === 'select' ? (\n\t\t<SelectPrimitive.Item {...itemProps}>\n\t\t\t<span className=\"grid min-w-0 flex-1 leading-tight\">\n\t\t\t\t<SelectPrimitive.ItemText className=\"truncate\">{children}</SelectPrimitive.ItemText>\n\t\t\t\t{text}\n\t\t\t</span>\n\t\t\t<SelectPrimitive.ItemIndicator render={indicator}>{check}</SelectPrimitive.ItemIndicator>\n\t\t</SelectPrimitive.Item>\n\t) : (\n\t\t<ComboboxPrimitive.Item {...itemProps}>\n\t\t\t<span className=\"grid min-w-0 flex-1 leading-tight\">\n\t\t\t\t<span className=\"truncate\">{children}</span>\n\t\t\t\t{text}\n\t\t\t</span>\n\t\t\t<ComboboxPrimitive.ItemIndicator render={indicator}>{check}</ComboboxPrimitive.ItemIndicator>\n\t\t</ComboboxPrimitive.Item>\n\t)\n}\n\n/**\n * The star next to an organisation that adds it to or removes it from the favourites. Put it after\n * `OrganizationSwitcherItem` and give it an `aria-label` that says what it does, e.g. \"Add Education to favourites\".\n * It is a cell of its own in the row: reach it with the right arrow key and press Enter. It needs `favorites` and\n * `searchable` on `OrganizationSwitcher` and renders nothing without them.\n */\nfunction OrganizationSwitcherFavorite({\n\tvalue,\n\tfavorite,\n\tclassName,\n\t...props\n}: Omit<React.ComponentProps<'div'>, 'ref' | 'children'> & {\n\t/** The id of the organisation */\n\tvalue: string\n\t/** Whether it is a favourite now; `organization.favorite` */\n\tfavorite: boolean\n}) {\n\tconst { mode, toggleFavorite } = useOrganizationSwitcher()\n\tif (mode !== 'grid') return null\n\n\treturn (\n\t\t<ComboboxPrimitive.Item\n\t\t\t// A value of its own, so the cell is never the selected organisation\n\t\t\tvalue={`favorite:${value}`}\n\t\t\tdata-slot=\"organization-switcher-favorite\"\n\t\t\tdata-favorite={favorite ? '' : undefined}\n\t\t\tclassName={cn(\n\t\t\t\t'flex size-8 shrink-0 cursor-default items-center justify-center rounded-md text-muted-foreground opacity-0 outline-hidden select-none group-hover/organization-switcher-row:opacity-100 group-has-data-highlighted/organization-switcher-row:opacity-100 data-favorite:text-warning data-favorite:opacity-100 data-highlighted:bg-accent data-highlighted:text-accent-foreground data-favorite:data-highlighted:text-warning',\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\t{...props}\n\t\t\t// Toggle the favourite instead of selecting the cell, and keep the list open\n\t\t\tonClick={(event) => {\n\t\t\t\tevent.preventBaseUIHandler()\n\t\t\t\ttoggleFavorite(value)\n\t\t\t}}\n\t\t\tonMouseUp={(event) => event.preventBaseUIHandler()}\n\t\t>\n\t\t\t<StarIcon aria-hidden=\"true\" className={cn('size-4', favorite && 'fill-current')} />\n\t\t</ComboboxPrimitive.Item>\n\t)\n}\n\n/**\n * The chevron after an organisation that hides or shows its sub-organisations. Put it last in the row, after\n * `OrganizationSwitcherItem` and `OrganizationSwitcherFavorite`, and give it an `aria-label` that says what it does,\n * e.g. \"Collapse Ekdalen Municipality\". It is a cell of its own in the row: reach it with the right arrow key and\n * press Enter. An organisation without sub-organisations gets an empty space of the same width, so the rows line up.\n * It needs `collapsible` and `searchable` on `OrganizationSwitcher` and renders nothing without them.\n */\nfunction OrganizationSwitcherExpander({\n\tvalue,\n\texpanded,\n\thasChildren,\n\tclassName,\n\t...props\n}: Omit<React.ComponentProps<'div'>, 'ref' | 'children'> & {\n\t/** The id of the organisation */\n\tvalue: string\n\t/** Whether its sub-organisations are listed now; `organization.expanded` */\n\texpanded: boolean\n\t/** Whether it has sub-organisations to hide; `organization.hasChildren` */\n\thasChildren: boolean\n}) {\n\tconst { collapsible, searching, toggleCollapsed } = useOrganizationSwitcher()\n\tconst flat = React.useContext(OrganizationSwitcherFlatContext)\n\t// Search results are a flat list, so there is nothing to collapse\n\tif (!collapsible || searching) return null\n\n\tif (flat || !hasChildren) {\n\t\treturn <span data-slot=\"organization-switcher-expander-spacer\" aria-hidden=\"true\" className=\"size-8 shrink-0\" />\n\t}\n\n\tconst Icon = expanded ? ChevronDownIcon : ChevronRightIcon\n\n\treturn (\n\t\t<ComboboxPrimitive.Item\n\t\t\t// A value of its own, so the cell is never the selected organisation\n\t\t\tvalue={`expander:${value}`}\n\t\t\taria-expanded={expanded}\n\t\t\tdata-slot=\"organization-switcher-expander\"\n\t\t\tclassName={cn(\n\t\t\t\t'flex size-8 shrink-0 cursor-default items-center justify-center rounded-md text-muted-foreground outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground',\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\t{...props}\n\t\t\t// Collapse or expand instead of selecting the cell, and keep the list open\n\t\t\tonClick={(event) => {\n\t\t\t\tevent.preventBaseUIHandler()\n\t\t\t\ttoggleCollapsed(value)\n\t\t\t}}\n\t\t\tonMouseUp={(event) => event.preventBaseUIHandler()}\n\t\t>\n\t\t\t<Icon aria-hidden=\"true\" className=\"size-4\" />\n\t\t</ComboboxPrimitive.Item>\n\t)\n}\n\nexport {\n\tOrganizationSwitcher,\n\tOrganizationSwitcherTrigger,\n\tOrganizationSwitcherValue,\n\tOrganizationSwitcherContent,\n\tOrganizationSwitcherLabel,\n\tOrganizationSwitcherSearch,\n\tOrganizationSwitcherEmpty,\n\tOrganizationSwitcherList,\n\tOrganizationSwitcherGroup,\n\tOrganizationSwitcherFavorites,\n\tOrganizationSwitcherItem,\n\tOrganizationSwitcherFavorite,\n\tOrganizationSwitcherExpander,\n\ttype Organization,\n\ttype OrganizationNode,\n}\n",
      "type": "registry:ui",
      "target": "components/ui/organization-switcher.tsx"
    }
  ],
  "meta": {
    "new": true
  },
  "type": "registry:ui"
}