{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "transfer",
  "title": "Transfer",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/transfer/transfer.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { ChevronLeft, ChevronRight, GripVertical, Search } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/components/ui/button'\nimport { Checkbox } from '@/components/ui/checkbox'\nimport { Input } from '@/components/ui/input'\nimport { ScrollArea } from '@/components/ui/scroll-area'\n\nexport interface TransferItem {\n  key: string\n  label: string\n  description?: string\n  disabled?: boolean\n}\n\nexport type TransferSide = 'left' | 'right'\n\ninterface TransferDragPayload {\n  keys: string[]\n  fromSide: TransferSide\n}\n\ninterface TransferContextValue {\n  disabled: boolean\n  showSearch: boolean\n  height: number | string\n  pageSize: number | null\n  filterFn: (query: string, item: TransferItem) => boolean\n  draggable: boolean\n  selectable: boolean\n  oneWay: boolean\n  dragPayload: TransferDragPayload | null\n  startDrag: (payload: TransferDragPayload) => void\n  endDrag: () => void\n  drop: (toSide: TransferSide, beforeKey: string | null) => void\n}\n\nconst TransferContext = React.createContext<TransferContextValue | null>(null)\n\nfunction useTransferContext(): TransferContextValue {\n  const ctx = React.useContext(TransferContext)\n  if (!ctx) throw new Error('TransferList must be used inside <Transfer>.')\n  return ctx\n}\n\nexport interface TransferProps {\n  targetKeys?: string[]\n  /** Uncontrolled initial target keys. */\n  defaultTargetKeys?: 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  className?: string\n  onTargetKeysChange?: (keys: string[]) => void\n  onChange?: (keys: string[], direction: 'left' | 'right', moved: string[]) => void\n  onSearch?: (payload: { direction: 'left' | 'right'; query: string }) => void\n  onSelectChange?: (payload: { left: string[]; right: string[] }) => void\n  /** Footer rendered under the left list. */\n  footerLeft?: React.ReactNode\n  /** Footer rendered under the right list. */\n  footerRight?: React.ReactNode\n}\n\nconst defaultFilter = (q: string, item: TransferItem) => item.label.toLowerCase().includes(q.toLowerCase())\n\nconst Transfer = React.forwardRef<HTMLDivElement, TransferProps>(\n  (\n    {\n      targetKeys,\n      defaultTargetKeys,\n      dataSource,\n      titles = ['Source', 'Target'],\n      showSearch = false,\n      filterFn,\n      height = 320,\n      pagination = false,\n      oneWay = false,\n      disabled = false,\n      draggable = false,\n      selectable = true,\n      className,\n      onTargetKeysChange,\n      onChange,\n      onSearch,\n      onSelectChange,\n      footerLeft,\n      footerRight,\n    },\n    ref,\n  ) => {\n    const isControlled = targetKeys !== undefined\n    const [internalKeys, setInternalKeys] = React.useState<string[]>(defaultTargetKeys ?? [])\n    const resolvedTargetKeys = isControlled ? targetKeys : internalKeys\n\n    const setTargetKeys = React.useCallback(\n      (next: string[]) => {\n        if (!isControlled) setInternalKeys(next)\n        onTargetKeysChange?.(next)\n      },\n      [isControlled, onTargetKeysChange],\n    )\n\n    const [selectedLeft, setSelectedLeft] = React.useState<string[]>([])\n    const [selectedRight, setSelectedRight] = React.useState<string[]>([])\n    const [dragPayload, setDragPayload] = React.useState<TransferDragPayload | null>(null)\n\n    const dataMap = React.useMemo(() => new Map(dataSource.map((i) => [i.key, i])), [dataSource])\n\n    const sourceItems = React.useMemo(\n      () => dataSource.filter((i) => !resolvedTargetKeys.includes(i.key)),\n      [dataSource, resolvedTargetKeys],\n    )\n\n    // When draggable, target order follows targetKeys exactly so reorder persists.\n    // Otherwise keep legacy dataSource ordering for backwards compat.\n    const targetItems = React.useMemo<TransferItem[]>(() => {\n      if (draggable) {\n        const out: TransferItem[] = []\n        for (const k of resolvedTargetKeys) {\n          const item = dataMap.get(k)\n          if (item) out.push(item)\n        }\n        return out\n      }\n      return dataSource.filter((i) => resolvedTargetKeys.includes(i.key))\n    }, [draggable, resolvedTargetKeys, dataMap, dataSource])\n\n    const pageSize = React.useMemo<number | null>(() => {\n      if (pagination === false) return null\n      if (pagination === true) return 10\n      return pagination.pageSize\n    }, [pagination])\n\n    const startDrag = React.useCallback((payload: TransferDragPayload) => {\n      setDragPayload(payload)\n    }, [])\n\n    const endDrag = React.useCallback(() => {\n      setDragPayload(null)\n    }, [])\n\n    // Refs so the stable `drop` callback always reads current values.\n    const stateRef = React.useRef({ resolvedTargetKeys, dragPayload, selectedLeft, selectedRight, dataMap, disabled, oneWay })\n    stateRef.current = { resolvedTargetKeys, dragPayload, selectedLeft, selectedRight, dataMap, disabled, oneWay }\n\n    const drop = React.useCallback(\n      (toSide: TransferSide, beforeKey: string | null) => {\n        const s = stateRef.current\n        const payload = s.dragPayload\n        setDragPayload(null)\n        if (!payload || s.disabled) return\n        const keys = payload.keys.filter((k) => {\n          const item = s.dataMap.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 (s.oneWay) return\n          const removeSet = new Set(keys)\n          const next = s.resolvedTargetKeys.filter((k) => !removeSet.has(k))\n          const nextRight = s.selectedRight.filter((k) => !removeSet.has(k))\n          setSelectedRight(nextRight)\n          setTargetKeys(next)\n          onChange?.(next, 'left', keys)\n          onSelectChange?.({ left: s.selectedLeft, right: nextRight })\n          return\n        }\n\n        // → right: insert (cross-list move) or reorder (within-target).\n        const movingSet = new Set(keys)\n        const without = s.resolvedTargetKeys.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          const nextLeft = s.selectedLeft.filter((k) => !movedSet.has(k))\n          setSelectedLeft(nextLeft)\n          setTargetKeys(next)\n          onChange?.(next, 'right', keys)\n          onSelectChange?.({ left: nextLeft, right: s.selectedRight })\n        } else {\n          // right → right: pure reorder, no change event (target set unchanged).\n          setTargetKeys(next)\n        }\n      },\n      [setTargetKeys, onChange, onSelectChange],\n    )\n\n    const contextValue = React.useMemo<TransferContextValue>(\n      () => ({\n        disabled,\n        showSearch,\n        height,\n        pageSize,\n        filterFn: filterFn ?? defaultFilter,\n        draggable,\n        selectable,\n        oneWay,\n        dragPayload,\n        startDrag,\n        endDrag,\n        drop,\n      }),\n      [disabled, showSearch, height, pageSize, filterFn, draggable, selectable, oneWay, dragPayload, startDrag, endDrag, drop],\n    )\n\n    function onLeftSelected(keys: string[]) {\n      setSelectedLeft(keys)\n      onSelectChange?.({ left: keys, right: selectedRight })\n    }\n\n    function onRightSelected(keys: string[]) {\n      setSelectedRight(keys)\n      onSelectChange?.({ left: selectedLeft, right: keys })\n    }\n\n    function moveRight() {\n      if (selectedLeft.length === 0) return\n      const next = [...resolvedTargetKeys, ...selectedLeft]\n      const moved = [...selectedLeft]\n      setSelectedLeft([])\n      setTargetKeys(next)\n      onChange?.(next, 'right', moved)\n      onSelectChange?.({ left: [], right: selectedRight })\n    }\n\n    function moveLeft() {\n      if (selectedRight.length === 0) return\n      const remove = new Set(selectedRight)\n      const next = resolvedTargetKeys.filter((k) => !remove.has(k))\n      const moved = [...selectedRight]\n      setSelectedRight([])\n      setTargetKeys(next)\n      onChange?.(next, 'left', moved)\n      onSelectChange?.({ left: selectedLeft, right: [] })\n    }\n\n    return (\n      <TransferContext.Provider value={contextValue}>\n        <div ref={ref} className={cn('flex items-stretch gap-3', className)} data-uipkge=\"\" data-slot=\"transfer\">\n          <div className=\"min-w-0 flex-1\">\n            <TransferList\n              side=\"left\"\n              title={titles[0]}\n              items={sourceItems}\n              selected={selectedLeft}\n              onSelectedChange={onLeftSelected}\n              onSearch={(q) => onSearch?.({ direction: 'left', query: q })}\n              footer={footerLeft}\n            />\n          </div>\n          <TransferOperation\n            canMoveRight={selectedLeft.length > 0 && !disabled}\n            canMoveLeft={selectedRight.length > 0 && !disabled}\n            oneWay={oneWay}\n            onMoveRight={moveRight}\n            onMoveLeft={moveLeft}\n          />\n          <div className=\"min-w-0 flex-1\">\n            <TransferList\n              side=\"right\"\n              title={titles[1]}\n              items={targetItems}\n              selected={selectedRight}\n              onSelectedChange={onRightSelected}\n              onSearch={(q) => onSearch?.({ direction: 'right', query: q })}\n              footer={footerRight}\n            />\n          </div>\n        </div>\n      </TransferContext.Provider>\n    )\n  },\n)\nTransfer.displayName = 'Transfer'\n\ninterface TransferOperationProps {\n  canMoveRight: boolean\n  canMoveLeft: boolean\n  oneWay?: boolean\n  onMoveRight: () => void\n  onMoveLeft: () => void\n}\n\nfunction TransferOperation({ canMoveRight, canMoveLeft, oneWay, onMoveRight, onMoveLeft }: TransferOperationProps) {\n  return (\n    <div className=\"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        onClick={onMoveRight}\n      >\n        <ChevronRight aria-hidden=\"true\" />\n      </Button>\n      {!oneWay && (\n        <Button\n          size=\"icon-sm\"\n          variant=\"outline\"\n          disabled={!canMoveLeft}\n          aria-label=\"Move selected to left\"\n          onClick={onMoveLeft}\n        >\n          <ChevronLeft aria-hidden=\"true\" />\n        </Button>\n      )}\n    </div>\n  )\n}\n\ninterface TransferListProps {\n  side: TransferSide\n  title: string\n  items: TransferItem[]\n  selected: string[]\n  onSelectedChange: (keys: string[]) => void\n  onSearch: (query: string) => void\n  footer?: React.ReactNode\n}\n\nfunction TransferList({ side, title, items, selected, onSelectedChange, onSearch, footer }: TransferListProps) {\n  const ctx = useTransferContext()\n\n  const [query, setQuery] = React.useState('')\n  const [page, setPage] = React.useState(1)\n\n  const filtered = React.useMemo(() => {\n    if (!query) return items\n    return items.filter((i) => ctx.filterFn(query, i))\n  }, [query, items, ctx])\n\n  const effectivePageSize = ctx.pageSize ?? Math.max(1, filtered.length)\n  const totalPages = Math.max(1, Math.ceil(filtered.length / effectivePageSize))\n\n  const visible = React.useMemo(() => {\n    if (!ctx.pageSize) return filtered\n    const start = (page - 1) * effectivePageSize\n    return filtered.slice(start, start + effectivePageSize)\n  }, [ctx.pageSize, filtered, page, effectivePageSize])\n\n  React.useEffect(() => {\n    if (page > totalPages) setPage(totalPages)\n  }, [page, totalPages])\n\n  const visibleEnabledKeys = React.useMemo(\n    () => visible.filter((i) => !i.disabled).map((i) => i.key),\n    [visible],\n  )\n  const selectedSet = React.useMemo(() => new Set(selected), [selected])\n\n  const visibleSelectedCount = visibleEnabledKeys.filter((k) => selectedSet.has(k)).length\n\n  const masterChecked = visibleEnabledKeys.length > 0 && visibleSelectedCount === visibleEnabledKeys.length\n  const masterIndeterminate = visibleSelectedCount > 0 && visibleSelectedCount < visibleEnabledKeys.length\n\n  const [lastAnchor, setLastAnchor] = React.useState<string | null>(null)\n\n  function toggleAll(checked: boolean) {\n    let next = [...selected]\n    if (checked) {\n      for (const k of visibleEnabledKeys) {\n        if (!selectedSet.has(k)) next.push(k)\n      }\n    } else {\n      next = next.filter((k) => !visibleEnabledKeys.includes(k))\n    }\n    onSelectedChange(next)\n  }\n\n  function toggleItem(item: TransferItem, checked: boolean) {\n    if (item.disabled || ctx.disabled) return\n    let next = [...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    onSelectedChange(next)\n    setLastAnchor(item.key)\n  }\n\n  function onRowClick(e: React.MouseEvent, item: TransferItem) {\n    if (item.disabled || ctx.disabled) return\n    // With checkbox visible, click toggles (matches checkbox UX).\n    if (ctx.selectable) {\n      toggleItem(item, !selectedSet.has(item.key))\n      return\n    }\n    // No checkbox: desktop list pattern — plain=replace, cmd/ctrl=toggle, shift=range.\n    const enabledKeys = visible.filter((i) => !i.disabled).map((i) => i.key)\n    if (e.shiftKey && lastAnchor && enabledKeys.includes(lastAnchor)) {\n      const start = enabledKeys.indexOf(lastAnchor)\n      const end = enabledKeys.indexOf(item.key)\n      const [lo, hi] = start < end ? [start, end] : [end, start]\n      onSelectedChange(enabledKeys.slice(lo, hi + 1))\n      return\n    }\n    if (e.metaKey || e.ctrlKey) {\n      let next = [...selected]\n      if (selectedSet.has(item.key)) next = next.filter((k) => k !== item.key)\n      else next.push(item.key)\n      onSelectedChange(next)\n      setLastAnchor(item.key)\n      return\n    }\n    onSelectedChange([item.key])\n    setLastAnchor(item.key)\n  }\n\n  function handleSearch(v: string) {\n    setQuery(v)\n    setPage(1)\n    onSearch(v)\n  }\n\n  function prevPage() {\n    if (page > 1) setPage((p) => p - 1)\n  }\n\n  function nextPage() {\n    if (page < totalPages) setPage((p) => p + 1)\n  }\n\n  const heightStyle: React.CSSProperties = {\n    height: typeof ctx.height === 'number' ? ctx.height + 'px' : ctx.height,\n  }\n\n  // ----- DnD -----\n\n  const [dropIndicator, setDropIndicator] = React.useState<{ key: string; position: 'before' | 'after' } | null>(null)\n  const [draggingKeys, setDraggingKeys] = React.useState<Set<string>>(new Set())\n\n  const isDropTarget = React.useMemo(() => {\n    const p = ctx.dragPayload\n    if (!p) return false\n    // left list rejects drops when oneWay\n    if (side === 'left' && ctx.oneWay && p.fromSide === 'right') return false\n    // left → left is a no-op (parent owns dataSource order)\n    if (side === 'left' && p.fromSide === 'left') return false\n    return true\n  }, [ctx.dragPayload, ctx.oneWay, side])\n\n  function onItemDragStart(e: React.DragEvent, item: TransferItem) {\n    if (!ctx.draggable || item.disabled || ctx.disabled) {\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.has(item.key)\n      ? selected.filter((k) => {\n          const i = items.find((x) => x.key === k)\n          return i && !i.disabled\n        })\n      : [item.key]\n    setDraggingKeys(new Set(keys))\n    ctx.startDrag({ keys, fromSide: 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\n  function onItemDragEnd() {\n    setDraggingKeys(new Set())\n    setDropIndicator(null)\n    ctx.endDrag()\n  }\n\n  function onItemDragOver(e: React.DragEvent, item: TransferItem) {\n    if (!isDropTarget) return\n    e.preventDefault()\n    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n    if (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    setDropIndicator({ key: item.key, position: after ? 'after' : 'before' })\n  }\n\n  function onItemDrop(e: React.DragEvent, item: TransferItem) {\n    if (!isDropTarget) return\n    e.preventDefault()\n    e.stopPropagation()\n    if (side === 'right') {\n      const after = dropIndicator?.position === 'after'\n      const idx = items.findIndex((x) => x.key === item.key)\n      const beforeKey = after ? (items[idx + 1]?.key ?? null) : item.key\n      ctx.drop('right', beforeKey)\n    } else {\n      ctx.drop('left', null)\n    }\n    setDropIndicator(null)\n  }\n\n  function onListDragOver(e: React.DragEvent) {\n    if (!isDropTarget) return\n    e.preventDefault()\n    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n  }\n\n  function onListDrop(e: React.DragEvent) {\n    if (!isDropTarget) return\n    e.preventDefault()\n    // Empty zone or below all items → append (right) / remove (left).\n    ctx.drop(side, null)\n    setDropIndicator(null)\n  }\n\n  function onListDragLeave(e: React.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      setDropIndicator(null)\n    }\n  }\n\n  return (\n    <div\n      className={cn(\n        'bg-card flex flex-col overflow-hidden rounded-md border transition-colors',\n        ctx.dragPayload && isDropTarget && 'ring-ring/40 ring-1',\n      )}\n    >\n      <div className=\"bg-muted/40 flex items-center justify-between gap-2 border-b px-3 py-2\">\n        <div className=\"flex min-w-0 items-center gap-2\">\n          {ctx.selectable && (\n            <Checkbox\n              checked={masterIndeterminate ? 'indeterminate' : masterChecked}\n              disabled={ctx.disabled || visibleEnabledKeys.length === 0}\n              onCheckedChange={(c) => toggleAll(c === true)}\n            />\n          )}\n          <span className=\"truncate text-sm font-medium\">{title}</span>\n        </div>\n        <span className=\"text-muted-foreground text-xs tabular-nums\">\n          {' '}\n          {selected.length}/{items.length}{' '}\n        </span>\n      </div>\n\n      {ctx.showSearch && (\n        <div className=\"border-b p-2\">\n          <div className=\"relative\">\n            <Search\n              className=\"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              value={query}\n              placeholder=\"Search\"\n              className=\"h-8 pl-8\"\n              onChange={(e) => handleSearch(e.target.value)}\n            />\n          </div>\n        </div>\n      )}\n\n      <ScrollArea style={heightStyle} className=\"flex-1\" onDragOver={onListDragOver} onDrop={onListDrop} onDragLeave={onListDragLeave}>\n        <ul role=\"listbox\" aria-multiselectable=\"true\" className=\"py-1\">\n          {visible.map((item) => (\n            <li\n              key={item.key}\n              role=\"option\"\n              aria-selected={selectedSet.has(item.key)}\n              draggable={ctx.draggable && !item.disabled && !ctx.disabled}\n              className={cn(\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 && selectedSet.has(item.key) && 'bg-accent',\n              )}\n              onClick={(e) => onRowClick(e, item)}\n              onDragStart={(e) => onItemDragStart(e, item)}\n              onDragEnd={onItemDragEnd}\n              onDragOver={(e) => onItemDragOver(e, item)}\n              onDrop={(e) => onItemDrop(e, item)}\n            >\n              {dropIndicator && dropIndicator.key === item.key && dropIndicator.position === 'before' && side === 'right' && (\n                <span\n                  className=\"bg-primary pointer-events-none absolute -top-px right-2 left-2 h-0.5 rounded-full\"\n                  aria-hidden=\"true\"\n                />\n              )}\n              {dropIndicator && dropIndicator.key === item.key && dropIndicator.position === 'after' && side === 'right' && (\n                <span\n                  className=\"bg-primary pointer-events-none absolute right-2 -bottom-px left-2 h-0.5 rounded-full\"\n                  aria-hidden=\"true\"\n                />\n              )}\n              {ctx.selectable && (\n                <Checkbox\n                  checked={selectedSet.has(item.key)}\n                  disabled={item.disabled || ctx.disabled}\n                  onCheckedChange={(c) => toggleItem(item, c === true)}\n                  onClick={(e) => e.stopPropagation()}\n                />\n              )}\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"truncate\">{item.label}</div>\n                {item.description && <div className=\"text-muted-foreground truncate text-xs\">{item.description}</div>}\n              </div>\n              {ctx.draggable && !item.disabled && (\n                <GripVertical className=\"text-muted-foreground/60 mt-0.5 size-3.5 shrink-0\" aria-hidden=\"true\" />\n              )}\n            </li>\n          ))}\n          {visible.length === 0 && <li className=\"text-muted-foreground px-3 py-6 text-center text-sm\">No items</li>}\n        </ul>\n      </ScrollArea>\n\n      {ctx.pageSize && totalPages > 1 && (\n        <div className=\"flex items-center justify-center gap-2 border-t p-2 text-xs\">\n          <button\n            type=\"button\"\n            className=\"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            onClick={prevPage}\n          >\n            Prev\n          </button>\n          <span className=\"tabular-nums\">\n            {page} / {totalPages}\n          </span>\n          <button\n            type=\"button\"\n            className=\"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            onClick={nextPage}\n          >\n            Next\n          </button>\n        </div>\n      )}\n\n      {footer && <div className=\"border-t p-2\">{footer}</div>}\n    </div>\n  )\n}\n\nexport { Transfer, TransferList, TransferOperation }\n",
      "type": "registry:ui",
      "target": "~/components/ui/transfer/transfer.tsx"
    },
    {
      "path": "packages/registry-react/components/transfer/index.ts",
      "content": "export {\n  Transfer,\n  TransferList,\n  TransferOperation,\n  type TransferProps,\n  type TransferItem,\n  type TransferSide,\n} from './transfer'\n",
      "type": "registry:ui",
      "target": "~/components/ui/transfer/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/input.json",
    "https://uipkge.dev/r/react/checkbox.json",
    "https://uipkge.dev/r/react/button.json",
    "https://uipkge.dev/r/react/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"
  ]
}