{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-board",
  "title": "Task Board",
  "description": "Kanban-style board for managing tasks with drag-and-drop functionality.",
  "registryDependencies": [
    "avatar",
    "badge",
    "button",
    "card",
    "dropdown-menu",
    "task-list"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/tasks/task-board.tsx",
      "content": "\"use client\";\n\nimport {\n  Calendar,\n  CheckCircle2,\n  Circle,\n  Clock,\n  MoreVertical,\n  Plus,\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 {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/registry/new-york/ui/dropdown-menu\";\nimport type { Task, TaskPriority, TaskStatus } from \"./task-list\";\n\nexport interface TaskBoardProps {\n  tasks?: Task[];\n  columns?: Array<{\n    id: TaskStatus;\n    title: string;\n    limit?: number;\n  }>;\n  onTaskMove?: (\n    taskId: string,\n    fromStatus: TaskStatus,\n    toStatus: TaskStatus\n  ) => Promise<void>;\n  onTaskSelect?: (taskId: string) => void;\n  onTaskCreate?: (status: TaskStatus) => void;\n  onTaskUpdate?: (taskId: string, updates: Partial<Task>) => Promise<void>;\n  onTaskDelete?: (taskId: string) => Promise<void>;\n  className?: string;\n  showColumnLimits?: boolean;\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 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\n  const month = date.toLocaleString(\"en-US\", { month: \"short\" });\n  const day = date.getDate();\n  return `${month} ${day}`;\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 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 TaskCardProps {\n  task: Task;\n  onTaskClick: (taskId: string) => void;\n  onStatusChange: (taskId: string, status: TaskStatus) => void;\n  onDelete: (taskId: string) => void;\n  isDragging?: boolean;\n  onDragStart?: (taskId: string) => void;\n  onDragEnd?: () => void;\n}\n\nfunction TaskCard({\n  task,\n  onTaskClick,\n  onStatusChange,\n  onDelete,\n  isDragging = false,\n  onDragStart,\n  onDragEnd,\n}: TaskCardProps) {\n  const overdue = isOverdue(task.dueDate);\n\n  const handleDragStart = (e: React.DragEvent) => {\n    e.dataTransfer.effectAllowed = \"move\";\n    e.dataTransfer.setData(\"text/plain\", task.id);\n    e.dataTransfer.setData(\"application/task-status\", task.status);\n    onDragStart?.(task.id);\n  };\n\n  const handleDragEnd = () => {\n    onDragEnd?.();\n  };\n\n  return (\n    <div\n      className={cn(\n        \"group relative flex cursor-move flex-col gap-2 rounded-lg border bg-card p-3 shadow-xs transition-all hover:shadow-sm\",\n        isDragging && \"opacity-50\"\n      )}\n      draggable\n      onClick={() => onTaskClick(task.id)}\n      onDragEnd={handleDragEnd}\n      onDragStart={handleDragStart}\n      role=\"button\"\n      tabIndex={0}\n    >\n      <div className=\"flex items-start justify-between gap-2\">\n        <h4 className=\"wrap-break-word min-w-0 flex-1 font-medium text-sm leading-tight\">\n          {task.title}\n        </h4>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild className=\"absolute top-1 right-1\">\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              Move to To Do\n            </DropdownMenuItem>\n            <DropdownMenuItem\n              onClick={() => onStatusChange(task.id, \"in_progress\")}\n            >\n              <Clock className=\"size-4\" />\n              Move to In Progress\n            </DropdownMenuItem>\n            <DropdownMenuItem onClick={() => onStatusChange(task.id, \"done\")}>\n              <CheckCircle2 className=\"size-4\" />\n              Move to 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      {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(\"size-2 rounded-full\", getPriorityColor(task.priority))}\n          title={task.priority}\n        />\n        {task.assignees && task.assignees.length > 0 && (\n          <div className=\"flex -space-x-1.5\">\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 alt={assignee.name} src={assignee.avatar} />\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        )}\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          <Badge className=\"text-xs\" variant=\"outline\">\n            {task.tags[0]}\n            {task.tags.length > 1 && ` +${task.tags.length - 1}`}\n          </Badge>\n        )}\n      </div>\n    </div>\n  );\n}\n\ninterface ColumnProps {\n  column: { id: TaskStatus; title: string; limit?: number };\n  tasks: Task[];\n  taskCount: number;\n  onTaskClick: (taskId: string) => void;\n  onStatusChange: (taskId: string, status: TaskStatus) => void;\n  onDelete: (taskId: string) => void;\n  onCreateTask?: () => void;\n  showColumnLimits?: boolean;\n  isDragOver?: boolean;\n  draggingTaskId?: string | null;\n  onDragOver?: (columnId: TaskStatus) => void;\n  onDragLeave?: () => void;\n  onTaskDragStart?: (taskId: string) => void;\n  onTaskDragEnd?: () => void;\n}\n\nfunction Column({\n  column,\n  tasks,\n  taskCount,\n  onTaskClick,\n  onStatusChange,\n  onDelete,\n  onCreateTask,\n  showColumnLimits,\n  isDragOver = false,\n  draggingTaskId,\n  onDragOver,\n  onDragLeave,\n  onTaskDragStart,\n  onTaskDragEnd,\n}: ColumnProps) {\n  const isAtLimit = column.limit !== undefined && taskCount >= column.limit;\n\n  const handleDragOver = (e: React.DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    e.dataTransfer.dropEffect = \"move\";\n    onDragOver?.(column.id);\n  };\n\n  const handleDrop = (e: React.DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n\n    const taskId = e.dataTransfer.getData(\"text/plain\");\n    const fromStatus = e.dataTransfer.getData(\n      \"application/task-status\"\n    ) as TaskStatus;\n\n    if (taskId && fromStatus !== column.id) {\n      onStatusChange(taskId, column.id);\n    }\n    onDragLeave?.();\n  };\n\n  const handleDragEnter = (e: React.DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    onDragOver?.(column.id);\n  };\n\n  const handleDragLeave = (e: React.DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    // Only clear if we're actually leaving the column\n    const rect = e.currentTarget.getBoundingClientRect();\n    const x = e.clientX;\n    const y = e.clientY;\n    if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {\n      onDragLeave?.();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        \"flex min-w-0 flex-1 flex-col gap-3 rounded-lg transition-colors\",\n        isDragOver && \"bg-primary/5 ring-2 ring-primary ring-offset-2\"\n      )}\n      onDragEnter={handleDragEnter}\n      onDragLeave={handleDragLeave}\n      onDragOver={handleDragOver}\n      onDrop={handleDrop}\n    >\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-2\">\n          <h3 className=\"font-semibold text-sm\">{column.title}</h3>\n          <Badge className=\"text-xs\" variant=\"secondary\">\n            {taskCount}\n            {column.limit !== undefined && ` / ${column.limit}`}\n          </Badge>\n        </div>\n        {onCreateTask && (\n          <Button\n            onClick={onCreateTask}\n            size=\"icon\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            <Plus className=\"size-4\" />\n          </Button>\n        )}\n      </div>\n      {showColumnLimits && isAtLimit && (\n        <div className=\"rounded-md border border-yellow-500/50 bg-yellow-500/10 p-2\">\n          <p className=\"text-xs text-yellow-700 dark:text-yellow-400\">\n            Column limit reached\n          </p>\n        </div>\n      )}\n      <div className=\"flex flex-1 flex-col gap-2 overflow-y-auto\">\n        {tasks.length === 0 ? (\n          <div className=\"flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed p-8 text-center\">\n            <Circle className=\"size-6 text-muted-foreground\" />\n            <p className=\"text-muted-foreground text-xs\">No tasks</p>\n          </div>\n        ) : (\n          tasks.map((task) => (\n            <TaskCard\n              isDragging={draggingTaskId === task.id}\n              key={task.id}\n              onDelete={onDelete}\n              onDragEnd={onTaskDragEnd}\n              onDragStart={onTaskDragStart}\n              onStatusChange={onStatusChange}\n              onTaskClick={onTaskClick}\n              task={task}\n            />\n          ))\n        )}\n      </div>\n    </div>\n  );\n}\n\nconst defaultColumns = [\n  { id: \"todo\" as TaskStatus, title: \"To Do\" },\n  { id: \"in_progress\" as TaskStatus, title: \"In Progress\" },\n  { id: \"done\" as TaskStatus, title: \"Done\" },\n];\n\nexport default function TaskBoard({\n  tasks = [],\n  columns = defaultColumns,\n  onTaskMove,\n  onTaskSelect,\n  onTaskCreate,\n  onTaskUpdate,\n  onTaskDelete,\n  className,\n  showColumnLimits = false,\n}: TaskBoardProps) {\n  const [draggingTaskId, setDraggingTaskId] = useState<string | null>(null);\n  const [dragOverColumn, setDragOverColumn] = useState<TaskStatus | null>(null);\n\n  const tasksByStatus = useMemo(() => {\n    const grouped: Record<TaskStatus, Task[]> = {\n      todo: [],\n      in_progress: [],\n      done: [],\n      cancelled: [],\n    };\n\n    tasks.forEach((task) => {\n      if (grouped[task.status]) {\n        grouped[task.status].push(task);\n      }\n    });\n\n    return grouped;\n  }, [tasks]);\n\n  const handleStatusChange = async (taskId: string, newStatus: TaskStatus) => {\n    const task = tasks.find((t) => t.id === taskId);\n    if (!task) return;\n\n    await onTaskMove?.(taskId, task.status, newStatus);\n    await onTaskUpdate?.(taskId, { status: newStatus });\n  };\n\n  const handleDelete = async (taskId: string) => {\n    await onTaskDelete?.(taskId);\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>Task Board</CardTitle>\n          <CardDescription>\n            {tasks.length} task{tasks.length !== 1 ? \"s\" : \"\"} across{\" \"}\n            {columns.length} columns\n          </CardDescription>\n        </div>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex gap-4 overflow-x-auto pb-2\">\n          {columns.map((column) => {\n            const columnTasks = tasksByStatus[column.id] || [];\n            return (\n              <Column\n                column={column}\n                draggingTaskId={draggingTaskId}\n                isDragOver={dragOverColumn === column.id}\n                key={column.id}\n                onCreateTask={\n                  onTaskCreate ? () => onTaskCreate(column.id) : undefined\n                }\n                onDelete={handleDelete}\n                onDragLeave={() => setDragOverColumn(null)}\n                onDragOver={(columnId) => setDragOverColumn(columnId)}\n                onStatusChange={handleStatusChange}\n                onTaskClick={onTaskSelect || (() => {})}\n                onTaskDragEnd={() => {\n                  setDraggingTaskId(null);\n                  setDragOverColumn(null);\n                }}\n                onTaskDragStart={(taskId) => setDraggingTaskId(taskId)}\n                showColumnLimits={showColumnLimits}\n                taskCount={columnTasks.length}\n                tasks={columnTasks}\n              />\n            );\n          })}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "tasks"
  ],
  "type": "registry:ui"
}