{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rail-list",
  "title": "Rail List",
  "description": "A controlled or uncontrolled horizontal selector rail with centered labels, roving focus, and an edge-underline indicator on the active item.",
  "files": [
    {
      "path": "registry/base/ui/rail-list.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type RailListEdge = \"top\" | \"bottom\";\n\nexport type RailListItem = {\n  value: string;\n  label: React.ReactNode;\n  disabled?: boolean;\n  id?: string;\n  ariaControls?: string;\n};\n\nexport type RailListProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"defaultValue\" | \"onChange\"\n> & {\n  items: RailListItem[];\n  /** Controlled active value. Pass null to render without an active item. */\n  value?: string | null;\n  /** Initial active value. Omit or pass null to start without an active item. */\n  defaultValue?: string | null;\n  onValueChange?: (value: string, item: RailListItem) => void;\n  /** Which edge of the active item carries the indicator bar. */\n  edge?: RailListEdge;\n  listClassName?: string;\n  itemClassName?: string;\n  labelClassName?: string;\n  indicatorClassName?: string;\n  onItemPointerEnter?: (item: RailListItem) => void;\n  onItemFocus?: (item: RailListItem) => void;\n  \"aria-label\"?: string;\n};\n\nfunction getEnabledItems(items: RailListItem[]) {\n  return items.filter((item) => !item.disabled);\n}\n\nfunction getSelectableValue(\n  items: RailListItem[],\n  value: string | null | undefined,\n) {\n  const enabledItems = getEnabledItems(items);\n\n  if (value == null) return null;\n\n  return enabledItems.some((item) => item.value === value) ? value : null;\n}\n\nexport function RailList({\n  items,\n  value,\n  defaultValue,\n  onValueChange,\n  edge = \"bottom\",\n  className,\n  listClassName,\n  itemClassName,\n  labelClassName,\n  indicatorClassName,\n  onItemPointerEnter,\n  onItemFocus,\n  \"aria-label\": ariaLabel = \"Options\",\n  ...props\n}: RailListProps) {\n  const isControlled = value !== undefined;\n  const itemRefs = React.useRef(new Map<string, HTMLButtonElement>());\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(() =>\n    getSelectableValue(items, defaultValue),\n  );\n\n  const enabledItems = React.useMemo(() => getEnabledItems(items), [items]);\n  const selectedValue = getSelectableValue(\n    items,\n    isControlled ? value : uncontrolledValue,\n  );\n  const focusableValue = selectedValue ?? enabledItems[0]?.value ?? null;\n\n  const setSelectedValue = React.useCallback(\n    (nextValue: string) => {\n      const nextItem = items.find((item) => item.value === nextValue);\n\n      if (!nextItem || nextItem.disabled || nextValue === selectedValue) {\n        return;\n      }\n\n      if (!isControlled) {\n        setUncontrolledValue(nextValue);\n      }\n\n      onValueChange?.(nextValue, nextItem);\n    },\n    [isControlled, items, onValueChange, selectedValue],\n  );\n\n  const focusItem = React.useCallback((nextValue: string) => {\n    itemRefs.current.get(nextValue)?.focus();\n  }, []);\n\n  const handleKeyDown = React.useCallback(\n    (\n      event: React.KeyboardEvent<HTMLButtonElement>,\n      currentValue: string,\n    ) => {\n      if (enabledItems.length === 0) return;\n\n      const currentIndex = Math.max(\n        enabledItems.findIndex((item) => item.value === currentValue),\n        0,\n      );\n      const lastIndex = enabledItems.length - 1;\n      let nextIndex = currentIndex;\n\n      if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n        nextIndex = currentIndex >= lastIndex ? 0 : currentIndex + 1;\n      } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n        nextIndex = currentIndex <= 0 ? lastIndex : currentIndex - 1;\n      } else if (event.key === \"Home\") {\n        nextIndex = 0;\n      } else if (event.key === \"End\") {\n        nextIndex = lastIndex;\n      } else {\n        return;\n      }\n\n      event.preventDefault();\n\n      const nextItem = enabledItems[nextIndex];\n\n      if (!nextItem) return;\n\n      setSelectedValue(nextItem.value);\n      focusItem(nextItem.value);\n    },\n    [enabledItems, focusItem, setSelectedValue],\n  );\n\n  return (\n    <div\n      {...props}\n      data-slot=\"rail-list\"\n      data-edge={edge}\n      className={cn(\"flex w-full min-w-0\", className)}\n    >\n      <ul\n        role=\"tablist\"\n        aria-label={ariaLabel}\n        aria-orientation=\"horizontal\"\n        data-slot=\"rail-list-list\"\n        className={cn(\n          \"flex w-full min-w-0 gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\",\n          listClassName,\n        )}\n      >\n        {items.map((item) => {\n          const isActive = selectedValue === item.value;\n\n          return (\n            <li\n              key={item.value}\n              role=\"presentation\"\n              className=\"flex min-w-fit flex-1\"\n            >\n              <button\n                id={item.id}\n                type=\"button\"\n                role=\"tab\"\n                aria-selected={isActive}\n                aria-controls={item.ariaControls}\n                disabled={item.disabled}\n                tabIndex={item.value === focusableValue ? 0 : -1}\n                ref={(element) => {\n                  if (element) {\n                    itemRefs.current.set(item.value, element);\n                  } else {\n                    itemRefs.current.delete(item.value);\n                  }\n                }}\n                data-slot=\"rail-list-trigger\"\n                data-active={isActive ? \"\" : undefined}\n                onClick={() => setSelectedValue(item.value)}\n                onKeyDown={(event) => handleKeyDown(event, item.value)}\n                onPointerEnter={() => onItemPointerEnter?.(item)}\n                onFocus={() => onItemFocus?.(item)}\n                className={cn(\n                  \"group relative flex min-h-11 w-full items-center justify-center px-3 py-2 text-center text-sm font-medium text-muted-foreground outline-none transition-colors duration-150 ease-out hover:text-foreground focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-current motion-reduce:transition-none disabled:pointer-events-none disabled:opacity-50 data-active:text-foreground\",\n                  itemClassName,\n                )}\n              >\n                <span\n                  aria-hidden=\"true\"\n                  data-slot=\"rail-list-indicator\"\n                  className={cn(\n                    \"absolute inset-x-3 h-0.5 scale-x-0 bg-current transition-transform duration-200 ease-out group-data-active:scale-x-100 motion-reduce:transition-none\",\n                    edge === \"top\" ? \"top-0\" : \"bottom-0\",\n                    indicatorClassName,\n                  )}\n                />\n                <span className={cn(\"truncate\", labelClassName)}>\n                  {item.label}\n                </span>\n              </button>\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/rail-list.tsx"
    }
  ],
  "meta": {
    "tags": [
      "selectable-list",
      "tab-list",
      "roving-focus",
      "horizontal",
      "underline",
      "indicator",
      "controlled",
      "uncontrolled",
      "optional-selection",
      "list"
    ],
    "effects": [
      "indicator-reveal",
      "reduced-motion"
    ]
  },
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}