---
title: Kanban
description: Drag-and-drop boards for tasks and team workflows.
group: Blocks
order: 30
---

Working kanban boards with drag-and-drop powered by `@dnd-kit`. Adjust the columns, cards, and tags around how your team works.

## Sprint board

Three quiet columns with straightforward task cards.

```tsx
"use client";

import { useMemo, useState, type ReactNode } from "react";
import {
  DndContext,
  DragOverlay,
  PointerSensor,
  closestCorners,
  useDroppable,
  useSensor,
  useSensors,
  type DragEndEvent,
  type DragOverEvent,
  type DragStartEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  arrayMove,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@/lib/utils";

type Kanban01Props = {
  className?: string;
};

type Card = {
  id: string;
  title: string;
  meta: string;
};

type Column = {
  id: string;
  title: string;
  cards: Card[];
};

const INITIAL: Column[] = [
  {
    id: "todo",
    title: "To do",
    cards: [
      { id: "t1", title: "Map pricing tiers", meta: "Marketing" },
      { id: "t2", title: "Collect launch assets", meta: "Design" },
    ],
  },
  {
    id: "doing",
    title: "In progress",
    cards: [
      { id: "t3", title: "Ship hero-05 polish", meta: "Product" },
      { id: "t4", title: "Write FAQ answers", meta: "Content" },
    ],
  },
  {
    id: "done",
    title: "Done",
    cards: [{ id: "t5", title: "Navbar variants", meta: "Engineering" }],
  },
];

function findColumnId(columns: Column[], id: string) {
  if (columns.some((column) => column.id === id)) return id;
  return columns.find((column) => column.cards.some((card) => card.id === id))
    ?.id;
}

function CardBody({ card }: { card: Card }) {
  return (
    <>
      <p className="text-sm font-medium tracking-wide text-foreground">
        {card.title}
      </p>
      <p className="mt-1 text-xs tracking-wide text-muted-foreground">
        {card.meta}
      </p>
    </>
  );
}

function SortableCard({
  card,
  className,
}: {
  card: Card;
  className?: string;
}) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: card.id });

  return (
    <li
      ref={setNodeRef}
      style={{
        transform: CSS.Transform.toString(transform),
        transition,
      }}
      className={cn(
        "cursor-grab rounded-lg bg-background px-3 py-3 active:cursor-grabbing",
        isDragging && "opacity-0",
        className,
      )}
      {...attributes}
      {...listeners}
    >
      <CardBody card={card} />
    </li>
  );
}

function ColumnDrop({
  id,
  children,
  empty,
}: {
  id: string;
  children: ReactNode;
  empty: boolean;
}) {
  const { setNodeRef } = useDroppable({ id });

  return (
    <ul
      ref={setNodeRef}
      className="flex flex-col gap-2 rounded-xl bg-muted p-2"
    >
      {empty ? (
        <li className="rounded-lg border border-dashed border-border px-3 py-3 text-center text-xs tracking-wide text-muted-foreground">
          Empty
        </li>
      ) : (
        children
      )}
    </ul>
  );
}

export function Kanban01({ className }: Kanban01Props) {
  const [columns, setColumns] = useState(INITIAL);
  const [activeId, setActiveId] = useState<string | null>(null);
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
  );

  const activeCard = useMemo(() => {
    if (!activeId) return null;
    for (const column of columns) {
      const card = column.cards.find((item) => item.id === activeId);
      if (card) return card;
    }
    return null;
  }, [activeId, columns]);

  function handleDragStart(event: DragStartEvent) {
    setActiveId(String(event.active.id));
  }

  function handleDragOver(event: DragOverEvent) {
    const { active, over } = event;
    if (!over) return;

    const activeCardId = String(active.id);
    const overId = String(over.id);

    setColumns((current) => {
      const fromId = findColumnId(current, activeCardId);
      const toId = findColumnId(current, overId);
      if (!fromId || !toId || fromId === toId) return current;

      const fromColumn = current.find((column) => column.id === fromId);
      const toColumn = current.find((column) => column.id === toId);
      if (!fromColumn || !toColumn) return current;

      const moving = fromColumn.cards.find((card) => card.id === activeCardId);
      if (!moving) return current;

      const overIndex = toColumn.cards.findIndex((card) => card.id === overId);
      const insertIndex = overIndex >= 0 ? overIndex : toColumn.cards.length;

      return current.map((column) => {
        if (column.id === fromId) {
          return {
            ...column,
            cards: column.cards.filter((card) => card.id !== activeCardId),
          };
        }
        if (column.id === toId) {
          const next = column.cards.filter((card) => card.id !== activeCardId);
          next.splice(insertIndex, 0, moving);
          return { ...column, cards: next };
        }
        return column;
      });
    });
  }

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;
    setActiveId(null);
    if (!over) return;

    const activeCardId = String(active.id);
    const overId = String(over.id);

    setColumns((current) => {
      const columnId = findColumnId(current, activeCardId);
      if (!columnId) return current;
      if (findColumnId(current, overId) !== columnId) return current;

      const column = current.find((item) => item.id === columnId);
      if (!column) return current;

      const oldIndex = column.cards.findIndex((card) => card.id === activeCardId);
      const newIndex =
        overId === columnId
          ? column.cards.length - 1
          : column.cards.findIndex((card) => card.id === overId);

      if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return current;

      return current.map((item) =>
        item.id === columnId
          ? { ...item, cards: arrayMove(item.cards, oldIndex, newIndex) }
          : item,
      );
    });
  }

  return (
    <section className={cn("@container w-full bg-background", className)}>
      <div className="mx-auto w-full max-w-5xl px-4 py-10 @[32rem]:px-6 @[32rem]:py-14">
        <div>
          <h2 className="text-xl font-medium tracking-wide text-foreground @[32rem]:text-2xl">
            Sprint board
          </h2>
          <p className="mt-2 text-sm tracking-wide text-muted-foreground">
            Drag cards — others shift to make room.
          </p>
        </div>

        <DndContext
          id="kanban-01"
          sensors={sensors}
          collisionDetection={closestCorners}
          onDragStart={handleDragStart}
          onDragOver={handleDragOver}
          onDragEnd={handleDragEnd}
        >
          <div className="mt-8 flex flex-col gap-6 @[40rem]:grid @[40rem]:grid-cols-3 @[40rem]:gap-4">
            {columns.map((column) => (
              <div key={column.id} className="flex w-full flex-col gap-3">
                <div className="flex items-center justify-between gap-2 px-1">
                  <h3 className="text-sm font-medium tracking-wide text-foreground">
                    {column.title}
                  </h3>
                  <span className="text-xs tracking-wide text-muted-foreground tabular-nums">
                    {column.cards.length}
                  </span>
                </div>
                <SortableContext
                  id={`kanban-01-${column.id}`}
                  items={column.cards.map((card) => card.id)}
                  strategy={verticalListSortingStrategy}
                >
                  <ColumnDrop
                    id={column.id}
                    empty={column.cards.length === 0}
                  >
                    {column.cards.map((card) => (
                      <SortableCard key={card.id} card={card} />
                    ))}
                  </ColumnDrop>
                </SortableContext>
              </div>
            ))}
          </div>

          <DragOverlay>
            {activeCard ? (
              <div className="cursor-grabbing rounded-lg bg-background px-3 py-3 ring-1 ring-border">
                <CardBody card={activeCard} />
              </div>
            ) : null}
          </DragOverlay>
        </DndContext>
      </div>
    </section>
  );
}
```

### Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add kanban-01
```

### Usage

```tsx
import { Kanban01 } from "@/components/blocks/kanban/kanban-01"
```

```tsx
<Kanban01 />
```

## Team board

A team board with priority badges, assignees, and add-task actions.

```tsx
"use client";

import { useMemo, useState, type ReactNode } from "react";
import {
  DndContext,
  DragOverlay,
  PointerSensor,
  closestCorners,
  useDroppable,
  useSensor,
  useSensors,
  type DragEndEvent,
  type DragOverEvent,
  type DragStartEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  arrayMove,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

type Kanban02Props = {
  className?: string;
};

type Card = {
  id: string;
  title: string;
  priority: "Low" | "Med" | "High";
  assignee: string;
};

type Column = {
  id: string;
  title: string;
  cards: Card[];
};

const INITIAL: Column[] = [
  {
    id: "backlog",
    title: "Backlog",
    cards: [
      {
        id: "c1",
        title: "Audit unused tokens",
        priority: "Low",
        assignee: "MC",
      },
      {
        id: "c2",
        title: "Draft onboarding email",
        priority: "Med",
        assignee: "JH",
      },
    ],
  },
  {
    id: "active",
    title: "Active",
    cards: [
      {
        id: "c3",
        title: "Checkout promo field",
        priority: "High",
        assignee: "RK",
      },
    ],
  },
  {
    id: "review",
    title: "Review",
    cards: [
      {
        id: "c4",
        title: "Gallery marquee pass",
        priority: "Med",
        assignee: "MC",
      },
    ],
  },
];

function findColumnId(columns: Column[], id: string) {
  if (columns.some((column) => column.id === id)) return id;
  return columns.find((column) => column.cards.some((card) => card.id === id))
    ?.id;
}

function CardBody({ card }: { card: Card }) {
  return (
    <>
      <div className="flex items-start justify-between gap-2">
        <p className="text-sm font-medium tracking-wide text-foreground">
          {card.title}
        </p>
        <Badge variant={card.priority === "High" ? "default" : "secondary"}>
          {card.priority}
        </Badge>
      </div>
      <div className="mt-3 flex items-center gap-2">
        <Avatar size="xs">
          <AvatarFallback>{card.assignee}</AvatarFallback>
        </Avatar>
        <span className="text-xs tracking-wide text-muted-foreground">
          {card.assignee}
        </span>
      </div>
    </>
  );
}

function SortableCard({ card }: { card: Card }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: card.id });

  return (
    <li
      ref={setNodeRef}
      style={{
        transform: CSS.Transform.toString(transform),
        transition,
      }}
      className={cn(
        "cursor-grab rounded-xl border border-border bg-background px-3 py-3 active:cursor-grabbing",
        isDragging && "opacity-0",
      )}
      {...attributes}
      {...listeners}
    >
      <CardBody card={card} />
    </li>
  );
}

function PlusIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M12 5v14" />
      <path d="M5 12h14" />
    </svg>
  );
}

function ColumnDrop({
  id,
  children,
  empty,
}: {
  id: string;
  children: ReactNode;
  empty: boolean;
}) {
  const { setNodeRef } = useDroppable({ id });

  return (
    <ul ref={setNodeRef} className="flex flex-col gap-2">
      {empty ? (
        <li className="rounded-xl border border-dashed border-border px-3 py-3 text-center text-xs tracking-wide text-muted-foreground">
          Empty
        </li>
      ) : (
        children
      )}
    </ul>
  );
}

export function Kanban02({ className }: Kanban02Props) {
  const [columns, setColumns] = useState(INITIAL);
  const [activeId, setActiveId] = useState<string | null>(null);
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
  );

  const activeCard = useMemo(() => {
    if (!activeId) return null;
    for (const column of columns) {
      const card = column.cards.find((item) => item.id === activeId);
      if (card) return card;
    }
    return null;
  }, [activeId, columns]);

  function addCard(columnId: string) {
    setColumns((current) =>
      current.map((column) =>
        column.id === columnId
          ? {
              ...column,
              cards: [
                ...column.cards,
                {
                  id: `${columnId}-${Date.now()}`,
                  title: "New task",
                  priority: "Low",
                  assignee: "You",
                },
              ],
            }
          : column,
      ),
    );
  }

  function handleDragStart(event: DragStartEvent) {
    setActiveId(String(event.active.id));
  }

  function handleDragOver(event: DragOverEvent) {
    const { active, over } = event;
    if (!over) return;

    const activeCardId = String(active.id);
    const overId = String(over.id);

    setColumns((current) => {
      const fromId = findColumnId(current, activeCardId);
      const toId = findColumnId(current, overId);
      if (!fromId || !toId || fromId === toId) return current;

      const fromColumn = current.find((column) => column.id === fromId);
      const toColumn = current.find((column) => column.id === toId);
      if (!fromColumn || !toColumn) return current;

      const moving = fromColumn.cards.find((card) => card.id === activeCardId);
      if (!moving) return current;

      const overIndex = toColumn.cards.findIndex((card) => card.id === overId);
      const insertIndex = overIndex >= 0 ? overIndex : toColumn.cards.length;

      return current.map((column) => {
        if (column.id === fromId) {
          return {
            ...column,
            cards: column.cards.filter((card) => card.id !== activeCardId),
          };
        }
        if (column.id === toId) {
          const next = column.cards.filter((card) => card.id !== activeCardId);
          next.splice(insertIndex, 0, moving);
          return { ...column, cards: next };
        }
        return column;
      });
    });
  }

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;
    setActiveId(null);
    if (!over) return;

    const activeCardId = String(active.id);
    const overId = String(over.id);

    setColumns((current) => {
      const columnId = findColumnId(current, activeCardId);
      if (!columnId) return current;
      if (findColumnId(current, overId) !== columnId) return current;

      const column = current.find((item) => item.id === columnId);
      if (!column) return current;

      const oldIndex = column.cards.findIndex((card) => card.id === activeCardId);
      const newIndex =
        overId === columnId
          ? column.cards.length - 1
          : column.cards.findIndex((card) => card.id === overId);

      if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return current;

      return current.map((item) =>
        item.id === columnId
          ? { ...item, cards: arrayMove(item.cards, oldIndex, newIndex) }
          : item,
      );
    });
  }

  return (
    <section className={cn("@container w-full bg-background", className)}>
      <div className="mx-auto w-full max-w-5xl px-4 py-10 @[32rem]:px-6 @[32rem]:py-14">
        <div>
          <h2 className="text-xl font-medium tracking-wide text-foreground @[32rem]:text-2xl">
            Team board
          </h2>
          <p className="mt-2 text-sm tracking-wide text-muted-foreground">
            Drag cards between columns — they make space as you move.
          </p>
        </div>

        <DndContext
          id="kanban-02"
          sensors={sensors}
          collisionDetection={closestCorners}
          onDragStart={handleDragStart}
          onDragOver={handleDragOver}
          onDragEnd={handleDragEnd}
        >
          <div className="mt-8 flex flex-col gap-6 @[40rem]:grid @[40rem]:grid-cols-3 @[40rem]:gap-4">
            {columns.map((column) => (
              <div key={column.id} className="flex w-full flex-col gap-3">
                <div className="flex items-center justify-between gap-2 px-1">
                  <h3 className="text-sm font-medium tracking-wide text-foreground">
                    {column.title}
                  </h3>
                  <div className="flex items-center gap-1">
                    <span className="text-xs tracking-wide text-muted-foreground tabular-nums">
                      {column.cards.length}
                    </span>
                    <Button
                      type="button"
                      size="icon-sm"
                      variant="ghost"
                      aria-label={`Add task to ${column.title}`}
                      onClick={() => addCard(column.id)}
                    >
                      <PlusIcon />
                    </Button>
                  </div>
                </div>
                <SortableContext
                  id={`kanban-02-${column.id}`}
                  items={column.cards.map((card) => card.id)}
                  strategy={verticalListSortingStrategy}
                >
                  <ColumnDrop
                    id={column.id}
                    empty={column.cards.length === 0}
                  >
                    {column.cards.map((card) => (
                      <SortableCard key={card.id} card={card} />
                    ))}
                  </ColumnDrop>
                </SortableContext>
              </div>
            ))}
          </div>

          <DragOverlay>
            {activeCard ? (
              <div className="cursor-grabbing rounded-xl border border-border bg-background px-3 py-3 ring-1 ring-border">
                <CardBody card={activeCard} />
              </div>
            ) : null}
          </DragOverlay>
        </DndContext>
      </div>
    </section>
  );
}
```

### Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add kanban-02
```

