{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hover-expand",
  "title": "Hover Expand",
  "description": "A vertical list of rows that expand on hover to reveal a full-width image, animated with Motion springs.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/components/unlumen/hover-expand/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface HoverExpandItem {\n  label: string;\n  /** e.g. country, year, category */\n  sublabel?: string;\n  image: string;\n  imageAlt?: string;\n  /** short descriptor shown when expanded */\n  description?: string;\n}\n\nexport interface HoverExpandProps {\n  items: HoverExpandItem[];\n  /**\n   * Row height when collapsed, in pixels.\n   * @default 68\n   */\n  collapsedHeight?: number;\n  /**\n   * Row height when expanded, in pixels.\n   * @default 320\n   */\n  expandedHeight?: number;\n  className?: string;\n}\n\nexport function HoverExpand({\n  items,\n  collapsedHeight = 68,\n  expandedHeight = 320,\n  className,\n}: HoverExpandProps) {\n  const [hoveredIndex, setHoveredIndex] = React.useState<number | null>(null);\n\n  return (\n    <div className={cn(\"flex flex-col w-full\", className)}>\n      <div className=\"w-full border-t border-current opacity-15\" />\n\n      {items.map((item, i) => {\n        const isHovered = hoveredIndex === i;\n        const isOtherHovered = hoveredIndex !== null && !isHovered;\n\n        return (\n          <React.Fragment key={i}>\n            <motion.div\n              className=\"relative w-full overflow-hidden cursor-default\"\n              animate={{\n                height: isHovered ? expandedHeight : collapsedHeight,\n                opacity: isOtherHovered ? 0.38 : 1,\n              }}\n              transition={{\n                height: {\n                  type: \"spring\",\n                  stiffness: 280,\n                  damping: 32,\n                  mass: 0.9,\n                },\n                opacity: { duration: 0.22, ease: \"easeOut\" },\n              }}\n              onHoverStart={() => setHoveredIndex(i)}\n              onHoverEnd={() => setHoveredIndex(null)}\n            >\n              <motion.div\n                className=\"absolute inset-0 w-full h-full\"\n                initial={false}\n                animate={{\n                  opacity: isHovered ? 1 : 0,\n                  scale: isHovered ? 1 : 1.06,\n                }}\n                transition={{\n                  opacity: { duration: 0.45, ease: [0.23, 1, 0.32, 1] },\n                  scale: { duration: 0.55, ease: [0.23, 1, 0.32, 1] },\n                }}\n              >\n                <img\n                  src={item.image}\n                  alt={item.imageAlt ?? \"\"}\n                  className=\"w-full h-full object-cover\"\n                  loading=\"lazy\"\n                  decoding=\"async\"\n                />\n                <div className=\"absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-black/10\" />\n              </motion.div>\n\n              <div className=\"absolute inset-0 flex items-end px-5 pb-4\">\n                <div className=\"flex w-full items-end justify-between gap-4\">\n                  <div className=\"flex items-baseline gap-3 min-w-0\">\n                    <motion.span\n                      className=\"text-xs tabular-nums shrink-0 opacity-40\"\n                      animate={{\n                        color: isHovered ? \"#ffffff\" : \"currentColor\",\n                        opacity: isHovered ? 0.5 : 0.4,\n                      }}\n                      transition={{ duration: 0.2 }}\n                    >\n                      {String(i + 1).padStart(2, \"0\")}\n                    </motion.span>\n\n                    <motion.span\n                      className=\"font-semibold tracking-tight truncate\"\n                      style={{ fontSize: \"clamp(1.1rem, 2.2vw, 1.5rem)\" }}\n                      animate={{\n                        color: isHovered ? \"#ffffff\" : \"currentColor\",\n                      }}\n                      transition={{ duration: 0.2 }}\n                    >\n                      {item.label}\n                    </motion.span>\n\n                    {item.description && (\n                      <motion.span\n                        className=\"text-sm text-white/70 truncate hidden sm:block\"\n                        initial={{ opacity: 0, x: -8 }}\n                        animate={{\n                          opacity: isHovered ? 1 : 0,\n                          x: isHovered ? 0 : -8,\n                        }}\n                        transition={{\n                          duration: 0.3,\n                          delay: isHovered ? 0.12 : 0,\n                          ease: [0.23, 1, 0.32, 1],\n                        }}\n                      >\n                        — {item.description}\n                      </motion.span>\n                    )}\n                  </div>\n\n                  {item.sublabel && (\n                    <motion.span\n                      className=\"text-xs tracking-widest uppercase shrink-0\"\n                      animate={{\n                        color: isHovered\n                          ? \"rgba(255,255,255,0.55)\"\n                          : \"currentColor\",\n                        opacity: isHovered ? 1 : 0.45,\n                      }}\n                      transition={{ duration: 0.2 }}\n                    >\n                      {item.sublabel}\n                    </motion.span>\n                  )}\n                </div>\n              </div>\n            </motion.div>\n\n            <div className=\"w-full border-t border-current opacity-15\" />\n          </React.Fragment>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/hover-expand.tsx"
    }
  ],
  "type": "registry:ui"
}