{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hover-expand-gallery",
  "type": "registry:ui",
  "title": "Hover Expand Gallery",
  "description": "A row of image panels where the hovered one expands and reveals its label.",
  "files": [
    {
      "path": "src/registry/ui/hover-expand-gallery.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport Image from \"next/image\";\nimport { cn } from \"@/lib/utils\";\n\nexport type HoverExpandItem = {\n  label: string;\n  image: { src: string; alt?: string };\n  /** Extra classes for the image, e.g. \"grayscale\". */\n  imageClassName?: string;\n};\n\nexport type HoverExpandGalleryProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  items: HoverExpandItem[];\n  /** Expanded panel (controlled). */\n  activeIndex?: number;\n  defaultIndex?: number;\n  onActiveChange?: (index: number) => void;\n  /** Any CSS color for the focus ring. */\n  accent?: string;\n};\n\nexport function HoverExpandGallery({\n  items,\n  activeIndex,\n  defaultIndex = 0,\n  onActiveChange,\n  accent = \"#0A6EFF\",\n  className,\n  style,\n  ...props\n}: HoverExpandGalleryProps) {\n  const [uncontrolled, setUncontrolled] = useState(defaultIndex);\n  const active = activeIndex ?? uncontrolled;\n  const panelRefs = useRef<(HTMLButtonElement | null)[]>([]);\n\n  const select = (index: number) => {\n    if (index === active) return;\n    if (activeIndex === undefined) setUncontrolled(index);\n    onActiveChange?.(index);\n  };\n\n  // Arrow keys move between panels; focusing one expands it.\n  const handleKeyDown = (event: React.KeyboardEvent, index: number) => {\n    const step = event.key === \"ArrowRight\" ? 1 : event.key === \"ArrowLeft\" ? -1 : 0;\n    if (!step) return;\n    event.preventDefault();\n    panelRefs.current[(index + step + items.length) % items.length]?.focus();\n  };\n\n  return (\n    <div\n      data-slot=\"hover-expand-gallery\"\n      role=\"list\"\n      style={{ \"--gallery-accent\": accent, ...style } as React.CSSProperties}\n      className={cn(\"flex h-[30rem] w-full gap-2\", className)}\n      {...props}\n    >\n      {items.map((item, index) => {\n        const isActive = index === active;\n        return (\n          <div\n            key={`${item.label}-${index}`}\n            role=\"listitem\"\n            // The expanded panel takes most of the row; the rest share what's left.\n            className={cn(\n              \"relative min-w-10 basis-0 transition-[flex-grow] duration-500 ease-[cubic-bezier(0.77,0,0.175,1)] motion-reduce:transition-none\",\n              isActive ? \"grow-[10]\" : \"grow\"\n            )}\n          >\n            <button\n              ref={(node) => {\n                panelRefs.current[index] = node;\n              }}\n              type=\"button\"\n              aria-label={item.label}\n              aria-current={isActive ? \"true\" : undefined}\n              onPointerEnter={(event) => event.pointerType === \"mouse\" && select(index)}\n              onFocus={() => select(index)}\n              // Taps don't hover, so a tap expands the panel instead.\n              onClick={() => select(index)}\n              onKeyDown={(event) => handleKeyDown(event, index)}\n              className=\"group absolute inset-0 cursor-pointer overflow-hidden rounded-md bg-neutral-200 outline-none focus-visible:ring-2 focus-visible:ring-[var(--gallery-accent)] focus-visible:ring-offset-2\"\n            >\n              <Image\n                src={item.image.src}\n                alt={item.image.alt ?? \"\"}\n                fill\n                sizes=\"(min-width: 768px) 360px, 60vw\"\n                className={cn(\"object-cover\", item.imageClassName)}\n              />\n              {/* A soft shade and the label, shown once the panel has opened. */}\n              <span\n                aria-hidden\n                className={cn(\n                  \"absolute inset-x-0 bottom-0 h-1/3 bg-linear-to-t from-black/45 to-transparent transition-opacity duration-300\",\n                  isActive ? \"opacity-100\" : \"opacity-0\"\n                )}\n              />\n              <span\n                aria-hidden\n                className={cn(\n                  \"absolute bottom-5 left-5 text-2xl font-light tracking-tight whitespace-nowrap text-white\",\n                  \"transition-[opacity,transform] ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-opacity\",\n                  isActive\n                    ? \"translate-y-0 opacity-100 delay-200 duration-300\"\n                    : \"translate-y-2 opacity-0 delay-0 duration-150 motion-reduce:translate-y-0\"\n                )}\n              >\n                {item.label}\n              </span>\n            </button>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n"
    }
  ]
}
