{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kanban-board",
  "type": "registry:block",
  "title": "Kanban board",
  "description": "Accessible card moves through real asynchronous callbacks; failures preserve positions.",
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "blocks/kanban-board.tsx",
      "type": "registry:component",
      "target": "components/shine/kanban-board.tsx",
      "content": "\"use client\";\nimport { useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nexport type BoardItem = { id: string; title: string; column: string; description?: string };\n/** Explicit move controls work with keyboard, touch and pointer; the server owns transitions. */\nexport function KanbanBoard({ title, columns, items, onOpen, onMove, loading = false, error, onRetry }: {\n title: string; columns: { id: string; label: string }[]; items: BoardItem[]; onOpen: (item: BoardItem) => void; onMove: (id: string, column: string) => Promise<void>; loading?: boolean; error?: string; onRetry: () => void;\n}) {\n const lock = useRef(false), [busy, setBusy] = useState<string | null>(null), [failure, setFailure] = useState(\"\"), [status, setStatus] = useState(\"\");\n async function move(item: BoardItem, column: string) { if (lock.current || column === item.column) return; lock.current = true; setBusy(item.id); setFailure(\"\"); setStatus(\"\"); try { await onMove(item.id, column); setStatus(item.title + \" moved to \" + columns.find(value => value.id === column)?.label + \".\"); } catch (cause) { setFailure(cause instanceof Error ? cause.message : \"Could not move the item. Try again.\"); } finally { lock.current = false; setBusy(null); } }\n const unknown = items.filter(item => !columns.some(column => column.id === item.column));\n return <section aria-label={title} data-product-pattern=\"kanban-board\" className=\"min-w-0 space-y-3\"><h2 className=\"text-lg font-semibold\">{title}</h2><p role=\"status\" className=\"text-sm\">{loading ? \"Loading board…\" : status}</p>{(error || failure) && <p role=\"alert\" className=\"text-sm text-destructive\">{error || failure}{error && <Button variant=\"outline\" type=\"button\" onClick={onRetry}>Retry board</Button>}</p>}{!loading && !error && unknown.length > 0 && <div role=\"alert\"><p>Some items have an unknown status. Open them to correct it.</p>{unknown.map(item => <Button key={item.id} type=\"button\" variant=\"outline\" onClick={() => onOpen(item)}>{item.title}</Button>)}</div>}\n  {!loading && !error && <div className=\"grid min-w-0 gap-4 md:grid-cols-2 xl:grid-cols-3\">{columns.map(column => <section key={column.id} aria-label={column.label} className=\"min-w-0 rounded-lg border border-border bg-muted p-3\"><h3 className=\"mb-3 font-medium\">{column.label} ({items.filter(item => item.column === column.id).length})</h3><ul className=\"space-y-3\">{items.filter(item => item.column === column.id).map(item => <li key={item.id} className=\"min-w-0 space-y-3 rounded-md border border-border bg-background p-3\"><Button type=\"button\" variant=\"ghost\" className=\"h-auto w-full justify-start whitespace-normal p-0 text-left\" onClick={() => onOpen(item)}>{item.title}</Button>{item.description && <p className=\"break-words text-sm text-muted-foreground\">{item.description}</p>}<label className=\"block space-y-1 text-sm\"><span>Move {item.title}</span><select aria-label={\"Move \" + item.title} className=\"h-11 w-full rounded-md border border-border bg-background px-2\" value={item.column} disabled={Boolean(busy)} onChange={e => void move(item, e.target.value)}>{columns.map(option => <option key={option.id} value={option.id}>{option.label}</option>)}</select></label></li>)}</ul>{!items.some(item => item.column === column.id) && <p className=\"text-sm text-muted-foreground\">No items</p>}</section>)}</div>}\n </section>;\n}\n"
    }
  ]
}
