{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A HoverExpand component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { KeyboardEvent } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type HoverExpandOrientation = \"horizontal\" | \"vertical\";\n\nexport interface HoverExpandItem {\n  alt?: string;\n  description?: string;\n  id: string;\n  image?: string;\n  title: string;\n}\n\nexport interface HoverExpandProps {\n  activeIndex?: number;\n  className?: string;\n  collapsedFlex?: number;\n  expandedFlex?: number;\n  items: HoverExpandItem[];\n  onActiveIndexChange?: (index: number) => void;\n  orientation?: HoverExpandOrientation;\n  showLabelsWhenCollapsed?: boolean;\n}\n\nconst DEFAULT_EXPANDED_FLEX = 4;\nconst DEFAULT_COLLAPSED_FLEX = 1;\nconst LABEL_ROTATION = -90;\n\nconst HoverExpand = ({\n  items,\n  orientation = \"horizontal\",\n  activeIndex: activeIndexProp,\n  onActiveIndexChange,\n  expandedFlex = DEFAULT_EXPANDED_FLEX,\n  collapsedFlex = DEFAULT_COLLAPSED_FLEX,\n  showLabelsWhenCollapsed = true,\n  className,\n}: HoverExpandProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [internalActive, setInternalActive] = useState(0);\n  const isControlled = activeIndexProp !== undefined;\n  const activeIndex = isControlled ? activeIndexProp : internalActive;\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n  const panelRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const isHorizontal = orientation === \"horizontal\";\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n\n    const handleChange = (event: MediaQueryListEvent) => {\n      setIsHoverDevice(event.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  const setActive = (index: number) => {\n    if (!isControlled) {\n      setInternalActive(index);\n    }\n    onActiveIndexChange?.(index);\n  };\n\n  const handleKeyDown = (\n    event: KeyboardEvent<HTMLButtonElement>,\n    index: number\n  ) => {\n    const nextKey = isHorizontal ? \"ArrowRight\" : \"ArrowDown\";\n    const prevKey = isHorizontal ? \"ArrowLeft\" : \"ArrowUp\";\n\n    if (event.key === nextKey) {\n      event.preventDefault();\n      const next = Math.min(index + 1, items.length - 1);\n      setActive(next);\n      panelRefs.current[next]?.focus();\n    } else if (event.key === prevKey) {\n      event.preventDefault();\n      const prev = Math.max(index - 1, 0);\n      setActive(prev);\n      panelRefs.current[prev]?.focus();\n    }\n  };\n\n  return (\n    <div\n      aria-label=\"Expandable image panels\"\n      className={cn(\n        \"flex w-full gap-2\",\n        isHorizontal ? \"h-[360px] flex-row\" : \"h-[520px] flex-col\",\n        className\n      )}\n      role=\"group\"\n    >\n      {items.map((item, index) => {\n        const isActive = index === activeIndex;\n        const shouldShowLabel = isActive || showLabelsWhenCollapsed;\n\n        return (\n          <motion.button\n            animate={{ flexGrow: isActive ? expandedFlex : collapsedFlex }}\n            aria-label={item.title}\n            aria-pressed={isActive}\n            className=\"relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-2xl border border-foreground/10 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand\"\n            initial={false}\n            key={item.id}\n            onClick={() => {\n              if (!isHoverDevice) {\n                setActive(index);\n              }\n            }}\n            onFocus={() => setActive(index)}\n            onKeyDown={(event) => handleKeyDown(event, index)}\n            onMouseEnter={() => {\n              if (isHoverDevice) {\n                setActive(index);\n              }\n            }}\n            ref={(el) => {\n              panelRefs.current[index] = el;\n            }}\n            style={{ flexBasis: 0 }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { bounce: 0.1, duration: 0.25, type: \"spring\" }\n            }\n            type=\"button\"\n          >\n            {item.image ? (\n              <img\n                alt={item.alt ?? item.title}\n                className=\"absolute inset-0 h-full w-full object-cover\"\n                draggable={false}\n                src={item.image}\n              />\n            ) : null}\n            <div\n              aria-hidden=\"true\"\n              className=\"absolute inset-0 bg-gradient-to-t from-black/70 via-black/10 to-transparent\"\n            />\n            <motion.div\n              animate={\n                shouldReduceMotion\n                  ? { opacity: shouldShowLabel ? 1 : 0 }\n                  : {\n                      opacity: shouldShowLabel ? 1 : 0,\n                      rotate: isHorizontal && !isActive ? LABEL_ROTATION : 0,\n                    }\n              }\n              className=\"absolute inset-0 flex items-end p-4\"\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { bounce: 0.1, duration: 0.25, type: \"spring\" }\n              }\n            >\n              <div\n                className={cn(\n                  isHorizontal && !isActive && \"origin-left whitespace-nowrap\"\n                )}\n              >\n                <p className=\"font-semibold text-sm text-white\">{item.title}</p>\n                {isActive && item.description ? (\n                  <p className=\"mt-1 text-white/80 text-xs\">\n                    {item.description}\n                  </p>\n                ) : null}\n              </div>\n            </motion.div>\n          </motion.button>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default HoverExpand;\n","path":"index.tsx","target":"components/smoothui/hover-expand/index.tsx","type":"registry:ui"}],"name":"hover-expand","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Hover Expand","type":"registry:ui"}