{"version":3,"file":"use-roving-index.cjs","names":["useUncontrolled"],"sources":["../../src/use-roving-index/use-roving-index.ts"],"sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\nimport { useUncontrolled } from '../use-uncontrolled/use-uncontrolled';\n\nexport interface UseRovingIndexInput {\n  /** Total number of items in the group */\n  total: number;\n\n  /** Which arrow keys navigate, `'horizontal'` by default */\n  orientation?: 'horizontal' | 'vertical' | 'both';\n\n  /** Whether navigation wraps at boundaries, `true` by default */\n  loop?: boolean;\n\n  /** Text direction, `'ltr'` by default */\n  dir?: 'rtl' | 'ltr';\n\n  /** Whether to click element when it receives focus via keyboard, `false` by default */\n  activateOnFocus?: boolean;\n\n  /** Number of columns for grid (2D) navigation. When set, enables grid mode */\n  columns?: number;\n\n  /** Controlled focused index */\n  focusedIndex?: number;\n\n  /** Initial focused index for uncontrolled mode, first non-disabled item by default */\n  initialIndex?: number;\n\n  /** Called when focused index changes */\n  onFocusChange?: (index: number) => void;\n\n  /** Function to check if item at given index is disabled, `() => false` by default */\n  isItemDisabled?: (index: number) => boolean;\n}\n\nexport interface UseRovingIndexGetItemPropsInput {\n  /** Index of the item in the group */\n  index: number;\n\n  /** Called when item is clicked */\n  onClick?: React.MouseEventHandler;\n\n  /** Called when keydown event fires on item */\n  onKeyDown?: React.KeyboardEventHandler;\n}\n\nexport interface UseRovingIndexReturnValue {\n  /** Get props to spread on each navigable item */\n  getItemProps: (options: UseRovingIndexGetItemPropsInput) => {\n    tabIndex: 0 | -1;\n    onKeyDown: React.KeyboardEventHandler;\n    onClick: React.MouseEventHandler;\n    ref: React.RefCallback<HTMLElement>;\n  };\n\n  /** Currently focused index */\n  focusedIndex: number;\n\n  /** Programmatically set focused index */\n  setFocusedIndex: (index: number) => void;\n}\n\nfunction findNextEnabled(\n  current: number,\n  total: number,\n  isItemDisabled: (index: number) => boolean,\n  loop: boolean\n): number {\n  for (let i = current + 1; i < total; i += 1) {\n    if (!isItemDisabled(i)) {\n      return i;\n    }\n  }\n\n  if (loop) {\n    for (let i = 0; i < current; i += 1) {\n      if (!isItemDisabled(i)) {\n        return i;\n      }\n    }\n  }\n\n  return current;\n}\n\nfunction findPreviousEnabled(\n  current: number,\n  total: number,\n  isItemDisabled: (index: number) => boolean,\n  loop: boolean\n): number {\n  for (let i = current - 1; i >= 0; i -= 1) {\n    if (!isItemDisabled(i)) {\n      return i;\n    }\n  }\n\n  if (loop) {\n    for (let i = total - 1; i > current; i -= 1) {\n      if (!isItemDisabled(i)) {\n        return i;\n      }\n    }\n  }\n\n  return current;\n}\n\nfunction findFirstEnabled(total: number, isItemDisabled: (index: number) => boolean): number {\n  for (let i = 0; i < total; i += 1) {\n    if (!isItemDisabled(i)) {\n      return i;\n    }\n  }\n\n  return 0;\n}\n\nfunction findLastEnabled(total: number, isItemDisabled: (index: number) => boolean): number {\n  for (let i = total - 1; i >= 0; i -= 1) {\n    if (!isItemDisabled(i)) {\n      return i;\n    }\n  }\n\n  return 0;\n}\n\nconst defaultIsItemDisabled = () => false;\n\nexport function useRovingIndex(input: UseRovingIndexInput): UseRovingIndexReturnValue {\n  const {\n    total,\n    orientation = 'horizontal',\n    loop = true,\n    dir = 'ltr',\n    activateOnFocus = false,\n    columns,\n    focusedIndex,\n    initialIndex,\n    onFocusChange,\n    isItemDisabled = defaultIsItemDisabled,\n  } = input;\n\n  const itemRefs = useRef<Map<number, HTMLElement>>(new Map());\n  const isGrid = typeof columns === 'number' && columns > 0;\n\n  const resolvedInitialIndex =\n    initialIndex !== undefined ? initialIndex : findFirstEnabled(total, isItemDisabled);\n\n  const [activeIndex, setActiveIndex] = useUncontrolled({\n    value: focusedIndex,\n    defaultValue: resolvedInitialIndex,\n    finalValue: 0,\n    onChange: onFocusChange,\n  });\n\n  useEffect(() => {\n    if (total === 0) {\n      return;\n    }\n\n    if (activeIndex >= total) {\n      setActiveIndex(findLastEnabled(total, isItemDisabled));\n    } else if (isItemDisabled(activeIndex)) {\n      setActiveIndex(findFirstEnabled(total, isItemDisabled));\n    }\n  }, [total, activeIndex, isItemDisabled]);\n\n  const focusItem = useCallback(\n    (index: number) => {\n      setActiveIndex(index);\n      const element = itemRefs.current.get(index);\n      if (element) {\n        element.focus();\n        if (activateOnFocus) {\n          element.click();\n        }\n      }\n    },\n    [activateOnFocus, setActiveIndex]\n  );\n\n  const handleGridKeyDown = useCallback(\n    (event: React.KeyboardEvent, currentIndex: number) => {\n      const row = Math.floor(currentIndex / columns!);\n      const col = currentIndex % columns!;\n      const totalRows = Math.ceil(total / columns!);\n      let nextIndex: number | null = null;\n\n      const isRtl = dir === 'rtl';\n\n      switch (event.key) {\n        case 'ArrowRight': {\n          const targetCol = isRtl ? col - 1 : col + 1;\n          if (targetCol >= 0 && targetCol < columns! && row * columns! + targetCol < total) {\n            const candidate = row * columns! + targetCol;\n            if (!isItemDisabled(candidate)) {\n              nextIndex = candidate;\n            }\n          }\n          break;\n        }\n\n        case 'ArrowLeft': {\n          const targetCol = isRtl ? col + 1 : col - 1;\n          if (targetCol >= 0 && targetCol < columns! && row * columns! + targetCol < total) {\n            const candidate = row * columns! + targetCol;\n            if (!isItemDisabled(candidate)) {\n              nextIndex = candidate;\n            }\n          }\n          break;\n        }\n\n        case 'ArrowDown': {\n          for (let r = row + 1; r < totalRows; r += 1) {\n            const candidate = r * columns! + col;\n            if (candidate < total && !isItemDisabled(candidate)) {\n              nextIndex = candidate;\n              break;\n            }\n          }\n          break;\n        }\n\n        case 'ArrowUp': {\n          for (let r = row - 1; r >= 0; r -= 1) {\n            const candidate = r * columns! + col;\n            if (candidate < total && !isItemDisabled(candidate)) {\n              nextIndex = candidate;\n              break;\n            }\n          }\n          break;\n        }\n\n        case 'Home': {\n          if (event.ctrlKey) {\n            nextIndex = findFirstEnabled(total, isItemDisabled);\n          } else {\n            const rowStart = row * columns!;\n            for (let i = rowStart; i < rowStart + columns! && i < total; i += 1) {\n              if (!isItemDisabled(i)) {\n                nextIndex = i;\n                break;\n              }\n            }\n          }\n          break;\n        }\n\n        case 'End': {\n          if (event.ctrlKey) {\n            nextIndex = findLastEnabled(total, isItemDisabled);\n          } else {\n            const rowStart = row * columns!;\n            const rowEnd = Math.min(rowStart + columns!, total) - 1;\n            for (let i = rowEnd; i >= rowStart; i -= 1) {\n              if (!isItemDisabled(i)) {\n                nextIndex = i;\n                break;\n              }\n            }\n          }\n          break;\n        }\n      }\n\n      if (nextIndex !== null && nextIndex !== currentIndex) {\n        event.preventDefault();\n        event.stopPropagation();\n        focusItem(nextIndex);\n      }\n    },\n    [total, columns, dir, isItemDisabled, focusItem]\n  );\n\n  const handleListKeyDown = useCallback(\n    (event: React.KeyboardEvent, currentIndex: number) => {\n      const isRtl = dir === 'rtl';\n      let nextIndex: number | null = null;\n\n      switch (event.key) {\n        case 'ArrowRight': {\n          if (orientation === 'horizontal' || orientation === 'both') {\n            nextIndex = isRtl\n              ? findPreviousEnabled(currentIndex, total, isItemDisabled, loop)\n              : findNextEnabled(currentIndex, total, isItemDisabled, loop);\n          }\n          break;\n        }\n\n        case 'ArrowLeft': {\n          if (orientation === 'horizontal' || orientation === 'both') {\n            nextIndex = isRtl\n              ? findNextEnabled(currentIndex, total, isItemDisabled, loop)\n              : findPreviousEnabled(currentIndex, total, isItemDisabled, loop);\n          }\n          break;\n        }\n\n        case 'ArrowDown': {\n          if (orientation === 'vertical' || orientation === 'both') {\n            nextIndex = findNextEnabled(currentIndex, total, isItemDisabled, loop);\n          }\n          break;\n        }\n\n        case 'ArrowUp': {\n          if (orientation === 'vertical' || orientation === 'both') {\n            nextIndex = findPreviousEnabled(currentIndex, total, isItemDisabled, loop);\n          }\n          break;\n        }\n\n        case 'Home': {\n          nextIndex = findFirstEnabled(total, isItemDisabled);\n          break;\n        }\n\n        case 'End': {\n          nextIndex = findLastEnabled(total, isItemDisabled);\n          break;\n        }\n      }\n\n      if (nextIndex !== null && nextIndex !== currentIndex) {\n        event.preventDefault();\n        event.stopPropagation();\n        focusItem(nextIndex);\n      }\n    },\n    [total, orientation, loop, dir, isItemDisabled, focusItem]\n  );\n\n  const getItemProps = useCallback(\n    (options: UseRovingIndexGetItemPropsInput) => {\n      const { index, onClick, onKeyDown } = options;\n\n      return {\n        tabIndex: (index === activeIndex ? 0 : -1) as 0 | -1,\n\n        ref: (node: HTMLElement | null) => {\n          if (node) {\n            itemRefs.current.set(index, node);\n          } else {\n            itemRefs.current.delete(index);\n          }\n        },\n\n        onKeyDown: (event: React.KeyboardEvent) => {\n          onKeyDown?.(event);\n\n          if (event.defaultPrevented) {\n            return;\n          }\n\n          if (isGrid) {\n            handleGridKeyDown(event, index);\n          } else {\n            handleListKeyDown(event, index);\n          }\n        },\n\n        onClick: (event: React.MouseEvent) => {\n          onClick?.(event);\n          setActiveIndex(index);\n        },\n      };\n    },\n    [activeIndex, isGrid, handleGridKeyDown, handleListKeyDown, setActiveIndex]\n  );\n\n  return {\n    getItemProps,\n    focusedIndex: activeIndex,\n    setFocusedIndex: setActiveIndex,\n  };\n}\n\nexport namespace useRovingIndex {\n  export type Input = UseRovingIndexInput;\n  export type GetItemPropsInput = UseRovingIndexGetItemPropsInput;\n  export type ReturnValue = UseRovingIndexReturnValue;\n}\n"],"mappings":"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"}