{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "transfer",
  "title": "Transfer",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/transfer/Transfer.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type { Snippet } from 'svelte'\n  import type { TransferItem } from './context'\n\n  // Omit DOM event props shadowed by component callbacks (onchange/onsearch carry payloads, not events).\n  export interface TransferProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange' | 'onsearch'> {\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    footerLeft?: Snippet\n    footerRight?: Snippet\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    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import TransferList from './TransferList.svelte'\n  import TransferOperation from './TransferOperation.svelte'\n  import { setTransferContext, type TransferDragPayload, type TransferSide } from './context'\n\n  let {\n    targetKeys = $bindable([]),\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    footerLeft,\n    footerRight,\n    onTargetKeysChange,\n    onchange,\n    onsearch,\n    onselectchange,\n    class: className,\n    ref = $bindable(null),\n    ...restProps\n  }: TransferProps = $props()\n\n  function setTargetKeys(next: string[]) {\n    targetKeys = next\n    onTargetKeysChange?.(next)\n  }\n\n  const dataMap = $derived(new Map(dataSource.map((i) => [i.key, i])))\n\n  const sourceItems = $derived(dataSource.filter((i) => !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.\n  const targetItems = $derived.by((): TransferItem[] => {\n    if (draggable) {\n      const out: TransferItem[] = []\n      for (const k of targetKeys) {\n        const item = dataMap.get(k)\n        if (item) out.push(item)\n      }\n      return out\n    }\n    return dataSource.filter((i) => targetKeys.includes(i.key))\n  })\n\n  let selectedLeft = $state<string[]>([])\n  let selectedRight = $state<string[]>([])\n\n  const pageSize = $derived.by((): number | null => {\n    if (pagination === false) return null\n    if (pagination === true) return 10\n    return pagination.pageSize\n  })\n\n  function defaultFilter(q: string, item: TransferItem) {\n    return item.label.toLowerCase().includes(q.toLowerCase())\n  }\n\n  let dragPayload = $state<TransferDragPayload | null>(null)\n\n  function startDrag(payload: TransferDragPayload) {\n    dragPayload = payload\n  }\n\n  function endDrag() {\n    dragPayload = null\n  }\n\n  function drop(toSide: TransferSide, beforeKey: string | null) {\n    const payload = dragPayload\n    dragPayload = null\n    if (!payload || disabled) return\n    const keys = payload.keys.filter((k) => {\n      const item = 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 (oneWay) return\n      const removeSet = new Set(keys)\n      const next = targetKeys.filter((k) => !removeSet.has(k))\n      selectedRight = selectedRight.filter((k) => !removeSet.has(k))\n      setTargetKeys(next)\n      onchange?.(next, 'left', keys)\n      onselectchange?.({ left: selectedLeft, right: selectedRight })\n      return\n    }\n\n    // → right: insert (cross-list move) or reorder (within-target).\n    const movingSet = new Set(keys)\n    const without = 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 = selectedLeft.filter((k) => !movedSet.has(k))\n      setTargetKeys(next)\n      onchange?.(next, 'right', keys)\n      onselectchange?.({ left: selectedLeft, right: selectedRight })\n    } else {\n      // right → right: pure reorder, no change event (target set unchanged).\n      setTargetKeys(next)\n    }\n  }\n\n  function onLeftSelected(keys: string[]) {\n    selectedLeft = keys\n    onselectchange?.({ left: keys, right: selectedRight })\n  }\n\n  function onRightSelected(keys: string[]) {\n    selectedRight = keys\n    onselectchange?.({ left: selectedLeft, right: keys })\n  }\n\n  function moveRight(keys?: string[]) {\n    if (disabled) return\n    const moved = keys?.length ? [...keys] : [...selectedLeft]\n    if (moved.length === 0) return\n    const next = [...targetKeys, ...moved.filter((k) => !targetKeys.includes(k))]\n    selectedLeft = selectedLeft.filter((k) => !moved.includes(k))\n    setTargetKeys(next)\n    onchange?.(next, 'right', moved)\n    onselectchange?.({ left: selectedLeft, right: selectedRight })\n  }\n\n  function moveLeft(keys?: string[]) {\n    if (disabled || oneWay) return\n    const moved = keys?.length ? [...keys] : [...selectedRight]\n    if (moved.length === 0) return\n    const remove = new Set(moved)\n    const next = targetKeys.filter((k) => !remove.has(k))\n    selectedRight = selectedRight.filter((k) => !remove.has(k))\n    setTargetKeys(next)\n    onchange?.(next, 'left', moved)\n    onselectchange?.({ left: selectedLeft, right: selectedRight })\n  }\n\n  setTransferContext({\n    get disabled() {\n      return disabled\n    },\n    get showSearch() {\n      return showSearch\n    },\n    get height() {\n      return height\n    },\n    get pageSize() {\n      return pageSize\n    },\n    get filterFn() {\n      return filterFn ?? defaultFilter\n    },\n    get draggable() {\n      return draggable\n    },\n    get selectable() {\n      return selectable\n    },\n    get oneWay() {\n      return oneWay\n    },\n    get dragPayload() {\n      return dragPayload\n    },\n    startDrag,\n    endDrag,\n    drop,\n    moveRight,\n    moveLeft,\n  })\n</script>\n\n<div bind:this={ref} data-uipkge data-slot=\"transfer\" class={cn('flex items-stretch gap-3', className)} {...restProps}>\n  <div class=\"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={(query) => onsearch?.({ direction: 'left', query })}\n      footer={footerLeft}\n    />\n  </div>\n  <TransferOperation\n    canMoveRight={selectedLeft.length > 0 && !disabled}\n    canMoveLeft={selectedRight.length > 0 && !disabled}\n    {oneWay}\n    onmoveright={() => moveRight()}\n    onmoveleft={() => 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      onselectedchange={onRightSelected}\n      onsearch={(query) => onsearch?.({ direction: 'right', query })}\n      footer={footerRight}\n    />\n  </div>\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/transfer/Transfer.svelte"
    },
    {
      "path": "packages/registry-svelte/components/transfer/TransferList.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { TransferItem, TransferSide } from './context'\n\n  export interface 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?: Snippet\n  }\n</script>\n\n<script lang=\"ts\">\n  import { GripVertical, Search } from '@lucide/svelte'\n  import { Checkbox } from '$lib/components/ui/checkbox'\n  import { Input } from '$lib/components/ui/input'\n  import { ScrollArea } from '$lib/components/ui/scroll-area'\n  import { getTransferContext } from './context'\n\n  let { side, title, items, selected, onselectedchange, onsearch, footer }: TransferListProps = $props()\n\n  const ctx = getTransferContext()\n\n  let query = $state('')\n  let page = $state(1)\n\n  const filtered = $derived(!query ? items : items.filter((i) => ctx.filterFn(query, i)))\n\n  const effectivePageSize = $derived(ctx.pageSize ?? Math.max(1, filtered.length))\n  const totalPages = $derived(Math.max(1, Math.ceil(filtered.length / effectivePageSize)))\n\n  const visible = $derived.by(() => {\n    if (!ctx.pageSize) return filtered\n    const start = (page - 1) * effectivePageSize\n    return filtered.slice(start, start + effectivePageSize)\n  })\n\n  $effect(() => {\n    if (page > totalPages) page = totalPages\n  })\n\n  const visibleEnabledKeys = $derived(visible.filter((i) => !i.disabled).map((i) => i.key))\n  const selectedSet = $derived(new Set(selected))\n\n  const visibleSelectedCount = $derived(visibleEnabledKeys.filter((k) => selectedSet.has(k)).length)\n\n  const masterChecked = $derived(\n    visibleEnabledKeys.length > 0 && visibleSelectedCount === visibleEnabledKeys.length,\n  )\n\n  const masterIndeterminate = $derived(\n    visibleSelectedCount > 0 && visibleSelectedCount < visibleEnabledKeys.length,\n  )\n\n  function emitSelected(keys: string[]) {\n    onselectedchange?.(keys)\n  }\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    emitSelected(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    emitSelected(next)\n    lastAnchor = item.key\n  }\n\n  let lastAnchor = $state<string | null>(null)\n\n  function onRowClick(e: 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      emitSelected(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      emitSelected(next)\n      lastAnchor = item.key\n      return\n    }\n    emitSelected([item.key])\n    lastAnchor = item.key\n  }\n\n  function getOptionRows(from: HTMLElement): HTMLElement[] {\n    const list = from.closest('[role=\"listbox\"]')\n    if (!list) return []\n    return Array.from(list.querySelectorAll<HTMLElement>('[role=\"option\"]:not([aria-disabled=\"true\"])'))\n  }\n\n  function focusOption(el: HTMLElement | null | undefined) {\n    el?.focus()\n  }\n\n  function onRowKeydown(e: KeyboardEvent, item: TransferItem) {\n    const target = e.currentTarget as HTMLElement\n\n    // Arrow navigation within the list.\n    if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n      e.preventDefault()\n      const rows = getOptionRows(target)\n      const idx = rows.indexOf(target)\n      if (idx < 0) return\n      focusOption(e.key === 'ArrowDown' ? rows[idx + 1] : rows[idx - 1])\n      return\n    }\n\n    if (e.key === 'Home') {\n      e.preventDefault()\n      focusOption(getOptionRows(target)[0])\n      return\n    }\n\n    if (e.key === 'End') {\n      e.preventDefault()\n      const rows = getOptionRows(target)\n      focusOption(rows[rows.length - 1])\n      return\n    }\n\n    // Keys to transfer: current multi-selection, or just the focused item.\n    function keysToTransfer(): string[] {\n      if (selectedSet.has(item.key) && selected.length > 0) {\n        return selected.filter((k) => {\n          const i = items.find((x) => x.key === k)\n          return i && !i.disabled\n        })\n      }\n      return item.disabled ? [] : [item.key]\n    }\n\n    // Ctrl/Cmd+Enter transfers selected items (or the focused item if none selected).\n    if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {\n      e.preventDefault()\n      if (item.disabled || ctx.disabled) return\n      const keys = keysToTransfer()\n      if (!keys.length) return\n      if (side === 'left') ctx.moveRight(keys)\n      else if (!ctx.oneWay) ctx.moveLeft(keys)\n      return\n    }\n\n    // Alt+Arrow transfers without redesigning the button strip.\n    if (e.key === 'ArrowRight' && e.altKey && side === 'left') {\n      e.preventDefault()\n      if (item.disabled || ctx.disabled) return\n      const keys = keysToTransfer()\n      if (keys.length) ctx.moveRight(keys)\n      return\n    }\n\n    if (e.key === 'ArrowLeft' && e.altKey && side === 'right' && !ctx.oneWay) {\n      e.preventDefault()\n      if (item.disabled || ctx.disabled) return\n      const keys = keysToTransfer()\n      if (keys.length) ctx.moveLeft(keys)\n      return\n    }\n\n    if (e.key !== 'Enter' && e.key !== ' ') return\n    e.preventDefault()\n    if (item.disabled || ctx.disabled) return\n    if (ctx.selectable) {\n      toggleItem(item, !selectedSet.has(item.key))\n      return\n    }\n    // Keyboard without modifiers: toggle single-item selection (desktop replace).\n    emitSelected(selectedSet.has(item.key) && selected.length === 1 ? [] : [item.key])\n    lastAnchor = item.key\n  }\n\n  function onSearch(v: string) {\n    query = v\n    page = 1\n    onsearch?.(v)\n  }\n\n  function prevPage() {\n    if (page > 1) page--\n  }\n\n  function nextPage() {\n    if (page < totalPages) page++\n  }\n\n  const heightStyle = $derived(\n    `height: ${typeof ctx.height === 'number' ? ctx.height + 'px' : ctx.height}`,\n  )\n\n  // ----- DnD -----\n\n  let dropIndicator = $state<{ key: string; position: 'before' | 'after' } | null>(null)\n  let draggingKeys = $state<Set<string>>(new Set())\n\n  const isDropTarget = $derived.by(() => {\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  })\n\n  function onItemDragStart(e: 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    draggingKeys = 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    draggingKeys = new Set()\n    dropIndicator = null\n    ctx.endDrag()\n  }\n\n  function onItemDragOver(e: 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    dropIndicator = { key: item.key, position: after ? 'after' : 'before' }\n  }\n\n  function onItemDrop(e: 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    dropIndicator = null\n  }\n\n  function onListDragOver(e: DragEvent) {\n    if (!isDropTarget) return\n    e.preventDefault()\n    if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'\n  }\n\n  function onListDrop(e: 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    dropIndicator = null\n  }\n\n  function 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 = null\n    }\n  }\n</script>\n\n<div\n  class=\"flex flex-col overflow-hidden rounded-md border bg-card transition-colors {ctx.dragPayload &&\n  isDropTarget\n    ? 'ring-1 ring-ring/40'\n    : ''}\"\n>\n  <div class=\"flex items-center justify-between gap-2 border-b bg-muted/40 px-3 py-2\">\n    <div class=\"flex min-w-0 items-center gap-2\">\n      {#if ctx.selectable}\n        <Checkbox\n          checked={masterIndeterminate ? 'indeterminate' : masterChecked}\n          disabled={ctx.disabled || visibleEnabledKeys.length === 0}\n          aria-label={`Select all in ${title}`}\n          onCheckedChange={(v: boolean | 'indeterminate') => toggleAll(v === true)}\n        />\n      {/if}\n      <span class=\"truncate text-sm font-medium\">{title}</span>\n    </div>\n    <span class=\"text-xs text-muted-foreground tabular-nums\"> {selected.length}/{items.length} </span>\n  </div>\n\n  {#if ctx.showSearch}\n    <div class=\"border-b p-2\">\n      <div class=\"relative\">\n        <Search\n          class=\"pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground\"\n          aria-hidden=\"true\"\n        />\n        <Input\n          value={query}\n          placeholder=\"Search\"\n          aria-label={`Search ${title}`}\n          class=\"h-8 pl-8\"\n          oninput={(e: Event) => onSearch((e.currentTarget as HTMLInputElement).value)}\n        />\n      </div>\n    </div>\n  {/if}\n\n  <ScrollArea\n    style={heightStyle}\n    class=\"flex-1\"\n    ondragover={onListDragOver}\n    ondrop={onListDrop}\n    ondragleave={onListDragLeave}\n  >\n    <ul role=\"listbox\" aria-label={title} aria-multiselectable=\"true\" class=\"py-1\">\n      {#each visible as item (item.key)}\n        <li\n          role=\"option\"\n          aria-selected={selectedSet.has(item.key)}\n          aria-disabled={item.disabled || undefined}\n          tabindex={item.disabled || ctx.disabled ? -1 : 0}\n          draggable={ctx.draggable && !item.disabled && !ctx.disabled}\n          class=\"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:ring-ring focus-visible:outline-none {item.disabled\n            ? 'cursor-not-allowed opacity-50'\n            : ''} {draggingKeys.has(item.key) ? 'opacity-40' : ''} {!ctx.selectable &&\n          selectedSet.has(item.key)\n            ? 'bg-accent'\n            : ''}\"\n          onclick={(e) => onRowClick(e, item)}\n          onkeydown={(e) => onRowKeydown(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          {#if dropIndicator && dropIndicator.key === item.key && dropIndicator.position === 'before' && side === 'right'}\n            <span\n              class=\"pointer-events-none absolute -top-px right-2 left-2 h-0.5 rounded-full bg-primary\"\n              aria-hidden=\"true\"\n            ></span>\n          {/if}\n          {#if dropIndicator && dropIndicator.key === item.key && dropIndicator.position === 'after' && side === 'right'}\n            <span\n              class=\"pointer-events-none absolute right-2 -bottom-px left-2 h-0.5 rounded-full bg-primary\"\n              aria-hidden=\"true\"\n            ></span>\n          {/if}\n          {#if ctx.selectable}\n            <Checkbox\n              checked={selectedSet.has(item.key)}\n              disabled={item.disabled || ctx.disabled}\n              onCheckedChange={(v: boolean | 'indeterminate') => toggleItem(item, v === true)}\n              onclick={(e: MouseEvent) => e.stopPropagation()}\n            />\n          {/if}\n          <div class=\"min-w-0 flex-1\">\n            <div class=\"truncate\">{item.label}</div>\n            {#if item.description}\n              <div class=\"truncate text-xs text-muted-foreground\">\n                {item.description}\n              </div>\n            {/if}\n          </div>\n          {#if ctx.draggable && !item.disabled}\n            <GripVertical class=\"mt-0.5 size-3.5 shrink-0 text-muted-foreground/60\" aria-hidden=\"true\" />\n          {/if}\n        </li>\n      {/each}\n      {#if visible.length === 0}\n        <li class=\"px-3 py-6 text-center text-sm text-muted-foreground\">No items</li>\n      {/if}\n    </ul>\n  </ScrollArea>\n\n  {#if ctx.pageSize && totalPages > 1}\n    <div class=\"flex items-center justify-center gap-2 border-t p-2 text-xs\">\n      <button\n        type=\"button\"\n        class=\"rounded px-2 py-1 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50\"\n        disabled={page <= 1}\n        aria-label={`Previous page of ${title}`}\n        onclick={prevPage}\n      >\n        Prev\n      </button>\n      <span class=\"tabular-nums\" aria-live=\"polite\">{page} / {totalPages}</span>\n      <button\n        type=\"button\"\n        class=\"rounded px-2 py-1 hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50\"\n        disabled={page >= totalPages}\n        aria-label={`Next page of ${title}`}\n        onclick={nextPage}\n      >\n        Next\n      </button>\n    </div>\n  {/if}\n\n  {#if footer}\n    <div class=\"border-t p-2\">\n      {@render footer()}\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/transfer/TransferList.svelte"
    },
    {
      "path": "packages/registry-svelte/components/transfer/TransferOperation.svelte",
      "content": "<script lang=\"ts\" module>\n  export interface TransferOperationProps {\n    canMoveRight: boolean\n    canMoveLeft: boolean\n    oneWay?: boolean\n    onmoveright?: () => void\n    onmoveleft?: () => void\n  }\n</script>\n\n<script lang=\"ts\">\n  import { ChevronLeft, ChevronRight } from '@lucide/svelte'\n  import { Button } from '$lib/components/ui/button'\n\n  let { canMoveRight, canMoveLeft, oneWay = false, onmoveright, onmoveleft }: TransferOperationProps = $props()\n</script>\n\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    onclick={() => onmoveright?.()}\n  >\n    <ChevronRight aria-hidden=\"true\" />\n  </Button>\n  {#if !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  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/transfer/TransferOperation.svelte"
    },
    {
      "path": "packages/registry-svelte/components/transfer/context.ts",
      "content": "import { getContext, setContext } from 'svelte'\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  readonly disabled: boolean\n  readonly showSearch: boolean\n  readonly height: number | string\n  readonly pageSize: number | null\n  readonly filterFn: (query: string, item: TransferItem) => boolean\n  readonly draggable: boolean\n  readonly selectable: boolean\n  readonly oneWay: boolean\n  readonly dragPayload: TransferDragPayload | null\n  startDrag: (payload: TransferDragPayload) => void\n  endDrag: () => void\n  drop: (toSide: TransferSide, beforeKey: string | null) => void\n  /** Move left→right. Optional keys override the current left selection. */\n  moveRight: (keys?: string[]) => void\n  /** Move right→left. Optional keys override the current right selection. */\n  moveLeft: (keys?: string[]) => void\n}\n\nconst TRANSFER_CONTEXT_KEY = Symbol('uipkge-transfer')\n\nexport function setTransferContext(ctx: TransferContext): void {\n  setContext(TRANSFER_CONTEXT_KEY, ctx)\n}\n\nexport function getTransferContext(): TransferContext {\n  const ctx = getContext<TransferContext | undefined>(TRANSFER_CONTEXT_KEY)\n  if (!ctx) throw new Error('TransferList must be used inside <Transfer>.')\n  return ctx\n}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/transfer/context.ts"
    },
    {
      "path": "packages/registry-svelte/components/transfer/index.ts",
      "content": "export { default as Transfer, type TransferProps } from './Transfer.svelte'\nexport { default as TransferList, type TransferListProps } from './TransferList.svelte'\nexport { default as TransferOperation, type TransferOperationProps } from './TransferOperation.svelte'\nexport type { TransferItem, TransferSide, TransferDragPayload } from './context'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/transfer/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/input.json",
    "https://uipkge.dev/r/svelte/checkbox.json",
    "https://uipkge.dev/r/svelte/button.json",
    "https://uipkge.dev/r/svelte/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"
  ]
}