{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "super-hover-list",
  "type": "registry:ui",
  "title": "Super Hover List",
  "description": "A scrollable index whose active row — and its revealed artwork — keeps tracking the cursor while the list scrolls beneath it, unlike native hover, via frame-by-frame hit-testing.",
  "registryDependencies": [
    "https://crafterui.com/r/super-hover.json"
  ],
  "files": [
    {
      "path": "registry/crafterui/ui/super-hover-list.tsx",
      "type": "registry:ui",
      "content": "\"use client\"\n\n// Inspired by Super Hover by Daniel Petho (https://super-hover.danielpetho.com) — MIT licensed.\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { createSuperHover } from \"@/registry/crafterui/ui/super-hover\"\n\nexport interface SuperHoverListItem {\n  /** Stable key; falls back to the row index. @default undefined */\n  id?: string | number\n  /** Primary cell, e.g. an album / file title. */\n  title: string\n  /** Secondary cell, e.g. an artist or author. @default undefined */\n  subtitle?: string\n  /** Trailing cell, e.g. a year. @default undefined */\n  meta?: string\n  /** Image revealed while the row is active (any URL). @default undefined */\n  image?: string\n}\n\nexport interface SuperHoverListProps {\n  /** Rows to render in the index. */\n  items: SuperHoverListItem[]\n  /**\n   * `super` keeps the active row in sync while scrolling under a still cursor;\n   * `native` uses the browser's `:hover` (only updates when the pointer moves).\n   * @default \"super\"\n   */\n  mode?: \"super\" | \"native\"\n  /**\n   * Auto-scroll the list and walk the active row down it (great for previews and\n   * showcasing the effect without a pointer). Pauses while hovered. @default false\n   */\n  autoplay?: boolean\n  /** Auto-scroll speed in pixels per frame when `autoplay` is on. @default 0.35 */\n  speed?: number\n  /** Edge length of the revealed artwork in pixels. @default 112 */\n  artworkSize?: number\n  /** Extra classes for the outer container. @default undefined */\n  className?: string\n}\n\n/** Fraction down the viewport the autoplay \"playhead\" sits at. */\nconst AUTOPLAY_ANCHOR = 0.42\n\nexport function SuperHoverList({\n  items,\n  mode = \"super\",\n  autoplay = false,\n  speed = 0.35,\n  artworkSize = 112,\n  className,\n}: SuperHoverListProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const rowRefs = React.useRef<(HTMLDivElement | null)[]>([])\n  const hoveringRef = React.useRef(false)\n  const autoActiveRef = React.useRef<HTMLElement | null>(null)\n\n  // Double the rows for a seamless autoplay loop; render once otherwise.\n  const rows = React.useMemo(\n    () => (autoplay ? [...items, ...items] : items),\n    [items, autoplay]\n  )\n\n  // Flip the artwork below its row when revealing it above would clip the top edge.\n  const placeArtwork = React.useCallback(\n    (row: HTMLElement | null) => {\n      const root = rootRef.current\n      if (!row || !root || !root.contains(row)) return\n      const rootRect = root.getBoundingClientRect()\n      const rowRect = row.getBoundingClientRect()\n      const wouldClipTop = rowRect.bottom - artworkSize < rootRect.top\n      row.toggleAttribute(\"data-artwork-below\", wouldClipTop)\n    },\n    [artworkSize]\n  )\n\n  // Super Hover controller + artwork placement on the events it dispatches.\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root || mode !== \"super\") return\n\n    const ctrl = createSuperHover({ root })\n    const onActive = (e: Event) => {\n      const target = e.target\n      if (target instanceof Element) {\n        placeArtwork(target.closest<HTMLElement>(\"[data-super-hover]\"))\n      }\n    }\n    root.addEventListener(\"superhoverenter\", onActive)\n    root.addEventListener(\"superhovermove\", onActive)\n    return () => {\n      root.removeEventListener(\"superhoverenter\", onActive)\n      root.removeEventListener(\"superhovermove\", onActive)\n      ctrl.destroy()\n    }\n  }, [mode, placeArtwork])\n\n  // Native mode: place artwork on plain mouseover.\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root || mode !== \"native\") return\n    const onOver = (e: MouseEvent) => {\n      if (e.target instanceof Element) {\n        placeArtwork(e.target.closest<HTMLElement>(\"[data-super-hover]\"))\n      }\n    }\n    root.addEventListener(\"mouseover\", onOver)\n    return () => root.removeEventListener(\"mouseover\", onOver)\n  }, [mode, placeArtwork])\n\n  // Autoplay: scroll the list and keep the row nearest the playhead active.\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root || !autoplay) return\n\n    const prefersReduced =\n      typeof window !== \"undefined\" &&\n      !!window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches\n    if (prefersReduced) return\n\n    let raf = 0\n    const setAutoActive = (row: HTMLElement | null) => {\n      if (autoActiveRef.current === row) return\n      autoActiveRef.current?.removeAttribute(\"data-autoplay-active\")\n      autoActiveRef.current = row\n      if (row) {\n        row.setAttribute(\"data-autoplay-active\", \"\")\n        placeArtwork(row)\n      }\n    }\n\n    const frame = () => {\n      if (!hoveringRef.current) {\n        // Wrap by the height of ONE copy (top of the first duplicated row minus\n        // the first row), not scrollHeight/2 — the latter folds in the scroll\n        // container's vertical padding and makes the loop jump each cycle.\n        const first = rowRefs.current[0]\n        const mid = rowRefs.current[items.length]\n        const period = first && mid ? mid.offsetTop - first.offsetTop : 0\n        if (period > 0 && root.scrollTop >= period) root.scrollTop -= period\n        root.scrollTop += speed\n\n        const box = root.getBoundingClientRect()\n        const anchor = box.top + box.height * AUTOPLAY_ANCHOR\n        let best: HTMLElement | null = null\n        let bestDist = Infinity\n        for (const row of rowRefs.current) {\n          if (!row) continue\n          const rect = row.getBoundingClientRect()\n          if (rect.bottom < box.top || rect.top > box.bottom) continue\n          const dist = Math.abs(rect.top + rect.height / 2 - anchor)\n          if (dist < bestDist) {\n            bestDist = dist\n            best = row\n          }\n        }\n        setAutoActive(best)\n      }\n      raf = requestAnimationFrame(frame)\n    }\n    raf = requestAnimationFrame(frame)\n    return () => {\n      cancelAnimationFrame(raf)\n      setAutoActive(null)\n    }\n  }, [autoplay, speed, placeArtwork, items.length])\n\n  const revealSelector =\n    mode === \"super\"\n      ? \"[&[data-super-hover-active]_.sh-art]:opacity-100 [&[data-super-hover-active]]:border-b-current [&[data-autoplay-active]_.sh-art]:opacity-100 [&[data-autoplay-active]]:border-b-current\"\n      : \"[&:hover_.sh-art]:opacity-100 hover:border-b-current [&[data-autoplay-active]_.sh-art]:opacity-100 [&[data-autoplay-active]]:border-b-current\"\n\n  return (\n    <div\n      className={cn(\n        \"bg-background text-foreground relative h-full w-full overflow-hidden font-mono uppercase\",\n        className\n      )}\n    >\n      <div\n        ref={rootRef}\n        onPointerEnter={() => {\n          hoveringRef.current = true\n          autoActiveRef.current?.removeAttribute(\"data-autoplay-active\")\n          autoActiveRef.current = null\n        }}\n        onPointerLeave={() => {\n          hoveringRef.current = false\n        }}\n        className=\"h-full cursor-pointer [scrollbar-width:none] overflow-x-hidden overflow-y-auto overscroll-contain [mask-image:linear-gradient(to_bottom,transparent,#000_12%,#000_88%,transparent)] px-4 py-6 sm:px-8 [&::-webkit-scrollbar]:hidden\"\n      >\n        <div className=\"mx-auto grid w-full max-w-5xl grid-cols-[3rem_minmax(0,42%)_minmax(0,1fr)_minmax(3.5rem,16%)_3.5rem] text-xs sm:text-sm\">\n          {rows.map((item, i) => (\n            <div\n              key={item.id != null ? `${item.id}-${i}` : i}\n              data-super-hover\n              ref={(el) => {\n                rowRefs.current[i] = el\n              }}\n              className={cn(\n                \"col-span-5 grid grid-cols-subgrid items-center gap-x-2 border-b border-transparent py-1.5 transition-colors duration-150\",\n                \"[&[data-artwork-below]_.sh-art]:top-[calc(100%+0.5rem)] [&[data-artwork-below]_.sh-art]:bottom-auto\",\n                revealSelector\n              )}\n            >\n              <div className=\"tabular-nums opacity-60\">\n                {String((i % items.length) + 1).padStart(3, \"0\")}\n              </div>\n              <div className=\"min-w-0 truncate\">{item.title}</div>\n              <div className=\"min-w-0 truncate opacity-70\">{item.subtitle}</div>\n              <div className=\"relative h-full min-w-0\">\n                {item.image ? (\n                  <div\n                    aria-hidden\n                    className=\"sh-art pointer-events-none absolute bottom-0 left-1/2 z-20 -translate-x-1/2 bg-cover bg-center opacity-0 shadow-lg transition-opacity duration-200 ease-out\"\n                    style={{\n                      width: artworkSize,\n                      height: artworkSize,\n                      backgroundImage: `url(${item.image})`,\n                    }}\n                  />\n                ) : null}\n              </div>\n              <div className=\"text-right tabular-nums opacity-60\">\n                {item.meta ?? \"—\"}\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  )\n}\n"
    }
  ]
}