### Usage

```tsx
import { Kanban02 } from "@/components/blocks/kanban/kanban-02"
```

```tsx
<Kanban02 />
```

## Filtered board

A column board with quick filters for each team.

```tsx
"use client";

import { useMemo, useState, type ReactNode } from "react";
import {
  DndContext,
  DragOverlay,
  PointerSensor,
  closestCorners,
  useDroppable,
  useSensor,
  useSensors,
  type DragEndEvent,
  type DragOverEvent,
  type DragStartEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  arrayMove,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { cn } from "@/lib/utils";

type Kanban03Props = {
  className?: string;
};

type Filter = "all" | "design" | "eng" | "content";
type Tag = Exclude<Filter, "all">;

type Card = {
  id: string;
  title: string;
  tag: Tag;
};

type Column = {
  id: string;
  title: string;
  cards: Card[];
};

const FILTERS: { id: Filter; label: string }[] = [
  { id: "all", label: "All" },
  { id: "design", label: "Design" },
  { id: "eng", label: "Eng" },
  { id: "content", label: "Content" },
];

const INITIAL: Column[] = [
  {
    id: "todo",
    title: "Ready",
    cards: [
      { id: "a1", title: "Icon pass for empty states", tag: "design" },
      { id: "a2", title: "Footer link audit", tag: "content" },
      { id: "a3", title: "Kanban drag affordance", tag: "eng" },
    ],
  },
  {
    id: "doing",
    title: "Building",
    cards: [
      { id: "a4", title: "Settings form layout", tag: "eng" },
      { id: "a5", title: "Team portrait hover", tag: "design" },
    ],
  },
  {
    id: "done",
    title: "Shipped",
    cards: [
      { id: "a6", title: "Login / signup pair", tag: "eng" },
      { id: "a7", title: "Pricing ticker", tag: "design" },
    ],
  },
];

function findColumnId(columns: Column[], id: string) {
  if (columns.some((column) => column.id === id)) return id;
  return columns.find((column) => column.cards.some((card) => card.id === id))
    ?.id;
}

function CardBody({ card }: { card: Card }) {
  return (
    <>
      <p className="text-sm tracking-wide text-foreground">{card.title}</p>
      <p className="mt-1 text-[11px] tracking-wide text-muted-foreground capitalize">
        {card.tag}
      </p>
    </>
  );
}

function SortableCard({ card }: { card: Card }) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: card.id });

  return (
    <li
      ref={setNodeRef}
      style={{
        transform: CSS.Transform.toString(transform),
        transition,
      }}
      className={cn(
        "cursor-grab rounded-lg bg-muted px-3 py-2.5 active:cursor-grabbing",
        isDragging && "opacity-0",
      )}
      {...attributes}
      {...listeners}
    >
      <CardBody card={card} />
    </li>
  );
}

function ColumnDrop({
  id,
  children,
  empty,
}: {
  id: string;
  children: ReactNode;
  empty: boolean;
}) {
  const { setNodeRef } = useDroppable({ id });

  return (
    <ul ref={setNodeRef} className="flex flex-col gap-2">
      {empty ? (
        <li className="rounded-lg border border-dashed border-border px-3 py-2.5 text-center text-xs tracking-wide text-muted-foreground">
          Empty
        </li>
      ) : (
        children
      )}
    </ul>
  );
}

export function Kanban03({ className }: Kanban03Props) {
  const [filter, setFilter] = useState<Filter>("all");
  const [columns, setColumns] = useState(INITIAL);
  const [activeId, setActiveId] = useState<string | null>(null);
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
  );

  const visibleColumns = useMemo(
    () =>
      columns.map((column) => ({
        ...column,
        cards:
          filter === "all"
            ? column.cards
            : column.cards.filter((card) => card.tag === filter),
      })),
    [columns, filter],
  );

  const activeCard = useMemo(() => {
    if (!activeId) return null;
    for (const column of columns) {
      const card = column.cards.find((item) => item.id === activeId);
      if (card) return card;
    }
    return null;
  }, [activeId, columns]);

  function handleDragStart(event: DragStartEvent) {
    setActiveId(String(event.active.id));
  }

  function handleDragOver(event: DragOverEvent) {
    const { active, over } = event;
    if (!over) return;

    const activeCardId = String(active.id);
    const overId = String(over.id);

    setColumns((current) => {
      const fromId = findColumnId(current, activeCardId);
      const toId = findColumnId(current, overId);
      if (!fromId || !toId || fromId === toId) return current;

      const fromColumn = current.find((column) => column.id === fromId);
      const toColumn = current.find((column) => column.id === toId);
      if (!fromColumn || !toColumn) return current;

      const moving = fromColumn.cards.find((card) => card.id === activeCardId);
      if (!moving) return current;

      const overIndex = toColumn.cards.findIndex((card) => card.id === overId);
      const insertIndex = overIndex >= 0 ? overIndex : toColumn.cards.length;

      return current.map((column) => {
        if (column.id === fromId) {
          return {
            ...column,
            cards: column.cards.filter((card) => card.id !== activeCardId),
          };
        }
        if (column.id === toId) {
          const next = column.cards.filter((card) => card.id !== activeCardId);
          next.splice(insertIndex, 0, moving);
          return { ...column, cards: next };
        }
        return column;
      });
    });
  }

  function handleDragEnd(event: DragEndEvent) {
    const { active, over } = event;
    setActiveId(null);
    if (!over) return;

    const activeCardId = String(active.id);
    const overId = String(over.id);

    setColumns((current) => {
      const columnId = findColumnId(current, activeCardId);
      if (!columnId) return current;
      if (findColumnId(current, overId) !== columnId) return current;

      const column = current.find((item) => item.id === columnId);
      if (!column) return current;

      const oldIndex = column.cards.findIndex((card) => card.id === activeCardId);
      const newIndex =
        overId === columnId
          ? column.cards.length - 1
          : column.cards.findIndex((card) => card.id === overId);

      if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return current;

      return current.map((item) =>
        item.id === columnId
          ? { ...item, cards: arrayMove(item.cards, oldIndex, newIndex) }
          : item,
      );
    });
  }

  return (
    <section className={cn("@container w-full bg-background", className)}>
      <div className="mx-auto w-full max-w-5xl px-4 py-10 @[32rem]:px-6 @[32rem]:py-14">
        <div className="flex flex-col gap-4 @[40rem]:flex-row @[40rem]:items-end @[40rem]:justify-between">
          <div>
            <h2 className="text-xl font-medium tracking-wide text-foreground @[32rem]:text-2xl">
              Filtered board
            </h2>
            <p className="mt-2 text-sm tracking-wide text-muted-foreground">
              Filter by team, then drag cards into place.
            </p>
          </div>
          <div className="flex flex-wrap gap-1.5">
            {FILTERS.map((item) => (
              <button
                key={item.id}
                type="button"
                onClick={() => setFilter(item.id)}
                className={cn(
                  "rounded-md px-2.5 py-1 text-xs tracking-wide transition-colors duration-200 ease-out",
                  filter === item.id
                    ? "bg-foreground text-background"
                    : "bg-muted text-muted-foreground hover:text-foreground",
                )}
              >
                {item.label}
              </button>
            ))}
          </div>
        </div>

        <DndContext
          id="kanban-03"
          sensors={sensors}
          collisionDetection={closestCorners}
          onDragStart={handleDragStart}
          onDragOver={handleDragOver}
          onDragEnd={handleDragEnd}
        >
          <div className="mt-8 flex flex-col gap-6 @[40rem]:grid @[40rem]:grid-cols-3 @[40rem]:gap-4">
            {visibleColumns.map((column) => (
              <div key={column.id} className="flex w-full flex-col gap-2">
                <div className="flex items-center justify-between gap-2 border-b border-border pb-2">
                  <h3 className="text-sm font-medium tracking-wide text-foreground">
                    {column.title}
                  </h3>
                  <span className="text-xs tracking-wide text-muted-foreground tabular-nums">
                    {column.cards.length}
                  </span>
                </div>
                <SortableContext
                  id={`kanban-03-${column.id}`}
                  items={column.cards.map((card) => card.id)}
                  strategy={verticalListSortingStrategy}
                >
                  <ColumnDrop id={column.id} empty={column.cards.length === 0}>
                    {column.cards.map((card) => (
                      <SortableCard key={card.id} card={card} />
                    ))}
                  </ColumnDrop>
                </SortableContext>
              </div>
            ))}
          </div>

          <DragOverlay>
            {activeCard ? (
              <div className="cursor-grabbing rounded-lg bg-muted px-3 py-2.5 ring-1 ring-border">
                <CardBody card={activeCard} />
              </div>
            ) : null}
          </DragOverlay>
        </DndContext>
      </div>
    </section>
  );
}
```

### Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add kanban-03
```

### Usage

```tsx
import { Kanban03 } from "@/components/blocks/kanban/kanban-03"
```

```tsx
<Kanban03 />
```
