{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "filter-bar",
  "type": "registry:component",
  "title": "Filter Bar",
  "description": "Horizontal bar with filter controls for content lists.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/filter-bar/filter-bar.tsx",
      "content": "\"use client\";\n\nimport { memo, useCallback, useState } from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\nimport { Badge } from \"@vllnt/ui\";\n\nexport type FilterOption = {\n  label: string;\n  value: string;\n};\n\nexport type FilterBarLabels = {\n  activeFilters?: string;\n  clearAll?: string;\n  clearTags?: string;\n  difficultyLabel?: string;\n  searchLabel?: string;\n  searchPlaceholder?: string;\n  tagsLabel?: string;\n};\n\nexport type FilterBarProps = {\n  className?: string;\n  /** Current active difficulty filter */\n  currentDifficulty: string;\n  /** Current active tags */\n  currentTags: string[];\n  /** Difficulty filter options */\n  difficultyOptions: readonly FilterOption[];\n  /** Labels for i18n */\n  labels?: FilterBarLabels;\n  /** Callback when filters change */\n  onFiltersChange: (filters: {\n    difficulty?: string;\n    search?: string;\n    tags?: string[];\n  }) => void;\n  /** Search query */\n  searchQuery: string;\n  /** Available tags */\n  tags: string[];\n};\n\n// Search input sub-component\nfunction SearchInput({\n  disabled,\n  label,\n  onChange,\n  placeholder,\n  value,\n}: {\n  disabled: boolean;\n  label: string;\n  onChange: (value: string) => void;\n  placeholder: string;\n  value: string;\n}): React.ReactNode {\n  return (\n    <div>\n      <label className=\"sr-only\" htmlFor=\"filter-search\">\n        {label}\n      </label>\n      <input\n        className={cn(\n          \"w-full px-4 py-2 border border-border rounded-lg\",\n          \"bg-background text-foreground placeholder:text-muted-foreground\",\n          \"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n        )}\n        defaultValue={value}\n        disabled={disabled}\n        id=\"filter-search\"\n        onChange={(event) => {\n          onChange(event.target.value);\n        }}\n        placeholder={placeholder}\n        type=\"text\"\n      />\n    </div>\n  );\n}\n\n// Difficulty filter sub-component\nfunction DifficultyFilter({\n  activeDifficulty,\n  disabled,\n  label,\n  onChange,\n  options,\n}: {\n  activeDifficulty: string;\n  disabled: boolean;\n  label: string;\n  onChange: (value: string) => void;\n  options: readonly FilterOption[];\n}): React.ReactNode {\n  return (\n    <div>\n      <div className=\"flex items-center gap-2 mb-2\">\n        <span className=\"text-sm font-medium\">{label}</span>\n      </div>\n      <div className=\"flex flex-wrap gap-2\">\n        {options.map((option) => {\n          const isActive = option.value === activeDifficulty;\n          return (\n            <button\n              className={cn(\n                \"px-3 py-1 text-sm rounded-lg border transition-colors\",\n                isActive\n                  ? \"bg-primary text-primary-foreground border-transparent\"\n                  : \"bg-background text-foreground border-border hover:bg-muted\",\n              )}\n              disabled={disabled}\n              key={option.value}\n              onClick={() => {\n                onChange(option.value);\n              }}\n              type=\"button\"\n            >\n              {option.label}\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\n// Tag filter sub-component\nfunction TagFilter({\n  clearLabel,\n  currentTags,\n  label,\n  onClearTags,\n  onTagToggle,\n  tags,\n}: {\n  clearLabel: string;\n  currentTags: string[];\n  label: string;\n  onClearTags: () => void;\n  onTagToggle: (tag: string) => void;\n  tags: string[];\n}): React.ReactNode {\n  if (tags.length === 0) return null;\n\n  return (\n    <div>\n      <div className=\"flex items-center gap-2 mb-2\">\n        <span className=\"text-sm font-medium\">{label}</span>\n        {currentTags.length > 0 ? (\n          <button\n            className=\"text-xs text-muted-foreground hover:text-foreground transition-colors\"\n            onClick={onClearTags}\n            type=\"button\"\n          >\n            {clearLabel}\n          </button>\n        ) : null}\n      </div>\n      <div className=\"flex flex-wrap gap-2\">\n        {tags.map((tag) => {\n          const isActive = currentTags.includes(tag);\n          return (\n            <Badge\n              className={cn(\n                \"cursor-pointer transition-all\",\n                isActive\n                  ? \"bg-primary text-primary-foreground border-transparent\"\n                  : \"hover:bg-muted\",\n              )}\n              key={tag}\n              onClick={() => {\n                onTagToggle(tag);\n              }}\n              variant={isActive ? \"default\" : \"outline\"}\n            >\n              {tag}\n            </Badge>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\n// Active filters summary sub-component\nfunction ActiveFiltersSummary({\n  clearAllLabel,\n  currentDifficulty,\n  currentTags,\n  difficultyOptions,\n  label,\n  onClearAll,\n  searchLabel,\n  searchQuery,\n}: {\n  clearAllLabel: string;\n  currentDifficulty: string;\n  currentTags: string[];\n  difficultyOptions: readonly FilterOption[];\n  label: string;\n  onClearAll: () => void;\n  searchLabel: string;\n  searchQuery: string;\n}): React.ReactNode {\n  if (!currentDifficulty && currentTags.length === 0 && !searchQuery)\n    return null;\n\n  const difficultyLabel = difficultyOptions.find(\n    (o) => o.value === currentDifficulty,\n  )?.label;\n\n  return (\n    <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n      <span>{label}</span>\n      {difficultyLabel ? (\n        <Badge className=\"capitalize\" variant=\"secondary\">\n          {difficultyLabel}\n        </Badge>\n      ) : null}\n      {currentTags.map((tag) => (\n        <Badge key={tag} variant=\"secondary\">\n          {tag}\n        </Badge>\n      ))}\n      {searchQuery ? (\n        <Badge variant=\"secondary\">\n          {searchLabel} &quot;{searchQuery}&quot;\n        </Badge>\n      ) : null}\n      <button\n        className=\"text-xs hover:underline\"\n        onClick={onClearAll}\n        type=\"button\"\n      >\n        {clearAllLabel}\n      </button>\n    </div>\n  );\n}\n\nconst DEFAULT_LABELS: Required<FilterBarLabels> = {\n  activeFilters: \"Active filters:\",\n  clearAll: \"Clear all\",\n  clearTags: \"Clear\",\n  difficultyLabel: \"Difficulty:\",\n  searchLabel: \"Search\",\n  searchPlaceholder: \"Search...\",\n  tagsLabel: \"Tags:\",\n};\n\nconst EMPTY_FILTER_BAR_LABELS: FilterBarLabels = {};\n\n// eslint-disable-next-line max-lines-per-function -- Complex filter component with sub-components\nfunction FilterBarImpl({\n  className,\n  currentDifficulty,\n  currentTags,\n  difficultyOptions,\n  labels = EMPTY_FILTER_BAR_LABELS,\n  onFiltersChange,\n  searchQuery,\n  tags,\n}: FilterBarProps): React.ReactNode {\n  const [isPending, setIsPending] = useState(false);\n  const mergedLabels = { ...DEFAULT_LABELS, ...labels };\n\n  const handleDifficultyChange = useCallback(\n    (difficulty: string): void => {\n      setIsPending(true);\n      onFiltersChange({ difficulty });\n      setIsPending(false);\n    },\n    [onFiltersChange],\n  );\n\n  const handleSearchChange = useCallback(\n    (search: string): void => {\n      onFiltersChange({ search });\n    },\n    [onFiltersChange],\n  );\n\n  const handleTagToggle = useCallback(\n    (tag: string): void => {\n      const newTags = currentTags.includes(tag)\n        ? currentTags.filter((t) => t !== tag)\n        : [...currentTags, tag];\n      onFiltersChange({ tags: newTags });\n    },\n    [currentTags, onFiltersChange],\n  );\n\n  const handleClearAll = useCallback((): void => {\n    onFiltersChange({ difficulty: \"all\", search: \"\", tags: [] });\n    const input = document.querySelector<HTMLInputElement>(\"#filter-search\");\n    if (input) input.value = \"\";\n  }, [onFiltersChange]);\n\n  const activeDifficulty = currentDifficulty || \"all\";\n\n  return (\n    <div className={cn(\"space-y-4 mb-8\", className)}>\n      <SearchInput\n        disabled={isPending}\n        label={mergedLabels.searchLabel}\n        onChange={handleSearchChange}\n        placeholder={mergedLabels.searchPlaceholder}\n        value={searchQuery}\n      />\n      <DifficultyFilter\n        activeDifficulty={activeDifficulty}\n        disabled={isPending}\n        label={mergedLabels.difficultyLabel}\n        onChange={handleDifficultyChange}\n        options={difficultyOptions}\n      />\n      <TagFilter\n        clearLabel={mergedLabels.clearTags}\n        currentTags={currentTags}\n        label={mergedLabels.tagsLabel}\n        onClearTags={() => {\n          onFiltersChange({ tags: [] });\n        }}\n        onTagToggle={handleTagToggle}\n        tags={tags}\n      />\n      <ActiveFiltersSummary\n        clearAllLabel={mergedLabels.clearAll}\n        currentDifficulty={currentDifficulty}\n        currentTags={currentTags}\n        difficultyOptions={difficultyOptions}\n        label={mergedLabels.activeFilters}\n        onClearAll={handleClearAll}\n        searchLabel={mergedLabels.searchLabel}\n        searchQuery={searchQuery}\n      />\n    </div>\n  );\n}\n\nexport const FilterBar = memo(FilterBarImpl);\nFilterBar.displayName = \"FilterBar\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
