{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kanban-board",
  "title": "Kanban Board",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-react/blocks/kanban-board/KanbanBoard.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Plus } from 'lucide-react'\nimport { type KanbanColumn as KanbanColumnType, type KanbanTask, findTaskById } from '@/lib/use-kanban'\nimport { PageHeader, PageHeaderHeading } from '@/components/ui/page'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { KanbanToolbar } from './KanbanToolbar'\nimport { KanbanColumn, type FilteredColumn } from './KanbanColumn'\nimport { KanbanListView } from './KanbanListView'\nimport { KanbanTaskSheet } from './KanbanTaskSheet'\nimport { KanbanAddTaskDialog } from './KanbanAddTaskDialog'\n\nexport interface KanbanBoardProps {\n  columns: KanbanColumnType[]\n  onColumnsChange?: (value: KanbanColumnType[]) => void\n  title?: string | null\n  description?: string | null\n  defaultColumnId?: string\n  hideHeader?: boolean\n  hideToolbar?: boolean\n  lockParentScroll?: boolean\n}\n\nexport function KanbanBoard({\n  columns,\n  onColumnsChange,\n  title = 'Kanban Board',\n  description = 'Drag tasks across columns to update their status.',\n  defaultColumnId = 'backlog',\n  hideHeader = false,\n  hideToolbar = false,\n  lockParentScroll = true,\n}: KanbanBoardProps) {\n  const kanbanEl = React.useRef<HTMLDivElement | null>(null)\n\n  React.useEffect(() => {\n    if (!lockParentScroll) return\n    const parentMain = kanbanEl.current?.closest('main[data-slot=\"sidebar-inset\"]') as HTMLElement | null\n    if (parentMain) parentMain.style.overflow = 'hidden'\n    document.documentElement.style.overflow = 'hidden'\n    return () => {\n      if (parentMain) parentMain.style.overflow = ''\n      document.documentElement.style.overflow = ''\n    }\n  }, [lockParentScroll])\n\n  const [searchQuery, setSearchQuery] = React.useState('')\n  const [selectedPriority, setSelectedPriority] = React.useState<string | null>(null)\n  const [selectedAssignee, setSelectedAssignee] = React.useState<string | null>(null)\n  const [viewMode, setViewMode] = React.useState<'board' | 'list'>('board')\n  const [collapsedColumns, setCollapsedColumns] = React.useState<Set<string>>(new Set())\n\n  const [detailOpen, setDetailOpen] = React.useState(false)\n  const [detailTask, setDetailTask] = React.useState<KanbanTask | null>(null)\n\n  const [addTaskOpen, setAddTaskOpen] = React.useState(false)\n  const [addTaskColumnId, setAddTaskColumnId] = React.useState(defaultColumnId)\n\n  const draggedTaskRef = React.useRef<string | null>(null)\n  const dragOverColumnRef = React.useRef<string | null>(null)\n  const dropTargetIndexRef = React.useRef<number>(-1)\n  const lastDragEndRef = React.useRef(0)\n\n  const [draggedTask, setDraggedTask] = React.useState<string | null>(null)\n  const [dragOverColumn, setDragOverColumn] = React.useState<string | null>(null)\n  const [dropTargetIndex, setDropTargetIndex] = React.useState<number>(-1)\n\n  // Mutating helper: the consumer owns `columns`, so we clone, mutate the\n  // clone, and emit it back via onColumnsChange — mirroring the Vue v-model.\n  function commitColumns(mutator: (cols: KanbanColumnType[]) => void) {\n    const next = columns.map((c) => ({ ...c, tasks: [...c.tasks] }))\n    mutator(next)\n    onColumnsChange?.(next)\n  }\n\n  const filteredColumns = React.useMemo<FilteredColumn[]>(() => {\n    return columns.map((col) => ({\n      ...col,\n      tasks: col.tasks.filter((task) => {\n        const q = searchQuery.toLowerCase()\n        const matchesSearch = !q || task.title.toLowerCase().includes(q) || task.id.toLowerCase().includes(q)\n        const matchesPriority = !selectedPriority || task.priority === selectedPriority\n        const matchesAssignee = !selectedAssignee || task.assignee.name === selectedAssignee\n        return matchesSearch && matchesPriority && matchesAssignee\n      }),\n    }))\n  }, [columns, searchQuery, selectedPriority, selectedAssignee])\n\n  const totalTasks = React.useMemo(() => columns.reduce((sum, col) => sum + col.tasks.length, 0), [columns])\n\n  function toggleCollapse(columnId: string) {\n    setCollapsedColumns((prev) => {\n      const next = new Set(prev)\n      if (next.has(columnId)) next.delete(columnId)\n      else next.add(columnId)\n      return next\n    })\n  }\n\n  function addComment(task: KanbanTask, text: string) {\n    commitColumns((cols) => {\n      const target = findTaskById(cols, task.id)\n      if (target) {\n        target.commentItems = [\n          ...target.commentItems,\n          {\n            id: `c${Date.now()}`,\n            author: 'Admin User',\n            authorColor: 'bg-chart-1/15 text-chart-1',\n            text,\n            time: 'Just now',\n          },\n        ]\n      }\n    })\n  }\n\n  function moveTask(task: KanbanTask, targetColumnId: string) {\n    commitColumns((cols) => {\n      const sourceCol = cols.find((c) => c.tasks.some((t) => t.id === task.id))\n      const targetCol = cols.find((c) => c.id === targetColumnId)\n      if (!sourceCol || !targetCol || sourceCol.id === targetColumnId) return\n      const taskIndex = sourceCol.tasks.findIndex((t) => t.id === task.id)\n      if (taskIndex === -1) return\n      const removed = sourceCol.tasks.splice(taskIndex, 1)\n      if (removed[0]) targetCol.tasks.push(removed[0])\n    })\n  }\n\n  function openTaskDetail(task: KanbanTask) {\n    setDetailTask(task)\n    setDetailOpen(true)\n  }\n\n  function onDragStart(event: React.DragEvent, taskId: string) {\n    draggedTaskRef.current = taskId\n    setDraggedTask(taskId)\n    if (event.dataTransfer) {\n      event.dataTransfer.effectAllowed = 'move'\n      event.dataTransfer.setData('text/plain', taskId)\n    }\n  }\n\n  function resetDrag() {\n    draggedTaskRef.current = null\n    dragOverColumnRef.current = null\n    dropTargetIndexRef.current = -1\n    setDraggedTask(null)\n    setDragOverColumn(null)\n    setDropTargetIndex(-1)\n    lastDragEndRef.current = Date.now()\n  }\n\n  function onDrop() {\n    const dragged = draggedTaskRef.current\n    const overColumn = dragOverColumnRef.current\n    if (!dragged || !overColumn) {\n      resetDrag()\n      return\n    }\n\n    commitColumns((cols) => {\n      let sourceColIdx = -1\n      let taskIdx = -1\n      for (let c = 0; c < cols.length; c++) {\n        const col = cols[c]\n        if (!col) continue\n        const tIdx = col.tasks.findIndex((t) => t.id === dragged)\n        if (tIdx !== -1) {\n          sourceColIdx = c\n          taskIdx = tIdx\n          break\n        }\n      }\n      const targetColIdx = cols.findIndex((c) => c.id === overColumn)\n      if (sourceColIdx === -1 || targetColIdx === -1) return\n\n      const sourceCol = cols[sourceColIdx]\n      const targetCol = cols[targetColIdx]\n      if (!sourceCol || !targetCol) return\n\n      const [task] = sourceCol.tasks.splice(taskIdx, 1)\n      if (!task) return\n\n      let insertAt = dropTargetIndexRef.current\n      if (insertAt < 0) insertAt = targetCol.tasks.length\n      if (sourceColIdx === targetColIdx && taskIdx < insertAt) insertAt--\n\n      targetCol.tasks.splice(insertAt, 0, task)\n    })\n    resetDrag()\n  }\n\n  function onCardClick(task: KanbanTask) {\n    if (Date.now() - lastDragEndRef.current < 200) return\n    openTaskDetail(task)\n  }\n\n  function onCardDragOver(event: React.DragEvent, columnId: string, taskIndex: number) {\n    dragOverColumnRef.current = columnId\n    setDragOverColumn(columnId)\n    const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()\n    const midY = rect.top + rect.height / 2\n    const idx = event.clientY < midY ? taskIndex : taskIndex + 1\n    dropTargetIndexRef.current = idx\n    setDropTargetIndex(idx)\n  }\n\n  function onLaneDragOver(_event: React.DragEvent, columnId: string, taskCount: number) {\n    dragOverColumnRef.current = columnId\n    setDragOverColumn(columnId)\n    dropTargetIndexRef.current = taskCount\n    setDropTargetIndex(taskCount)\n  }\n\n  function openAddTask(columnId: string) {\n    setAddTaskColumnId(columnId)\n    setAddTaskOpen(true)\n  }\n\n  function onCreateTask(columnId: string, tasks: KanbanTask[]) {\n    commitColumns((cols) => {\n      const col = cols.find((c) => c.id === columnId)\n      if (col) col.tasks.push(...tasks)\n    })\n  }\n\n  return (\n    <div\n      ref={kanbanEl}\n      data-slot=\"kanban-board\"\n      className=\"kanban-page flex h-[calc(100dvh-3.5rem-3rem)] flex-col overflow-hidden lg:h-[calc(100dvh-3.5rem-3rem)]\"\n    >\n      {!hideHeader && (\n        <div className=\"mb-3 shrink-0\">\n          <PageHeader>\n            <div className=\"flex items-start justify-between gap-4\">\n              <PageHeaderHeading title={title ?? ''} description={description ?? ''} />\n              <div className=\"flex shrink-0 items-center gap-2\">\n                <Badge variant=\"secondary\" className=\"font-mono text-xs tabular-nums\">\n                  {totalTasks} tasks\n                </Badge>\n                <Button size=\"sm\" onClick={() => openAddTask(defaultColumnId)}>\n                  <Plus className=\"size-4\" />\n                  Add Task\n                </Button>\n              </div>\n            </div>\n          </PageHeader>\n        </div>\n      )}\n\n      {!hideToolbar && (\n        <KanbanToolbar\n          searchQuery={searchQuery}\n          selectedPriority={selectedPriority}\n          selectedAssignee={selectedAssignee}\n          viewMode={viewMode}\n          onSearchQueryChange={setSearchQuery}\n          onSelectedPriorityChange={setSelectedPriority}\n          onSelectedAssigneeChange={setSelectedAssignee}\n          onViewModeChange={setViewMode}\n        />\n      )}\n\n      {viewMode === 'board' ? (\n        <div className=\"kanban-board relative flex min-h-0 flex-1 items-start gap-3 overflow-auto pb-3\">\n          {filteredColumns.map((column) => (\n            <KanbanColumn\n              key={column.id}\n              column={column}\n              collapsed={collapsedColumns.has(column.id)}\n              draggedTask={draggedTask}\n              dragOverColumn={dragOverColumn}\n              dropTargetIndex={dropTargetIndex}\n              allColumns={columns}\n              onToggleCollapse={toggleCollapse}\n              onAddTask={openAddTask}\n              onCardClick={onCardClick}\n              onCardQuickView={openTaskDetail}\n              onDragStart={onDragStart}\n              onDragEnd={resetDrag}\n              onCardDragOver={onCardDragOver}\n              onLaneDragOver={onLaneDragOver}\n              onDrop={onDrop}\n            />\n          ))}\n        </div>\n      ) : (\n        <KanbanListView\n          columns={filteredColumns}\n          allColumns={columns}\n          onTaskClick={openTaskDetail}\n          onMoveTask={moveTask}\n        />\n      )}\n\n      <KanbanTaskSheet\n        open={detailOpen}\n        task={detailTask}\n        columns={columns}\n        onOpenChange={setDetailOpen}\n        onMoveTask={moveTask}\n        onAddComment={addComment}\n      />\n\n      <KanbanAddTaskDialog\n        open={addTaskOpen}\n        columns={columns}\n        initialColumnId={addTaskColumnId}\n        onOpenChange={setAddTaskOpen}\n        onCreate={onCreateTask}\n      />\n\n      <style>{`\n        .kanban-board {\n          scrollbar-width: thin;\n          scrollbar-color: hsl(var(--border)) transparent;\n        }\n        .kanban-board::-webkit-scrollbar {\n          height: 6px;\n          width: 6px;\n        }\n        .kanban-board::-webkit-scrollbar-thumb {\n          background-color: hsl(var(--border));\n          border-radius: 3px;\n        }\n        .kanban-board::-webkit-scrollbar-corner {\n          background: transparent;\n        }\n        .kanban-card {\n          animation: card-in 0.25s ease-out both;\n        }\n        @keyframes card-in {\n          from {\n            opacity: 0;\n            transform: translateY(6px);\n          }\n        }\n        .kanban-card:hover .kanban-accent {\n          box-shadow: 0 0 3px currentColor;\n        }\n        .kanban-lane {\n          min-height: 60px;\n        }\n        .kanban-list {\n          scrollbar-width: thin;\n          scrollbar-color: hsl(var(--border)) transparent;\n        }\n        .kanban-list::-webkit-scrollbar {\n          height: 6px;\n          width: 6px;\n        }\n        .kanban-list::-webkit-scrollbar-thumb {\n          background-color: hsl(var(--border));\n          border-radius: 3px;\n        }\n      `}</style>\n    </div>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/KanbanBoard.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/KanbanCard.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { MoreHorizontal, MessageSquare, Paperclip, ExternalLink } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { type KanbanColumn as KanbanColumnType, type KanbanTask, priorityConfig, getTaskColumn } from '@/lib/use-kanban'\nimport { Button } from '@/components/ui/button'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'\nimport { TagBadge } from './TagBadge'\nimport { SubtaskProgress } from './SubtaskProgress'\nimport { DueDateBadge } from './DueDateBadge'\nimport { UserAvatar } from './UserAvatar'\n\nexport function KanbanCard({\n  task,\n  isDone,\n  columns,\n  onClick,\n  onQuickView,\n}: {\n  task: KanbanTask\n  isDone: boolean\n  columns: KanbanColumnType[]\n  onClick?: (task: KanbanTask) => void\n  onQuickView?: (task: KanbanTask) => void\n}) {\n  const subtasksDone = React.useMemo(() => {\n    if (!columns || !task.subtaskIds.length) return 0\n    return task.subtaskIds.filter((id) => getTaskColumn(columns, id)?.id === 'done').length\n  }, [columns, task.subtaskIds])\n\n  return (\n    <div\n      className={cn(\n        'kanban-card group/card bg-card relative cursor-grab rounded-lg border p-3 transition-all duration-150',\n        'hover:border-border hover:shadow-md active:scale-[0.97] active:cursor-grabbing',\n        isDone ? 'opacity-75 hover:opacity-100' : '',\n      )}\n      onClick={() => onClick?.(task)}\n    >\n      <div\n        className={cn(\n          'kanban-accent absolute top-3 bottom-3 left-0 w-[1.5px] rounded-full transition-all duration-150',\n          priorityConfig[task.priority].bg,\n          task.priority === 'low' ? 'opacity-40' : task.priority === 'medium' ? 'opacity-60' : 'opacity-90',\n        )}\n      />\n\n      <div className=\"mb-1 flex items-center justify-between pl-2\">\n        <span className=\"text-muted-foreground/70 font-mono text-[11px]\">{task.id}</span>\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              className=\"text-muted-foreground -mr-1 size-6 opacity-0 transition-opacity group-hover/card:opacity-100\"\n              onClick={(e) => e.stopPropagation()}\n            >\n              <MoreHorizontal className=\"size-3.5\" />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\" className=\"w-36\">\n            <DropdownMenuItem\n              onClick={(e) => {\n                e.stopPropagation()\n                onQuickView?.(task)\n              }}\n            >\n              Quick view\n            </DropdownMenuItem>\n            <DropdownMenuItem asChild>\n              <a href={`/dashboard/kanban/${task.id}`} className=\"gap-2\">\n                <ExternalLink className=\"size-3.5\" />\n                Open detail\n              </a>\n            </DropdownMenuItem>\n            <DropdownMenuItem>Edit</DropdownMenuItem>\n            <DropdownMenuItem>Move to...</DropdownMenuItem>\n            <DropdownMenuItem>Assign to...</DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem className=\"text-destructive\">Delete</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n\n      <p\n        className={cn(\n          'mb-2 pl-2 text-[13px] leading-snug font-medium',\n          isDone ? 'decoration-muted-foreground/40 line-through' : '',\n        )}\n      >\n        {task.title}\n      </p>\n\n      {task.tags.length > 0 && (\n        <div className=\"mb-2 flex flex-wrap gap-1 pl-2\">\n          {task.tags.map((tag) => (\n            <TagBadge key={tag.label} label={tag.label} color={tag.color} />\n          ))}\n        </div>\n      )}\n\n      {task.subtaskIds.length > 0 && (\n        <div className=\"mb-2 pl-2\">\n          <SubtaskProgress done={subtasksDone} total={task.subtaskIds.length} />\n        </div>\n      )}\n\n      <div className=\"flex items-center gap-2 pl-2\">\n        {task.dueDate && <DueDateBadge dueDate={task.dueDate} variant=\"chip\" />}\n\n        {task.commentItems.length > 0 && (\n          <div className=\"text-muted-foreground/70 flex items-center gap-1 text-[11px]\">\n            <MessageSquare className=\"size-3\" />\n            {task.commentItems.length}\n          </div>\n        )}\n\n        {task.fileItems.length > 0 && (\n          <div className=\"text-muted-foreground/70 flex items-center gap-1 text-[11px]\">\n            <Paperclip className=\"size-3\" />\n            {task.fileItems.length}\n          </div>\n        )}\n\n        <div className=\"ml-auto\">\n          <TooltipProvider delayDuration={200}>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <span>\n                  <UserAvatar name={task.assignee.name} color={task.assignee.color} size=\"xs\" />\n                </span>\n              </TooltipTrigger>\n              <TooltipContent side=\"bottom\" className=\"text-xs\">\n                {task.assignee.name}\n              </TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n        </div>\n      </div>\n    </div>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/KanbanCard.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/KanbanColumn.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Plus, MoreHorizontal, ChevronsLeft, ChevronsRight } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { type KanbanColumn as KanbanColumnType, type KanbanTask } from '@/lib/use-kanban'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { KanbanCard } from './KanbanCard'\n\nexport interface FilteredColumn {\n  id: string\n  title: string\n  color: string\n  dotColor: string\n  tasks: KanbanTask[]\n}\n\nexport function KanbanColumn({\n  column,\n  collapsed,\n  draggedTask,\n  dragOverColumn,\n  dropTargetIndex,\n  allColumns,\n  onToggleCollapse,\n  onAddTask,\n  onCardClick,\n  onCardQuickView,\n  onDragStart,\n  onDragEnd,\n  onCardDragOver,\n  onLaneDragOver,\n  onDrop,\n}: {\n  column: FilteredColumn\n  collapsed: boolean\n  draggedTask: string | null\n  dragOverColumn: string | null\n  dropTargetIndex: number\n  allColumns: KanbanColumnType[]\n  onToggleCollapse: (columnId: string) => void\n  onAddTask: (columnId: string) => void\n  onCardClick: (task: KanbanTask) => void\n  onCardQuickView: (task: KanbanTask) => void\n  onDragStart: (event: React.DragEvent, taskId: string) => void\n  onDragEnd: () => void\n  onCardDragOver: (event: React.DragEvent, columnId: string, taskIndex: number) => void\n  onLaneDragOver: (event: React.DragEvent, columnId: string, taskCount: number) => void\n  onDrop: () => void\n}) {\n  return (\n    <div\n      className={cn('group/col flex shrink-0 flex-col transition-all duration-200', collapsed ? 'w-12' : 'w-[300px]')}\n      onDragOver={(e) => e.preventDefault()}\n      onDrop={() => onDrop()}\n    >\n      {collapsed ? (\n        <button\n          className=\"bg-muted/40 hover:bg-muted/60 flex h-full flex-col items-center gap-2 rounded-xl px-1 pt-3 pb-4 transition-colors\"\n          onClick={() => onToggleCollapse(column.id)}\n        >\n          <span className={cn('size-2 shrink-0 rounded-full', column.dotColor)} />\n          <span\n            className={cn(\n              'text-[11px] font-semibold tracking-tight',\n              column.color,\n              'rotate-180 [writing-mode:vertical-lr]',\n            )}\n          >\n            {column.title}\n          </span>\n          <Badge variant=\"secondary\" className=\"mt-1 h-5 min-w-5 justify-center rounded-md px-1 text-[10px]\">\n            {column.tasks.length}\n          </Badge>\n          <ChevronsRight className=\"text-muted-foreground mt-auto size-3.5\" />\n        </button>\n      ) : (\n        <>\n          <div className=\"bg-background/95 sticky top-0 z-10 mb-2 flex items-center gap-2 px-2 py-1.5 backdrop-blur-sm\">\n            <button\n              className=\"text-muted-foreground/50 hover:text-muted-foreground shrink-0 transition-colors\"\n              title=\"Collapse column\"\n              onClick={() => onToggleCollapse(column.id)}\n            >\n              <ChevronsLeft className=\"size-3.5\" />\n            </button>\n            <span className={cn('size-2 shrink-0 rounded-full', column.dotColor)} />\n            <h3 className={cn('text-[13px] font-semibold tracking-tight', column.color)}>{column.title}</h3>\n            <span className=\"text-muted-foreground bg-muted rounded-md px-1.5 py-0.5 text-[11px] font-medium tabular-nums\">\n              {column.tasks.length}\n            </span>\n            <div className=\"ml-auto flex items-center\">\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"text-muted-foreground size-6 opacity-0 transition-opacity group-hover/col:opacity-100\"\n              >\n                <MoreHorizontal className=\"size-3.5\" />\n              </Button>\n              <Button\n                variant=\"ghost\"\n                size=\"icon\"\n                className=\"text-muted-foreground size-6\"\n                onClick={() => onAddTask(column.id)}\n              >\n                <Plus className=\"size-3.5\" />\n              </Button>\n            </div>\n          </div>\n\n          <div\n            className={cn(\n              'kanban-lane flex flex-col rounded-xl p-2 transition-all duration-200',\n              dragOverColumn === column.id && draggedTask\n                ? 'bg-primary/[0.06] ring-primary/25 ring-1 ring-inset'\n                : 'bg-muted/40',\n            )}\n            onDragOver={(e) => {\n              e.preventDefault()\n              onLaneDragOver(e, column.id, column.tasks.length)\n            }}\n          >\n            {column.tasks.map((task, taskIndex) => (\n              <React.Fragment key={task.id}>\n                <div\n                  className={cn(\n                    'drop-indicator mx-1 transition-all duration-150',\n                    dragOverColumn === column.id &&\n                      dropTargetIndex === taskIndex &&\n                      draggedTask &&\n                      draggedTask !== task.id\n                      ? 'bg-primary h-0.5 rounded-full'\n                      : 'h-0',\n                  )}\n                />\n                <div\n                  data-task-id={task.id}\n                  draggable=\"true\"\n                  className={cn(\n                    'mt-2 first:mt-0',\n                    draggedTask === task.id ? 'scale-95 rotate-1 opacity-30' : 'opacity-100',\n                  )}\n                  onDragStart={(e) => onDragStart(e, task.id)}\n                  onDragEnd={() => onDragEnd()}\n                  onDragOver={(e) => {\n                    e.stopPropagation()\n                    e.preventDefault()\n                    onCardDragOver(e, column.id, taskIndex)\n                  }}\n                >\n                  <KanbanCard\n                    task={task}\n                    isDone={column.id === 'done'}\n                    columns={allColumns}\n                    onClick={onCardClick}\n                    onQuickView={onCardQuickView}\n                  />\n                </div>\n              </React.Fragment>\n            ))}\n\n            {column.tasks.length > 0 && (\n              <div\n                className={cn(\n                  'drop-indicator mx-1 transition-all duration-150',\n                  dragOverColumn === column.id && dropTargetIndex === column.tasks.length && draggedTask\n                    ? 'bg-primary mt-2 h-0.5 rounded-full'\n                    : 'h-0',\n                )}\n              />\n            )}\n\n            {column.tasks.length === 0 && (\n              <button\n                className=\"text-muted-foreground/50 hover:text-muted-foreground hover:border-muted-foreground/30 flex flex-1 flex-col items-center justify-center rounded-lg border border-dashed py-10 transition-colors\"\n                onClick={() => onAddTask(column.id)}\n              >\n                <Plus className=\"mb-1 size-4\" />\n                <p className=\"text-xs\">No tasks</p>\n              </button>\n            )}\n          </div>\n\n          <button\n            className=\"text-muted-foreground hover:text-foreground hover:bg-muted/60 mt-2 flex w-full items-center justify-center gap-1.5 rounded-lg border border-dashed py-2 text-xs transition-colors\"\n            onClick={() => onAddTask(column.id)}\n          >\n            <Plus className=\"size-3.5\" />\n            Add task\n          </button>\n        </>\n      )}\n    </div>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/KanbanColumn.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/KanbanListView.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { ChevronDown, ChevronRight, ExternalLink, MessageSquare, Paperclip, ArrowUpDown } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { type KanbanColumn as KanbanColumnType, type KanbanTask, getTaskColumn } from '@/lib/use-kanban'\nimport { Badge } from '@/components/ui/badge'\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'\nimport { type FilteredColumn } from './KanbanColumn'\nimport { TagBadge } from './TagBadge'\nimport { SubtaskProgress } from './SubtaskProgress'\nimport { PriorityBadge } from './PriorityBadge'\nimport { DueDateBadge } from './DueDateBadge'\nimport { UserAvatar } from './UserAvatar'\n\ntype SortField = 'id' | 'title' | 'priority' | 'assignee' | 'dueDate' | 'status'\ntype SortDir = 'asc' | 'desc'\n\ninterface FlatTask {\n  task: KanbanTask\n  columnId: string\n  columnTitle: string\n  dotColor: string\n}\n\nconst priorityOrder: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3 }\n\nexport function KanbanListView({\n  columns,\n  allColumns,\n  onTaskClick,\n  onMoveTask,\n}: {\n  columns: FilteredColumn[]\n  allColumns: KanbanColumnType[]\n  onTaskClick: (task: KanbanTask) => void\n  onMoveTask: (task: KanbanTask, targetColumnId: string) => void\n}) {\n  const [sortField, setSortField] = React.useState<SortField>('status')\n  const [sortDir, setSortDir] = React.useState<SortDir>('asc')\n  const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set())\n\n  function toggleSort(field: SortField) {\n    if (sortField === field) {\n      setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))\n    } else {\n      setSortField(field)\n      setSortDir('asc')\n    }\n  }\n\n  function toggleGroup(columnId: string) {\n    setCollapsedGroups((prev) => {\n      const next = new Set(prev)\n      if (next.has(columnId)) next.delete(columnId)\n      else next.add(columnId)\n      return next\n    })\n  }\n\n  const flatTasks = React.useMemo<FlatTask[]>(() => {\n    const items: FlatTask[] = []\n    for (const col of columns) {\n      for (const task of col.tasks) {\n        items.push({ task, columnId: col.id, columnTitle: col.title, dotColor: col.dotColor })\n      }\n    }\n    return [...items].sort((a, b) => {\n      let cmp = 0\n      switch (sortField) {\n        case 'id': {\n          const numA = parseInt(a.task.id.replace(/^[A-Z]+-/, ''), 10)\n          const numB = parseInt(b.task.id.replace(/^[A-Z]+-/, ''), 10)\n          cmp = numA - numB\n          break\n        }\n        case 'title':\n          cmp = a.task.title.localeCompare(b.task.title)\n          break\n        case 'priority':\n          cmp = (priorityOrder[a.task.priority] ?? 99) - (priorityOrder[b.task.priority] ?? 99)\n          break\n        case 'assignee':\n          cmp = a.task.assignee.name.localeCompare(b.task.assignee.name)\n          break\n        case 'dueDate':\n          cmp = (a.task.dueDate ?? '9999').localeCompare(b.task.dueDate ?? '9999')\n          break\n        case 'status': {\n          const colOrder = allColumns.map((c) => c.id)\n          cmp = colOrder.indexOf(a.columnId) - colOrder.indexOf(b.columnId)\n          break\n        }\n      }\n      return sortDir === 'desc' ? -cmp : cmp\n    })\n  }, [columns, allColumns, sortField, sortDir])\n\n  const groupedTasks = React.useMemo(() => {\n    const groups: { column: KanbanColumnType; tasks: FlatTask[] }[] = []\n    for (const col of allColumns) {\n      const tasks = flatTasks.filter((t) => t.columnId === col.id)\n      groups.push({ column: col, tasks })\n    }\n    return groups\n  }, [allColumns, flatTasks])\n\n  function subtasksDone(task: KanbanTask): number {\n    if (!task.subtaskIds.length) return 0\n    return task.subtaskIds.filter((id) => getTaskColumn(allColumns, id)?.id === 'done').length\n  }\n\n  const headerCols: { field: SortField; label: string }[] = [\n    { field: 'id', label: 'ID' },\n    { field: 'title', label: 'Task' },\n    { field: 'status', label: 'Status' },\n    { field: 'priority', label: 'Priority' },\n    { field: 'assignee', label: 'Assignee' },\n    { field: 'dueDate', label: 'Due' },\n  ]\n\n  return (\n    <div className=\"kanban-list flex min-h-0 flex-1 flex-col overflow-auto pb-3\">\n      <div className=\"bg-muted/50 sticky top-0 z-10 grid grid-cols-[60px_1fr_100px_110px_130px_100px_80px] items-center gap-2 rounded-t-lg border px-3 py-2 text-[11px] font-semibold tracking-wider uppercase\">\n        {headerCols.map((h) => (\n          <button key={h.field} className=\"flex items-center gap-1 text-left\" onClick={() => toggleSort(h.field)}>\n            {h.label}\n            <ArrowUpDown\n              className={cn('size-3', sortField === h.field ? 'text-foreground' : 'text-muted-foreground/50')}\n            />\n          </button>\n        ))}\n        <span className=\"text-center\">Info</span>\n      </div>\n\n      {groupedTasks.map((group) => (\n        <React.Fragment key={group.column.id}>\n          <button\n            className=\"bg-muted/30 hover:bg-muted/50 flex items-center gap-2 border-x border-b px-3 py-1.5 text-left transition-colors\"\n            onClick={() => toggleGroup(group.column.id)}\n          >\n            {collapsedGroups.has(group.column.id) ? (\n              <ChevronRight className=\"text-muted-foreground size-3.5\" />\n            ) : (\n              <ChevronDown className=\"text-muted-foreground size-3.5\" />\n            )}\n            <span className={cn('size-2 rounded-full', group.column.dotColor)} />\n            <span className=\"text-sm font-medium\">{group.column.title}</span>\n            <Badge variant=\"secondary\" className=\"ml-1 h-4 px-1.5 text-[10px] tabular-nums\">\n              {group.tasks.length}\n            </Badge>\n          </button>\n\n          {!collapsedGroups.has(group.column.id) &&\n            group.tasks.map((item) => (\n              <div\n                key={item.task.id}\n                className=\"hover:bg-muted/30 grid cursor-pointer grid-cols-[60px_1fr_100px_110px_130px_100px_80px] items-center gap-2 border-x border-b px-3 py-2 transition-colors\"\n                onClick={() => onTaskClick(item.task)}\n              >\n                <span className=\"text-muted-foreground font-mono text-[11px]\">{item.task.id}</span>\n\n                <div className=\"min-w-0\">\n                  <div className=\"flex items-center gap-2\">\n                    <span\n                      className={cn(\n                        'truncate text-[13px] font-medium',\n                        item.columnId === 'done' ? 'text-muted-foreground line-through' : '',\n                      )}\n                    >\n                      {item.task.title}\n                    </span>\n                    <a\n                      href={`/dashboard/kanban/${item.task.id}`}\n                      className=\"text-muted-foreground hover:text-foreground shrink-0 opacity-0 transition-opacity group-hover/row:opacity-100\"\n                      onClick={(e) => e.stopPropagation()}\n                    >\n                      <ExternalLink className=\"size-3\" />\n                    </a>\n                  </div>\n                  {(item.task.tags.length > 0 || item.task.subtaskIds.length > 0) && (\n                    <div className=\"mt-0.5 flex items-center gap-1.5\">\n                      {item.task.tags.map((tag) => (\n                        <TagBadge\n                          key={tag.label}\n                          label={tag.label}\n                          color={tag.color}\n                          className=\"!px-1.5 !py-0 !text-[9px]\"\n                        />\n                      ))}\n                      {item.task.subtaskIds.length > 0 && (\n                        <SubtaskProgress\n                          done={subtasksDone(item.task)}\n                          total={item.task.subtaskIds.length}\n                          className=\"ml-1\"\n                        />\n                      )}\n                    </div>\n                  )}\n                </div>\n\n                <div>\n                  <Select\n                    value={item.columnId}\n                    onValueChange={(val) => onMoveTask(item.task, String(val))}\n                  >\n                    <SelectTrigger\n                      className=\"hover:bg-muted h-6 w-auto gap-1 rounded-md border-none bg-transparent px-1.5 text-[11px] font-medium shadow-none\"\n                      onClick={(e) => e.stopPropagation()}\n                    >\n                      <span className=\"flex items-center gap-1.5\">\n                        <span className={cn('size-1.5 rounded-full', item.dotColor)} />\n                        <SelectValue />\n                      </span>\n                    </SelectTrigger>\n                    <SelectContent>\n                      {allColumns.map((col) => (\n                        <SelectItem key={col.id} value={col.id}>\n                          <span className=\"flex items-center gap-1.5\">\n                            <span className={cn('size-1.5 rounded-full', col.dotColor)} />\n                            {col.title}\n                          </span>\n                        </SelectItem>\n                      ))}\n                    </SelectContent>\n                  </Select>\n                </div>\n\n                <PriorityBadge priority={item.task.priority} />\n\n                <div className=\"flex items-center gap-2\">\n                  <UserAvatar name={item.task.assignee.name} color={item.task.assignee.color} size=\"xs\" />\n                  <span className=\"truncate text-[12px]\">{item.task.assignee.name}</span>\n                </div>\n\n                <div>\n                  {item.task.dueDate ? (\n                    <DueDateBadge dueDate={item.task.dueDate} variant=\"chip\" />\n                  ) : (\n                    <span className=\"text-muted-foreground/50 text-[11px]\">—</span>\n                  )}\n                </div>\n\n                <div className=\"flex items-center justify-center gap-2\">\n                  <TooltipProvider delayDuration={200}>\n                    {item.task.commentItems.length > 0 && (\n                      <Tooltip>\n                        <TooltipTrigger asChild>\n                          <span className=\"text-muted-foreground/70 flex items-center gap-0.5 text-[11px]\">\n                            <MessageSquare className=\"size-3\" />\n                            {item.task.commentItems.length}\n                          </span>\n                        </TooltipTrigger>\n                        <TooltipContent className=\"text-xs\">{item.task.commentItems.length} comments</TooltipContent>\n                      </Tooltip>\n                    )}\n                    {item.task.fileItems.length > 0 && (\n                      <Tooltip>\n                        <TooltipTrigger asChild>\n                          <span className=\"text-muted-foreground/70 flex items-center gap-0.5 text-[11px]\">\n                            <Paperclip className=\"size-3\" />\n                            {item.task.fileItems.length}\n                          </span>\n                        </TooltipTrigger>\n                        <TooltipContent className=\"text-xs\">{item.task.fileItems.length} files</TooltipContent>\n                      </Tooltip>\n                    )}\n                  </TooltipProvider>\n                </div>\n              </div>\n            ))}\n        </React.Fragment>\n      ))}\n\n      {flatTasks.length === 0 && (\n        <div className=\"text-muted-foreground flex flex-1 items-center justify-center rounded-b-lg border-x border-b py-12 text-sm\">\n          No tasks match your filters.\n        </div>\n      )}\n    </div>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/KanbanListView.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/KanbanTaskSheet.tsx",
      "content": "'use client'\n\nimport { Clock, ExternalLink, Download } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { type KanbanColumn as KanbanColumnType, type KanbanTask, priorityConfig, fileIconMap } from '@/lib/use-kanban'\nimport { Button } from '@/components/ui/button'\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport { Sheet, SheetContent, SheetTitle, SheetDescription, SheetFooter, SheetClose } from '@/components/ui/sheet'\nimport { TagBadge } from './TagBadge'\nimport { PriorityBadge } from './PriorityBadge'\nimport { DueDateBadge } from './DueDateBadge'\nimport { UserAvatar } from './UserAvatar'\nimport { SubtaskList } from './SubtaskList'\nimport { CommentList } from './CommentList'\n\nexport function KanbanTaskSheet({\n  open,\n  task,\n  columns,\n  onOpenChange,\n  onMoveTask,\n  onAddComment,\n}: {\n  open: boolean\n  task: KanbanTask | null\n  columns: KanbanColumnType[]\n  onOpenChange: (value: boolean) => void\n  onMoveTask: (task: KanbanTask, columnId: string) => void\n  onAddComment: (task: KanbanTask, text: string) => void\n}) {\n  const columnIdForTask = task\n    ? (columns.find((c) => c.tasks.some((t) => t.id === task.id))?.id ?? '')\n    : ''\n\n  return (\n    <Sheet open={open} onOpenChange={onOpenChange}>\n      <SheetContent className=\"flex flex-col gap-0 overflow-hidden p-0 sm:max-w-[420px]\">\n        {task && (\n          <>\n            <div className={cn('h-1 w-full shrink-0', priorityConfig[task.priority]?.bg)} />\n\n            <div className=\"shrink-0 px-5 pt-4 pb-3\">\n              <div className=\"mb-3 flex items-center gap-2\">\n                <span className=\"text-muted-foreground font-mono text-[11px] tracking-tight\">{task.id}</span>\n                <span className=\"text-muted-foreground/30\">·</span>\n                <Select value={columnIdForTask} onValueChange={(val) => onMoveTask(task, String(val))}>\n                  <SelectTrigger className=\"hover:bg-secondary h-5 w-auto gap-1 rounded-md border-none bg-transparent px-1.5 text-[11px] font-medium shadow-none\">\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {columns.map((col) => (\n                      <SelectItem key={col.id} value={col.id}>\n                        <span className=\"flex items-center gap-1.5\">\n                          <span className={cn('size-1.5 rounded-full', col.dotColor)} />\n                          {col.title}\n                        </span>\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n                <PriorityBadge priority={task.priority} iconSize=\"size-3\" className=\"ml-auto\" />\n              </div>\n\n              <SheetTitle className=\"text-[15px] leading-snug font-semibold tracking-tight\">{task.title}</SheetTitle>\n              <SheetDescription className=\"sr-only\">Task details</SheetDescription>\n              {task.description ? (\n                <div\n                  className=\"text-muted-foreground rich-text-content prose prose-sm dark:prose-invert mt-1.5 max-w-none text-[13px] leading-relaxed\"\n                  dangerouslySetInnerHTML={{ __html: task.description }}\n                />\n              ) : (\n                <p className=\"text-muted-foreground mt-1.5 text-[13px] leading-relaxed\">No description provided.</p>\n              )}\n\n              <div className=\"mt-3 flex flex-wrap gap-1.5\">\n                {task.tags.length ? (\n                  task.tags.map((tag) => <TagBadge key={tag.label} label={tag.label} color={tag.color} />)\n                ) : (\n                  <span className=\"text-muted-foreground text-[11px]\">No tags</span>\n                )}\n              </div>\n            </div>\n\n            <div className=\"bg-border mx-5 h-px\" />\n\n            <div className=\"flex-1 overflow-y-auto\">\n              <div className=\"space-y-4 px-5 py-3\">\n                <div className=\"flex items-center gap-3\">\n                  <UserAvatar name={task.assignee.name} color={task.assignee.color} size=\"md\" />\n                  <div>\n                    <p className=\"text-[13px] leading-tight font-medium\">{task.assignee.name}</p>\n                    <p className=\"text-muted-foreground text-[11px]\">Assignee</p>\n                  </div>\n                  <div className=\"ml-auto text-right\">\n                    {task.dueDate ? (\n                      <DueDateBadge dueDate={task.dueDate} />\n                    ) : (\n                      <p className=\"text-muted-foreground flex items-center gap-1 text-[13px] leading-tight\">\n                        <Clock className=\"size-3\" />\n                        No due date\n                      </p>\n                    )}\n                  </div>\n                </div>\n\n                {task.parentId && (\n                  <div className=\"flex items-center gap-2\">\n                    <span className=\"text-muted-foreground text-[11px]\">Parent:</span>\n                    <a\n                      href={`/dashboard/kanban/${task.parentId}`}\n                      className=\"text-primary text-[12px] font-medium hover:underline\"\n                      onClick={() => onOpenChange(false)}\n                    >\n                      {task.parentId}\n                    </a>\n                  </div>\n                )}\n\n                <div>\n                  <h4 className=\"mb-2 text-[13px] font-semibold\">\n                    Subtasks\n                    {task.subtaskIds.length > 0 && (\n                      <span className=\"text-muted-foreground font-normal\"> ({task.subtaskIds.length})</span>\n                    )}\n                  </h4>\n                  <SubtaskList subtaskIds={task.subtaskIds} columns={columns} compact />\n                </div>\n\n                <div className=\"bg-border h-px\" />\n\n                <div>\n                  <h4 className=\"mb-2 text-[13px] font-semibold\">\n                    Comments\n                    {task.commentItems.length > 0 && (\n                      <span className=\"text-muted-foreground font-normal\"> ({task.commentItems.length})</span>\n                    )}\n                  </h4>\n                  <CommentList comments={task.commentItems} compact onAdd={(text) => onAddComment(task, text)} />\n                </div>\n\n                <div className=\"bg-border h-px\" />\n\n                <div>\n                  <h4 className=\"mb-2 text-[13px] font-semibold\">\n                    Files\n                    {task.fileItems.length > 0 && (\n                      <span className=\"text-muted-foreground font-normal\"> ({task.fileItems.length})</span>\n                    )}\n                  </h4>\n                  {task.fileItems.length ? (\n                    <div className=\"space-y-1\">\n                      {task.fileItems.map((file) => {\n                        const FileIcon = fileIconMap[file.type]\n                        return (\n                          <div\n                            key={file.id}\n                            className=\"hover:bg-muted/50 group/file flex items-center gap-2.5 rounded-md px-1.5 py-1.5 transition-colors\"\n                          >\n                            <div className=\"bg-muted flex size-8 shrink-0 items-center justify-center rounded-md\">\n                              <FileIcon className=\"text-muted-foreground size-4\" />\n                            </div>\n                            <div className=\"min-w-0 flex-1\">\n                              <p className=\"truncate text-[12px] font-medium\">{file.name}</p>\n                              <p className=\"text-muted-foreground text-[10px]\">{file.size}</p>\n                            </div>\n                            <Button\n                              variant=\"ghost\"\n                              size=\"icon\"\n                              className=\"size-7 shrink-0 opacity-0 transition-opacity group-hover/file:opacity-100\"\n                            >\n                              <Download className=\"size-3.5\" />\n                            </Button>\n                          </div>\n                        )\n                      })}\n                    </div>\n                  ) : (\n                    <p className=\"text-muted-foreground text-[12px]\">No files attached.</p>\n                  )}\n                </div>\n              </div>\n            </div>\n\n            <SheetFooter className=\"shrink-0 border-t px-5 py-3\">\n              <div className=\"flex w-full items-center gap-2\">\n                <a href={`/dashboard/kanban/${task.id}`} className=\"flex-1\" onClick={() => onOpenChange(false)}>\n                  <Button variant=\"outline\" size=\"sm\" className=\"w-full gap-1.5\">\n                    <ExternalLink className=\"size-3.5\" />\n                    View full detail\n                  </Button>\n                </a>\n                <SheetClose asChild>\n                  <Button variant=\"ghost\" size=\"sm\">\n                    Close\n                  </Button>\n                </SheetClose>\n              </div>\n            </SheetFooter>\n          </>\n        )}\n      </SheetContent>\n    </Sheet>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/KanbanTaskSheet.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/KanbanAddTaskDialog.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Plus, X, CheckCircle2, Calendar as CalendarIcon } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport {\n  type KanbanColumn as KanbanColumnType,\n  type KanbanTask,\n  priorityConfig,\n  assignees,\n  tagPresets,\n} from '@/lib/use-kanban'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select'\nimport {\n  Dialog,\n  DialogScrollContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n} from '@/components/ui/dialog'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { Calendar } from '@/components/ui/calendar'\nimport { RichTextEditor } from '@/components/ui/rich-text-editor'\n\ninterface AddTaskForm {\n  title: string\n  description: string\n  priority: KanbanTask['priority']\n  assigneeKey: keyof typeof assignees\n  tagKeys: (keyof typeof tagPresets)[]\n  subtaskTexts: string[]\n}\n\nconst emptyForm = (): AddTaskForm => ({\n  title: '',\n  description: '',\n  priority: 'medium',\n  assigneeKey: 'alice',\n  tagKeys: [],\n  subtaskTexts: [],\n})\n\nexport function KanbanAddTaskDialog({\n  open,\n  columns,\n  initialColumnId,\n  onOpenChange,\n  onCreate,\n}: {\n  open: boolean\n  columns: KanbanColumnType[]\n  initialColumnId: string\n  onOpenChange: (value: boolean) => void\n  onCreate: (columnId: string, tasks: KanbanTask[]) => void\n}) {\n  const [columnId, setColumnId] = React.useState(initialColumnId)\n  const [dueDate, setDueDate] = React.useState<Date | undefined>(undefined)\n  const [form, setForm] = React.useState<AddTaskForm>(emptyForm)\n  const [newSubtaskText, setNewSubtaskText] = React.useState('')\n\n  React.useEffect(() => {\n    setColumnId(initialColumnId)\n  }, [initialColumnId])\n\n  React.useEffect(() => {\n    if (open) {\n      setColumnId(initialColumnId)\n      setForm(emptyForm())\n      setDueDate(undefined)\n      setNewSubtaskText('')\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [open])\n\n  function addSubtask() {\n    const text = newSubtaskText.trim()\n    if (!text) return\n    setForm((f) => ({ ...f, subtaskTexts: [...f.subtaskTexts, text] }))\n    setNewSubtaskText('')\n  }\n\n  function removeSubtask(index: number) {\n    setForm((f) => ({ ...f, subtaskTexts: f.subtaskTexts.filter((_, i) => i !== index) }))\n  }\n\n  function toggleTag(key: keyof typeof tagPresets) {\n    setForm((f) => {\n      const idx = f.tagKeys.indexOf(key)\n      const tagKeys = idx >= 0 ? f.tagKeys.filter((k) => k !== key) : [...f.tagKeys, key]\n      return { ...f, tagKeys }\n    })\n  }\n\n  function submit() {\n    if (!form.title.trim()) return\n    const maxId = columns\n      .flatMap((c) => c.tasks)\n      .reduce((max, t) => {\n        const num = parseInt(t.id.replace(/^[A-Z]+-/, ''), 10)\n        return num > max ? num : max\n      }, 0)\n    const idPrefix = columns.flatMap((c) => c.tasks)[0]?.id.split('-')[0] ?? 'TASK'\n    const parentId = `${idPrefix}-${maxId + 1}`\n    const subtaskTasks: KanbanTask[] = form.subtaskTexts.map((text, i) => ({\n      id: `${idPrefix}-${maxId + 2 + i}`,\n      title: text,\n      priority: 'medium' as const,\n      assignee: assignees[form.assigneeKey],\n      tags: [],\n      parentId,\n      subtaskIds: [],\n      commentItems: [],\n      fileItems: [],\n    }))\n    const newTask: KanbanTask = {\n      id: parentId,\n      title: form.title.trim(),\n      description: form.description.trim() || undefined,\n      priority: form.priority,\n      assignee: assignees[form.assigneeKey],\n      tags: form.tagKeys.map((k) => tagPresets[k]),\n      dueDate: dueDate ? dueDate.toISOString().slice(0, 10) : undefined,\n      subtaskIds: subtaskTasks.map((t) => t.id),\n      commentItems: [],\n      fileItems: [],\n    }\n    onCreate(columnId, [newTask, ...subtaskTasks])\n    onOpenChange(false)\n  }\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogScrollContent className=\"sm:max-w-[680px]\">\n        <DialogHeader>\n          <DialogTitle>New Task</DialogTitle>\n          <DialogDescription>\n            Adding task to {columns.find((c) => c.id === columnId)?.title ?? 'column'}\n          </DialogDescription>\n        </DialogHeader>\n\n        <div className=\"grid gap-4 py-2\">\n          <div className=\"grid gap-1.5\">\n            <Label htmlFor=\"task-title\">Title</Label>\n            <Input\n              id=\"task-title\"\n              value={form.title}\n              placeholder=\"Enter task title\"\n              onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}\n            />\n          </div>\n\n          <div className=\"grid gap-1.5\">\n            <Label>\n              Description\n              <span className=\"text-muted-foreground text-xs\">(optional)</span>\n            </Label>\n            <RichTextEditor\n              value={form.description}\n              onValueChange={(val) => setForm((f) => ({ ...f, description: val }))}\n            />\n          </div>\n\n          <div className=\"grid grid-cols-3 gap-4\">\n            <div className=\"grid gap-1.5\">\n              <Label htmlFor=\"task-priority\">Priority</Label>\n              <Select\n                value={form.priority}\n                onValueChange={(val) => setForm((f) => ({ ...f, priority: val as KanbanTask['priority'] }))}\n              >\n                <SelectTrigger id=\"task-priority\">\n                  <SelectValue placeholder=\"Priority\" />\n                </SelectTrigger>\n                <SelectContent>\n                  {Object.entries(priorityConfig).map(([key, config]) => (\n                    <SelectItem key={key} value={key}>\n                      {config.label}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n\n            <div className=\"grid gap-1.5\">\n              <Label htmlFor=\"task-assignee\">Assignee</Label>\n              <Select\n                value={form.assigneeKey}\n                onValueChange={(val) => setForm((f) => ({ ...f, assigneeKey: val as keyof typeof assignees }))}\n              >\n                <SelectTrigger id=\"task-assignee\">\n                  <SelectValue placeholder=\"Assignee\" />\n                </SelectTrigger>\n                <SelectContent>\n                  {Object.entries(assignees).map(([key, person]) => (\n                    <SelectItem key={key} value={key}>\n                      {person.name}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n\n            <div className=\"grid gap-1.5\">\n              <Label htmlFor=\"task-column\">Column</Label>\n              <Select value={columnId} onValueChange={setColumnId}>\n                <SelectTrigger id=\"task-column\">\n                  <SelectValue placeholder=\"Column\" />\n                </SelectTrigger>\n                <SelectContent>\n                  {columns.map((col) => (\n                    <SelectItem key={col.id} value={col.id}>\n                      {col.title}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n          </div>\n\n          <div className=\"grid gap-1.5\">\n            <Label>\n              Due Date\n              <span className=\"text-muted-foreground text-xs\">(optional)</span>\n            </Label>\n            <Popover>\n              <PopoverTrigger asChild>\n                <Button\n                  variant=\"outline\"\n                  className={cn('w-full justify-start text-left font-normal', !dueDate && 'text-muted-foreground')}\n                >\n                  <CalendarIcon className=\"mr-2 size-4\" />\n                  {dueDate ? dueDate.toLocaleDateString('en-US', { dateStyle: 'medium' }) : 'Pick a date'}\n                </Button>\n              </PopoverTrigger>\n              <PopoverContent className=\"w-auto p-0\">\n                <Calendar mode=\"single\" selected={dueDate} onSelect={setDueDate} />\n              </PopoverContent>\n            </Popover>\n          </div>\n\n          <div className=\"grid gap-1.5\">\n            <Label>\n              Tags\n              <span className=\"text-muted-foreground text-xs\">(optional)</span>\n            </Label>\n            <div className=\"flex flex-wrap gap-2\">\n              {Object.entries(tagPresets).map(([key, tag]) => {\n                const active = form.tagKeys.includes(key as keyof typeof tagPresets)\n                return (\n                  <button\n                    key={key}\n                    type=\"button\"\n                    className={cn(\n                      'inline-flex items-center gap-1 rounded-full border px-3 py-1 text-xs font-medium transition-colors',\n                      active\n                        ? 'bg-primary text-primary-foreground border-transparent'\n                        : 'border-border bg-background text-foreground hover:bg-muted',\n                    )}\n                    onClick={() => toggleTag(key as keyof typeof tagPresets)}\n                  >\n                    {active && <CheckCircle2 className=\"size-3\" />}\n                    {tag.label}\n                  </button>\n                )\n              })}\n            </div>\n          </div>\n\n          <div className=\"grid gap-1.5\">\n            <Label>\n              Subtasks\n              <span className=\"text-muted-foreground text-xs\">(optional)</span>\n            </Label>\n            {form.subtaskTexts.length > 0 && (\n              <div className=\"space-y-2\">\n                {form.subtaskTexts.map((text, index) => (\n                  <div key={index} className=\"flex items-center gap-2\">\n                    <span className=\"flex-1 text-sm\">{text}</span>\n                    <Button variant=\"ghost\" size=\"icon\" className=\"size-6\" onClick={() => removeSubtask(index)}>\n                      <X className=\"size-3\" />\n                    </Button>\n                  </div>\n                ))}\n              </div>\n            )}\n            <div className=\"flex items-center gap-2\">\n              <Input\n                value={newSubtaskText}\n                placeholder=\"Add a subtask\"\n                onChange={(e) => setNewSubtaskText(e.target.value)}\n                onKeyUp={(e) => e.key === 'Enter' && addSubtask()}\n              />\n              <Button variant=\"outline\" size=\"icon\" onClick={addSubtask}>\n                <Plus className=\"size-4\" />\n              </Button>\n            </div>\n          </div>\n        </div>\n\n        <DialogFooter>\n          <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n            Cancel\n          </Button>\n          <Button disabled={!form.title.trim()} onClick={submit}>\n            Create Task\n          </Button>\n        </DialogFooter>\n      </DialogScrollContent>\n    </Dialog>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/KanbanAddTaskDialog.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/KanbanToolbar.tsx",
      "content": "'use client'\n\nimport { Search, Filter, ChevronDown, X, LayoutGrid, List } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { priorityConfig, assignees, getInitials } from '@/lib/use-kanban'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { Input } from '@/components/ui/input'\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'\n\nexport function KanbanToolbar({\n  searchQuery,\n  selectedPriority,\n  selectedAssignee,\n  viewMode,\n  onSearchQueryChange,\n  onSelectedPriorityChange,\n  onSelectedAssigneeChange,\n  onViewModeChange,\n}: {\n  searchQuery: string\n  selectedPriority: string | null\n  selectedAssignee: string | null\n  viewMode: 'board' | 'list'\n  onSearchQueryChange: (value: string) => void\n  onSelectedPriorityChange: (value: string | null) => void\n  onSelectedAssigneeChange: (value: string | null) => void\n  onViewModeChange: (value: 'board' | 'list') => void\n}) {\n  return (\n    <div className=\"mb-3 flex shrink-0 items-center gap-2\">\n      <div className=\"relative w-56\">\n        <Search className=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\" />\n        <Input\n          value={searchQuery}\n          placeholder=\"Search tasks...\"\n          className=\"h-8 pl-8 text-sm\"\n          onChange={(e) => onSearchQueryChange(e.target.value)}\n        />\n      </div>\n\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button variant=\"outline\" size=\"sm\" className=\"h-8 gap-1.5 text-xs\">\n            <Filter className=\"size-3\" />\n            Priority\n            {selectedPriority && (\n              <Badge variant=\"default\" className=\"ml-0.5 h-4 min-w-4 justify-center rounded px-1 text-[10px]\">\n                1\n              </Badge>\n            )}\n            <ChevronDown className=\"text-muted-foreground size-3\" />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"start\" className=\"w-40\">\n          {Object.entries(priorityConfig).map(([key, config]) => {\n            const Icon = config.icon\n            return (\n              <DropdownMenuItem\n                key={key}\n                className=\"gap-2\"\n                onClick={() => onSelectedPriorityChange(selectedPriority === key ? null : key)}\n              >\n                <Icon className={cn('size-3.5', config.class)} />\n                {config.label}\n                {selectedPriority === key && <span className=\"bg-primary ml-auto size-1.5 rounded-full\" />}\n              </DropdownMenuItem>\n            )\n          })}\n          {selectedPriority && <DropdownMenuSeparator />}\n          {selectedPriority && (\n            <DropdownMenuItem onClick={() => onSelectedPriorityChange(null)}>Clear filter</DropdownMenuItem>\n          )}\n        </DropdownMenuContent>\n      </DropdownMenu>\n\n      {selectedAssignee && (\n        <Badge\n          variant=\"secondary\"\n          className=\"h-7 cursor-pointer gap-1 pr-1.5 text-xs\"\n          onClick={() => onSelectedAssigneeChange(null)}\n        >\n          {selectedAssignee}\n          <X className=\"size-3 opacity-50\" />\n        </Badge>\n      )}\n\n      <ToggleGroup\n        type=\"single\"\n        size=\"sm\"\n        value={viewMode}\n        className=\"bg-muted ml-auto flex items-center gap-0.5 rounded-md p-0.5\"\n        onValueChange={(v) => v && onViewModeChange(v as 'board' | 'list')}\n      >\n        <ToggleGroupItem\n          value=\"board\"\n          className=\"data-[state=on]:bg-background size-7 rounded-sm p-0 data-[state=on]:shadow-sm\"\n          title=\"Board view\"\n        >\n          <LayoutGrid className=\"size-3.5\" />\n        </ToggleGroupItem>\n        <ToggleGroupItem\n          value=\"list\"\n          className=\"data-[state=on]:bg-background size-7 rounded-sm p-0 data-[state=on]:shadow-sm\"\n          title=\"List view\"\n        >\n          <List className=\"size-3.5\" />\n        </ToggleGroupItem>\n      </ToggleGroup>\n\n      <div className=\"flex items-center\">\n        <TooltipProvider delayDuration={300}>\n          {Object.entries(assignees).map(([key, a]) => (\n            <Tooltip key={key}>\n              <TooltipTrigger asChild>\n                <button\n                  className={cn(\n                    // rounded-full so the selected ring + focus ring trace the\n                    // Avatar's circular outline instead of the rectangular button.\n                    'ring-background focus-visible:ring-ring/50 relative -ml-1.5 rounded-full transition-all outline-none first:ml-0 focus-visible:ring-1',\n                    selectedAssignee === a.name\n                      ? 'ring-primary z-20 ring-1'\n                      : selectedAssignee && selectedAssignee !== a.name\n                        ? 'opacity-40 hover:opacity-70'\n                        : 'hover:z-10 hover:scale-110',\n                  )}\n                  onClick={() => onSelectedAssigneeChange(selectedAssignee === a.name ? null : a.name)}\n                >\n                  <Avatar className=\"border-background size-7 border-2\">\n                    <AvatarFallback className={cn('text-[10px] font-semibold', a.color)}>\n                      {getInitials(a.name)}\n                    </AvatarFallback>\n                  </Avatar>\n                </button>\n              </TooltipTrigger>\n              <TooltipContent side=\"bottom\" className=\"text-xs\">\n                {a.name}\n                {selectedAssignee === a.name && <span className=\"text-muted-foreground ml-1\">(filtered)</span>}\n              </TooltipContent>\n            </Tooltip>\n          ))}\n        </TooltipProvider>\n      </div>\n    </div>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/KanbanToolbar.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/PriorityBadge.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { priorityConfig } from '@/lib/use-kanban'\n\nexport function PriorityBadge({\n  priority,\n  iconSize = 'size-3.5',\n  className,\n}: {\n  priority: string\n  iconSize?: string\n  className?: string\n}) {\n  const config = priorityConfig[priority]\n  if (!config) return null\n  const Icon = config.icon\n  return (\n    <div className={cn('flex items-center gap-1', className)}>\n      <Icon className={cn(iconSize, config.class)} />\n      <span className={cn('text-[11px] font-semibold', config.class)}>{config.label}</span>\n    </div>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/PriorityBadge.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/DueDateBadge.tsx",
      "content": "'use client'\n\nimport { Clock } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { getDueStatus, formatDueDate } from '@/lib/use-kanban'\n\nexport function DueDateBadge({\n  dueDate,\n  variant,\n  className,\n}: {\n  dueDate: string\n  variant?: 'chip' | 'inline'\n  className?: string\n}) {\n  const status = getDueStatus(dueDate)\n  const formatted = formatDueDate(dueDate)\n\n  const chipClasses =\n    status === 'overdue'\n      ? 'bg-destructive/10 text-destructive'\n      : status === 'soon'\n        ? 'bg-warning/10 text-warning'\n        : 'text-muted-foreground bg-muted'\n\n  const inlineClasses =\n    status === 'overdue' ? 'text-destructive' : status === 'soon' ? 'text-warning' : 'text-foreground'\n\n  if (variant === 'chip') {\n    return (\n      <div\n        className={cn('flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium', chipClasses, className)}\n      >\n        <Clock className=\"size-3\" />\n        {formatted}\n      </div>\n    )\n  }\n\n  return (\n    <p className={cn('flex items-center gap-1 text-[13px] leading-tight font-medium', inlineClasses, className)}>\n      <Clock className=\"size-3\" />\n      {formatted}\n    </p>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/DueDateBadge.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/TagBadge.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\n\nexport function TagBadge({ label, color, className }: { label: string; color: string; className?: string }) {\n  return (\n    <span className={cn('rounded-md px-1.5 py-0.5 text-[11px] font-medium ring-1 ring-inset', color, className)}>\n      {label}\n    </span>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/TagBadge.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/UserAvatar.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { getInitials } from '@/lib/use-kanban'\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar'\n\nconst userAvatarSizeMap = {\n  xs: { avatar: 'size-6', text: 'text-[8px]' },\n  sm: { avatar: 'size-7', text: 'text-[9px]' },\n  md: { avatar: 'size-8', text: 'text-[11px]' },\n}\n\nexport function UserAvatar({\n  name,\n  color,\n  size = 'sm',\n  className,\n}: {\n  name: string\n  color: string\n  size?: 'xs' | 'sm' | 'md'\n  className?: string\n}) {\n  return (\n    <Avatar className={cn(userAvatarSizeMap[size].avatar, 'shrink-0', className)}>\n      <AvatarFallback className={cn(userAvatarSizeMap[size].text, 'font-bold', color)}>\n        {getInitials(name)}\n      </AvatarFallback>\n    </Avatar>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/UserAvatar.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/SubtaskProgress.tsx",
      "content": "'use client'\n\nimport { CheckCircle2 } from 'lucide-react'\nimport { cn } from '@/lib/utils'\n\nexport function SubtaskProgress({ done, total, className }: { done: number; total: number; className?: string }) {\n  if (total <= 0) return null\n  const percent = total > 0 ? Math.round((done / total) * 100) : 0\n  const isComplete = done === total && total > 0\n  return (\n    <div className={className}>\n      <div className=\"mb-1 flex items-center justify-between\">\n        <span className=\"text-muted-foreground text-[11px]\">\n          {isComplete && <CheckCircle2 className=\"text-success mr-0.5 inline size-3\" />}\n          {done}/{total} subtasks\n        </span>\n        <span className=\"text-muted-foreground text-[11px] font-medium tabular-nums\">{percent}%</span>\n      </div>\n      <div className=\"bg-muted h-1.5 overflow-hidden rounded-full\">\n        <div\n          className={cn('h-full rounded-full transition-all duration-500', isComplete ? 'bg-success' : 'bg-primary')}\n          style={{ width: `${percent}%` }}\n        />\n      </div>\n    </div>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/SubtaskProgress.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/SubtaskList.tsx",
      "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport {\n  type KanbanColumn as KanbanColumnType,\n  type KanbanTask,\n  findTaskById,\n  getTaskColumn,\n} from '@/lib/use-kanban'\n\nexport function SubtaskList({\n  subtaskIds,\n  columns,\n  compact = false,\n}: {\n  subtaskIds: string[]\n  columns: KanbanColumnType[]\n  compact?: boolean\n}) {\n  const subtasks = subtaskIds\n    .map((id) => {\n      const task = findTaskById(columns, id)\n      const column = getTaskColumn(columns, id)\n      return task ? { task, column } : null\n    })\n    .filter(Boolean) as { task: KanbanTask; column: KanbanColumnType | undefined }[]\n\n  const doneCount = subtasks.filter((s) => s.column?.id === 'done').length\n  const percent = subtasks.length > 0 ? Math.round((doneCount / subtasks.length) * 100) : 0\n\n  if (!subtasks.length) {\n    return (\n      <p className={compact ? 'text-muted-foreground text-[12px]' : 'text-muted-foreground text-sm'}>\n        No subtasks yet.\n      </p>\n    )\n  }\n\n  return (\n    <div>\n      <div className=\"mb-2 flex items-center justify-between\">\n        <span className={cn('text-muted-foreground tabular-nums', compact ? 'text-[11px]' : 'text-[13px]')}>\n          {doneCount}/{subtasks.length} done\n        </span>\n        <span className={cn('text-muted-foreground tabular-nums', compact ? 'text-[10px]' : 'text-[11px]')}>\n          {percent}%\n        </span>\n      </div>\n      <div className=\"bg-muted mb-3 h-1.5 overflow-hidden rounded-full\">\n        <div\n          className={cn(\n            'h-full rounded-full transition-all duration-500',\n            doneCount === subtasks.length ? 'bg-success' : 'bg-primary',\n          )}\n          style={{ width: `${percent}%` }}\n        />\n      </div>\n\n      <div className={compact ? 'space-y-0.5' : 'space-y-1'}>\n        {subtasks.map(({ task, column }) => (\n          <a\n            key={task.id}\n            href={`/dashboard/kanban/${task.id}`}\n            className={cn(\n              'group/subtask flex items-center gap-2 rounded-md transition-colors',\n              compact ? 'px-1 py-1' : 'px-1.5 py-1.5',\n              'hover:bg-muted/50',\n            )}\n          >\n            <span className={cn('size-1.5 shrink-0 rounded-full', column?.dotColor ?? 'bg-muted-foreground')} />\n            <span\n              className={cn('shrink-0 font-mono', compact ? 'text-[10px]' : 'text-[11px]', 'text-muted-foreground/70')}\n            >\n              {task.id}\n            </span>\n            <span\n              className={cn(\n                'min-w-0 flex-1 truncate',\n                compact ? 'text-[12px]' : 'text-[13px]',\n                column?.id === 'done' ? 'text-muted-foreground line-through' : 'text-foreground',\n              )}\n            >\n              {task.title}\n            </span>\n            <span\n              className={cn(\n                'shrink-0 rounded-md px-1.5 py-0.5 font-medium',\n                compact ? 'text-[9px]' : 'text-[10px]',\n                column?.color ?? 'text-muted-foreground',\n              )}\n            >\n              {column?.title ?? 'Unknown'}\n            </span>\n          </a>\n        ))}\n      </div>\n    </div>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/SubtaskList.tsx"
    },
    {
      "path": "packages/registry-react/blocks/kanban-board/CommentList.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Send } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { type CommentItem } from '@/lib/use-kanban'\nimport { Button } from '@/components/ui/button'\nimport { RichTextEditor } from '@/components/ui/rich-text-editor'\nimport { UserAvatar } from './UserAvatar'\n\nexport function CommentList({\n  comments,\n  compact = false,\n  onAdd,\n}: {\n  comments: CommentItem[]\n  compact?: boolean\n  onAdd?: (text: string) => void\n}) {\n  const [newComment, setNewComment] = React.useState('')\n\n  function submit() {\n    const stripped = newComment.replace(/<[^>]*>/g, '').trim()\n    if (!stripped) return\n    onAdd?.(newComment)\n    setNewComment('')\n  }\n\n  return (\n    <>\n      {comments.length ? (\n        <div className={compact ? 'space-y-3' : 'space-y-4'}>\n          {comments.map((comment) => (\n            <div key={comment.id} className={compact ? 'flex gap-2.5' : 'flex gap-3'}>\n              <UserAvatar name={comment.author} color={comment.authorColor} size={compact ? 'xs' : 'sm'} />\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"flex items-baseline gap-2\">\n                  <span className={compact ? 'text-[12px] font-semibold' : 'text-[13px] font-semibold'}>\n                    {compact ? comment.author.split(' ')[0] : comment.author}\n                  </span>\n                  <span\n                    className={\n                      compact ? 'text-muted-foreground text-[10px]' : 'text-muted-foreground text-[11px]'\n                    }\n                  >\n                    {comment.time}\n                  </span>\n                </div>\n                <div\n                  className={cn(\n                    'rich-text-content prose prose-sm dark:prose-invert mt-0.5 max-w-none',\n                    compact\n                      ? 'text-muted-foreground text-[12px] leading-relaxed'\n                      : 'text-muted-foreground text-[13px] leading-relaxed',\n                  )}\n                  dangerouslySetInnerHTML={{ __html: comment.text }}\n                />\n              </div>\n            </div>\n          ))}\n        </div>\n      ) : (\n        <p className={compact ? 'text-muted-foreground text-[12px]' : 'text-muted-foreground text-sm'}>\n          No comments yet.\n        </p>\n      )}\n\n      <div className={compact ? 'mt-3 flex gap-2' : 'mt-4 flex gap-3 border-t pt-4'}>\n        <div className={compact ? 'mt-1' : 'mt-1.5'}>\n          <UserAvatar name=\"Admin User\" color=\"bg-chart-1/15 text-chart-1\" size={compact ? 'xs' : 'sm'} />\n        </div>\n        <div className=\"min-w-0 flex-1 space-y-2\">\n          <RichTextEditor\n            value={newComment}\n            onValueChange={setNewComment}\n            placeholder=\"Write a comment...\"\n            minHeight={compact ? '60px' : '80px'}\n            className={compact ? 'text-[12px]' : 'text-[13px]'}\n          />\n          <div className=\"flex justify-end\">\n            <Button\n              size=\"sm\"\n              className={compact ? 'h-7 gap-1 text-[11px]' : 'h-8 gap-1.5 text-xs'}\n              disabled={!newComment.replace(/<[^>]*>/g, '').trim()}\n              onClick={submit}\n            >\n              <Send className=\"size-3\" />\n              Comment\n            </Button>\n          </div>\n        </div>\n      </div>\n    </>\n  )\n}",
      "type": "registry:block",
      "target": "~/components/blocks/kanban-board/CommentList.tsx"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/use-kanban.json",
    "https://uipkge.dev/r/react/kanban-data.json",
    "https://uipkge.dev/r/react/page.json",
    "https://uipkge.dev/r/react/card.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/badge.json",
    "https://uipkge.dev/r/react/input.json",
    "https://uipkge.dev/r/react/label.json",
    "https://uipkge.dev/r/react/select.json",
    "https://uipkge.dev/r/react/sheet.json",
    "https://uipkge.dev/r/react/dialog.json",
    "https://uipkge.dev/r/react/popover.json",
    "https://uipkge.dev/r/react/calendar.json",
    "https://uipkge.dev/r/react/dropdown-menu.json",
    "https://uipkge.dev/r/react/tooltip.json",
    "https://uipkge.dev/r/react/avatar.json",
    "https://uipkge.dev/r/react/toggle-group.json",
    "https://uipkge.dev/r/react/rich-text-editor.json"
  ],
  "description": "Full kanban surface: 4 status columns, drag-and-drop card movement with insertion indicator, board/list view toggle, search + priority + assignee filters, collapsible columns, click-to-open task detail Sheet (comments, subtasks, files), full-form Add Task Dialog with priority/assignee/due-date/tags/subtasks. Mega-block: ships 14 React files (KanbanBoard + 6 kanban-* views + 7 small badges/lists). Bind with columns/onColumnsChange; consumer owns the columns array so swapping the in-memory store for Drizzle/Postgres is a one-line change. Auto-pulls the use-kanban hook (types + configs + helpers) and kanban-data lib (seed columns).",
  "categories": [
    "dashboard",
    "data"
  ]
}