"use client"; import { useState } from "react"; import { useSortable } from "@dnd-kit/sortable"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { useDndContext, useDroppable } from "@dnd-kit/core"; import { CSS } from "@dnd-kit/utilities"; import { GripVertical, MoreVertical, Trash2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { GroupCard } from "@/components/board/group-card"; import { AddGroupPopover } from "@/components/board/add-group-popover"; import { ConfirmDeleteDialog } from "@/components/confirm-delete-dialog"; import { useBoard } from "@/components/board/board-context"; import { useBoardView } from "@/components/board/board-view-provider"; import type { CategoryDTO } from "@/types/board"; export function CategoryLane({ category }: { category: CategoryDTO }) { const { removeCategory } = useBoard(); const { view } = useBoardView(); const [confirmOpen, setConfirmOpen] = useState(false); const isEmpty = category.groups.length === 0; // Compact hides any group with no unchecked to-dos left -- which also // covers a brand-new group with no to-dos at all, since it has zero // unchecked ones too. const visibleGroups = view === "compact" ? category.groups.filter((g) => g.todos.some((t) => !t.completed)) : category.groups; // The lane itself is sortable (so lanes can be reordered), which // registers its *entire* rect -- header, cards, empty space, all of it -- // as a droppable. Dragging a group card over empty lane space then had // two overlapping droppables to resolve against: this lane's own // "category" droppable and the narrower "category-dropzone" below. With // `closestCenter`, the much larger lane rect often won, so drop handling // never saw a "group" or "category-dropzone" target and silently did // nothing. Disabling this lane's droppable side while a *group* (not a // lane) is being dragged removes it from collision detection entirely, // leaving only the group cards and the dropzone as valid targets. const { active, over } = useDndContext(); const isDraggingGroup = active?.data.current?.type === "group"; // While a group is being dragged, light up this lane the moment the // pointer is over anything inside it -- a card or the empty dropzone // below them -- so an (especially empty or short) lane still gives clear // "drop here" feedback even when there's no card directly under the // cursor to highlight. const overData = over?.data.current as { categoryId?: string } | undefined; const isDropTargetLane = isDraggingGroup && overData?.categoryId === category.id; const { setNodeRef, setActivatorNodeRef, attributes, listeners, transform, transition, isDragging, } = useSortable({ id: category.id, data: { type: "category" }, disabled: { draggable: false, droppable: isDraggingGroup }, }); const { setNodeRef: setDropzoneRef } = useDroppable({ id: `category-dropzone-${category.id}`, data: { type: "category-dropzone", categoryId: category.id }, }); const groupIds = visibleGroups.map((g) => g.id); return ( <>