{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "transfer",
  "title": "Transfer",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/transfer/Transfer.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, provide, ref, toRef } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport TransferList from './TransferList.vue'\nimport TransferOperation from './TransferOperation.vue'\nimport { TRANSFER_INJECTION_KEY, type TransferDragPayload, type TransferItem, type TransferSide } from './context'\n\nconst props = withDefaults(\n  defineProps<{\n    targetKeys?: string[]\n    dataSource: TransferItem[]\n    titles?: [string, string]\n    showSearch?: boolean\n    filterFn?: (query: string, item: TransferItem) => boolean\n    height?: number | string\n    pagination?: boolean | { pageSize: number }\n    oneWay?: boolean\n    disabled?: boolean\n    draggable?: boolean\n    selectable?: boolean\n    class?: HTMLAttributes['class']\n  }>(),\n  {\n    targetKeys: () => [],\n    titles: () => ['Source', 'Target'],\n    showSearch: false,\n    height: 320,\n    pagination: false,\n    oneWay: false,\n    disabled: false,\n    draggable: false,\n    selectable: true,\n  },\n)\n\nconst emits = defineEmits<{\n  (e: 'update:targetKeys', keys: string[]): void\n  (e: 'change', keys: string[], direction: 'left' | 'right', moved: string[]): void\n  (e: 'search', payload: { direction: 'left' | 'right'; query: string }): void\n  (e: 'select-change', payload: { left: string[]; right: string[] }): void\n}>()\n\nconst dataMap = computed(() => new Map(props.dataSource.map((i) => [i.key, i])))\n\nconst sourceItems = computed(() => props.dataSource.filter((i) => !props.targetKeys.includes(i.key)))\n\n// When draggable, target order follows targetKeys exactly so reorder persists.\n// Otherwise keep legacy dataSource ordering for backwards compat.\nconst targetItems = computed<TransferItem[]>(() => {\n  if (props.draggable) {\n    const out: TransferItem[] = []\n    for (const k of props.targetKeys) {\n      const item = dataMap.value.get(k)\n      if (item) out.push(item)\n    }\n    return out\n  }\n  return props.dataSource.filter((i) => props.targetKeys.includes(i.key))\n})\n\nconst selectedLeft = ref<string[]>([])\nconst selectedRight = ref<string[]>([])\n\nconst pageSize = computed<number | null>(() => {\n  if (props.pagination === false) return null\n  if (props.pagination === true) return 10\n  return props.pagination.pageSize\n})\n\nconst defaultFilter = (q: string, item: TransferItem) => item.label.toLowerCase().includes(q.toLowerCase())\n\nconst dragPayload = ref<TransferDragPayload | null>(null)\n\nfunction startDrag(payload: TransferDragPayload) {\n  dragPayload.value = payload\n}\n\nfunction endDrag() {\n  dragPayload.value = null\n}\n\nfunction drop(toSide: TransferSide, beforeKey: string | null) {\n  const payload = dragPayload.value\n  dragPayload.value = null\n  if (!payload || props.disabled) return\n  const keys = payload.keys.filter((k) => {\n    const item = dataMap.value.get(k)\n    return item && !item.disabled\n  })\n  if (keys.length === 0) return\n\n  // left → left: reorder source not supported (parent owns dataSource order). No-op.\n  if (payload.fromSide === 'left' && toSide === 'left') return\n\n  // right → left: remove from targetKeys (skip when oneWay).\n  if (payload.fromSide === 'right' && toSide === 'left') {\n    if (props.oneWay) return\n    const removeSet = new Set(keys)\n    const next = props.targetKeys.filter((k) => !removeSet.has(k))\n    selectedRight.value = selectedRight.value.filter((k) => !removeSet.has(k))\n    emits('update:targetKeys', next)\n    emits('change', next, 'left', keys)\n    emits('select-change', { left: selectedLeft.value, right: selectedRight.value })\n    return\n  }\n\n  // → right: insert (cross-list move) or reorder (within-target).\n  const movingSet = new Set(keys)\n  const without = props.targetKeys.filter((k) => !movingSet.has(k))\n  let insertAt = without.length\n  if (beforeKey != null) {\n    const idx = without.indexOf(beforeKey)\n    if (idx >= 0) insertAt = idx\n  }\n  const next = [...without.slice(0, insertAt), ...keys, ...without.slice(insertAt)]\n  if (payload.fromSide === 'left') {\n    const movedSet = new Set(keys)\n    selectedLeft.value = selectedLeft.value.filter((k) => !movedSet.has(k))\n    emits('update:targetKeys', next)\n    emits('change', next, 'right', keys)\n    emits('select-change', { left: selectedLeft.value, right: selectedRight.value })\n  } else {\n    // right → right: pure reorder, no change event (target set unchanged).\n    emits('update:targetKeys', next)\n  }\n}\n\nprovide(TRANSFER_INJECTION_KEY, {\n  disabled: toRef(props, 'disabled'),\n  showSearch: toRef(props, 'showSearch'),\n  height: toRef(props, 'height'),\n  pageSize,\n  filterFn: props.filterFn ?? defaultFilter,\n  draggable: toRef(props, 'draggable'),\n  selectable: toRef(props, 'selectable'),\n  oneWay: toRef(props, 'oneWay'),\n  dragPayload,\n  startDrag,\n  endDrag,\n  drop,\n})\n\nfunction onLeftSelected(keys: string[]) {\n  selectedLeft.value = keys\n  emits('select-change', { left: keys, right: selectedRight.value })\n}\n\nfunction onRightSelected(keys: string[]) {\n  selectedRight.value = keys\n  emits('select-change', { left: selectedLeft.value, right: keys })\n}\n\nfunction moveRight() {\n  if (selectedLeft.value.length === 0) return\n  const next = [...props.targetKeys, ...selectedLeft.value]\n  const moved = [...selectedLeft.value]\n  selectedLeft.value = []\n  emits('update:targetKeys', next)\n  emits('change', next, 'right', moved)\n  emits('select-change', { left: [], right: selectedRight.value })\n}\n\nfunction moveLeft() {\n  if (selectedRight.value.length === 0) return\n  const remove = new Set(selectedRight.value)\n  const next = props.targetKeys.filter((k) => !remove.has(k))\n  const moved = [...selectedRight.value]\n  selectedRight.value = []\n  emits('update:targetKeys', next)\n  emits('change', next, 'left', moved)\n  emits('select-change', { left: selectedLeft.value, right: [] })\n}\n</script>\n\n<template>\n  <div :class=\"cn('flex items-stretch gap-3', props.class)\" data-uipkge data-slot=\"transfer\">\n    <div class=\"min-w-0 flex-1\">\n      <TransferList\n        side=\"left\"\n        :title=\"titles[0]\"\n        :items=\"sourceItems\"\n        :selected=\"selectedLeft\"\n        @update:selected=\"onLeftSelected\"\n        @search=\"emits('search', { direction: 'left', query: $event })\"\n      >\n        <template v-if=\"$slots['footer-left']\" #footer>\n          <slot name=\"footer-left\" />\n        </template>\n      </TransferList>\n    </div>\n    <TransferOperation\n      :can-move-right=\"selectedLeft.length > 0 && !disabled\"\n      :can-move-left=\"selectedRight.length > 0 && !disabled\"\n      :one-way=\"oneWay\"\n      @move-right=\"moveRight\"\n      @move-left=\"moveLeft\"\n    />\n    <div class=\"min-w-0 flex-1\">\n      <TransferList\n        side=\"right\"\n        :title=\"titles[1]\"\n        :items=\"targetItems\"\n        :selected=\"selectedRight\"\n        @update:selected=\"onRightSelected\"\n        @search=\"emits('search', { direction: 'right', query: $event })\"\n      >\n        <template v-if=\"$slots['footer-right']\" #footer>\n          <slot name=\"footer-right\" />\n        </template>\n      </TransferList>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/transfer/Transfer.vue"
    },
    {
      "path": "packages/registry-vue/components/transfer/TransferList.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, inject, ref, watch } from 'vue'\nimport { GripVertical, Search } from 'lucide-vue-next'\nimport { Checkbox } from '@/components/ui/checkbox'\nimport { Input } from '@/components/ui/input'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { TRANSFER_INJECTION_KEY, type TransferItem, type TransferSide } from './context'\n\nconst props = defineProps<{\n  side: TransferSide\n  title: string\n  items: TransferItem[]\n  selected: string[]\n}>()\n\nconst emits = defineEmits<{\n  (e: 'update:selected', keys: string[]): void\n  (e: 'search', query: string): void\n}>()\n\nconst _maybeCtx = inject(TRANSFER_INJECTION_KEY, null)\nif (!_maybeCtx) throw new Error('TransferList must be used inside <Transfer>.')\nconst ctx: NonNullable<typeof _maybeCtx> = _maybeCtx\n\nconst query = ref('')\nconst page = ref(1)\n\nconst filtered = computed(() => {\n  if (!query.value) return props.items\n  return props.items.filter((i) => ctx.filterFn(query.value, i))\n})\n\nconst effectivePageSize = computed(() => ctx.pageSize.value ?? Math.max(1, filtered.value.length))\nconst totalPages = computed(() => Math.max(1, Math.ceil(filtered.value.length / effectivePageSize.value)))\n\nconst visible = computed(() => {\n  if (!ctx.pageSize.value) return filtered.value\n  const start = (page.value - 1) * effectivePageSize.value\n  return filtered.value.slice(start, start + effectivePageSize.value)\n})\n\nwatch(filtered, () => {\n  if (page.value > totalPages.value) page.value = totalPages.value\n})\n\nconst visibleEnabledKeys = computed(() => visible.value.filter((i) => !i.disabled).map((i) => i.key))\nconst selectedSet = computed(() => new Set(props.selected))\n\nconst visibleSelectedCount = computed(() => visibleEnabledKeys.value.filter((k) => selectedSet.value.has(k)).length)\n\nconst masterChecked = computed(\n  () => visibleEnabledKeys.value.length > 0 && visibleSelectedCount.value === visibleEnabledKeys.value.length,\n)\n\nconst masterIndeterminate = computed(\n  () => visibleSelectedCount.value > 0 && visibleSelectedCount.value < visibleEnabledKeys.value.length,\n)\n\nfunction toggleAll(checked: boolean) {\n  let next = [...props.selected]\n  if (checked) {\n    for (const k of visibleEnabledKeys.value) {\n      if (!selectedSet.value.has(k)) next.push(k)\n    }\n  } else {\n    next = next.filter((k) => !visibleEnabledKeys.value.includes(k))\n  }\n  emits('update:selected', next)\n}\n\nfunction toggleItem(item: TransferItem, checked: boolean) {\n  if (item.disabled || ctx.disabled.value) return\n  let next = [...props.selected]\n  if (checked) {\n    if (!next.includes(item.key)) next.push(item.key)\n  } else {\n    next = next.filter((k) => k !== item.key)\n  }\n  emits('update:selected', next)\n  lastAnchor.value = item.key\n}\n\nconst lastAnchor = ref<string | null>(null)\n\nfunction onRowClick(e: MouseEvent, item: TransferItem) {\n  if (item.disabled || ctx.disabled.value) return\n  // With checkbox visible, click toggles (matches checkbox UX).\n  if (ctx.selectable.value) {\n    toggleItem(item, !selectedSet.value.has(item.key))\n    return\n  }\n  // No checkbox: desktop list pattern — plain=replace, cmd/ctrl=toggle, shift=range.\n  const enabledKeys = visible.value.filter((i) => !i.disabled).map((i) => i.key)\n  if (e.shiftKey && lastAnchor.value && enabledKeys.includes(lastAnchor.value)) {\n    const start = enabledKeys.indexOf(lastAnchor.value)\n    const end = enabledKeys.indexOf(item.key)\n    const [lo, hi] = start < end ? [start, end] : [end, start]\n    emits('update:selected', enabledKeys.slice(lo, hi + 1))\n    return\n  }\n  if (e.metaKey || e.ctrlKey) {\n    let next = [...props.selected]\n    if (selectedSet.value.has(item.key)) next = next.filter((k) => k !== item.key)\n    else next.push(item.key)\n    emits('update:selected', next)\n    lastAnchor.value = item.key\n    return\n  }\n  emits('update:selected', [item.key])\n  lastAnchor.value = item.key\n}\n\nfunction onSearch(v: string) {\n  query.value = v\n  page.value = 1\n  emits('search', v)\n}\n\nfunction prevPage() {\n  if (page.value > 1) page.value--\n}\n\nfunction nextPage() {\n  if (page.value < totalPages.value) page.value++\n}\n\nconst heightStyle = computed(() => ({\n  height: typeof ctx.height.value === 'number' ? ctx.height.value + 'px' : ctx.height.value,\n}))\n\n// ----- DnD -----\n\nconst dropIndicator = ref<{ key: string; position: 'before' | 'after' } | null>(null)\nconst draggingKeys = ref<Set<string>>(new Set())\n\nconst isDropTarget = computed(() => {\n  const p = ctx.dragPayload.value\n  if (!p) return false\n  // left list rejects drops when oneWay\n  if (props.side === 'left' && ctx.oneWay.value && p.fromSide === 'right') return false\n  // left → left is a no-op (parent owns dataSource order)\n  if (props.side === 'left' && p.fromSide === 'left') return false\n  return true\n})\n\nfunction onItemDragStart(e: DragEvent, item: TransferItem) {\n  if (!ctx.draggable.value || item.disabled || ctx.disabled.value) {\n    e.preventDefault()\n    return\n  }\n  // If the dragged row is part of the current selection, drag the whole selection.\n  // Else, drag just this row (and clear selection visually for clarity).\n  const keys = selectedSet.value.has(item.key)\n    ? props.selected.filter((k) => {\n        const i = props.items.find((x) => x.key === k)\n        return i && !i.disabled\n      })\n    : [item.key]\n  draggingKeys.value = new Set(keys)\n  ctx.startDrag({ keys, fromSide: props.side })\n  if (e.dataTransfer) {\n    e.dataTransfer.effectAllowed = 'move'\n    // Required by Firefox to actually start the drag.\n    try {\n      e.dataTransfer.setData('text/plain', keys.join(','))\n    } catch {\n      /* noop */\n    }\n  }\n}\n\nfunction onItemDragEnd() {\n  draggingKeys.value = new Set()\n  dropIndicator.value = null\n  ctx.endDrag()\n}\n\nfunction onItemDragOver(e: DragEvent, item: TransferItem) {\n  if (!isDropTarget.value) return\n  e.preventDefault()\n  if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n  if (props.side !== 'right') {\n    // left list: no insertion indicator, drop just removes from target\n    return\n  }\n  const target = e.currentTarget as HTMLElement\n  const rect = target.getBoundingClientRect()\n  const after = e.clientY > rect.top + rect.height / 2\n  dropIndicator.value = { key: item.key, position: after ? 'after' : 'before' }\n}\n\nfunction onItemDrop(e: DragEvent, item: TransferItem) {\n  if (!isDropTarget.value) return\n  e.preventDefault()\n  e.stopPropagation()\n  if (props.side === 'right') {\n    const after = dropIndicator.value?.position === 'after'\n    const idx = props.items.findIndex((x) => x.key === item.key)\n    const beforeKey = after ? (props.items[idx + 1]?.key ?? null) : item.key\n    ctx.drop('right', beforeKey)\n  } else {\n    ctx.drop('left', null)\n  }\n  dropIndicator.value = null\n}\n\nfunction onListDragOver(e: DragEvent) {\n  if (!isDropTarget.value) return\n  e.preventDefault()\n  if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n}\n\nfunction onListDrop(e: DragEvent) {\n  if (!isDropTarget.value) return\n  e.preventDefault()\n  // Empty zone or below all items → append (right) / remove (left).\n  ctx.drop(props.side, null)\n  dropIndicator.value = null\n}\n\nfunction onListDragLeave(e: DragEvent) {\n  // Clear indicator only when leaving the list container, not when crossing item rows.\n  const related = e.relatedTarget as Node | null\n  const current = e.currentTarget as Node\n  if (!related || !current.contains(related)) {\n    dropIndicator.value = null\n  }\n}\n</script>\n\n<template>\n  <div\n    class=\"bg-card flex flex-col overflow-hidden rounded-md border transition-colors\"\n    :class=\"[ctx.dragPayload.value && isDropTarget && 'ring-ring/40 ring-1']\"\n  >\n    <div class=\"bg-muted/40 flex items-center justify-between gap-2 border-b px-3 py-2\">\n      <div class=\"flex min-w-0 items-center gap-2\">\n        <Checkbox\n          v-if=\"ctx.selectable.value\"\n          :model-value=\"masterIndeterminate ? 'indeterminate' : masterChecked\"\n          :disabled=\"ctx.disabled.value || visibleEnabledKeys.length === 0\"\n          @update:model-value=\"toggleAll($event === true)\"\n        />\n        <span class=\"truncate text-sm font-medium\">{{ title }}</span>\n      </div>\n      <span class=\"text-muted-foreground text-xs tabular-nums\"> {{ selected.length }}/{{ items.length }} </span>\n    </div>\n\n    <div v-if=\"ctx.showSearch.value\" class=\"border-b p-2\">\n      <div class=\"relative\">\n        <Search\n          class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2\"\n          aria-hidden=\"true\"\n        />\n        <Input\n          :model-value=\"query\"\n          placeholder=\"Search\"\n          class=\"h-8 pl-8\"\n          @update:model-value=\"(v) => onSearch(String(v ?? ''))\"\n        />\n      </div>\n    </div>\n\n    <ScrollArea\n      :style=\"heightStyle\"\n      class=\"flex-1\"\n      @dragover=\"onListDragOver\"\n      @drop=\"onListDrop\"\n      @dragleave=\"onListDragLeave\"\n    >\n      <ul role=\"listbox\" aria-multiselectable=\"true\" class=\"py-1\">\n        <li\n          v-for=\"item in visible\"\n          :key=\"item.key\"\n          role=\"option\"\n          :aria-selected=\"selectedSet.has(item.key)\"\n          :draggable=\"ctx.draggable.value && !item.disabled && !ctx.disabled.value\"\n          :class=\"[\n            'hover:bg-accent focus-visible:ring-ring relative flex min-h-11 cursor-pointer items-start gap-2 px-3 py-3 text-sm select-none focus-visible:ring-2 focus-visible:outline-none',\n            item.disabled && 'cursor-not-allowed opacity-50',\n            draggingKeys.has(item.key) && 'opacity-40',\n            !ctx.selectable.value && selectedSet.has(item.key) && 'bg-accent',\n          ]\"\n          @click=\"(e) => onRowClick(e, item)\"\n          @dragstart=\"(e) => onItemDragStart(e, item)\"\n          @dragend=\"onItemDragEnd\"\n          @dragover=\"(e) => onItemDragOver(e, item)\"\n          @drop=\"(e) => onItemDrop(e, item)\"\n        >\n          <span\n            v-if=\"\n              dropIndicator && dropIndicator.key === item.key && dropIndicator.position === 'before' && side === 'right'\n            \"\n            class=\"bg-primary pointer-events-none absolute -top-px right-2 left-2 h-0.5 rounded-full\"\n            aria-hidden=\"true\"\n          />\n          <span\n            v-if=\"\n              dropIndicator && dropIndicator.key === item.key && dropIndicator.position === 'after' && side === 'right'\n            \"\n            class=\"bg-primary pointer-events-none absolute right-2 -bottom-px left-2 h-0.5 rounded-full\"\n            aria-hidden=\"true\"\n          />\n          <Checkbox\n            v-if=\"ctx.selectable.value\"\n            :model-value=\"selectedSet.has(item.key)\"\n            :disabled=\"item.disabled || ctx.disabled.value\"\n            @update:model-value=\"toggleItem(item, $event === true)\"\n            @click.stop\n          />\n          <div class=\"min-w-0 flex-1\">\n            <div class=\"truncate\">{{ item.label }}</div>\n            <div v-if=\"item.description\" class=\"text-muted-foreground truncate text-xs\">\n              {{ item.description }}\n            </div>\n          </div>\n          <GripVertical\n            v-if=\"ctx.draggable.value && !item.disabled\"\n            class=\"text-muted-foreground/60 mt-0.5 size-3.5 shrink-0\"\n            aria-hidden=\"true\"\n          />\n        </li>\n        <li v-if=\"visible.length === 0\" class=\"text-muted-foreground px-3 py-6 text-center text-sm\">No items</li>\n      </ul>\n    </ScrollArea>\n\n    <div\n      v-if=\"ctx.pageSize.value && totalPages > 1\"\n      class=\"flex items-center justify-center gap-2 border-t p-2 text-xs\"\n    >\n      <button\n        type=\"button\"\n        class=\"hover:bg-accent focus-visible:ring-ring rounded px-2 py-1 focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50\"\n        :disabled=\"page <= 1\"\n        @click=\"prevPage\"\n      >\n        Prev\n      </button>\n      <span class=\"tabular-nums\">{{ page }} / {{ totalPages }}</span>\n      <button\n        type=\"button\"\n        class=\"hover:bg-accent focus-visible:ring-ring rounded px-2 py-1 focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50\"\n        :disabled=\"page >= totalPages\"\n        @click=\"nextPage\"\n      >\n        Next\n      </button>\n    </div>\n\n    <div v-if=\"$slots.footer\" class=\"border-t p-2\">\n      <slot name=\"footer\" />\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/transfer/TransferList.vue"
    },
    {
      "path": "packages/registry-vue/components/transfer/TransferOperation.vue",
      "content": "<script setup lang=\"ts\">\nimport { ChevronLeft, ChevronRight } from 'lucide-vue-next'\nimport { Button } from '@/components/ui/button'\n\ndefineProps<{\n  canMoveRight: boolean\n  canMoveLeft: boolean\n  oneWay?: boolean\n}>()\n\ndefineEmits<{\n  (e: 'move-right'): void\n  (e: 'move-left'): void\n}>()\n</script>\n\n<template>\n  <div class=\"flex flex-col items-center justify-center gap-2 px-2\">\n    <Button\n      size=\"icon-sm\"\n      variant=\"outline\"\n      :disabled=\"!canMoveRight\"\n      aria-label=\"Move selected to right\"\n      @click=\"$emit('move-right')\"\n    >\n      <ChevronRight aria-hidden=\"true\" />\n    </Button>\n    <Button\n      v-if=\"!oneWay\"\n      size=\"icon-sm\"\n      variant=\"outline\"\n      :disabled=\"!canMoveLeft\"\n      aria-label=\"Move selected to left\"\n      @click=\"$emit('move-left')\"\n    >\n      <ChevronLeft aria-hidden=\"true\" />\n    </Button>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/transfer/TransferOperation.vue"
    },
    {
      "path": "packages/registry-vue/components/transfer/context.ts",
      "content": "import type { InjectionKey, Ref } from 'vue'\n\nexport interface TransferItem {\n  key: string\n  label: string\n  description?: string\n  disabled?: boolean\n}\n\nexport type TransferSide = 'left' | 'right'\n\nexport interface TransferDragPayload {\n  keys: string[]\n  fromSide: TransferSide\n}\n\nexport interface TransferContext {\n  disabled: Ref<boolean>\n  showSearch: Ref<boolean>\n  height: Ref<number | string>\n  pageSize: Ref<number | null>\n  filterFn: (query: string, item: TransferItem) => boolean\n  draggable: Ref<boolean>\n  selectable: Ref<boolean>\n  oneWay: Ref<boolean>\n  dragPayload: Ref<TransferDragPayload | null>\n  startDrag: (payload: TransferDragPayload) => void\n  endDrag: () => void\n  drop: (toSide: TransferSide, beforeKey: string | null) => void\n}\n\nexport const TRANSFER_INJECTION_KEY: InjectionKey<TransferContext> = Symbol('uipkge-transfer')\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/transfer/context.ts"
    },
    {
      "path": "packages/registry-vue/components/transfer/index.ts",
      "content": "export type { TransferItem } from './context'\nexport { default as Transfer } from './Transfer.vue'\nexport { default as TransferList } from './TransferList.vue'\nexport { default as TransferOperation } from './TransferOperation.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/transfer/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/input.json",
    "https://uipkge.dev/r/vue/checkbox.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/scroll-area.json"
  ],
  "description": "Dual-list move-between control. Two columns plus a center pair of move buttons. Optional search, pagination, and one-way mode.",
  "categories": [
    "data-display"
  ]
}