{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "AccordionGallery-TS-TW",
	"title": "AccordionGallery",
	"description": "Panels expand on hover or focus, revealing parallax imagery and captions.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "AccordionGallery/AccordionGallery.tsx",
			"content": "'use client';\n\nimport { useRef, useEffect, useState, useCallback, CSSProperties, KeyboardEvent, MouseEvent } from 'react';\nimport { gsap } from 'gsap';\n\nexport interface AccordionGalleryItem {\n  image: string;\n  label?: string;\n  link?: string;\n  alt?: string;\n}\n\nexport interface AccordionGalleryProps {\n  items?: AccordionGalleryItem[];\n  defaultIndex?: number;\n  accentColor?: string;\n  overlayColor?: string;\n  textColor?: string;\n  height?: number;\n  gap?: number;\n  radius?: number;\n  expandRatio?: number;\n  orientation?: 'horizontal' | 'vertical';\n  duration?: number;\n  ease?: string;\n  parallax?: number;\n  tilt?: number;\n  stagger?: number;\n  trigger?: 'hover' | 'click';\n  showLabels?: boolean;\n  grayscale?: boolean;\n  className?: string;\n}\n\nconst DEFAULT_ITEMS: AccordionGalleryItem[] = [\n  { image: 'https://picsum.photos/id/1015/900/1200', label: 'Canyon', link: '#' },\n  { image: 'https://picsum.photos/id/1018/900/1200', label: 'Ridgeline', link: '#' },\n  { image: 'https://picsum.photos/id/1039/900/1200', label: 'Falls', link: '#' },\n  { image: 'https://picsum.photos/id/1043/900/1200', label: 'Harbour', link: '#' },\n  { image: 'https://picsum.photos/id/1044/900/1200', label: 'Skyline', link: '#' }\n];\n\nconst AccordionGallery = ({\n  items = DEFAULT_ITEMS,\n  defaultIndex = 2,\n  accentColor = '#ffffff',\n  overlayColor = '#060010',\n  textColor = '#ffffff',\n  height = 460,\n  gap = 10,\n  radius = 16,\n  expandRatio = 0.52,\n  orientation = 'horizontal',\n  duration = 0.6,\n  ease = 'power3.out',\n  parallax = 0.5,\n  tilt = 8,\n  stagger = 0.06,\n  trigger = 'hover',\n  showLabels = true,\n  grayscale = true,\n  className = ''\n}: AccordionGalleryProps) => {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const panelRefs = useRef<(HTMLElement | null)[]>([]);\n  const mediaRefs = useRef<(HTMLElement | null)[]>([]);\n  const barRefs = useRef<(HTMLElement | null)[]>([]);\n  const textRefs = useRef<(HTMLElement | null)[]>([]);\n  const tlRef = useRef<gsap.core.Timeline | null>(null);\n  const firstRunRef = useRef(true);\n  const mediaSizeRef = useRef(320);\n\n  const vertical = orientation === 'vertical';\n  const count = items.length;\n  const [active, setActive] = useState(Math.min(Math.max(defaultIndex, 0), count - 1));\n\n  const prefersReduced =\n    typeof window !== 'undefined' && window.matchMedia\n      ? window.matchMedia('(prefers-reduced-motion: reduce)').matches\n      : false;\n\n  const overlayBg = `linear-gradient(180deg, transparent 45%, color-mix(in srgb, ${overlayColor} 78%, transparent) 100%), color-mix(in srgb, ${overlayColor} calc(var(--ag-dim, 0.35) * 100%), transparent)`;\n\n  const applyLayout = useCallback(\n    (animate: boolean) => {\n      const panels = panelRefs.current;\n      if (!panels.length) return;\n\n      const r = Math.min(Math.max(expandRatio, 0.2), 0.9);\n      const grow = count > 1 ? (r * (count - 1)) / (1 - r) : 1;\n      const mediaSize = mediaSizeRef.current;\n\n      tlRef.current?.kill();\n      const dur = animate && !prefersReduced ? duration : 0;\n      const tl = gsap.timeline();\n\n      panels.forEach((panel, i) => {\n        if (!panel) return;\n        const isActive = i === active;\n        const media = mediaRefs.current[i];\n        const bar = barRefs.current[i];\n        const text = textRefs.current[i];\n\n        const rot = isActive ? 0 : i < active ? tilt : -tilt;\n        const rotProp = vertical ? { rotateX: -rot } : { rotateY: rot };\n\n        tl.to(panel, { flexGrow: isActive ? grow : 1, ...rotProp, duration: dur, ease }, 0);\n\n        if (media) {\n          const drift = Math.max(-1.5, Math.min(1.5, active - i));\n          const shift = drift * parallax * mediaSize * 0.06;\n          const gray = grayscale ? (isActive ? 0 : 1) : 0;\n          tl.to(\n            media,\n            {\n              xPercent: -50,\n              yPercent: -50,\n              x: vertical ? 0 : isActive ? 0 : shift,\n              y: vertical ? (isActive ? 0 : shift) : 0,\n              '--ag-gray': gray,\n              '--ag-dim': isActive ? 0 : 0.35,\n              duration: dur,\n              ease\n            },\n            0\n          );\n        }\n\n        if (showLabels && bar && text) {\n          if (isActive) {\n            tl.to([bar, text], { opacity: 1, x: 0, duration: dur, ease, stagger: prefersReduced ? 0 : stagger }, 0);\n          } else {\n            tl.to([bar, text], { opacity: 0, x: -14, duration: dur * 0.6, ease }, 0);\n          }\n        }\n      });\n\n      tlRef.current = tl;\n    },\n    [\n      active,\n      count,\n      expandRatio,\n      duration,\n      ease,\n      vertical,\n      tilt,\n      parallax,\n      grayscale,\n      showLabels,\n      stagger,\n      prefersReduced\n    ]\n  );\n\n  useEffect(() => {\n    const el = rootRef.current;\n    if (!el) return;\n\n    const measure = () => {\n      const rect = el.getBoundingClientRect();\n      const total = vertical ? rect.height : rect.width;\n      const usable = Math.max(total - gap * (count - 1), 120);\n      const size = Math.max(140, usable * Math.min(Math.max(expandRatio, 0.2), 0.9) * 1.22);\n      mediaSizeRef.current = size;\n      el.style.setProperty('--ag-media-size', `${size}px`);\n      applyLayout(!firstRunRef.current);\n    };\n\n    measure();\n    const ro = new ResizeObserver(measure);\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [applyLayout, gap, count, expandRatio, vertical]);\n\n  useEffect(() => {\n    applyLayout(!firstRunRef.current);\n    firstRunRef.current = false;\n  }, [applyLayout]);\n\n  useEffect(\n    () => () => {\n      tlRef.current?.kill();\n    },\n    []\n  );\n\n  const handleEnter = (i: number) => {\n    if (trigger === 'hover') setActive(i);\n  };\n\n  const handleClick = (i: number, e: MouseEvent) => {\n    if (i !== active) {\n      e.preventDefault();\n      setActive(i);\n    }\n  };\n\n  const handleKeyDown = (i: number, e: KeyboardEvent) => {\n    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n      e.preventDefault();\n      setActive((i + 1) % count);\n    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n      e.preventDefault();\n      setActive((i - 1 + count) % count);\n    }\n  };\n\n  return (\n    <div\n      ref={rootRef}\n      className={`flex ${vertical ? 'flex-col' : 'flex-row'} w-full max-w-full [perspective:1400px] max-[520px]:!flex-col max-[520px]:[perspective:none] ${className}`}\n      style={{ gap: `${gap}px`, height: vertical ? `${Math.round(height * 1.6)}px` : `${height}px` }}\n      role=\"list\"\n      aria-label=\"Image accordion gallery\"\n    >\n      {items.map((item, i) => {\n        const isActive = i === active;\n        const Tag = (item.link ? 'a' : 'div') as 'a';\n        return (\n          <Tag\n            key={i}\n            ref={(el: HTMLElement | null) => {\n              panelRefs.current[i] = el;\n            }}\n            className=\"group relative block min-w-0 min-h-0 flex-[1_1_0] cursor-pointer overflow-hidden bg-[#0a0713] no-underline outline-none [transform-style:preserve-3d] [transform-origin:center] [box-shadow:0_10px_30px_-18px_rgba(0,0,0,0.8)] focus-visible:[box-shadow:0_0_0_2px_var(--ag-accent),0_10px_30px_-18px_rgba(0,0,0,0.8)] max-[520px]:min-h-[84px] max-[520px]:!transform-none\"\n            style={\n              {\n                borderRadius: `${radius}px`,\n                '--ag-accent': accentColor,\n                willChange: 'flex-grow, transform'\n              } as CSSProperties\n            }\n            href={item.link || undefined}\n            onClick={e => handleClick(i, e)}\n            onMouseEnter={() => handleEnter(i)}\n            onFocus={() => setActive(i)}\n            onKeyDown={e => handleKeyDown(i, e)}\n            role=\"listitem\"\n            tabIndex={0}\n            aria-current={isActive ? 'true' : undefined}\n            aria-label={item.label}\n          >\n            <span className=\"absolute inset-0 overflow-hidden [border-radius:inherit]\">\n              <span\n                ref={(el: HTMLElement | null) => {\n                  mediaRefs.current[i] = el;\n                }}\n                className=\"absolute top-1/2 left-1/2 [filter:grayscale(var(--ag-gray,1))]\"\n                style={{\n                  width: vertical ? '100%' : 'var(--ag-media-size, 320px)',\n                  height: vertical ? 'var(--ag-media-size, 320px)' : '100%',\n                  willChange: 'transform, filter'\n                }}\n              >\n                <img\n                  src={item.image}\n                  alt={item.alt || item.label || ''}\n                  draggable={false}\n                  className=\"block h-full w-full select-none object-cover [-webkit-user-drag:none]\"\n                />\n              </span>\n              <span\n                className=\"pointer-events-none absolute inset-0\"\n                style={{ background: overlayBg }}\n                aria-hidden=\"true\"\n              />\n            </span>\n            {showLabels && (\n              <span\n                className=\"pointer-events-none absolute bottom-5 left-5 right-5 z-[2] flex items-center gap-3\"\n                aria-hidden=\"true\"\n              >\n                <span\n                  ref={(el: HTMLElement | null) => {\n                    barRefs.current[i] = el;\n                  }}\n                  className=\"h-[26px] w-[3px] flex-none rounded-[3px] opacity-0\"\n                  style={{\n                    background: accentColor,\n                    boxShadow: `0 0 12px color-mix(in srgb, ${accentColor} 60%, transparent)`\n                  }}\n                />\n                <span\n                  ref={(el: HTMLElement | null) => {\n                    textRefs.current[i] = el;\n                  }}\n                  className=\"overflow-hidden text-ellipsis whitespace-nowrap text-[clamp(1rem,1.4vw,1.4rem)] font-semibold tracking-[0.01em] opacity-0 [text-shadow:0_2px_14px_rgba(0,0,0,0.55)]\"\n                  style={{ color: textColor }}\n                >\n                  {item.label}\n                </span>\n              </span>\n            )}\n          </Tag>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default AccordionGallery;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"gsap@^3.13.0"
	]
}