{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "kanban-board",
  "type": "registry:ui",
  "description": "Kanban board with drag-and-drop columns and cards",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/default/ui/kanban-board.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useRef, useState } from \"react\"\nimport Image from \"next/image\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\n/* ═══════════════════════════════════════════════════════════════════════\n   DESIGN TOKENS\n   ═══════════════════════════════════════════════════════════════════════ */\n\nconst TAG_VARIANTS = {\n  feature: { bg: \"rgba(59,130,246,0.10)\", color: \"#2563eb\" },\n  bug: { bg: \"rgba(239,68,68,0.10)\", color: \"#dc2626\" },\n  design: { bg: \"rgba(168,85,247,0.10)\", color: \"#9333ea\" },\n  done: { bg: \"rgba(16,185,129,0.10)\", color: \"#059669\" },\n  improvement: { bg: \"rgba(245,158,11,0.10)\", color: \"#d97706\" },\n  docs: { bg: \"rgba(6,182,212,0.10)\", color: \"#0891b2\" },\n}\n\nconst COLUMN_META = {\n  todo: {\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        fill=\"none\"\n        height=\"13\"\n        stroke=\"#b0b0b0\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"2.2\"\n        viewBox=\"0 0 24 24\"\n        width=\"13\"\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"10\" />\n      </svg>\n    ),\n  },\n  \"in-progress\": {\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        fill=\"none\"\n        height=\"13\"\n        stroke=\"#f59e0b\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"2.2\"\n        viewBox=\"0 0 24 24\"\n        width=\"13\"\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"10\" />\n        <polyline points=\"12 6 12 12 16 14\" />\n      </svg>\n    ),\n  },\n  done: {\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        fill=\"none\"\n        height=\"13\"\n        stroke=\"#10b981\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"2.2\"\n        viewBox=\"0 0 24 24\"\n        width=\"13\"\n      >\n        <circle cx=\"12\" cy=\"12\" r=\"10\" />\n        <path d=\"m9 12 2 2 4-4\" />\n      </svg>\n    ),\n  },\n}\n\nexport type TagVariant = keyof typeof TAG_VARIANTS\nexport type ColumnStatus = keyof typeof COLUMN_META\n\nexport type CardTag = {\n  label: string\n  variant: TagVariant\n}\n\nexport type Assignee = {\n  name: string\n  avatar?: string\n}\n\nexport type CardData = {\n  id: string\n  title: string\n  tags?: CardTag[]\n  assignee?: Assignee\n  date?: string\n}\n\nexport type ColumnData = {\n  id: ColumnStatus\n  title: string\n  status: ColumnStatus\n  cards: CardData[]\n}\n\nexport type DragState = {\n  cardId: string\n  fromColumn: ColumnStatus\n} | null\n\ntype TagProps = {\n  label: string\n  variant: TagVariant\n}\n\ntype AvatarProps = {\n  src?: string\n  name?: string\n}\n\ntype CardProps = {\n  card: CardData\n  isDone: boolean\n  index: number\n  onDragStart: () => void\n  onDragEnd: () => void\n  isDragging: boolean\n}\n\ntype DropIndicatorProps = {\n  isActive: boolean\n}\n\nexport type ColumnProps = {\n  column: ColumnData\n  onDrop: (\n    cardId: string,\n    fromColumnId: ColumnStatus,\n    toColumnId: ColumnStatus\n  ) => void\n  dragState: DragState\n  setDragState: React.Dispatch<React.SetStateAction<DragState>>\n}\n\nconst springTransition = {\n  type: \"spring\" as const,\n  stiffness: 500,\n  damping: 30,\n}\nexport const gentleSpring = {\n  type: \"spring\" as const,\n  stiffness: 300,\n  damping: 26,\n}\n\n/* ═══════════════════════════════════════════════════════════════════════\n   TAG\n   ═══════════════════════════════════════════════════════════════════════ */\n\nfunction Tag({ label, variant }: TagProps) {\n  const v = TAG_VARIANTS[variant] || TAG_VARIANTS.feature\n  return (\n    <span\n      className=\"inline-block rounded-[6px] px-[7px] py-[2.5px] font-semibold text-[10.5px] leading-snug tracking-wide\"\n      style={{ backgroundColor: v.bg, color: v.color }}\n    >\n      {label}\n    </span>\n  )\n}\n\n/* ═══════════════════════════════════════════════════════════════════════\n   AVATAR\n   ═══════════════════════════════════════════════════════════════════════ */\n\nfunction Avatar({ src, name }: AvatarProps) {\n  if (src) {\n    return (\n      <Image\n        alt={name || \"\"}\n        className=\"size-[22px] rounded-full object-cover\"\n        height={22}\n        src={src}\n        style={{\n          outline: \"1px solid var(--border)\",\n          outlineOffset: \"-1px\",\n        }}\n        width={22}\n      />\n    )\n  }\n  const initials = (name || \"?\")\n    .split(\" \")\n    .map((w) => w[0])\n    .join(\"\")\n    .slice(0, 2)\n    .toUpperCase()\n  return (\n    <div\n      className=\"flex size-[22px] items-center justify-center rounded-full font-bold text-[9px]\"\n      style={{ background: \"var(--muted)\", color: \"var(--muted-foreground)\" }}\n    >\n      {initials}\n    </div>\n  )\n}\n\n/* ═══════════════════════════════════════════════════════════════════════\n   CARD\n   ═══════════════════════════════════════════════════════════════════════ */\n\nconst cardVariants = {\n  initial: { opacity: 0, y: 8, scale: 0.97, filter: \"blur(4px)\" },\n  animate: { opacity: 1, y: 0, scale: 1, filter: \"blur(0px)\" },\n  exit: { opacity: 0, y: -6, scale: 0.98, filter: \"blur(3px)\" },\n}\n\nfunction Card({\n  card,\n  isDone,\n  index,\n  onDragStart,\n  onDragEnd,\n  isDragging,\n}: CardProps) {\n  const shouldReduce = useReducedMotion()\n  const tags = card.tags ?? []\n\n  return (\n    <motion.div\n      animate=\"animate\"\n      className=\"not-prose flex cursor-grab select-none flex-col gap-1.5 active:cursor-grabbing\"\n      draggable\n      exit=\"exit\"\n      initial=\"initial\"\n      layout\n      layoutId={card.id}\n      onDragEnd={onDragEnd}\n      onDragStart={onDragStart}\n      style={{\n        borderRadius: 14,\n        background: \"var(--muted)\",\n        padding: \"8px 10px\",\n        boxShadow: isDragging\n          ? \"0 0 0 1px var(--border), 0 12px 32px rgba(0,0,0,0.18), 0 4px 8px rgba(0,0,0,0.10)\"\n          : \"0 0 0 1px var(--border), 0 1px 2px rgba(0,0,0,0.04), 0 2px 6px rgba(0,0,0,0.03)\",\n        opacity: isDone && !isDragging ? 0.5 : 1,\n        position: \"relative\",\n        zIndex: isDragging ? 50 : 1,\n      }}\n      transition={{\n        ...gentleSpring,\n        delay: shouldReduce ? 0 : index * 0.05,\n        layout: springTransition,\n      }}\n      variants={cardVariants}\n      whileHover={{\n        y: -2,\n        boxShadow:\n          \"0 0 0 1px var(--border), 0 4px 12px rgba(0,0,0,0.10), 0 2px 4px rgba(0,0,0,0.06)\",\n        transition: { type: \"spring\", stiffness: 400, damping: 20 },\n      }}\n      whileTap={{ scale: 0.97 }}\n    >\n      {/* Tags */}\n      {tags.length > 0 && (\n        <div className=\"flex flex-wrap gap-1\">\n          {tags.map((tag) => (\n            <Tag\n              key={`${card.id}-${tag.label}-${tag.variant}`}\n              label={tag.label}\n              variant={tag.variant}\n            />\n          ))}\n        </div>\n      )}\n\n      {/* Title — `div` avoids `.prose p` margins when embedded in MDX/docs */}\n      <div\n        className=\"font-semibold text-[11.5px] leading-snug\"\n        style={{\n          color: \"var(--foreground)\",\n          textDecoration: isDone ? \"line-through\" : \"none\",\n          textDecorationColor: isDone ? \"var(--muted-foreground)\" : undefined,\n          textWrap: \"pretty\",\n        }}\n      >\n        {card.title}\n      </div>\n\n      {/* Footer */}\n      {(card.assignee || card.date) && (\n        <div className=\"flex items-center justify-between\">\n          {card.assignee ? (\n            <Avatar name={card.assignee.name} src={card.assignee.avatar} />\n          ) : (\n            <div />\n          )}\n          {card.date && (\n            <span\n              className=\"font-medium text-[10.5px] tabular-nums\"\n              style={{ color: \"var(--muted-foreground)\" }}\n            >\n              {card.date}\n            </span>\n          )}\n        </div>\n      )}\n    </motion.div>\n  )\n}\n\n/* ═══════════════════════════════════════════════════════════════════════\n   DROP INDICATOR\n   ═══════════════════════════════════════════════════════════════════════ */\n\nfunction DropIndicator({ isActive }: DropIndicatorProps) {\n  return (\n    <motion.div\n      animate={{\n        opacity: isActive ? 1 : 0,\n        scaleX: isActive ? 1 : 0.3,\n      }}\n      className=\"pointer-events-none mx-auto\"\n      initial={false}\n      style={{\n        height: 3,\n        width: \"60%\",\n        borderRadius: 99,\n        background:\n          \"linear-gradient(90deg, transparent, rgba(59,130,246,0.4), transparent)\",\n        marginTop: 2,\n        marginBottom: 2,\n      }}\n      transition={{ type: \"spring\", stiffness: 500, damping: 28 }}\n    />\n  )\n}\n\n/* ═══════════════════════════════════════════════════════════════════════\n   COLUMN\n   ═══════════════════════════════════════════════════════════════════════ */\n\nexport function Column({\n  column,\n  onDrop,\n  dragState,\n  setDragState,\n}: ColumnProps) {\n  const [dragOver, setDragOver] = useState(false)\n  const meta = COLUMN_META[column.status] || COLUMN_META.todo\n  const isDone = column.status === \"done\"\n  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n\n  const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {\n    e.preventDefault()\n    e.dataTransfer.dropEffect = \"move\"\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current)\n    }\n    setDragOver(true)\n  }, [])\n\n  const handleDragLeave = useCallback(() => {\n    timeoutRef.current = setTimeout(() => setDragOver(false), 60)\n  }, [])\n\n  const handleDrop = useCallback(\n    (e: React.DragEvent<HTMLDivElement>) => {\n      e.preventDefault()\n      setDragOver(false)\n      if (dragState) {\n        onDrop(dragState.cardId, dragState.fromColumn, column.id)\n      }\n    },\n    [dragState, onDrop, column.id]\n  )\n\n  return (\n    <div className=\"flex h-full min-h-0 min-w-0 flex-col\">\n      {/* Header */}\n      <motion.div\n        animate={{ opacity: 1, y: 0 }}\n        className=\"mb-[14px] flex items-center gap-[7px] px-[2px]\"\n        initial={{ opacity: 0, y: -4 }}\n        transition={{ ...gentleSpring, delay: 0.05 }}\n      >\n        {meta.icon}\n        <span\n          className=\"font-bold text-[13px] tracking-[-0.01em]\"\n          style={{ color: \"var(--foreground)\" }}\n        >\n          {column.title}\n        </span>\n        <motion.span\n          animate={{ scale: 1, opacity: 1 }}\n          className=\"ml-auto font-semibold text-[12.5px] tabular-nums\"\n          initial={{ scale: 0.6, opacity: 0 }}\n          key={column.cards.length}\n          style={{ color: \"var(--muted-foreground)\" }}\n          transition={springTransition}\n        >\n          {column.cards.length}\n        </motion.span>\n      </motion.div>\n\n      {/* Card list — flex-1 so drop target spans full column below header (short / empty columns) */}\n      <motion.div\n        animate={{\n          backgroundColor: dragOver\n            ? \"rgba(59,130,246,0.045)\"\n            : \"rgba(0,0,0,0)\",\n          scale: dragOver ? 1.012 : 1,\n        }}\n        className=\"flex min-h-0 flex-1 flex-col gap-2 rounded-2xl p-0.5\"\n        onDragLeave={handleDragLeave}\n        onDragOver={handleDragOver}\n        onDrop={handleDrop}\n        transition={{ type: \"spring\", stiffness: 400, damping: 24 }}\n      >\n        <DropIndicator isActive={dragOver} />\n\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {column.cards.map((card, i) => (\n            <Card\n              card={card}\n              index={i}\n              isDone={isDone}\n              isDragging={dragState?.cardId === card.id}\n              key={card.id}\n              onDragEnd={() => setDragState(null)}\n              onDragStart={() =>\n                setDragState({ cardId: card.id, fromColumn: column.id })\n              }\n            />\n          ))}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}