{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kanban",
  "title": "Kanban",
  "description": "A composable drag-and-drop kanban board with sortable columns, sortable cards, handles, overlays, and move callbacks.",
  "dependencies": [
    "@dnd-kit/core",
    "@dnd-kit/sortable",
    "@dnd-kit/utilities",
    "@radix-ui/react-slot"
  ],
  "registryDependencies": [
    "tabs"
  ],
  "files": [
    {
      "path": "registry/corr/components/kanban.tsx",
      "content": "import * as React from \"react\"\nimport {\n  defaultDropAnimationSideEffects,\n  DndContext,\n  DragOverlay,\n  KeyboardSensor,\n  MeasuringStrategy,\n  MouseSensor,\n  TouchSensor,\n  useSensor,\n  useSensors,\n  type DraggableAttributes,\n  type DraggableSyntheticListeners,\n  type DragEndEvent,\n  type DragOverEvent,\n  type DragStartEvent,\n  type DropAnimation,\n  type Modifiers,\n  type UniqueIdentifier,\n} from \"@dnd-kit/core\"\nimport {\n  arrayMove,\n  defaultAnimateLayoutChanges,\n  rectSortingStrategy,\n  SortableContext,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n  type AnimateLayoutChanges,\n} from \"@dnd-kit/sortable\"\nimport { CSS } from \"@dnd-kit/utilities\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { createPortal } from \"react-dom\"\n\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { cn } from \"@/lib/utils\"\n\ntype Columns<T> = Record<string, T[]>\n\ninterface KanbanContextProps<T> {\n  columns: Columns<T>\n  setColumns: (columns: Columns<T>) => void\n  getItemId: (item: T) => string\n  columnIds: string[]\n  activeId: UniqueIdentifier | null\n  setActiveId: (id: UniqueIdentifier | null) => void\n  findContainer: (id: UniqueIdentifier) => string | undefined\n  isColumn: (id: UniqueIdentifier) => boolean\n  modifiers?: Modifiers\n}\n\nconst KanbanContext = React.createContext<KanbanContextProps<unknown>>({\n  columns: {},\n  setColumns: () => undefined,\n  getItemId: () => \"\",\n  columnIds: [],\n  activeId: null,\n  setActiveId: () => undefined,\n  findContainer: () => undefined,\n  isColumn: () => false,\n  modifiers: undefined,\n})\n\nconst ColumnContext = React.createContext<{\n  attributes: DraggableAttributes\n  listeners: DraggableSyntheticListeners | undefined\n  isDragging?: boolean\n  disabled?: boolean\n}>({\n  attributes: {} as DraggableAttributes,\n  listeners: undefined,\n  isDragging: false,\n  disabled: false,\n})\n\nconst ItemContext = React.createContext<{\n  listeners: DraggableSyntheticListeners | undefined\n  isDragging?: boolean\n  disabled?: boolean\n}>({\n  listeners: undefined,\n  isDragging: false,\n  disabled: false,\n})\n\nconst IsOverlayContext = React.createContext(false)\n\nconst animateLayoutChanges: AnimateLayoutChanges = (args) =>\n  defaultAnimateLayoutChanges({ ...args, wasDragging: true })\n\nconst dropAnimationConfig: DropAnimation = {\n  sideEffects: defaultDropAnimationSideEffects({\n    styles: {\n      active: {\n        opacity: \"0.4\",\n      },\n    },\n  }),\n}\n\nfunction useMediaQuery(query: string) {\n  const [matches, setMatches] = React.useState(false)\n\n  React.useEffect(() => {\n    const media = window.matchMedia(query)\n    const sync = () => setMatches(media.matches)\n\n    sync()\n    media.addEventListener(\"change\", sync)\n\n    return () => media.removeEventListener(\"change\", sync)\n  }, [query])\n\n  return matches\n}\n\nexport interface KanbanMoveEvent {\n  event: DragEndEvent\n  activeContainer: string\n  activeIndex: number\n  overContainer: string\n  overIndex: number\n}\n\nexport interface KanbanRootProps<\n  T,\n> extends React.HTMLAttributes<HTMLDivElement> {\n  value: Columns<T>\n  onValueChange: (value: Columns<T>) => void\n  getItemValue: (item: T) => string\n  children: React.ReactNode\n  onMove?: (event: KanbanMoveEvent) => void\n  asChild?: boolean\n  modifiers?: Modifiers\n}\n\nexport function Kanban<T>({\n  value,\n  onValueChange,\n  getItemValue,\n  children,\n  className,\n  asChild = false,\n  onMove,\n  modifiers,\n  ...props\n}: KanbanRootProps<T>) {\n  const columns = value\n  const setColumns = onValueChange\n  const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null)\n  const sensors = useSensors(\n    useSensor(MouseSensor, {\n      activationConstraint: {\n        distance: 10,\n      },\n    }),\n    useSensor(TouchSensor, {\n      activationConstraint: {\n        delay: 250,\n        tolerance: 5,\n      },\n    }),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    })\n  )\n  const columnIds = React.useMemo(() => Object.keys(columns), [columns])\n  const isColumn = React.useCallback(\n    (id: UniqueIdentifier) => columnIds.includes(String(id)),\n    [columnIds]\n  )\n  const findContainer = React.useCallback(\n    (id: UniqueIdentifier) => {\n      if (isColumn(id)) return String(id)\n\n      return columnIds.find((key) =>\n        columns[key].some((item) => getItemValue(item) === id)\n      )\n    },\n    [columnIds, columns, getItemValue, isColumn]\n  )\n\n  const handleDragStart = React.useCallback((event: DragStartEvent) => {\n    setActiveId(event.active.id)\n  }, [])\n\n  const handleDragOver = React.useCallback(\n    (event: DragOverEvent) => {\n      const { active, over } = event\n      if (!over || isColumn(active.id)) return\n\n      const activeContainer = findContainer(active.id)\n      const overContainer = findContainer(over.id)\n\n      if (!activeContainer || !overContainer) return\n\n      if (activeContainer !== overContainer) {\n        const activeItems = columns[activeContainer]\n        const overItems = columns[overContainer]\n        const activeIndex = activeItems.findIndex(\n          (item) => getItemValue(item) === active.id\n        )\n        let overIndex = overItems.findIndex(\n          (item) => getItemValue(item) === over.id\n        )\n\n        if (isColumn(over.id)) {\n          overIndex = overItems.length\n        }\n\n        const newActiveItems = [...activeItems]\n        const newOverItems = [...overItems]\n        const [movedItem] = newActiveItems.splice(activeIndex, 1)\n        newOverItems.splice(overIndex, 0, movedItem)\n\n        setColumns({\n          ...columns,\n          [activeContainer]: newActiveItems,\n          [overContainer]: newOverItems,\n        })\n      } else {\n        const container = activeContainer\n        const activeIndex = columns[container].findIndex(\n          (item) => getItemValue(item) === active.id\n        )\n        const overIndex = columns[container].findIndex(\n          (item) => getItemValue(item) === over.id\n        )\n\n        if (activeIndex !== overIndex) {\n          setColumns({\n            ...columns,\n            [container]: arrayMove(columns[container], activeIndex, overIndex),\n          })\n        }\n      }\n    },\n    [columns, findContainer, getItemValue, isColumn, setColumns]\n  )\n\n  const handleDragCancel = React.useCallback(() => {\n    setActiveId(null)\n  }, [])\n\n  const handleDragEnd = React.useCallback(\n    (event: DragEndEvent) => {\n      const { active, over } = event\n      setActiveId(null)\n\n      if (!over) return\n\n      if (onMove && !isColumn(active.id)) {\n        const activeSortable = active.data.current?.sortable as\n          | { containerId?: UniqueIdentifier; index?: number }\n          | undefined\n        const overSortable = over.data.current?.sortable as\n          | { containerId?: UniqueIdentifier; index?: number }\n          | undefined\n        const activeContainer =\n          (activeSortable?.containerId\n            ? String(activeSortable.containerId)\n            : undefined) ?? findContainer(active.id)\n        const overContainer =\n          (overSortable?.containerId\n            ? String(overSortable.containerId)\n            : undefined) ??\n          (isColumn(over.id) ? String(over.id) : findContainer(over.id))\n\n        if (activeContainer && overContainer) {\n          const activeIndex =\n            typeof activeSortable?.index === \"number\"\n              ? activeSortable.index\n              : columns[activeContainer].findIndex(\n                  (item) => getItemValue(item) === active.id\n                )\n          const overIndex = isColumn(over.id)\n            ? columns[overContainer].length\n            : typeof overSortable?.index === \"number\"\n              ? overSortable.index\n              : columns[overContainer].findIndex(\n                  (item) => getItemValue(item) === over.id\n                )\n\n          onMove({\n            event,\n            activeContainer,\n            activeIndex,\n            overContainer,\n            overIndex,\n          })\n        }\n\n        return\n      }\n\n      if (isColumn(active.id) && isColumn(over.id)) {\n        const activeIndex = columnIds.indexOf(String(active.id))\n        const overIndex = columnIds.indexOf(String(over.id))\n\n        if (activeIndex !== overIndex) {\n          const newOrder = arrayMove(\n            Object.keys(columns),\n            activeIndex,\n            overIndex\n          )\n          const newColumns: Columns<T> = {}\n          newOrder.forEach((key) => {\n            newColumns[key] = columns[key]\n          })\n          setColumns(newColumns)\n        }\n\n        return\n      }\n\n      const activeContainer = findContainer(active.id)\n      const overContainer = findContainer(over.id)\n\n      if (\n        activeContainer &&\n        overContainer &&\n        activeContainer === overContainer\n      ) {\n        const container = activeContainer\n        const activeIndex = columns[container].findIndex(\n          (item) => getItemValue(item) === active.id\n        )\n        const overIndex = columns[container].findIndex(\n          (item) => getItemValue(item) === over.id\n        )\n\n        if (activeIndex !== overIndex) {\n          setColumns({\n            ...columns,\n            [container]: arrayMove(columns[container], activeIndex, overIndex),\n          })\n        }\n      }\n    },\n    [\n      columnIds,\n      columns,\n      findContainer,\n      getItemValue,\n      isColumn,\n      onMove,\n      setColumns,\n    ]\n  )\n\n  const contextValue = React.useMemo(\n    () =>\n      ({\n        columns,\n        setColumns,\n        getItemId: getItemValue,\n        columnIds,\n        activeId,\n        setActiveId,\n        findContainer,\n        isColumn,\n        modifiers,\n      }) satisfies KanbanContextProps<T>,\n    [\n      activeId,\n      columnIds,\n      columns,\n      findContainer,\n      getItemValue,\n      isColumn,\n      modifiers,\n      setColumns,\n    ]\n  )\n  const Comp = asChild ? Slot : \"div\"\n\n  return (\n    <KanbanContext.Provider value={contextValue as KanbanContextProps<unknown>}>\n      <DndContext\n        sensors={sensors}\n        modifiers={modifiers}\n        measuring={{\n          droppable: {\n            strategy: MeasuringStrategy.Always,\n          },\n        }}\n        onDragStart={handleDragStart}\n        onDragOver={handleDragOver}\n        onDragEnd={handleDragEnd}\n        onDragCancel={handleDragCancel}\n      >\n        <Comp\n          data-slot=\"kanban\"\n          data-dragging={activeId !== null}\n          className={cn(activeId !== null && \"cursor-grabbing!\", className)}\n          {...props}\n        >\n          {children}\n        </Comp>\n      </DndContext>\n    </KanbanContext.Provider>\n  )\n}\n\nexport interface KanbanBoardProps extends React.HTMLAttributes<HTMLDivElement> {\n  asChild?: boolean\n  mobileMode?: \"auto\" | \"tabs\" | \"stack\"\n  getColumnLabel?: (columnId: string) => React.ReactNode\n}\n\nexport function KanbanBoard({\n  className,\n  asChild = false,\n  mobileMode = \"auto\",\n  getColumnLabel,\n  children,\n  ...props\n}: KanbanBoardProps) {\n  const { columnIds } = React.useContext(KanbanContext)\n  const Comp = asChild ? Slot : \"div\"\n  const childArray = React.Children.toArray(children)\n  const isSmallScreen = useMediaQuery(\"(max-width: 639px)\")\n  const [activeColumn, setActiveColumn] = React.useState(\n    () => columnIds[0] ?? \"\"\n  )\n  const selectedColumn = columnIds.includes(activeColumn)\n    ? activeColumn\n    : columnIds[0]\n\n  React.useEffect(() => {\n    if (!columnIds.includes(activeColumn)) {\n      setActiveColumn(columnIds[0] ?? \"\")\n    }\n  }, [activeColumn, columnIds])\n\n  const showTabs =\n    mobileMode === \"tabs\" || (mobileMode === \"auto\" && isSmallScreen)\n  const showStack = mobileMode === \"stack\"\n  const showDesktopBoard =\n    showStack || (mobileMode === \"auto\" && !isSmallScreen)\n  const tabs = showTabs ? (\n    <Tabs\n      value={selectedColumn}\n      onValueChange={setActiveColumn}\n      className=\"mb-3\"\n    >\n      <TabsList className=\"w-full justify-start overflow-x-auto overflow-y-hidden\">\n        {columnIds.map((columnId) => (\n          <TabsTrigger\n            key={columnId}\n            value={columnId}\n            className=\"shrink-0\"\n          >\n            {getColumnLabel?.(columnId) ?? columnId}\n          </TabsTrigger>\n        ))}\n      </TabsList>\n    </Tabs>\n  ) : null\n\n  const selectedChildren = childArray.filter((child) => {\n    if (!React.isValidElement<{ value?: string; id?: string }>(child)) {\n      return true\n    }\n\n    const childColumnId = child.props.value ?? child.props.id\n    return childColumnId === selectedColumn\n  })\n\n  return (\n    <SortableContext items={columnIds} strategy={rectSortingStrategy}>\n      <>\n        {tabs}\n        {showDesktopBoard ? (\n          <Comp\n            data-slot=\"kanban-board\"\n            className={cn(\n              showStack\n                ? \"grid auto-rows-fr gap-4 sm:grid-cols-3\"\n                : \"grid auto-rows-fr gap-4 sm:grid-cols-3\",\n              className\n            )}\n            {...props}\n          >\n            {children}\n          </Comp>\n        ) : null}\n        {showTabs ? (\n          <div\n            data-slot=\"kanban-board-mobile\"\n            className=\"grid gap-4\"\n          >\n            {selectedChildren}\n          </div>\n        ) : null}\n      </>\n    </SortableContext>\n  )\n}\n\nexport interface KanbanColumnProps extends React.HTMLAttributes<HTMLDivElement> {\n  value: string\n  disabled?: boolean\n  asChild?: boolean\n}\n\nexport function KanbanColumn({\n  value,\n  className,\n  asChild = false,\n  disabled,\n  children,\n  ...props\n}: KanbanColumnProps) {\n  const isOverlay = React.useContext(IsOverlayContext)\n  const {\n    setNodeRef,\n    transform,\n    transition,\n    attributes,\n    listeners,\n    isDragging: isSortableDragging,\n  } = useSortable({\n    id: value,\n    disabled: disabled || isOverlay,\n    animateLayoutChanges,\n  })\n  const { activeId, isColumn } = React.useContext(KanbanContext)\n  const isColumnDragging = activeId ? isColumn(activeId) : false\n  const style = {\n    transition,\n    transform: CSS.Transform.toString(transform),\n  } as React.CSSProperties\n  const Comp = asChild ? Slot : \"div\"\n\n  if (isOverlay) {\n    return (\n      <ColumnContext.Provider\n        value={{\n          attributes: {} as DraggableAttributes,\n          listeners: undefined,\n          isDragging: true,\n          disabled: false,\n        }}\n      >\n        <Comp\n          data-slot=\"kanban-column\"\n          data-value={value}\n          data-dragging={true}\n          className={cn(\"group/kanban-column flex flex-col\", className)}\n          {...props}\n        >\n          {children}\n        </Comp>\n      </ColumnContext.Provider>\n    )\n  }\n\n  return (\n    <ColumnContext.Provider\n      value={{ attributes, listeners, isDragging: isColumnDragging, disabled }}\n    >\n      <Comp\n        data-slot=\"kanban-column\"\n        data-value={value}\n        data-dragging={isSortableDragging}\n        data-disabled={disabled}\n        ref={setNodeRef}\n        style={style}\n        className={cn(\n          \"group/kanban-column flex flex-col\",\n          isSortableDragging && \"z-50 opacity-50\",\n          disabled && \"opacity-50\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </Comp>\n    </ColumnContext.Provider>\n  )\n}\n\nexport interface KanbanColumnHandleProps extends React.HTMLAttributes<HTMLDivElement> {\n  cursor?: boolean\n  asChild?: boolean\n}\n\nexport function KanbanColumnHandle({\n  className,\n  asChild = false,\n  cursor = true,\n  children,\n  ...props\n}: KanbanColumnHandleProps) {\n  const { attributes, listeners, isDragging, disabled } =\n    React.useContext(ColumnContext)\n  const Comp = asChild ? Slot : \"div\"\n\n  return (\n    <Comp\n      data-slot=\"kanban-column-handle\"\n      data-dragging={isDragging}\n      data-disabled={disabled}\n      {...attributes}\n      {...listeners}\n      className={cn(\n        \"opacity-0 transition-opacity group-hover/kanban-column:opacity-100\",\n        cursor && (isDragging ? \"cursor-grabbing!\" : \"cursor-grab!\"),\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </Comp>\n  )\n}\n\nexport interface KanbanItemProps extends React.HTMLAttributes<HTMLDivElement> {\n  value: string\n  disabled?: boolean\n  asChild?: boolean\n}\n\nexport function KanbanItem({\n  value,\n  className,\n  asChild = false,\n  disabled,\n  children,\n  ...props\n}: KanbanItemProps) {\n  const isOverlay = React.useContext(IsOverlayContext)\n  const {\n    setNodeRef,\n    transform,\n    transition,\n    attributes,\n    listeners,\n    isDragging: isSortableDragging,\n  } = useSortable({\n    id: value,\n    disabled: disabled || isOverlay,\n    animateLayoutChanges,\n  })\n  const { activeId, isColumn } = React.useContext(KanbanContext)\n  const isItemDragging = activeId ? !isColumn(activeId) : false\n  const style = {\n    transition,\n    transform: CSS.Transform.toString(transform),\n  } as React.CSSProperties\n  const Comp = asChild ? Slot : \"div\"\n\n  if (isOverlay) {\n    return (\n      <ItemContext.Provider\n        value={{ listeners: undefined, isDragging: true, disabled: false }}\n      >\n        <Comp\n          data-slot=\"kanban-item\"\n          data-value={value}\n          data-dragging={true}\n          className={cn(className)}\n          {...props}\n        >\n          {children}\n        </Comp>\n      </ItemContext.Provider>\n    )\n  }\n\n  return (\n    <ItemContext.Provider\n      value={{ listeners, isDragging: isItemDragging, disabled }}\n    >\n      <Comp\n        data-slot=\"kanban-item\"\n        data-value={value}\n        data-dragging={isSortableDragging}\n        data-disabled={disabled}\n        ref={setNodeRef}\n        style={style}\n        {...attributes}\n        className={cn(\n          isSortableDragging && \"z-50 opacity-50\",\n          disabled && \"opacity-50\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </Comp>\n    </ItemContext.Provider>\n  )\n}\n\nexport interface KanbanItemHandleProps extends React.HTMLAttributes<HTMLDivElement> {\n  cursor?: boolean\n  asChild?: boolean\n}\n\nexport function KanbanItemHandle({\n  className,\n  asChild = false,\n  cursor = true,\n  children,\n  ...props\n}: KanbanItemHandleProps) {\n  const { listeners, isDragging, disabled } = React.useContext(ItemContext)\n  const Comp = asChild ? Slot : \"div\"\n\n  return (\n    <Comp\n      data-slot=\"kanban-item-handle\"\n      data-dragging={isDragging}\n      data-disabled={disabled}\n      {...listeners}\n      className={cn(\n        cursor && (isDragging ? \"cursor-grabbing!\" : \"cursor-grab!\"),\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </Comp>\n  )\n}\n\nexport interface KanbanColumnContentProps extends React.HTMLAttributes<HTMLDivElement> {\n  value: string\n  asChild?: boolean\n}\n\nexport function KanbanColumnContent({\n  value,\n  className,\n  asChild = false,\n  children,\n  ...props\n}: KanbanColumnContentProps) {\n  const { columns, getItemId } = React.useContext(KanbanContext)\n  const itemIds = React.useMemo(\n    () => (columns[value] ?? []).map(getItemId),\n    [columns, getItemId, value]\n  )\n  const Comp = asChild ? Slot : \"div\"\n\n  return (\n    <SortableContext items={itemIds} strategy={verticalListSortingStrategy}>\n      <Comp\n        data-slot=\"kanban-column-content\"\n        className={cn(\"flex flex-col gap-2\", className)}\n        {...props}\n      >\n        {children}\n      </Comp>\n    </SortableContext>\n  )\n}\n\nexport interface KanbanOverlayProps extends Omit<\n  React.ComponentProps<typeof DragOverlay>,\n  \"children\"\n> {\n  children?:\n    | React.ReactNode\n    | ((params: {\n        value: UniqueIdentifier\n        variant: \"column\" | \"item\"\n      }) => React.ReactNode)\n}\n\nexport function KanbanOverlay({\n  children,\n  className,\n  ...props\n}: KanbanOverlayProps) {\n  const { activeId, isColumn, modifiers } = React.useContext(KanbanContext)\n  const [mounted, setMounted] = React.useState(false)\n\n  React.useLayoutEffect(() => setMounted(true), [])\n\n  const variant = activeId ? (isColumn(activeId) ? \"column\" : \"item\") : \"item\"\n  const content =\n    activeId && children\n      ? typeof children === \"function\"\n        ? children({ value: activeId, variant })\n        : children\n      : null\n\n  if (!mounted) return null\n\n  return createPortal(\n    <DragOverlay\n      dropAnimation={dropAnimationConfig}\n      modifiers={modifiers}\n      className={cn(\"z-50\", activeId && \"cursor-grabbing\", className)}\n      {...props}\n    >\n      <IsOverlayContext.Provider value={true}>\n        {content}\n      </IsOverlayContext.Provider>\n    </DragOverlay>,\n    document.body\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}