{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "board",
  "title": "Board — compositional kanban / sortable-list primitive",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/board/Board.vue",
      "content": "<script setup lang=\"ts\">\nimport { provide, ref, toRef, type Ref } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { BOARD_CONTEXT, type BoardAcceptsFn, type BoardDensity, type BoardOrientation } from './context'\n\ninterface Props {\n  class?: HTMLAttributes['class']\n  orientation?: BoardOrientation\n  density?: BoardDensity\n  /** TransitionGroup name applied by BoardLaneBody. Defaults to `motion-list`. */\n  motion?: string\n  /** Predicate run per drop. Defaults to always-accept. */\n  accepts?: BoardAcceptsFn\n  /** Imperative move from a parent's useBoard composable. */\n  moveItem?: (itemId: string | string[], toLaneId: string, toIndex?: number) => void\n  /** External state — pass `state.draggingId` etc. from useBoard. */\n  draggingId?: string | null\n  draggingIds?: readonly string[]\n  dragOverLaneId?: string | null\n  justMovedId?: string | null\n  selectedIds?: ReadonlySet<string>\n  /** Optional toggle/clearSelection from useBoard so the primitive can\n   *  expose selection mutations through context (consumed by BoardCard\n   *  click handler). Both default to no-ops, so Board still works with\n   *  consumers that don't wire selection. */\n  toggleSelection?: (itemId: string, additive?: boolean) => void\n  clearSelection?: () => void\n  registerAllowedLanes?: (cardId: string, lanes: readonly string[] | undefined) => void\n  unregisterAllowedLanes?: (cardId: string) => void\n  registerLaneDisabled?: (laneId: string, disabled: boolean) => void\n  unregisterLaneDisabled?: (laneId: string) => void\n  isLaneAcceptingFor?: (laneId: string) => boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  orientation: 'horizontal',\n  density: 'default',\n  motion: 'motion-list',\n  accepts: () => () => true,\n  moveItem: () => () => {},\n  draggingIds: () => [] as readonly string[],\n  selectedIds: () => new Set<string>() as ReadonlySet<string>,\n  toggleSelection: () => () => {},\n  clearSelection: () => () => {},\n  registerAllowedLanes: () => () => {},\n  unregisterAllowedLanes: () => () => {},\n  registerLaneDisabled: () => () => {},\n  unregisterLaneDisabled: () => () => {},\n  isLaneAcceptingFor: () => () => true,\n})\n\nconst draggingIdRef = toRef(props, 'draggingId')\nconst draggingIdsRef = toRef(props, 'draggingIds')\nconst dragOverLaneIdRef = toRef(props, 'dragOverLaneId')\nconst justMovedIdRef = toRef(props, 'justMovedId')\nconst selectedIdsRef = toRef(props, 'selectedIds')\nconst laneIds = ref<symbol[]>([])\n\nprovide(BOARD_CONTEXT, {\n  orientation: toRef(props, 'orientation'),\n  density: toRef(props, 'density'),\n  motion: toRef(props, 'motion'),\n  draggingId: draggingIdRef as unknown as Ref<string | null>,\n  draggingIds: draggingIdsRef as unknown as Ref<readonly string[]>,\n  dragOverLaneId: dragOverLaneIdRef as unknown as Ref<string | null>,\n  justMovedId: justMovedIdRef as unknown as Ref<string | null>,\n  selectedIds: selectedIdsRef as unknown as Ref<ReadonlySet<string>>,\n  accepts: toRef(props, 'accepts'),\n  moveItem: (...args) => props.moveItem!(...args),\n  toggleSelection: (...args) => props.toggleSelection!(...args),\n  clearSelection: () => props.clearSelection!(),\n  registerAllowedLanes: (...args) => props.registerAllowedLanes!(...args),\n  unregisterAllowedLanes: (...args) => props.unregisterAllowedLanes!(...args),\n  registerLaneDisabled: (...args) => props.registerLaneDisabled!(...args),\n  unregisterLaneDisabled: (...args) => props.unregisterLaneDisabled!(...args),\n  isLaneAcceptingFor: (laneId) => props.isLaneAcceptingFor!(laneId),\n  laneIds,\n  registerLane: (id) => {\n    if (!laneIds.value.includes(id)) laneIds.value.push(id)\n  },\n  unregisterLane: (id) => {\n    laneIds.value = laneIds.value.filter((i) => i !== id)\n  },\n})\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"board\" :data-orientation=\"orientation\" :class=\"cn('w-full', props.class)\">\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/board/Board.vue"
    },
    {
      "path": "packages/registry-vue/components/board/BoardLane.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, inject, onBeforeUnmount, onMounted, provide, toRef, watch } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { BOARD_CONTEXT, BOARD_LANE_CONTEXT } from './context'\nimport { boardLaneVariants } from './board.variants'\n\ninterface Props {\n  id: string\n  class?: HTMLAttributes['class']\n  tone?: 'default' | 'plain'\n  /** Disable drops on this lane. Cards inside still render and stay\n   *  draggable; only the drop target is inert + visually dimmed. */\n  disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), { tone: 'default', disabled: false })\n\nconst board = inject(BOARD_CONTEXT, null)\nif (!board) {\n  throw new Error('<BoardLane> must be a descendant of <Board>.')\n}\n\nconst _laneSymbol = Symbol('BoardLane')\nonMounted(() => {\n  board.registerLane(_laneSymbol)\n  board.registerLaneDisabled(props.id, props.disabled)\n})\nonBeforeUnmount(() => {\n  board.unregisterLane(_laneSymbol)\n  board.unregisterLaneDisabled(props.id)\n})\n\n// Keep the disabled registry in sync when the prop changes mid-flight.\nwatch(\n  () => props.disabled,\n  (v) => board.registerLaneDisabled(props.id, v),\n)\nwatch(\n  () => props.id,\n  (newId, oldId) => {\n    board.unregisterLaneDisabled(oldId)\n    board.registerLaneDisabled(newId, props.disabled)\n  },\n)\n\nconst isDragOver = computed(() => board.dragOverLaneId.value === props.id)\nconst isAccepting = computed(() => {\n  if (!board.draggingId.value) return false\n  if (props.disabled) return false\n  return board.isLaneAcceptingFor(props.id)\n})\n\nprovide(BOARD_LANE_CONTEXT, {\n  laneId: toRef(props, 'id'),\n  isDragOver,\n  isAccepting,\n  disabled: toRef(props, 'disabled'),\n})\n\nconst state = computed<'idle' | 'over' | 'rejecting'>(() => {\n  if (!isDragOver.value) return 'idle'\n  return isAccepting.value ? 'over' : 'rejecting'\n})\n\nconst emits = defineEmits<{\n  dragover: [e: DragEvent]\n  drop: [e: DragEvent]\n  dragleave: []\n}>()\n\nfunction onDragOver(e: DragEvent) {\n  // Emit unconditionally so the composable can set dragOverLaneId\n  // (drives the rejecting-ring visual). The composable's isAllowed\n  // checks the disabled-lane registry and refuses preventDefault when\n  // it should — the browser's own refuse-to-drop semantics + our\n  // rejecting visual cover the rest.\n  emits('dragover', e)\n}\nfunction onDrop(e: DragEvent) {\n  if (props.disabled) {\n    e.preventDefault()\n    return\n  }\n  emits('drop', e)\n}\nfunction onDragLeave() {\n  emits('dragleave')\n}\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"board-lane\"\n    data-board-lane\n    :data-lane-id=\"id\"\n    :data-state=\"state\"\n    :data-disabled=\"disabled || undefined\"\n    :aria-disabled=\"disabled || undefined\"\n    :class=\"\n      cn(\n        boardLaneVariants({ tone, state }),\n        // Disabled lane dims only the lane chrome (border, background,\n        // header). Cards inside stay legible — the rejection signal is\n        // carried by the no-drop cursor + the missing accept-ring.\n        disabled && 'opacity-80 [&>[data-slot=board-lane-header]]:opacity-60',\n        props.class,\n      )\n    \"\n    @dragover=\"onDragOver\"\n    @drop=\"onDrop\"\n    @dragleave=\"onDragLeave\"\n  >\n    <slot :is-drag-over=\"isDragOver\" :is-accepting=\"isAccepting\" :disabled=\"disabled\" />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/board/BoardLane.vue"
    },
    {
      "path": "packages/registry-vue/components/board/BoardLaneHeader.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"board-lane-header\" :class=\"cn('flex items-center justify-between gap-2', props.class)\">\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/board/BoardLaneHeader.vue"
    },
    {
      "path": "packages/registry-vue/components/board/BoardLaneBody.vue",
      "content": "<script setup lang=\"ts\">\nimport { inject } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { BOARD_CONTEXT } from './context'\n\ninterface Props {\n  class?: HTMLAttributes['class']\n  /** Override the TransitionGroup name. Defaults to the board-level motion preset. */\n  motion?: string\n}\n\nconst props = defineProps<Props>()\n\nconst board = inject(BOARD_CONTEXT, null)\nconst motionName = () => props.motion ?? board?.motion.value ?? 'motion-list'\n</script>\n\n<template>\n  <!-- Inner padding (py-1 / px-0.5) reserves breathing room for the\n       per-card hover-lift (-translate-y-0.5), the focus / drag / moved\n       rings (ring-2 + ring-offset-1 ≈ 3px outward), and the hover\n       shadow halo. Without it, the first / last cards' hover state\n       crops against the overflow-y-auto edge. pr-1 still wins on the\n       right so the thin scrollbar has a gutter. -->\n  <div\n    data-uipkge\n    data-slot=\"board-lane-body\"\n    :class=\"\n      cn('flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-0.5 py-1 pr-1 [scrollbar-width:thin]', props.class)\n    \"\n  >\n    <!-- ClientOnly: Vue 3 TransitionGroup emits Fragment vnodes on the\n         server and proper DOM elements on the client, which trips a\n         hydration mismatch on every card slot. Render a plain div on\n         SSR (same shape, no animations — which the client can't see\n         pre-hydration anyway) and swap to TransitionGroup on mount. -->\n    <ClientOnly>\n      <TransitionGroup :name=\"motionName()\" tag=\"div\" class=\"relative flex flex-col gap-2\">\n        <slot />\n      </TransitionGroup>\n      <template #fallback>\n        <div class=\"relative flex flex-col gap-2\">\n          <slot />\n        </div>\n      </template>\n    </ClientOnly>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/board/BoardLaneBody.vue"
    },
    {
      "path": "packages/registry-vue/components/board/BoardLaneEmpty.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n  /** Show only when this is true (consumer wires from `lane.length === 0`). */\n  when?: boolean\n}>()\n</script>\n\n<template>\n  <div\n    v-if=\"when !== false\"\n    data-uipkge\n    data-slot=\"board-lane-empty\"\n    :class=\"\n      cn(\n        'text-muted-foreground/70 border-border/60 rounded-lg border border-dashed py-6 text-center text-xs',\n        props.class,\n      )\n    \"\n  >\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/board/BoardLaneEmpty.vue"
    },
    {
      "path": "packages/registry-vue/components/board/BoardCard.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, inject, onBeforeUnmount, onMounted, provide, ref, toRef, watch } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { BOARD_CARD_CONTEXT, BOARD_CONTEXT, BOARD_LANE_CONTEXT } from './context'\nimport { boardCardVariants } from './board.variants'\n\ninterface Props {\n  id: string\n  class?: HTMLAttributes['class']\n  /** Disable keyboard grab (e.g. for read-only boards). Default: enabled. */\n  keyboardDraggable?: boolean\n  /** Disable the card entirely — non-draggable, non-clickable, dimmed.\n   *  Use for cards locked by a workflow rule or a server policy. */\n  disabled?: boolean\n  /** Per-card allow-list. When set, the card can only be dropped into\n   *  these lane ids; useBoard rejects any other target silently. Omit\n   *  to allow every lane the global `accepts` predicate permits. */\n  allowedLanes?: readonly string[]\n  /** Show the click-to-select chrome (ring + cmd/shift-click multi-select).\n   *  Default true. Turn off for read-only or single-tap-to-open boards. */\n  selectable?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  keyboardDraggable: true,\n  disabled: false,\n  selectable: true,\n})\n\nconst board = inject(BOARD_CONTEXT, null)\nconst lane = inject(BOARD_LANE_CONTEXT, null)\nif (!board || !lane) {\n  throw new Error('<BoardCard> must be a descendant of <Board> and <BoardLane>.')\n}\n\nconst emits = defineEmits<{\n  dragstart: [e: DragEvent]\n  dragend: []\n  click: [e: MouseEvent]\n}>()\n\nconst isKeyboardGrabbed = ref(false)\n// Either source of truth wins — `draggingIds` is the multi-select-aware\n// list, `draggingId` is the single-anchor (backward-compat for consumers\n// that don't pass the multi state through). Keyboard-grabbed adds the\n// same visual without touching parent state.\nconst isDragging = computed(\n  () => board.draggingIds.value.includes(props.id) || board.draggingId.value === props.id || isKeyboardGrabbed.value,\n)\nconst isJustMoved = computed(() => board.justMovedId.value === props.id)\nconst isSelected = computed(() => board.selectedIds.value.has(props.id))\n\nprovide(BOARD_CARD_CONTEXT, {\n  cardId: toRef(props, 'id'),\n  laneId: lane.laneId,\n  isDragging,\n  isJustMoved,\n  isSelected,\n  disabled: toRef(props, 'disabled'),\n})\n\n// Per-card allowed-lanes registry. Re-run if the id OR the list changes.\nonMounted(() => board.registerAllowedLanes(props.id, props.allowedLanes))\nonBeforeUnmount(() => board.unregisterAllowedLanes(props.id))\nwatch(\n  () => [props.id, props.allowedLanes] as const,\n  ([id, lanes], _prev) => {\n    const prevId = _prev?.[0]\n    if (prevId && prevId !== id) board.unregisterAllowedLanes(prevId)\n    board.registerAllowedLanes(id, lanes)\n  },\n  { deep: true },\n)\n\nconst state = computed<'idle' | 'dragging' | 'moved'>(() => {\n  if (isDragging.value) return 'dragging'\n  if (isJustMoved.value) return 'moved'\n  return 'idle'\n})\n\nconst cardEl = ref<HTMLButtonElement | null>(null)\n\nfunction onKeydown(e: KeyboardEvent) {\n  if (props.disabled) return\n  // Enter activates the card (default action) — emits @click for the\n  // consumer to open a detail panel / navigate / etc. Native <button>\n  // gets this for free; we re-emit because the card root is a\n  // <div role=\"button\"> (chosen so consumers can nest <button>/links\n  // inside the card without violating \"no interactive content in a\n  // button\" — see commit context). The event still surfaces through\n  // the standard @click slot.\n  if (e.key === 'Enter') {\n    e.preventDefault()\n    emits('click', new MouseEvent('click', { bubbles: true, cancelable: true }))\n    return\n  }\n  if (!props.keyboardDraggable) return\n  if (e.key === ' ') {\n    e.preventDefault()\n    isKeyboardGrabbed.value = !isKeyboardGrabbed.value\n    return\n  }\n  if (e.key === 'Escape' && isKeyboardGrabbed.value) {\n    e.preventDefault()\n    isKeyboardGrabbed.value = false\n    return\n  }\n  if (!isKeyboardGrabbed.value) return\n  if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {\n    e.preventDefault()\n    const allLanes = Array.from(document.querySelectorAll<HTMLElement>('[data-board-lane]')).filter(\n      (el) => !el.hasAttribute('data-disabled'),\n    )\n    const currentIdx = allLanes.findIndex((el) => el.dataset.laneId === lane.laneId.value)\n    if (currentIdx === -1 || allLanes.length === 0) return\n    const delta = e.key === 'ArrowLeft' ? -1 : 1\n    const nextLane = allLanes[(currentIdx + delta + allLanes.length) % allLanes.length]\n    if (nextLane?.dataset.laneId) {\n      board.moveItem(props.id, nextLane.dataset.laneId)\n      requestAnimationFrame(() => {\n        const moved = document.querySelector<HTMLElement>(`[data-board-card-id=\"${props.id}\"]`)\n        moved?.focus()\n      })\n    }\n    return\n  }\n  if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {\n    e.preventDefault()\n    const laneEl = cardEl.value?.closest('[data-board-lane]')\n    if (!laneEl) return\n    const cards = Array.from(laneEl.querySelectorAll<HTMLElement>('[data-board-card]'))\n    const currentIdx = cards.findIndex((el) => el.dataset.boardCardId === props.id)\n    if (currentIdx === -1) return\n    const delta = e.key === 'ArrowUp' ? -1 : 1\n    const targetIdx = Math.max(0, Math.min(cards.length - 1, currentIdx + delta))\n    if (targetIdx !== currentIdx) board.moveItem(props.id, lane.laneId.value, targetIdx)\n  }\n}\n\nfunction onDragStart(e: DragEvent) {\n  if (props.disabled) {\n    e.preventDefault()\n    return\n  }\n  emits('dragstart', e)\n}\nfunction onDragEnd() {\n  emits('dragend')\n}\nfunction onClick(e: MouseEvent) {\n  if (props.disabled) {\n    e.preventDefault()\n    return\n  }\n  // Cmd/Ctrl/Shift+click toggles the selection (multi-select for drag);\n  // plain click clears any selection and just emits to the consumer\n  // (typically opens a detail Sheet).\n  if (props.selectable && (e.metaKey || e.ctrlKey || e.shiftKey)) {\n    e.preventDefault()\n    board.toggleSelection(props.id, true)\n    return\n  }\n  if (board.draggingIds.value.includes(props.id)) return\n  if (board.selectedIds.value.size > 0) board.clearSelection()\n  emits('click', e)\n}\n</script>\n\n<template>\n  <!-- Root is `<div role=\"button\">` rather than `<button type=\"button\">`\n       so consumers can nest interactive content (buttons, links, menus)\n       inside cards. The HTML5 spec forbids interactive content inside a\n       <button>; browsers silently de-nest the inner element which\n       breaks its events. Enter is wired manually in onKeydown to match\n       the native button default-action behaviour. -->\n  <div\n    ref=\"cardEl\"\n    role=\"button\"\n    data-uipkge\n    data-slot=\"board-card\"\n    data-board-card\n    :data-board-card-id=\"id\"\n    :data-state=\"state\"\n    :data-selected=\"isSelected || undefined\"\n    :data-disabled=\"disabled || undefined\"\n    :aria-disabled=\"disabled || undefined\"\n    :aria-pressed=\"selectable ? isSelected : undefined\"\n    :class=\"\n      cn(\n        boardCardVariants({ state }),\n        isSelected && 'ring-primary/60 ring-offset-background ring-2 ring-offset-1',\n        disabled && 'pointer-events-none cursor-not-allowed opacity-50 shadow-none grayscale hover:translate-y-0',\n        props.class,\n      )\n    \"\n    :draggable=\"disabled ? false : true\"\n    :tabindex=\"disabled ? -1 : 0\"\n    :aria-roledescription=\"keyboardDraggable && !disabled ? 'draggable card' : undefined\"\n    @click=\"onClick\"\n    @keydown=\"onKeydown\"\n    @dragstart=\"onDragStart\"\n    @dragend=\"onDragEnd\"\n  >\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/board/BoardCard.vue"
    },
    {
      "path": "packages/registry-vue/components/board/board.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file so consuming SFCs avoid the\n * circular-dep trap when importing from `index.ts`. Same pattern as\n * `timeline.variants.ts` / `card.variants.ts`.\n */\n\nexport const boardLaneVariants = cva(\n  'flex min-h-0 flex-col gap-3 rounded-xl border p-3 motion-safe:transition-[background-color,border-color,box-shadow] motion-safe:duration-200 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)]',\n  {\n    variants: {\n      tone: {\n        default: 'bg-muted/30',\n        plain: 'bg-transparent',\n      },\n      // Visual weight of `over` and `rejecting` deliberately matches so\n      // the eye reads them as the same kind of signal (drop intent),\n      // differing only in tone (primary = OK, destructive = NO).\n      state: {\n        idle: '',\n        over: 'border-primary/60 bg-primary/5 ring-2 ring-primary/30 ring-offset-1 ring-offset-background',\n        rejecting:\n          'border-destructive/40 bg-destructive/5 ring-2 ring-destructive/30 ring-offset-1 ring-offset-background',\n      },\n    },\n    defaultVariants: { tone: 'default', state: 'idle' },\n  },\n)\n\nexport const boardCardVariants = cva(\n  'bg-card hover:border-primary/40 group block w-full cursor-grab rounded-md border p-2 text-left outline-none motion-safe:transition-[transform,box-shadow,opacity,border-color] motion-safe:duration-200 motion-safe:ease-[cubic-bezier(0.16,1,0.3,1)] focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-primary/30 active:cursor-grabbing',\n  {\n    variants: {\n      state: {\n        idle: 'shadow-sm motion-safe:hover:-translate-y-0.5 hover:shadow-md',\n        dragging: 'scale-95 opacity-40 shadow-none',\n        moved: 'shadow-sm ring-2 ring-primary/50 ring-offset-1 ring-offset-background',\n      },\n    },\n    defaultVariants: { state: 'idle' },\n  },\n)\n\nexport type BoardLaneVariantsProps = VariantProps<typeof boardLaneVariants>\nexport type BoardCardVariantsProps = VariantProps<typeof boardCardVariants>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/board/board.variants.ts"
    },
    {
      "path": "packages/registry-vue/components/board/context.ts",
      "content": "import type { InjectionKey, Ref } from 'vue'\n\nexport type BoardOrientation = 'horizontal' | 'vertical'\nexport type BoardDensity = 'compact' | 'default' | 'comfortable'\n\n/** Drop event emitted by useBoard handlers. */\nexport interface BoardDropEvent {\n  /** First (anchor) item moved. For multi-item drops, see `itemIds`. */\n  itemId: string\n  /** All items moved in this drop, in their final visual order. */\n  itemIds: string[]\n  from: string\n  to: string\n  /** Insertion index of the first item in the target lane. */\n  index: number\n}\n\n/** Predicate consumers pass to control which items each lane accepts. */\nexport type BoardAcceptsFn = (itemId: string, fromLaneId: string, toLaneId: string) => boolean\n\nexport interface BoardContext {\n  orientation: Ref<BoardOrientation>\n  density: Ref<BoardDensity>\n  /** TransitionGroup name. Defaults to `motion-list` (the @uipkge motion preset). */\n  motion: Ref<string>\n  /** Currently-grabbed primary card id (pointer drag OR keyboard grab). */\n  draggingId: Ref<string | null>\n  /** All cards being dragged this turn — usually `[draggingId]`, but\n   *  expands to the full selection when the user grabs one of a multi-\n   *  selected set. Lanes read this to compute drop math (the dragged\n   *  cards are excluded from the insertion-index walk). */\n  draggingIds: Ref<readonly string[]>\n  /** Lane currently under the pointer/keyboard cursor. */\n  dragOverLaneId: Ref<string | null>\n  /** Card that just landed — used by consumers for a momentary highlight. */\n  justMovedId: Ref<string | null>\n  /** Multi-select set. Click-and-drag any selected card moves the whole\n   *  set; click a non-selected card to drag that one alone. */\n  selectedIds: Ref<ReadonlySet<string>>\n  /** Predicate run by lanes; defaults to always-accept. */\n  accepts: Ref<BoardAcceptsFn>\n  /** Imperative move — used by keyboard handlers + external callers. */\n  moveItem: (itemId: string | string[], toLaneId: string, toIndex?: number) => void\n  /** Toggle one item in the selection set (or replace it if `additive` is false). */\n  toggleSelection: (itemId: string, additive?: boolean) => void\n  /** Drop the entire selection. */\n  clearSelection: () => void\n  /** Per-card allow-list registry. BoardCard registers its own `allowedLanes`\n   *  on mount; lanes consult this to short-circuit rejected drops without\n   *  the consumer having to encode the rule inside `accepts`. */\n  registerAllowedLanes: (cardId: string, lanes: readonly string[] | undefined) => void\n  unregisterAllowedLanes: (cardId: string) => void\n  /** Whether the lane's drops are accepted for the dragging item, given\n   *  the current `accepts` + per-card allowedLanes + lane disabled state. */\n  isLaneAcceptingFor: (laneId: string) => boolean\n  /** Lane registration (mirrors Timeline pattern). */\n  laneIds: Ref<symbol[]>\n  registerLane: (id: symbol) => void\n  unregisterLane: (id: symbol) => void\n  /** Lane disabled registry — lanes call register/unregister; useBoard +\n   *  isLaneAcceptingFor read from it. */\n  registerLaneDisabled: (laneId: string, disabled: boolean) => void\n  unregisterLaneDisabled: (laneId: string) => void\n}\n\nexport const BOARD_CONTEXT: InjectionKey<BoardContext> = Symbol('BoardContext')\n\nexport interface BoardLaneContext {\n  laneId: Ref<string>\n  isDragOver: Ref<boolean>\n  isAccepting: Ref<boolean>\n  disabled: Ref<boolean>\n}\n\nexport const BOARD_LANE_CONTEXT: InjectionKey<BoardLaneContext> = Symbol('BoardLaneContext')\n\nexport interface BoardCardContext {\n  cardId: Ref<string>\n  laneId: Ref<string>\n  isDragging: Ref<boolean>\n  isJustMoved: Ref<boolean>\n  isSelected: Ref<boolean>\n  disabled: Ref<boolean>\n}\n\nexport const BOARD_CARD_CONTEXT: InjectionKey<BoardCardContext> = Symbol('BoardCardContext')\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/board/context.ts"
    },
    {
      "path": "packages/registry-vue/components/board/index.ts",
      "content": "export { default as Board } from './Board.vue'\nexport { default as BoardLane } from './BoardLane.vue'\nexport { default as BoardLaneHeader } from './BoardLaneHeader.vue'\nexport { default as BoardLaneBody } from './BoardLaneBody.vue'\nexport { default as BoardLaneEmpty } from './BoardLaneEmpty.vue'\nexport { default as BoardCard } from './BoardCard.vue'\n\nexport {\n  BOARD_CONTEXT,\n  BOARD_LANE_CONTEXT,\n  BOARD_CARD_CONTEXT,\n  type BoardAcceptsFn,\n  type BoardContext,\n  type BoardCardContext,\n  type BoardDensity,\n  type BoardDropEvent,\n  type BoardLaneContext,\n  type BoardOrientation,\n} from './context'\n\nexport {\n  boardCardVariants,\n  boardLaneVariants,\n  type BoardCardVariantsProps,\n  type BoardLaneVariantsProps,\n} from './board.variants'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/board/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/motion-list.json"
  ],
  "description": "Six small composable components for any board / kanban / sortable-list surface — opinionated about drop targeting and animation, agnostic about layout, data, and chrome. Drop `<Board>` around a grid of `<BoardLane>` columns; slot `<BoardLaneHeader>`, `<BoardLaneBody>`, `<BoardLaneEmpty>`, and `<BoardCard>` items inside each lane. State lives in the sibling `useBoard()` hook (insertion-index drop math, keyboard a11y, accept predicate). Animation comes from the `motion-list` motion preset by default — enter / leave / move all share one settle curve so a card travelling between two lanes reads as one continuous motion. Mirrors the Timeline / Card sub-component pattern (three-level context injection: board → lane → card). The current monolithic `@uipkge/kanban-board` block can be rebuilt on top of this primitive — Board is the layer underneath, kanban-board is one opinionated assembly.",
  "categories": [
    "data",
    "layout"
  ]
}