{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sortable",
  "title": "Sortable",
  "author": "@fma-ds/ds",
  "description": "A reorderable list, row or grid (`layout`: `vertical`, `horizontal`, `grid`).\n\nGive it the items as `value` (or `defaultValue`) with `onChange`, and one `SortableItem` per item whose `id` matches `getKey(item)` (defaults to `item.id`) — as static children or a render function.\n\n- **Pointer**: items drag from anywhere; a line marks the drop position.\n- **`SortableItemHandle`**: required on every item — the visible grip and its keyboard / screen-reader drag button. Enter starts, arrows pick the drop position, Enter drops; arrows reach it in a vertical list, Tab in a row or grid.\n- **`isDisabled`**: pins an item in place.\n- **`selectionMode`**: lets several selected items move together.\n- **`renderDragPreview`**: replaces the browser's drag snapshot.\n- **Events**: `onChange` also gets the moved keys and the drop target; `onDragStart`/`onDragEnd` follow react-aria.",
  "dependencies": [
    "cn@^0.5.0",
    "lucide-react@^1.61.0",
    "react-aria-components@^1.22.1"
  ],
  "files": [
    {
      "path": "src/core/sortable/sortable.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"cn\";\nimport { GripVerticalIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n  Button as AriaButton,\n  type DragAndDropOptions,\n  DropIndicator,\n  GridList,\n  GridListItem,\n  type GridListItemProps,\n  type GridListProps,\n  type Key,\n  useDragAndDrop,\n} from \"react-aria-components\";\n\ntype SortableLayout = \"vertical\" | \"horizontal\" | \"grid\";\n\n/** Where a reorder put the moved items: before or after `key`. */\ninterface SortableMoveDetails {\n  keys: Set<Key>;\n  target: { key: Key; dropPosition: \"before\" | \"after\" };\n}\n\ninterface SortableProps<T extends object>\n  extends Omit<\n    GridListProps<T>,\n    | \"items\"\n    | \"children\"\n    | \"layout\"\n    | \"orientation\"\n    | \"dragAndDropHooks\"\n    | \"onChange\"\n    | \"defaultValue\"\n  > {\n  /** Items in their current order (controlled). */\n  value?: T[];\n  /** Initial order (uncontrolled). Needs `children` as a render function. */\n  defaultValue?: T[];\n  /** Called with the reordered items after a drop. */\n  onChange?: (value: T[], details: SortableMoveDetails) => void;\n  /** The item's key; must match the `id` of its `SortableItem`. Defaults to `item.id`. */\n  getKey?: (item: T) => Key;\n  /** `vertical` list, `horizontal` row, or wrapping `grid`. */\n  layout?: SortableLayout;\n  /** One `SortableItem` per item, or a function rendering one from an item. */\n  children: React.ReactNode | ((item: T) => React.ReactNode);\n  /** Custom drag preview; receives the keys being dragged. Defaults to the browser's snapshot. */\n  renderDragPreview?: (keys: Key[]) => React.ReactElement;\n  onDragStart?: DragAndDropOptions<T>[\"onDragStart\"];\n  onDragEnd?: DragAndDropOptions<T>[\"onDragEnd\"];\n}\n\nconst defaultGetKey = (item: object) => (item as { id: Key }).id;\n\n/** Moves `keys` (kept in their current order) before or after `target.key`. */\nfunction moveItems<T extends object>(\n  items: T[],\n  getKey: (item: T) => Key,\n  { keys, target }: SortableMoveDetails,\n): T[] {\n  const moved = items.filter((item) => keys.has(getKey(item)));\n  const rest = items.filter((item) => !keys.has(getKey(item)));\n  let at = rest.findIndex((item) => getKey(item) === target.key);\n  if (at === -1) return items;\n  if (target.dropPosition === \"after\") at += 1;\n  return [...rest.slice(0, at), ...moved, ...rest.slice(at)];\n}\n\n/**\n * A reorderable list, row or grid: drag items with the pointer, or with the\n * keyboard from a `SortableItemHandle` (Enter, arrows, Enter).\n */\nfunction Sortable<T extends object>({\n  value: valueProp,\n  defaultValue,\n  onChange,\n  getKey = defaultGetKey,\n  layout = \"vertical\",\n  children,\n  renderDragPreview,\n  onDragStart,\n  onDragEnd,\n  className,\n  ...props\n}: SortableProps<T>) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue ?? []);\n  const value = valueProp ?? uncontrolled;\n\n  const { dragAndDropHooks } = useDragAndDrop<T>({\n    getItems: (keys) => [...keys].map((key) => ({ \"text/plain\": String(key) })),\n    onReorder: (event) => {\n      if (event.target.dropPosition === \"on\") return;\n      const details: SortableMoveDetails = {\n        keys: event.keys,\n        target: { key: event.target.key, dropPosition: event.target.dropPosition },\n      };\n      const next = moveItems(value, getKey, details);\n      if (valueProp === undefined) setUncontrolled(next);\n      onChange?.(next, details);\n    },\n    renderDragPreview: renderDragPreview\n      ? (items) => renderDragPreview(items.map((item) => item[\"text/plain\"] ?? \"\"))\n      : undefined,\n    renderDropIndicator: (target) => (\n      <DropIndicator\n        target={target}\n        data-slot=\"sortable-drop-indicator\"\n        className={cn(\n          // Takes no room: the negative margins cancel the extra gap it adds.\n          \"relative outline-none\",\n          layout === \"vertical\"\n            ? \"-my-[calc(var(--sortable-gap)/2)] h-0 before:inset-x-0 before:-top-px before:h-0.5\"\n            : \"-mx-[calc(var(--sortable-gap)/2)] w-0 before:inset-y-0 before:-left-px before:w-0.5\",\n          \"before:absolute before:rounded-full data-drop-target:before:bg-primary\",\n        )}\n      />\n    ),\n    onDragStart,\n    onDragEnd,\n  });\n\n  return (\n    <GridList<T>\n      data-slot=\"sortable\"\n      data-layout={layout}\n      layout={layout === \"grid\" ? \"grid\" : \"stack\"}\n      orientation={layout === \"vertical\" ? \"vertical\" : \"horizontal\"}\n      // Arrows walk the items in a row or grid, so Tab reaches the handle there.\n      keyboardNavigationBehavior={layout === \"vertical\" ? \"arrow\" : \"tab\"}\n      items={typeof children === \"function\" ? value : undefined}\n      dragAndDropHooks={dragAndDropHooks}\n      className={cn(\n        \"flex gap-(--sortable-gap) outline-none [--sortable-gap:--spacing(2)]\",\n        layout === \"vertical\" && \"flex-col\",\n        layout === \"grid\" && \"flex-wrap\",\n        className as string,\n      )}\n      {...props}\n    >\n      {typeof children === \"function\" ? (item: T) => children(item) : (children as React.ReactNode)}\n    </GridList>\n  );\n}\n\ninterface SortableItemProps extends Omit<GridListItemProps, \"id\" | \"children\"> {\n  /** Must match the key `getKey` returns for this item. */\n  id: Key;\n  children?: React.ReactNode;\n}\n\n/** A sortable item. Dragged items get `data-dragging`; `isDisabled` pins one in place. */\nfunction SortableItem({ className, children, ...props }: SortableItemProps) {\n  return (\n    <GridListItem\n      data-slot=\"sortable-item\"\n      className={cn(\n        \"relative flex cursor-grab items-center gap-2 rounded-md outline-none transition-opacity data-dragging:cursor-grabbing data-dragging:opacity-50 data-focus-visible:ring-2 data-focus-visible:ring-ring/50 data-disabled:cursor-default data-disabled:opacity-50\",\n        className as string,\n      )}\n      {...props}\n    >\n      {children}\n    </GridListItem>\n  );\n}\n\n/**\n * The item's grip, and its keyboard / screen-reader drag button: focus it,\n * Enter starts the drag, arrows pick the drop position, Enter drops.\n */\nfunction SortableItemHandle({\n  className,\n  children,\n  ...props\n}: Omit<React.ComponentProps<typeof AriaButton>, \"slot\">) {\n  return (\n    <AriaButton\n      slot=\"drag\"\n      data-slot=\"sortable-item-handle\"\n      className={cn(\n        \"flex size-7 shrink-0 cursor-grab items-center justify-center rounded-md text-muted-foreground outline-none data-disabled:cursor-default data-focus-visible:ring-2 data-focus-visible:ring-ring/50 data-hovered:bg-accent data-hovered:text-accent-foreground\",\n        className as string,\n      )}\n      {...props}\n    >\n      {children ?? <GripVerticalIcon aria-hidden=\"true\" className=\"size-4\" />}\n    </AriaButton>\n  );\n}\n\nexport type { SortableItemProps, SortableLayout, SortableMoveDetails, SortableProps };\nexport { Sortable, SortableItem, SortableItemHandle };\n",
      "type": "registry:component",
      "target": "components/fma-ds/sortable.tsx"
    }
  ],
  "meta": {
    "status": "stable",
    "source": "original",
    "license": "MIT",
    "tags": [
      "data-display",
      "drag-and-drop",
      "reorder",
      "list",
      "react-aria"
    ],
    "contentHash": "0f29f5662dc9"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}