{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "transfer-list",
  "type": "registry:ui",
  "title": "Transfer List",
  "description": "A dual-listbox for moving items between two lists, with search, check-all and keyboard support.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "checkbox",
    "input"
  ],
  "files": [
    {
      "path": "registry/new-york/transfer-list/transfer-list.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  ChevronsLeftIcon,\n  ChevronsRightIcon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/new-york/ui/button\"\nimport { Checkbox } from \"@/registry/new-york/ui/checkbox\"\nimport { Input } from \"@/registry/new-york/ui/input\"\n\nexport type TransferItem = {\n  value: string\n  label: React.ReactNode\n  disabled?: boolean\n}\n\ntype TransferListProps = {\n  items: TransferItem[]\n  /** Values in the right pane (controlled). */\n  value?: string[]\n  defaultValue?: string[]\n  onValueChange?: (value: string[]) => void\n  titles?: [string, string]\n  searchable?: boolean\n  className?: string\n}\n\nfunction TransferList({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  titles = [\"Available\", \"Selected\"],\n  searchable = true,\n  className,\n}: TransferListProps) {\n  const [internal, setInternal] = React.useState(defaultValue ?? [])\n  const [checked, setChecked] = React.useState<Set<string>>(() => new Set())\n  const selected = value ?? internal\n\n  const selectedSet = new Set(selected)\n  const left = items.filter((item) => !selectedSet.has(item.value))\n  const right = items.filter((item) => selectedSet.has(item.value))\n\n  function setSelected(next: string[]) {\n    if (value === undefined) setInternal(next)\n    onValueChange?.(next)\n  }\n\n  function toggle(values: string[], on: boolean) {\n    setChecked((prev) => {\n      const next = new Set(prev)\n      for (const v of values) {\n        if (on) next.add(v)\n        else next.delete(v)\n      }\n      return next\n    })\n  }\n\n  function movable(source: TransferItem[], onlyChecked: boolean) {\n    return source\n      .filter((item) => !item.disabled && (!onlyChecked || checked.has(item.value)))\n      .map((item) => item.value)\n  }\n\n  function move(toRight: boolean, onlyChecked: boolean) {\n    const moving = movable(toRight ? left : right, onlyChecked)\n    const movingSet = new Set(moving)\n    setSelected(\n      toRight\n        ? [...selected, ...moving]\n        : selected.filter((v) => !movingSet.has(v))\n    )\n    toggle(moving, false)\n  }\n\n  const actions = [\n    { label: \"Move all to \" + titles[1], icon: ChevronsRightIcon, toRight: true, onlyChecked: false },\n    { label: \"Move checked to \" + titles[1], icon: ChevronRightIcon, toRight: true, onlyChecked: true },\n    { label: \"Move checked to \" + titles[0], icon: ChevronLeftIcon, toRight: false, onlyChecked: true },\n    { label: \"Move all to \" + titles[0], icon: ChevronsLeftIcon, toRight: false, onlyChecked: false },\n  ]\n\n  return (\n    <div\n      data-slot=\"transfer-list\"\n      className={cn(\n        \"grid w-full gap-3 sm:grid-cols-[1fr_auto_1fr] sm:items-center\",\n        className\n      )}\n    >\n      <TransferPane\n        title={titles[0]}\n        items={left}\n        checked={checked}\n        onToggle={toggle}\n        searchable={searchable}\n      />\n      <div className=\"flex justify-center gap-2 sm:flex-col\">\n        {actions.map(({ label, icon: Icon, toRight, onlyChecked }) => (\n          <Button\n            key={label}\n            type=\"button\"\n            variant=\"outline\"\n            size=\"icon\"\n            aria-label={label}\n            title={label}\n            disabled={!movable(toRight ? left : right, onlyChecked).length}\n            onClick={() => move(toRight, onlyChecked)}\n          >\n            <Icon className=\"rotate-90 sm:rotate-0\" />\n          </Button>\n        ))}\n      </div>\n      <TransferPane\n        title={titles[1]}\n        items={right}\n        checked={checked}\n        onToggle={toggle}\n        searchable={searchable}\n      />\n    </div>\n  )\n}\n\nfunction TransferPane({\n  title,\n  items,\n  checked,\n  onToggle,\n  searchable,\n}: {\n  title: string\n  items: TransferItem[]\n  checked: Set<string>\n  onToggle: (values: string[], on: boolean) => void\n  searchable: boolean\n}) {\n  const titleId = React.useId()\n  const [query, setQuery] = React.useState(\"\")\n\n  // Non-string labels are searched by their value.\n  const q = query.trim().toLowerCase()\n  const visible = q\n    ? items.filter((item) =>\n        (typeof item.label === \"string\" ? item.label : item.value)\n          .toLowerCase()\n          .includes(q)\n      )\n    : items\n  const enabled = visible.filter((item) => !item.disabled)\n  const checkedCount = items.filter((item) => checked.has(item.value)).length\n  const enabledChecked = enabled.filter((item) => checked.has(item.value)).length\n  // Plain boolean (no \"indeterminate\") so it works with both Radix and Base UI checkboxes.\n  const allChecked = enabled.length > 0 && enabledChecked === enabled.length\n\n  return (\n    <div\n      role=\"group\"\n      aria-labelledby={titleId}\n      data-slot=\"transfer-list-pane\"\n      className=\"flex min-w-0 flex-col rounded-md border bg-background shadow-xs\"\n    >\n      <div className=\"flex items-center gap-2 border-b px-3 py-2\">\n        <Checkbox\n          aria-label={`Check all in ${title}`}\n          checked={allChecked}\n          disabled={!enabled.length}\n          onCheckedChange={(c) =>\n            onToggle(\n              enabled.map((item) => item.value),\n              c === true\n            )\n          }\n        />\n        <span id={titleId} className=\"text-sm font-medium\">\n          {title}\n        </span>\n        <span className=\"ml-auto text-xs text-muted-foreground tabular-nums\">\n          {checkedCount}/{items.length}\n        </span>\n      </div>\n      {searchable && (\n        <div className=\"border-b p-2\">\n          <Input\n            type=\"search\"\n            placeholder=\"Search…\"\n            aria-label={`Search ${title}`}\n            value={query}\n            onChange={(e) => setQuery(e.target.value)}\n            className=\"h-8\"\n          />\n        </div>\n      )}\n      <ul className=\"h-64 overflow-y-auto p-1\">\n        {visible.map((item) => (\n          <li key={item.value}>\n            <label className=\"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 has-[:disabled]:hover:bg-transparent\">\n              <Checkbox\n                checked={checked.has(item.value)}\n                disabled={item.disabled}\n                onCheckedChange={(c) => onToggle([item.value], c === true)}\n              />\n              {item.label}\n            </label>\n          </li>\n        ))}\n        {!visible.length && (\n          <li className=\"py-6 text-center text-sm text-muted-foreground\">\n            No items\n          </li>\n        )}\n      </ul>\n    </div>\n  )\n}\n\nexport { TransferList }\n",
      "type": "registry:ui"
    }
  ]
}