{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kanban-2",
  "title": "Single In Progress Column",
  "description": "Single In Progress kanban column of sortable rich task cards showing priority, labels, a due date, and a stacked assignee avatar group.",
  "dependencies": [
    "@dnd-kit/core",
    "@dnd-kit/sortable",
    "@dnd-kit/utilities",
    "@base-ui/react"
  ],
  "registryDependencies": [
    "avatar",
    "badge",
    "card",
    "dialog",
    "scroll-area",
    "separator"
  ],
  "files": [
    {
      "path": "registry/blocks/kanban/2/kanban-block.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  DndContext,\n  DragOverlay,\n  KeyboardSensor,\n  PointerSensor,\n  closestCenter,\n  useSensor,\n  useSensors,\n  type DragEndEvent,\n  type DragStartEvent,\n} from \"@dnd-kit/core\"\nimport {\n  SortableContext,\n  arrayMove,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarGroup,\n  AvatarGroupCount,\n  AvatarImage,\n} from \"@/components/ui/avatar\"\nimport { Badge } from \"@/components/ui/badge\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport { ScrollArea } from \"@/components/ui/scroll-area\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { IconPlaceholder } from \"@/components/icons/icon-placeholder\"\n\n/** Props a call site may pass through to an icon. */\ntype IconProps = { className?: string; size?: number | string }\n\ntype Priority = \"critical\" | \"high\" | \"medium\" | \"low\"\ntype LabelTag = \"Frontend\" | \"Backend\" | \"Design\" | \"API\" | \"DevOps\" | \"QA\"\n\ntype Assignee = {\n  name: string\n  initials: string\n}\n\ntype Task = {\n  id: string\n  title: string\n  description: string\n  priority: Priority\n  labels: LabelTag[]\n  dueDate: string\n  assignees: Assignee[]\n  commentCount: number\n}\n\nconst priorityConfig: Record<\n  Priority,\n  {\n    label: string\n    icon: React.ElementType\n    variant: \"destructive\" | \"default\" | \"secondary\" | \"outline\"\n    dot: string\n  }\n> = {\n  critical: {\n    label: \"Critical\",\n    icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"CircleAlert\"\n        tabler=\"IconAlertCircle\"\n        hugeicons=\"AlertCircleIcon\"\n        phosphor=\"WarningCircle\"\n        remixicon=\"RiErrorWarningLine\"\n        {...p}\n      />\n    ),\n    variant: \"destructive\",\n    dot: \"bg-destructive\",\n  },\n  high: {\n    label: \"High\",\n    icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"Flashlight\"\n        tabler=\"IconBulb\"\n        hugeicons=\"FlashlightIcon\"\n        phosphor=\"Flashlight\"\n        remixicon=\"RiFlashlightLine\"\n        {...p}\n      />\n    ),\n    variant: \"default\",\n    dot: \"bg-primary\",\n  },\n  medium: {\n    label: \"Medium\",\n    icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"Clock\"\n        tabler=\"IconClock\"\n        hugeicons=\"TimeIcon\"\n        phosphor=\"Clock\"\n        remixicon=\"RiTimeLine\"\n        {...p}\n      />\n    ),\n    variant: \"secondary\",\n    dot: \"bg-muted-foreground\",\n  },\n  low: {\n    label: \"Low\",\n    icon: (p: IconProps) => (\n      <IconPlaceholder\n        lucide=\"CircleCheck\"\n        tabler=\"IconCircleCheck\"\n        hugeicons=\"CheckmarkCircle02Icon\"\n        phosphor=\"CheckCircle\"\n        remixicon=\"RiCheckboxCircleLine\"\n        {...p}\n      />\n    ),\n    variant: \"outline\",\n    dot: \"bg-border\",\n  },\n}\n\nconst labelVariant: Record<LabelTag, \"outline\" | \"secondary\"> = {\n  Frontend: \"outline\",\n  Backend: \"secondary\",\n  Design: \"outline\",\n  API: \"secondary\",\n  DevOps: \"secondary\",\n  QA: \"outline\",\n}\n\nconst assigneeAvatars: Record<string, string> = {\n  \"Sam Rivera\": \"https://i.pravatar.cc/150?img=15\",\n  \"Jordan Kim\": \"https://i.pravatar.cc/150?img=8\",\n  \"Morgan Lee\": \"https://i.pravatar.cc/150?img=26\",\n  \"Taylor Obi\": \"https://i.pravatar.cc/150?img=59\",\n  \"Jamie Park\": \"https://i.pravatar.cc/150?img=12\",\n  \"Alex Chen\": \"https://i.pravatar.cc/150?img=33\",\n}\n\nconst initialTasks: Task[] = [\n  {\n    id: \"ACM-412\",\n    title: \"Resolve CORS error on the upload endpoint\",\n    description:\n      \"POST /api/v2/files returns 403 for cross-origin requests; preflight OPTIONS never reaches the handler.\",\n    priority: \"critical\",\n    labels: [\"Backend\", \"API\"],\n    dueDate: \"Jun 18\",\n    assignees: [\n      { name: \"Sam Rivera\", initials: \"SR\" },\n      { name: \"Jordan Kim\", initials: \"JK\" },\n    ],\n    commentCount: 7,\n  },\n  {\n    id: \"ACM-398\",\n    title: \"Redesign the account settings page\",\n    description:\n      \"Consolidate billing, profile, and notification preferences into a single tabbed layout using the new design tokens.\",\n    priority: \"high\",\n    labels: [\"Frontend\", \"Design\"],\n    dueDate: \"Jun 23\",\n    assignees: [\n      { name: \"Morgan Lee\", initials: \"ML\" },\n      { name: \"Taylor Obi\", initials: \"TO\" },\n      { name: \"Jamie Park\", initials: \"JP\" },\n    ],\n    commentCount: 4,\n  },\n  {\n    id: \"ACM-385\",\n    title: \"Add rate limiting to the public REST API\",\n    description:\n      \"Implement a sliding-window limiter (100 req/min per key) with proper Retry-After headers on 429 responses.\",\n    priority: \"high\",\n    labels: [\"Backend\", \"API\", \"DevOps\"],\n    dueDate: \"Jun 26\",\n    assignees: [{ name: \"Alex Chen\", initials: \"AC\" }],\n    commentCount: 2,\n  },\n  {\n    id: \"ACM-371\",\n    title: \"Write end-to-end tests for the checkout flow\",\n    description:\n      \"Cover happy path, declined card, and coupon-code scenarios using Playwright against the staging environment.\",\n    priority: \"medium\",\n    labels: [\"QA\", \"Frontend\"],\n    dueDate: \"Jul 2\",\n    assignees: [\n      { name: \"Sam Rivera\", initials: \"SR\" },\n      { name: \"Morgan Lee\", initials: \"ML\" },\n    ],\n    commentCount: 0,\n  },\n  {\n    id: \"ACM-360\",\n    title: \"Document the webhook payload schema\",\n    description:\n      \"Add OpenAPI 3.1 schemas for all event types and publish the spec to the developer portal.\",\n    priority: \"low\",\n    labels: [\"Backend\", \"API\"],\n    dueDate: \"Jul 8\",\n    assignees: [{ name: \"Jamie Park\", initials: \"JP\" }],\n    commentCount: 1,\n  },\n]\n\nfunction TaskCardBody({\n  task,\n  dragHandle,\n  dragging,\n}: {\n  task: Task\n  dragHandle?: React.ReactNode\n  dragging?: boolean\n}) {\n  const pCfg = priorityConfig[task.priority]\n  const PriorityIcon = pCfg.icon\n  const visibleAssignees = task.assignees.slice(0, 3)\n  const overflow = task.assignees.length - visibleAssignees.length\n\n  return (\n    <Card\n      className={`group border-border/80 transition-shadow duration-150 hover:shadow-md ${\n        dragging ? \"shadow-lg ring-1 ring-foreground/15\" : \"\"\n      }`}\n    >\n      <CardHeader className=\"pb-2\">\n        <div className=\"flex items-center gap-2 pb-1\">\n          <span className={`size-1.5 shrink-0 ${pCfg.dot}`} />\n          <span className=\"font-mono text-[10px] font-medium tracking-wide text-muted-foreground/70 uppercase\">\n            {task.id}\n          </span>\n          {dragHandle ?? (\n            <IconPlaceholder\n              lucide=\"GripVertical\"\n              tabler=\"IconGripVertical\"\n              hugeicons=\"Drag01Icon\"\n              phosphor=\"DotsSixVertical\"\n              remixicon=\"RiDraggable\"\n              className=\"ml-auto size-3.5 text-muted-foreground/40\"\n            />\n          )}\n        </div>\n        <CardTitle className=\"text-sm leading-snug font-medium\">\n          {task.title}\n        </CardTitle>\n        <CardDescription className=\"line-clamp-2 text-xs leading-relaxed\">\n          {task.description}\n        </CardDescription>\n      </CardHeader>\n\n      <CardContent className=\"pt-0\">\n        <div className=\"flex flex-wrap gap-1.5 pb-3.5\">\n          <Badge variant={pCfg.variant} className=\"gap-1\">\n            <PriorityIcon data-icon=\"inline-start\" />\n            {pCfg.label}\n          </Badge>\n          {task.labels.map((lbl) => (\n            <Badge\n              key={lbl}\n              variant={labelVariant[lbl]}\n              className=\"font-normal\"\n            >\n              {lbl}\n            </Badge>\n          ))}\n        </div>\n\n        <Separator className=\"mb-3.5 opacity-50\" />\n\n        <div className=\"flex items-center justify-between gap-3\">\n          <div className=\"flex items-center gap-3\">\n            <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n              <IconPlaceholder\n                lucide=\"Calendar\"\n                tabler=\"IconCalendar\"\n                hugeicons=\"CalendarIcon\"\n                phosphor=\"Calendar\"\n                remixicon=\"RiCalendarLine\"\n                className=\"size-3.5 shrink-0\"\n              />\n              <span className=\"tabular-nums\">{task.dueDate}</span>\n            </div>\n            {task.commentCount > 0 && (\n              <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n                <IconPlaceholder\n                  lucide=\"MessageCircle\"\n                  tabler=\"IconMessageCircle\"\n                  hugeicons=\"Message01Icon\"\n                  phosphor=\"ChatCircle\"\n                  remixicon=\"RiChat3Line\"\n                  className=\"size-3.5 shrink-0\"\n                />\n                <span className=\"tabular-nums\">{task.commentCount}</span>\n              </div>\n            )}\n          </div>\n\n          <AvatarGroup>\n            {visibleAssignees.map((a) => (\n              <Avatar key={a.name} size=\"sm\">\n                <AvatarImage\n                  src={assigneeAvatars[a.name]}\n                  alt={a.name}\n                  className=\"grayscale\"\n                />\n                <AvatarFallback>{a.initials}</AvatarFallback>\n              </Avatar>\n            ))}\n            {overflow > 0 && <AvatarGroupCount>+{overflow}</AvatarGroupCount>}\n          </AvatarGroup>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n\nfunction SortableTaskCard({\n  task,\n  onOpen,\n}: {\n  task: Task\n  onOpen: (task: Task) => void\n}) {\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    setActivatorNodeRef,\n    transform,\n    transition,\n    isDragging,\n  } = useSortable({ id: task.id })\n\n  const style: React.CSSProperties = {\n    transform: CSS.Transform.toString(transform),\n    transition,\n    opacity: isDragging ? 0 : 1,\n  }\n\n  return (\n    <div ref={setNodeRef} style={style} className=\"relative\">\n      <button\n        type=\"button\"\n        className=\"block w-full cursor-pointer text-left outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n        aria-label={`Open task ${task.id}: ${task.title}`}\n        onClick={() => onOpen(task)}\n      >\n        <TaskCardBody\n          task={task}\n          dragHandle={\n            <span\n              ref={setActivatorNodeRef}\n              {...attributes}\n              {...listeners}\n              role=\"button\"\n              tabIndex={0}\n              aria-label={`Reorder task ${task.id}`}\n              className=\"-my-1 ml-auto flex cursor-grab touch-none items-center justify-center p-1 text-muted-foreground/50 outline-none hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing\"\n              onClick={(e) => e.stopPropagation()}\n            >\n              <IconPlaceholder\n                lucide=\"GripVertical\"\n                tabler=\"IconGripVertical\"\n                hugeicons=\"Drag01Icon\"\n                phosphor=\"DotsSixVertical\"\n                remixicon=\"RiDraggable\"\n                className=\"size-3.5\"\n              />\n            </span>\n          }\n        />\n      </button>\n    </div>\n  )\n}\n\nexport default function KanbanBlock() {\n  const [tasks, setTasks] = React.useState<Task[]>(initialTasks)\n  const [activeId, setActiveId] = React.useState<string | null>(null)\n  const [detailTask, setDetailTask] = React.useState<Task | null>(null)\n  const [detailOpen, setDetailOpen] = React.useState(false)\n\n  const sensors = useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    })\n  )\n\n  const activeTask = activeId\n    ? (tasks.find((t) => t.id === activeId) ?? null)\n    : null\n\n  function handleDragStart(event: DragStartEvent) {\n    setActiveId(String(event.active.id))\n  }\n\n  function handleDragEnd(event: DragEndEvent) {\n    const { active, over } = event\n    setActiveId(null)\n    if (!over || active.id === over.id) return\n    setTasks((prev) => {\n      const oldIndex = prev.findIndex((t) => t.id === active.id)\n      const newIndex = prev.findIndex((t) => t.id === over.id)\n      if (oldIndex < 0 || newIndex < 0) return prev\n      return arrayMove(prev, oldIndex, newIndex)\n    })\n  }\n\n  function openDetail(task: Task) {\n    setDetailTask(task)\n    setDetailOpen(true)\n  }\n\n  return (\n    <section className=\"flex w-full items-center justify-center bg-background px-6 py-12 text-foreground\">\n      <div className=\"w-full max-w-xl\">\n        <div className=\"flex items-center gap-3 px-1 pb-5\">\n          <div className=\"flex shrink-0 items-center gap-2.5\">\n            <span className=\"text-sm font-semibold tracking-tight\">\n              In Progress\n            </span>\n            <Badge\n              variant=\"secondary\"\n              className=\"px-1.5 font-medium tabular-nums\"\n            >\n              {tasks.length}\n            </Badge>\n          </div>\n          <Separator orientation=\"horizontal\" className=\"flex-1 opacity-60\" />\n        </div>\n\n        <DndContext\n          id=\"kanban-2-board\"\n          sensors={sensors}\n          collisionDetection={closestCenter}\n          onDragStart={handleDragStart}\n          onDragEnd={handleDragEnd}\n          onDragCancel={() => setActiveId(null)}\n        >\n          <SortableContext\n            items={tasks.map((t) => t.id)}\n            strategy={verticalListSortingStrategy}\n          >\n            <ScrollArea className=\"h-[34rem] [&_[data-slot=scroll-area-viewport]]:scroll-fade-y\">\n              <div className=\"flex flex-col gap-2.5 p-1 pr-2.5\">\n                {tasks.map((task) => (\n                  <SortableTaskCard\n                    key={task.id}\n                    task={task}\n                    onOpen={openDetail}\n                  />\n                ))}\n              </div>\n            </ScrollArea>\n          </SortableContext>\n\n          <DragOverlay>\n            {activeTask ? (\n              <div className=\"cursor-grabbing\">\n                <TaskCardBody task={activeTask} dragging />\n              </div>\n            ) : null}\n          </DragOverlay>\n        </DndContext>\n      </div>\n\n      <Dialog open={detailOpen} onOpenChange={setDetailOpen}>\n        <DialogContent>\n          {detailTask && (\n            <>\n              <DialogHeader>\n                <div className=\"mb-1 flex items-center gap-2\">\n                  <span\n                    className={`size-1.5 shrink-0 ${priorityConfig[detailTask.priority].dot}`}\n                  />\n                  <span className=\"font-mono text-[10px] font-medium tracking-wide text-muted-foreground/70 uppercase\">\n                    {detailTask.id}\n                  </span>\n                </div>\n                <DialogTitle>{detailTask.title}</DialogTitle>\n                <DialogDescription>{detailTask.description}</DialogDescription>\n              </DialogHeader>\n\n              <div className=\"flex flex-wrap gap-1.5\">\n                <Badge\n                  variant={priorityConfig[detailTask.priority].variant}\n                  className=\"gap-1\"\n                >\n                  {React.createElement(\n                    priorityConfig[detailTask.priority].icon,\n                    {\n                      \"data-icon\": \"inline-start\",\n                    }\n                  )}\n                  {priorityConfig[detailTask.priority].label}\n                </Badge>\n                {detailTask.labels.map((lbl) => (\n                  <Badge\n                    key={lbl}\n                    variant={labelVariant[lbl]}\n                    className=\"font-normal\"\n                  >\n                    {lbl}\n                  </Badge>\n                ))}\n              </div>\n\n              <Separator className=\"opacity-50\" />\n\n              <div className=\"flex items-center justify-between gap-3\">\n                <div className=\"flex items-center gap-3\">\n                  <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n                    <IconPlaceholder\n                      lucide=\"Calendar\"\n                      tabler=\"IconCalendar\"\n                      hugeicons=\"CalendarIcon\"\n                      phosphor=\"Calendar\"\n                      remixicon=\"RiCalendarLine\"\n                      className=\"size-3.5 shrink-0\"\n                    />\n                    <span className=\"tabular-nums\">{detailTask.dueDate}</span>\n                  </div>\n                  {detailTask.commentCount > 0 && (\n                    <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground\">\n                      <IconPlaceholder\n                        lucide=\"MessageCircle\"\n                        tabler=\"IconMessageCircle\"\n                        hugeicons=\"Message01Icon\"\n                        phosphor=\"ChatCircle\"\n                        remixicon=\"RiChat3Line\"\n                        className=\"size-3.5 shrink-0\"\n                      />\n                      <span className=\"tabular-nums\">\n                        {detailTask.commentCount}\n                      </span>\n                    </div>\n                  )}\n                </div>\n\n                <AvatarGroup>\n                  {detailTask.assignees.slice(0, 3).map((a) => (\n                    <Avatar key={a.name} size=\"sm\">\n                      <AvatarImage\n                        src={assigneeAvatars[a.name]}\n                        alt={a.name}\n                        className=\"grayscale\"\n                      />\n                      <AvatarFallback>{a.initials}</AvatarFallback>\n                    </Avatar>\n                  ))}\n                  {detailTask.assignees.length > 3 && (\n                    <AvatarGroupCount>\n                      +{detailTask.assignees.length - 3}\n                    </AvatarGroupCount>\n                  )}\n                </AvatarGroup>\n              </div>\n\n              <DialogFooter showCloseButton />\n            </>\n          )}\n        </DialogContent>\n      </Dialog>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/kanban-2.tsx"
    }
  ],
  "meta": {
    "height": "688px",
    "tier": "free"
  },
  "categories": [
    "kanban"
  ],
  "type": "registry:block"
}