{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"c-kanban-6","type":"registry:block","title":"Kanban board persisted to a backend","description":"Persist moves with onValueCommit, optimistic updates, and Sonner rollback","dependencies":["sonner"],"registryDependencies":["@reui/badge","button","card","@reui/kanban"],"files":[{"path":"c-kanban-6.tsx","type":"registry:block","content":"\"use client\"\n\nimport { ComponentProps, useState } from \"react\"\nimport { Badge } from \"@/components/reui/badge\"\nimport {\n  Kanban,\n  KanbanBoard,\n  KanbanColumn,\n  KanbanColumnContent,\n  KanbanColumnHandle,\n  KanbanItem,\n  KanbanItemHandle,\n  KanbanOverlay,\n  type KanbanCommitMeta,\n} from \"@/components/reui/kanban\"\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent, CardHeader } from \"@/components/ui/card\"\nimport { IconPlaceholder } from \"@/app/(create)/components/icon-placeholder\"\n\ninterface Task {\n  id: string\n  title: string\n  priority: \"low\" | \"medium\" | \"high\"\n}\n\nconst COLUMN_TITLES: Record<string, string> = {\n  todo: \"To Do\",\n  inProgress: \"In Progress\",\n  done: \"Done\",\n}\n\n// Simulated backend. In a real app this would be a tRPC mutation or a fetch\n// to your API. It rejects roughly one in four calls so the optimistic\n// rollback path is easy to see.\nfunction persistBoard(): Promise<void> {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => {\n      if (Math.random() < 0.25) {\n        reject(new Error(\"Network error\"))\n      } else {\n        resolve()\n      }\n    }, 700)\n  })\n}\n\ninterface TaskCardProps extends Omit<\n  ComponentProps<typeof KanbanItem>,\n  \"value\" | \"children\"\n> {\n  task: Task\n  asHandle?: boolean\n  isOverlay?: boolean\n}\n\nfunction TaskCard({ task, asHandle, isOverlay, ...props }: TaskCardProps) {\n  const cardContent = (\n    <Card>\n      <CardContent className=\"flex items-center justify-between gap-2\">\n        <span className=\"line-clamp-1 text-sm font-medium\">{task.title}</span>\n        <Badge\n          variant={\n            task.priority === \"high\"\n              ? \"destructive-light\"\n              : task.priority === \"medium\"\n                ? \"primary-light\"\n                : \"warning-light\"\n          }\n          className=\"pointer-events-none h-5 shrink-0 rounded-sm px-1.5 text-xs capitalize\"\n        >\n          {task.priority}\n        </Badge>\n      </CardContent>\n    </Card>\n  )\n\n  return (\n    <KanbanItem value={task.id} {...props}>\n      {asHandle && !isOverlay ? (\n        <KanbanItemHandle>{cardContent}</KanbanItemHandle>\n      ) : (\n        cardContent\n      )}\n    </KanbanItem>\n  )\n}\n\ninterface TaskColumnProps extends Omit<\n  ComponentProps<typeof KanbanColumn>,\n  \"children\"\n> {\n  tasks: Task[]\n  isOverlay?: boolean\n}\n\nfunction TaskColumn({ value, tasks, isOverlay, ...props }: TaskColumnProps) {\n  return (\n    <KanbanColumn value={value} {...props}>\n      <Card className=\"mb-2.5\">\n        <CardHeader className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-2.5\">\n            <span className=\"text-sm font-semibold\">\n              {COLUMN_TITLES[value]}\n            </span>\n            <Badge variant=\"outline\">{tasks.length}</Badge>\n          </div>\n          <KanbanColumnHandle\n            render={(props) => (\n              <Button {...props} size=\"icon-xs\" variant=\"ghost\">\n                <IconPlaceholder\n                  lucide=\"GripVerticalIcon\"\n                  tabler=\"IconGripVertical\"\n                  hugeicons=\"DragDropVerticalIcon\"\n                  phosphor=\"DotsSixVerticalIcon\"\n                  remixicon=\"RiDraggable\"\n                />\n              </Button>\n            )}\n          />\n        </CardHeader>\n        <CardContent>\n          <KanbanColumnContent value={value} className=\"flex flex-col gap-2.5\">\n            {tasks.map((task) => (\n              <TaskCard\n                key={task.id}\n                task={task}\n                asHandle={!isOverlay}\n                isOverlay={isOverlay}\n              />\n            ))}\n          </KanbanColumnContent>\n        </CardContent>\n      </Card>\n    </KanbanColumn>\n  )\n}\n\nexport function Pattern() {\n  const [columns, setColumns] = useState<Record<string, Task[]>>({\n    todo: [\n      { id: \"1\", title: \"Add authentication\", priority: \"high\" },\n      { id: \"2\", title: \"Create API endpoints\", priority: \"medium\" },\n      { id: \"3\", title: \"Write documentation\", priority: \"low\" },\n    ],\n    inProgress: [\n      { id: \"4\", title: \"Design system updates\", priority: \"high\" },\n      { id: \"5\", title: \"Implement dark mode\", priority: \"medium\" },\n    ],\n    done: [{ id: \"6\", title: \"Setup project\", priority: \"low\" }],\n  })\n\n  // Fires once per completed drag, never during the hover preview. The first\n  // argument is the final board (already shown, because onValueChange applied\n  // it live), so we only need meta.previousValue to roll back on failure.\n  const handleValueCommit = (\n    _next: Record<string, Task[]>,\n    meta: KanbanCommitMeta<Task>\n  ) => {\n    const previous = meta.previousValue\n    const label =\n      meta.kind === \"column\"\n        ? `Reordered \"${COLUMN_TITLES[meta.activeContainer] ?? meta.activeContainer}\"`\n        : `Moved to \"${COLUMN_TITLES[meta.overContainer] ?? meta.overContainer}\"`\n\n    toast.promise(persistBoard(), {\n      loading: \"Saving board...\",\n      success: () => label,\n      error: () => {\n        // Roll back to the pre-drag arrangement. In production prefer a\n        // refetch here so a newer drag is not clobbered by this snapshot.\n        setColumns(previous)\n        return \"Could not save. Board restored.\"\n      },\n    })\n  }\n\n  return (\n    <Kanban\n      value={columns}\n      onValueChange={setColumns}\n      getItemValue={(item) => item.id}\n      onValueCommit={handleValueCommit}\n    >\n      <KanbanBoard className=\"grid auto-rows-fr grid-cols-3\">\n        {Object.entries(columns).map(([columnValue, tasks]) => (\n          <TaskColumn key={columnValue} value={columnValue} tasks={tasks} />\n        ))}\n      </KanbanBoard>\n      <KanbanOverlay className=\"bg-muted/10 rounded-md border-2 border-dashed\" />\n    </Kanban>\n  )\n}","target":"components/examples/c-kanban-6.tsx"}],"meta":{"order":6,"gridSize":1,"label":"Persisted Moves"}}