{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kanban",
  "title": "Kanban",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/kanban/Kanban.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { cn } from '@/lib/utils'\nimport { kanbanCardVariants, kanbanColumnVariants } from './kanban.variants'\nimport { Plus } from 'lucide-react'\n\n/* ------------------------------------------------------------------ */\n/* Contexts                                                           */\n/* ------------------------------------------------------------------ */\n\nexport interface KanbanMoveEvent {\n  cardId: string\n  fromColumnId: string\n  toColumnId: string\n  toIndex?: number\n}\n\ninterface KanbanContextValue {\n  draggingCardId: string | null\n  draggingColumnId: string | null\n  overColumnId: string | null\n  /** Set while a card is held by keyboard (Space), not by pointer drag. */\n  grabbedCardId: string | null\n  setDraggingCard: (cardId: string | null, columnId: string | null) => void\n  setOverColumn: (columnId: string | null) => void\n  setGrabbedCard: (cardId: string | null) => void\n  /** Speak a message through the board's polite live region. */\n  announce: (message: string) => void\n  onCardMove?: (event: KanbanMoveEvent) => void\n}\n\nconst KanbanContext = React.createContext<KanbanContextValue | null>(null)\n\nexport function useKanbanContext() {\n  const ctx = React.useContext(KanbanContext)\n  if (!ctx) {\n    throw new Error('Kanban components must be used within a <Kanban /> provider')\n  }\n  return ctx\n}\n\ninterface KanbanColumnContextValue {\n  columnId: string\n  isOver: boolean\n  /** Column label, used to announce keyboard moves. Falls back to the id. */\n  label?: string\n}\n\nconst KanbanColumnContext = React.createContext<KanbanColumnContextValue | null>(null)\n\nexport function useKanbanColumnContext() {\n  const ctx = React.useContext(KanbanColumnContext)\n  if (!ctx) {\n    throw new Error('Kanban column sub-components must be used within a <KanbanColumn />')\n  }\n  return ctx\n}\n\n/* ------------------------------------------------------------------ */\n/* Kanban (Root Provider)                                             */\n/* ------------------------------------------------------------------ */\n\nexport interface KanbanProps extends React.HTMLAttributes<HTMLDivElement> {\n  onCardMove?: (event: KanbanMoveEvent) => void\n}\n\nexport const Kanban = React.forwardRef<HTMLDivElement, KanbanProps>(\n  ({ className, onCardMove, children, ...props }, ref) => {\n    const [draggingCardId, setDraggingCardId] = React.useState<string | null>(null)\n    const [draggingColumnId, setDraggingColumnId] = React.useState<string | null>(null)\n    const [overColumnId, setOverColumnId] = React.useState<string | null>(null)\n    const [grabbedCardId, setGrabbedCardId] = React.useState<string | null>(null)\n    // Keyboard moves are silent to a screen reader — the card just appears\n    // somewhere else. This region narrates pick up / move / drop / cancel.\n    const [announcement, setAnnouncement] = React.useState('')\n\n    const setDraggingCard = React.useCallback((cardId: string | null, columnId: string | null) => {\n      setDraggingCardId(cardId)\n      setDraggingColumnId(columnId)\n    }, [])\n\n    const setOverColumn = React.useCallback((columnId: string | null) => {\n      setOverColumnId(columnId)\n    }, [])\n\n    const setGrabbedCard = React.useCallback((cardId: string | null) => {\n      setGrabbedCardId(cardId)\n    }, [])\n\n    const announce = React.useCallback((message: string) => {\n      // Re-assigning the same string would not re-trigger the live region.\n      setAnnouncement((current) => (current === message ? `${message} ` : message))\n    }, [])\n\n    const ctx = React.useMemo<KanbanContextValue>(\n      () => ({\n        draggingCardId,\n        draggingColumnId,\n        overColumnId,\n        grabbedCardId,\n        setDraggingCard,\n        setOverColumn,\n        setGrabbedCard,\n        announce,\n        onCardMove,\n      }),\n      [\n        draggingCardId,\n        draggingColumnId,\n        overColumnId,\n        grabbedCardId,\n        setDraggingCard,\n        setOverColumn,\n        setGrabbedCard,\n        announce,\n        onCardMove,\n      ],\n    )\n\n    return (\n      <KanbanContext.Provider value={ctx}>\n        <div ref={ref} data-uipkge=\"\" data-slot=\"kanban\" className={cn('w-full', className)} {...props}>\n          {children}\n          <div data-slot=\"kanban-live-region\" className=\"sr-only\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n            {announcement}\n          </div>\n        </div>\n      </KanbanContext.Provider>\n    )\n  },\n)\nKanban.displayName = 'Kanban'\n\n/* ------------------------------------------------------------------ */\n/* KanbanBoard                                                        */\n/* ------------------------------------------------------------------ */\n\nexport const KanbanBoard = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, children, ...props }, ref) => (\n    <div\n      ref={ref}\n      data-slot=\"kanban-board\"\n      className={cn('flex w-full items-start gap-4 overflow-x-auto pb-4', className)}\n      {...props}\n    >\n      {children}\n    </div>\n  ),\n)\nKanbanBoard.displayName = 'KanbanBoard'\n\n/* ------------------------------------------------------------------ */\n/* KanbanColumn                                                       */\n/* ------------------------------------------------------------------ */\n\nexport interface KanbanColumnProps extends React.HTMLAttributes<HTMLDivElement> {\n  id: string\n  /** Accessible name for the column, also used in keyboard move\n   *  announcements (\"moved to In progress\"). Falls back to the id. */\n  label?: string\n}\n\nexport const KanbanColumn = React.forwardRef<HTMLDivElement, KanbanColumnProps>(\n  ({ id, label, className, children, onDragOver, onDragLeave, onDrop, ...props }, ref) => {\n    const kanban = useKanbanContext()\n    const isOver = kanban.overColumnId === id\n\n    const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {\n      e.preventDefault()\n      e.dataTransfer.dropEffect = 'move'\n      if (kanban.overColumnId !== id) {\n        kanban.setOverColumn(id)\n      }\n      onDragOver?.(e)\n    }\n\n    const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {\n      if (e.currentTarget.contains(e.relatedTarget as Node)) return\n      if (kanban.overColumnId === id) {\n        kanban.setOverColumn(null)\n      }\n      onDragLeave?.(e)\n    }\n\n    const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {\n      e.preventDefault()\n      if (kanban.draggingCardId && kanban.draggingColumnId) {\n        kanban.onCardMove?.({\n          cardId: kanban.draggingCardId,\n          fromColumnId: kanban.draggingColumnId,\n          toColumnId: id,\n        })\n      }\n      kanban.setDraggingCard(null, null)\n      kanban.setOverColumn(null)\n      onDrop?.(e)\n    }\n\n    const columnCtx = React.useMemo<KanbanColumnContextValue>(\n      () => ({ columnId: id, isOver, label }),\n      [id, isOver, label],\n    )\n\n    return (\n      <KanbanColumnContext.Provider value={columnCtx}>\n        <div\n          ref={ref}\n          data-uipkge=\"\"\n          data-slot=\"kanban-column\"\n          role=\"group\"\n          aria-label={label ?? id}\n          data-column-id={id}\n          data-over={isOver ? '' : undefined}\n          onDragOver={handleDragOver}\n          onDragLeave={handleDragLeave}\n          onDrop={handleDrop}\n          className={cn(kanbanColumnVariants({ isOver }), className)}\n          {...props}\n        >\n          {children}\n        </div>\n      </KanbanColumnContext.Provider>\n    )\n  },\n)\nKanbanColumn.displayName = 'KanbanColumn'\n\n/* ------------------------------------------------------------------ */\n/* KanbanColumnHeader & Sub-components                                */\n/* ------------------------------------------------------------------ */\n\nexport const KanbanColumnHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, children, ...props }, ref) => (\n    <div\n      ref={ref}\n      data-slot=\"kanban-column-header\"\n      className={cn('flex items-center justify-between gap-2 px-1 py-0.5', className)}\n      {...props}\n    >\n      {children}\n    </div>\n  ),\n)\nKanbanColumnHeader.displayName = 'KanbanColumnHeader'\n\nexport interface KanbanColumnDotProps extends React.HTMLAttributes<HTMLSpanElement> {\n  color?: string\n}\n\nexport const KanbanColumnDot = React.forwardRef<HTMLSpanElement, KanbanColumnDotProps>(\n  ({ className, color = 'bg-primary', ...props }, ref) => (\n    <span\n      ref={ref}\n      data-slot=\"kanban-column-dot\"\n      className={cn('size-2 shrink-0 rounded-full', color, className)}\n      {...props}\n    />\n  ),\n)\nKanbanColumnDot.displayName = 'KanbanColumnDot'\n\nexport const KanbanColumnTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(\n  ({ className, children, ...props }, ref) => (\n    <h3\n      ref={ref}\n      data-slot=\"kanban-column-title\"\n      className={cn('text-foreground text-sm font-semibold tracking-tight', className)}\n      {...props}\n    >\n      {children}\n    </h3>\n  ),\n)\nKanbanColumnTitle.displayName = 'KanbanColumnTitle'\n\nexport interface KanbanColumnCountProps extends React.HTMLAttributes<HTMLSpanElement> {\n  count?: number | string\n}\n\nexport const KanbanColumnCount = React.forwardRef<HTMLSpanElement, KanbanColumnCountProps>(\n  ({ className, count, children, ...props }, ref) => (\n    <span\n      ref={ref}\n      data-slot=\"kanban-column-count\"\n      className={cn(\n        'bg-muted text-muted-foreground rounded-md px-1.5 py-0.5 text-xs font-medium tabular-nums',\n        className,\n      )}\n      {...props}\n    >\n      {count ?? children}\n    </span>\n  ),\n)\nKanbanColumnCount.displayName = 'KanbanColumnCount'\n\nexport const KanbanColumnAdd = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(\n  ({ className, children, ...props }, ref) => (\n    <button\n      ref={ref}\n      type=\"button\"\n      data-slot=\"kanban-column-add\"\n      className={cn(\n        'text-muted-foreground hover:bg-background hover:text-foreground focus-visible:ring-ring inline-flex size-6 items-center justify-center rounded-md transition-colors focus-visible:ring-1 focus-visible:outline-none',\n        className,\n      )}\n      {...props}\n    >\n      {children ?? <Plus className=\"size-3.5\" />}\n    </button>\n  ),\n)\nKanbanColumnAdd.displayName = 'KanbanColumnAdd'\n\n/* ------------------------------------------------------------------ */\n/* KanbanColumnBody & Empty                                           */\n/* ------------------------------------------------------------------ */\n\nexport const KanbanColumnBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, children, ...props }, ref) => (\n    <div\n      ref={ref}\n      data-slot=\"kanban-column-body\"\n      className={cn('flex flex-1 flex-col gap-2 overflow-y-auto py-1', className)}\n      {...props}\n    >\n      {children}\n    </div>\n  ),\n)\nKanbanColumnBody.displayName = 'KanbanColumnBody'\n\nexport const KanbanColumnEmpty = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, children, ...props }, ref) => (\n    <div\n      ref={ref}\n      data-slot=\"kanban-column-empty\"\n      className={cn(\n        'border-border/60 text-muted-foreground/60 flex flex-1 items-center justify-center rounded-lg border border-dashed py-8 text-xs',\n        className,\n      )}\n      {...props}\n    >\n      {children ?? 'No cards'}\n    </div>\n  ),\n)\nKanbanColumnEmpty.displayName = 'KanbanColumnEmpty'\n\n/* ------------------------------------------------------------------ */\n/* KanbanCard & Sub-components                                        */\n/* ------------------------------------------------------------------ */\n\nexport interface KanbanCardProps extends React.HTMLAttributes<HTMLDivElement> {\n  id: string\n  disabled?: boolean\n  /** Disable the keyboard grab (Space / arrows) while leaving pointer\n   *  dragging intact. Default: enabled. */\n  keyboardDraggable?: boolean\n}\n\nexport const KanbanCard = React.forwardRef<HTMLDivElement, KanbanCardProps>(\n  (\n    {\n      id,\n      disabled = false,\n      keyboardDraggable = true,\n      className,\n      children,\n      onDragStart,\n      onDragEnd,\n      onKeyDown,\n      onBlur,\n      ...props\n    },\n    ref,\n  ) => {\n    const kanban = useKanbanContext()\n    const column = useKanbanColumnContext()\n    const cardRef = React.useRef<HTMLDivElement | null>(null)\n    const isGrabbed = kanban.grabbedCardId === id\n    const isDragging = kanban.draggingCardId === id || isGrabbed\n    const canKeyboardDrag = keyboardDraggable && !disabled\n\n    const setRefs = React.useCallback(\n      (node: HTMLDivElement | null) => {\n        cardRef.current = node\n        if (typeof ref === 'function') ref(node)\n        else if (ref) ref.current = node\n      },\n      [ref],\n    )\n\n    const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {\n      if (disabled) {\n        e.preventDefault()\n        return\n      }\n      e.dataTransfer.effectAllowed = 'move'\n      e.dataTransfer.setData('text/plain', id)\n      kanban.setDraggingCard(id, column.columnId)\n      onDragStart?.(e)\n    }\n\n    const handleDragEnd = (e: React.DragEvent<HTMLDivElement>) => {\n      kanban.setDraggingCard(null, null)\n      kanban.setOverColumn(null)\n      onDragEnd?.(e)\n    }\n\n    const columnName = (el: HTMLElement | null) => el?.getAttribute('aria-label') || el?.dataset.columnId || 'column'\n\n    /** Ordered columns of the board this card sits in. */\n    const boardColumns = () => {\n      const board: ParentNode = cardRef.current?.closest('[data-slot=\"kanban\"]') ?? document\n      return Array.from(board.querySelectorAll<HTMLElement>('[data-slot=\"kanban-column\"]'))\n    }\n\n    // The consumer owns the data, so a moved card unmounts here and mounts\n    // again under the new column. Chasing it with requestAnimationFrame races\n    // React's commit — the query resolves the node that is on its way out and\n    // focus lands on <body>. Instead, whichever instance is mounted while the\n    // card is held takes focus back after the commit that rendered it.\n    React.useEffect(() => {\n      if (!isGrabbed) return\n      const node = cardRef.current\n      if (node && document.activeElement !== node) node.focus()\n    }, [isGrabbed])\n\n    const grab = () => {\n      kanban.setGrabbedCard(id)\n      kanban.setDraggingCard(id, column.columnId)\n      kanban.setOverColumn(column.columnId)\n      kanban.announce('Picked up card. Use the arrow keys to move it, space to drop, escape to cancel.')\n    }\n\n    const release = (cancelled: boolean) => {\n      kanban.setGrabbedCard(null)\n      kanban.setDraggingCard(null, null)\n      kanban.setOverColumn(null)\n      kanban.announce(cancelled ? 'Move cancelled.' : 'Card dropped.')\n    }\n\n    const moveToColumn = (delta: -1 | 1) => {\n      const columns = boardColumns()\n      const currentIdx = columns.findIndex((el) => el.dataset.columnId === column.columnId)\n      if (currentIdx === -1) return\n      const target = columns[currentIdx + delta]\n      // Deliberately not wrapping: running off the end of a board should\n      // stop, not teleport the card back to the first column.\n      if (!target?.dataset.columnId) return\n      kanban.onCardMove?.({ cardId: id, fromColumnId: column.columnId, toColumnId: target.dataset.columnId })\n      kanban.setDraggingCard(id, target.dataset.columnId)\n      kanban.setOverColumn(target.dataset.columnId)\n      kanban.announce(`Moved to ${columnName(target)}.`)\n    }\n\n    const moveWithinColumn = (delta: -1 | 1) => {\n      const columnEl = cardRef.current?.closest<HTMLElement>('[data-slot=\"kanban-column\"]')\n      if (!columnEl) return\n      const cards = Array.from(columnEl.querySelectorAll<HTMLElement>('[data-slot=\"kanban-card\"]'))\n      const currentIdx = cards.findIndex((el) => el.dataset.cardId === id)\n      if (currentIdx === -1) return\n      const targetIdx = currentIdx + delta\n      if (targetIdx < 0 || targetIdx > cards.length - 1) return\n      kanban.onCardMove?.({\n        cardId: id,\n        fromColumnId: column.columnId,\n        toColumnId: column.columnId,\n        toIndex: targetIdx,\n      })\n      kanban.announce(`Position ${targetIdx + 1} of ${cards.length}.`)\n    }\n\n    const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n      onKeyDown?.(e)\n      if (!canKeyboardDrag || e.defaultPrevented) return\n\n      if (e.key === ' ' || e.key === 'Spacebar') {\n        e.preventDefault()\n        if (isGrabbed) release(false)\n        else grab()\n        return\n      }\n      if (!isGrabbed) return\n      if (e.key === 'Escape') {\n        e.preventDefault()\n        release(true)\n        return\n      }\n      if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {\n        e.preventDefault()\n        moveToColumn(e.key === 'ArrowLeft' ? -1 : 1)\n        return\n      }\n      if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {\n        e.preventDefault()\n        moveWithinColumn(e.key === 'ArrowUp' ? -1 : 1)\n      }\n    }\n\n    const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {\n      onBlur?.(e)\n      // A grabbed card that loses focus (click elsewhere, Tab) would\n      // otherwise stay stuck in the held state with no way back to it.\n      if (isGrabbed) release(true)\n    }\n\n    return (\n      <div\n        ref={setRefs}\n        data-uipkge=\"\"\n        data-slot=\"kanban-card\"\n        data-card-id={id}\n        data-state={isGrabbed ? 'grabbed' : isDragging ? 'dragging' : 'idle'}\n        data-disabled={disabled || undefined}\n        role={canKeyboardDrag ? 'button' : undefined}\n        tabIndex={canKeyboardDrag ? 0 : undefined}\n        aria-disabled={disabled || undefined}\n        aria-roledescription={canKeyboardDrag ? 'draggable card' : undefined}\n        aria-pressed={canKeyboardDrag ? isGrabbed : undefined}\n        draggable={!disabled}\n        onDragStart={handleDragStart}\n        onDragEnd={handleDragEnd}\n        onKeyDown={handleKeyDown}\n        onBlur={handleBlur}\n        className={cn(kanbanCardVariants({ isDragging }), disabled && 'pointer-events-none opacity-50', className)}\n        {...props}\n      >\n        {children}\n      </div>\n    )\n  },\n)\nKanbanCard.displayName = 'KanbanCard'\n\nexport const KanbanCardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, children, ...props }, ref) => (\n    <div ref={ref} data-slot=\"kanban-card-header\" className={cn('flex flex-col gap-1', className)} {...props}>\n      {children}\n    </div>\n  ),\n)\nKanbanCardHeader.displayName = 'KanbanCardHeader'\n\nexport const KanbanCardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, children, ...props }, ref) => (\n    <p\n      ref={ref}\n      data-slot=\"kanban-card-title\"\n      className={cn('text-foreground text-sm leading-snug font-medium', className)}\n      {...props}\n    >\n      {children}\n    </p>\n  ),\n)\nKanbanCardTitle.displayName = 'KanbanCardTitle'\n\nexport const KanbanCardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, children, ...props }, ref) => (\n    <p\n      ref={ref}\n      data-slot=\"kanban-card-description\"\n      className={cn('text-muted-foreground line-clamp-2 text-xs', className)}\n      {...props}\n    >\n      {children}\n    </p>\n  ),\n)\nKanbanCardDescription.displayName = 'KanbanCardDescription'\n\nexport const KanbanCardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, children, ...props }, ref) => (\n    <div\n      ref={ref}\n      data-slot=\"kanban-card-footer\"\n      className={cn('text-muted-foreground mt-1 flex items-center justify-between gap-2 pt-1 text-xs', className)}\n      {...props}\n    >\n      {children}\n    </div>\n  ),\n)\nKanbanCardFooter.displayName = 'KanbanCardFooter'\n",
      "type": "registry:ui",
      "target": "~/components/ui/kanban/Kanban.tsx"
    },
    {
      "path": "packages/registry-react/components/kanban/kanban.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\nexport const kanbanColumnVariants = cva(\n  'flex min-h-[300px] w-72 shrink-0 flex-col gap-2 rounded-xl border bg-muted/40 p-3 transition-colors duration-200 ease-out',\n  {\n    variants: {\n      isOver: {\n        true: 'border-primary/50 bg-primary/5 ring-2 ring-primary/20',\n        false: 'border-border/70',\n      },\n    },\n    defaultVariants: {\n      isOver: false,\n    },\n  },\n)\n\nexport const kanbanCardVariants = cva(\n  'group relative flex cursor-grab flex-col gap-2 rounded-lg border bg-card p-3 text-card-foreground shadow-xs transition-[border-color,box-shadow,opacity] duration-150 ease-out active:cursor-grabbing hover:border-border hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n  {\n    variants: {\n      isDragging: {\n        // Keyboard grab reuses this state, so the held card stays fully\n        // opaque — unlike a pointer drag there is no drag image to look at.\n        true: 'shadow-none ring-2 ring-primary/40 opacity-100 data-[state=dragging]:opacity-40',\n        false: 'opacity-100',\n      },\n    },\n    defaultVariants: {\n      isDragging: false,\n    },\n  },\n)\n\nexport type KanbanColumnVariantsProps = VariantProps<typeof kanbanColumnVariants>\nexport type KanbanCardVariantsProps = VariantProps<typeof kanbanCardVariants>\n",
      "type": "registry:ui",
      "target": "~/components/ui/kanban/kanban.variants.ts"
    },
    {
      "path": "packages/registry-react/components/kanban/index.ts",
      "content": "export * from './Kanban'\nexport * from './kanban.variants'\n",
      "type": "registry:ui",
      "target": "~/components/ui/kanban/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Composable compound Kanban primitive for building board, pipeline, and agile workflows with tactile card drag-and-drop mechanics.",
  "categories": [
    "data-display",
    "layout"
  ]
}