{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "section-list",
  "title": "Section List",
  "description": "A grouped list with sticky section headings and an alphabet index that jumps on tap or drag, announces the letter and works with a keyboard.",
  "files": [
    {
      "path": "registry/tealmac/section-list.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface ListSection<T> {\n  /** Short key shown on the scrubber (\"A\", \"B\", \"#\"). */\n  id: string\n  /** Heading of the section. Defaults to `id`. */\n  title?: string\n  items: T[]\n}\n\nexport interface SectionListProps<T> extends Omit<\n  React.ComponentProps<\"div\">,\n  \"children\"\n> {\n  sections: ListSection<T>[]\n  renderItem: (item: T, index: number) => React.ReactNode\n  getKey: (item: T) => string\n  /** Show the alphabet index along the end edge. Default true. */\n  scrubber?: boolean\n  /** Accessible name of the list. */\n  label: string\n}\n\n/**\n * A list grouped under sticky headings with an index along the edge. Press,\n * or drag a finger down the index to jump between sections; the current\n * letter is announced and shown in a bubble. With a keyboard, the index is a\n * list of buttons you can Tab to. The component scrolls itself: give it a\n * height.\n */\nexport function SectionList<T>({\n  sections,\n  renderItem,\n  getKey,\n  scrubber = true,\n  label,\n  className,\n  ...props\n}: SectionListProps<T>) {\n  const scrollRef = React.useRef<HTMLDivElement>(null)\n  const indexRef = React.useRef<HTMLDivElement>(null)\n  const [current, setCurrent] = React.useState<string | null>(null)\n  const [announce, setAnnounce] = React.useState(\"\")\n  const clearTimer = React.useRef<number | undefined>(undefined)\n\n  const jump = React.useCallback((id: string) => {\n    const root = scrollRef.current\n    const target = root?.querySelector<HTMLElement>(\n      `[data-section=\"${CSS.escape(id)}\"]`\n    )\n    if (!root || !target) return\n    root.scrollTop = target.offsetTop - root.offsetTop\n    setCurrent(id)\n    setAnnounce(`Section ${id}`)\n    window.clearTimeout(clearTimer.current)\n    clearTimer.current = window.setTimeout(() => setCurrent(null), 700)\n  }, [])\n\n  const fromPoint = (x: number, y: number) => {\n    const el = document.elementFromPoint?.(x, y)\n    const id = el?.closest<HTMLElement>(\"[data-index]\")?.dataset.index\n    if (id) jump(id)\n  }\n\n  return (\n    <div\n      data-slot=\"section-list\"\n      className={cn(\"relative flex min-h-0\", className)}\n      {...props}\n    >\n      <div\n        ref={scrollRef}\n        role=\"region\"\n        aria-label={label}\n        // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n        tabIndex={0}\n        className=\"focus-visible:ring-ring min-w-0 flex-1 overflow-y-auto scroll-smooth outline-none focus-visible:ring-2 focus-visible:ring-inset motion-reduce:scroll-auto\"\n      >\n        {sections.map((section) => (\n          <section\n            key={section.id}\n            data-section={section.id}\n            aria-label={section.title ?? section.id}\n          >\n            <h3 className=\"bg-muted/90 text-muted-foreground sticky top-0 z-10 px-4 py-1 text-xs font-semibold backdrop-blur\">\n              {section.title ?? section.id}\n            </h3>\n            <ul className=\"divide-y\">\n              {section.items.map((item, index) => (\n                <li key={getKey(item)}>{renderItem(item, index)}</li>\n              ))}\n            </ul>\n          </section>\n        ))}\n      </div>\n      {scrubber && (\n        <nav\n          aria-label={`${label} index`}\n          className=\"flex w-6 shrink-0 items-center justify-center\"\n        >\n          <div\n            ref={indexRef}\n            onPointerDown={(event) => {\n              if (event.pointerType === \"mouse\") return\n              event.currentTarget.setPointerCapture?.(event.pointerId)\n              fromPoint(event.clientX, event.clientY)\n            }}\n            onPointerMove={(event) => {\n              if (event.buttons) fromPoint(event.clientX, event.clientY)\n            }}\n            className=\"flex touch-none flex-col items-center py-1\"\n          >\n            {sections.map((section) => (\n              <button\n                key={section.id}\n                type=\"button\"\n                data-index={section.id}\n                aria-label={`Jump to ${section.title ?? section.id}`}\n                onClick={() => jump(section.id)}\n                className=\"text-primary focus-visible:ring-ring grid h-4 w-6 place-items-center rounded text-[10px] leading-none font-semibold outline-none focus-visible:ring-2\"\n              >\n                {section.id}\n              </button>\n            ))}\n          </div>\n        </nav>\n      )}\n      {current && (\n        <div\n          aria-hidden\n          className=\"bg-foreground/80 text-background pointer-events-none absolute end-12 top-1/2 grid size-14 -translate-y-1/2 place-items-center rounded-2xl text-2xl font-bold\"\n        >\n          {current}\n        </div>\n      )}\n      <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}