{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "board",
  "title": "Board",
  "description": "A lane board over dnd-kit: Board / BoardColumn / BoardCard. onMove(cardId, toColumnId, toIndex) reports cross-column moves and same-column reorders. toIndex is the zero-based position in the destination column’s rendered list after removing the moved card; a drop on column space appends, and an empty column gives 0. onDragStateChange?(dragging) reports pickup and drop or cancellation, so callers can pause polling during a drag. shadcn publishes no board, so this is a brand primitive rather than a restyle. Keyboard-operable by construction: dnd-kit's KeyboardSensor with a lane-aware coordinateGetter (left/right cross lanes, up/down move within one) plus screen-reader announcements composed from the caller's own card and lane labels. Semantic role tokens only — a picked-up card is marked by a ring and a lift and a drop target by a dashed hairline, so neither state relies on colour, and no lane or card is filled with orange. The board scrolls sideways and a lane scrolls down; neither makes the page scroll horizontally. Honours prefers-reduced-motion.",
  "dependencies": [
    "@dnd-kit/core@6.3.1",
    "@dnd-kit/sortable@10.0.0"
  ],
  "registryDependencies": [
    "@waimakers/utils",
    "@waimakers/theme"
  ],
  "files": [
    {
      "path": "src/components/ui/board.tsx",
      "content": "import {\n  DndContext,\n  DragOverlay,\n  KeyboardCode,\n  KeyboardSensor,\n  MouseSensor,\n  TouchSensor,\n  closestCenter,\n  pointerWithin,\n  useDroppable,\n  useSensor,\n  useSensors,\n} from \"@dnd-kit/core\";\nimport type {\n  Announcements,\n  ClientRect,\n  CollisionDetection,\n  DragEndEvent,\n  DragStartEvent,\n  KeyboardCoordinateGetter,\n  ScreenReaderInstructions,\n  UniqueIdentifier,\n} from \"@dnd-kit/core\";\nimport {\n  SortableContext,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport {\n  type ComponentProps,\n  type ReactNode,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { cn, focusRing } from \"../../lib/utils\";\n\ninterface BoardCardRecord {\n  label: string;\n  columnId: string;\n  content: ReactNode;\n}\n\nconst BOARD_CARD_SURFACE = \"rounded-lg border border-border bg-card p-3 text-sm text-foreground\";\n\ninterface BoardContextValue {\n  cards: Map<string, BoardCardRecord>;\n  columns: Map<string, string>;\n}\n\nconst BoardContext = createContext<BoardContextValue | null>(null);\n\nfunction useBoard(): BoardContextValue {\n  const context = useContext(BoardContext);\n  if (context === null) {\n    throw new Error(\"Board parts must be used within a Board.\");\n  }\n  return context;\n}\n\ninterface BoardColumnContextValue {\n  columnId: string;\n  registerCard: (id: string, node: HTMLElement) => void;\n  unregisterCard: (id: string) => void;\n}\n\nconst BoardColumnContext = createContext<BoardColumnContextValue | null>(null);\n\nfunction useBoardColumn(): BoardColumnContextValue {\n  const context = useContext(BoardColumnContext);\n  if (context === null) {\n    throw new Error(\"BoardCard must be used within a BoardColumn.\");\n  }\n  return context;\n}\n\nfunction usePrefersReducedMotion(): boolean {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") {\n      return;\n    }\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const sync = () => setReduced(query.matches);\n    sync();\n    query.addEventListener(\"change\", sync);\n    return () => query.removeEventListener(\"change\", sync);\n  }, []);\n\n  return reduced;\n}\n\nfunction columnIdOf(data: Record<string, unknown> | undefined): string | null {\n  if (data?.[\"type\"] !== \"column\" && data?.[\"type\"] !== \"card\") {\n    return null;\n  }\n  return typeof data[\"columnId\"] === \"string\" ? data[\"columnId\"] : null;\n}\n\nconst boardCoordinateGetter: KeyboardCoordinateGetter = (event, args) => {\n  if (event.code !== KeyboardCode.Left && event.code !== KeyboardCode.Right) {\n    return sortableKeyboardCoordinates(event, args);\n  }\n\n  const { collisionRect, droppableContainers, droppableRects } = args.context;\n  if (collisionRect === null) {\n    return undefined;\n  }\n\n  const lanes = droppableContainers\n    .toArray()\n    .filter((container) => !container.disabled && container.data.current?.[\"type\"] === \"column\")\n    .map((container) => droppableRects.get(container.id))\n    .filter((rect): rect is ClientRect => rect !== undefined)\n    .sort((a, b) => a.left - b.left);\n  if (lanes.length === 0) {\n    return undefined;\n  }\n\n  const centre = collisionRect.left + collisionRect.width / 2;\n  let currentLane = 0;\n  let shortest = Number.POSITIVE_INFINITY;\n  lanes.forEach((rect, index) => {\n    const distance = Math.abs(rect.left + rect.width / 2 - centre);\n    if (distance < shortest) {\n      shortest = distance;\n      currentLane = index;\n    }\n  });\n\n  const target = lanes[currentLane + (event.code === KeyboardCode.Right ? 1 : -1)];\n  if (target === undefined) {\n    return undefined;\n  }\n\n  return {\n    x: target.left + target.width / 2 - collisionRect.width / 2,\n    y: args.currentCoordinates.y,\n  };\n};\n\nconst boardCollisionDetection: CollisionDetection = (args) => {\n  const hits = pointerWithin(args);\n  const cards = hits.filter(\n    (hit) =>\n      args.droppableContainers.find((container) => container.id === hit.id)?.data.current?.[\n        \"type\"\n      ] === \"card\",\n  );\n  return cards.length > 0 ? cards : hits.length > 0 ? hits : closestCenter(args);\n};\n\nexport interface BoardProps extends ComponentProps<\"div\"> {\n  onMove: (cardId: string, toColumnId: string, toIndex: number) => void;\n  onDragStateChange?: (dragging: boolean) => void;\n  children: ReactNode;\n}\n\nfunction Board({ onMove, onDragStateChange, className, children, ...props }: BoardProps) {\n  const cards = useRef(new Map<string, BoardCardRecord>()).current;\n  const columns = useRef(new Map<string, string>()).current;\n  const context = useMemo<BoardContextValue>(() => ({ cards, columns }), [cards, columns]);\n  const reducedMotion = usePrefersReducedMotion();\n\n  const dndId = useId();\n\n  const sensors = useSensors(\n    useSensor(MouseSensor, { activationConstraint: { distance: 4 } }),\n    useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 8 } }),\n    useSensor(KeyboardSensor, { coordinateGetter: boardCoordinateGetter }),\n  );\n\n  const cardLabel = useCallback(\n    (id: UniqueIdentifier) => cards.get(String(id))?.label ?? String(id),\n    [cards],\n  );\n  const laneLabel = useCallback(\n    (columnId: string | null) => (columnId === null ? null : (columns.get(columnId) ?? columnId)),\n    [columns],\n  );\n  const laneOf = useCallback(\n    (id: UniqueIdentifier) => laneLabel(cards.get(String(id))?.columnId ?? null),\n    [cards, laneLabel],\n  );\n\n  const announcements = useMemo<Announcements>(\n    () => ({\n      onDragStart: ({ active }) => {\n        const lane = laneOf(active.id);\n        return lane === null\n          ? `Picked up ${cardLabel(active.id)}.`\n          : `Picked up ${cardLabel(active.id)}. It is in ${lane}.`;\n      },\n      onDragOver: ({ active, over }) => {\n        const lane = laneLabel(columnIdOf(over?.data.current));\n        return lane === null\n          ? `${cardLabel(active.id)} is not over a lane.`\n          : `${cardLabel(active.id)} is over ${lane}.`;\n      },\n      onDragEnd: ({ active, over }) => {\n        const lane = laneLabel(columnIdOf(over?.data.current));\n        return lane === null\n          ? `${cardLabel(active.id)} was dropped outside every lane and did not move.`\n          : `${cardLabel(active.id)} was dropped into ${lane}.`;\n      },\n      onDragCancel: ({ active }) => {\n        const lane = laneOf(active.id);\n        return lane === null\n          ? `Moving ${cardLabel(active.id)} was cancelled.`\n          : `Moving ${cardLabel(active.id)} was cancelled. It is still in ${lane}.`;\n      },\n    }),\n    [cardLabel, laneLabel, laneOf],\n  );\n\n  const screenReaderInstructions = useMemo<ScreenReaderInstructions>(\n    () => ({\n      draggable:\n        \"To move a card, press space or enter to pick it up. Use the left and right arrow keys to \" +\n        \"choose a lane and the up and down arrow keys to move within one. Press space or enter \" +\n        \"again to drop it, or escape to leave it where it was.\",\n    }),\n    [],\n  );\n\n  const [activeCardId, setActiveCardId] = useState<string | null>(null);\n\n  const handleDragStart = useCallback(\n    ({ active }: DragStartEvent) => {\n      setActiveCardId(String(active.id));\n      onDragStateChange?.(true);\n    },\n    [onDragStateChange],\n  );\n\n  const handleDragCancel = useCallback(() => {\n    setActiveCardId(null);\n    onDragStateChange?.(false);\n  }, [onDragStateChange]);\n\n  const handleDragEnd = useCallback(\n    ({ active, over }: DragEndEvent) => {\n      setActiveCardId(null);\n      onDragStateChange?.(false);\n      const toColumnId = columnIdOf(over?.data.current);\n      if (toColumnId === null) {\n        return;\n      }\n      const cardId = String(active.id);\n      const sortable = over?.data.current?.[\"sortable\"] as\n        | { items: UniqueIdentifier[] }\n        | undefined;\n      const items = (sortable?.items ?? over?.data.current?.[\"items\"] ?? []) as UniqueIdentifier[];\n      const fromIndex = items.indexOf(active.id);\n      const overIndex = items.indexOf(over?.id ?? \"\");\n      const translated = active.rect.current.translated;\n      const after =\n        translated !== null &&\n        over !== null &&\n        translated.top + translated.height / 2 > over.rect.top + over.rect.height / 2;\n      const toIndex =\n        overIndex < 0\n          ? items.filter((id) => id !== active.id).length\n          : fromIndex >= 0\n            ? overIndex\n            : overIndex + (after ? 1 : 0);\n      if (cards.get(cardId)?.columnId === toColumnId && fromIndex === toIndex) {\n        return;\n      }\n      onMove(cardId, toColumnId, toIndex);\n    },\n    [cards, onMove, onDragStateChange],\n  );\n\n  const activeCard = activeCardId === null ? undefined : cards.get(activeCardId);\n  return (\n    <BoardContext.Provider value={context}>\n      <DndContext\n        id={dndId}\n        sensors={sensors}\n        collisionDetection={boardCollisionDetection}\n        accessibility={{ announcements, screenReaderInstructions }}\n        onDragStart={handleDragStart}\n        onDragEnd={handleDragEnd}\n        onDragCancel={handleDragCancel}\n      >\n        <div\n          data-slot=\"board\"\n          className={cn(\n            \"flex w-full items-start gap-4 overflow-x-auto pb-2 [scrollbar-width:thin]\",\n            className,\n          )}\n          {...props}\n        >\n          {children}\n        </div>\n        <DragOverlay dropAnimation={reducedMotion ? null : undefined} className=\"cursor-grabbing\">\n          {activeCard === undefined ? null : (\n            <div\n              data-slot=\"board-drag-overlay\"\n              aria-hidden=\"true\"\n              inert\n              className={cn(BOARD_CARD_SURFACE, \"shadow-brand-overlay ring-2 ring-ring\")}\n            >\n              {activeCard.content}\n            </div>\n          )}\n        </DragOverlay>\n      </DndContext>\n    </BoardContext.Provider>\n  );\n}\n\nexport interface BoardColumnProps extends ComponentProps<\"div\"> {\n  id: string;\n  label: string;\n  count: number;\n\n  description?: string;\n  children: ReactNode;\n}\n\nfunction BoardColumn({\n  id,\n  label,\n  count,\n  description,\n  className,\n  children,\n  ...props\n}: BoardColumnProps) {\n  const board = useBoard();\n  const headingId = useId();\n  const countId = useId();\n  const descriptionId = useId();\n\n  const { columns } = board;\n  useEffect(() => {\n    columns.set(id, label);\n    return () => {\n      columns.delete(id);\n    };\n  }, [columns, id, label]);\n\n  const nodes = useRef(new Map<string, HTMLElement>()).current;\n  const [cardIds, setCardIds] = useState<string[]>([]);\n  const { setNodeRef, isOver } = useDroppable({\n    id,\n    data: { type: \"column\", columnId: id, items: cardIds },\n  });\n\n  useEffect(() => {\n    setCardIds((current) => inDocumentOrder(current, nodes));\n  }, [children, nodes]);\n\n  const registerCard = useCallback(\n    (cardId: string, node: HTMLElement) => {\n      nodes.set(cardId, node);\n      setCardIds((current) =>\n        inDocumentOrder(current.includes(cardId) ? current : [...current, cardId], nodes),\n      );\n    },\n    [nodes],\n  );\n\n  const unregisterCard = useCallback(\n    (cardId: string) => {\n      nodes.delete(cardId);\n      setCardIds((current) =>\n        current.includes(cardId) ? current.filter((entry) => entry !== cardId) : current,\n      );\n    },\n    [nodes],\n  );\n\n  const columnContext = useMemo<BoardColumnContextValue>(\n    () => ({ columnId: id, registerCard, unregisterCard }),\n    [id, registerCard, unregisterCard],\n  );\n  return (\n    <BoardColumnContext.Provider value={columnContext}>\n      <div\n        ref={setNodeRef}\n        data-slot=\"board-column\"\n        data-over={isOver ? \"true\" : undefined}\n        role=\"group\"\n        aria-labelledby={`${headingId} ${countId}`}\n        aria-describedby={description === undefined ? undefined : descriptionId}\n        className={cn(\n          \"flex max-h-full w-72 shrink-0 flex-col gap-3 rounded-xl border bg-muted p-3\",\n          isOver ? \"border-dashed border-ring bg-accent\" : \"border-border\",\n          \"transition-colors duration-(--dur-fast,120ms) ease-brand motion-reduce:transition-none\",\n          className,\n        )}\n        {...props}\n      >\n        <div className=\"flex items-baseline justify-between gap-2 px-1\">\n          <span\n            id={headingId}\n            className=\"text-brand-eyebrow font-semibold tracking-brand-eyebrow text-muted-foreground uppercase\"\n          >\n            {label}\n          </span>\n          <span id={countId} className=\"font-mono text-xs text-muted-foreground tabular-nums\">\n            {count}\n          </span>\n        </div>\n        {description === undefined ? null : (\n          <p\n            id={descriptionId}\n            className=\"px-1 text-xs leading-snug text-balance text-muted-foreground\"\n          >\n            {description}\n          </p>\n        )}\n        <div className=\"flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto [scrollbar-width:thin]\">\n          <SortableContext items={cardIds} strategy={verticalListSortingStrategy}>\n            {children}\n          </SortableContext>\n        </div>\n      </div>\n    </BoardColumnContext.Provider>\n  );\n}\n\nexport interface BoardCardProps extends ComponentProps<\"div\"> {\n  id: string;\n\n  label?: string;\n  children: ReactNode;\n}\n\nfunction BoardCard({ id, label, className, children, ...props }: BoardCardProps) {\n  const { cards } = useBoard();\n  const { columnId, registerCard, unregisterCard } = useBoardColumn();\n  const reducedMotion = usePrefersReducedMotion();\n\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    setActivatorNodeRef,\n    transform,\n    transition,\n    isDragging,\n  } = useSortable({\n    id,\n    data: { type: \"card\", columnId },\n    transition: { duration: 220, easing: \"var(--ease)\" },\n  });\n  useEffect(() => {\n    cards.set(id, { label: label ?? id, columnId, content: children });\n    return () => {\n      cards.delete(id);\n    };\n  }, [cards, id, label, columnId, children]);\n\n  const setRef = useCallback(\n    (node: HTMLElement | null) => {\n      setNodeRef(node);\n      setActivatorNodeRef(node);\n      if (node === null) {\n        unregisterCard(id);\n      } else {\n        registerCard(id, node);\n      }\n    },\n    [setNodeRef, setActivatorNodeRef, registerCard, unregisterCard, id],\n  );\n  return (\n    <div\n      ref={setRef}\n      data-slot=\"board-card\"\n      data-dragging={isDragging ? \"true\" : undefined}\n      style={{\n        transform:\n          isDragging || transform === null\n            ? undefined\n            : `translate3d(${transform.x}px, ${transform.y}px, 0) scaleX(${transform.scaleX}) scaleY(${transform.scaleY})`,\n        transition: reducedMotion ? undefined : transition,\n      }}\n      className={cn(\n        \"cursor-grab\",\n        BOARD_CARD_SURFACE,\n        focusRing,\n        \"data-[dragging=true]:cursor-grabbing data-[dragging=true]:opacity-40\",\n        className,\n      )}\n      {...props}\n      {...attributes}\n      {...listeners}\n    >\n      {children}\n    </div>\n  );\n}\n\nfunction inDocumentOrder(ids: string[], nodes: Map<string, HTMLElement>): string[] {\n  const sorted = [...ids].sort((a, b) => {\n    const nodeA = nodes.get(a);\n    const nodeB = nodes.get(b);\n    if (nodeA === undefined || nodeB === undefined) {\n      return 0;\n    }\n    return (nodeA.compareDocumentPosition(nodeB) & Node.DOCUMENT_POSITION_FOLLOWING) === 0 ? 1 : -1;\n  });\n  return sorted.every((id, index) => id === ids[index]) ? ids : sorted;\n}\n\nexport { Board, BoardCard, BoardColumn };\n",
      "type": "registry:ui",
      "target": "components/ui/board.tsx"
    }
  ],
  "type": "registry:ui"
}