{
  "name": "kanban",
  "type": "registry:block",
  "dependencies": [
    "@dnd-kit/core",
    "@dnd-kit/sortable",
    "@dnd-kit/utilities",
    "lucide-react",
    "react"
  ],
  "registryDependencies": [
    "button",
    "card",
    "input"
  ],
  "files": [
    {
      "path": "./items/blocks/kanban/component.tsx",
      "type": "registry:block",
      "target": "./components/block/shuip/kanban.tsx",
      "content": "'use client';\n\nimport {\n  closestCorners,\n  DndContext,\n  type DragEndEvent,\n  type DragOverEvent,\n  DragOverlay,\n  type DragStartEvent,\n  KeyboardSensor,\n  PointerSensor,\n  useDroppable,\n  useSensor,\n  useSensors,\n} from '@dnd-kit/core';\nimport {\n  arrayMove,\n  SortableContext,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n} from '@dnd-kit/sortable';\nimport { CSS } from '@dnd-kit/utilities';\nimport { GripVertical, Plus, Search } from 'lucide-react';\nimport * as React from 'react';\nimport { Button } from '@/components/ui/button';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Input } from '@/components/ui/input';\nimport { cn } from '@/lib/utils';\n\nexport type KanbanColumn = {\n  id: string;\n  label: string;\n  color?: string;\n};\n\nexport type KanbanField<T> = {\n  key: keyof T;\n  label?: string;\n  render?: (value: T[keyof T], item: T) => React.ReactNode;\n};\n\nexport type KanbanProps<T extends Record<string, unknown>> = {\n  columns: KanbanColumn[];\n  data?: T[];\n  defaultData?: T[];\n  onDataChange?: (next: T[]) => void;\n  idField?: keyof T;\n  columnField: keyof T;\n  title?: (item: T) => React.ReactNode;\n  fields?: KanbanField<T>[];\n  cardContent?: (item: T) => React.ReactNode;\n  searchableFields?: (keyof T)[];\n  searchPlaceholder?: string;\n  renderColumnSummary?: (items: T[], column: KanbanColumn) => React.ReactNode;\n  onCardClick?: (item: T) => void;\n  onCardAdd?: (columnId: string) => void;\n  onCardMove?: (e: { item: T; fromColumn: string; toColumn: string; toIndex: number }) => void;\n  className?: string;\n};\n\nexport function Kanban<T extends Record<string, unknown>>({\n  columns,\n  data,\n  defaultData,\n  onDataChange,\n  idField = 'id' as keyof T,\n  columnField,\n  title,\n  fields,\n  cardContent,\n  searchableFields,\n  searchPlaceholder = 'Search...',\n  renderColumnSummary,\n  onCardClick,\n  onCardAdd,\n  onCardMove,\n  className,\n}: KanbanProps<T>) {\n  const getId = React.useCallback((item: T) => String(item[idField]), [idField]);\n  const getColumn = React.useCallback((item: T) => String(item[columnField]), [columnField]);\n\n  const [items, setItems] = React.useState<T[]>(() => data ?? defaultData ?? []);\n  const draggingRef = React.useRef(false);\n  const fromColumnRef = React.useRef<string | null>(null);\n  const fromIndexRef = React.useRef<number | null>(null);\n  const startItemsRef = React.useRef<T[] | null>(null);\n  React.useEffect(() => {\n    if (data && !draggingRef.current) setItems(data);\n  }, [data]);\n\n  const [query, setQuery] = React.useState('');\n  const deferredQuery = React.useDeferredValue(query);\n  const [activeId, setActiveId] = React.useState<string | null>(null);\n\n  const sensors = useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),\n    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n  );\n\n  const visible = React.useMemo(() => {\n    const q = deferredQuery.trim().toLowerCase();\n    if (!q || !searchableFields?.length) return items;\n    return items.filter((it) =>\n      searchableFields.some((f) =>\n        String(it[f] ?? '')\n          .toLowerCase()\n          .includes(q),\n      ),\n    );\n  }, [items, deferredQuery, searchableFields]);\n\n  const itemsByColumn = React.useMemo(() => {\n    const grouped = new Map<string, T[]>(columns.map((column) => [column.id, []]));\n    for (const item of visible) {\n      grouped.get(getColumn(item))?.push(item);\n    }\n    return grouped;\n  }, [columns, visible, getColumn]);\n\n  const columnIndexOf = React.useCallback(\n    (list: T[], columnId: string, cardId: string) =>\n      list.filter((it) => getColumn(it) === columnId).findIndex((it) => getId(it) === cardId),\n    [getColumn, getId],\n  );\n\n  function handleDragStart(event: DragStartEvent) {\n    draggingRef.current = true;\n    const id = String(event.active.id);\n    const startItem = items.find((it) => getId(it) === id);\n    const startColumn = startItem ? getColumn(startItem) : null;\n    fromColumnRef.current = startColumn;\n    fromIndexRef.current = startColumn == null ? null : columnIndexOf(items, startColumn, id);\n    startItemsRef.current = items;\n    setActiveId(id);\n  }\n\n  // Reorder live so the rendered list always matches the drag preview; the drop\n  // handler only reads the settled order and notifies. This keeps intra- and\n  // cross-column moves consistent (no off-by-one between preview and commit).\n  function handleDragOver(event: DragOverEvent) {\n    const { active, over } = event;\n    if (!over) return;\n    const activeCardId = String(active.id);\n    const overId = String(over.id);\n    if (activeCardId === overId) return;\n\n    setItems((prev) => {\n      const activeIndex = prev.findIndex((it) => getId(it) === activeCardId);\n      if (activeIndex < 0) return prev;\n\n      const overIsColumn = columns.some((c) => c.id === overId);\n      const overItem = overIsColumn ? undefined : prev.find((it) => getId(it) === overId);\n      const overColumn = overIsColumn ? overId : overItem ? getColumn(overItem) : null;\n      if (overColumn == null) return prev;\n\n      const columnChanged = getColumn(prev[activeIndex]) !== overColumn;\n      const updated = columnChanged\n        ? prev.map((it, i) => (i === activeIndex ? ({ ...it, [columnField]: overColumn } as T) : it))\n        : prev;\n      const activeIndexNow = columnChanged ? updated.findIndex((it) => getId(it) === activeCardId) : activeIndex;\n\n      let overIndex = activeIndexNow;\n      if (overIsColumn) {\n        for (let i = 0; i < updated.length; i++) {\n          if (getId(updated[i]) !== activeCardId && getColumn(updated[i]) === overColumn) overIndex = i;\n        }\n      } else {\n        overIndex = updated.findIndex((it) => getId(it) === overId);\n      }\n\n      if (overIndex < 0 || (!columnChanged && overIndex === activeIndexNow)) return updated;\n      return arrayMove(updated, activeIndexNow, overIndex);\n    });\n  }\n\n  function handleDragEnd(event: DragEndEvent) {\n    draggingRef.current = false;\n    setActiveId(null);\n    const fromColumn = fromColumnRef.current;\n    const fromIndex = fromIndexRef.current;\n    fromColumnRef.current = null;\n    fromIndexRef.current = null;\n    if (fromColumn == null) return;\n\n    const activeCardId = String(event.active.id);\n    const movedItem = items.find((it) => getId(it) === activeCardId);\n    if (!movedItem) return;\n\n    const toColumn = getColumn(movedItem);\n    const toIndex = columnIndexOf(items, toColumn, activeCardId);\n    startItemsRef.current = null;\n    if (toColumn === fromColumn && toIndex === fromIndex) return;\n\n    onDataChange?.(items);\n    onCardMove?.({ item: movedItem, fromColumn, toColumn, toIndex });\n  }\n\n  function handleDragCancel() {\n    draggingRef.current = false;\n    const snapshot = startItemsRef.current;\n    startItemsRef.current = null;\n    fromColumnRef.current = null;\n    fromIndexRef.current = null;\n    setActiveId(null);\n    if (snapshot) setItems(snapshot);\n  }\n\n  const activeItem = activeId ? (items.find((it) => getId(it) === activeId) ?? null) : null;\n\n  const renderTitle = React.useCallback((item: T): React.ReactNode => (title ? title(item) : null), [title]);\n\n  const renderBody = React.useCallback(\n    (item: T): React.ReactNode => {\n      if (cardContent) return cardContent(item);\n      if (!fields?.length) return null;\n      return (\n        <div className='flex flex-wrap gap-x-2 gap-y-1 text-xs text-muted-foreground'>\n          {fields.map((field) => {\n            const value = item[field.key];\n            return (\n              <span key={String(field.key)} className='whitespace-nowrap'>\n                {field.label ? <span className='font-medium'>{field.label}: </span> : null}\n                {field.render ? field.render(value, item) : String(value ?? '')}\n              </span>\n            );\n          })}\n        </div>\n      );\n    },\n    [cardContent, fields],\n  );\n\n  return (\n    <div className={cn('flex flex-col gap-4', className)}>\n      {searchableFields?.length ? (\n        <div className='relative w-full max-w-xs'>\n          <Search className='absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground' />\n          <Input\n            value={query}\n            onChange={(e) => setQuery(e.target.value)}\n            placeholder={searchPlaceholder}\n            className='pl-8'\n          />\n        </div>\n      ) : null}\n\n      <DndContext\n        sensors={sensors}\n        collisionDetection={closestCorners}\n        onDragStart={handleDragStart}\n        onDragOver={handleDragOver}\n        onDragEnd={handleDragEnd}\n        onDragCancel={handleDragCancel}\n      >\n        <div className='flex flex-row gap-4 overflow-x-auto pb-2'>\n          {columns.map((column) => {\n            const columnItems = itemsByColumn.get(column.id) ?? [];\n            return (\n              <KanbanColumnView\n                key={column.id}\n                column={column}\n                count={columnItems.length}\n                summary={renderColumnSummary?.(columnItems, column)}\n                onAdd={onCardAdd ? () => onCardAdd(column.id) : undefined}\n              >\n                <SortableContext items={columnItems.map((it) => getId(it))} strategy={verticalListSortingStrategy}>\n                  {columnItems.length === 0 ? (\n                    <p className='rounded-md border border-dashed p-3 text-center text-xs text-muted-foreground'>\n                      No items\n                    </p>\n                  ) : (\n                    columnItems.map((item) => (\n                      <SortableCard\n                        key={getId(item)}\n                        id={getId(item)}\n                        columnId={column.id}\n                        title={renderTitle(item)}\n                        body={renderBody(item)}\n                        onClick={onCardClick ? () => onCardClick(item) : undefined}\n                      />\n                    ))\n                  )}\n                </SortableContext>\n              </KanbanColumnView>\n            );\n          })}\n        </div>\n\n        <DragOverlay>\n          {activeItem ? (\n            <KanbanCardFace\n              title={renderTitle(activeItem)}\n              body={renderBody(activeItem)}\n              className='rotate-3 shadow-lg'\n              handle={<GripVertical className='mt-0.5 size-4 shrink-0 text-muted-foreground' />}\n            />\n          ) : null}\n        </DragOverlay>\n      </DndContext>\n    </div>\n  );\n}\n\nfunction KanbanColumnView({\n  column,\n  count,\n  summary,\n  onAdd,\n  children,\n}: {\n  column: KanbanColumn;\n  count: number;\n  summary?: React.ReactNode;\n  onAdd?: () => void;\n  children: React.ReactNode;\n}) {\n  const { setNodeRef, isOver } = useDroppable({ id: column.id });\n  return (\n    <div className='flex w-72 shrink-0 flex-col gap-3'>\n      <div className='flex items-center gap-2'>\n        {column.color ? <span className='size-2 rounded-full' style={{ backgroundColor: column.color }} /> : null}\n        <span className='text-sm font-medium'>{column.label}</span>\n        <span className='rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground'>{count}</span>\n        {summary != null ? <span className='ml-auto text-xs text-muted-foreground'>{summary}</span> : null}\n        {onAdd ? (\n          <Button variant='ghost' size='icon' className={cn('size-6', summary == null && 'ml-auto')} onClick={onAdd}>\n            <Plus className='size-4' />\n          </Button>\n        ) : null}\n      </div>\n      <div\n        ref={setNodeRef}\n        className={cn(\n          'flex min-h-24 flex-col gap-2 rounded-lg border border-transparent p-2 transition-colors',\n          isOver && 'border-border bg-muted/50',\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\nfunction SortableCard({\n  id,\n  columnId,\n  title,\n  body,\n  onClick,\n}: {\n  id: string;\n  columnId: string;\n  title?: React.ReactNode;\n  body?: React.ReactNode;\n  onClick?: () => void;\n}) {\n  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({\n    id,\n    data: { columnId },\n  });\n  const style: React.CSSProperties = {\n    transform: CSS.Transform.toString(transform),\n    transition,\n  };\n  return (\n    <div ref={setNodeRef} style={style} className={cn(isDragging && 'opacity-50')}>\n      <KanbanCardFace\n        title={title}\n        body={body}\n        onClick={onClick}\n        handle={\n          <button\n            type='button'\n            className='mt-0.5 cursor-grab touch-none text-muted-foreground active:cursor-grabbing'\n            onClick={(e) => e.stopPropagation()}\n            {...attributes}\n            {...listeners}\n          >\n            <GripVertical className='size-4' />\n          </button>\n        }\n      />\n    </div>\n  );\n}\n\nfunction KanbanCardFace({\n  title,\n  body,\n  onClick,\n  handle,\n  className,\n}: {\n  title?: React.ReactNode;\n  body?: React.ReactNode;\n  onClick?: () => void;\n  handle?: React.ReactNode;\n  className?: string;\n}) {\n  const hasBody = body != null;\n  return (\n    <Card className={cn('gap-0 py-0', onClick && 'cursor-pointer', className)} onClick={onClick}>\n      <CardHeader className={cn('flex flex-row items-start gap-2 space-y-0 px-3 pt-3', hasBody ? 'pb-0' : 'pb-3')}>\n        {handle}\n        {title != null ? (\n          <CardTitle className='flex-1 text-sm leading-snug'>{title}</CardTitle>\n        ) : (\n          <span className='flex-1' />\n        )}\n      </CardHeader>\n      {hasBody ? <CardContent className='px-3 pb-3 pl-9 pt-1'>{body}</CardContent> : null}\n    </Card>\n  );\n}\n"
    }
  ],
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
