{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "facet-filter",
  "title": "Facet Filter",
  "description": "Facet Filter from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/checkbox.json",
    "https://zero-design.itdone.de/r/heading-tag.json",
    "https://zero-design.itdone.de/r/input.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/toggle-group.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/facet-filter.tsx",
      "content": "'use client';\n\nimport { ChevronDown, Minus, Plus } from 'lucide-react';\nimport { Children, createContext, forwardRef, useContext, useId, useState, type HTMLAttributes, type ReactNode, type Ref } from 'react';\n\nimport { focusRing } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport type { HeadingTag } from '@/components/ui/heading-tag';\nimport { Checkbox } from '@/components/ui/checkbox';\nimport { Input } from '@/components/ui/input';\nimport { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\n// `checkbox` (default) renders checkbox rows. `toggle-links` renders link style add/remove rows (toggle buttons with aria-pressed).\nexport type FacetSelectionMode = 'checkbox' | 'toggle-links';\n\n// Leading colour dot of a group heading or an option. `bg-*` token classes are applied as classes, anything else (e.g. a project colour from data) as a background colour.\nconst FacetDot = ({ color }: { color: string }) => (\n  <span\n    aria-hidden=\"true\"\n    data-facet-dot=\"\"\n    className={cn('h-2 w-2 shrink-0 rounded-full', color.startsWith('bg-') ? color : 'bg-current')}\n    style={color.startsWith('bg-') ? undefined : { backgroundColor: color }}\n  />\n);\n\nconst FacetSelectionModeContext = createContext<FacetSelectionMode>('checkbox');\n\nexport interface FacetOperator {\n  value: string;\n  label: string;\n}\n\nexport interface FacetFilterProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {\n  title?: ReactNode;\n  // Heading element of the title, so the filter fits the document outline. Default h2.\n  titleAs?: HeadingTag;\n  description?: ReactNode;\n  // Match operator switch, e.g. Partial and Exact. Omit for no switch.\n  operators?: FacetOperator[];\n  operator?: string;\n  defaultOperator?: string;\n  onOperatorChange?: (value: string) => void;\n  operatorLabel?: string;\n  // Slot below the switch, e.g. a saved filter select.\n  saved?: ReactNode;\n  // Shows a Reset link at the right of the header and calls this when pressed.\n  onReset?: () => void;\n  resetLabel?: string;\n  // Default row style of all groups. A group can override it.\n  selectionMode?: FacetSelectionMode;\n  // FacetGroup elements.\n  children?: ReactNode;\n}\n\n// Filter column with a header, match operator and facet groups. No overlay chrome: it sits in a split view.\nconst FacetFilter = forwardRef<HTMLElement, FacetFilterProps>(\n  (\n    { className, title: titleProp, titleAs: Title = 'h2', description, operators, operator, defaultOperator, onOperatorChange, operatorLabel: operatorLabelProp, saved, onReset, resetLabel: resetLabelProp, selectionMode = 'checkbox', children, ...props },\n    ref,\n  ) => {\n    const l = useLabels();\n    const title = titleProp ?? l.filters;\n    const operatorLabel = operatorLabelProp ?? l.matchMode;\n    const resetLabel = resetLabelProp ?? l.reset;\n    const zero = usePreset() === 'zero';\n    const [inner, setInner] = useState(defaultOperator ?? operators?.[0]?.value ?? '');\n    const current = operator ?? inner;\n    return (\n      <aside\n        ref={ref}\n        aria-label={typeof title === 'string' ? title : l.filters}\n        className={cn('typography-ui flex w-filter-column max-w-full flex-col gap-4 border-0 border-r border-solid border-hairline p-4 text-foreground', className)}\n        {...props}\n      >\n        <header className=\"flex flex-col gap-1\">\n          <div className=\"flex items-center justify-between gap-2\">\n            <Title className={zero ? 'typography-ui-semibold' : 'typography-ui-lg-semibold'}>{title}</Title>\n            {onReset ? (\n              <button\n                type=\"button\"\n                onClick={onReset}\n                className={cn('typography-link-sm shrink-0 rounded-item text-text-subtle hover:text-text-default hover:underline', focusRing)}\n              >\n                {resetLabel}\n              </button>\n            ) : null}\n          </div>\n          {description ? <p className=\"typography-text-extra-small text-muted-foreground\">{description}</p> : null}\n        </header>\n        {operators && operators.length > 0 ? (\n          <ToggleGroup\n            type=\"single\"\n            variant=\"segmented\"\n            size=\"sm\"\n            aria-label={operatorLabel}\n            value={current}\n            onValueChange={(value) => {\n              if (!value) return;\n              if (operator === undefined) setInner(value);\n              onOperatorChange?.(value);\n            }}\n            className=\"grid w-full auto-cols-fr grid-flow-col gap-0 rounded-control bg-muted p-1\"\n          >\n            {operators.map((o) => (\n              <ToggleGroupItem key={o.value} value={o.value} className=\"w-full\">\n                {o.label}\n              </ToggleGroupItem>\n            ))}\n          </ToggleGroup>\n        ) : null}\n        {saved}\n        <FacetSelectionModeContext.Provider value={selectionMode}>{children}</FacetSelectionModeContext.Provider>\n      </aside>\n    );\n  },\n);\nFacetFilter.displayName = 'FacetFilter';\n\nexport interface FacetGroupProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {\n  label: string;\n  // Heading element of the group label, so the filter fits the document outline. Default h3.\n  titleAs?: HeadingTag;\n  // Leading colour dot before the group label, e.g. a project colour. A `bg-*` token class or a CSS colour.\n  dotColor?: string;\n  // Result count after the group label. A number, or text such as \"99+\".\n  count?: number | string;\n  // Adds a search field that filters the options client side via onQueryChange (controlled).\n  searchable?: boolean;\n  query?: string;\n  onQueryChange?: (query: string) => void;\n  searchPlaceholder?: string;\n  // Options shown before \"Show n more\". Omit to show all.\n  limit?: number;\n  moreLabel?: (hidden: number) => string;\n  lessLabel?: string;\n  // Makes the heading a button that collapses and expands the group.\n  collapsible?: boolean;\n  // Initial state when uncontrolled.\n  defaultCollapsed?: boolean;\n  // Controlled collapsed state. Pair with `onCollapsedChange`.\n  collapsed?: boolean;\n  onCollapsedChange?: (collapsed: boolean) => void;\n  // Row style of this group, overriding the filter default. `toggle-links` renders add/remove link rows instead of checkboxes.\n  selectionMode?: FacetSelectionMode;\n  // FacetOption elements.\n  children?: ReactNode;\n}\n\nconst FacetGroup = forwardRef<HTMLElement, FacetGroupProps>(\n  (\n    { className, label, titleAs: Title = 'h3', dotColor, count, searchable = false, query, onQueryChange, searchPlaceholder: searchPlaceholderProp, limit, moreLabel: moreLabelProp, lessLabel: lessLabelProp, collapsible = false, defaultCollapsed = false, collapsed: collapsedProp, onCollapsedChange, selectionMode: selectionModeProp, children, ...props },\n    ref,\n  ) => {\n    const l = useLabels();\n    const inheritedMode = useContext(FacetSelectionModeContext);\n    const selectionMode = selectionModeProp ?? inheritedMode;\n    const zero = usePreset() === 'zero';\n    const searchPlaceholder = searchPlaceholderProp ?? l.search;\n    const moreLabel = moreLabelProp ?? l.showMore;\n    const lessLabel = lessLabelProp ?? l.showLess;\n    const [expanded, setExpanded] = useState(false);\n    const listId = useId();\n    const bodyId = useId();\n    const [innerCollapsed, setInnerCollapsed] = useState(defaultCollapsed);\n    const collapsed = collapsible && (collapsedProp ?? innerCollapsed);\n    const toggleCollapsed = () => {\n      if (collapsedProp === undefined) setInnerCollapsed(!collapsed);\n      onCollapsedChange?.(!collapsed);\n    };\n    const options = Children.toArray(children);\n    const hidden = limit != null && !expanded ? Math.max(0, options.length - limit) : 0;\n    const visible = limit != null && !expanded ? options.slice(0, limit) : options;\n    // Without dot and count the heading stays a plain text node.\n    const headingContent =\n      dotColor || count != null ? (\n        <span className=\"flex min-w-0 items-center gap-2\">\n          {dotColor ? <FacetDot color={dotColor} /> : null}\n          <span className=\"min-w-0 truncate\">{label}</span>\n          {count != null ? <span className=\"typography-text-extra-small tabular-nums text-muted-foreground\">{count}</span> : null}\n        </span>\n      ) : (\n        label\n      );\n    return (\n      <section ref={ref} aria-label={label} className={cn('flex flex-col gap-2', className)} {...props}>\n        <Title className=\"typography-ui-medium\">\n          {collapsible ? (\n            <button\n              type=\"button\"\n              aria-expanded={!collapsed}\n              aria-controls={bodyId}\n              onClick={toggleCollapsed}\n              className={cn('typography-ui-medium flex w-full items-center justify-between gap-2 rounded-item text-left', focusRing)}\n            >\n              {headingContent}\n              <ChevronDown aria-hidden=\"true\" className={cn('h-icon-xs w-icon-xs shrink-0 transition-transform motion-reduce:transition-none', collapsed && '-rotate-90')} />\n            </button>\n          ) : (\n            headingContent\n          )}\n        </Title>\n        <div id={bodyId} data-collapsed={collapsed || undefined} className={cn('flex-col gap-2', collapsed ? 'hidden' : 'flex')}>\n        {searchable ? (\n          <Input variant=\"search\" aria-label={l.searchIn(label)} placeholder={searchPlaceholder} value={query} onChange={(e) => onQueryChange?.(e.target.value)} />\n        ) : null}\n        <FacetSelectionModeContext.Provider value={selectionMode}>\n          <ul id={listId} className={cn('m-0 flex list-none flex-col p-0', zero ? 'gap-0' : 'gap-2')}>\n            {visible.map((option, i) => (\n              <li key={i}>{option}</li>\n            ))}\n          </ul>\n        </FacetSelectionModeContext.Provider>\n        {limit != null && options.length > limit ? (\n          <button\n            type=\"button\"\n            aria-expanded={expanded}\n            aria-controls={listId}\n            onClick={() => setExpanded((e) => !e)}\n            className={cn('typography-ui-medium h-6 self-start rounded-item text-left text-foreground hover:underline', focusRing)}\n          >\n            {expanded ? lessLabel : moreLabel(hidden || options.length - limit)}\n          </button>\n        ) : null}\n        </div>\n      </section>\n    );\n  },\n);\nFacetGroup.displayName = 'FacetGroup';\n\nexport interface FacetOptionProps extends Omit<HTMLAttributes<HTMLLabelElement>, 'onChange'> {\n  label: ReactNode;\n  checked?: boolean;\n  defaultChecked?: boolean;\n  onCheckedChange?: (checked: boolean) => void;\n  disabled?: boolean;\n  // Leading colour dot before the label (after the checkbox or add/remove icon), e.g. a project colour. A `bg-*` token class or a CSS colour.\n  dotColor?: string;\n  // Result count next to the label. A number, or text such as \"99+\" or \"n/a\".\n  count?: number | string;\n}\n\nconst FacetOption = forwardRef<HTMLLabelElement, FacetOptionProps>(\n  ({ className, label, checked, defaultChecked, onCheckedChange, disabled, dotColor, count, ...props }, ref) => {\n    const id = useId();\n    const labelId = useId();\n    const mode = useContext(FacetSelectionModeContext);\n    // Zero: 28px rows with the 16px checkbox.\n    const zero = usePreset() === 'zero';\n    const [inner, setInner] = useState(defaultChecked ?? false);\n    const selected = checked ?? inner;\n    if (mode === 'toggle-links') {\n      const { onClick, ...rest } = props as HTMLAttributes<HTMLButtonElement>;\n      return (\n        <button\n          ref={ref as unknown as Ref<HTMLButtonElement>}\n          type=\"button\"\n          aria-pressed={selected}\n          disabled={disabled}\n          data-selected={selected || undefined}\n          onClick={(event) => {\n            onClick?.(event);\n            if (checked === undefined) setInner(!selected);\n            onCheckedChange?.(!selected);\n          }}\n          className={cn(\n            'typography-ui flex w-full cursor-pointer items-center gap-2 rounded-item text-left text-link-default hover:text-link-hover hover:underline active:text-link-active disabled:cursor-not-allowed disabled:opacity-disabled',\n            zero ? 'h-control-sm' : 'h-6',\n            selected && 'font-medium',\n            focusRing,\n            className,\n          )}\n          {...rest}\n        >\n          {selected ? <Minus aria-hidden=\"true\" className=\"h-icon-xs w-icon-xs shrink-0\" /> : <Plus aria-hidden=\"true\" className=\"h-icon-xs w-icon-xs shrink-0\" />}\n          {dotColor ? <FacetDot color={dotColor} /> : null}\n          <span className=\"min-w-0 flex-1 truncate\">{label}</span>\n          {count != null ? <span className=\"typography-text-extra-small tabular-nums text-muted-foreground\">{count}</span> : null}\n        </button>\n      );\n    }\n    return (\n      <label ref={ref} htmlFor={id} className={cn('typography-ui flex cursor-pointer items-center gap-2', zero ? 'h-control-sm' : 'h-6', disabled && 'cursor-not-allowed opacity-disabled', className)} {...props}>\n        <Checkbox\n          id={id}\n          aria-labelledby={labelId}\n          size={zero ? 'sm' : 'default'}\n          checked={checked}\n          defaultChecked={defaultChecked}\n          disabled={disabled}\n          onCheckedChange={(value) => onCheckedChange?.(value === true)}\n        />\n        {dotColor ? <FacetDot color={dotColor} /> : null}\n        <span id={labelId} className=\"min-w-0 flex-1 truncate\">\n          {label}\n        </span>\n        {count != null ? <span className=\"typography-text-extra-small tabular-nums text-muted-foreground\">{count}</span> : null}\n      </label>\n    );\n  },\n);\nFacetOption.displayName = 'FacetOption';\n\nexport { FacetFilter, FacetGroup, FacetOption };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}