{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-grid-sort-menu",
  "title": "Data Grid Sort Menu",
  "description": "A sort menu component for the data grid with drag-and-drop reordering",
  "dependencies": [
    "@tanstack/react-table",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "command",
    "popover",
    "select"
  ],
  "files": [
    {
      "path": "src/components/data-grid/data-grid-sort-menu.tsx",
      "content": "\"use client\";\n\nimport type { ColumnSort, SortDirection, Table } from \"@tanstack/react-table\";\nimport {\n  ArrowDownUp,\n  ChevronsUpDown,\n  GripVertical,\n  Trash2,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport { useDirection } from \"@/components/ui/direction\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  Sortable,\n  SortableContent,\n  SortableItem,\n  SortableItemHandle,\n  SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport { cn } from \"@/lib/utils\";\n\nconst SORT_SHORTCUT_KEY = \"s\";\nconst REMOVE_SORT_SHORTCUTS = new Set([\"backspace\", \"delete\"]);\nconst SORT_ORDERS = [\n  { label: \"Asc\", value: \"asc\" },\n  { label: \"Desc\", value: \"desc\" },\n];\n\ninterface DataGridSortMenuProps<TData>\n  extends React.ComponentProps<typeof PopoverContent> {\n  table: Table<TData>;\n  disabled?: boolean;\n}\n\nexport function DataGridSortMenu<TData>({\n  table,\n  disabled,\n  className,\n  ...props\n}: DataGridSortMenuProps<TData>) {\n  const dir = useDirection();\n  const id = React.useId();\n  const labelId = React.useId();\n  const descriptionId = React.useId();\n  const [open, setOpen] = React.useState(false);\n  const addButtonRef = React.useRef<HTMLButtonElement>(null);\n\n  const sorting = table.getState().sorting;\n  const onSortingChange = table.setSorting;\n\n  const { columnLabels, columns } = React.useMemo(() => {\n    const labels = new Map<string, string>();\n    const sortingIds = new Set(sorting.map((s) => s.id));\n    const availableColumns: { id: string; label: string }[] = [];\n\n    for (const column of table.getAllColumns()) {\n      if (!column.getCanSort()) continue;\n\n      const label = column.columnDef.meta?.label ?? column.id;\n      labels.set(column.id, label);\n\n      if (!sortingIds.has(column.id)) {\n        availableColumns.push({ id: column.id, label });\n      }\n    }\n\n    return {\n      columnLabels: labels,\n      columns: availableColumns,\n    };\n  }, [sorting, table]);\n\n  const onSortAdd = React.useCallback(() => {\n    const firstColumn = columns[0];\n    if (!firstColumn) return;\n\n    onSortingChange((prevSorting) => [\n      ...prevSorting,\n      { id: firstColumn.id, desc: false },\n    ]);\n  }, [columns, onSortingChange]);\n\n  const onSortUpdate = React.useCallback(\n    (sortId: string, updates: Partial<ColumnSort>) => {\n      onSortingChange((prevSorting) => {\n        if (!prevSorting) return prevSorting;\n        return prevSorting.map((sort) =>\n          sort.id === sortId ? { ...sort, ...updates } : sort,\n        );\n      });\n    },\n    [onSortingChange],\n  );\n\n  const onSortRemove = React.useCallback(\n    (sortId: string) => {\n      onSortingChange((prevSorting) =>\n        prevSorting.filter((item) => item.id !== sortId),\n      );\n    },\n    [onSortingChange],\n  );\n\n  const onSortingReset = React.useCallback(\n    () => onSortingChange(table.initialState.sorting),\n    [onSortingChange, table.initialState.sorting],\n  );\n\n  React.useEffect(() => {\n    function onKeyDown(event: KeyboardEvent) {\n      if (\n        event.target instanceof HTMLInputElement ||\n        event.target instanceof HTMLTextAreaElement ||\n        (event.target instanceof HTMLElement &&\n          event.target.contentEditable === \"true\")\n      ) {\n        return;\n      }\n\n      if (\n        event.key.toLowerCase() === SORT_SHORTCUT_KEY &&\n        (event.ctrlKey || event.metaKey) &&\n        event.shiftKey\n      ) {\n        event.preventDefault();\n        setOpen((prev) => !prev);\n      }\n    }\n\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => window.removeEventListener(\"keydown\", onKeyDown);\n  }, []);\n\n  const onTriggerKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLButtonElement>) => {\n      if (\n        REMOVE_SORT_SHORTCUTS.has(event.key.toLowerCase()) &&\n        sorting.length > 0\n      ) {\n        event.preventDefault();\n        onSortingReset();\n      }\n    },\n    [sorting.length, onSortingReset],\n  );\n\n  return (\n    <Sortable\n      value={sorting}\n      onValueChange={onSortingChange}\n      getItemValue={(item) => item.id}\n    >\n      <Popover open={open} onOpenChange={setOpen}>\n        <PopoverTrigger asChild>\n          <Button\n            dir={dir}\n            variant=\"outline\"\n            className=\"font-normal\"\n            onKeyDown={onTriggerKeyDown}\n            disabled={disabled}\n          >\n            <ArrowDownUp className=\"text-muted-foreground\" />\n            Sort\n            {sorting.length > 0 && (\n              <Badge\n                variant=\"secondary\"\n                className=\"h-[18.24px] rounded-[3.2px] px-[5.12px] font-mono font-normal text-[10.4px]\"\n              >\n                {sorting.length}\n              </Badge>\n            )}\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent\n          aria-labelledby={labelId}\n          aria-describedby={descriptionId}\n          dir={dir}\n          className={cn(\n            \"flex w-full max-w-(--radix-popover-content-available-width) flex-col gap-3.5 p-4 sm:min-w-[380px]\",\n            className,\n          )}\n          {...props}\n        >\n          <div className=\"flex flex-col gap-1\">\n            <h4 id={labelId} className=\"font-medium leading-none\">\n              {sorting.length > 0 ? \"Sort by\" : \"No sorting applied\"}\n            </h4>\n            <p\n              id={descriptionId}\n              className={cn(\n                \"text-muted-foreground text-sm\",\n                sorting.length > 0 && \"sr-only\",\n              )}\n            >\n              {sorting.length > 0\n                ? \"Modify sorting to organize your rows.\"\n                : \"Add sorting to organize your rows.\"}\n            </p>\n          </div>\n          {sorting.length > 0 && (\n            <SortableContent asChild>\n              <div\n                role=\"list\"\n                className=\"flex max-h-[300px] flex-col gap-2 overflow-y-auto p-1\"\n              >\n                {sorting.map((sort) => (\n                  <DataTableSortItem\n                    key={sort.id}\n                    sort={sort}\n                    sortItemId={`${id}-sort-${sort.id}`}\n                    dir={dir}\n                    columns={columns}\n                    columnLabels={columnLabels}\n                    onSortUpdate={onSortUpdate}\n                    onSortRemove={onSortRemove}\n                  />\n                ))}\n              </div>\n            </SortableContent>\n          )}\n          <div className=\"flex w-full items-center gap-2\">\n            <Button\n              className=\"rounded\"\n              ref={addButtonRef}\n              onClick={onSortAdd}\n              disabled={columns.length === 0}\n            >\n              Add sort\n            </Button>\n            {sorting.length > 0 && (\n              <Button\n                variant=\"outline\"\n                className=\"rounded\"\n                onClick={onSortingReset}\n              >\n                Reset sorting\n              </Button>\n            )}\n          </div>\n        </PopoverContent>\n      </Popover>\n      <SortableOverlay>\n        <div dir={dir} className=\"flex items-center gap-2\">\n          <div className=\"h-8 w-44 rounded-sm bg-primary/10\" />\n          <div className=\"h-8 w-24 rounded-sm bg-primary/10\" />\n          <div className=\"size-8 shrink-0 rounded-sm bg-primary/10\" />\n          <div className=\"size-8 shrink-0 rounded-sm bg-primary/10\" />\n        </div>\n      </SortableOverlay>\n    </Sortable>\n  );\n}\n\ninterface DataTableSortItemProps {\n  sort: ColumnSort;\n  sortItemId: string;\n  dir: \"ltr\" | \"rtl\";\n  columns: { id: string; label: string }[];\n  columnLabels: Map<string, string>;\n  onSortUpdate: (sortId: string, updates: Partial<ColumnSort>) => void;\n  onSortRemove: (sortId: string) => void;\n}\n\nfunction DataTableSortItem({\n  sort,\n  sortItemId,\n  dir,\n  columns,\n  columnLabels,\n  onSortUpdate,\n  onSortRemove,\n}: DataTableSortItemProps) {\n  const fieldListboxId = `${sortItemId}-field-listbox`;\n  const fieldTriggerId = `${sortItemId}-field-trigger`;\n  const directionListboxId = `${sortItemId}-direction-listbox`;\n\n  const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n  const [showDirectionSelector, setShowDirectionSelector] =\n    React.useState(false);\n\n  const onItemKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<HTMLDivElement>) => {\n      if (\n        event.target instanceof HTMLInputElement ||\n        event.target instanceof HTMLTextAreaElement\n      ) {\n        return;\n      }\n\n      if (showFieldSelector || showDirectionSelector) {\n        return;\n      }\n\n      if (REMOVE_SORT_SHORTCUTS.has(event.key.toLowerCase())) {\n        event.preventDefault();\n        onSortRemove(sort.id);\n      }\n    },\n    [sort.id, showFieldSelector, showDirectionSelector, onSortRemove],\n  );\n\n  return (\n    <SortableItem value={sort.id} asChild>\n      <div\n        role=\"listitem\"\n        id={sortItemId}\n        tabIndex={-1}\n        className=\"flex items-center gap-2\"\n        onKeyDown={onItemKeyDown}\n      >\n        <Popover open={showFieldSelector} onOpenChange={setShowFieldSelector}>\n          <PopoverTrigger asChild>\n            <Button\n              id={fieldTriggerId}\n              aria-controls={fieldListboxId}\n              variant=\"outline\"\n              className=\"w-44 justify-between rounded font-normal\"\n            >\n              <span className=\"truncate\">{columnLabels.get(sort.id)}</span>\n              <ChevronsUpDown className=\"opacity-50\" />\n            </Button>\n          </PopoverTrigger>\n          <PopoverContent\n            id={fieldListboxId}\n            dir={dir}\n            className=\"w-(--radix-popover-trigger-width) p-0\"\n          >\n            <Command>\n              <CommandInput placeholder=\"Search fields...\" />\n              <CommandList>\n                <CommandEmpty>No fields found.</CommandEmpty>\n                <CommandGroup>\n                  {columns.map((column) => (\n                    <CommandItem\n                      key={column.id}\n                      value={column.id}\n                      onSelect={(value) => onSortUpdate(sort.id, { id: value })}\n                    >\n                      <span className=\"truncate\">{column.label}</span>\n                    </CommandItem>\n                  ))}\n                </CommandGroup>\n              </CommandList>\n            </Command>\n          </PopoverContent>\n        </Popover>\n        <Select\n          open={showDirectionSelector}\n          onOpenChange={setShowDirectionSelector}\n          value={sort.desc ? \"desc\" : \"asc\"}\n          onValueChange={(value: SortDirection) =>\n            onSortUpdate(sort.id, { desc: value === \"desc\" })\n          }\n        >\n          <SelectTrigger\n            aria-controls={directionListboxId}\n            className=\"w-24 rounded\"\n          >\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent\n            id={directionListboxId}\n            className=\"min-w-(--radix-select-trigger-width)\"\n          >\n            <SelectGroup>\n              {SORT_ORDERS.map((order) => (\n                <SelectItem key={order.value} value={order.value}>\n                  {order.label}\n                </SelectItem>\n              ))}\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n        <Button\n          aria-controls={sortItemId}\n          variant=\"outline\"\n          size=\"icon\"\n          className=\"size-8 shrink-0 rounded\"\n          onClick={() => onSortRemove(sort.id)}\n        >\n          <Trash2 />\n        </Button>\n        <SortableItemHandle asChild>\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            className=\"size-8 shrink-0 rounded\"\n          >\n            <GripVertical />\n          </Button>\n        </SortableItemHandle>\n      </div>\n    </SortableItem>\n  );\n}\n",
      "type": "registry:component",
      "target": "src/components/data-grid/data-grid-sort-menu.tsx"
    },
    {
      "path": "src/components/ui/direction.tsx",
      "content": "\"use client\";\n\nimport { Direction } from \"radix-ui\";\nimport type * as React from \"react\";\n\nfunction DirectionProvider({\n  dir,\n  direction,\n  children,\n}: React.ComponentProps<typeof Direction.DirectionProvider> & {\n  direction?: React.ComponentProps<typeof Direction.DirectionProvider>[\"dir\"];\n}) {\n  return (\n    <Direction.DirectionProvider dir={direction ?? dir}>\n      {children}\n    </Direction.DirectionProvider>\n  );\n}\n\nconst useDirection = Direction.useDirection;\n\nexport { DirectionProvider, useDirection };\n",
      "type": "registry:ui",
      "target": "src/components/ui/direction.tsx"
    },
    {
      "path": "src/components/ui/sortable.tsx",
      "content": "\"use client\";\n\nimport {\n  type Announcements,\n  closestCenter,\n  closestCorners,\n  DndContext,\n  type DndContextProps,\n  type DragEndEvent,\n  type DraggableAttributes,\n  type DraggableSyntheticListeners,\n  DragOverlay,\n  type DragStartEvent,\n  type DropAnimation,\n  defaultDropAnimationSideEffects,\n  KeyboardSensor,\n  MouseSensor,\n  type ScreenReaderInstructions,\n  TouchSensor,\n  type UniqueIdentifier,\n  useSensor,\n  useSensors,\n} from \"@dnd-kit/core\";\nimport {\n  restrictToHorizontalAxis,\n  restrictToParentElement,\n  restrictToVerticalAxis,\n} from \"@dnd-kit/modifiers\";\nimport {\n  arrayMove,\n  horizontalListSortingStrategy,\n  SortableContext,\n  type SortableContextProps,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\n\nconst orientationConfig = {\n  vertical: {\n    modifiers: [restrictToVerticalAxis, restrictToParentElement],\n    strategy: verticalListSortingStrategy,\n    collisionDetection: closestCenter,\n  },\n  horizontal: {\n    modifiers: [restrictToHorizontalAxis, restrictToParentElement],\n    strategy: horizontalListSortingStrategy,\n    collisionDetection: closestCenter,\n  },\n  mixed: {\n    modifiers: [restrictToParentElement],\n    strategy: undefined,\n    collisionDetection: closestCorners,\n  },\n};\n\nconst ROOT_NAME = \"Sortable\";\nconst CONTENT_NAME = \"SortableContent\";\nconst ITEM_NAME = \"SortableItem\";\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\nconst OVERLAY_NAME = \"SortableOverlay\";\n\ninterface SortableRootContextValue<T> {\n  id: string;\n  items: UniqueIdentifier[];\n  modifiers: DndContextProps[\"modifiers\"];\n  strategy: SortableContextProps[\"strategy\"];\n  activeId: UniqueIdentifier | null;\n  setActiveId: (id: UniqueIdentifier | null) => void;\n  getItemValue: (item: T) => UniqueIdentifier;\n  flatCursor: boolean;\n}\n\nconst SortableRootContext =\n  React.createContext<SortableRootContextValue<unknown> | null>(null);\n\nfunction useSortableContext(consumerName: string) {\n  const context = React.useContext(SortableRootContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n  }\n  return context;\n}\n\ninterface GetItemValue<T> {\n  /**\n   * Callback that returns a unique identifier for each sortable item. Required for array of objects.\n   * @example getItemValue={(item) => item.id}\n   */\n  getItemValue: (item: T) => UniqueIdentifier;\n}\n\ntype SortableProps<T> = DndContextProps &\n  (T extends object ? GetItemValue<T> : Partial<GetItemValue<T>>) & {\n    value: T[];\n    onValueChange?: (items: T[]) => void;\n    onMove?: (\n      event: DragEndEvent & { activeIndex: number; overIndex: number },\n    ) => void;\n    strategy?: SortableContextProps[\"strategy\"];\n    orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\n    flatCursor?: boolean;\n  };\n\nfunction Sortable<T>(props: SortableProps<T>) {\n  const {\n    value,\n    onValueChange,\n    collisionDetection,\n    modifiers,\n    strategy,\n    onMove,\n    orientation = \"vertical\",\n    flatCursor = false,\n    getItemValue: getItemValueProp,\n    accessibility,\n    ...sortableProps\n  } = props;\n\n  const id = React.useId();\n  const [activeId, setActiveId] = React.useState<UniqueIdentifier | null>(null);\n\n  const sensors = useSensors(\n    useSensor(MouseSensor),\n    useSensor(TouchSensor),\n    useSensor(KeyboardSensor, {\n      coordinateGetter: sortableKeyboardCoordinates,\n    }),\n  );\n  const config = React.useMemo(\n    () => orientationConfig[orientation],\n    [orientation],\n  );\n\n  const getItemValue = React.useCallback(\n    (item: T): UniqueIdentifier => {\n      if (typeof item === \"object\" && !getItemValueProp) {\n        throw new Error(\n          \"`getItemValue` is required when using array of objects\",\n        );\n      }\n      return getItemValueProp\n        ? getItemValueProp(item)\n        : (item as UniqueIdentifier);\n    },\n    [getItemValueProp],\n  );\n\n  const items = React.useMemo(() => {\n    return value.map((item) => getItemValue(item));\n  }, [value, getItemValue]);\n\n  const onDragStart = React.useCallback(\n    (event: DragStartEvent) => {\n      sortableProps.onDragStart?.(event);\n\n      if (event.activatorEvent.defaultPrevented) return;\n\n      setActiveId(event.active.id);\n    },\n    [sortableProps.onDragStart],\n  );\n\n  const onDragEnd = React.useCallback(\n    (event: DragEndEvent) => {\n      sortableProps.onDragEnd?.(event);\n\n      if (event.activatorEvent.defaultPrevented) return;\n\n      const { active, over } = event;\n      if (over && active.id !== over?.id) {\n        const activeIndex = value.findIndex(\n          (item) => getItemValue(item) === active.id,\n        );\n        const overIndex = value.findIndex(\n          (item) => getItemValue(item) === over.id,\n        );\n\n        if (onMove) {\n          onMove({ ...event, activeIndex, overIndex });\n        } else {\n          onValueChange?.(arrayMove(value, activeIndex, overIndex));\n        }\n      }\n      setActiveId(null);\n    },\n    [value, onValueChange, onMove, getItemValue, sortableProps.onDragEnd],\n  );\n\n  const onDragCancel = React.useCallback(\n    (event: DragEndEvent) => {\n      sortableProps.onDragCancel?.(event);\n\n      if (event.activatorEvent.defaultPrevented) return;\n\n      setActiveId(null);\n    },\n    [sortableProps.onDragCancel],\n  );\n\n  const announcements: Announcements = React.useMemo(\n    () => ({\n      onDragStart({ active }) {\n        const activeValue = active.id.toString();\n        return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\n      },\n      onDragOver({ active, over }) {\n        if (over) {\n          const overIndex = over.data.current?.sortable.index ?? 0;\n          const activeIndex = active.data.current?.sortable.index ?? 0;\n          const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n          const activeValue = active.id.toString();\n          return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n        }\n        return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n      },\n      onDragEnd({ active, over }) {\n        const activeValue = active.id.toString();\n        if (over) {\n          const overIndex = over.data.current?.sortable.index ?? 0;\n          return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\n        }\n        return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\n      },\n      onDragCancel({ active }) {\n        const activeIndex = active.data.current?.sortable.index ?? 0;\n        const activeValue = active.id.toString();\n        return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\n      },\n      onDragMove({ active, over }) {\n        if (over) {\n          const overIndex = over.data.current?.sortable.index ?? 0;\n          const activeIndex = active.data.current?.sortable.index ?? 0;\n          const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n          const activeValue = active.id.toString();\n          return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n        }\n        return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n      },\n    }),\n    [value],\n  );\n\n  const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\n    () => ({\n      draggable: `\n        To pick up a sortable item, press space or enter.\n        While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\n        Press space or enter again to drop the item in its new position, or press escape to cancel.\n      `,\n    }),\n    [orientation],\n  );\n\n  const contextValue = React.useMemo(\n    () => ({\n      id,\n      items,\n      modifiers: modifiers ?? config.modifiers,\n      strategy: strategy ?? config.strategy,\n      activeId,\n      setActiveId,\n      getItemValue,\n      flatCursor,\n    }),\n    [\n      id,\n      items,\n      modifiers,\n      strategy,\n      config.modifiers,\n      config.strategy,\n      activeId,\n      getItemValue,\n      flatCursor,\n    ],\n  );\n\n  return (\n    <SortableRootContext.Provider\n      value={contextValue as SortableRootContextValue<unknown>}\n    >\n      <DndContext\n        collisionDetection={collisionDetection ?? config.collisionDetection}\n        modifiers={modifiers ?? config.modifiers}\n        sensors={sensors}\n        {...sortableProps}\n        id={id}\n        onDragStart={onDragStart}\n        onDragEnd={onDragEnd}\n        onDragCancel={onDragCancel}\n        accessibility={{\n          announcements,\n          screenReaderInstructions,\n          ...accessibility,\n        }}\n      />\n    </SortableRootContext.Provider>\n  );\n}\n\nconst SortableContentContext = React.createContext<boolean>(false);\n\ninterface SortableContentProps extends React.ComponentProps<\"div\"> {\n  strategy?: SortableContextProps[\"strategy\"];\n  children: React.ReactNode;\n  asChild?: boolean;\n  withoutSlot?: boolean;\n}\n\nfunction SortableContent(props: SortableContentProps) {\n  const {\n    strategy: strategyProp,\n    asChild,\n    withoutSlot,\n    children,\n    ref,\n    ...contentProps\n  } = props;\n\n  const context = useSortableContext(CONTENT_NAME);\n\n  const ContentPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SortableContentContext.Provider value={true}>\n      <SortableContext\n        items={context.items}\n        strategy={strategyProp ?? context.strategy}\n      >\n        {withoutSlot ? (\n          children\n        ) : (\n          <ContentPrimitive\n            data-slot=\"sortable-content\"\n            {...contentProps}\n            ref={ref}\n          >\n            {children}\n          </ContentPrimitive>\n        )}\n      </SortableContext>\n    </SortableContentContext.Provider>\n  );\n}\n\ninterface SortableItemContextValue {\n  id: string;\n  attributes: DraggableAttributes;\n  listeners: DraggableSyntheticListeners | undefined;\n  setActivatorNodeRef: (node: HTMLElement | null) => void;\n  isDragging?: boolean;\n  disabled?: boolean;\n}\n\nconst SortableItemContext =\n  React.createContext<SortableItemContextValue | null>(null);\n\nfunction useSortableItemContext(consumerName: string) {\n  const context = React.useContext(SortableItemContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n  }\n  return context;\n}\n\ninterface SortableItemProps extends React.ComponentProps<\"div\"> {\n  value: UniqueIdentifier;\n  asHandle?: boolean;\n  asChild?: boolean;\n  disabled?: boolean;\n}\n\nfunction SortableItem(props: SortableItemProps) {\n  const {\n    value,\n    style,\n    asHandle,\n    asChild,\n    disabled,\n    className,\n    ref,\n    ...itemProps\n  } = props;\n\n  const inSortableContent = React.useContext(SortableContentContext);\n  const inSortableOverlay = React.useContext(SortableOverlayContext);\n\n  if (!inSortableContent && !inSortableOverlay) {\n    throw new Error(\n      `\\`${ITEM_NAME}\\` must be used within \\`${CONTENT_NAME}\\` or \\`${OVERLAY_NAME}\\``,\n    );\n  }\n\n  if (value === \"\") {\n    throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\n  }\n\n  const context = useSortableContext(ITEM_NAME);\n  const id = React.useId();\n  const {\n    attributes,\n    listeners,\n    setNodeRef,\n    setActivatorNodeRef,\n    transform,\n    transition,\n    isDragging,\n  } = useSortable({ id: value, disabled });\n\n  const composedRef = useComposedRefs(ref, (node) => {\n    if (disabled) return;\n    setNodeRef(node);\n    if (asHandle) setActivatorNodeRef(node);\n  });\n\n  const composedStyle = React.useMemo<React.CSSProperties>(() => {\n    return {\n      transform: CSS.Translate.toString(transform),\n      transition,\n      ...style,\n    };\n  }, [transform, transition, style]);\n\n  const itemContext = React.useMemo<SortableItemContextValue>(\n    () => ({\n      id,\n      attributes,\n      listeners,\n      setActivatorNodeRef,\n      isDragging,\n      disabled,\n    }),\n    [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\n  );\n\n  const ItemPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <SortableItemContext.Provider value={itemContext}>\n      <ItemPrimitive\n        id={id}\n        data-disabled={disabled}\n        data-dragging={isDragging ? \"\" : undefined}\n        data-slot=\"sortable-item\"\n        {...itemProps}\n        {...(asHandle && !disabled ? attributes : {})}\n        {...(asHandle && !disabled ? listeners : {})}\n        ref={composedRef}\n        style={composedStyle}\n        className={cn(\n          \"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1\",\n          {\n            \"touch-none select-none\": asHandle,\n            \"cursor-default\": context.flatCursor,\n            \"data-dragging:cursor-grabbing\": !context.flatCursor,\n            \"cursor-grab\": !isDragging && asHandle && !context.flatCursor,\n            \"opacity-50\": isDragging,\n            \"pointer-events-none opacity-50\": disabled,\n          },\n          className,\n        )}\n      />\n    </SortableItemContext.Provider>\n  );\n}\n\ninterface SortableItemHandleProps extends React.ComponentProps<\"button\"> {\n  asChild?: boolean;\n}\n\nfunction SortableItemHandle(props: SortableItemHandleProps) {\n  const { asChild, disabled, className, ref, ...itemHandleProps } = props;\n\n  const context = useSortableContext(ITEM_HANDLE_NAME);\n  const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);\n\n  const isDisabled = disabled ?? itemContext.disabled;\n\n  const composedRef = useComposedRefs(ref, (node) => {\n    if (!isDisabled) return;\n    itemContext.setActivatorNodeRef(node);\n  });\n\n  const HandlePrimitive = asChild ? SlotPrimitive.Slot : \"button\";\n\n  return (\n    <HandlePrimitive\n      type=\"button\"\n      aria-controls={itemContext.id}\n      data-disabled={isDisabled}\n      data-dragging={itemContext.isDragging ? \"\" : undefined}\n      data-slot=\"sortable-item-handle\"\n      {...itemHandleProps}\n      {...(isDisabled ? {} : itemContext.attributes)}\n      {...(isDisabled ? {} : itemContext.listeners)}\n      ref={composedRef}\n      className={cn(\n        \"select-none disabled:pointer-events-none disabled:opacity-50\",\n        context.flatCursor\n          ? \"cursor-default\"\n          : \"cursor-grab data-dragging:cursor-grabbing\",\n        className,\n      )}\n      disabled={isDisabled}\n    />\n  );\n}\n\nconst SortableOverlayContext = React.createContext(false);\n\nconst dropAnimation: DropAnimation = {\n  sideEffects: defaultDropAnimationSideEffects({\n    styles: {\n      active: {\n        opacity: \"0.4\",\n      },\n    },\n  }),\n};\n\ninterface SortableOverlayProps\n  extends Omit<React.ComponentProps<typeof DragOverlay>, \"children\"> {\n  container?: Element | DocumentFragment | null;\n  children?:\n    | ((params: { value: UniqueIdentifier }) => React.ReactNode)\n    | React.ReactNode;\n}\n\nfunction SortableOverlay(props: SortableOverlayProps) {\n  const { container: containerProp, children, ...overlayProps } = props;\n\n  const context = useSortableContext(OVERLAY_NAME);\n\n  const [mounted, setMounted] = React.useState(false);\n\n  React.useLayoutEffect(() => setMounted(true), []);\n\n  const container =\n    containerProp ?? (mounted ? globalThis.document?.body : null);\n\n  if (!container) return null;\n\n  return ReactDOM.createPortal(\n    <DragOverlay\n      dropAnimation={dropAnimation}\n      modifiers={context.modifiers}\n      className={cn(!context.flatCursor && \"cursor-grabbing\")}\n      {...overlayProps}\n    >\n      <SortableOverlayContext.Provider value={true}>\n        {context.activeId\n          ? typeof children === \"function\"\n            ? children({ value: context.activeId })\n            : children\n          : null}\n      </SortableOverlayContext.Provider>\n    </DragOverlay>,\n    container,\n  );\n}\n\nexport {\n  Sortable,\n  SortableContent,\n  SortableItem,\n  SortableItemHandle,\n  SortableOverlay,\n  type SortableProps,\n};\n",
      "type": "registry:ui",
      "target": "src/components/ui/sortable.tsx"
    },
    {
      "path": "src/lib/compose-refs.ts",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // biome-ignore lint/correctness/useExhaustiveDependencies: we don't want to re-run this callback when the refs change\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:component"
}