{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kanban-1",
  "title": "Three Column Task Board",
  "description": "Kanban board with To Do, In Progress, and Done columns, drag and drop between them, and cards that open a task detail dialog.",
  "dependencies": [
    "@dnd-kit/core",
    "@dnd-kit/sortable",
    "@dnd-kit/utilities",
    "@base-ui/react"
  ],
  "registryDependencies": [
    "avatar",
    "badge",
    "card",
    "dialog",
    "scroll-area"
  ],
  "files": [
    {
      "path": "registry/blocks/kanban/1/kanban-block.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  DndContext,\n  DragOverlay,\n  KeyboardSensor,\n  PointerSensor,\n  closestCorners,\n  useDroppable,\n  useSensor,\n  useSensors,\n  type DragEndEvent,\n  type DragOverEvent,\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\"\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Card, CardContent, CardHeader, CardTitle } 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\"\n\ntype TaskLabel = \"Bug\" | \"Feature\" | \"Docs\" | \"Design\" | \"Infra\" | \"Research\"\n\nconst labelVariant: Record<\n  TaskLabel,\n  \"default\" | \"secondary\" | \"outline\" | \"destructive\"\n> = {\n  Bug: \"destructive\",\n  Feature: \"default\",\n  Docs: \"secondary\",\n  Design: \"secondary\",\n  Infra: \"outline\",\n  Research: \"outline\",\n}\n\ntype Task = {\n  id: string\n  title: string\n  label: TaskLabel\n  assignee: string\n  initials: string\n  avatarSrc: string\n  priority: \"high\" | \"medium\" | \"low\"\n}\n\ntype Column = {\n  id: string\n  title: string\n  accent: string\n  tasks: Task[]\n}\n\nconst initialColumns: Column[] = [\n  {\n    id: \"todo\",\n    title: \"To Do\",\n    accent: \"bg-muted-foreground/20\",\n    tasks: [\n      {\n        id: \"t1\",\n        title: \"Audit accessibility across all form components\",\n        label: \"Docs\",\n        assignee: \"Morgan Lee\",\n        initials: \"ML\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=47\",\n        priority: \"medium\",\n      },\n      {\n        id: \"t2\",\n        title: \"Investigate memory leak in background worker\",\n        label: \"Bug\",\n        assignee: \"Sam Rivera\",\n        initials: \"SR\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=12\",\n        priority: \"high\",\n      },\n      {\n        id: \"t3\",\n        title: \"Add dark-mode support to email templates\",\n        label: \"Design\",\n        assignee: \"Jamie Park\",\n        initials: \"JP\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=33\",\n        priority: \"low\",\n      },\n      {\n        id: \"t4\",\n        title: \"Define quarterly OKRs with product team\",\n        label: \"Research\",\n        assignee: \"Alex Chen\",\n        initials: \"AC\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=68\",\n        priority: \"medium\",\n      },\n    ],\n  },\n  {\n    id: \"in-progress\",\n    title: \"In Progress\",\n    accent: \"bg-primary\",\n    tasks: [\n      {\n        id: \"t5\",\n        title: \"Migrate CI/CD pipeline to GitHub Actions\",\n        label: \"Infra\",\n        assignee: \"Jordan Kim\",\n        initials: \"JK\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=51\",\n        priority: \"high\",\n      },\n      {\n        id: \"t6\",\n        title: \"Build multi-step onboarding wizard\",\n        label: \"Feature\",\n        assignee: \"Taylor Obi\",\n        initials: \"TO\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=22\",\n        priority: \"high\",\n      },\n      {\n        id: \"t7\",\n        title: \"Fix pagination on the invoices list view\",\n        label: \"Bug\",\n        assignee: \"Sam Rivera\",\n        initials: \"SR\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=12\",\n        priority: \"medium\",\n      },\n    ],\n  },\n  {\n    id: \"done\",\n    title: \"Done\",\n    accent: \"bg-muted-foreground/30\",\n    tasks: [\n      {\n        id: \"t8\",\n        title: \"Set up error monitoring with Sentry\",\n        label: \"Infra\",\n        assignee: \"Alex Chen\",\n        initials: \"AC\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=68\",\n        priority: \"medium\",\n      },\n      {\n        id: \"t9\",\n        title: \"Write API reference for webhooks endpoint\",\n        label: \"Docs\",\n        assignee: \"Jamie Park\",\n        initials: \"JP\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=33\",\n        priority: \"low\",\n      },\n      {\n        id: \"t10\",\n        title: \"Redesign settings page layout\",\n        label: \"Design\",\n        assignee: \"Morgan Lee\",\n        initials: \"ML\",\n        avatarSrc: \"https://i.pravatar.cc/32?img=47\",\n        priority: \"low\",\n      },\n    ],\n  },\n]\n\nconst priorityDot: Record<Task[\"priority\"], string> = {\n  high: \"bg-destructive\",\n  medium: \"bg-primary\",\n  low: \"bg-muted-foreground/40\",\n}\n\nconst priorityLabel: Record<Task[\"priority\"], string> = {\n  high: \"High priority\",\n  medium: \"Medium priority\",\n  low: \"Low priority\",\n}\n\nfunction TaskCardBody({\n  task,\n  dimmed,\n  dragging,\n}: {\n  task: Task\n  dimmed?: boolean\n  dragging?: boolean\n}) {\n  return (\n    <Card\n      size=\"sm\"\n      className={`transition-shadow duration-150 hover:shadow-md ${\n        dimmed ? \"opacity-60 transition-opacity hover:opacity-100\" : \"\"\n      }${dragging ? \"shadow-lg ring-1 ring-foreground/15\" : \"\"}`}\n    >\n      <CardHeader>\n        <div className=\"flex items-start gap-2\">\n          <span\n            className={`mt-1.5 size-1.5 shrink-0 rounded-full ${priorityDot[task.priority]}`}\n            role=\"img\"\n            aria-label={priorityLabel[task.priority]}\n          />\n          <CardTitle className=\"text-sm leading-snug font-medium\">\n            {task.title}\n          </CardTitle>\n        </div>\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex items-center justify-between gap-2\">\n          <Badge variant={labelVariant[task.label]}>{task.label}</Badge>\n          <div className=\"flex items-center gap-1.5\">\n            <span className=\"truncate text-xs text-muted-foreground\">\n              {task.assignee}\n            </span>\n            <Avatar size=\"sm\">\n              <AvatarImage\n                src={task.avatarSrc}\n                alt={task.assignee}\n                className=\"grayscale\"\n              />\n              <AvatarFallback>{task.initials}</AvatarFallback>\n            </Avatar>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  )\n}\n\nfunction SortableTaskCard({\n  task,\n  dimmed,\n  onOpen,\n}: {\n  task: Task\n  dimmed?: boolean\n  onOpen: (task: Task) => void\n}) {\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\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  const movedRef = React.useRef(false)\n\n  return (\n    <div\n      ref={setNodeRef}\n      style={style}\n      {...attributes}\n      {...listeners}\n      role=\"button\"\n      tabIndex={0}\n      aria-label={`Open task: ${task.title}`}\n      className=\"cursor-grab touch-none outline-none focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing\"\n      onPointerDownCapture={() => {\n        movedRef.current = false\n      }}\n      onPointerMoveCapture={() => {\n        movedRef.current = true\n      }}\n      onClick={() => {\n        if (!movedRef.current) onOpen(task)\n      }}\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\") {\n          e.preventDefault()\n          onOpen(task)\n        }\n      }}\n    >\n      <TaskCardBody task={task} dimmed={dimmed} />\n    </div>\n  )\n}\n\nfunction BoardColumn({\n  column,\n  onOpen,\n  reflowKey,\n}: {\n  column: Column\n  onOpen: (task: Task) => void\n  reflowKey: number\n}) {\n  const { setNodeRef, isOver } = useDroppable({ id: column.id })\n\n  return (\n    <div className=\"flex w-[78%] shrink-0 flex-col gap-3 sm:w-auto sm:shrink\">\n      <div className=\"flex items-center gap-2.5\">\n        <span className={`size-2 ${column.accent}`} />\n        <span className=\"text-xs font-semibold tracking-wider text-muted-foreground uppercase\">\n          {column.title}\n        </span>\n        <span className=\"ml-auto flex size-5 items-center justify-center rounded-md border border-border text-[10px] font-semibold text-muted-foreground tabular-nums\">\n          {column.tasks.length}\n        </span>\n      </div>\n\n      <div className={`h-px w-full ${column.accent} opacity-60`} />\n\n      <SortableContext\n        items={column.tasks.map((t) => t.id)}\n        strategy={verticalListSortingStrategy}\n      >\n        <ScrollArea\n          key={reflowKey}\n          className={`h-80 transition-colors [&_[data-slot=scroll-area-viewport]]:scroll-fade-y ${isOver ? \"bg-muted/40\" : \"\"}`}\n        >\n          <div\n            ref={setNodeRef}\n            className=\"flex min-h-full flex-col gap-2 p-0.5 pr-2.5\"\n          >\n            {column.tasks.map((task) => (\n              <SortableTaskCard\n                key={task.id}\n                task={task}\n                dimmed={column.id === \"done\"}\n                onOpen={onOpen}\n              />\n            ))}\n            {column.tasks.length === 0 && (\n              <div className=\"flex flex-1 items-center justify-center rounded-lg border border-dashed border-border py-8 text-xs text-muted-foreground\">\n                Drop here\n              </div>\n            )}\n          </div>\n        </ScrollArea>\n      </SortableContext>\n    </div>\n  )\n}\n\nexport default function KanbanBlock() {\n  const [columns, setColumns] = React.useState<Column[]>(initialColumns)\n  const [activeTask, setActiveTask] = React.useState<Task | null>(null)\n  const [detailTask, setDetailTask] = React.useState<Task | null>(null)\n  const [detailOpen, setDetailOpen] = React.useState(false)\n  const [reflow, setReflow] = React.useState(0)\n\n  const sensors = useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    })\n  )\n\n  const findColumnId = React.useCallback(\n    (id: string): string | undefined => {\n      if (columns.some((c) => c.id === id)) return id\n      return columns.find((c) => c.tasks.some((t) => t.id === id))?.id\n    },\n    [columns]\n  )\n\n  const totalTasks = columns.reduce((n, c) => n + c.tasks.length, 0)\n  const doneCount = columns.find((c) => c.id === \"done\")?.tasks.length ?? 0\n\n  function handleDragStart(event: DragStartEvent) {\n    const id = String(event.active.id)\n    const task = columns.flatMap((c) => c.tasks).find((t) => t.id === id)\n    setActiveTask(task ?? null)\n  }\n\n  function handleDragOver(event: DragOverEvent) {\n    const { active, over } = event\n    if (!over) return\n    const activeId = String(active.id)\n    const overId = String(over.id)\n\n    const activeCol = findColumnId(activeId)\n    const overCol = findColumnId(overId)\n    if (!activeCol || !overCol || activeCol === overCol) return\n\n    setColumns((prev) => {\n      const from = prev.find((c) => c.id === activeCol)!\n      const to = prev.find((c) => c.id === overCol)!\n      const moving = from.tasks.find((t) => t.id === activeId)\n      if (!moving) return prev\n\n      const overIndex = to.tasks.findIndex((t) => t.id === overId)\n      const insertAt = overIndex >= 0 ? overIndex : to.tasks.length\n\n      return prev.map((c) => {\n        if (c.id === activeCol) {\n          return { ...c, tasks: c.tasks.filter((t) => t.id !== activeId) }\n        }\n        if (c.id === overCol) {\n          const next = [...c.tasks]\n          next.splice(insertAt, 0, moving)\n          return { ...c, tasks: next }\n        }\n        return c\n      })\n    })\n  }\n\n  function handleDragEnd(event: DragEndEvent) {\n    const { active, over } = event\n    setActiveTask(null)\n    setReflow((n) => n + 1)\n    if (!over) return\n\n    const activeId = String(active.id)\n    const overId = String(over.id)\n    const activeCol = findColumnId(activeId)\n    const overCol = findColumnId(overId)\n    if (!activeCol || !overCol) return\n\n    if (activeCol === overCol) {\n      setColumns((prev) =>\n        prev.map((c) => {\n          if (c.id !== activeCol) return c\n          const oldIndex = c.tasks.findIndex((t) => t.id === activeId)\n          const newIndex = c.tasks.findIndex((t) => t.id === overId)\n          if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return c\n          return { ...c, tasks: arrayMove(c.tasks, oldIndex, newIndex) }\n        })\n      )\n    }\n  }\n\n  function openDetail(task: Task) {\n    setDetailTask(task)\n    setDetailOpen(true)\n  }\n\n  return (\n    <section className=\"flex min-h-svh w-full items-start justify-center bg-background px-6 py-12 text-foreground\">\n      <div className=\"mx-auto w-full max-w-5xl\">\n        <div className=\"mb-8 flex items-end justify-between border-b border-border pb-5\">\n          <div>\n            <p className=\"mb-1 text-xs font-medium tracking-widest text-muted-foreground uppercase\">\n              Acme Engineering\n            </p>\n            <h1 className=\"font-heading text-2xl font-bold tracking-tight\">\n              Sprint Board\n            </h1>\n          </div>\n          <Badge variant=\"outline\" className=\"mb-0.5\">\n            Q3 Sprint 7\n          </Badge>\n        </div>\n\n        <DndContext\n          id=\"kanban-1-board\"\n          sensors={sensors}\n          collisionDetection={closestCorners}\n          onDragStart={handleDragStart}\n          onDragOver={handleDragOver}\n          onDragEnd={handleDragEnd}\n          onDragCancel={() => {\n            setActiveTask(null)\n            setReflow((n) => n + 1)\n          }}\n        >\n          <div className=\"-mx-1 flex gap-5 overflow-x-auto px-1 pb-3 sm:mx-0 sm:grid sm:grid-cols-3 sm:overflow-visible sm:px-0 sm:pb-0\">\n            {columns.map((col) => (\n              <BoardColumn\n                key={col.id}\n                column={col}\n                onOpen={openDetail}\n                reflowKey={reflow}\n              />\n            ))}\n          </div>\n\n          <DragOverlay>\n            {activeTask ? (\n              <div className=\"cursor-grabbing\">\n                <TaskCardBody task={activeTask} dragging />\n              </div>\n            ) : null}\n          </DragOverlay>\n        </DndContext>\n\n        <div className=\"mt-8 flex items-center gap-3 border-t border-border pt-4\">\n          <div className=\"flex -space-x-2\">\n            {[\n              {\n                src: \"https://i.pravatar.cc/32?img=47\",\n                name: \"Morgan Lee\",\n                initials: \"ML\",\n              },\n              {\n                src: \"https://i.pravatar.cc/32?img=12\",\n                name: \"Sam Rivera\",\n                initials: \"SR\",\n              },\n              {\n                src: \"https://i.pravatar.cc/32?img=33\",\n                name: \"Jamie Park\",\n                initials: \"JP\",\n              },\n              {\n                src: \"https://i.pravatar.cc/32?img=68\",\n                name: \"Alex Chen\",\n                initials: \"AC\",\n              },\n              {\n                src: \"https://i.pravatar.cc/32?img=51\",\n                name: \"Jordan Kim\",\n                initials: \"JK\",\n              },\n            ].map((member) => (\n              <Avatar\n                key={member.name}\n                size=\"sm\"\n                className=\"border-2 border-background\"\n              >\n                <AvatarImage\n                  src={member.src}\n                  alt={member.name}\n                  className=\"grayscale\"\n                />\n                <AvatarFallback>{member.initials}</AvatarFallback>\n              </Avatar>\n            ))}\n          </div>\n          <div className=\"flex items-center gap-x-4 text-xs text-muted-foreground tabular-nums\">\n            <span>\n              <span className=\"font-medium text-foreground\">5</span>{\" \"}\n              Contributors\n            </span>\n            <span>\n              <span className=\"font-medium text-foreground\">{totalTasks}</span>{\" \"}\n              Tasks\n            </span>\n            <span>\n              <span className=\"font-medium text-foreground\">{doneCount}</span>{\" \"}\n              Completed\n            </span>\n          </div>\n        </div>\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 rounded-full ${priorityDot[detailTask.priority]}`}\n                  />\n                  <span className=\"text-[10px] font-semibold tracking-wider text-muted-foreground uppercase\">\n                    {priorityLabel[detailTask.priority]}\n                  </span>\n                </div>\n                <DialogTitle>{detailTask.title}</DialogTitle>\n                <DialogDescription>\n                  Task in the{\" \"}\n                  {columns.find((c) =>\n                    c.tasks.some((t) => t.id === detailTask.id)\n                  )?.title ?? \"board\"}{\" \"}\n                  column. Drag the card to move it across the board.\n                </DialogDescription>\n              </DialogHeader>\n\n              <div className=\"flex items-center justify-between gap-2 border-t border-border pt-3\">\n                <Badge variant={labelVariant[detailTask.label]}>\n                  {detailTask.label}\n                </Badge>\n                <div className=\"flex items-center gap-2\">\n                  <span className=\"text-xs text-muted-foreground\">\n                    {detailTask.assignee}\n                  </span>\n                  <Avatar size=\"sm\">\n                    <AvatarImage\n                      src={detailTask.avatarSrc}\n                      alt={detailTask.assignee}\n                      className=\"grayscale\"\n                    />\n                    <AvatarFallback>{detailTask.initials}</AvatarFallback>\n                  </Avatar>\n                </div>\n              </div>\n\n              <DialogFooter showCloseButton />\n            </>\n          )}\n        </DialogContent>\n      </Dialog>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/kanban-1.tsx"
    }
  ],
  "meta": {
    "height": "647px",
    "tier": "free"
  },
  "categories": [
    "kanban"
  ],
  "type": "registry:block"
}