{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "LineSidebar-TS-TW",
	"title": "LineSidebar",
	"description": "Static list navigation with a cursor-proximity effect that shifts and highlights nearby items.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "LineSidebar/LineSidebar.tsx",
			"content": "import { useRef, useState, useCallback, useEffect, type CSSProperties } from 'react';\n\ntype Falloff = 'linear' | 'smooth' | 'sharp';\n\nexport interface LineSidebarProps {\n  items?: string[];\n  accentColor?: string;\n  textColor?: string;\n  markerColor?: string;\n  showIndex?: boolean;\n  showMarker?: boolean;\n  proximityRadius?: number;\n  maxShift?: number;\n  falloff?: Falloff;\n  markerLength?: number;\n  markerGap?: number;\n  tickScale?: number;\n  scaleTick?: boolean;\n  itemGap?: number;\n  fontSize?: number;\n  smoothing?: number;\n  defaultActive?: number | null;\n  onItemClick?: (index: number, label: string) => void;\n  className?: string;\n}\n\nconst FALLOFF_CURVES: Record<Falloff, (p: number) => number> = {\n  linear: p => p,\n  smooth: p => p * p * (3 - 2 * p),\n  sharp: p => p * p * p\n};\n\nconst DEFAULT_ITEMS = [\n  'Overview',\n  'Components',\n  'Animations',\n  'Backgrounds',\n  'Showcase',\n  'Playground',\n  'Templates',\n  'Changelog',\n  'Community',\n  'Resources',\n  'Documentation',\n  'Support'\n];\n\nconst LineSidebar = ({\n  items = DEFAULT_ITEMS,\n  accentColor = '#A855F7',\n  textColor = '#c4c4c4',\n  markerColor = '#6c6c6c',\n  showIndex = true,\n  showMarker = true,\n  proximityRadius = 100,\n  maxShift = 30,\n  falloff = 'smooth',\n  markerLength = 60,\n  markerGap = 0,\n  tickScale = 0.5,\n  scaleTick = true,\n  itemGap = 20,\n  fontSize = 1.1,\n  smoothing = 100,\n  defaultActive = null,\n  onItemClick,\n  className = ''\n}: LineSidebarProps) => {\n  const listRef = useRef<HTMLUListElement>(null);\n  const itemRefs = useRef<(HTMLLIElement | null)[]>([]);\n  const targetsRef = useRef<number[]>([]);\n  const currentRef = useRef<number[]>([]);\n  const rafRef = useRef<number | null>(null);\n  const lastRef = useRef(0);\n  const activeRef = useRef<number | null>(defaultActive);\n  const smoothingRef = useRef(smoothing);\n  const [activeIndex, setActiveIndex] = useState<number | null>(defaultActive);\n\n  activeRef.current = activeIndex;\n  smoothingRef.current = smoothing;\n\n  // Single rAF loop that eases every item's --effect toward its target using\n  // frame-rate independent exponential smoothing, so color, shift and scale\n  // all move together without staggering CSS transitions.\n  const runFrame = useCallback((now: number) => {\n    const dt = Math.min((now - lastRef.current) / 1000, 0.05);\n    lastRef.current = now;\n    const tau = Math.max(smoothingRef.current, 1) / 1000;\n    const k = 1 - Math.exp(-dt / tau);\n\n    let moving = false;\n    const items = itemRefs.current;\n    for (let i = 0; i < items.length; i++) {\n      const el = items[i];\n      if (!el) continue;\n      const target = Math.max(targetsRef.current[i] || 0, activeRef.current === i ? 1 : 0);\n      const cur = currentRef.current[i] || 0;\n      const next = cur + (target - cur) * k;\n      const settled = Math.abs(target - next) < 0.0015;\n      const value = settled ? target : next;\n      currentRef.current[i] = value;\n      el.style.setProperty('--effect', value.toFixed(4));\n      if (!settled) moving = true;\n    }\n\n    rafRef.current = moving ? requestAnimationFrame(runFrame) : null;\n  }, []);\n\n  const startLoop = useCallback(() => {\n    if (rafRef.current != null) {\n      cancelAnimationFrame(rafRef.current);\n    }\n    lastRef.current = performance.now();\n    rafRef.current = requestAnimationFrame(runFrame);\n  }, [runFrame]);\n\n  const handlePointerMove = useCallback(\n    (e: React.PointerEvent<HTMLUListElement>) => {\n      const list = listRef.current;\n      if (!list) return;\n      const rect = list.getBoundingClientRect();\n      const pointerY = e.clientY - rect.top;\n      const ease = FALLOFF_CURVES[falloff] ?? FALLOFF_CURVES.linear;\n      const items = itemRefs.current;\n      for (let i = 0; i < items.length; i++) {\n        const el = items[i];\n        if (!el) continue;\n        const center = el.offsetTop + el.offsetHeight / 2;\n        const distance = Math.abs(pointerY - center);\n        targetsRef.current[i] = ease(Math.max(0, 1 - distance / proximityRadius));\n      }\n      startLoop();\n    },\n    [falloff, proximityRadius, startLoop]\n  );\n\n  const handlePointerLeave = useCallback(() => {\n    targetsRef.current = targetsRef.current.map(() => 0);\n    startLoop();\n  }, [startLoop]);\n\n  const handleClick = useCallback(\n    (index: number, label: string) => {\n      setActiveIndex(index);\n      onItemClick?.(index, label);\n    },\n    [onItemClick]\n  );\n\n  useEffect(() => {\n    startLoop();\n  }, [activeIndex, startLoop]);\n\n  useEffect(\n    () => () => {\n      if (rafRef.current != null) cancelAnimationFrame(rafRef.current);\n      rafRef.current = null;\n    },\n    []\n  );\n\n  const tickClass = showMarker\n    ? `after:absolute after:left-[calc(-1*var(--marker-length)-var(--marker-gap))] after:top-[calc(100%+var(--item-gap)/2)] after:h-px after:opacity-50 after:content-[''] last:after:content-none after:[background-color:var(--marker-color)] after:[width:calc(var(--marker-length)*var(--tick-scale))] ${\n        scaleTick\n          ? \"after:origin-left after:[transform:translateY(-50%)_scaleX(calc(0.7+var(--effect,0)*0.6))]\"\n          : 'after:-translate-y-1/2'\n      }`\n    : '';\n\n  return (\n    <nav\n      className={`relative flex justify-start${showMarker ? ' [padding-left:calc(var(--marker-length)+var(--marker-gap))]' : ''}${className ? ` ${className}` : ''}`}\n      style={\n        {\n          '--accent-color': accentColor,\n          '--text-color': textColor,\n          '--marker-color': markerColor,\n          '--marker-length': `${markerLength}px`,\n          '--marker-gap': `${markerGap}px`,\n          '--tick-scale': tickScale,\n          '--max-shift': `${maxShift}px`,\n          '--item-gap': `${itemGap}px`,\n          '--font-size': `${fontSize}rem`,\n          '--smoothing': `${smoothing}ms`\n        } as CSSProperties\n      }\n    >\n      <ul\n        ref={listRef}\n        onPointerMove={handlePointerMove}\n        onPointerLeave={handlePointerLeave}\n        className=\"m-0 flex list-none flex-col py-4 [gap:var(--item-gap)]\"\n      >\n        {items.map((label, index) => (\n          <li\n            key={`${label}-${index}`}\n            ref={el => {\n              itemRefs.current[index] = el;\n            }}\n            aria-current={activeIndex === index ? 'true' : undefined}\n            onClick={() => handleClick(index, label)}\n            className={`relative cursor-pointer before:absolute before:-inset-x-12 before:-inset-y-[6px] before:content-[''] ${tickClass}`}\n          >\n            {showMarker && (\n              <span\n                aria-hidden=\"true\"\n                className=\"absolute left-[calc(-1*var(--marker-length)-var(--marker-gap))] top-1/2 h-px w-[length:var(--marker-length)] origin-left [background-color:color-mix(in_srgb,var(--accent-color)_calc(var(--effect,0)*100%),var(--marker-color))] [transform:translateY(-50%)_scaleX(calc(0.7+var(--effect,0)*0.5))]\"\n              />\n            )}\n            <span className=\"relative inline-flex items-baseline leading-[1.2] [color:color-mix(in_srgb,var(--accent-color)_calc(var(--effect,0)*100%),var(--text-color))] [font-size:var(--font-size)] [transform:translateX(calc(var(--effect,0)*var(--max-shift)))]\">\n              {showIndex && (\n                <span className=\"mr-[0.6rem] font-mono text-[0.85em] [opacity:calc(0.55+var(--effect,0)*0.45)]\">\n                  {String(index + 1).padStart(2, '0')}\n                </span>\n              )}\n              <span>{label}</span>\n            </span>\n          </li>\n        ))}\n      </ul>\n    </nav>\n  );\n};\n\nexport default LineSidebar;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": []
}