{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"kanban","type":"registry:ui","title":"kanban","description":"A kanban board is a visual tool that helps you manage and visualize your work. It is a board with columns, and each column represents a status, e.g. \"Backlog\", \"In Progress\", \"Done\".","author":"Hayden Bleasel <hello@haydenbleasel.com>","dependencies":["@dnd-kit/core","@dnd-kit/sortable","@dnd-kit/utilities","tunnel-rat"],"devDependencies":[],"registryDependencies":["card","scroll-area"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport type {\n  Announcements,\n  DndContextProps,\n  DragEndEvent,\n  DragOverEvent,\n  DragStartEvent,\n} from \"@dnd-kit/core\";\nimport {\n  closestCenter,\n  DndContext,\n  DragOverlay,\n  KeyboardSensor,\n  MouseSensor,\n  TouchSensor,\n  useDroppable,\n  useSensor,\n  useSensors,\n} from \"@dnd-kit/core\";\nimport { arrayMove, SortableContext, useSortable } from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport {\n  createContext,\n  type HTMLAttributes,\n  type ReactNode,\n  useContext,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport tunnel from \"tunnel-rat\";\nimport { Card } from \"@/components/ui/card\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/utils\";\n\nconst t = tunnel();\n\nexport type { DragEndEvent } from \"@dnd-kit/core\";\n\ntype KanbanItemProps = {\n  id: string;\n  name: string;\n  column: string;\n} & Record<string, unknown>;\n\ntype KanbanColumnProps = {\n  id: string;\n  name: string;\n} & Record<string, unknown>;\n\ntype KanbanContextProps<\n  T extends KanbanItemProps = KanbanItemProps,\n  C extends KanbanColumnProps = KanbanColumnProps,\n> = {\n  columns: C[];\n  data: T[];\n  activeCardId: string | null;\n};\n\nconst KanbanContext = createContext<KanbanContextProps>({\n  columns: [],\n  data: [],\n  activeCardId: null,\n});\n\nexport type KanbanBoardProps = {\n  id: string;\n  children: ReactNode;\n  className?: string;\n};\n\nexport const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => {\n  const { isOver, setNodeRef } = useDroppable({\n    id,\n  });\n\n  return (\n    <div\n      className={cn(\n        \"flex size-full min-h-40 flex-col divide-y overflow-hidden rounded-md border bg-secondary text-xs shadow-sm ring-2 transition-all\",\n        isOver ? \"ring-primary\" : \"ring-transparent\",\n        className\n      )}\n      ref={setNodeRef}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport type KanbanCardProps<T extends KanbanItemProps = KanbanItemProps> = T & {\n  children?: ReactNode;\n  className?: string;\n};\n\nexport const KanbanCard = <T extends KanbanItemProps = KanbanItemProps>({\n  id,\n  name,\n  children,\n  className,\n}: KanbanCardProps<T>) => {\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    transition,\n    transform,\n    isDragging,\n  } = useSortable({\n    id,\n  });\n  const { activeCardId } = useContext(KanbanContext) as KanbanContextProps;\n\n  const style = {\n    transition,\n    transform: CSS.Transform.toString(transform),\n  };\n\n  return (\n    <>\n      <div style={style} {...listeners} {...attributes} ref={setNodeRef}>\n        <Card\n          className={cn(\n            \"cursor-grab gap-4 rounded-md p-3 shadow-sm\",\n            isDragging && \"pointer-events-none cursor-grabbing opacity-30\",\n            className\n          )}\n        >\n          {children ?? <p className=\"m-0 font-medium text-sm\">{name}</p>}\n        </Card>\n      </div>\n      {activeCardId === id && (\n        <t.In>\n          <Card\n            className={cn(\n              \"cursor-grab gap-4 rounded-md p-3 shadow-sm ring-2 ring-primary\",\n              isDragging && \"cursor-grabbing\",\n              className\n            )}\n          >\n            {children ?? <p className=\"m-0 font-medium text-sm\">{name}</p>}\n          </Card>\n        </t.In>\n      )}\n    </>\n  );\n};\n\nexport type KanbanCardsProps<T extends KanbanItemProps = KanbanItemProps> =\n  Omit<HTMLAttributes<HTMLDivElement>, \"children\" | \"id\"> & {\n    children: (item: T) => ReactNode;\n    id: string;\n  };\n\nexport const KanbanCards = <T extends KanbanItemProps = KanbanItemProps>({\n  children,\n  className,\n  ...props\n}: KanbanCardsProps<T>) => {\n  const { data } = useContext(KanbanContext) as KanbanContextProps<T>;\n  const filteredData = data.filter((item) => item.column === props.id);\n  const items = filteredData.map((item) => item.id);\n\n  return (\n    <ScrollArea className=\"overflow-hidden\">\n      <SortableContext items={items}>\n        <div\n          className={cn(\"flex flex-grow flex-col gap-2 p-2\", className)}\n          {...props}\n        >\n          {filteredData.map(children)}\n        </div>\n      </SortableContext>\n      <ScrollBar orientation=\"vertical\" />\n    </ScrollArea>\n  );\n};\n\nexport type KanbanHeaderProps = HTMLAttributes<HTMLDivElement>;\n\nexport const KanbanHeader = ({ className, ...props }: KanbanHeaderProps) => (\n  <div className={cn(\"m-0 p-2 font-semibold text-sm\", className)} {...props} />\n);\n\nexport type KanbanProviderProps<\n  T extends KanbanItemProps = KanbanItemProps,\n  C extends KanbanColumnProps = KanbanColumnProps,\n> = Omit<DndContextProps, \"children\"> & {\n  children: (column: C) => ReactNode;\n  className?: string;\n  columns: C[];\n  data: T[];\n  onDataChange?: (data: T[]) => void;\n  onDragStart?: (event: DragStartEvent) => void;\n  onDragEnd?: (event: DragEndEvent) => void;\n  onDragOver?: (event: DragOverEvent) => void;\n};\n\nexport const KanbanProvider = <\n  T extends KanbanItemProps = KanbanItemProps,\n  C extends KanbanColumnProps = KanbanColumnProps,\n>({\n  children,\n  onDragStart,\n  onDragEnd,\n  onDragOver,\n  className,\n  columns,\n  data,\n  onDataChange,\n  ...props\n}: KanbanProviderProps<T, C>) => {\n  const [activeCardId, setActiveCardId] = useState<string | null>(null);\n\n  const sensors = useSensors(\n    useSensor(MouseSensor),\n    useSensor(TouchSensor),\n    useSensor(KeyboardSensor)\n  );\n\n  const handleDragStart = (event: DragStartEvent) => {\n    const card = data.find((item) => item.id === event.active.id);\n    if (card) {\n      setActiveCardId(event.active.id as string);\n    }\n    onDragStart?.(event);\n  };\n\n  const handleDragOver = (event: DragOverEvent) => {\n    const { active, over } = event;\n\n    if (!over) {\n      return;\n    }\n\n    const activeItem = data.find((item) => item.id === active.id);\n    const overItem = data.find((item) => item.id === over.id);\n\n    if (!activeItem) {\n      return;\n    }\n\n    const activeColumn = activeItem.column;\n    const overColumn =\n      overItem?.column ||\n      columns.find((col) => col.id === over.id)?.id ||\n      columns[0]?.id;\n\n    if (activeColumn !== overColumn) {\n      let newData = [...data];\n      const activeIndex = newData.findIndex((item) => item.id === active.id);\n      const overIndex = newData.findIndex((item) => item.id === over.id);\n\n      newData[activeIndex].column = overColumn;\n      newData = arrayMove(newData, activeIndex, overIndex);\n\n      onDataChange?.(newData);\n    }\n\n    onDragOver?.(event);\n  };\n\n  const handleDragEnd = (event: DragEndEvent) => {\n    setActiveCardId(null);\n\n    onDragEnd?.(event);\n\n    const { active, over } = event;\n\n    if (!over || active.id === over.id) {\n      return;\n    }\n\n    let newData = [...data];\n\n    const oldIndex = newData.findIndex((item) => item.id === active.id);\n    const newIndex = newData.findIndex((item) => item.id === over.id);\n\n    newData = arrayMove(newData, oldIndex, newIndex);\n\n    onDataChange?.(newData);\n  };\n\n  const announcements: Announcements = {\n    onDragStart({ active }) {\n      const { name, column } = data.find((item) => item.id === active.id) ?? {};\n\n      return `Picked up the card \"${name}\" from the \"${column}\" column`;\n    },\n    onDragOver({ active, over }) {\n      const { name } = data.find((item) => item.id === active.id) ?? {};\n      const newColumn = columns.find((column) => column.id === over?.id)?.name;\n\n      return `Dragged the card \"${name}\" over the \"${newColumn}\" column`;\n    },\n    onDragEnd({ active, over }) {\n      const { name } = data.find((item) => item.id === active.id) ?? {};\n      const newColumn = columns.find((column) => column.id === over?.id)?.name;\n\n      return `Dropped the card \"${name}\" into the \"${newColumn}\" column`;\n    },\n    onDragCancel({ active }) {\n      const { name } = data.find((item) => item.id === active.id) ?? {};\n\n      return `Cancelled dragging the card \"${name}\"`;\n    },\n  };\n\n  return (\n    <KanbanContext.Provider value={{ columns, data, activeCardId }}>\n      <DndContext\n        accessibility={{ announcements }}\n        collisionDetection={closestCenter}\n        onDragEnd={handleDragEnd}\n        onDragOver={handleDragOver}\n        onDragStart={handleDragStart}\n        sensors={sensors}\n        {...props}\n      >\n        <div\n          className={cn(\n            \"grid size-full auto-cols-fr grid-flow-col gap-4\",\n            className\n          )}\n        >\n          {columns.map((column) => children(column))}\n        </div>\n        {typeof window !== \"undefined\" &&\n          createPortal(\n            <DragOverlay>\n              <t.Out />\n            </DragOverlay>,\n            document.body\n          )}\n      </DndContext>\n    </KanbanContext.Provider>\n  );\n};\n","target":"components/kibo-ui/kanban/index.tsx"}],"css":{}}