{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-list",
  "title": "Task List",
  "description": "List view of tasks with sorting, filtering, bulk selection, and inline status updates.",
  "registryDependencies": [
    "avatar",
    "badge",
    "button",
    "card",
    "checkbox",
    "dropdown-menu",
    "input-group",
    "select",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/tasks/task-list.tsx",
      "content": "\"use client\";\n\nimport {\n  Calendar,\n  CheckCircle2,\n  Circle,\n  Clock,\n  Filter,\n  MoreVertical,\n  Plus,\n  Search,\n  SortAsc,\n  User,\n  X,\n} from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/registry/new-york/ui/avatar\";\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 { Checkbox } from \"@/registry/new-york/ui/checkbox\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/registry/new-york/ui/dropdown-menu\";\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\";\n\nexport type TaskStatus = \"todo\" | \"in_progress\" | \"done\" | \"cancelled\";\nexport type TaskPriority = \"low\" | \"medium\" | \"high\" | \"urgent\";\n\nexport interface TaskAssignee {\n  id: string;\n  name: string;\n  avatar?: string;\n}\n\nexport interface Task {\n  id: string;\n  title: string;\n  description?: string;\n  status: TaskStatus;\n  priority: TaskPriority;\n  assignees?: TaskAssignee[];\n  dueDate?: Date;\n  tags?: string[];\n  projectId?: string;\n  createdAt: Date;\n  updatedAt: Date;\n}\n\nexport interface TaskListProps {\n  tasks?: Task[];\n  onTaskSelect?: (taskId: string) => void;\n  onTaskUpdate?: (taskId: string, updates: Partial<Task>) => Promise<void>;\n  onTaskDelete?: (taskId: string) => Promise<void>;\n  onBulkAction?: (taskIds: string[], action: string) => Promise<void>;\n  onCreateTask?: () => void;\n  className?: string;\n  showSearch?: boolean;\n  showFilters?: boolean;\n  showBulkActions?: boolean;\n  itemsPerPage?: number;\n}\n\nfunction formatDate(date: Date): string {\n  const now = new Date();\n  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n  const tomorrow = new Date(today);\n  tomorrow.setDate(tomorrow.getDate() + 1);\n  const dateOnly = new Date(\n    date.getFullYear(),\n    date.getMonth(),\n    date.getDate()\n  );\n\n  if (dateOnly.getTime() === today.getTime()) {\n    return \"Today\";\n  }\n  if (dateOnly.getTime() === tomorrow.getTime()) {\n    return \"Tomorrow\";\n  }\n\n  const month = date.toLocaleString(\"en-US\", { month: \"short\" });\n  const day = date.getDate();\n  return `${month} ${day}`;\n}\n\nfunction getStatusIcon(status: TaskStatus) {\n  switch (status) {\n    case \"done\":\n      return CheckCircle2;\n    case \"in_progress\":\n      return Clock;\n    default:\n      return Circle;\n  }\n}\n\nfunction getStatusColor(status: TaskStatus): string {\n  switch (status) {\n    case \"done\":\n      return \"text-green-600\";\n    case \"in_progress\":\n      return \"text-blue-600\";\n    case \"cancelled\":\n      return \"text-muted-foreground\";\n    default:\n      return \"text-muted-foreground\";\n  }\n}\n\nfunction getPriorityColor(priority: TaskPriority): string {\n  switch (priority) {\n    case \"urgent\":\n      return \"bg-red-500\";\n    case \"high\":\n      return \"bg-orange-500\";\n    case \"medium\":\n      return \"bg-yellow-500\";\n    default:\n      return \"bg-gray-500\";\n  }\n}\n\nfunction getPriorityLabel(priority: TaskPriority): string {\n  return priority.charAt(0).toUpperCase() + priority.slice(1);\n}\n\nfunction isOverdue(date?: Date): boolean {\n  if (!date) return false;\n  const now = new Date();\n  now.setHours(0, 0, 0, 0);\n  const dueDate = new Date(date);\n  dueDate.setHours(0, 0, 0, 0);\n  return dueDate < now;\n}\n\ninterface TaskItemProps {\n  task: Task;\n  isSelected: boolean;\n  onSelect: (taskId: string, selected: boolean) => void;\n  onTaskClick: (taskId: string) => void;\n  onStatusChange: (taskId: string, status: TaskStatus) => void;\n  onDelete: (taskId: string) => void;\n}\n\nfunction TaskItem({\n  task,\n  isSelected,\n  onSelect,\n  onTaskClick,\n  onStatusChange,\n  onDelete,\n}: TaskItemProps) {\n  const overdue = isOverdue(task.dueDate);\n\n  return (\n    <div\n      className={cn(\n        \"group flex items-start gap-3 rounded-lg border bg-card p-4 transition-colors hover:bg-accent\",\n        isSelected && \"border-primary bg-primary/5\"\n      )}\n    >\n      <Checkbox\n        checked={isSelected}\n        onCheckedChange={(checked) => onSelect(task.id, checked === true)}\n        onClick={(e) => e.stopPropagation()}\n      />\n      <div className=\"flex min-w-0 flex-1 items-start gap-3\">\n        <button\n          className=\"flex min-w-0 flex-1 items-start gap-3 text-left\"\n          onClick={() => onTaskClick(task.id)}\n          type=\"button\"\n        >\n          {task.status === \"done\" ? (\n            <CheckCircle2\n              className={cn(\n                \"mt-0.5 size-5 shrink-0\",\n                getStatusColor(task.status)\n              )}\n            />\n          ) : task.status === \"in_progress\" ? (\n            <Clock\n              className={cn(\n                \"mt-0.5 size-5 shrink-0\",\n                getStatusColor(task.status)\n              )}\n            />\n          ) : (\n            <Circle\n              className={cn(\n                \"mt-0.5 size-5 shrink-0\",\n                getStatusColor(task.status)\n              )}\n            />\n          )}\n          <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n            <h4 className=\"wrap-break-word font-medium text-sm leading-tight\">\n              {task.title}\n            </h4>\n            {task.description && (\n              <p className=\"line-clamp-2 text-muted-foreground text-xs\">\n                {task.description}\n              </p>\n            )}\n            <div className=\"flex flex-wrap items-center gap-2\">\n              <div\n                className={cn(\n                  \"size-2 rounded-full\",\n                  getPriorityColor(task.priority)\n                )}\n                title={getPriorityLabel(task.priority)}\n              />\n              {task.assignees && task.assignees.length > 0 && (\n                <div className=\"flex items-center gap-1\">\n                  <User className=\"size-3 text-muted-foreground\" />\n                  <div className=\"flex -space-x-2\">\n                    {task.assignees.slice(0, 3).map((assignee) => (\n                      <Avatar\n                        className=\"size-5 border-2 border-background\"\n                        key={assignee.id}\n                      >\n                        <AvatarImage\n                          alt={assignee.name}\n                          src={assignee.avatar}\n                        />\n                        <AvatarFallback className=\"text-xs\">\n                          {assignee.name.charAt(0).toUpperCase()}\n                        </AvatarFallback>\n                      </Avatar>\n                    ))}\n                    {task.assignees.length > 3 && (\n                      <div className=\"flex size-5 items-center justify-center rounded-full border-2 border-background bg-muted text-muted-foreground text-xs\">\n                        +{task.assignees.length - 3}\n                      </div>\n                    )}\n                  </div>\n                </div>\n              )}\n              {task.dueDate && (\n                <div\n                  className={cn(\n                    \"flex items-center gap-1 text-xs\",\n                    overdue ? \"text-destructive\" : \"text-muted-foreground\"\n                  )}\n                >\n                  <Calendar className=\"size-3\" />\n                  <span>{formatDate(task.dueDate)}</span>\n                </div>\n              )}\n              {task.tags && task.tags.length > 0 && (\n                <div className=\"flex flex-wrap gap-1\">\n                  {task.tags.slice(0, 3).map((tag) => (\n                    <Badge className=\"text-xs\" key={tag} variant=\"outline\">\n                      {tag}\n                    </Badge>\n                  ))}\n                  {task.tags.length > 3 && (\n                    <Badge className=\"text-xs\" variant=\"outline\">\n                      +{task.tags.length - 3}\n                    </Badge>\n                  )}\n                </div>\n              )}\n            </div>\n          </div>\n        </button>\n        <div className=\"flex shrink-0 items-center\">\n          <DropdownMenu>\n            <DropdownMenuTrigger asChild>\n              <Button\n                aria-label={`More options for ${task.title}`}\n                className=\"opacity-0 transition-opacity group-hover:opacity-100\"\n                onClick={(e) => e.stopPropagation()}\n                size=\"icon\"\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                <MoreVertical className=\"size-4\" />\n              </Button>\n            </DropdownMenuTrigger>\n            <DropdownMenuContent align=\"end\">\n              <DropdownMenuItem onClick={() => onStatusChange(task.id, \"todo\")}>\n                <Circle className=\"size-4\" />\n                Mark as To Do\n              </DropdownMenuItem>\n              <DropdownMenuItem\n                onClick={() => onStatusChange(task.id, \"in_progress\")}\n              >\n                <Clock className=\"size-4\" />\n                Mark as In Progress\n              </DropdownMenuItem>\n              <DropdownMenuItem onClick={() => onStatusChange(task.id, \"done\")}>\n                <CheckCircle2 className=\"size-4\" />\n                Mark as Done\n              </DropdownMenuItem>\n              <DropdownMenuSeparator />\n              <DropdownMenuItem\n                onClick={() => onDelete(task.id)}\n                variant=\"destructive\"\n              >\n                <X className=\"size-4\" />\n                Delete\n              </DropdownMenuItem>\n            </DropdownMenuContent>\n          </DropdownMenu>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default function TaskList({\n  tasks = [],\n  onTaskSelect,\n  onTaskUpdate,\n  onTaskDelete,\n  onBulkAction,\n  onCreateTask,\n  className,\n  showSearch = true,\n  showFilters = true,\n  showBulkActions = true,\n  itemsPerPage = 20,\n}: TaskListProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [statusFilter, setStatusFilter] = useState<string>(\"all\");\n  const [priorityFilter, setPriorityFilter] = useState<string>(\"all\");\n  const [sortBy, setSortBy] = useState<string>(\"updated\");\n  const [selectedTasks, setSelectedTasks] = useState<Set<string>>(new Set());\n  const [currentPage, setCurrentPage] = useState(1);\n\n  const filteredTasks = useMemo(() => {\n    let filtered = [...tasks];\n\n    if (searchQuery.trim()) {\n      const query = searchQuery.toLowerCase();\n      filtered = filtered.filter(\n        (task) =>\n          task.title.toLowerCase().includes(query) ||\n          task.description?.toLowerCase().includes(query) ||\n          task.tags?.some((tag) => tag.toLowerCase().includes(query))\n      );\n    }\n\n    if (statusFilter !== \"all\") {\n      filtered = filtered.filter((task) => task.status === statusFilter);\n    }\n\n    if (priorityFilter !== \"all\") {\n      filtered = filtered.filter((task) => task.priority === priorityFilter);\n    }\n\n    filtered.sort((a, b) => {\n      switch (sortBy) {\n        case \"title\":\n          return a.title.localeCompare(b.title);\n        case \"priority\": {\n          const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 };\n          return (\n            (priorityOrder[b.priority] || 0) - (priorityOrder[a.priority] || 0)\n          );\n        }\n        case \"dueDate\":\n          if (!(a.dueDate || b.dueDate)) return 0;\n          if (!a.dueDate) return 1;\n          if (!b.dueDate) return -1;\n          return a.dueDate.getTime() - b.dueDate.getTime();\n        case \"updated\":\n        default:\n          return b.updatedAt.getTime() - a.updatedAt.getTime();\n      }\n    });\n\n    return filtered;\n  }, [tasks, searchQuery, statusFilter, priorityFilter, sortBy]);\n\n  const paginatedTasks = useMemo(() => {\n    const start = (currentPage - 1) * itemsPerPage;\n    return filteredTasks.slice(start, start + itemsPerPage);\n  }, [filteredTasks, currentPage, itemsPerPage]);\n\n  const totalPages = Math.ceil(filteredTasks.length / itemsPerPage);\n\n  const handleTaskSelect = (taskId: string, selected: boolean) => {\n    setSelectedTasks((prev) => {\n      const next = new Set(prev);\n      if (selected) {\n        next.add(taskId);\n      } else {\n        next.delete(taskId);\n      }\n      return next;\n    });\n  };\n\n  const handleSelectAll = (checked: boolean) => {\n    if (checked) {\n      setSelectedTasks(new Set(paginatedTasks.map((t) => t.id)));\n    } else {\n      setSelectedTasks(new Set());\n    }\n  };\n\n  const handleStatusChange = async (taskId: string, status: TaskStatus) => {\n    await onTaskUpdate?.(taskId, { status });\n  };\n\n  const handleDelete = async (taskId: string) => {\n    await onTaskDelete?.(taskId);\n    setSelectedTasks((prev) => {\n      const next = new Set(prev);\n      next.delete(taskId);\n      return next;\n    });\n  };\n\n  const handleBulkAction = async (action: string) => {\n    if (selectedTasks.size === 0) return;\n    await onBulkAction?.(Array.from(selectedTasks), action);\n    setSelectedTasks(new Set());\n  };\n\n  const allSelected =\n    paginatedTasks.length > 0 &&\n    paginatedTasks.every((t) => selectedTasks.has(t.id));\n  const someSelected = paginatedTasks.some((t) => selectedTasks.has(t.id));\n\n  return (\n    <Card className={cn(\"w-full shadow-xs\", className)}>\n      <CardHeader>\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"flex flex-wrap items-center justify-between gap-3\">\n            <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n              <CardTitle>Tasks</CardTitle>\n              <CardDescription>\n                {filteredTasks.length} task\n                {filteredTasks.length !== 1 ? \"s\" : \"\"}\n              </CardDescription>\n            </div>\n            {onCreateTask && (\n              <Button onClick={onCreateTask} type=\"button\">\n                <Plus className=\"size-4\" />\n                New Task\n              </Button>\n            )}\n          </div>\n          {showSearch && (\n            <InputGroup>\n              <InputGroupAddon>\n                <Search className=\"size-4\" />\n              </InputGroupAddon>\n              <InputGroupInput\n                onChange={(e) => setSearchQuery(e.target.value)}\n                placeholder=\"Search tasks…\"\n                type=\"search\"\n                value={searchQuery}\n              />\n              {searchQuery && (\n                <Button\n                  aria-label=\"Clear search\"\n                  className=\"absolute top-1/2 right-2 -translate-y-1/2\"\n                  onClick={() => setSearchQuery(\"\")}\n                  size=\"icon\"\n                  type=\"button\"\n                  variant=\"ghost\"\n                >\n                  <X className=\"size-4\" />\n                </Button>\n              )}\n            </InputGroup>\n          )}\n          {showFilters && (\n            <div className=\"flex flex-wrap items-center gap-2\">\n              <Select onValueChange={setStatusFilter} value={statusFilter}>\n                <SelectTrigger className=\"w-full sm:w-[140px]\">\n                  <Filter className=\"size-4\" />\n                  <SelectValue placeholder=\"Status\" />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"all\">All Status</SelectItem>\n                  <SelectItem value=\"todo\">To Do</SelectItem>\n                  <SelectItem value=\"in_progress\">In Progress</SelectItem>\n                  <SelectItem value=\"done\">Done</SelectItem>\n                  <SelectItem value=\"cancelled\">Cancelled</SelectItem>\n                </SelectContent>\n              </Select>\n              <Select onValueChange={setPriorityFilter} value={priorityFilter}>\n                <SelectTrigger className=\"w-full sm:w-[140px]\">\n                  <SelectValue placeholder=\"Priority\" />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"all\">All Priorities</SelectItem>\n                  <SelectItem value=\"urgent\">Urgent</SelectItem>\n                  <SelectItem value=\"high\">High</SelectItem>\n                  <SelectItem value=\"medium\">Medium</SelectItem>\n                  <SelectItem value=\"low\">Low</SelectItem>\n                </SelectContent>\n              </Select>\n              <Select onValueChange={setSortBy} value={sortBy}>\n                <SelectTrigger className=\"w-fit\">\n                  <SortAsc className=\"size-4\" />\n                  <SelectValue placeholder=\"Sort by\" />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"updated\">Last Updated</SelectItem>\n                  <SelectItem value=\"title\">Title</SelectItem>\n                  <SelectItem value=\"priority\">Priority</SelectItem>\n                  <SelectItem value=\"dueDate\">Due Date</SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n          )}\n          {showBulkActions && selectedTasks.size > 0 && (\n            <div className=\"flex items-center gap-2 rounded-lg border bg-muted/30 p-2\">\n              <span className=\"text-muted-foreground text-sm\">\n                {selectedTasks.size} selected\n              </span>\n              <Separator orientation=\"vertical\" />\n              <div className=\"flex gap-1\">\n                <Button\n                  onClick={() => handleBulkAction(\"complete\")}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  Complete\n                </Button>\n                <Button\n                  onClick={() => handleBulkAction(\"delete\")}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  Delete\n                </Button>\n              </div>\n            </div>\n          )}\n        </div>\n      </CardHeader>\n      <CardContent>\n        {paginatedTasks.length === 0 ? (\n          <div className=\"flex flex-col items-center justify-center gap-4 py-12 text-center\">\n            <div className=\"flex size-12 items-center justify-center rounded-full bg-muted\">\n              <Circle className=\"size-6 text-muted-foreground\" />\n            </div>\n            <div className=\"flex flex-col gap-2\">\n              <p className=\"font-medium text-sm\">\n                {searchQuery ||\n                statusFilter !== \"all\" ||\n                priorityFilter !== \"all\"\n                  ? \"No tasks match your filters\"\n                  : \"No tasks yet\"}\n              </p>\n              <p className=\"text-muted-foreground text-sm\">\n                {onCreateTask\n                  ? \"Create your first task to get started\"\n                  : \"Tasks will appear here\"}\n              </p>\n            </div>\n            {onCreateTask && (\n              <Button onClick={onCreateTask} type=\"button\" variant=\"outline\">\n                <Plus className=\"size-4\" />\n                Create Task\n              </Button>\n            )}\n          </div>\n        ) : (\n          <div className=\"flex flex-col gap-2\">\n            {showBulkActions && (\n              <div className=\"flex items-center gap-2 px-1\">\n                <Checkbox\n                  checked={allSelected}\n                  onCheckedChange={handleSelectAll}\n                />\n                <span className=\"text-muted-foreground text-xs\">\n                  Select all\n                </span>\n              </div>\n            )}\n            {paginatedTasks.map((task) => (\n              <TaskItem\n                isSelected={selectedTasks.has(task.id)}\n                key={task.id}\n                onDelete={handleDelete}\n                onSelect={handleTaskSelect}\n                onStatusChange={handleStatusChange}\n                onTaskClick={onTaskSelect || (() => {})}\n                task={task}\n              />\n            ))}\n            {totalPages > 1 && (\n              <div className=\"flex items-center justify-center gap-2 pt-4\">\n                <Button\n                  disabled={currentPage === 1}\n                  onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  Previous\n                </Button>\n                <span className=\"text-muted-foreground text-sm\">\n                  Page {currentPage} of {totalPages}\n                </span>\n                <Button\n                  disabled={currentPage >= totalPages}\n                  onClick={() =>\n                    setCurrentPage((p) => Math.min(totalPages, p + 1))\n                  }\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  Next\n                </Button>\n              </div>\n            )}\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "tasks"
  ],
  "type": "registry:ui"
}