{"version":3,"file":"scopedKeydownHandler.cjs","sources":["../../../../src/components/utils/scopedKeydownHandler.ts"],"sourcesContent":["export interface ScopedKeydownHandlerOptions {\n  parentSelector: string;\n  siblingSelector: string;\n  loop?: boolean;\n  orientation?: 'vertical' | 'horizontal';\n  onKeyDown?: (event: React.KeyboardEvent<HTMLElement>) => void;\n}\n\n// Typeahead state is module-scoped (shared by every handler instance) because\n// focus moves between siblings as keys are pressed — each keydown lands on a\n// different element's handler, so a per-handler buffer could never accumulate.\n// Only one roving-focus group holds keyboard focus at a time; the parent check\n// resets the buffer when focus moves to a different group.\nconst TYPEAHEAD_RESET_MS = 500;\nlet typeaheadBuffer = '';\nlet typeaheadExpiresAt = 0;\nlet typeaheadParent: Element | null = null;\n\nfunction isTypeaheadKey(event: React.KeyboardEvent<HTMLElement>): boolean {\n  // single printable character; Space activates items, modifiers are shortcuts\n  return (\n    event.key.length === 1 &&\n    event.key !== ' ' &&\n    !event.ctrlKey &&\n    !event.altKey &&\n    !event.metaKey\n  );\n}\n\nfunction getTypeaheadIndex(\n  siblings: HTMLElement[],\n  currentIndex: number,\n  parent: Element,\n  key: string\n): number | null {\n  const now = Date.now();\n  if (now > typeaheadExpiresAt || parent !== typeaheadParent) {\n    typeaheadBuffer = '';\n  }\n  typeaheadParent = parent;\n  typeaheadExpiresAt = now + TYPEAHEAD_RESET_MS;\n  typeaheadBuffer += key.toLowerCase();\n\n  // repeated presses of the same character cycle through matches instead of\n  // accumulating into an unmatchable prefix (\"dd\")\n  const chars = typeaheadBuffer.split('');\n  const isRepeat = chars.every((ch) => ch === chars[0]);\n  const query = isRepeat ? chars[0] : typeaheadBuffer;\n\n  // search wraps, starting AFTER the focused item; the focused item itself is\n  // checked last so a longer prefix that still matches it keeps focus in place\n  for (let i = 1; i <= siblings.length; i++) {\n    const candidateIndex = (currentIndex + i) % siblings.length;\n    const text = (siblings[candidateIndex].textContent || '').trim().toLowerCase();\n    if (text.startsWith(query)) {\n      return candidateIndex;\n    }\n  }\n  return null;\n}\n\nexport function createScopedKeydownHandler(\n  options: ScopedKeydownHandlerOptions\n): (event: React.KeyboardEvent<HTMLElement>) => void {\n  const {\n    parentSelector,\n    siblingSelector,\n    loop = true,\n    orientation = 'vertical',\n    onKeyDown,\n  } = options;\n\n  const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';\n  const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';\n\n  return (event: React.KeyboardEvent<HTMLElement>) => {\n    onKeyDown?.(event);\n\n    const target = event.currentTarget;\n    const parent = target.closest(parentSelector);\n    if (!parent) return;\n\n    const siblings = Array.from(\n      parent.querySelectorAll<HTMLElement>(siblingSelector)\n    );\n\n    const currentIndex = siblings.indexOf(target);\n    if (currentIndex === -1) return;\n\n    let nextIndex: number | null = null;\n\n    switch (event.key) {\n      case nextKey: {\n        event.preventDefault();\n        if (currentIndex < siblings.length - 1) {\n          nextIndex = currentIndex + 1;\n        } else if (loop) {\n          nextIndex = 0;\n        }\n        break;\n      }\n      case prevKey: {\n        event.preventDefault();\n        if (currentIndex > 0) {\n          nextIndex = currentIndex - 1;\n        } else if (loop) {\n          nextIndex = siblings.length - 1;\n        }\n        break;\n      }\n      case 'Home': {\n        event.preventDefault();\n        nextIndex = 0;\n        break;\n      }\n      case 'End': {\n        event.preventDefault();\n        nextIndex = siblings.length - 1;\n        break;\n      }\n      default: {\n        if (isTypeaheadKey(event)) {\n          const matchIndex = getTypeaheadIndex(siblings, currentIndex, parent, event.key);\n          if (matchIndex !== null) {\n            event.preventDefault();\n            nextIndex = matchIndex;\n          }\n        }\n        break;\n      }\n    }\n\n    if (nextIndex !== null && siblings[nextIndex]) {\n      siblings[nextIndex].focus();\n    }\n  };\n}\n"],"names":[],"mappings":"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"}