{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "kanban-task-board",
  "title": "Kanban Task Board",
  "type": "registry:page",
  "files": [
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/KanbanBoard.svelte",
      "content": "<script lang=\"ts\">\n  import { onMount } from 'svelte'\n  import type { Component } from 'svelte'\n  import { Plus } from '@lucide/svelte'\n  import { PageHeader, PageHeaderHeading } from '$lib/components/ui/page'\n  import { Button } from '$lib/components/ui/button'\n  import { Badge } from '$lib/components/ui/badge'\n  import KanbanToolbar from './KanbanToolbar.svelte'\n  import KanbanColumn from './KanbanColumn.svelte'\n  import KanbanListView from './KanbanListView.svelte'\n  import KanbanTaskSheet from './KanbanTaskSheet.svelte'\n  import KanbanAddTaskDialog from './KanbanAddTaskDialog.svelte'\n  import type { KanbanColumn as KanbanColumnType, KanbanTask } from '$lib/composables/useKanban'\n  import { setKanbanLinkContext, type KanbanLinkValue } from './link-context'\n\n  interface KanbanBoardProps {\n    columns?: KanbanColumnType[]\n    title?: string | null\n    description?: string | null\n    defaultColumnId?: string\n    hideHeader?: boolean\n    hideToolbar?: boolean\n    lockParentScroll?: boolean\n    linkComponent?: string | Component<any>\n    onColumnsChange?: (columns: KanbanColumnType[]) => void\n  }\n\n  let {\n    columns = $bindable([]),\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    linkComponent = 'a',\n    onColumnsChange,\n  }: KanbanBoardProps = $props()\n\n  // Provide the link component to all child views (KanbanCard, KanbanListView,\n  // KanbanTaskSheet, SubtaskList) so they render SPA-aware links without\n  // hard-coding a router. Defaults to a plain <a> — SvelteKit enhances\n  // anchors with client-side routing automatically; pass a custom component\n  // for anything fancier.\n  setKanbanLinkContext({\n    get current(): KanbanLinkValue {\n      return linkComponent\n    },\n  })\n\n  // The board mutates the columns array in place (splice/push on nested task\n  // lists, like the Vue twin). Re-assign afterwards so `bind:columns`\n  // parents and downstream $deriveds observe the change.\n  function commit() {\n    columns = [...columns]\n    onColumnsChange?.(columns)\n  }\n\n  let kanbanEl: HTMLElement | null = $state(null)\n  let parentMain: HTMLElement | null = null\n\n  onMount(() => {\n    if (!lockParentScroll) return\n    parentMain = kanbanEl?.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) {\n        parentMain.style.overflow = ''\n        parentMain = null\n      }\n      document.documentElement.style.overflow = ''\n    }\n  })\n\n  let searchQuery = $state('')\n  let selectedPriority = $state<string | null>(null)\n  let selectedAssignee = $state<string | null>(null)\n  let viewMode = $state<'board' | 'list'>('board')\n\n  const filteredColumns = $derived(\n    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  )\n\n  const totalTasks = $derived(columns.reduce((sum, col) => sum + col.tasks.length, 0))\n\n  let collapsedColumns = $state<Set<string>>(new Set())\n\n  function toggleCollapse(columnId: string) {\n    const next = new Set(collapsedColumns)\n    if (next.has(columnId)) next.delete(columnId)\n    else next.add(columnId)\n    collapsedColumns = next\n  }\n\n  function 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    commit()\n  }\n\n  function moveTask(task: KanbanTask, targetColumnId: string) {\n    const sourceCol = columns.find((c) => c.tasks.some((t) => t.id === task.id))\n    const targetCol = columns.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    commit()\n  }\n\n  let detailOpen = $state(false)\n  let detailTask = $state<KanbanTask | null>(null)\n\n  function openTaskDetail(task: KanbanTask) {\n    detailTask = task\n    detailOpen = true\n  }\n\n  let draggedTask = $state<string | null>(null)\n  let dragOverColumn = $state<string | null>(null)\n  let dropTargetIndex = $state(-1)\n  let lastDragEnd = 0\n\n  function onDragStart(event: DragEvent, taskId: string) {\n    draggedTask = taskId\n    if (event.dataTransfer) {\n      event.dataTransfer.effectAllowed = 'move'\n      event.dataTransfer.setData('text/plain', taskId)\n    }\n  }\n\n  function resetDrag() {\n    draggedTask = null\n    dragOverColumn = null\n    dropTargetIndex = -1\n    lastDragEnd = Date.now()\n  }\n\n  function onDrop() {\n    if (!draggedTask || !dragOverColumn) {\n      resetDrag()\n      return\n    }\n\n    let sourceColIdx = -1\n    let taskIdx = -1\n    for (let c = 0; c < columns.length; c++) {\n      const col = columns[c]\n      if (!col) continue\n      const tIdx = col.tasks.findIndex((t) => t.id === draggedTask)\n      if (tIdx !== -1) {\n        sourceColIdx = c\n        taskIdx = tIdx\n        break\n      }\n    }\n    const targetColIdx = columns.findIndex((c) => c.id === dragOverColumn)\n    if (sourceColIdx === -1 || targetColIdx === -1) {\n      resetDrag()\n      return\n    }\n\n    const sourceCol = columns[sourceColIdx]\n    const targetCol = columns[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\n    if (insertAt < 0) insertAt = targetCol.tasks.length\n    if (sourceColIdx === targetColIdx && taskIdx < insertAt) insertAt--\n\n    targetCol.tasks.splice(insertAt, 0, task)\n    commit()\n    resetDrag()\n  }\n\n  function onCardClick(task: KanbanTask) {\n    if (Date.now() - lastDragEnd < 200) return\n    openTaskDetail(task)\n  }\n\n  function onCardDragOver(event: DragEvent, columnId: string, taskIndex: number) {\n    dragOverColumn = columnId\n    const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()\n    const midY = rect.top + rect.height / 2\n    dropTargetIndex = event.clientY < midY ? taskIndex : taskIndex + 1\n  }\n\n  function onLaneDragOver(_event: DragEvent, columnId: string, taskCount: number) {\n    dragOverColumn = columnId\n    dropTargetIndex = taskCount\n  }\n\n  let addTaskOpen = $state(false)\n  let addTaskColumnId = $state(defaultColumnId)\n\n  function openAddTask(columnId: string) {\n    addTaskColumnId = columnId\n    addTaskOpen = true\n  }\n\n  function onCreateTask(columnId: string, tasks: KanbanTask[]) {\n    const col = columns.find((c) => c.id === columnId)\n    if (col) col.tasks.push(...tasks)\n    commit()\n  }\n</script>\n\n<div\n  bind:this={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  {#if !hideHeader}\n    <div class=\"mb-3 shrink-0\">\n      <PageHeader>\n        <PageHeaderHeading title={title ?? ''} description={description ?? ''} />\n        {#snippet actions()}\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\" onclick={() => openAddTask(defaultColumnId)}>\n              <Plus class=\"size-4\" />\n              Add Task\n            </Button>\n          </div>\n        {/snippet}\n      </PageHeader>\n    </div>\n  {/if}\n\n  {#if !hideToolbar}\n    <KanbanToolbar\n      bind:searchQuery\n      bind:selectedPriority\n      bind:selectedAssignee\n      bind:viewMode\n    />\n  {/if}\n\n  {#if viewMode === 'board'}\n    <div class=\"kanban-board relative flex min-h-0 flex-1 items-start gap-3 overflow-auto pb-3\">\n      {#each filteredColumns as column (column.id)}\n        <KanbanColumn\n          {column}\n          columns={columns}\n          collapsed={collapsedColumns.has(column.id)}\n          {draggedTask}\n          {dragOverColumn}\n          {dropTargetIndex}\n          onToggleCollapse={toggleCollapse}\n          onAddTask={openAddTask}\n          onCardClick={onCardClick}\n          onCardQuickView={openTaskDetail}\n          {onDragStart}\n          onDragEnd={resetDrag}\n          {onCardDragOver}\n          {onLaneDragOver}\n          onDrop={onDrop}\n        />\n      {/each}\n    </div>\n  {:else}\n    <KanbanListView\n      columns={filteredColumns}\n      allColumns={columns}\n      onTaskClick={openTaskDetail}\n      onMoveTask={moveTask}\n    />\n  {/if}\n\n  <KanbanTaskSheet\n    bind:open={detailOpen}\n    task={detailTask}\n    columns={columns}\n    onMoveTask={moveTask}\n    onAddComment={addComment}\n  />\n\n  <KanbanAddTaskDialog\n    bind:open={addTaskOpen}\n    columns={columns}\n    initialColumnId={addTaskColumnId}\n    onCreate={onCreateTask}\n  />\n</div>\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</style>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/KanbanBoard.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/SimpleKanban.svelte",
      "content": "<script lang=\"ts\" module>\n  export interface SimpleKanbanItem {\n    id: string\n    title: string\n    description?: string\n    date?: string\n    priority?: 'low' | 'medium' | 'high' | 'urgent'\n    tag?: string\n    tagColor?: string\n    assignee?: {\n      name: string\n      initials?: string\n      avatar?: string\n    }\n  }\n\n  export interface SimpleKanbanColumn {\n    id: string\n    title: string\n    color?: string\n    dotColor?: string\n    items: SimpleKanbanItem[]\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Plus, Calendar, GripVertical, Check } from '@lucide/svelte'\n  import { cn } from '$lib/utils'\n  import { Button } from '$lib/components/ui/button'\n  import { Avatar, AvatarFallback } from '$lib/components/ui/avatar'\n\n  interface SimpleKanbanProps {\n    columns?: SimpleKanbanColumn[]\n    allowAdd?: boolean\n    className?: string\n    cardClassName?: string\n    onColumnsChange?: (columns: SimpleKanbanColumn[]) => void\n    onCardClick?: (item: SimpleKanbanItem, columnId: string) => void\n    onCardMove?: (item: SimpleKanbanItem, fromColumnId: string, toColumnId: string, newIndex: number) => void\n    onAddItem?: (columnId: string, title: string) => void\n  }\n\n  let {\n    columns = $bindable([]),\n    allowAdd = true,\n    className,\n    cardClassName,\n    onColumnsChange,\n    onCardClick,\n    onCardMove,\n    onAddItem,\n  }: SimpleKanbanProps = $props()\n\n  const DEFAULT_DOT_COLORS: Record<string, string> = {\n    backlog: 'bg-muted-foreground/60',\n    todo: 'bg-info',\n    'in-progress': 'bg-warning',\n    review: 'bg-chart-1',\n    done: 'bg-success',\n  }\n\n  const PRIORITY_STYLES: Record<string, { label: string; class: string }> = {\n    low: { label: 'Low', class: 'bg-muted text-muted-foreground' },\n    medium: { label: 'Medium', class: 'bg-info/10 text-info' },\n    high: { label: 'High', class: 'bg-warning/10 text-warning' },\n    urgent: { label: 'Urgent', class: 'bg-destructive/10 text-destructive font-medium' },\n  }\n\n  let draggedId = $state<string | null>(null)\n  let dragOverColumnId = $state<string | null>(null)\n  let dropTargetIndex = $state(-1)\n  let addingColumnId = $state<string | null>(null)\n  let newTitle = $state('')\n  let lastDragTime = 0\n\n  function handleDragStart(e: DragEvent, id: string) {\n    draggedId = id\n    if (e.dataTransfer) {\n      e.dataTransfer.effectAllowed = 'move'\n      e.dataTransfer.setData('text/plain', id)\n    }\n  }\n\n  function handleDragEnd() {\n    draggedId = null\n    dragOverColumnId = null\n    dropTargetIndex = -1\n    lastDragTime = Date.now()\n  }\n\n  function handleDragOverColumn(e: DragEvent, columnId: string, itemsCount: number) {\n    e.preventDefault()\n    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n    dragOverColumnId = columnId\n    dropTargetIndex = itemsCount\n  }\n\n  function handleCardDragOver(e: DragEvent, columnId: string, index: number) {\n    e.preventDefault()\n    e.stopPropagation()\n    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n    dragOverColumnId = columnId\n    const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()\n    const midY = rect.top + rect.height / 2\n    dropTargetIndex = e.clientY < midY ? index : index + 1\n  }\n\n  function commit(next: SimpleKanbanColumn[]) {\n    columns = next\n    onColumnsChange?.(next)\n  }\n\n  function handleDrop(e: DragEvent, targetColumnId: string) {\n    e.preventDefault()\n    if (!draggedId || !targetColumnId) {\n      handleDragEnd()\n      return\n    }\n\n    const next = columns.map((col) => ({\n      ...col,\n      items: [...col.items],\n    }))\n\n    let sourceColIdx = -1\n    let itemIdx = -1\n    for (let c = 0; c < next.length; c++) {\n      const col = next[c]\n      if (!col) continue\n      const idx = col.items.findIndex((item) => item.id === draggedId)\n      if (idx !== -1) {\n        sourceColIdx = c\n        itemIdx = idx\n        break\n      }\n    }\n\n    const targetColIdx = next.findIndex((c) => c.id === targetColumnId)\n    if (sourceColIdx === -1 || targetColIdx === -1) {\n      handleDragEnd()\n      return\n    }\n\n    const sourceCol = next[sourceColIdx]\n    const targetCol = next[targetColIdx]\n    if (!sourceCol || !targetCol) {\n      handleDragEnd()\n      return\n    }\n\n    const [removed] = sourceCol.items.splice(itemIdx, 1)\n    if (!removed) {\n      handleDragEnd()\n      return\n    }\n\n    let at = dropTargetIndex\n    if (at < 0) at = targetCol.items.length\n    if (sourceColIdx === targetColIdx && itemIdx < at) at--\n\n    targetCol.items.splice(at, 0, removed)\n    commit(next)\n    onCardMove?.(removed, sourceCol.id, targetColumnId, at)\n\n    handleDragEnd()\n  }\n\n  function handleInlineAdd(columnId: string) {\n    if (!newTitle.trim()) {\n      addingColumnId = null\n      return\n    }\n\n    const text = newTitle.trim()\n    onAddItem?.(columnId, text)\n\n    const next = columns.map((col) => ({\n      ...col,\n      items: [...col.items],\n    }))\n    const col = next.find((c) => c.id === columnId)\n    if (col) {\n      col.items.push({\n        id: `item-${Date.now()}`,\n        title: text,\n      })\n      commit(next)\n    }\n\n    newTitle = ''\n    addingColumnId = null\n  }\n\n  function handleCardClick(item: SimpleKanbanItem, columnId: string) {\n    if (Date.now() - lastDragTime < 150) return\n    onCardClick?.(item, columnId)\n  }\n</script>\n\n<div\n  data-slot=\"simple-kanban\"\n  class={cn('flex min-h-[380px] w-full [scrollbar-width:thin] items-start gap-4 overflow-x-auto pb-4', className)}\n>\n  {#each columns as column (column.id)}\n    <div\n      data-slot=\"kanban-column\"\n      class={cn(\n        'bg-muted/40 border-border/80 flex w-72 shrink-0 flex-col rounded-xl border p-3 transition-colors',\n        dragOverColumnId === column.id && 'border-primary/50 bg-muted/60 ring-primary/10 ring-2',\n      )}\n      ondragover={(e) => handleDragOverColumn(e, column.id, column.items.length)}\n      ondrop={(e) => handleDrop(e, column.id)}\n    >\n      <!-- Column Header -->\n      <div class=\"mb-3 flex items-center justify-between gap-2 px-1\">\n        <div class=\"flex items-center gap-2\">\n          <span\n            class={cn(\n              'size-2 rounded-full',\n              column.dotColor || column.color || DEFAULT_DOT_COLORS[column.id] || 'bg-primary',\n            )}\n          ></span>\n          <h3 class=\"text-foreground text-sm font-semibold tracking-tight\">{column.title}</h3>\n          <span class=\"bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-semibold tabular-nums\">\n            {column.items.length}\n          </span>\n        </div>\n        {#if allowAdd}\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            class=\"text-muted-foreground hover:text-foreground size-7 rounded-lg\"\n            aria-label=\"Add item to {column.title}\"\n            onclick={() => {\n              addingColumnId = column.id\n              newTitle = ''\n            }}\n          >\n            <Plus class=\"size-3.5\" />\n          </Button>\n        {/if}\n      </div>\n\n      <!-- Column Cards Container -->\n      <div class=\"flex min-h-[120px] flex-1 flex-col gap-2.5\">\n        <!-- Inline quick add input -->\n        {#if addingColumnId === column.id}\n          <div\n            class=\"bg-card border-border animate-in fade-in zoom-in-95 rounded-lg border p-2.5 shadow-xs duration-150\"\n          >\n            <input\n              bind:value={newTitle}\n              type=\"text\"\n              autofocus\n              placeholder=\"Item title...\"\n              class=\"placeholder:text-muted-foreground/60 w-full bg-transparent text-sm font-medium outline-none\"\n              onkeydown={(e) => {\n                if (e.key === 'Enter') handleInlineAdd(column.id)\n                if (e.key === 'Escape') addingColumnId = null\n              }}\n            />\n            <div class=\"mt-2.5 flex items-center justify-end gap-1.5\">\n              <Button size=\"sm\" variant=\"ghost\" class=\"h-7 px-2 text-xs\" onclick={() => (addingColumnId = null)}>\n                Cancel\n              </Button>\n              <Button size=\"sm\" class=\"h-7 gap-1 px-2.5 text-xs\" onclick={() => handleInlineAdd(column.id)}>\n                <Check class=\"size-3\" />\n                Add\n              </Button>\n            </div>\n          </div>\n        {/if}\n\n        <!-- Cards list -->\n        {#each column.items as item, index (item.id)}\n          {#if dragOverColumnId === column.id && dropTargetIndex === index && draggedId !== item.id}\n            <div class=\"bg-primary/20 h-1.5 w-full rounded-full\"></div>\n          {/if}\n          <div\n            role=\"button\"\n            tabindex=\"0\"\n            draggable=\"true\"\n            data-slot=\"kanban-card\"\n            class={cn(\n              'group bg-card text-card-foreground border-border/80 hover:border-border relative flex cursor-grab flex-col gap-2 rounded-lg border p-3 shadow-xs transition-colors hover:shadow-sm active:cursor-grabbing',\n              draggedId === item.id && 'ring-primary/40 opacity-40 shadow-md ring-2',\n              cardClassName,\n            )}\n            ondragstart={(e) => handleDragStart(e, item.id)}\n            ondragend={handleDragEnd}\n            ondragover={(e) => handleCardDragOver(e, column.id, index)}\n            onclick={() => handleCardClick(item, column.id)}\n            onkeydown={(e) => {\n              if (e.key === 'Enter' || e.key === ' ') {\n                e.preventDefault()\n                handleCardClick(item, column.id)\n              }\n            }}\n          >\n            <!-- Top row: Title + Grip handle -->\n            <div class=\"flex items-start justify-between gap-2\">\n              <p class=\"text-foreground line-clamp-2 text-sm leading-snug font-medium\">\n                {item.title}\n              </p>\n              <GripVertical\n                class=\"text-muted-foreground/30 size-3.5 shrink-0 opacity-0 transition-opacity group-hover:opacity-100\"\n              />\n            </div>\n\n            <!-- Optional description -->\n            {#if item.description}\n              <p class=\"text-muted-foreground line-clamp-2 text-xs leading-relaxed\">\n                {item.description}\n              </p>\n            {/if}\n\n            <!-- Metadata row: Date, Tag, Priority, Assignee -->\n            {#if item.date || item.tag || item.priority || item.assignee}\n              <div class=\"border-border/40 mt-1 flex flex-wrap items-center justify-between gap-1.5 border-t pt-1 text-xs\">\n                <div class=\"flex flex-wrap items-center gap-1.5\">\n                  {#if item.date}\n                    <div class=\"text-muted-foreground flex items-center gap-1 text-xs\">\n                      <Calendar class=\"text-muted-foreground/70 size-3\" />\n                      <span>{item.date}</span>\n                    </div>\n                  {/if}\n\n                  {#if item.tag}\n                    <span class={cn('rounded-md px-1.5 py-0.5 text-xs font-medium', item.tagColor || 'bg-secondary text-secondary-foreground')}>\n                      {item.tag}\n                    </span>\n                  {/if}\n\n                  {#if item.priority && PRIORITY_STYLES[item.priority]}\n                    <span class={cn('rounded-md px-1.5 py-0.5 text-xs', PRIORITY_STYLES[item.priority].class)}>\n                      {PRIORITY_STYLES[item.priority].label}\n                    </span>\n                  {/if}\n                </div>\n\n                {#if item.assignee}\n                  <Avatar class=\"border-background size-5 shrink-0 border\">\n                    <AvatarFallback class=\"text-xs font-medium\">\n                      {item.assignee.initials || item.assignee.name.slice(0, 2).toUpperCase()}\n                    </AvatarFallback>\n                  </Avatar>\n                {/if}\n              </div>\n            {/if}\n          </div>\n        {/each}\n\n        <!-- Trailing drop indicator -->\n        {#if dragOverColumnId === column.id && dropTargetIndex >= column.items.length && draggedId}\n          <div class=\"bg-primary/20 h-1.5 w-full rounded-full\"></div>\n        {/if}\n\n        <!-- Empty column placeholder -->\n        {#if column.items.length === 0 && addingColumnId !== column.id}\n          <div\n            class=\"border-border/60 text-muted-foreground/60 flex flex-1 items-center justify-center rounded-lg border border-dashed py-8 text-center text-xs\"\n          >\n            Drop items here\n          </div>\n        {/if}\n      </div>\n    </div>\n  {/each}\n</div>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/SimpleKanban.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/KanbanCard.svelte",
      "content": "<script lang=\"ts\">\n  import { type KanbanTask, type KanbanColumn, priorityConfig, getTaskColumn } from '$lib/composables/useKanban'\n  import TagBadge from './TagBadge.svelte'\n  import SubtaskProgress from './SubtaskProgress.svelte'\n  import DueDateBadge from './DueDateBadge.svelte'\n  import UserAvatar from './UserAvatar.svelte'\n  import KanbanLink from './KanbanLink.svelte'\n  import { Button } from '$lib/components/ui/button'\n  import {\n    DropdownMenu,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuSeparator,\n    DropdownMenuTrigger,\n  } from '$lib/components/ui/dropdown-menu'\n  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '$lib/components/ui/tooltip'\n  import { MoreHorizontal, MessageSquare, Paperclip, ExternalLink } from '@lucide/svelte'\n\n  interface KanbanCardProps {\n    task: KanbanTask\n    isDone: boolean\n    columns: KanbanColumn[]\n    onClick?: (task: KanbanTask) => void\n    onQuickView?: (task: KanbanTask) => void\n  }\n\n  let { task, isDone, columns, onClick, onQuickView }: KanbanCardProps = $props()\n\n  const subtasksDone = $derived(\n    !columns || !task.subtaskIds.length\n      ? 0\n      : task.subtaskIds.filter((id) => getTaskColumn(columns, id)?.id === 'done').length,\n  )\n\n  // Screen-reader name: title first, then status + priority for context.\n  const ariaLabel = $derived(\n    [task.title, columns ? getTaskColumn(columns, task.id)?.title : undefined, `${priorityConfig[task.priority].label} priority`]\n      .filter(Boolean)\n      .join(', '),\n  )\n</script>\n\n<div\n  data-slot=\"kanban-board\"\n  class={[\n    'kanban-card group/card bg-card relative cursor-grab rounded-lg border p-3 transition-colors duration-150',\n    'hover:border-border hover:shadow-md active:scale-[0.97] active:cursor-grabbing',\n    'focus-visible:border-ring focus-visible:ring-ring/50 outline-none focus-visible:ring-[3px]',\n    isDone ? 'opacity-75 hover:opacity-100 focus-visible:opacity-100' : '',\n  ]}\n  onclick={() => onClick?.(task)}\n  onkeydown={(e) => {\n    if (e.target !== e.currentTarget) return\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault()\n      onClick?.(task)\n    }\n  }}\n  role=\"button\"\n  tabindex=\"0\"\n  aria-label={ariaLabel}\n>\n  <div\n    class={[\n      'kanban-accent absolute top-3 bottom-3 left-0 w-[0.09375rem] rounded-full transition-colors duration-150',\n      priorityConfig[task.priority]?.bg,\n      task.priority === 'low' ? 'opacity-40' : task.priority === 'medium' ? 'opacity-60' : 'opacity-90',\n    ]}\n  ></div>\n\n  <div class=\"mb-1 flex items-center justify-between pl-2\">\n    <span class=\"text-muted-foreground/70 font-mono text-xs\">{task.id}</span>\n    <DropdownMenu>\n      <DropdownMenuTrigger>\n        {#snippet child({ props })}\n          <Button\n            aria-label=\"More actions for {task.id}\"\n            variant=\"ghost\"\n            size=\"icon\"\n            {...props}\n            class=\"text-muted-foreground -mr-1 size-6 opacity-0 transition-opacity group-focus-within/card:opacity-100 group-hover/card:opacity-100 data-[state=open]:opacity-100\"\n            onclick={(e: MouseEvent) => {\n              e.stopPropagation()\n              ;(props.onclick as ((e: MouseEvent) => void) | undefined)?.(e)\n            }}\n          >\n            <MoreHorizontal class=\"size-3.5\" aria-hidden=\"true\" />\n          </Button>\n        {/snippet}\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" class=\"w-36\">\n        <DropdownMenuItem\n          onclick={(e) => {\n            e.stopPropagation()\n            onQuickView?.(task)\n          }}>Quick view</DropdownMenuItem\n        >\n        <DropdownMenuItem>\n          <KanbanLink href=\"/dashboard/kanban/{task.id}\" to=\"/dashboard/kanban/{task.id}\" class=\"flex items-center gap-2\">\n            <ExternalLink class=\"size-3.5\" />\n            Open detail\n          </KanbanLink>\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 class={['mb-2 pl-2 text-sm leading-snug font-medium', isDone ? 'decoration-muted-foreground/40 line-through' : '']}>\n    {task.title}\n  </p>\n\n  {#if task.tags.length}\n    <div class=\"mb-2 flex flex-wrap gap-1 pl-2\">\n      {#each task.tags as tag (tag.label)}\n        <TagBadge label={tag.label} color={tag.color} />\n      {/each}\n    </div>\n  {/if}\n\n  {#if task.subtaskIds.length}\n    <div class=\"mb-2 pl-2\">\n      <SubtaskProgress done={subtasksDone} total={task.subtaskIds.length} />\n    </div>\n  {/if}\n\n  <div class=\"flex items-center gap-2 pl-2\">\n    {#if task.dueDate}\n      <DueDateBadge dueDate={task.dueDate} variant=\"chip\" />\n    {/if}\n\n    {#if task.commentItems.length}\n      <div class=\"text-muted-foreground/70 flex items-center gap-1 text-xs\">\n        <MessageSquare class=\"size-3\" />\n        {task.commentItems.length}\n      </div>\n    {/if}\n\n    {#if task.fileItems.length}\n      <div class=\"text-muted-foreground/70 flex items-center gap-1 text-xs\">\n        <Paperclip class=\"size-3\" />\n        {task.fileItems.length}\n      </div>\n    {/if}\n\n    <div class=\"ml-auto\">\n      <TooltipProvider delayDuration={200}>\n        <Tooltip>\n          <TooltipTrigger>\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\n<style>\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:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/KanbanCard.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/KanbanColumn.svelte",
      "content": "<script lang=\"ts\">\n  import { Plus, MoreHorizontal, ChevronsLeft, ChevronsRight } from '@lucide/svelte'\n  import { Button } from '$lib/components/ui/button'\n  import { Badge } from '$lib/components/ui/badge'\n  import KanbanCard from './KanbanCard.svelte'\n  import type { KanbanColumn as KanbanColumnType, KanbanTask } from '$lib/composables/useKanban'\n\n  interface KanbanColumnProps {\n    column: { id: string; title: string; color: string; dotColor: string; tasks: KanbanTask[] }\n    columns: KanbanColumnType[]\n    collapsed: boolean\n    draggedTask: string | null\n    dragOverColumn: string | null\n    dropTargetIndex: number\n    onToggleCollapse?: (columnId: string) => void\n    onAddTask?: (columnId: string) => void\n    onCardClick?: (task: KanbanTask) => void\n    onCardQuickView?: (task: KanbanTask) => void\n    onDragStart?: (event: DragEvent, taskId: string) => void\n    onDragEnd?: () => void\n    onCardDragOver?: (event: DragEvent, columnId: string, taskIndex: number) => void\n    onLaneDragOver?: (event: DragEvent, columnId: string, taskCount: number) => void\n    onDrop?: () => void\n  }\n\n  let {\n    column,\n    columns,\n    collapsed,\n    draggedTask,\n    dragOverColumn,\n    dropTargetIndex,\n    onToggleCollapse,\n    onAddTask,\n    onCardClick,\n    onCardQuickView,\n    onDragStart,\n    onDragEnd,\n    onCardDragOver,\n    onLaneDragOver,\n    onDrop,\n  }: KanbanColumnProps = $props()\n</script>\n\n<div\n  data-slot=\"kanban-board\"\n  class={['group/col flex shrink-0 flex-col transition-[width] duration-200', collapsed ? 'w-12' : 'w-80']}\n  ondragover={(e) => e.preventDefault()}\n  ondrop={() => onDrop?.()}\n>\n  {#if collapsed}\n    <button\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      onclick={() => onToggleCollapse?.(column.id)}\n    >\n      <span class={['size-2 shrink-0 rounded-full', column.dotColor]}></span>\n      <span class={['text-xs font-semibold tracking-tight', column.color, 'rotate-180 [writing-mode:vertical-lr]']}>\n        {column.title}\n      </span>\n      <Badge variant=\"secondary\" class=\"mt-1 h-5 min-w-5 justify-center rounded-md px-1 text-xs\">\n        {column.tasks.length}\n      </Badge>\n      <ChevronsRight class=\"text-muted-foreground mt-auto size-3.5\" />\n    </button>\n  {: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        onclick={() => onToggleCollapse?.(column.id)}\n      >\n        <ChevronsLeft class=\"size-3.5\" />\n      </button>\n      <span class={['size-2 shrink-0 rounded-full', column.dotColor]}></span>\n      <h3 class={['text-sm 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-xs font-medium tabular-nums\">\n        {column.tasks.length}\n      </span>\n      <div class=\"ml-auto flex items-center\">\n        <Button\n          aria-label=\"Column options\"\n          variant=\"ghost\"\n          size=\"icon\"\n          class=\"text-muted-foreground size-6 opacity-0 group-hover/col:opacity-100\"\n        >\n          <MoreHorizontal class=\"size-3.5\" />\n        </Button>\n        <Button\n          aria-label=\"Add task\"\n          variant=\"ghost\"\n          size=\"icon\"\n          class=\"text-muted-foreground size-6\"\n          onclick={() => onAddTask?.(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-colors 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      {#each column.tasks as task, taskIndex (task.id)}\n        <div\n          class={[\n            'drop-indicator mx-1 transition-colors 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        ></div>\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          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}\n            {columns}\n            isDone={column.id === 'done'}\n            onClick={(t) => onCardClick?.(t)}\n            onQuickView={(t) => onCardQuickView?.(t)}\n          />\n        </div>\n      {/each}\n\n      {#if column.tasks.length > 0}\n        <div\n          class={[\n            'drop-indicator mx-1 transition-colors 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        ></div>\n      {/if}\n\n      {#if column.tasks.length === 0}\n        <button\n          aria-label=\"Add task\"\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          onclick={() => onAddTask?.(column.id)}\n        >\n          <Plus class=\"mb-1 size-4\" />\n          <p class=\"text-xs\">No tasks</p>\n        </button>\n      {/if}\n    </div>\n\n    <button\n      aria-label=\"Add task\"\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      onclick={() => onAddTask?.(column.id)}\n    >\n      <Plus class=\"size-3.5\" />\n      Add task\n    </button>\n  {/if}\n</div>\n\n<style>\n  .kanban-lane {\n    min-height: 60px;\n  }\n</style>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/KanbanColumn.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/KanbanListView.svelte",
      "content": "<script lang=\"ts\">\n  import type { KanbanTask, KanbanColumn } from '$lib/composables/useKanban'\n  import { getTaskColumn } from '$lib/composables/useKanban'\n  import PriorityBadge from './PriorityBadge.svelte'\n  import TagBadge from './TagBadge.svelte'\n  import DueDateBadge from './DueDateBadge.svelte'\n  import UserAvatar from './UserAvatar.svelte'\n  import SubtaskProgress from './SubtaskProgress.svelte'\n  import KanbanLink from './KanbanLink.svelte'\n  import { Badge } from '$lib/components/ui/badge'\n  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '$lib/components/ui/select'\n  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '$lib/components/ui/tooltip'\n  import { MessageSquare, Paperclip, ExternalLink, ArrowUpDown, ChevronDown, ChevronRight } from '@lucide/svelte'\n\n  interface KanbanListViewProps {\n    columns: KanbanColumn[]\n    allColumns: KanbanColumn[]\n    onTaskClick?: (task: KanbanTask) => void\n    onMoveTask?: (task: KanbanTask, targetColumnId: string) => void\n  }\n\n  let { columns, allColumns, onTaskClick, onMoveTask }: KanbanListViewProps = $props()\n\n  type SortField = 'id' | 'title' | 'priority' | 'assignee' | 'dueDate' | 'status'\n  type SortDir = 'asc' | 'desc'\n\n  let sortField = $state<SortField>('status')\n  let sortDir = $state<SortDir>('asc')\n\n  const priorityOrder: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3 }\n\n  function toggleSort(field: SortField) {\n    if (sortField === field) {\n      sortDir = sortDir === 'asc' ? 'desc' : 'asc'\n    } else {\n      sortField = field\n      sortDir = 'asc'\n    }\n  }\n\n  const groupByStatus = $state(true)\n  let collapsedGroups = $state<Set<string>>(new Set())\n\n  function toggleGroup(columnId: string) {\n    const next = new Set(collapsedGroups)\n    if (next.has(columnId)) next.delete(columnId)\n    else next.add(columnId)\n    collapsedGroups = next\n  }\n\n  interface FlatTask {\n    task: KanbanTask\n    columnId: string\n    columnTitle: string\n    dotColor: string\n  }\n\n  function sortTasks(tasks: FlatTask[]): FlatTask[] {\n    return [...tasks].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  }\n\n  const flatTasks = $derived.by((): FlatTask[] => {\n    const items: FlatTask[] = []\n    for (const col of 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\n  const groupedTasks = $derived.by(() => {\n    if (!groupByStatus) return null\n    const groups: { column: KanbanColumn; 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  })\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</script>\n\n<div data-slot=\"kanban-board\" 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-xs font-semibold tracking-wider uppercase\"\n  >\n    <button class=\"flex items-center gap-1 text-left\" onclick={() => 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\" onclick={() => 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\" onclick={() => 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\" onclick={() => 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\" onclick={() => 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\" onclick={() => 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  {#if groupedTasks}\n    {#each groupedTasks as group (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        onclick={() => toggleGroup(group.column.id)}\n      >\n        {#if collapsedGroups.has(group.column.id)}\n          <ChevronRight class=\"text-muted-foreground size-3.5\" />\n        {:else}\n          <ChevronDown class=\"text-muted-foreground size-3.5\" />\n        {/if}\n        <span class={['size-2 rounded-full', group.column.dotColor]}></span>\n        <span class=\"text-sm font-medium\">{group.column.title}</span>\n        <Badge variant=\"secondary\" class=\"ml-1 h-4 px-1.5 text-xs tabular-nums\">\n          {group.tasks.length}\n        </Badge>\n      </button>\n\n      {#if !collapsedGroups.has(group.column.id)}\n        {#each group.tasks as item (item.task.id)}\n          <div\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            onclick={() => onTaskClick?.(item.task)}\n            onkeydown={(e) => {\n              if (e.key === 'Enter') onTaskClick?.(item.task)\n            }}\n            role=\"button\"\n            tabindex=\"0\"\n          >\n            <span class=\"text-muted-foreground font-mono text-xs\">{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-sm font-medium',\n                    item.columnId === 'done' ? 'text-muted-foreground line-through' : '',\n                  ]}\n                >\n                  {item.task.title}\n                </span>\n                <KanbanLink\n                  href=\"/dashboard/kanban/{item.task.id}\"\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                  onclick={(e) => e.stopPropagation()}\n                >\n                  <ExternalLink class=\"size-3\" />\n                </KanbanLink>\n              </div>\n              {#if item.task.tags.length || item.task.subtaskIds.length}\n                <div class=\"mt-0.5 flex items-center gap-1.5\">\n                  {#each item.task.tags as tag (tag.label)}\n                    <TagBadge label={tag.label} color={tag.color} class=\"!px-1.5 !py-0 !text-xs\" />\n                  {/each}\n                  {#if item.task.subtaskIds.length}\n                    <SubtaskProgress\n                      done={subtasksDone(item.task)}\n                      total={item.task.subtaskIds.length}\n                      class=\"ml-1\"\n                    />\n                  {/if}\n                </div>\n              {/if}\n            </div>\n\n            <div>\n              <Select value={item.columnId} onValueChange={(val) => onMoveTask?.(item.task, String(val))}>\n                <SelectTrigger\n                  class=\"hover:bg-muted h-6 w-auto gap-1 rounded-md border-none bg-transparent px-1.5 text-xs font-medium shadow-none\"\n                  onclick={(e) => e.stopPropagation()}\n                >\n                  <span class=\"flex items-center gap-1.5\">\n                    <span class={['size-1.5 rounded-full', item.dotColor]}></span>\n                    <SelectValue />\n                  </span>\n                </SelectTrigger>\n                <SelectContent>\n                  {#each allColumns as col (col.id)}\n                    <SelectItem value={col.id}>\n                      <span class=\"flex items-center gap-1.5\">\n                        <span class={['size-1.5 rounded-full', col.dotColor]}></span>\n                        {col.title}\n                      </span>\n                    </SelectItem>\n                  {/each}\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-xs\">{item.task.assignee.name}</span>\n            </div>\n\n            <div>\n              {#if item.task.dueDate}\n                <DueDateBadge dueDate={item.task.dueDate} variant=\"chip\" />\n              {:else}\n                <span class=\"text-muted-foreground/50 text-xs\">—</span>\n              {/if}\n            </div>\n\n            <div class=\"flex items-center justify-center gap-2\">\n              <TooltipProvider delayDuration={200}>\n                {#if item.task.commentItems.length}\n                  <Tooltip>\n                    <TooltipTrigger>\n                      <span class=\"text-muted-foreground/70 flex items-center gap-0.5 text-xs\">\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                {/if}\n                {#if item.task.fileItems.length}\n                  <Tooltip>\n                    <TooltipTrigger>\n                      <span class=\"text-muted-foreground/70 flex items-center gap-0.5 text-xs\">\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                {/if}\n              </TooltipProvider>\n            </div>\n          </div>\n        {/each}\n      {/if}\n    {/each}\n  {/if}\n\n  {#if flatTasks.length === 0}\n    <div\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  {/if}\n</div>\n\n<style>\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:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/KanbanListView.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/KanbanTaskSheet.svelte",
      "content": "<script lang=\"ts\">\n  import { type KanbanTask, type KanbanColumn, priorityConfig, fileIconMap } from '$lib/composables/useKanban'\n  import PriorityBadge from './PriorityBadge.svelte'\n  import TagBadge from './TagBadge.svelte'\n  import DueDateBadge from './DueDateBadge.svelte'\n  import UserAvatar from './UserAvatar.svelte'\n  import CommentList from './CommentList.svelte'\n  import SubtaskList from './SubtaskList.svelte'\n  import KanbanLink from './KanbanLink.svelte'\n  import { Sheet, SheetContent, SheetTitle, SheetDescription, SheetFooter, SheetClose } from '$lib/components/ui/sheet'\n  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '$lib/components/ui/select'\n  import { Button } from '$lib/components/ui/button'\n  import { Clock, Download, ExternalLink } from '@lucide/svelte'\n  import { tick } from 'svelte'\n\n  interface KanbanTaskSheetProps {\n    open?: boolean\n    task: KanbanTask | null\n    columns: KanbanColumn[]\n    onMoveTask?: (task: KanbanTask, columnId: string) => void\n    onAddComment?: (task: KanbanTask, text: string) => void\n  }\n\n  let {\n    open = $bindable(false),\n    task,\n    columns,\n    onMoveTask,\n    onAddComment,\n  }: KanbanTaskSheetProps = $props()\n\n  const columnIdForTask = $derived.by(() => {\n    if (!task) return ''\n    return columns.find((c) => c.tasks.some((t) => t.id === task.id))?.id ?? ''\n  })\n\n  // A status change re-renders the card in its new column, so the element that\n  // opened the sheet is gone by close time; send focus to the card's new node.\n  // Runs after SheetContent's own focus restore (a microtask later).\n  $effect(() => {\n    if (!open) return\n    return () => {\n      const id = task?.id\n      if (!id || typeof document === 'undefined') return\n      tick().then(() => {\n        document.querySelector<HTMLElement>(`[data-task-id=\"${id}\"] > [role=\"button\"]`)?.focus()\n      })\n    }\n  })\n</script>\n\n<Sheet bind:open>\n  <SheetContent data-slot=\"kanban-board\" class=\"flex max-w-[420px] flex-col gap-0 overflow-hidden p-0 sm:max-w-full\">\n    {#if task}\n      <div class={['h-1 w-full shrink-0', priorityConfig[task.priority]?.bg]}></div>\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-xs tracking-tight\">{task.id}</span>\n          <span class=\"text-muted-foreground/30\">·</span>\n          <Select value={columnIdForTask} onValueChange={(val) => task && onMoveTask?.(task, String(val))}>\n            <SelectTrigger\n              aria-label=\"Status\"\n              class=\"hover:bg-secondary h-5 w-auto gap-1 rounded-md border-none bg-transparent px-1.5 text-xs font-medium shadow-none\"\n            >\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent>\n              {#each columns as col (col.id)}\n                <SelectItem value={col.id}>\n                  <span class=\"flex items-center gap-1.5\">\n                    <span class={['size-1.5 rounded-full', col.dotColor]}></span>\n                    {col.title}\n                  </span>\n                </SelectItem>\n              {/each}\n            </SelectContent>\n          </Select>\n          <PriorityBadge priority={task.priority} iconSize=\"size-3\" class=\"ml-auto\" />\n        </div>\n\n        <SheetTitle class=\"text-sm leading-snug font-semibold tracking-tight\">\n          {task.title}\n        </SheetTitle>\n        <SheetDescription class=\"sr-only\">Task details</SheetDescription>\n        {#if task.description}\n          <div\n            class=\"text-muted-foreground rich-text-content prose prose-sm dark:prose-invert mt-1.5 max-w-none text-sm leading-relaxed\"\n          >\n            {@html task.description}\n          </div>\n        {:else}\n          <p class=\"text-muted-foreground mt-1.5 text-sm leading-relaxed\">No description provided.</p>\n        {/if}\n\n        <div class=\"mt-3 flex flex-wrap gap-1.5\">\n          {#if task.tags.length}\n            {#each task.tags as tag (tag.label)}\n              <TagBadge label={tag.label} color={tag.color} />\n            {/each}\n          {:else}\n            <span class=\"text-muted-foreground text-xs\">No tags</span>\n          {/if}\n        </div>\n      </div>\n\n      <div class=\"bg-border mx-5 h-px\"></div>\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-sm leading-tight font-medium\">{task.assignee.name}</p>\n              <p class=\"text-muted-foreground text-xs\">Assignee</p>\n            </div>\n            <div class=\"ml-auto text-right\">\n              {#if task.dueDate}\n                <DueDateBadge dueDate={task.dueDate} />\n              {:else}\n                <p class=\"text-muted-foreground flex items-center gap-1 text-sm leading-tight\">\n                  <Clock class=\"size-3\" />\n                  No due date\n                </p>\n              {/if}\n            </div>\n          </div>\n\n          {#if task.parentId}\n            <div class=\"flex items-center gap-2\">\n              <span class=\"text-muted-foreground text-xs\">Parent:</span>\n              <KanbanLink\n                href=\"/dashboard/kanban/{task.parentId}\"\n                to=\"/dashboard/kanban/{task.parentId}\"\n                class=\"text-primary text-xs font-medium hover:underline\"\n                onclick={() => (open = false)}\n              >\n                {task.parentId}\n              </KanbanLink>\n            </div>\n          {/if}\n\n          <div>\n            <h4 class=\"mb-2 text-sm font-semibold\">\n              Subtasks\n              {#if task.subtaskIds.length}\n                <span class=\"text-muted-foreground font-normal\">\n                  ({task.subtaskIds.length})\n                </span>\n              {/if}\n            </h4>\n            <SubtaskList subtaskIds={task.subtaskIds} {columns} compact />\n          </div>\n\n          <div class=\"bg-border h-px\"></div>\n\n          <div>\n            <h4 class=\"mb-2 text-sm font-semibold\">\n              Comments\n              {#if task.commentItems.length}\n                <span class=\"text-muted-foreground font-normal\">\n                  ({task.commentItems.length})\n                </span>\n              {/if}\n            </h4>\n            <CommentList\n              comments={task.commentItems}\n              compact\n              onAdd={(text) => task && onAddComment?.(task, text)}\n            />\n          </div>\n\n          <div class=\"bg-border h-px\"></div>\n\n          <div>\n            <h4 class=\"mb-2 text-sm font-semibold\">\n              Files\n              {#if task.fileItems.length}\n                <span class=\"text-muted-foreground font-normal\">\n                  ({task.fileItems.length})\n                </span>\n              {/if}\n            </h4>\n            {#if task.fileItems.length}\n              <div class=\"space-y-1\">\n                {#each task.fileItems as file (file.id)}\n                  {@const FileIcon = fileIconMap[file.type] ?? fileIconMap.other}\n                  <div\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                      {#if FileIcon}\n                        <FileIcon class=\"text-muted-foreground size-4\" />\n                      {/if}\n                    </div>\n                    <div class=\"min-w-0 flex-1\">\n                      <p class=\"truncate text-xs font-medium\">{file.name}</p>\n                      <p class=\"text-muted-foreground text-xs\">{file.size}</p>\n                    </div>\n                    <Button\n                      aria-label=\"Download attachment\"\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      class=\"size-7 shrink-0 opacity-0 group-hover/file:opacity-100\"\n                    >\n                      <Download class=\"size-3.5\" />\n                    </Button>\n                  </div>\n                {/each}\n              </div>\n            {:else}\n              <p class=\"text-muted-foreground text-xs\">No files attached.</p>\n            {/if}\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          <KanbanLink\n            href=\"/dashboard/kanban/{task.id}\"\n            to=\"/dashboard/kanban/{task.id}\"\n            class=\"flex-1\"\n            onclick={() => (open = false)}\n          >\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          </KanbanLink>\n          <SheetClose>\n            {#snippet child({ props })}\n              <Button variant=\"ghost\" size=\"sm\" {...props}>Close</Button>\n            {/snippet}\n          </SheetClose>\n        </div>\n      </SheetFooter>\n    {/if}\n  </SheetContent>\n</Sheet>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/KanbanTaskSheet.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/KanbanAddTaskDialog.svelte",
      "content": "<script lang=\"ts\">\n  import type { KanbanTask, KanbanColumn } from '$lib/composables/useKanban'\n  import { priorityConfig, assignees, tagPresets } from '$lib/composables/useKanban'\n  import {\n    Dialog,\n    DialogScrollContent,\n    DialogHeader,\n    DialogTitle,\n    DialogDescription,\n    DialogFooter,\n  } from '$lib/components/ui/dialog'\n  import { Button } from '$lib/components/ui/button'\n  import { Input } from '$lib/components/ui/input'\n  import { Label } from '$lib/components/ui/label'\n  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '$lib/components/ui/select'\n  import { Popover, PopoverContent, PopoverTrigger } from '$lib/components/ui/popover'\n  import { Calendar } from '$lib/components/ui/calendar'\n  import { RichTextEditor } from '$lib/components/ui/rich-text-editor'\n  import { Plus, X, CheckCircle2, Calendar as CalendarIcon } from '@lucide/svelte'\n  import { getLocalTimeZone, DateFormatter, type DateValue } from '@internationalized/date'\n\n  interface KanbanAddTaskDialogProps {\n    open?: boolean\n    columns: KanbanColumn[]\n    initialColumnId: string\n    onCreate?: (columnId: string, tasks: KanbanTask[]) => void\n  }\n\n  let { open = $bindable(false), columns, initialColumnId, onCreate }: KanbanAddTaskDialogProps = $props()\n\n  const df = new DateFormatter('en-US', { dateStyle: 'medium' })\n\n  let columnId = $state(initialColumnId)\n  let dueDate = $state<DateValue | undefined>(undefined)\n  let form = $state({\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  })\n  let newSubtaskText = $state('')\n\n  function resetForm() {\n    columnId = initialColumnId\n    form = {\n      title: '',\n      description: '',\n      priority: 'medium',\n      assigneeKey: 'alice',\n      tagKeys: [],\n      subtaskTexts: [],\n    }\n    dueDate = undefined\n    newSubtaskText = ''\n  }\n\n  // Reset the draft whenever the dialog opens (and track late column changes).\n  $effect(() => {\n    if (open) resetForm()\n  })\n\n  $effect(() => {\n    columnId = initialColumnId\n  })\n\n  function addSubtask() {\n    const text = newSubtaskText.trim()\n    if (!text) return\n    form.subtaskTexts.push(text)\n    newSubtaskText = ''\n  }\n\n  function removeSubtask(index: number) {\n    form.subtaskTexts.splice(index, 1)\n  }\n\n  function toggleTag(key: keyof typeof tagPresets) {\n    const idx = form.tagKeys.indexOf(key)\n    if (idx >= 0) form.tagKeys.splice(idx, 1)\n    else form.tagKeys.push(key)\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.toString() : undefined,\n      subtaskIds: subtaskTasks.map((t) => t.id),\n      commentItems: [],\n      fileItems: [],\n    }\n    onCreate?.(columnId, [newTask, ...subtaskTasks])\n    open = false\n  }\n</script>\n\n<Dialog bind:open>\n  <DialogScrollContent data-slot=\"kanban-board\" class=\"max-w-[680px] sm:max-w-full\">\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\" bind:value={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 bind:value={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 value={form.priority} onValueChange={(v) => (form.priority = v as KanbanTask['priority'])}>\n            <SelectTrigger id=\"task-priority\">\n              <SelectValue placeholder=\"Priority\" />\n            </SelectTrigger>\n            <SelectContent>\n              {#each Object.entries(priorityConfig) as [key, config] (key)}\n                <SelectItem value={key}>\n                  {config.label}\n                </SelectItem>\n              {/each}\n            </SelectContent>\n          </Select>\n        </div>\n\n        <div class=\"grid gap-1.5\">\n          <Label for=\"task-assignee\">Assignee</Label>\n          <Select\n            value={form.assigneeKey}\n            onValueChange={(v) => (form.assigneeKey = v as keyof typeof assignees)}\n          >\n            <SelectTrigger id=\"task-assignee\">\n              <SelectValue placeholder=\"Assignee\" />\n            </SelectTrigger>\n            <SelectContent>\n              {#each Object.entries(assignees) as [key, person] (key)}\n                <SelectItem value={key}>\n                  {person.name}\n                </SelectItem>\n              {/each}\n            </SelectContent>\n          </Select>\n        </div>\n\n        <div class=\"grid gap-1.5\">\n          <Label for=\"task-column\">Column</Label>\n          <Select bind:value={columnId}>\n            <SelectTrigger id=\"task-column\">\n              <SelectValue placeholder=\"Column\" />\n            </SelectTrigger>\n            <SelectContent>\n              {#each columns as col (col.id)}\n                <SelectItem value={col.id}>\n                  {col.title}\n                </SelectItem>\n              {/each}\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>\n            {#snippet child({ props })}\n              <Button\n                variant=\"outline\"\n                {...props}\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            {/snippet}\n          </PopoverTrigger>\n          <PopoverContent class=\"w-auto p-0\">\n            <Calendar bind:value={dueDate} />\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          {#each Object.entries(tagPresets) as [key, tag] (key)}\n            {@const tagKey = key as keyof typeof tagPresets}\n            {@const selected = form.tagKeys.includes(tagKey)}\n            <button\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                selected\n                  ? 'bg-primary text-primary-foreground border-transparent'\n                  : 'border-border bg-background text-foreground hover:bg-muted',\n              ]}\n              onclick={() => toggleTag(tagKey)}\n            >\n              {#if selected}\n                <CheckCircle2 class=\"size-3\" />\n              {/if}\n              {tag.label}\n            </button>\n          {/each}\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        {#if form.subtaskTexts.length}\n          <div class=\"space-y-2\">\n            {#each form.subtaskTexts as text, index (index)}\n              <div class=\"flex items-center gap-2\">\n                <span class=\"flex-1 text-sm\">{text}</span>\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  class=\"size-6\"\n                  aria-label=\"Remove subtask\"\n                  onclick={() => removeSubtask(index)}\n                >\n                  <X class=\"size-3\" />\n                </Button>\n              </div>\n            {/each}\n          </div>\n        {/if}\n        <div class=\"flex items-center gap-2\">\n          <Input\n            bind:value={newSubtaskText}\n            placeholder=\"Add a subtask\"\n            onkeydown={(e) => {\n              if (e.key === 'Enter') addSubtask()\n            }}\n          />\n          <Button variant=\"outline\" size=\"icon\" aria-label=\"Add subtask\" onclick={addSubtask}>\n            <Plus class=\"size-4\" />\n          </Button>\n        </div>\n      </div>\n    </div>\n\n    <DialogFooter>\n      <Button variant=\"outline\" onclick={() => (open = false)}>Cancel</Button>\n      <Button disabled={!form.title.trim()} onclick={submit}>Create Task</Button>\n    </DialogFooter>\n  </DialogScrollContent>\n</Dialog>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/KanbanAddTaskDialog.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/KanbanToolbar.svelte",
      "content": "<script lang=\"ts\">\n  import { Search, Filter, ChevronDown, X, LayoutGrid, List } from '@lucide/svelte'\n  import { Input } from '$lib/components/ui/input'\n  import { Button } from '$lib/components/ui/button'\n  import { Badge } from '$lib/components/ui/badge'\n  import { ToggleGroup, ToggleGroupItem } from '$lib/components/ui/toggle-group'\n  import {\n    DropdownMenu,\n    DropdownMenuContent,\n    DropdownMenuItem,\n    DropdownMenuSeparator,\n    DropdownMenuTrigger,\n  } from '$lib/components/ui/dropdown-menu'\n  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '$lib/components/ui/tooltip'\n  import { Avatar, AvatarFallback } from '$lib/components/ui/avatar'\n  import { priorityConfig, assignees, getInitials } from '$lib/composables/useKanban'\n\n  interface KanbanToolbarProps {\n    searchQuery?: string\n    selectedPriority?: string | null\n    selectedAssignee?: string | null\n    viewMode?: 'board' | 'list'\n  }\n\n  let {\n    searchQuery = $bindable(''),\n    selectedPriority = $bindable(null),\n    selectedAssignee = $bindable(null),\n    viewMode = $bindable('board'),\n  }: KanbanToolbarProps = $props()\n</script>\n\n<div data-slot=\"kanban-board\" 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 bind:value={searchQuery} placeholder=\"Search tasks...\" class=\"h-8 pl-8 text-sm\" />\n  </div>\n\n  <DropdownMenu>\n    <DropdownMenuTrigger>\n      {#snippet child({ props })}\n        <Button variant=\"outline\" size=\"sm\" {...props} class=\"h-8 gap-1.5 text-xs\">\n          <Filter class=\"size-3\" />\n          Priority\n          {#if selectedPriority}\n            <Badge variant=\"default\" class=\"ml-0.5 h-4 min-w-4 justify-center rounded px-1 text-xs\">1</Badge>\n          {/if}\n          <ChevronDown class=\"text-muted-foreground size-3\" />\n        </Button>\n      {/snippet}\n    </DropdownMenuTrigger>\n    <DropdownMenuContent align=\"start\" class=\"w-40\">\n      {#each Object.entries(priorityConfig) as [key, config] (key)}\n        {@const Icon = config.icon}\n        <DropdownMenuItem\n          class=\"gap-2\"\n          onclick={() => (selectedPriority = selectedPriority === key ? null : key)}\n        >\n          <Icon class={['size-3.5', config.class]} />\n          {config.label}\n          {#if selectedPriority === key}\n            <span class=\"bg-primary ml-auto size-1.5 rounded-full\"></span>\n          {/if}\n        </DropdownMenuItem>\n      {/each}\n      {#if selectedPriority}\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onclick={() => (selectedPriority = null)}>Clear filter</DropdownMenuItem>\n      {/if}\n    </DropdownMenuContent>\n  </DropdownMenu>\n\n  {#if selectedAssignee}\n    <Badge\n      variant=\"secondary\"\n      class=\"h-7 cursor-pointer gap-1 pr-1.5 text-xs\"\n      onclick={() => (selectedAssignee = null)}\n    >\n      {selectedAssignee}\n      <X class=\"size-3 opacity-50\" />\n    </Badge>\n  {/if}\n\n  <ToggleGroup\n    type=\"single\"\n    size=\"sm\"\n    animated={false}\n    value={viewMode}\n    onValueChange={(v) => {\n      if (v) viewMode = v as 'board' | 'list'\n    }}\n    class=\"bg-muted ml-auto flex items-center gap-0.5 rounded-md p-0.5\"\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 delayDuration={300}>\n      {#each Object.entries(assignees) as [key, a] (key)}\n        <Tooltip>\n          <TooltipTrigger>\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-colors 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={() => (selectedAssignee = selectedAssignee === a.name ? null : a.name)}\n            >\n              <Avatar class=\"border-background size-7 border-2\">\n                <AvatarFallback class={['text-xs 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            {#if selectedAssignee === a.name}\n              <span class=\"text-muted-foreground ml-1\">(filtered)</span>\n            {/if}\n          </TooltipContent>\n        </Tooltip>\n      {/each}\n    </TooltipProvider>\n  </div>\n</div>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/KanbanToolbar.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/KanbanLink.svelte",
      "content": "<script lang=\"ts\">\n  import type { Snippet } from 'svelte'\n  import { getKanbanLinkContext } from './link-context'\n\n  interface KanbanLinkProps {\n    href: string\n    /** Router-style destination (passed to custom link components; defaults to `href`). */\n    to?: string\n    class?: string\n    onclick?: (e: MouseEvent) => void\n    children?: Snippet\n  }\n\n  let { href, to, class: className, onclick, children }: KanbanLinkProps = $props()\n\n  const linkCtx = getKanbanLinkContext()\n</script>\n\n{#if typeof linkCtx.current === 'string'}\n  <a {href} class={className} {onclick}>{@render children?.()}</a>\n{:else}\n  {@const Link = linkCtx.current}\n  <Link {href} to={to ?? href} class={className} {onclick}>{@render children?.()}</Link>\n{/if}\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/KanbanLink.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/PriorityBadge.svelte",
      "content": "<script lang=\"ts\">\n  import { priorityConfig } from '$lib/composables/useKanban'\n  import { cn } from '$lib/utils'\n\n  interface PriorityBadgeProps {\n    priority: string\n    iconSize?: string\n    class?: string\n  }\n\n  let { priority, iconSize = 'size-3.5', class: className }: PriorityBadgeProps = $props()\n\n  const config = $derived(priorityConfig[priority])\n</script>\n\n{#if config}\n  {@const Icon = config.icon}\n  <div data-slot=\"kanban-board\" class={cn('flex items-center gap-1', className)}>\n    <Icon class={[iconSize, config.class]} />\n    <span class={['text-xs font-semibold', config.class]}>{config.label}</span>\n  </div>\n{/if}\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/PriorityBadge.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/DueDateBadge.svelte",
      "content": "<script lang=\"ts\">\n  import { Clock } from '@lucide/svelte'\n  import { getDueStatus, formatDueDate } from '$lib/composables/useKanban'\n  import { cn } from '$lib/utils'\n\n  interface DueDateBadgeProps {\n    dueDate: string\n    variant?: 'chip' | 'inline'\n    class?: string\n  }\n\n  let { dueDate, variant, class: className }: DueDateBadgeProps = $props()\n\n  const status = $derived(getDueStatus(dueDate))\n  const formatted = $derived(formatDueDate(dueDate))\n\n  const chipClasses = $derived(\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\n  const inlineClasses = $derived(\n    status === 'overdue' ? 'text-destructive' : status === 'soon' ? 'text-warning' : 'text-foreground',\n  )\n</script>\n\n{#if variant === 'chip'}\n  <div\n    data-slot=\"kanban-board\"\n    class={cn('flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium', chipClasses, className)}\n  >\n    <Clock class=\"size-3\" />\n    {formatted}\n  </div>\n{:else}\n  <p data-slot=\"kanban-board\" class={cn('flex items-center gap-1 text-sm leading-tight font-medium', inlineClasses, className)}>\n    <Clock class=\"size-3\" />\n    {formatted}\n  </p>\n{/if}\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/DueDateBadge.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/TagBadge.svelte",
      "content": "<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  interface TagBadgeProps {\n    label: string\n    color: string\n    class?: string\n  }\n\n  let { label, color, class: className }: TagBadgeProps = $props()\n</script>\n\n<span\n  data-slot=\"kanban-board\"\n  class={cn('rounded-md px-1.5 py-0.5 text-xs font-medium ring-1 ring-inset', color, className)}\n>\n  {label}\n</span>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/TagBadge.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/UserAvatar.svelte",
      "content": "<script lang=\"ts\">\n  import { Avatar, AvatarFallback } from '$lib/components/ui/avatar'\n  import { getInitials } from '$lib/composables/useKanban'\n  import { cn } from '$lib/utils'\n\n  interface UserAvatarProps {\n    name: string\n    color: string\n    size?: 'xs' | 'sm' | 'md'\n    class?: string\n  }\n\n  let { name, color, size = 'sm', class: className }: UserAvatarProps = $props()\n\n  const sizeMap = {\n    xs: { avatar: 'size-6', text: 'text-xs' },\n    sm: { avatar: 'size-7', text: 'text-xs' },\n    md: { avatar: 'size-8', text: 'text-xs' },\n  }\n</script>\n\n<Avatar data-slot=\"kanban-board\" class={cn(sizeMap[size].avatar, 'shrink-0', className)}>\n  <AvatarFallback class={[sizeMap[size].text, 'font-bold', color]}>\n    {getInitials(name)}\n  </AvatarFallback>\n</Avatar>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/UserAvatar.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/SubtaskProgress.svelte",
      "content": "<script lang=\"ts\">\n  import { CheckCircle2 } from '@lucide/svelte'\n\n  interface SubtaskProgressProps {\n    done: number\n    total: number\n    barHeight?: string\n    class?: string\n  }\n\n  let { done, total, barHeight = 'h-1.5', class: className }: SubtaskProgressProps = $props()\n\n  const percent = $derived(total > 0 ? Math.round((done / total) * 100) : 0)\n  const isComplete = $derived(done === total && total > 0)\n</script>\n\n{#if total > 0}\n  <div data-slot=\"kanban-board\" class={className}>\n    <div class=\"mb-1 flex items-center justify-between\">\n      <span class=\"text-muted-foreground text-xs\">\n        {#if isComplete}\n          <CheckCircle2 class=\"text-success mr-0.5 inline size-3\" />\n        {/if}\n        {done}/{total} subtasks\n      </span>\n      <span class=\"text-muted-foreground text-xs font-medium tabular-nums\">{percent}%</span>\n    </div>\n    <div class={['bg-muted overflow-hidden rounded-full', barHeight]}>\n      <div\n        class={['h-full rounded-full transition-[width,background-color] duration-500', isComplete ? 'bg-success' : 'bg-primary']}\n        style=\"width: {percent}%\"\n      ></div>\n    </div>\n  </div>\n{/if}\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/SubtaskProgress.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/SubtaskList.svelte",
      "content": "<script lang=\"ts\">\n  import type { KanbanColumn } from '$lib/composables/useKanban'\n  import { findTaskById, getTaskColumn } from '$lib/composables/useKanban'\n  import KanbanLink from './KanbanLink.svelte'\n\n  interface SubtaskListProps {\n    subtaskIds: string[]\n    columns: KanbanColumn[]\n    compact?: boolean\n  }\n\n  let { subtaskIds, columns, compact = false }: SubtaskListProps = $props()\n\n  const subtasks = $derived(\n    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((s): s is NonNullable<typeof s> => s !== null),\n  )\n\n  const doneCount = $derived(subtasks.filter((s) => s.column?.id === 'done').length)\n  const percent = $derived(subtasks.length > 0 ? Math.round((doneCount / subtasks.length) * 100) : 0)\n</script>\n\n{#if subtasks.length}\n  <div data-slot=\"kanban-board\">\n    <div class=\"mb-2 flex items-center justify-between\">\n      <span class=\"text-muted-foreground text-xs tabular-nums\">{doneCount}/{subtasks.length} done</span>\n      <span class=\"text-muted-foreground text-xs font-medium tabular-nums\">{percent}%</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-[width,background-color] duration-500',\n          doneCount === subtasks.length ? 'bg-success' : 'bg-primary',\n        ]}\n        style=\"width: {percent}%\"\n      ></div>\n    </div>\n\n    <div class={compact ? 'space-y-0.5' : 'space-y-1'}>\n      {#each subtasks as { task, column } (task.id)}\n        <KanbanLink\n          href=\"/dashboard/kanban/{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          ].join(' ')}\n        >\n          <span class={['size-1.5 shrink-0 rounded-full', column?.dotColor ?? 'bg-muted-foreground']}></span>\n          <span class=\"text-muted-foreground/70 shrink-0 font-mono text-xs\">\n            {task.id}\n          </span>\n          <span\n            class={[\n              'min-w-0 flex-1 truncate',\n              compact ? 'text-xs' : 'text-sm',\n              column?.id === 'done' ? 'text-muted-foreground line-through' : 'text-foreground',\n            ]}\n          >\n            {task.title}\n          </span>\n          <span class={['shrink-0 rounded-md px-1.5 py-0.5 text-xs font-medium', column?.color ?? 'text-muted-foreground']}>\n            {column?.title ?? 'Unknown'}\n          </span>\n        </KanbanLink>\n      {/each}\n    </div>\n  </div>\n{:else}\n  <p class={compact ? 'text-muted-foreground text-xs' : 'text-muted-foreground text-sm'}>No subtasks yet.</p>\n{/if}\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/SubtaskList.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/CommentList.svelte",
      "content": "<script lang=\"ts\">\n  import { Send } from '@lucide/svelte'\n  import { Button } from '$lib/components/ui/button'\n  import { RichTextEditor } from '$lib/components/ui/rich-text-editor'\n  import UserAvatar from './UserAvatar.svelte'\n  import type { CommentItem } from '$lib/composables/useKanban'\n\n  interface CommentListProps {\n    comments: CommentItem[]\n    compact?: boolean\n    onAdd?: (text: string) => void\n  }\n\n  let { comments, compact = false, onAdd }: CommentListProps = $props()\n\n  let newComment = $state('')\n\n  const stripped = $derived(newComment.replace(/<[^>]*>/g, '').trim())\n\n  function submit() {\n    if (!stripped) return\n    onAdd?.(newComment)\n    newComment = ''\n  }\n</script>\n\n{#if comments.length}\n  <div data-slot=\"kanban-board\" class={compact ? 'space-y-3' : 'space-y-4'}>\n    {#each comments as comment (comment.id)}\n      <div 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-xs font-semibold' : 'text-sm font-semibold'}>\n              {compact ? comment.author.split(' ')[0] : comment.author}\n            </span>\n            <span class=\"text-muted-foreground text-xs\">\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 ? 'text-muted-foreground text-xs leading-relaxed' : 'text-muted-foreground text-sm leading-relaxed',\n            ]}\n          >\n            {@html comment.text}\n          </div>\n        </div>\n      </div>\n    {/each}\n  </div>\n{:else}\n  <p class={compact ? 'text-muted-foreground text-xs' : 'text-muted-foreground text-sm'}>No comments yet.</p>\n{/if}\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      bind:value={newComment}\n      placeholder=\"Write a comment...\"\n      minHeight={compact ? '60px' : '80px'}\n      class={compact ? 'text-xs' : 'text-sm'}\n    />\n    <div class=\"flex justify-end\">\n      <Button size=\"sm\" class={compact ? 'h-7 gap-1 text-xs' : 'h-8 gap-1.5 text-xs'} disabled={!stripped} onclick={submit}>\n        <Send class=\"size-3\" />\n        Comment\n      </Button>\n    </div>\n  </div>\n</div>\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/CommentList.svelte"
    },
    {
      "path": "packages/registry-svelte/blocks/kanban-task-board/link-context.ts",
      "content": "import { getContext, setContext } from 'svelte'\nimport type { Component } from 'svelte'\n\n// Shared context for the kanban-board link component.\n// KanbanBoard provides it; KanbanCard, KanbanListView, KanbanTaskSheet,\n// and SubtaskList consume it (through KanbanLink) so task links render\n// with the consumer's router component or a plain <a> by default.\n// A getter keeps consumers reactive if the prop changes post-mount.\nexport type KanbanLinkValue = string | Component<any>\n\nconst KEY = Symbol('kanbanLink')\n\nexport function setKanbanLinkContext(link: { readonly current: KanbanLinkValue }) {\n  setContext(KEY, link)\n}\n\nexport function getKanbanLinkContext(): { readonly current: KanbanLinkValue } {\n  return getContext<{ readonly current: KanbanLinkValue }>(KEY) ?? { current: 'a' }\n}\n",
      "type": "registry:page",
      "target": "~/src/lib/components/blocks/kanban-task-board/link-context.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte",
    "@internationalized/date"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/use-kanban.json",
    "https://uipkge.dev/r/svelte/page.json",
    "https://uipkge.dev/r/svelte/button.json",
    "https://uipkge.dev/r/svelte/badge.json",
    "https://uipkge.dev/r/svelte/input.json",
    "https://uipkge.dev/r/svelte/label.json",
    "https://uipkge.dev/r/svelte/select.json",
    "https://uipkge.dev/r/svelte/sheet.json",
    "https://uipkge.dev/r/svelte/dialog.json",
    "https://uipkge.dev/r/svelte/popover.json",
    "https://uipkge.dev/r/svelte/calendar.json",
    "https://uipkge.dev/r/svelte/dropdown-menu.json",
    "https://uipkge.dev/r/svelte/tooltip.json",
    "https://uipkge.dev/r/svelte/avatar.json",
    "https://uipkge.dev/r/svelte/toggle-group.json",
    "https://uipkge.dev/r/svelte/rich-text-editor.json"
  ],
  "description": "Full kanban task board 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 16 Svelte files (KanbanBoard + SimpleKanban + 6 kanban-* views + 7 small badges/lists + KanbanLink) plus link-context; types, configs and helpers come from the use-kanban hook. Bind with bind:columns; consumer owns the columns array so swapping the in-memory store for Drizzle/Postgres is a one-line change.",
  "categories": [
    "productivity",
    "dashboard",
    "data"
  ]
}