{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "kanban-board-shadcnui", "type": "registry:block", "title": "Kanban Board", "description": "Interactive Kanban board with drag-and-drop, glassmorphism styling, and task management features", "registryDependencies": [ "button" ], "dependencies": [ "framer-motion", "react" ], "files": [ { "path": "@uitripled/react-shadcn/src/components/components/kanban/kanban-board.tsx", "content": "\"use client\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport {\n closestCorners,\n defaultDropAnimationSideEffects,\n DndContext,\n DragEndEvent,\n DragOverEvent,\n DragOverlay,\n DragStartEvent,\n DropAnimation,\n KeyboardSensor,\n PointerSensor,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport {\n arrayMove,\n horizontalListSortingStrategy,\n SortableContext,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport {\n Clock,\n Filter,\n MessageSquare,\n MoreHorizontal,\n Paperclip,\n Plus,\n Search,\n} from \"lucide-react\";\nimport { useMemo, useState } from \"react\";\n\n// --- Types ---\n\ntype Id = string | number;\n\ntype Column = {\n id: Id;\n title: string;\n};\n\ntype Task = {\n id: Id;\n columnId: Id;\n content: string;\n priority: \"low\" | \"medium\" | \"high\";\n tags: string[];\n comments: number;\n attachments: number;\n dueDate?: string;\n assignees: string[];\n};\n\n// --- Mock Data ---\n\nconst initialColumns: Column[] = [\n { id: \"todo\", title: \"To Do\" },\n { id: \"in-progress\", title: \"In Progress\" },\n { id: \"review\", title: \"Review\" },\n { id: \"done\", title: \"Done\" },\n];\n\nconst initialTasks: Task[] = [\n {\n id: \"1\",\n columnId: \"todo\",\n content: \"Design new landing page\",\n priority: \"high\",\n tags: [\"Design\", \"Marketing\"],\n comments: 3,\n attachments: 2,\n dueDate: \"Tomorrow\",\n assignees: [\"/avatars/01.png\", \"/avatars/02.png\"],\n },\n {\n id: \"2\",\n columnId: \"todo\",\n content: \"Competitor analysis\",\n priority: \"medium\",\n tags: [\"Research\"],\n comments: 1,\n attachments: 0,\n dueDate: \"Next Week\",\n assignees: [\"/avatars/03.png\"],\n },\n {\n id: \"3\",\n columnId: \"in-progress\",\n content: \"Implement authentication flow\",\n priority: \"high\",\n tags: [\"Dev\", \"Backend\"],\n comments: 5,\n attachments: 1,\n dueDate: \"Today\",\n assignees: [\"/avatars/04.png\", \"/avatars/05.png\"],\n },\n {\n id: \"4\",\n columnId: \"in-progress\",\n content: \"Fix navigation bug on mobile\",\n priority: \"low\",\n tags: [\"Bug\", \"Mobile\"],\n comments: 0,\n attachments: 0,\n assignees: [\"/avatars/01.png\"],\n },\n {\n id: \"5\",\n columnId: \"review\",\n content: \"Update documentation\",\n priority: \"medium\",\n tags: [\"Docs\"],\n comments: 2,\n attachments: 1,\n dueDate: \"Yesterday\",\n assignees: [\"/avatars/02.png\"],\n },\n {\n id: \"6\",\n columnId: \"done\",\n content: \"Release v1.0.0\",\n priority: \"high\",\n tags: [\"Release\"],\n comments: 12,\n attachments: 4,\n dueDate: \"Last Week\",\n assignees: [\"/avatars/01.png\", \"/avatars/02.png\", \"/avatars/03.png\"],\n },\n];\n\n// --- Components ---\n\nexport function KanbanBoard() {\n const [columns, setColumns] = useState(initialColumns);\n const [tasks, setTasks] = useState(initialTasks);\n const [activeColumn, setActiveColumn] = useState(null);\n const [activeTask, setActiveTask] = useState(null);\n const [searchQuery, setSearchQuery] = useState(\"\");\n\n const sensors = useSensors(\n useSensor(PointerSensor, {\n activationConstraint: {\n distance: 3, // 3px movement required before drag starts\n },\n }),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n })\n );\n\n const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);\n\n const filteredTasks = useMemo(() => {\n if (!searchQuery) return tasks;\n return tasks.filter((task) =>\n task.content.toLowerCase().includes(searchQuery.toLowerCase())\n );\n }, [tasks, searchQuery]);\n\n function onDragStart(event: DragStartEvent) {\n if (event.active.data.current?.type === \"Column\") {\n setActiveColumn(event.active.data.current.column);\n return;\n }\n\n if (event.active.data.current?.type === \"Task\") {\n setActiveTask(event.active.data.current.task);\n return;\n }\n }\n\n function onDragOver(event: DragOverEvent) {\n const { active, over } = event;\n if (!over) return;\n\n const activeId = active.id;\n const overId = over.id;\n\n if (activeId === overId) return;\n\n const isActiveTask = active.data.current?.type === \"Task\";\n const isOverTask = over.data.current?.type === \"Task\";\n\n if (!isActiveTask) return;\n\n // Im dropping a Task over another Task\n if (isActiveTask && isOverTask) {\n setTasks((tasks) => {\n const activeIndex = tasks.findIndex((t) => t.id === activeId);\n const overIndex = tasks.findIndex((t) => t.id === overId);\n\n if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) {\n tasks[activeIndex].columnId = tasks[overIndex].columnId;\n }\n\n return arrayMove(tasks, activeIndex, overIndex);\n });\n }\n\n const isOverColumn = over.data.current?.type === \"Column\";\n\n // Im dropping a Task over a column\n if (isActiveTask && isOverColumn) {\n setTasks((tasks) => {\n const activeIndex = tasks.findIndex((t) => t.id === activeId);\n\n tasks[activeIndex].columnId = overId;\n console.log(\"DROPPING TASK OVER COLUMN\", { activeIndex });\n return arrayMove(tasks, activeIndex, activeIndex);\n });\n }\n }\n\n function onDragEnd(event: DragEndEvent) {\n setActiveColumn(null);\n setActiveTask(null);\n\n const { active, over } = event;\n if (!over) return;\n\n const activeId = active.id;\n const overId = over.id;\n\n if (activeId === overId) return;\n\n const isActiveColumn = active.data.current?.type === \"Column\";\n if (isActiveColumn) {\n setColumns((columns) => {\n const activeIndex = columns.findIndex((col) => col.id === activeId);\n const overIndex = columns.findIndex((col) => col.id === overId);\n return arrayMove(columns, activeIndex, overIndex);\n });\n }\n }\n\n const dropAnimation: DropAnimation = {\n sideEffects: defaultDropAnimationSideEffects({\n styles: {\n active: {\n opacity: \"0.5\",\n },\n },\n }),\n };\n\n return (\n
\n {/* Glassmorphism background blobs */}\n
\n
\n
\n
\n
\n {/* Header */}\n
\n {/* Gradient overlay */}\n
\n
\n

\n Product Roadmap\n

\n

\n Manage tasks, track progress, and collaborate with your team.\n

\n
\n
\n
\n \n setSearchQuery(e.target.value)}\n />\n
\n \n \n \n \n
\n
\n\n {/* Board */}\n \n
\n \n {columns.map((col) => (\n task.columnId === col.id)}\n />\n ))}\n \n
\n\n {/* Drag Overlay */}\n \n {activeColumn && (\n task.columnId === activeColumn.id)}\n isOverlay\n />\n )}\n {activeTask && }\n \n \n
\n );\n}\n\ninterface BoardColumnProps {\n column: Column;\n tasks: Task[];\n isOverlay?: boolean;\n}\n\nfunction BoardColumn({ column, tasks, isOverlay }: BoardColumnProps) {\n const {\n setNodeRef,\n attributes,\n listeners,\n transform,\n transition,\n isDragging,\n } = useSortable({\n id: column.id,\n data: {\n type: \"Column\",\n column,\n },\n });\n\n const style = {\n transition,\n transform: CSS.Translate.toString(transform),\n };\n\n const tasksIds = useMemo(() => tasks.map((task) => task.id), [tasks]);\n\n return (\n \n {/* Gradient overlay for column */}\n
\n\n {/* Column Header */}\n \n
\n
\n {tasks.length}\n
\n

{column.title}

\n
\n \n \n \n
\n\n {/* Column Content */}\n
\n \n {tasks.map((task) => (\n \n ))}\n \n \n \n Add Task\n \n
\n
\n );\n}\n\ninterface TaskCardProps {\n task: Task;\n isOverlay?: boolean;\n}\n\nfunction TaskCard({ task, isOverlay }: TaskCardProps) {\n const {\n setNodeRef,\n attributes,\n listeners,\n transform,\n transition,\n isDragging,\n } = useSortable({\n id: task.id,\n data: {\n type: \"Task\",\n task,\n },\n });\n\n const style = {\n transition,\n transform: CSS.Translate.toString(transform),\n };\n\n const priorityColors = {\n low: \"bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20\",\n medium:\n \"bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20\",\n high: \"bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20\",\n };\n\n return (\n \n {/* Gradient overlay for card */}\n
\n\n {/* Header / Tags */}\n
\n
\n \n {task.priority}\n \n {task.tags.map((tag) => (\n \n {tag}\n \n ))}\n
\n \n \n \n
\n\n {/* Content */}\n

\n {task.content}\n

\n\n {/* Footer */}\n
\n
\n {task.dueDate && (\n \n \n {task.dueDate}\n
\n )}\n {(task.comments > 0 || task.attachments > 0) && (\n
\n {task.comments > 0 && (\n
\n \n {task.comments}\n
\n )}\n {task.attachments > 0 && (\n
\n \n {task.attachments}\n
\n )}\n
\n )}\n
\n\n {/* Assignees */}\n {task.assignees.length > 0 && (\n
\n {task.assignees.map((avatar, i) => (\n \n \n \n U{i + 1}\n \n \n ))}\n
\n )}\n
\n
\n );\n}\n", "type": "registry:block", "target": "components/uitripled/kanban-board-shadcnui.tsx" } ] }