{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "kanban",
  "title": "Kanban",
  "type": "registry:ui",
  "description": "Composable compound Kanban primitive for building board, pipeline, and agile workflows with tactile card drag-and-drop mechanics.",
  "categories": [
    "data-display",
    "layout"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/kanban/kanban.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { Plus } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { icon } from '../../lib/icon'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { kanbanCardVariants, kanbanColumnVariants } from './kanban.variants'\n\nexport interface KanbanMoveEvent {\n  cardId: string\n  fromColumnId: string\n  toColumnId: string\n  toIndex?: number\n}\n\n/* -------------------------------------------------------------------- Kanban */\n\nexport class UipKanban extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n      }\n    `,\n  ]\n\n  static properties = {\n    draggingCardId: { state: true },\n    draggingColumnId: { state: true },\n    overColumnId: { state: true },\n  }\n\n  draggingCardId: string | null = null\n  draggingColumnId: string | null = null\n  overColumnId: string | null = null\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban')\n  }\n\n  setDraggingCard(cardId: string | null, columnId: string | null) {\n    this.draggingCardId = cardId\n    this.draggingColumnId = columnId\n  }\n\n  setOverColumn(columnId: string | null) {\n    this.overColumnId = columnId\n  }\n\n  moveCard(cardId: string, toColumnId: string, toIndex?: number) {\n    const fromColumnId = this.draggingColumnId ?? ''\n    this.dispatchEvent(\n      new CustomEvent('card-move', {\n        detail: { cardId, fromColumnId, toColumnId, toIndex },\n        bubbles: true,\n        composed: true,\n      }),\n    )\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"w-full\">\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* -------------------------------------------------------------- Kanban Board */\n\nexport class UipKanbanBoard extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-board')\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"base\"\n        class=\"flex w-full gap-4 overflow-x-auto pb-4 scrollbar-thin scrollbar-thumb-muted-foreground/20\"\n      >\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* ------------------------------------------------------------- Kanban Column */\n\nexport class UipKanbanColumn extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: flex;\n        flex-shrink: 0;\n      }\n    `,\n  ]\n\n  static properties = {\n    columnId: { type: String, attribute: 'column-id' },\n    isOver: { state: true },\n  }\n\n  columnId = ''\n  isOver = false\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-column')\n  }\n\n  private onDragOver(e: DragEvent) {\n    e.preventDefault()\n    this.isOver = true\n    const kanban = this.closest('uip-kanban') as UipKanban | null\n    kanban?.setOverColumn(this.columnId)\n  }\n\n  private onDragLeave() {\n    this.isOver = false\n  }\n\n  private onDrop(e: DragEvent) {\n    e.preventDefault()\n    this.isOver = false\n    const cardId = e.dataTransfer?.getData('text/plain')\n    if (cardId) {\n      const kanban = this.closest('uip-kanban') as UipKanban | null\n      kanban?.moveCard(cardId, this.columnId)\n    }\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"base\"\n        data-column-id=${this.columnId}\n        class=${kanbanColumnVariants({ isOver: this.isOver })}\n        @dragover=${this.onDragOver}\n        @dragleave=${this.onDragLeave}\n        @drop=${this.onDrop}\n      >\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* ------------------------------------------------------ Kanban Column Header */\n\nexport class UipKanbanColumnHeader extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-column-header')\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"flex items-center justify-between gap-2 px-1 py-1\">\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* --------------------------------------------------------- Kanban Column Dot */\n\nexport class UipKanbanColumnDot extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: inline-flex;\n      }\n    `,\n  ]\n\n  static properties = {\n    color: { type: String },\n  }\n\n  color = 'bg-primary'\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-column-dot')\n  }\n\n  render() {\n    return html`<span part=\"base\" class=${cn('size-2 rounded-full shrink-0', this.color)}></span>`\n  }\n}\n\n/* ------------------------------------------------------- Kanban Column Title */\n\nexport class UipKanbanColumnTitle extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: inline;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-column-title')\n  }\n\n  render() {\n    return html`<h3 part=\"base\" class=\"text-sm font-semibold text-foreground tracking-tight\"><slot></slot></h3>`\n  }\n}\n\n/* ------------------------------------------------------- Kanban Column Count */\n\nexport class UipKanbanColumnCount extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: inline;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-column-count')\n  }\n\n  render() {\n    return html`\n      <span\n        part=\"base\"\n        class=\"bg-muted text-muted-foreground ml-1.5 rounded-full px-2 py-0.5 text-xs font-medium tabular-nums\"\n      >\n        <slot></slot>\n      </span>\n    `\n  }\n}\n\n/* --------------------------------------------------------- Kanban Column Add */\n\nexport class UipKanbanColumnAdd extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: inline-flex;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-column-add')\n  }\n\n  render() {\n    return html`\n      <button\n        type=\"button\"\n        part=\"base\"\n        class=\"text-muted-foreground hover:text-foreground hover:bg-muted/80 focus-visible:ring-ring flex size-6 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n        aria-label=\"Add card\"\n      >\n        <slot>${icon(Plus, 'plus', 'size-4')}</slot>\n      </button>\n    `\n  }\n}\n\n/* -------------------------------------------------------- Kanban Column Body */\n\nexport class UipKanbanColumnBody extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n        flex: 1 1 0%;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-column-body')\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"flex flex-1 flex-col gap-2 overflow-y-auto\">\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* ------------------------------------------------------- Kanban Column Empty */\n\nexport class UipKanbanColumnEmpty extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-column-empty')\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"base\"\n        class=\"border-dashed border-border/80 text-muted-foreground/60 flex h-24 items-center justify-center rounded-lg border-2 text-xs\"\n      >\n        <slot>No cards</slot>\n      </div>\n    `\n  }\n}\n\n/* --------------------------------------------------------------- Kanban Card */\n\nexport class UipKanbanCard extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n        width: 100%;\n      }\n    `,\n  ]\n\n  static properties = {\n    cardId: { type: String, attribute: 'card-id' },\n    disabled: { type: Boolean },\n    isDragging: { state: true },\n  }\n\n  cardId = ''\n  disabled = false\n  isDragging = false\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-card')\n  }\n\n  private onDragStart(e: DragEvent) {\n    if (this.disabled) return\n    this.isDragging = true\n    e.dataTransfer?.setData('text/plain', this.cardId)\n    const col = this.closest('uip-kanban-column') as UipKanbanColumn | null\n    const kanban = this.closest('uip-kanban') as UipKanban | null\n    kanban?.setDraggingCard(this.cardId, col?.columnId ?? null)\n  }\n\n  private onDragEnd() {\n    this.isDragging = false\n    const kanban = this.closest('uip-kanban') as UipKanban | null\n    kanban?.setDraggingCard(null, null)\n  }\n\n  render() {\n    return html`\n      <div\n        part=\"base\"\n        draggable=${!this.disabled}\n        data-card-id=${this.cardId}\n        class=${cn(\n          kanbanCardVariants({ isDragging: this.isDragging }),\n          this.disabled && 'pointer-events-none opacity-50',\n        )}\n        @dragstart=${this.onDragStart}\n        @dragend=${this.onDragEnd}\n      >\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* -------------------------------------------------------- Kanban Card Header */\n\nexport class UipKanbanCardHeader extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-card-header')\n  }\n\n  render() {\n    return html`<div part=\"base\" class=\"flex flex-col gap-1\"><slot></slot></div>`\n  }\n}\n\n/* --------------------------------------------------------- Kanban Card Title */\n\nexport class UipKanbanCardTitle extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-card-title')\n  }\n\n  render() {\n    return html`<p part=\"base\" class=\"text-foreground text-sm leading-snug font-medium\"><slot></slot></p>`\n  }\n}\n\n/* --------------------------------------------------- Kanban Card Description */\n\nexport class UipKanbanCardDescription extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-card-description')\n  }\n\n  render() {\n    return html`<p part=\"base\" class=\"text-muted-foreground line-clamp-2 text-xs\"><slot></slot></p>`\n  }\n}\n\n/* -------------------------------------------------------- Kanban Card Footer */\n\nexport class UipKanbanCardFooter extends LitElement {\n  static styles = [\n    tailwind,\n    css`\n      :host {\n        display: block;\n      }\n    `,\n  ]\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'kanban-card-footer')\n  }\n\n  render() {\n    return html`\n      <div part=\"base\" class=\"text-muted-foreground mt-1 flex items-center justify-between gap-2 pt-1 text-xs\">\n        <slot></slot>\n      </div>\n    `\n  }\n}\n\n/* -------------------------------------------------------------- Registration */\n\ncustomElements.get('uip-kanban') || customElements.define('uip-kanban', UipKanban)\ncustomElements.get('uip-kanban-board') || customElements.define('uip-kanban-board', UipKanbanBoard)\ncustomElements.get('uip-kanban-column') || customElements.define('uip-kanban-column', UipKanbanColumn)\ncustomElements.get('uip-kanban-column-header') || customElements.define('uip-kanban-column-header', UipKanbanColumnHeader)\ncustomElements.get('uip-kanban-column-dot') || customElements.define('uip-kanban-column-dot', UipKanbanColumnDot)\ncustomElements.get('uip-kanban-column-title') || customElements.define('uip-kanban-column-title', UipKanbanColumnTitle)\ncustomElements.get('uip-kanban-column-count') || customElements.define('uip-kanban-column-count', UipKanbanColumnCount)\ncustomElements.get('uip-kanban-column-add') || customElements.define('uip-kanban-column-add', UipKanbanColumnAdd)\ncustomElements.get('uip-kanban-column-body') || customElements.define('uip-kanban-column-body', UipKanbanColumnBody)\ncustomElements.get('uip-kanban-column-empty') || customElements.define('uip-kanban-column-empty', UipKanbanColumnEmpty)\ncustomElements.get('uip-kanban-card') || customElements.define('uip-kanban-card', UipKanbanCard)\ncustomElements.get('uip-kanban-card-header') || customElements.define('uip-kanban-card-header', UipKanbanCardHeader)\ncustomElements.get('uip-kanban-card-title') || customElements.define('uip-kanban-card-title', UipKanbanCardTitle)\ncustomElements.get('uip-kanban-card-description') || customElements.define('uip-kanban-card-description', UipKanbanCardDescription)\ncustomElements.get('uip-kanban-card-footer') || customElements.define('uip-kanban-card-footer', UipKanbanCardFooter)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-kanban': UipKanban\n    'uip-kanban-board': UipKanbanBoard\n    'uip-kanban-column': UipKanbanColumn\n    'uip-kanban-column-header': UipKanbanColumnHeader\n    'uip-kanban-column-dot': UipKanbanColumnDot\n    'uip-kanban-column-title': UipKanbanColumnTitle\n    'uip-kanban-column-count': UipKanbanColumnCount\n    'uip-kanban-column-add': UipKanbanColumnAdd\n    'uip-kanban-column-body': UipKanbanColumnBody\n    'uip-kanban-column-empty': UipKanbanColumnEmpty\n    'uip-kanban-card': UipKanbanCard\n    'uip-kanban-card-header': UipKanbanCardHeader\n    'uip-kanban-card-title': UipKanbanCardTitle\n    'uip-kanban-card-description': UipKanbanCardDescription\n    'uip-kanban-card-footer': UipKanbanCardFooter\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/kanban/kanban.ts"
    },
    {
      "path": "packages/elements-lit/src/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        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:file",
      "target": "src/uip/components/kanban/kanban.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
