{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filter-toolbar",
  "title": "Filter Toolbar",
  "dependencies": [
    "@phosphor-icons/react"
  ],
  "registryDependencies": [
    "https://ui.hotfix.jobs/r/button.json",
    "https://ui.hotfix.jobs/r/input.json",
    "https://ui.hotfix.jobs/r/popover.json",
    "https://ui.hotfix.jobs/r/recipes.json",
    "https://ui.hotfix.jobs/r/scroller.json",
    "https://ui.hotfix.jobs/r/spinner.json",
    "https://ui.hotfix.jobs/r/tokens.json",
    "https://ui.hotfix.jobs/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/blocks/filter-toolbar/index.tsx",
      "content": "\"use client\";\n\nimport { CaretDown, Check } from \"@phosphor-icons/react/dist/ssr\";\nimport { useId, useMemo, useRef, useState } from \"react\";\nimport type * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { itemGroupLabel, itemRow } from \"@/lib/recipes\";\nimport { Button, type ButtonProps } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Scroller } from \"@/components/ui/scroller\";\nimport { Spinner } from \"@/components/ui/spinner\";\n\nexport type FilterToolbarOverflow = \"scroll\" | \"wrap\";\nexport type FilterToolbarCountVisibility = \"always\" | \"responsive\";\n\nexport interface FilterToolbarProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  children: React.ReactNode;\n  /** Horizontal rail by default; wrap is useful on spacious directory pages. */\n  overflow?: FilterToolbarOverflow;\n  /** Number of active filters. Controls visibility of the clear action. */\n  activeCount?: number;\n  onClearAll?: () => void;\n  clearLabel?: string;\n  /** Preformatted result count or other quiet metadata. */\n  count?: React.ReactNode;\n  /** Responsive hides count below the sm breakpoint. */\n  countVisibility?: FilterToolbarCountVisibility;\n  /** Pinned controls such as sorting or view selection. */\n  trailing?: React.ReactNode;\n  ariaLabel?: string;\n  filtersClassName?: string;\n  trailingClassName?: string;\n}\n\n/** Responsive filter rail with clear, count, and pinned trailing actions. */\nexport function FilterToolbar({\n  children,\n  overflow = \"scroll\",\n  activeCount = 0,\n  onClearAll,\n  clearLabel = \"Clear all\",\n  count,\n  countVisibility = \"responsive\",\n  trailing,\n  ariaLabel = \"Filters\",\n  className,\n  filtersClassName,\n  trailingClassName,\n  ...props\n}: FilterToolbarProps): React.ReactElement {\n  const clearAction =\n    activeCount > 0 && onClearAll ? (\n      <div className=\"shrink-0\">\n        <Button\n          variant=\"tertiary\"\n          size=\"md\"\n          className=\"text-ink-muted\"\n          onClick={onClearAll}\n        >\n          {clearLabel}\n        </Button>\n      </div>\n    ) : null;\n\n  return (\n    <div\n      data-slot=\"filter-toolbar\"\n      data-overflow={overflow}\n      className={cn(\n        \"flex w-full min-w-0 items-center gap-2\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        data-slot=\"filter-toolbar-filters\"\n        className=\"min-w-0 flex-1\"\n      >\n        {overflow === \"scroll\" ? (\n          <Scroller\n            overflow=\"x\"\n            ariaLabel={ariaLabel}\n            childrenContainerClassName={cn(\n              \"items-center gap-1 pe-1 [&>*]:shrink-0\",\n              filtersClassName,\n            )}\n          >\n            {children}\n            {clearAction}\n          </Scroller>\n        ) : (\n          <div\n            role=\"group\"\n            aria-label={ariaLabel}\n            className={cn(\n              \"flex flex-wrap items-center gap-1\",\n              filtersClassName,\n            )}\n          >\n            {children}\n            {clearAction}\n          </div>\n        )}\n      </div>\n\n      {(count != null || trailing != null) && (\n        <div\n          data-slot=\"filter-toolbar-trailing\"\n          className={cn(\n            \"flex shrink-0 items-center gap-2 sm:gap-3\",\n            trailingClassName,\n          )}\n        >\n          {count != null && (\n            <span\n              aria-live=\"polite\"\n              aria-atomic=\"true\"\n              className={cn(\n                \"whitespace-nowrap text-mini font-medium tabular-nums text-ink-muted\",\n                countVisibility === \"responsive\" && \"hidden sm:inline\",\n              )}\n            >\n              {count}\n            </span>\n          )}\n          {trailing}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport interface FilterToolbarTriggerProps\n  extends Omit<\n    ButtonProps,\n    \"children\" | \"icon\" | \"variant\" | \"size\" | \"value\"\n  > {\n  label: string;\n  value?: React.ReactNode;\n  active?: boolean;\n  icon?: React.ReactNode;\n}\n\n/** Standard filter trigger with a persistent category label and active summary. */\nexport function FilterToolbarTrigger({\n  label,\n  value,\n  active: activeProp,\n  icon,\n  className,\n  ...props\n}: FilterToolbarTriggerProps): React.ReactElement {\n  const active = activeProp ?? value != null;\n\n  return (\n    <Button\n      variant=\"secondary\"\n      size=\"md\"\n      icon={icon}\n      data-active={active || undefined}\n      className={cn(\n        \"group\",\n        active &&\n          \"bg-fill-2 hover:bg-fill-2 focus-visible:bg-fill-2 data-[popup-open]:bg-fill-2\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"whitespace-nowrap\">\n        {label}\n        {active && value != null && (\n          <>\n            <span className=\"text-ink-muted\">: </span>\n            {value}\n          </>\n        )}\n      </span>\n      <CaretDown\n        aria-hidden\n        className=\"size-3.5 transition-transform duration-[var(--duration-state)] ease-[var(--ease-standard)] group-data-[popup-open]:rotate-180\"\n      />\n    </Button>\n  );\n}\n\nexport interface FilterToolbarPickerOption {\n  id: string;\n  value: string;\n  label: React.ReactNode;\n  description?: React.ReactNode;\n  icon?: React.ReactNode;\n  suffix?: React.ReactNode;\n  disabled?: boolean;\n}\n\nexport interface FilterToolbarPickerSection {\n  id: string;\n  label?: React.ReactNode;\n  options: FilterToolbarPickerOption[];\n}\n\nexport interface FilterToolbarPickerProps {\n  label: string;\n  icon?: React.ReactNode;\n  selected: string[];\n  onSelectionChange: (selected: string[]) => void;\n  query: string;\n  onQueryChange: (query: string) => void;\n  sections: FilterToolbarPickerSection[];\n  placeholder?: string;\n  summary?: React.ReactNode;\n  loading?: boolean;\n  emptyContent?: React.ReactNode;\n  clearLabel?: string;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n  contentClassName?: string;\n}\n\n/** Searchable multi-select filter contained inside a toolbar Popover. */\nexport function FilterToolbarPicker({\n  label,\n  icon,\n  selected,\n  onSelectionChange,\n  query,\n  onQueryChange,\n  sections,\n  placeholder = `Search ${label.toLowerCase()}`,\n  summary,\n  loading = false,\n  emptyContent = \"No matches found.\",\n  clearLabel = `Clear ${label.toLowerCase()}`,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n  contentClassName,\n}: FilterToolbarPickerProps): React.ReactElement {\n  const generatedId = useId();\n  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);\n  const [activeIndex, setActiveIndex] = useState(-1);\n  const activeIndexRef = useRef(-1);\n  const open = controlledOpen ?? uncontrolledOpen;\n  const options = useMemo(\n    () => sections.flatMap((section) => section.options),\n    [sections],\n  );\n  const sectionOffsets = useMemo(\n    () =>\n      sections.map((_, sectionIndex) =>\n        sections\n          .slice(0, sectionIndex)\n          .reduce(\n            (total, section) => total + section.options.length,\n            0,\n          ),\n      ),\n    [sections],\n  );\n  const listboxId = `${generatedId}-listbox`;\n\n  const updateActiveIndex = (index: number) => {\n    activeIndexRef.current = index;\n    setActiveIndex(index);\n  };\n\n  const resolvedSummary =\n    summary ??\n    (selected.length === 1\n      ? options.find((option) => option.value === selected[0])?.label\n      : selected.length > 1\n        ? String(selected.length)\n        : undefined);\n\n  const setOpen = (next: boolean) => {\n    if (controlledOpen === undefined) setUncontrolledOpen(next);\n    onOpenChange?.(next);\n    if (!next) {\n      updateActiveIndex(-1);\n      onQueryChange(\"\");\n    }\n  };\n\n  const toggleOption = (option: FilterToolbarPickerOption) => {\n    if (option.disabled) return;\n    const next = selected.includes(option.value)\n      ? selected.filter((value) => value !== option.value)\n      : [...selected, option.value];\n    onSelectionChange(next);\n  };\n\n  const moveActive = (direction: 1 | -1) => {\n    if (options.length === 0) return;\n    let next = activeIndexRef.current;\n    for (let attempts = 0; attempts < options.length; attempts += 1) {\n      next = (next + direction + options.length) % options.length;\n      if (!options[next]?.disabled) {\n        updateActiveIndex(next);\n        return;\n      }\n    }\n  };\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n      event.preventDefault();\n      moveActive(event.key === \"ArrowDown\" ? 1 : -1);\n      return;\n    }\n    if (event.key === \"Enter\" && activeIndexRef.current >= 0) {\n      event.preventDefault();\n      const option = options[activeIndexRef.current];\n      if (option) toggleOption(option);\n      return;\n    }\n    if (event.key === \"Escape\") {\n      event.preventDefault();\n      setOpen(false);\n    }\n  };\n\n  const hasOptions = options.length > 0;\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger\n        render={\n          <FilterToolbarTrigger\n            label={label}\n            value={resolvedSummary}\n            active={selected.length > 0}\n            icon={icon}\n            className={className}\n          />\n        }\n      />\n      <PopoverContent\n        align=\"start\"\n        sideOffset={8}\n        initialFocus\n        className={cn(\"overflow-hidden p-0 md:w-80\", contentClassName)}\n      >\n        <div className=\"p-1\">\n          <Input\n            size=\"lg\"\n            type=\"search\"\n            value={query}\n            onChange={(event) => {\n              onQueryChange(event.target.value);\n              updateActiveIndex(-1);\n            }}\n            onKeyDown={handleKeyDown}\n            placeholder={placeholder}\n            aria-label={placeholder}\n            autoComplete=\"off\"\n            aria-autocomplete=\"list\"\n            aria-controls={listboxId}\n            aria-expanded\n            aria-activedescendant={\n              activeIndex >= 0\n                ? `${generatedId}-option-${activeIndex}`\n                : undefined\n            }\n            suffix={loading ? <Spinner size=\"sm\" /> : undefined}\n          />\n        </div>\n\n        <div className=\"max-h-[min(360px,55dvh)] overflow-y-auto p-1\">\n          {hasOptions ? (\n            <div id={listboxId} role=\"listbox\" aria-label={label} aria-multiselectable=\"true\">\n              {sections.map((section, sectionIndex) => (\n                <div key={section.id} data-slot=\"filter-toolbar-picker-section\">\n                  {section.label && (\n                    <div\n                      className={cn(\n                        itemGroupLabel.base,\n                        itemGroupLabel.comfortable,\n                      )}\n                    >\n                      {section.label}\n                    </div>\n                  )}\n                  {section.options.map((option, optionIndex) => {\n                    const index = sectionOffsets[sectionIndex] + optionIndex;\n                    const active = index === activeIndex;\n                    const checked = selected.includes(option.value);\n                    return (\n                      <button\n                        key={option.id}\n                        id={`${generatedId}-option-${index}`}\n                        type=\"button\"\n                        role=\"option\"\n                        aria-selected={checked}\n                        disabled={option.disabled}\n                        data-active={active || undefined}\n                        data-selected={checked || undefined}\n                        className={cn(\n                          itemRow.base,\n                          itemRow.comfortable,\n                          \"w-full gap-3 text-start\",\n                        )}\n                        onMouseEnter={() => updateActiveIndex(index)}\n                        onMouseDown={(event) => event.preventDefault()}\n                        onClick={() => toggleOption(option)}\n                      >\n                        {option.icon && (\n                          <span className=\"flex shrink-0 items-center [&_svg]:size-4\">\n                            {option.icon}\n                          </span>\n                        )}\n                        <span className=\"min-w-0 flex-1\">\n                          <span className=\"block truncate text-small text-ink\">\n                            {option.label}\n                          </span>\n                          {option.description && (\n                            <span className=\"block truncate text-mini text-ink-muted\">\n                              {option.description}\n                            </span>\n                          )}\n                        </span>\n                        {option.suffix && !checked && (\n                          <span className=\"shrink-0 text-mini text-ink-muted\">\n                            {option.suffix}\n                          </span>\n                        )}\n                        {checked && (\n                          <Check aria-hidden className=\"size-4 shrink-0\" />\n                        )}\n                      </button>\n                    );\n                  })}\n                </div>\n              ))}\n            </div>\n          ) : (\n            <div className=\"px-4 py-8 text-center text-small text-ink-muted\">\n              {loading ? \"Searching\" : emptyContent}\n            </div>\n          )}\n        </div>\n\n        {selected.length > 0 && (\n          <div className=\"flex justify-end px-2 pb-2 pt-1\">\n            <Button\n              variant=\"tertiary\"\n              size=\"sm\"\n              className=\"text-ink-muted\"\n              onClick={() => onSelectionChange([])}\n            >\n              {clearLabel}\n            </Button>\n          </div>\n        )}\n      </PopoverContent>\n    </Popover>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/blocks/filter-toolbar/index.tsx"
    }
  ],
  "type": "registry:block"
}