{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kanban-4",
  "title": "Personal Task Lanes",
  "description": "Personal my-tasks kanban with Today, This Week, and Later lanes of compact rows you drag and drop to reprioritize.",
  "dependencies": [
    "@dnd-kit/core",
    "@dnd-kit/sortable",
    "@dnd-kit/utilities"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/blocks/kanban/4/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\"\nimport { cn } from \"@/lib/utils\"\nimport { IconPlaceholder } from \"@/components/icons/icon-placeholder\"\n\ntype Priority = \"high\" | \"medium\" | \"low\"\n\nconst priorityDot: Record<Priority, string> = {\n  high: \"bg-destructive\",\n  medium: \"bg-primary\",\n  low: \"bg-muted-foreground/40\",\n}\n\ntype Task = { id: string; title: string; project: string; priority: Priority }\ntype Lane = { id: string; title: string; hint: string; tasks: Task[] }\n\nconst initialLanes: Lane[] = [\n  {\n    id: \"today\",\n    title: \"Today\",\n    hint: \"Due by end of day\",\n    tasks: [\n      {\n        id: \"m1\",\n        title: \"Reply to design review comments\",\n        project: \"Web App\",\n        priority: \"high\",\n      },\n      {\n        id: \"m2\",\n        title: \"Finalize sprint retro notes\",\n        project: \"Team\",\n        priority: \"medium\",\n      },\n      {\n        id: \"m3\",\n        title: \"Approve new marketing copy\",\n        project: \"Website\",\n        priority: \"low\",\n      },\n    ],\n  },\n  {\n    id: \"week\",\n    title: \"This Week\",\n    hint: \"Planned for the next few days\",\n    tasks: [\n      {\n        id: \"m4\",\n        title: \"Draft the analytics dashboard spec\",\n        project: \"Web App\",\n        priority: \"medium\",\n      },\n      {\n        id: \"m5\",\n        title: \"Pair on the billing migration\",\n        project: \"Platform\",\n        priority: \"high\",\n      },\n    ],\n  },\n  {\n    id: \"later\",\n    title: \"Later\",\n    hint: \"Backlog, not yet scheduled\",\n    tasks: [\n      {\n        id: \"m6\",\n        title: \"Explore a mobile push strategy\",\n        project: \"Growth\",\n        priority: \"low\",\n      },\n      {\n        id: \"m7\",\n        title: \"Refactor the settings form\",\n        project: \"Web App\",\n        priority: \"low\",\n      },\n    ],\n  },\n]\n\nfunction TaskRowBody({ task, dragging }: { task: Task; dragging?: boolean }) {\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-3 rounded-lg border border-border bg-card px-3 py-2.5 transition-shadow duration-150\",\n        dragging && \"shadow-lg ring-1 ring-foreground/15\"\n      )}\n    >\n      <IconPlaceholder\n        lucide=\"GripVertical\"\n        tabler=\"IconGripVertical\"\n        hugeicons=\"Drag01Icon\"\n        phosphor=\"DotsSixVertical\"\n        remixicon=\"RiDraggable\"\n        className=\"size-4 shrink-0 text-muted-foreground/50\"\n        aria-hidden=\"true\"\n      />\n      <span\n        className={cn(\n          \"size-1.5 shrink-0 rounded-full\",\n          priorityDot[task.priority]\n        )}\n        aria-label={`${task.priority} priority`}\n      />\n      <span className=\"flex-1 truncate text-sm font-medium\">{task.title}</span>\n      <span className=\"hidden shrink-0 text-xs text-muted-foreground sm:inline\">\n        {task.project}\n      </span>\n    </div>\n  )\n}\n\nfunction SortableTaskRow({ task }: { task: Task }) {\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    transform,\n    transition,\n    isDragging,\n  } = useSortable({ id: task.id })\n  return (\n    <div\n      ref={setNodeRef}\n      style={{\n        transform: CSS.Transform.toString(transform),\n        transition,\n        opacity: isDragging ? 0 : 1,\n      }}\n      {...attributes}\n      {...listeners}\n      className=\"cursor-grab touch-none outline-none focus-visible:ring-2 focus-visible:ring-ring active:cursor-grabbing\"\n    >\n      <TaskRowBody task={task} />\n    </div>\n  )\n}\n\nfunction TaskLane({ lane }: { lane: Lane }) {\n  const { setNodeRef, isOver } = useDroppable({ id: lane.id })\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <div className=\"flex items-baseline justify-between\">\n        <h2 className=\"font-heading text-sm font-semibold tracking-tight\">\n          {lane.title}\n        </h2>\n        <span className=\"text-xs text-muted-foreground\">{lane.hint}</span>\n      </div>\n      <SortableContext\n        items={lane.tasks.map((t) => t.id)}\n        strategy={verticalListSortingStrategy}\n      >\n        <div\n          ref={setNodeRef}\n          className={cn(\n            \"flex min-h-16 flex-col gap-2 rounded-lg border border-dashed p-2 transition-colors\",\n            isOver ? \"border-foreground/30 bg-muted/40\" : \"border-border\"\n          )}\n        >\n          {lane.tasks.map((task) => (\n            <SortableTaskRow key={task.id} task={task} />\n          ))}\n          {lane.tasks.length === 0 && (\n            <div className=\"flex flex-1 items-center justify-center py-4 text-xs text-muted-foreground\">\n              Drop a task here\n            </div>\n          )}\n        </div>\n      </SortableContext>\n    </div>\n  )\n}\n\nexport default function KanbanBlock() {\n  const [lanes, setLanes] = React.useState<Lane[]>(initialLanes)\n  const [activeTask, setActiveTask] = React.useState<Task | null>(null)\n\n  const sensors = useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })\n  )\n\n  const findLaneId = React.useCallback(\n    (id: string) =>\n      lanes.some((l) => l.id === id)\n        ? id\n        : lanes.find((l) => l.tasks.some((t) => t.id === id))?.id,\n    [lanes]\n  )\n\n  function handleDragStart(event: DragStartEvent) {\n    const id = String(event.active.id)\n    setActiveTask(\n      lanes.flatMap((l) => l.tasks).find((t) => t.id === id) ?? null\n    )\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    const activeLane = findLaneId(activeId)\n    const overLane = findLaneId(overId)\n    if (!activeLane || !overLane || activeLane === overLane) return\n    setLanes((prev) => {\n      const from = prev.find((l) => l.id === activeLane)!\n      const moving = from.tasks.find((t) => t.id === activeId)\n      if (!moving) return prev\n      const to = prev.find((l) => l.id === overLane)!\n      const overIndex = to.tasks.findIndex((t) => t.id === overId)\n      const insertAt = overIndex >= 0 ? overIndex : to.tasks.length\n      return prev.map((l) => {\n        if (l.id === activeLane)\n          return { ...l, tasks: l.tasks.filter((t) => t.id !== activeId) }\n        if (l.id === overLane) {\n          const next = [...l.tasks]\n          next.splice(insertAt, 0, moving)\n          return { ...l, tasks: next }\n        }\n        return l\n      })\n    })\n  }\n\n  function handleDragEnd(event: DragEndEvent) {\n    const { active, over } = event\n    setActiveTask(null)\n    if (!over) return\n    const activeId = String(active.id)\n    const overId = String(over.id)\n    const lane = findLaneId(activeId)\n    if (!lane || lane !== findLaneId(overId)) return\n    setLanes((prev) =>\n      prev.map((l) => {\n        if (l.id !== lane) return l\n        const oldIndex = l.tasks.findIndex((t) => t.id === activeId)\n        const newIndex = l.tasks.findIndex((t) => t.id === overId)\n        if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return l\n        return { ...l, tasks: arrayMove(l.tasks, oldIndex, newIndex) }\n      })\n    )\n  }\n\n  const total = lanes.reduce((n, l) => n + l.tasks.length, 0)\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-lg\">\n        <div className=\"mb-6 flex items-end justify-between border-b border-border pb-4\">\n          <div>\n            <p className=\"mb-1 text-xs font-medium tracking-widest text-muted-foreground uppercase\">\n              My Tasks\n            </p>\n            <h1 className=\"font-heading text-2xl font-bold tracking-tight\">\n              Today &amp; Beyond\n            </h1>\n          </div>\n          <span className=\"text-xs text-muted-foreground tabular-nums\">\n            {total} tasks\n          </span>\n        </div>\n\n        <DndContext\n          id=\"kanban-4-board\"\n          sensors={sensors}\n          collisionDetection={closestCorners}\n          onDragStart={handleDragStart}\n          onDragOver={handleDragOver}\n          onDragEnd={handleDragEnd}\n          onDragCancel={() => setActiveTask(null)}\n        >\n          <div className=\"flex flex-col gap-5\">\n            {lanes.map((lane) => (\n              <TaskLane key={lane.id} lane={lane} />\n            ))}\n          </div>\n          <DragOverlay>\n            {activeTask ? (\n              <div className=\"cursor-grabbing\">\n                <TaskRowBody task={activeTask} dragging />\n              </div>\n            ) : null}\n          </DragOverlay>\n        </DndContext>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/kanban-4.tsx"
    }
  ],
  "meta": {
    "height": "701px",
    "tier": "free"
  },
  "categories": [
    "kanban"
  ],
  "type": "registry:block"
}