{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-filters",
  "title": "Task Filters",
  "description": "Filter and search tasks by status, priority, assignees, projects, and tags with active filter badges.",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "input-group",
    "select",
    "separator",
    "task-list"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/tasks/task-filters.tsx",
      "content": "\"use client\";\n\nimport { Search, X } from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/registry/new-york/ui/badge\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/new-york/ui/select\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\nimport type { TaskPriority, TaskStatus } from \"./task-list\";\n\nexport interface TaskFilter {\n  status?: TaskStatus[];\n  priority?: TaskPriority[];\n  assigneeIds?: string[];\n  dueDateRange?: { start?: Date; end?: Date };\n  tags?: string[];\n  projectIds?: string[];\n  search?: string;\n}\n\nexport interface SavedFilter {\n  id: string;\n  name: string;\n  filter: TaskFilter;\n}\n\nexport interface TaskFiltersProps {\n  onFilterChange?: (filter: TaskFilter) => void;\n  savedFilters?: SavedFilter[];\n  onSaveFilter?: (name: string, filter: TaskFilter) => Promise<void>;\n  onLoadFilter?: (filterId: string) => void;\n  onDeleteFilter?: (filterId: string) => Promise<void>;\n  availableAssignees?: Array<{ id: string; name: string }>;\n  availableProjects?: Array<{ id: string; name: string }>;\n  availableTags?: string[];\n  className?: string;\n  showSavedFilters?: boolean;\n}\n\nexport default function TaskFilters({\n  onFilterChange,\n  savedFilters = [],\n  onSaveFilter,\n  onLoadFilter,\n  onDeleteFilter,\n  availableAssignees = [],\n  availableProjects = [],\n  availableTags = [],\n  className,\n  showSavedFilters = true,\n}: TaskFiltersProps) {\n  const [search, setSearch] = useState(\"\");\n  const [status, setStatus] = useState<string[]>([]);\n  const [priority, setPriority] = useState<string[]>([]);\n  const [assignees, setAssignees] = useState<string[]>([]);\n  const [projects, setProjects] = useState<string[]>([]);\n  const [tags, setTags] = useState<string[]>([]);\n  const [dueDateStart, setDueDateStart] = useState(\"\");\n  const [dueDateEnd, setDueDateEnd] = useState(\"\");\n\n  const activeFilters = useMemo(() => {\n    const count =\n      (status.length > 0 ? 1 : 0) +\n      (priority.length > 0 ? 1 : 0) +\n      (assignees.length > 0 ? 1 : 0) +\n      (projects.length > 0 ? 1 : 0) +\n      (tags.length > 0 ? 1 : 0) +\n      (dueDateStart || dueDateEnd ? 1 : 0) +\n      (search.trim() ? 1 : 0);\n    return count;\n  }, [\n    status,\n    priority,\n    assignees,\n    projects,\n    tags,\n    dueDateStart,\n    dueDateEnd,\n    search,\n  ]);\n\n  const currentFilter: TaskFilter = useMemo(\n    () => ({\n      status: status.length > 0 ? (status as TaskStatus[]) : undefined,\n      priority: priority.length > 0 ? (priority as TaskPriority[]) : undefined,\n      assigneeIds: assignees.length > 0 ? assignees : undefined,\n      projectIds: projects.length > 0 ? projects : undefined,\n      tags: tags.length > 0 ? tags : undefined,\n      dueDateRange:\n        dueDateStart || dueDateEnd\n          ? {\n              start: dueDateStart ? new Date(dueDateStart) : undefined,\n              end: dueDateEnd ? new Date(dueDateEnd) : undefined,\n            }\n          : undefined,\n      search: search.trim() || undefined,\n    }),\n    [\n      status,\n      priority,\n      assignees,\n      projects,\n      tags,\n      dueDateStart,\n      dueDateEnd,\n      search,\n    ]\n  );\n\n  const handleFilterChange = () => {\n    onFilterChange?.(currentFilter);\n  };\n\n  const handleClearAll = () => {\n    setSearch(\"\");\n    setStatus([]);\n    setPriority([]);\n    setAssignees([]);\n    setProjects([]);\n    setTags([]);\n    setDueDateStart(\"\");\n    setDueDateEnd(\"\");\n    onFilterChange?.({});\n  };\n\n  const toggleArrayItem = <T,>(\n    array: T[],\n    item: T,\n    setter: (value: T[]) => void\n  ) => {\n    if (array.includes(item)) {\n      setter(array.filter((i) => i !== item));\n    } else {\n      setter([...array, item]);\n    }\n  };\n\n  return (\n    <Card className={cn(\"w-full shadow-xs\", className)}>\n      <CardHeader>\n        <div className=\"flex flex-col gap-1\">\n          <CardTitle>Filters</CardTitle>\n          <CardDescription>\n            {activeFilters > 0\n              ? `${activeFilters} active filter${activeFilters !== 1 ? \"s\" : \"\"}`\n              : \"No active filters\"}\n          </CardDescription>\n        </div>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-4\">\n          <InputGroup>\n            <InputGroupAddon>\n              <Search className=\"size-4\" />\n            </InputGroupAddon>\n            <InputGroupInput\n              onChange={(e) => {\n                setSearch(e.target.value);\n                handleFilterChange();\n              }}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") {\n                  handleFilterChange();\n                }\n              }}\n              placeholder=\"Search tasks…\"\n              type=\"search\"\n              value={search}\n            />\n            {search && (\n              <Button\n                aria-label=\"Clear search\"\n                className=\"absolute top-1/2 right-2 -translate-y-1/2\"\n                onClick={() => {\n                  setSearch(\"\");\n                  handleFilterChange();\n                }}\n                size=\"icon\"\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                <X className=\"size-4\" />\n              </Button>\n            )}\n          </InputGroup>\n\n          <div className=\"grid gap-4 sm:grid-cols-2\">\n            <div className=\"flex flex-col gap-2\">\n              <label className=\"text-muted-foreground text-sm\">Status</label>\n              <div className=\"flex flex-wrap gap-2\">\n                {(\n                  [\"todo\", \"in_progress\", \"done\", \"cancelled\"] as TaskStatus[]\n                ).map((s) => (\n                  <Button\n                    key={s}\n                    onClick={() => {\n                      toggleArrayItem(status, s, setStatus);\n                      handleFilterChange();\n                    }}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant={status.includes(s) ? \"default\" : \"outline\"}\n                  >\n                    {s === \"in_progress\"\n                      ? \"In Progress\"\n                      : s.charAt(0).toUpperCase() + s.slice(1)}\n                  </Button>\n                ))}\n              </div>\n            </div>\n\n            <div className=\"flex flex-col gap-2\">\n              <label className=\"text-muted-foreground text-sm\">Priority</label>\n              <div className=\"flex flex-wrap gap-2\">\n                {([\"low\", \"medium\", \"high\", \"urgent\"] as TaskPriority[]).map(\n                  (p) => (\n                    <Button\n                      key={p}\n                      onClick={() => {\n                        toggleArrayItem(priority, p, setPriority);\n                        handleFilterChange();\n                      }}\n                      size=\"sm\"\n                      type=\"button\"\n                      variant={priority.includes(p) ? \"default\" : \"outline\"}\n                    >\n                      {p.charAt(0).toUpperCase() + p.slice(1)}\n                    </Button>\n                  )\n                )}\n              </div>\n            </div>\n          </div>\n\n          {availableAssignees.length > 0 && (\n            <div className=\"flex flex-col gap-2\">\n              <label className=\"text-muted-foreground text-sm\">Assignees</label>\n              <Select\n                onValueChange={(value) => {\n                  toggleArrayItem(assignees, value, setAssignees);\n                  handleFilterChange();\n                }}\n                value=\"\"\n              >\n                <SelectTrigger>\n                  <SelectValue placeholder=\"Select assignees\" />\n                </SelectTrigger>\n                <SelectContent>\n                  {availableAssignees.map((assignee) => (\n                    <SelectItem key={assignee.id} value={assignee.id}>\n                      {assignee.name}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n              {assignees.length > 0 && (\n                <div className=\"flex flex-wrap gap-1\">\n                  {assignees.map((id) => {\n                    const assignee = availableAssignees.find(\n                      (a) => a.id === id\n                    );\n                    return assignee ? (\n                      <Badge\n                        className=\"flex items-center gap-1\"\n                        key={id}\n                        variant=\"secondary\"\n                      >\n                        {assignee.name}\n                        <button\n                          aria-label={`Remove ${assignee.name}`}\n                          onClick={() => {\n                            setAssignees(assignees.filter((a) => a !== id));\n                            handleFilterChange();\n                          }}\n                          type=\"button\"\n                        >\n                          <X className=\"size-3\" />\n                        </button>\n                      </Badge>\n                    ) : null;\n                  })}\n                </div>\n              )}\n            </div>\n          )}\n\n          {availableProjects.length > 0 && (\n            <div className=\"flex flex-col gap-2\">\n              <label className=\"text-muted-foreground text-sm\">Projects</label>\n              <Select\n                onValueChange={(value) => {\n                  toggleArrayItem(projects, value, setProjects);\n                  handleFilterChange();\n                }}\n                value=\"\"\n              >\n                <SelectTrigger>\n                  <SelectValue placeholder=\"Select projects\" />\n                </SelectTrigger>\n                <SelectContent>\n                  {availableProjects.map((project) => (\n                    <SelectItem key={project.id} value={project.id}>\n                      {project.name}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n              {projects.length > 0 && (\n                <div className=\"flex flex-wrap gap-1\">\n                  {projects.map((id) => {\n                    const project = availableProjects.find((p) => p.id === id);\n                    return project ? (\n                      <Badge\n                        className=\"flex items-center gap-1\"\n                        key={id}\n                        variant=\"secondary\"\n                      >\n                        {project.name}\n                        <button\n                          aria-label={`Remove ${project.name}`}\n                          onClick={() => {\n                            setProjects(projects.filter((p) => p !== id));\n                            handleFilterChange();\n                          }}\n                          type=\"button\"\n                        >\n                          <X className=\"size-3\" />\n                        </button>\n                      </Badge>\n                    ) : null;\n                  })}\n                </div>\n              )}\n            </div>\n          )}\n\n          <div className=\"grid gap-4 sm:grid-cols-2\">\n            <div className=\"flex flex-col gap-2\">\n              <label className=\"text-muted-foreground text-sm\">\n                Due Date From\n              </label>\n              <InputGroup>\n                <InputGroupInput\n                  onChange={(e) => {\n                    setDueDateStart(e.target.value);\n                    handleFilterChange();\n                  }}\n                  type=\"date\"\n                  value={dueDateStart}\n                />\n              </InputGroup>\n            </div>\n            <div className=\"flex flex-col gap-2\">\n              <label className=\"text-muted-foreground text-sm\">\n                Due Date To\n              </label>\n              <InputGroup>\n                <InputGroupInput\n                  onChange={(e) => {\n                    setDueDateEnd(e.target.value);\n                    handleFilterChange();\n                  }}\n                  type=\"date\"\n                  value={dueDateEnd}\n                />\n              </InputGroup>\n            </div>\n          </div>\n\n          {activeFilters > 0 && (\n            <>\n              <Separator />\n              <div className=\"flex items-center justify-between\">\n                <span className=\"text-muted-foreground text-sm\">\n                  {activeFilters} active filter{activeFilters !== 1 ? \"s\" : \"\"}\n                </span>\n                <Button\n                  onClick={handleClearAll}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  <X className=\"size-4\" />\n                  Clear All\n                </Button>\n              </div>\n            </>\n          )}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "tasks"
  ],
  "type": "registry:ui"
}