Kanban

Drag-and-drop boards for tasks and team workflows.

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.

Sprint board

Drag cards — others shift to make room.

To do

2
  • Map pricing tiers

    Marketing

  • Collect launch assets

    Design

In progress

2
  • Ship hero-05 polish

    Product

  • Write FAQ answers

    Content

Done

1
  • Navbar variants

    Engineering

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

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

Usage

import { Kanban01 } from "@/components/blocks/kanban/kanban-01"
<Kanban01 />

Team board

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

Team board

Drag cards between columns — they make space as you move.

Backlog

2
  • Audit unused tokens

    Low
    MCMC
  • Draft onboarding email

    Med
    JHJH

Active

1
  • Checkout promo field

    High
    RKRK

Review

1
  • Gallery marquee pass

    Med
    MCMC
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

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

Usage

import { Kanban02 } from "@/components/blocks/kanban/kanban-02"
<Kanban02 />

Filtered board

A column board with quick filters for each team.

Filtered board

Filter by team, then drag cards into place.

Ready

3
  • Icon pass for empty states

    design

  • Footer link audit

    content

  • Kanban drag affordance

    eng

Building

2
  • Settings form layout

    eng

  • Team portrait hover

    design

Shipped

2
  • Login / signup pair

    eng

  • Pricing ticker

    design

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

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

Usage

import { Kanban03 } from "@/components/blocks/kanban/kanban-03"
<Kanban03 />