{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "kanban-board",
  "title": "Kanban Board",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/kanban-board/KanbanBoard.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref } from 'vue'\nimport { Plus } from 'lucide-vue-next'\nimport { PageHeader, PageHeaderHeading } from '@/components/ui/page'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport KanbanToolbar from './KanbanToolbar.vue'\nimport KanbanColumn from './KanbanColumn.vue'\nimport KanbanListView from './KanbanListView.vue'\nimport KanbanTaskSheet from './KanbanTaskSheet.vue'\nimport KanbanAddTaskDialog from './KanbanAddTaskDialog.vue'\nimport type { KanbanColumn as KanbanColumnType, KanbanTask } from '@/composables/useKanban'\n\ninterface Props {\n  columns: KanbanColumnType[]\n  title?: string | null\n  description?: string | null\n  defaultColumnId?: string\n  hideHeader?: boolean\n  hideToolbar?: boolean\n  lockParentScroll?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\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})\n\nconst emits = defineEmits<{\n  'update:columns': [value: KanbanColumnType[]]\n}>()\n\nconst columnsRef = computed({\n  get: () => props.columns,\n  set: (v) => emits('update:columns', v),\n})\n\nconst kanbanEl = ref<HTMLElement | null>(null)\nlet parentMain: HTMLElement | null = null\n\nonMounted(() => {\n  if (!props.lockParentScroll) return\n  parentMain = kanbanEl.value?.closest('main[data-slot=\"sidebar-inset\"]') as HTMLElement | null\n  if (parentMain) parentMain.style.overflow = 'hidden'\n  document.documentElement.style.overflow = 'hidden'\n})\n\nonUnmounted(() => {\n  if (parentMain) {\n    parentMain.style.overflow = ''\n    parentMain = null\n  }\n  if (props.lockParentScroll) document.documentElement.style.overflow = ''\n})\n\nconst searchQuery = ref('')\nconst selectedPriority = ref<string | null>(null)\nconst selectedAssignee = ref<string | null>(null)\nconst viewMode = ref<'board' | 'list'>('board')\n\nconst filteredColumns = computed(() => {\n  return columnsRef.value.map((col) => ({\n    ...col,\n    tasks: col.tasks.filter((task) => {\n      const q = searchQuery.value.toLowerCase()\n      const matchesSearch = !q || task.title.toLowerCase().includes(q) || task.id.toLowerCase().includes(q)\n      const matchesPriority = !selectedPriority.value || task.priority === selectedPriority.value\n      const matchesAssignee = !selectedAssignee.value || task.assignee.name === selectedAssignee.value\n      return matchesSearch && matchesPriority && matchesAssignee\n    }),\n  }))\n})\n\nconst totalTasks = computed(() => columnsRef.value.reduce((sum, col) => sum + col.tasks.length, 0))\n\nconst collapsedColumns = ref<Set<string>>(new Set())\n\nfunction toggleCollapse(columnId: string) {\n  const next = new Set(collapsedColumns.value)\n  if (next.has(columnId)) next.delete(columnId)\n  else next.add(columnId)\n  collapsedColumns.value = next\n}\n\nfunction addComment(task: KanbanTask, text: string) {\n  task.commentItems.push({\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\nfunction moveTask(task: KanbanTask, targetColumnId: string) {\n  const sourceCol = columnsRef.value.find((c) => c.tasks.some((t) => t.id === task.id))\n  const targetCol = columnsRef.value.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\nconst detailOpen = ref(false)\nconst detailTask = ref<KanbanTask | null>(null)\n\nfunction openTaskDetail(task: KanbanTask) {\n  detailTask.value = task\n  detailOpen.value = true\n}\n\nconst draggedTask = ref<string | null>(null)\nconst dragOverColumn = ref<string | null>(null)\nconst dropTargetIndex = ref<number>(-1)\nlet lastDragEnd = 0\n\nfunction onDragStart(event: DragEvent, taskId: string) {\n  draggedTask.value = taskId\n  if (event.dataTransfer) {\n    event.dataTransfer.effectAllowed = 'move'\n    event.dataTransfer.setData('text/plain', taskId)\n  }\n}\n\nfunction resetDrag() {\n  draggedTask.value = null\n  dragOverColumn.value = null\n  dropTargetIndex.value = -1\n  lastDragEnd = Date.now()\n}\n\nfunction onDrop() {\n  if (!draggedTask.value || !dragOverColumn.value) {\n    resetDrag()\n    return\n  }\n\n  let sourceColIdx = -1\n  let taskIdx = -1\n  for (let c = 0; c < columnsRef.value.length; c++) {\n    const col = columnsRef.value[c]\n    if (!col) continue\n    const tIdx = col.tasks.findIndex((t) => t.id === draggedTask.value)\n    if (tIdx !== -1) {\n      sourceColIdx = c\n      taskIdx = tIdx\n      break\n    }\n  }\n  const targetColIdx = columnsRef.value.findIndex((c) => c.id === dragOverColumn.value)\n  if (sourceColIdx === -1 || targetColIdx === -1) {\n    resetDrag()\n    return\n  }\n\n  const sourceCol = columnsRef.value[sourceColIdx]\n  const targetCol = columnsRef.value[targetColIdx]\n  if (!sourceCol || !targetCol) {\n    resetDrag()\n    return\n  }\n\n  const [task] = sourceCol.tasks.splice(taskIdx, 1)\n  if (!task) {\n    resetDrag()\n    return\n  }\n\n  let insertAt = dropTargetIndex.value\n  if (insertAt < 0) insertAt = targetCol.tasks.length\n  if (sourceColIdx === targetColIdx && taskIdx < insertAt) insertAt--\n\n  targetCol.tasks.splice(insertAt, 0, task)\n  resetDrag()\n}\n\nfunction onCardClick(task: KanbanTask) {\n  if (Date.now() - lastDragEnd < 200) return\n  openTaskDetail(task)\n}\n\nfunction onCardDragOver(event: DragEvent, columnId: string, taskIndex: number) {\n  dragOverColumn.value = columnId\n  const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()\n  const midY = rect.top + rect.height / 2\n  dropTargetIndex.value = event.clientY < midY ? taskIndex : taskIndex + 1\n}\n\nfunction onLaneDragOver(_event: DragEvent, columnId: string, taskCount: number) {\n  dragOverColumn.value = columnId\n  dropTargetIndex.value = taskCount\n}\n\nconst addTaskOpen = ref(false)\nconst addTaskColumnId = ref(props.defaultColumnId)\n\nfunction openAddTask(columnId: string) {\n  addTaskColumnId.value = columnId\n  addTaskOpen.value = true\n}\n\nfunction onCreateTask(columnId: string, tasks: KanbanTask[]) {\n  const col = columnsRef.value.find((c) => c.id === columnId)\n  if (col) col.tasks.push(...tasks)\n}\n</script>\n\n<template>\n  <div\n    ref=\"kanbanEl\"\n    data-slot=\"kanban-board\"\n    class=\"kanban-page flex h-[calc(100dvh-3.5rem-3rem)] flex-col overflow-hidden lg:h-[calc(100dvh-3.5rem-3rem)]\"\n  >\n    <div v-if=\"!hideHeader\" class=\"mb-3 shrink-0\">\n      <PageHeader>\n        <div class=\"flex items-start justify-between gap-4\">\n          <PageHeaderHeading :title=\"title ?? ''\" :description=\"description ?? ''\" />\n          <div class=\"flex shrink-0 items-center gap-2\">\n            <Badge variant=\"secondary\" class=\"font-mono text-xs tabular-nums\">{{ totalTasks }} tasks</Badge>\n            <Button size=\"sm\" @click=\"openAddTask(defaultColumnId)\">\n              <Plus class=\"size-4\" />\n              Add Task\n            </Button>\n          </div>\n        </div>\n      </PageHeader>\n    </div>\n\n    <KanbanToolbar\n      v-if=\"!hideToolbar\"\n      v-model:search-query=\"searchQuery\"\n      v-model:selected-priority=\"selectedPriority\"\n      v-model:selected-assignee=\"selectedAssignee\"\n      v-model:view-mode=\"viewMode\"\n    />\n\n    <div\n      v-if=\"viewMode === 'board'\"\n      class=\"kanban-board relative flex min-h-0 flex-1 items-start gap-3 overflow-auto pb-3\"\n    >\n      <KanbanColumn\n        v-for=\"column in filteredColumns\"\n        :key=\"column.id\"\n        :column=\"column\"\n        :collapsed=\"collapsedColumns.has(column.id)\"\n        :dragged-task=\"draggedTask\"\n        :drag-over-column=\"dragOverColumn\"\n        :drop-target-index=\"dropTargetIndex\"\n        @toggle-collapse=\"toggleCollapse\"\n        @add-task=\"openAddTask\"\n        @card-click=\"onCardClick\"\n        @card-quick-view=\"openTaskDetail\"\n        @drag-start=\"onDragStart\"\n        @drag-end=\"resetDrag\"\n        @card-drag-over=\"onCardDragOver\"\n        @lane-drag-over=\"onLaneDragOver\"\n        @drop=\"onDrop\"\n      />\n    </div>\n\n    <KanbanListView\n      v-else\n      :columns=\"filteredColumns\"\n      :all-columns=\"columnsRef\"\n      @task-click=\"openTaskDetail\"\n      @move-task=\"moveTask\"\n    />\n\n    <KanbanTaskSheet\n      v-model:open=\"detailOpen\"\n      :task=\"detailTask\"\n      :columns=\"columnsRef\"\n      @move-task=\"moveTask\"\n      @add-comment=\"addComment\"\n    />\n\n    <KanbanAddTaskDialog\n      v-model:open=\"addTaskOpen\"\n      :columns=\"columnsRef\"\n      :initial-column-id=\"addTaskColumnId\"\n      @create=\"onCreateTask\"\n    />\n  </div>\n</template>\n\n<style scoped>\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</style>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/KanbanBoard.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/KanbanCard.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, defineEmits } from 'vue'\nimport { type KanbanTask, type KanbanColumn, priorityConfig, getTaskColumn } from '@/composables/useKanban'\nimport TagBadge from './TagBadge.vue'\nimport SubtaskProgress from './SubtaskProgress.vue'\nimport DueDateBadge from './DueDateBadge.vue'\nimport UserAvatar from './UserAvatar.vue'\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 { MoreHorizontal, MessageSquare, Paperclip, ExternalLink } from 'lucide-vue-next'\n\nconst props = defineProps<{\n  task: KanbanTask\n  isDone: boolean\n}>()\n\nconst columns = useState<KanbanColumn[]>('kanban-columns')\n\nconst subtasksDone = computed(() => {\n  if (!columns.value || !props.task.subtaskIds.length) return 0\n  return props.task.subtaskIds.filter((id) => getTaskColumn(columns.value, id)?.id === 'done').length\n})\n\ndefineEmits<{\n  click: [task: KanbanTask]\n  'quick-view': [task: KanbanTask]\n}>()\n</script>\n\n<template>\n  <div\n    :class=\"[\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    @click=\"$emit('click', task)\"\n  >\n    <div\n      :class=\"[\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 class=\"mb-1 flex items-center justify-between pl-2\">\n      <span class=\"text-muted-foreground/70 font-mono text-[11px]\">{{ task.id }}</span>\n      <DropdownMenu>\n        <DropdownMenuTrigger as-child>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            class=\"text-muted-foreground -mr-1 size-6 opacity-0 transition-opacity group-hover/card:opacity-100\"\n            @click.stop\n          >\n            <MoreHorizontal class=\"size-3.5\" />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align=\"end\" class=\"w-36\">\n          <DropdownMenuItem @click.stop=\"$emit('quick-view', task)\">Quick view</DropdownMenuItem>\n          <DropdownMenuItem as-child>\n            <NuxtLink :to=\"`/dashboard/kanban/${task.id}`\" class=\"gap-2\">\n              <ExternalLink class=\"size-3.5\" />\n              Open detail\n            </NuxtLink>\n          </DropdownMenuItem>\n          <DropdownMenuItem>Edit</DropdownMenuItem>\n          <DropdownMenuItem>Move to...</DropdownMenuItem>\n          <DropdownMenuItem>Assign to...</DropdownMenuItem>\n          <DropdownMenuSeparator />\n          <DropdownMenuItem class=\"text-destructive\">Delete</DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n\n    <p\n      :class=\"[\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    <div v-if=\"task.tags.length\" class=\"mb-2 flex flex-wrap gap-1 pl-2\">\n      <TagBadge v-for=\"tag in task.tags\" :key=\"tag.label\" :label=\"tag.label\" :color=\"tag.color\" />\n    </div>\n\n    <div v-if=\"task.subtaskIds.length\" class=\"mb-2 pl-2\">\n      <SubtaskProgress :done=\"subtasksDone\" :total=\"task.subtaskIds.length\" />\n    </div>\n\n    <div class=\"flex items-center gap-2 pl-2\">\n      <DueDateBadge v-if=\"task.dueDate\" :due-date=\"task.dueDate\" variant=\"chip\" />\n\n      <div v-if=\"task.commentItems.length\" class=\"text-muted-foreground/70 flex items-center gap-1 text-[11px]\">\n        <MessageSquare class=\"size-3\" />\n        {{ task.commentItems.length }}\n      </div>\n\n      <div v-if=\"task.fileItems.length\" class=\"text-muted-foreground/70 flex items-center gap-1 text-[11px]\">\n        <Paperclip class=\"size-3\" />\n        {{ task.fileItems.length }}\n      </div>\n\n      <div class=\"ml-auto\">\n        <TooltipProvider :delay-duration=\"200\">\n          <Tooltip>\n            <TooltipTrigger as-child>\n              <UserAvatar :name=\"task.assignee.name\" :color=\"task.assignee.color\" size=\"xs\" />\n            </TooltipTrigger>\n            <TooltipContent side=\"bottom\" class=\"text-xs\">{{ task.assignee.name }}</TooltipContent>\n          </Tooltip>\n        </TooltipProvider>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style scoped>\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</style>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/KanbanCard.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/KanbanColumn.vue",
      "content": "<script setup lang=\"ts\">\nimport { Plus, MoreHorizontal, ChevronsLeft, ChevronsRight } from 'lucide-vue-next'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport KanbanCard from './KanbanCard.vue'\nimport type { KanbanTask } from '@/composables/useKanban'\n\ndefineProps<{\n  column: { id: string; title: string; color: string; dotColor: string; tasks: KanbanTask[] }\n  collapsed: boolean\n  draggedTask: string | null\n  dragOverColumn: string | null\n  dropTargetIndex: number\n}>()\n\ndefineEmits<{\n  'toggle-collapse': [columnId: string]\n  'add-task': [columnId: string]\n  'card-click': [task: KanbanTask]\n  'card-quick-view': [task: KanbanTask]\n  'drag-start': [event: DragEvent, taskId: string]\n  'drag-end': []\n  'card-drag-over': [event: DragEvent, columnId: string, taskIndex: number]\n  'lane-drag-over': [event: DragEvent, columnId: string, taskCount: number]\n  drop: []\n}>()\n</script>\n\n<template>\n  <div\n    :class=\"['group/col flex shrink-0 flex-col transition-all duration-200', collapsed ? 'w-12' : 'w-[300px]']\"\n    @dragover.prevent\n    @drop=\"$emit('drop')\"\n  >\n    <button\n      v-if=\"collapsed\"\n      class=\"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      @click=\"$emit('toggle-collapse', column.id)\"\n    >\n      <span :class=\"['size-2 shrink-0 rounded-full', column.dotColor]\" />\n      <span\n        :class=\"['text-[11px] font-semibold tracking-tight', column.color, 'rotate-180 [writing-mode:vertical-lr]']\"\n      >\n        {{ column.title }}\n      </span>\n      <Badge variant=\"secondary\" class=\"mt-1 h-5 min-w-5 justify-center rounded-md px-1 text-[10px]\">\n        {{ column.tasks.length }}\n      </Badge>\n      <ChevronsRight class=\"text-muted-foreground mt-auto size-3.5\" />\n    </button>\n\n    <template v-else>\n      <div class=\"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          class=\"text-muted-foreground/50 hover:text-muted-foreground shrink-0 transition-colors\"\n          title=\"Collapse column\"\n          @click=\"$emit('toggle-collapse', column.id)\"\n        >\n          <ChevronsLeft class=\"size-3.5\" />\n        </button>\n        <span :class=\"['size-2 shrink-0 rounded-full', column.dotColor]\" />\n        <h3 :class=\"['text-[13px] font-semibold tracking-tight', column.color]\">{{ column.title }}</h3>\n        <span class=\"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 class=\"ml-auto flex items-center\">\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            class=\"text-muted-foreground size-6 opacity-0 transition-opacity group-hover/col:opacity-100\"\n          >\n            <MoreHorizontal class=\"size-3.5\" />\n          </Button>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            class=\"text-muted-foreground size-6\"\n            @click=\"$emit('add-task', column.id)\"\n          >\n            <Plus class=\"size-3.5\" />\n          </Button>\n        </div>\n      </div>\n\n      <div\n        :class=\"[\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        @dragover.prevent=\"$emit('lane-drag-over', $event, column.id, column.tasks.length)\"\n      >\n        <template v-for=\"(task, taskIndex) in column.tasks\" :key=\"task.id\">\n          <div\n            :class=\"[\n              'drop-indicator mx-1 transition-all duration-150',\n              dragOverColumn === column.id && dropTargetIndex === taskIndex && draggedTask && 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            :class=\"['mt-2 first:mt-0', draggedTask === task.id ? 'scale-95 rotate-1 opacity-30' : 'opacity-100']\"\n            @dragstart=\"$emit('drag-start', $event, task.id)\"\n            @dragend=\"$emit('drag-end')\"\n            @dragover.stop.prevent=\"$emit('card-drag-over', $event, column.id, taskIndex)\"\n          >\n            <KanbanCard\n              :task=\"task\"\n              :is-done=\"column.id === 'done'\"\n              @click=\"$emit('card-click', $event)\"\n              @quick-view=\"$emit('card-quick-view', $event)\"\n            />\n          </div>\n        </template>\n\n        <div\n          v-if=\"column.tasks.length > 0\"\n          :class=\"[\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        <button\n          v-if=\"column.tasks.length === 0\"\n          class=\"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          @click=\"$emit('add-task', column.id)\"\n        >\n          <Plus class=\"mb-1 size-4\" />\n          <p class=\"text-xs\">No tasks</p>\n        </button>\n      </div>\n\n      <button\n        class=\"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        @click=\"$emit('add-task', column.id)\"\n      >\n        <Plus class=\"size-3.5\" />\n        Add task\n      </button>\n    </template>\n  </div>\n</template>\n\n<style scoped>\n.kanban-lane {\n  min-height: 60px;\n}\n</style>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/KanbanColumn.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/KanbanListView.vue",
      "content": "<script setup lang=\"ts\">\nimport { ref, computed } from 'vue'\nimport type { KanbanTask, KanbanColumn } from '@/composables/useKanban'\nimport { getTaskColumn } from '@/composables/useKanban'\nimport PriorityBadge from './PriorityBadge.vue'\nimport TagBadge from './TagBadge.vue'\nimport DueDateBadge from './DueDateBadge.vue'\nimport UserAvatar from './UserAvatar.vue'\nimport SubtaskProgress from './SubtaskProgress.vue'\nimport { Badge } from '@/components/ui/badge'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'\nimport { MessageSquare, Paperclip, ExternalLink, ArrowUpDown, ChevronDown, ChevronRight } from 'lucide-vue-next'\n\nconst props = defineProps<{\n  columns: KanbanColumn[]\n  allColumns: KanbanColumn[]\n}>()\n\ndefineEmits<{\n  'task-click': [task: KanbanTask]\n  'move-task': [task: KanbanTask, targetColumnId: string]\n}>()\n\ntype SortField = 'id' | 'title' | 'priority' | 'assignee' | 'dueDate' | 'status'\ntype SortDir = 'asc' | 'desc'\n\nconst sortField = ref<SortField>('status')\nconst sortDir = ref<SortDir>('asc')\n\nconst priorityOrder: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3 }\n\nfunction toggleSort(field: SortField) {\n  if (sortField.value === field) {\n    sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'\n  } else {\n    sortField.value = field\n    sortDir.value = 'asc'\n  }\n}\n\nconst groupByStatus = ref(true)\nconst collapsedGroups = ref<Set<string>>(new Set())\n\nfunction toggleGroup(columnId: string) {\n  const next = new Set(collapsedGroups.value)\n  if (next.has(columnId)) next.delete(columnId)\n  else next.add(columnId)\n  collapsedGroups.value = next\n}\n\ninterface FlatTask {\n  task: KanbanTask\n  columnId: string\n  columnTitle: string\n  dotColor: string\n}\n\nfunction sortTasks(tasks: FlatTask[]): FlatTask[] {\n  return [...tasks].sort((a, b) => {\n    let cmp = 0\n    switch (sortField.value) {\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 = props.allColumns.map((c) => c.id)\n        cmp = colOrder.indexOf(a.columnId) - colOrder.indexOf(b.columnId)\n        break\n      }\n    }\n    return sortDir.value === 'desc' ? -cmp : cmp\n  })\n}\n\nconst flatTasks = computed<FlatTask[]>(() => {\n  const items: FlatTask[] = []\n  for (const col of props.columns) {\n    for (const task of col.tasks) {\n      items.push({\n        task,\n        columnId: col.id,\n        columnTitle: col.title,\n        dotColor: col.dotColor,\n      })\n    }\n  }\n  return sortTasks(items)\n})\n\nconst groupedTasks = computed(() => {\n  if (!groupByStatus.value) return null\n  const groups: { column: KanbanColumn; tasks: FlatTask[] }[] = []\n  for (const col of props.allColumns) {\n    const tasks = flatTasks.value.filter((t) => t.columnId === col.id)\n    groups.push({ column: col, tasks })\n  }\n  return groups\n})\n\nfunction subtasksDone(task: KanbanTask): number {\n  if (!task.subtaskIds.length) return 0\n  return task.subtaskIds.filter((id) => getTaskColumn(props.allColumns, id)?.id === 'done').length\n}\n</script>\n\n<template>\n  <div class=\"kanban-list flex min-h-0 flex-1 flex-col overflow-auto pb-3\">\n    <div\n      class=\"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    >\n      <button class=\"flex items-center gap-1 text-left\" @click=\"toggleSort('id')\">\n        ID\n        <ArrowUpDown :class=\"['size-3', sortField === 'id' ? 'text-foreground' : 'text-muted-foreground/50']\" />\n      </button>\n      <button class=\"flex items-center gap-1 text-left\" @click=\"toggleSort('title')\">\n        Task\n        <ArrowUpDown :class=\"['size-3', sortField === 'title' ? 'text-foreground' : 'text-muted-foreground/50']\" />\n      </button>\n      <button class=\"flex items-center gap-1 text-left\" @click=\"toggleSort('status')\">\n        Status\n        <ArrowUpDown :class=\"['size-3', sortField === 'status' ? 'text-foreground' : 'text-muted-foreground/50']\" />\n      </button>\n      <button class=\"flex items-center gap-1 text-left\" @click=\"toggleSort('priority')\">\n        Priority\n        <ArrowUpDown :class=\"['size-3', sortField === 'priority' ? 'text-foreground' : 'text-muted-foreground/50']\" />\n      </button>\n      <button class=\"flex items-center gap-1 text-left\" @click=\"toggleSort('assignee')\">\n        Assignee\n        <ArrowUpDown :class=\"['size-3', sortField === 'assignee' ? 'text-foreground' : 'text-muted-foreground/50']\" />\n      </button>\n      <button class=\"flex items-center gap-1 text-left\" @click=\"toggleSort('dueDate')\">\n        Due\n        <ArrowUpDown :class=\"['size-3', sortField === 'dueDate' ? 'text-foreground' : 'text-muted-foreground/50']\" />\n      </button>\n      <span class=\"text-center\">Info</span>\n    </div>\n\n    <template v-if=\"groupedTasks\">\n      <template v-for=\"group in groupedTasks\" :key=\"group.column.id\">\n        <button\n          class=\"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          @click=\"toggleGroup(group.column.id)\"\n        >\n          <component\n            :is=\"collapsedGroups.has(group.column.id) ? ChevronRight : ChevronDown\"\n            class=\"text-muted-foreground size-3.5\"\n          />\n          <span :class=\"['size-2 rounded-full', group.column.dotColor]\" />\n          <span class=\"text-sm font-medium\">{{ group.column.title }}</span>\n          <Badge variant=\"secondary\" class=\"ml-1 h-4 px-1.5 text-[10px] tabular-nums\">\n            {{ group.tasks.length }}\n          </Badge>\n        </button>\n\n        <template v-if=\"!collapsedGroups.has(group.column.id)\">\n          <div\n            v-for=\"item in group.tasks\"\n            :key=\"item.task.id\"\n            class=\"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            @click=\"$emit('task-click', item.task)\"\n          >\n            <span class=\"text-muted-foreground font-mono text-[11px]\">{{ item.task.id }}</span>\n\n            <div class=\"min-w-0\">\n              <div class=\"flex items-center gap-2\">\n                <span\n                  :class=\"[\n                    'truncate text-[13px] font-medium',\n                    item.columnId === 'done' ? 'text-muted-foreground line-through' : '',\n                  ]\"\n                >\n                  {{ item.task.title }}\n                </span>\n                <NuxtLink\n                  :to=\"`/dashboard/kanban/${item.task.id}`\"\n                  class=\"text-muted-foreground hover:text-foreground shrink-0 opacity-0 transition-opacity group-hover/row:opacity-100\"\n                  @click.stop\n                >\n                  <ExternalLink class=\"size-3\" />\n                </NuxtLink>\n              </div>\n              <div v-if=\"item.task.tags.length || item.task.subtaskIds.length\" class=\"mt-0.5 flex items-center gap-1.5\">\n                <TagBadge\n                  v-for=\"tag in item.task.tags\"\n                  :key=\"tag.label\"\n                  :label=\"tag.label\"\n                  :color=\"tag.color\"\n                  class=\"!px-1.5 !py-0 !text-[9px]\"\n                />\n                <SubtaskProgress\n                  v-if=\"item.task.subtaskIds.length\"\n                  :done=\"subtasksDone(item.task)\"\n                  :total=\"item.task.subtaskIds.length\"\n                  class=\"ml-1\"\n                />\n              </div>\n            </div>\n\n            <div>\n              <Select\n                :model-value=\"item.columnId\"\n                @update:model-value=\"(val) => $emit('move-task', item.task, String(val))\"\n              >\n                <SelectTrigger\n                  class=\"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                  @click.stop\n                >\n                  <span class=\"flex items-center gap-1.5\">\n                    <span :class=\"['size-1.5 rounded-full', item.dotColor]\" />\n                    <SelectValue />\n                  </span>\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem v-for=\"col in allColumns\" :key=\"col.id\" :value=\"col.id\">\n                    <span class=\"flex items-center gap-1.5\">\n                      <span :class=\"['size-1.5 rounded-full', col.dotColor]\" />\n                      {{ col.title }}\n                    </span>\n                  </SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n\n            <PriorityBadge :priority=\"item.task.priority\" />\n\n            <div class=\"flex items-center gap-2\">\n              <UserAvatar :name=\"item.task.assignee.name\" :color=\"item.task.assignee.color\" size=\"xs\" />\n              <span class=\"truncate text-[12px]\">{{ item.task.assignee.name }}</span>\n            </div>\n\n            <div>\n              <DueDateBadge v-if=\"item.task.dueDate\" :due-date=\"item.task.dueDate\" variant=\"chip\" />\n              <span v-else class=\"text-muted-foreground/50 text-[11px]\">—</span>\n            </div>\n\n            <div class=\"flex items-center justify-center gap-2\">\n              <TooltipProvider :delay-duration=\"200\">\n                <Tooltip v-if=\"item.task.commentItems.length\">\n                  <TooltipTrigger as-child>\n                    <span class=\"text-muted-foreground/70 flex items-center gap-0.5 text-[11px]\">\n                      <MessageSquare class=\"size-3\" />\n                      {{ item.task.commentItems.length }}\n                    </span>\n                  </TooltipTrigger>\n                  <TooltipContent class=\"text-xs\">{{ item.task.commentItems.length }} comments</TooltipContent>\n                </Tooltip>\n                <Tooltip v-if=\"item.task.fileItems.length\">\n                  <TooltipTrigger as-child>\n                    <span class=\"text-muted-foreground/70 flex items-center gap-0.5 text-[11px]\">\n                      <Paperclip class=\"size-3\" />\n                      {{ item.task.fileItems.length }}\n                    </span>\n                  </TooltipTrigger>\n                  <TooltipContent class=\"text-xs\">{{ item.task.fileItems.length }} files</TooltipContent>\n                </Tooltip>\n              </TooltipProvider>\n            </div>\n          </div>\n        </template>\n      </template>\n    </template>\n\n    <div\n      v-if=\"flatTasks.length === 0\"\n      class=\"text-muted-foreground flex flex-1 items-center justify-center rounded-b-lg border-x border-b py-12 text-sm\"\n    >\n      No tasks match your filters.\n    </div>\n  </div>\n</template>\n\n<style scoped>\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",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/KanbanListView.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/KanbanTaskSheet.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { type KanbanTask, type KanbanColumn, priorityConfig, fileIconMap } from '@/composables/useKanban'\nimport PriorityBadge from './PriorityBadge.vue'\nimport TagBadge from './TagBadge.vue'\nimport DueDateBadge from './DueDateBadge.vue'\nimport UserAvatar from './UserAvatar.vue'\nimport CommentList from './CommentList.vue'\nimport SubtaskList from './SubtaskList.vue'\nimport { Sheet, SheetContent, SheetTitle, SheetDescription, SheetFooter, SheetClose } from '@/components/ui/sheet'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Button } from '@/components/ui/button'\nimport { Clock, Download, ExternalLink } from 'lucide-vue-next'\n\nconst props = defineProps<{\n  open: boolean\n  task: KanbanTask | null\n  columns: KanbanColumn[]\n}>()\n\ndefineEmits<{\n  'update:open': [value: boolean]\n  'move-task': [task: KanbanTask, columnId: string]\n  'add-comment': [task: KanbanTask, text: string]\n}>()\n\nconst columnIdForTask = computed(() => {\n  if (!props.task) return ''\n  return props.columns.find((c) => c.tasks.some((t) => t.id === props.task!.id))?.id ?? ''\n})\n</script>\n\n<template>\n  <Sheet :open=\"open\" @update:open=\"$emit('update:open', $event)\">\n    <SheetContent class=\"flex flex-col gap-0 overflow-hidden p-0 sm:max-w-[420px]\">\n      <template v-if=\"task\">\n        <div :class=\"['h-1 w-full shrink-0', priorityConfig[task.priority]?.bg]\" />\n\n        <div class=\"shrink-0 px-5 pt-4 pb-3\">\n          <div class=\"mb-3 flex items-center gap-2\">\n            <span class=\"text-muted-foreground font-mono text-[11px] tracking-tight\">{{ task.id }}</span>\n            <span class=\"text-muted-foreground/30\">·</span>\n            <Select\n              :model-value=\"columnIdForTask\"\n              @update:model-value=\"(val) => task && $emit('move-task', task, String(val))\"\n            >\n              <SelectTrigger\n                class=\"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              >\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem v-for=\"col in columns\" :key=\"col.id\" :value=\"col.id\">\n                  <span class=\"flex items-center gap-1.5\">\n                    <span :class=\"['size-1.5 rounded-full', col.dotColor]\" />\n                    {{ col.title }}\n                  </span>\n                </SelectItem>\n              </SelectContent>\n            </Select>\n            <PriorityBadge :priority=\"task.priority\" icon-size=\"size-3\" class=\"ml-auto\" />\n          </div>\n\n          <SheetTitle class=\"text-[15px] leading-snug font-semibold tracking-tight\">\n            {{ task.title }}\n          </SheetTitle>\n          <SheetDescription class=\"sr-only\">Task details</SheetDescription>\n          <div\n            v-if=\"task.description\"\n            class=\"text-muted-foreground rich-text-content prose prose-sm dark:prose-invert mt-1.5 max-w-none text-[13px] leading-relaxed\"\n            v-html=\"task.description\"\n          />\n          <p v-else class=\"text-muted-foreground mt-1.5 text-[13px] leading-relaxed\">No description provided.</p>\n\n          <div class=\"mt-3 flex flex-wrap gap-1.5\">\n            <template v-if=\"task.tags.length\">\n              <TagBadge v-for=\"tag in task.tags\" :key=\"tag.label\" :label=\"tag.label\" :color=\"tag.color\" />\n            </template>\n            <span v-else class=\"text-muted-foreground text-[11px]\">No tags</span>\n          </div>\n        </div>\n\n        <div class=\"bg-border mx-5 h-px\" />\n\n        <div class=\"flex-1 overflow-y-auto\">\n          <div class=\"space-y-4 px-5 py-3\">\n            <div class=\"flex items-center gap-3\">\n              <UserAvatar :name=\"task.assignee.name\" :color=\"task.assignee.color\" size=\"md\" />\n              <div>\n                <p class=\"text-[13px] leading-tight font-medium\">{{ task.assignee.name }}</p>\n                <p class=\"text-muted-foreground text-[11px]\">Assignee</p>\n              </div>\n              <div class=\"ml-auto text-right\">\n                <DueDateBadge v-if=\"task.dueDate\" :due-date=\"task.dueDate\" />\n                <p v-else class=\"text-muted-foreground flex items-center gap-1 text-[13px] leading-tight\">\n                  <Clock class=\"size-3\" />\n                  No due date\n                </p>\n              </div>\n            </div>\n\n            <div v-if=\"task.parentId\" class=\"flex items-center gap-2\">\n              <span class=\"text-muted-foreground text-[11px]\">Parent:</span>\n              <NuxtLink\n                :to=\"`/dashboard/kanban/${task.parentId}`\"\n                class=\"text-primary text-[12px] font-medium hover:underline\"\n                @click=\"$emit('update:open', false)\"\n              >\n                {{ task.parentId }}\n              </NuxtLink>\n            </div>\n\n            <div>\n              <h4 class=\"mb-2 text-[13px] font-semibold\">\n                Subtasks\n                <span v-if=\"task.subtaskIds.length\" class=\"text-muted-foreground font-normal\">\n                  ({{ task.subtaskIds.length }})\n                </span>\n              </h4>\n              <SubtaskList :subtask-ids=\"task.subtaskIds\" :columns=\"columns\" compact />\n            </div>\n\n            <div class=\"bg-border h-px\" />\n\n            <div>\n              <h4 class=\"mb-2 text-[13px] font-semibold\">\n                Comments\n                <span v-if=\"task.commentItems.length\" class=\"text-muted-foreground font-normal\">\n                  ({{ task.commentItems.length }})\n                </span>\n              </h4>\n              <CommentList\n                :comments=\"task.commentItems\"\n                compact\n                @add=\"(text) => task && $emit('add-comment', task, text)\"\n              />\n            </div>\n\n            <div class=\"bg-border h-px\" />\n\n            <div>\n              <h4 class=\"mb-2 text-[13px] font-semibold\">\n                Files\n                <span v-if=\"task.fileItems.length\" class=\"text-muted-foreground font-normal\">\n                  ({{ task.fileItems.length }})\n                </span>\n              </h4>\n              <div v-if=\"task.fileItems.length\" class=\"space-y-1\">\n                <div\n                  v-for=\"file in task.fileItems\"\n                  :key=\"file.id\"\n                  class=\"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 class=\"bg-muted flex size-8 shrink-0 items-center justify-center rounded-md\">\n                    <component :is=\"fileIconMap[file.type]\" class=\"text-muted-foreground size-4\" />\n                  </div>\n                  <div class=\"min-w-0 flex-1\">\n                    <p class=\"truncate text-[12px] font-medium\">{{ file.name }}</p>\n                    <p class=\"text-muted-foreground text-[10px]\">{{ file.size }}</p>\n                  </div>\n                  <Button\n                    variant=\"ghost\"\n                    size=\"icon\"\n                    class=\"size-7 shrink-0 opacity-0 transition-opacity group-hover/file:opacity-100\"\n                  >\n                    <Download class=\"size-3.5\" />\n                  </Button>\n                </div>\n              </div>\n              <p v-else class=\"text-muted-foreground text-[12px]\">No files attached.</p>\n            </div>\n          </div>\n        </div>\n\n        <SheetFooter class=\"shrink-0 border-t px-5 py-3\">\n          <div class=\"flex w-full items-center gap-2\">\n            <NuxtLink :to=\"`/dashboard/kanban/${task.id}`\" class=\"flex-1\" @click=\"$emit('update:open', false)\">\n              <Button variant=\"outline\" size=\"sm\" class=\"w-full gap-1.5\">\n                <ExternalLink class=\"size-3.5\" />\n                View full detail\n              </Button>\n            </NuxtLink>\n            <SheetClose as-child>\n              <Button variant=\"ghost\" size=\"sm\">Close</Button>\n            </SheetClose>\n          </div>\n        </SheetFooter>\n      </template>\n    </SheetContent>\n  </Sheet>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/KanbanTaskSheet.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/KanbanAddTaskDialog.vue",
      "content": "<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\nimport type { KanbanTask, KanbanColumn } from '@/composables/useKanban'\nimport { priorityConfig, assignees, tagPresets } from '@/composables/useKanban'\nimport {\n  Dialog,\n  DialogScrollContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n} from '@/components/ui/dialog'\nimport { Button } from '@/components/ui/button'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { Calendar } from '@/components/ui/calendar'\nimport { RichTextEditor } from '@/components/ui/rich-text-editor'\nimport { Plus, X, CheckCircle2, CalendarIcon } from 'lucide-vue-next'\nimport { getLocalTimeZone, DateFormatter, type DateValue } from '@internationalized/date'\n\nconst props = defineProps<{\n  open: boolean\n  columns: KanbanColumn[]\n  initialColumnId: string\n}>()\n\nconst emit = defineEmits<{\n  'update:open': [value: boolean]\n  create: [columnId: string, tasks: KanbanTask[]]\n}>()\n\nconst df = new DateFormatter('en-US', { dateStyle: 'medium' })\n\nconst columnId = ref(props.initialColumnId)\nconst dueDate = ref<DateValue | undefined>(undefined)\nconst form = ref({\n  title: '',\n  description: '',\n  priority: 'medium' as KanbanTask['priority'],\n  assigneeKey: 'alice' as keyof typeof assignees,\n  tagKeys: [] as (keyof typeof tagPresets)[],\n  subtaskTexts: [] as string[],\n})\nconst newSubtaskText = ref('')\n\nwatch(\n  () => props.initialColumnId,\n  (val) => {\n    columnId.value = val\n  },\n)\n\nwatch(\n  () => props.open,\n  (val) => {\n    if (val) {\n      columnId.value = props.initialColumnId\n      form.value = {\n        title: '',\n        description: '',\n        priority: 'medium',\n        assigneeKey: 'alice',\n        tagKeys: [],\n        subtaskTexts: [],\n      }\n      dueDate.value = undefined\n      newSubtaskText.value = ''\n    }\n  },\n)\n\nfunction addSubtask() {\n  const text = newSubtaskText.value.trim()\n  if (!text) return\n  form.value.subtaskTexts.push(text)\n  newSubtaskText.value = ''\n}\n\nfunction removeSubtask(index: number) {\n  form.value.subtaskTexts.splice(index, 1)\n}\n\nfunction toggleTag(key: keyof typeof tagPresets) {\n  const idx = form.value.tagKeys.indexOf(key)\n  if (idx >= 0) form.value.tagKeys.splice(idx, 1)\n  else form.value.tagKeys.push(key)\n}\n\nfunction submit() {\n  if (!form.value.title.trim()) return\n  const maxId = props.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 = props.columns.flatMap((c) => c.tasks)[0]?.id.split('-')[0] ?? 'TASK'\n  const parentId = `${idPrefix}-${maxId + 1}`\n  const subtaskTasks: KanbanTask[] = form.value.subtaskTexts.map((text, i) => ({\n    id: `${idPrefix}-${maxId + 2 + i}`,\n    title: text,\n    priority: 'medium' as const,\n    assignee: assignees[form.value.assigneeKey],\n    tags: [],\n    parentId,\n    subtaskIds: [],\n    commentItems: [],\n    fileItems: [],\n  }))\n  const newTask: KanbanTask = {\n    id: parentId,\n    title: form.value.title.trim(),\n    description: form.value.description.trim() || undefined,\n    priority: form.value.priority,\n    assignee: assignees[form.value.assigneeKey],\n    tags: form.value.tagKeys.map((k) => tagPresets[k]),\n    dueDate: dueDate.value ? dueDate.value.toString() : undefined,\n    subtaskIds: subtaskTasks.map((t) => t.id),\n    commentItems: [],\n    fileItems: [],\n  }\n  emit('create', columnId.value, [newTask, ...subtaskTasks])\n  emit('update:open', false)\n}\n</script>\n\n<template>\n  <Dialog :open=\"open\" @update:open=\"$emit('update:open', $event)\">\n    <DialogScrollContent class=\"sm:max-w-[680px]\">\n      <DialogHeader>\n        <DialogTitle>New Task</DialogTitle>\n        <DialogDescription>\n          Adding task to\n          {{ columns.find((c) => c.id === columnId)?.title ?? 'column' }}\n        </DialogDescription>\n      </DialogHeader>\n\n      <div class=\"grid gap-4 py-2\">\n        <div class=\"grid gap-1.5\">\n          <Label for=\"task-title\">Title</Label>\n          <Input id=\"task-title\" v-model=\"form.title\" placeholder=\"Enter task title\" />\n        </div>\n\n        <div class=\"grid gap-1.5\">\n          <Label>\n            Description\n            <span class=\"text-muted-foreground text-xs\">(optional)</span>\n          </Label>\n          <RichTextEditor v-model=\"form.description\" />\n        </div>\n\n        <div class=\"grid grid-cols-3 gap-4\">\n          <div class=\"grid gap-1.5\">\n            <Label for=\"task-priority\">Priority</Label>\n            <Select v-model=\"form.priority\">\n              <SelectTrigger id=\"task-priority\">\n                <SelectValue placeholder=\"Priority\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem v-for=\"(config, key) in priorityConfig\" :key=\"key\" :value=\"key\">\n                  {{ config.label }}\n                </SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n\n          <div class=\"grid gap-1.5\">\n            <Label for=\"task-assignee\">Assignee</Label>\n            <Select v-model=\"form.assigneeKey\">\n              <SelectTrigger id=\"task-assignee\">\n                <SelectValue placeholder=\"Assignee\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem v-for=\"(person, key) in assignees\" :key=\"key\" :value=\"key\">\n                  {{ person.name }}\n                </SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n\n          <div class=\"grid gap-1.5\">\n            <Label for=\"task-column\">Column</Label>\n            <Select v-model=\"columnId\">\n              <SelectTrigger id=\"task-column\">\n                <SelectValue placeholder=\"Column\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem v-for=\"col in columns\" :key=\"col.id\" :value=\"col.id\">\n                  {{ col.title }}\n                </SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n        </div>\n\n        <div class=\"grid gap-1.5\">\n          <Label>\n            Due Date\n            <span class=\"text-muted-foreground text-xs\">(optional)</span>\n          </Label>\n          <Popover>\n            <PopoverTrigger as-child>\n              <Button\n                variant=\"outline\"\n                :class=\"['w-full justify-start text-left font-normal', !dueDate && 'text-muted-foreground']\"\n              >\n                <CalendarIcon class=\"mr-2 size-4\" />\n                {{ dueDate ? df.format(dueDate.toDate(getLocalTimeZone())) : 'Pick a date' }}\n              </Button>\n            </PopoverTrigger>\n            <PopoverContent class=\"w-auto p-0\">\n              <Calendar v-model=\"dueDate\" initial-focus />\n            </PopoverContent>\n          </Popover>\n        </div>\n\n        <div class=\"grid gap-1.5\">\n          <Label>\n            Tags\n            <span class=\"text-muted-foreground text-xs\">(optional)</span>\n          </Label>\n          <div class=\"flex flex-wrap gap-2\">\n            <button\n              v-for=\"(tag, key) in tagPresets\"\n              :key=\"key\"\n              type=\"button\"\n              :class=\"[\n                'inline-flex items-center gap-1 rounded-full border px-3 py-1 text-xs font-medium transition-colors',\n                form.tagKeys.includes(key as keyof typeof tagPresets)\n                  ? 'bg-primary text-primary-foreground border-transparent'\n                  : 'border-border bg-background text-foreground hover:bg-muted',\n              ]\"\n              @click=\"toggleTag(key as keyof typeof tagPresets)\"\n            >\n              <CheckCircle2 v-if=\"form.tagKeys.includes(key as keyof typeof tagPresets)\" class=\"size-3\" />\n              {{ tag.label }}\n            </button>\n          </div>\n        </div>\n\n        <div class=\"grid gap-1.5\">\n          <Label>\n            Subtasks\n            <span class=\"text-muted-foreground text-xs\">(optional)</span>\n          </Label>\n          <div v-if=\"form.subtaskTexts.length\" class=\"space-y-2\">\n            <div v-for=\"(text, index) in form.subtaskTexts\" :key=\"index\" class=\"flex items-center gap-2\">\n              <span class=\"flex-1 text-sm\">{{ text }}</span>\n              <Button variant=\"ghost\" size=\"icon\" class=\"size-6\" @click=\"removeSubtask(index)\">\n                <X class=\"size-3\" />\n              </Button>\n            </div>\n          </div>\n          <div class=\"flex items-center gap-2\">\n            <Input v-model=\"newSubtaskText\" placeholder=\"Add a subtask\" @keyup.enter=\"addSubtask\" />\n            <Button variant=\"outline\" size=\"icon\" @click=\"addSubtask\">\n              <Plus class=\"size-4\" />\n            </Button>\n          </div>\n        </div>\n      </div>\n\n      <DialogFooter>\n        <Button variant=\"outline\" @click=\"$emit('update:open', false)\">Cancel</Button>\n        <Button :disabled=\"!form.title.trim()\" @click=\"submit\">Create Task</Button>\n      </DialogFooter>\n    </DialogScrollContent>\n  </Dialog>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/KanbanAddTaskDialog.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/KanbanToolbar.vue",
      "content": "<script setup lang=\"ts\">\nimport { Search, Filter, ChevronDown, X, LayoutGrid, List } from 'lucide-vue-next'\nimport { Input } from '@/components/ui/input'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'\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 { Avatar, AvatarFallback } from '@/components/ui/avatar'\nimport { priorityConfig, assignees, getInitials } from '@/composables/useKanban'\n\ndefineProps<{\n  searchQuery: string\n  selectedPriority: string | null\n  selectedAssignee: string | null\n  viewMode: 'board' | 'list'\n}>()\n\ndefineEmits<{\n  'update:searchQuery': [value: string]\n  'update:selectedPriority': [value: string | null]\n  'update:selectedAssignee': [value: string | null]\n  'update:viewMode': [value: 'board' | 'list']\n}>()\n</script>\n\n<template>\n  <div class=\"mb-3 flex shrink-0 items-center gap-2\">\n    <div class=\"relative w-56\">\n      <Search class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\" />\n      <Input\n        :model-value=\"searchQuery\"\n        placeholder=\"Search tasks...\"\n        class=\"h-8 pl-8 text-sm\"\n        @update:model-value=\"$emit('update:searchQuery', $event)\"\n      />\n    </div>\n\n    <DropdownMenu>\n      <DropdownMenuTrigger as-child>\n        <Button variant=\"outline\" size=\"sm\" class=\"h-8 gap-1.5 text-xs\">\n          <Filter class=\"size-3\" />\n          Priority\n          <Badge\n            v-if=\"selectedPriority\"\n            variant=\"default\"\n            class=\"ml-0.5 h-4 min-w-4 justify-center rounded px-1 text-[10px]\"\n          >\n            1\n          </Badge>\n          <ChevronDown class=\"text-muted-foreground size-3\" />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\" class=\"w-40\">\n        <DropdownMenuItem\n          v-for=\"(config, key) in priorityConfig\"\n          :key=\"key\"\n          class=\"gap-2\"\n          @click=\"$emit('update:selectedPriority', selectedPriority === key ? null : (key as string))\"\n        >\n          <component :is=\"config.icon\" :class=\"['size-3.5', config.class]\" />\n          {{ config.label }}\n          <span v-if=\"selectedPriority === key\" class=\"bg-primary ml-auto size-1.5 rounded-full\" />\n        </DropdownMenuItem>\n        <DropdownMenuSeparator v-if=\"selectedPriority\" />\n        <DropdownMenuItem v-if=\"selectedPriority\" @click=\"$emit('update:selectedPriority', null)\">\n          Clear filter\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n\n    <Badge\n      v-if=\"selectedAssignee\"\n      variant=\"secondary\"\n      class=\"h-7 cursor-pointer gap-1 pr-1.5 text-xs\"\n      @click=\"$emit('update:selectedAssignee', null)\"\n    >\n      {{ selectedAssignee }}\n      <X class=\"size-3 opacity-50\" />\n    </Badge>\n\n    <ToggleGroup\n      type=\"single\"\n      size=\"sm\"\n      :model-value=\"viewMode\"\n      class=\"bg-muted ml-auto flex items-center gap-0.5 rounded-md p-0.5\"\n      @update:model-value=\"(v) => v && $emit('update:viewMode', v as 'board' | 'list')\"\n    >\n      <ToggleGroupItem\n        value=\"board\"\n        class=\"data-[state=on]:bg-background size-7 rounded-sm p-0 data-[state=on]:shadow-sm\"\n        title=\"Board view\"\n      >\n        <LayoutGrid class=\"size-3.5\" />\n      </ToggleGroupItem>\n      <ToggleGroupItem\n        value=\"list\"\n        class=\"data-[state=on]:bg-background size-7 rounded-sm p-0 data-[state=on]:shadow-sm\"\n        title=\"List view\"\n      >\n        <List class=\"size-3.5\" />\n      </ToggleGroupItem>\n    </ToggleGroup>\n\n    <div class=\"flex items-center\">\n      <TooltipProvider :delay-duration=\"300\">\n        <Tooltip v-for=\"(a, key) in assignees\" :key=\"key\">\n          <TooltipTrigger as-child>\n            <button\n              :class=\"[\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              @click=\"$emit('update:selectedAssignee', selectedAssignee === a.name ? null : a.name)\"\n            >\n              <Avatar class=\"border-background size-7 border-2\">\n                <AvatarFallback :class=\"['text-[10px] font-semibold', a.color]\">\n                  {{ getInitials(a.name) }}\n                </AvatarFallback>\n              </Avatar>\n            </button>\n          </TooltipTrigger>\n          <TooltipContent side=\"bottom\" class=\"text-xs\">\n            {{ a.name }}\n            <span v-if=\"selectedAssignee === a.name\" class=\"text-muted-foreground ml-1\">(filtered)</span>\n          </TooltipContent>\n        </Tooltip>\n      </TooltipProvider>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/KanbanToolbar.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/PriorityBadge.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { priorityConfig } from '@/composables/useKanban'\nimport { cn } from '@/lib/utils'\n\nconst props = withDefaults(\n  defineProps<{\n    priority: string\n    iconSize?: string\n    class?: string\n  }>(),\n  {\n    iconSize: 'size-3.5',\n  },\n)\n\nconst config = computed(() => priorityConfig[props.priority])\n</script>\n\n<template>\n  <div v-if=\"config\" :class=\"cn('flex items-center gap-1', props.class)\">\n    <component :is=\"config.icon\" :class=\"[iconSize, config.class]\" />\n    <span :class=\"['text-[11px] font-semibold', config.class]\">{{ config.label }}</span>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/PriorityBadge.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/DueDateBadge.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Clock } from 'lucide-vue-next'\nimport { getDueStatus, formatDueDate } from '@/composables/useKanban'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  dueDate: string\n  variant?: 'chip' | 'inline'\n  class?: string\n}>()\n\nconst status = computed(() => getDueStatus(props.dueDate))\nconst formatted = computed(() => formatDueDate(props.dueDate))\n\nconst chipClasses = computed(() => {\n  switch (status.value) {\n    case 'overdue':\n      return 'bg-destructive/10 text-destructive'\n    case 'soon':\n      return 'bg-warning/10 text-warning'\n    default:\n      return 'text-muted-foreground bg-muted'\n  }\n})\n\nconst inlineClasses = computed(() => {\n  switch (status.value) {\n    case 'overdue':\n      return 'text-destructive'\n    case 'soon':\n      return 'text-warning'\n    default:\n      return 'text-foreground'\n  }\n})\n</script>\n\n<template>\n  <div\n    v-if=\"variant === 'chip'\"\n    :class=\"cn('flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium', chipClasses, props.class)\"\n  >\n    <Clock class=\"size-3\" />\n    {{ formatted }}\n  </div>\n\n  <p v-else :class=\"cn('flex items-center gap-1 text-[13px] leading-tight font-medium', inlineClasses, props.class)\">\n    <Clock class=\"size-3\" />\n    {{ formatted }}\n  </p>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/DueDateBadge.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/TagBadge.vue",
      "content": "<script setup lang=\"ts\">\nimport { cn } from '@/lib/utils'\n\ndefineProps<{\n  label: string\n  color: string\n  class?: string\n}>()\n</script>\n\n<template>\n  <span :class=\"cn('rounded-md px-1.5 py-0.5 text-[11px] font-medium ring-1 ring-inset', color, $props.class)\">\n    {{ label }}\n  </span>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/TagBadge.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/UserAvatar.vue",
      "content": "<script setup lang=\"ts\">\nimport { Avatar, AvatarFallback } from '@/components/ui/avatar'\nimport { getInitials } from '@/composables/useKanban'\nimport { cn } from '@/lib/utils'\n\ndefineProps<{\n  name: string\n  color: string\n  size?: 'xs' | 'sm' | 'md'\n  class?: string\n}>()\n\nconst sizeMap = {\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</script>\n\n<template>\n  <Avatar :class=\"cn(sizeMap[size ?? 'sm'].avatar, 'shrink-0', $props.class)\">\n    <AvatarFallback :class=\"[sizeMap[size ?? 'sm'].text, 'font-bold', color]\">\n      {{ getInitials(name) }}\n    </AvatarFallback>\n  </Avatar>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/UserAvatar.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/SubtaskProgress.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { CheckCircle2 } from 'lucide-vue-next'\n\nconst props = defineProps<{\n  done: number\n  total: number\n  barHeight?: string\n}>()\n\nconst percent = computed(() => (props.total > 0 ? Math.round((props.done / props.total) * 100) : 0))\nconst isComplete = computed(() => props.done === props.total && props.total > 0)\n</script>\n\n<template>\n  <div v-if=\"total > 0\">\n    <div class=\"mb-1 flex items-center justify-between\">\n      <span class=\"text-muted-foreground text-[11px]\">\n        <CheckCircle2 v-if=\"isComplete\" class=\"text-success mr-0.5 inline size-3\" />\n        {{ done }}/{{ total }} subtasks\n      </span>\n      <span class=\"text-muted-foreground text-[11px] font-medium tabular-nums\">{{ percent }}%</span>\n    </div>\n    <div :class=\"['bg-muted overflow-hidden rounded-full', barHeight ?? 'h-1.5']\">\n      <div\n        :class=\"['h-full rounded-full transition-all duration-500', isComplete ? 'bg-success' : 'bg-primary']\"\n        :style=\"{ width: `${percent}%` }\"\n      />\n    </div>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/SubtaskProgress.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/SubtaskList.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { KanbanColumn } from '@/composables/useKanban'\nimport { findTaskById, getTaskColumn } from '@/composables/useKanban'\n\nconst props = defineProps<{\n  subtaskIds: string[]\n  columns: KanbanColumn[]\n  compact?: boolean\n}>()\n\nconst subtasks = computed(() => {\n  return props.subtaskIds\n    .map((id) => {\n      const task = findTaskById(props.columns, id)\n      const column = getTaskColumn(props.columns, id)\n      return task ? { task, column } : null\n    })\n    .filter(Boolean) as {\n    task: NonNullable<ReturnType<typeof findTaskById>>\n    column: ReturnType<typeof getTaskColumn>\n  }[]\n})\n\nconst doneCount = computed(() => subtasks.value.filter((s) => s.column?.id === 'done').length)\n</script>\n\n<template>\n  <div v-if=\"subtasks.length\">\n    <div class=\"mb-2 flex items-center justify-between\">\n      <span :class=\"compact ? 'text-[11px]' : 'text-[13px]'\" class=\"text-muted-foreground tabular-nums\">\n        {{ doneCount }}/{{ subtasks.length }} done\n      </span>\n      <span :class=\"compact ? 'text-[10px]' : 'text-[11px]'\" class=\"text-muted-foreground tabular-nums\">\n        {{ subtasks.length > 0 ? Math.round((doneCount / subtasks.length) * 100) : 0 }}%\n      </span>\n    </div>\n    <div class=\"bg-muted mb-3 h-1.5 overflow-hidden rounded-full\">\n      <div\n        :class=\"[\n          'h-full rounded-full transition-all duration-500',\n          doneCount === subtasks.length ? 'bg-success' : 'bg-primary',\n        ]\"\n        :style=\"{ width: `${subtasks.length > 0 ? Math.round((doneCount / subtasks.length) * 100) : 0}%` }\"\n      />\n    </div>\n\n    <div :class=\"compact ? 'space-y-0.5' : 'space-y-1'\">\n      <NuxtLink\n        v-for=\"{ task, column } in subtasks\"\n        :key=\"task.id\"\n        :to=\"`/dashboard/kanban/${task.id}`\"\n        :class=\"[\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 :class=\"['size-1.5 shrink-0 rounded-full', column?.dotColor ?? 'bg-muted-foreground']\" />\n        <span :class=\"['shrink-0 font-mono', compact ? 'text-[10px]' : 'text-[11px]', 'text-muted-foreground/70']\">\n          {{ task.id }}\n        </span>\n        <span\n          :class=\"[\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          :class=\"[\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      </NuxtLink>\n    </div>\n  </div>\n  <p v-else :class=\"compact ? 'text-muted-foreground text-[12px]' : 'text-muted-foreground text-sm'\">\n    No subtasks yet.\n  </p>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/SubtaskList.vue"
    },
    {
      "path": "packages/registry-vue/blocks/kanban-board/CommentList.vue",
      "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { Send } from 'lucide-vue-next'\nimport { Button } from '@/components/ui/button'\nimport { RichTextEditor } from '@/components/ui/rich-text-editor'\nimport UserAvatar from './UserAvatar.vue'\nimport type { CommentItem } from '@/composables/useKanban'\n\nwithDefaults(\n  defineProps<{\n    comments: CommentItem[]\n    compact?: boolean\n  }>(),\n  {\n    compact: false,\n  },\n)\n\nconst emit = defineEmits<{\n  add: [text: string]\n}>()\n\nconst newComment = ref('')\n\nfunction submit() {\n  const stripped = newComment.value.replace(/<[^>]*>/g, '').trim()\n  if (!stripped) return\n  emit('add', newComment.value)\n  newComment.value = ''\n}\n</script>\n\n<template>\n  <div v-if=\"comments.length\" :class=\"compact ? 'space-y-3' : 'space-y-4'\">\n    <div v-for=\"comment in comments\" :key=\"comment.id\" :class=\"compact ? 'flex gap-2.5' : 'flex gap-3'\">\n      <UserAvatar :name=\"comment.author\" :color=\"comment.authorColor\" :size=\"compact ? 'xs' : 'sm'\" />\n      <div class=\"min-w-0 flex-1\">\n        <div class=\"flex items-baseline gap-2\">\n          <span :class=\"compact ? 'text-[12px] font-semibold' : 'text-[13px] font-semibold'\">\n            {{ compact ? comment.author.split(' ')[0] : comment.author }}\n          </span>\n          <span :class=\"compact ? 'text-muted-foreground text-[10px]' : 'text-muted-foreground text-[11px]'\">\n            {{ comment.time }}\n          </span>\n        </div>\n        <div\n          :class=\"[\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          v-html=\"comment.text\"\n        />\n      </div>\n    </div>\n  </div>\n  <p v-else :class=\"compact ? 'text-muted-foreground text-[12px]' : 'text-muted-foreground text-sm'\">\n    No comments yet.\n  </p>\n\n  <div :class=\"compact ? 'mt-3 flex gap-2' : 'mt-4 flex gap-3 border-t pt-4'\">\n    <div :class=\"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 class=\"min-w-0 flex-1 space-y-2\">\n      <RichTextEditor\n        v-model:model-value=\"newComment\"\n        placeholder=\"Write a comment...\"\n        :min-height=\"compact ? '60px' : '80px'\"\n        :class=\"compact ? 'text-[12px]' : 'text-[13px]'\"\n      />\n      <div class=\"flex justify-end\">\n        <Button\n          size=\"sm\"\n          :class=\"compact ? 'h-7 gap-1 text-[11px]' : 'h-8 gap-1.5 text-xs'\"\n          :disabled=\"!newComment.replace(/<[^>]*>/g, '').trim()\"\n          @click=\"submit\"\n        >\n          <Send class=\"size-3\" />\n          Comment\n        </Button>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/kanban-board/CommentList.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next",
    "@internationalized/date"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/use-kanban.json",
    "https://uipkge.dev/r/vue/kanban-data.json",
    "https://uipkge.dev/r/vue/page.json",
    "https://uipkge.dev/r/vue/card.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/input.json",
    "https://uipkge.dev/r/vue/label.json",
    "https://uipkge.dev/r/vue/select.json",
    "https://uipkge.dev/r/vue/sheet.json",
    "https://uipkge.dev/r/vue/dialog.json",
    "https://uipkge.dev/r/vue/popover.json",
    "https://uipkge.dev/r/vue/calendar.json",
    "https://uipkge.dev/r/vue/dropdown-menu.json",
    "https://uipkge.dev/r/vue/tooltip.json",
    "https://uipkge.dev/r/vue/avatar.json",
    "https://uipkge.dev/r/vue/toggle-group.json",
    "https://uipkge.dev/r/vue/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 Vue files (KanbanBoard + 6 kanban-* views + 7 small badges/lists). Bind with v-model:columns; 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"
  ]
}