{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A ExpandableCards component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Play } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst _AVATAR_SIZE = 96;\n// ease-out-quint for entering/exiting elements\nconst EASE_OUT_QUINT = [0.23, 1, 0.32, 1] as const;\n\nexport interface Card {\n  author?: {\n    name: string;\n    role: string;\n    image: string;\n  };\n  content: string;\n  id: number;\n  image: string;\n  title: string;\n}\n\nexport interface ExpandableCardsProps {\n  cardClassName?: string;\n  cards: Card[];\n  className?: string;\n  onSelect?: (id: number | null) => void;\n  selectedCard?: number | null;\n}\n\nexport default function ExpandableCards({\n  cards,\n  selectedCard: controlledSelected,\n  onSelect,\n  className = \"\",\n  cardClassName = \"\",\n}: ExpandableCardsProps) {\n  const [internalSelected, setInternalSelected] = useState<number | null>(null);\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  const selectedCard =\n    controlledSelected === undefined ? internalSelected : controlledSelected;\n\n  useEffect(() => {\n    if (scrollRef.current) {\n      const { scrollWidth } = scrollRef.current;\n      const { clientWidth } = scrollRef.current;\n      scrollRef.current.scrollLeft = (scrollWidth - clientWidth) / 2;\n    }\n  }, []);\n\n  const handleCardClick = (id: number) => {\n    if (selectedCard === id) {\n      if (onSelect) {\n        onSelect(null);\n      } else {\n        setInternalSelected(null);\n      }\n    } else {\n      if (onSelect) {\n        onSelect(id);\n      } else {\n        setInternalSelected(id);\n      }\n      // Center the clicked card in view\n      const cardElement = document.querySelector(`[data-card-id=\"${id}\"]`);\n      if (cardElement) {\n        cardElement.scrollIntoView({\n          behavior: \"smooth\",\n          block: \"nearest\",\n          inline: \"center\",\n        });\n      }\n    }\n  };\n\n  return (\n    <div\n      className={`flex w-full flex-col gap-4 overflow-scroll p-4 ${className}`}\n    >\n      <div\n        className=\"scrollbar-hide mx-auto flex overflow-x-auto pt-4 pb-8\"\n        ref={scrollRef}\n        style={{\n          scrollPaddingLeft: \"20%\",\n          scrollSnapType: \"x mandatory\",\n        }}\n      >\n        {cards.map((card) => (\n          <motion.div\n            animate={{\n              width: selectedCard === card.id ? \"500px\" : \"200px\",\n            }}\n            className={`relative mr-4 h-[300px] shrink-0 cursor-pointer overflow-hidden rounded-2xl border bg-background shadow-lg ${cardClassName}`}\n            data-card-id={card.id}\n            key={card.id}\n            layout\n            style={{\n              scrollSnapAlign: \"start\",\n            }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    duration: 0.25,\n                    ease: EASE_OUT_QUINT,\n                  }\n            }\n          >\n            <div className=\"relative h-full w-[200px]\">\n              <img\n                alt={card.title}\n                className=\"h-full w-full object-cover\"\n                draggable={false}\n                height={300}\n                src={card.image || \"/placeholder.svg\"}\n                width={200}\n              />\n              <div\n                aria-hidden=\"true\"\n                className=\"absolute inset-0 bg-linear-to-b from-35% from-black/65 via-50% via-black/20 to-75% to-black/65\"\n              />\n              <div className=\"absolute inset-0 flex flex-col justify-between p-6 text-white\">\n                <h2 className=\"font-bold text-2xl\">\n                  {/* The `after` overlay makes the whole card face clickable\n                      without nesting the play button inside another control. */}\n                  <button\n                    aria-expanded={selectedCard === card.id}\n                    className=\"rounded-sm text-left after:absolute after:inset-0 after:content-[''] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n                    onClick={() => handleCardClick(card.id)}\n                    type=\"button\"\n                  >\n                    {card.title}\n                  </button>\n                </h2>\n                <div className=\"relative flex items-center gap-2\">\n                  <button\n                    aria-label={`Play video: ${card.title}`}\n                    className=\"ease flex h-12 min-h-[44px] w-12 min-w-[44px] items-center justify-center rounded-full bg-background/30 backdrop-blur-sm transition-transform duration-200 hover:scale-110 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      // Handle play action\n                    }}\n                    type=\"button\"\n                  >\n                    <Play className=\"h-6 w-6 text-white\" />\n                  </button>\n                  <span className=\"font-medium text-sm\">Play video</span>\n                </div>\n              </div>\n            </div>\n            <AnimatePresence mode=\"popLayout\">\n              {selectedCard === card.id && (\n                <motion.div\n                  animate={\n                    shouldReduceMotion\n                      ? { opacity: 1, width: \"300px\" }\n                      : { filter: \"blur(0px)\", opacity: 1, width: \"300px\" }\n                  }\n                  className=\"absolute top-0 right-0 h-full bg-background\"\n                  exit={\n                    shouldReduceMotion\n                      ? { opacity: 0, width: 0 }\n                      : { filter: \"blur(5px)\", opacity: 0, width: 0 }\n                  }\n                  initial={\n                    shouldReduceMotion\n                      ? { opacity: 0, width: 0 }\n                      : { filter: \"blur(5px)\", opacity: 0, width: 0 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          duration: 0.25,\n                          ease: EASE_OUT_QUINT,\n                          opacity: { delay: 0.1, duration: 0.2 },\n                        }\n                  }\n                >\n                  <motion.div\n                    animate={\n                      shouldReduceMotion\n                        ? { opacity: 1, x: 0 }\n                        : { filter: \"blur(0px)\", opacity: 1, x: 0 }\n                    }\n                    className=\"flex h-full flex-col justify-between p-8\"\n                    exit={\n                      shouldReduceMotion\n                        ? { opacity: 0, x: 20 }\n                        : { filter: \"blur(5px)\", opacity: 0, x: 20 }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 0, x: 20 }\n                        : { filter: \"blur(5px)\", opacity: 0, x: 20 }\n                    }\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { delay: 0.2, duration: 0.2, ease: EASE_OUT_QUINT }\n                    }\n                  >\n                    <p className=\"text-primary-foreground text-sm\">\n                      {card.content}\n                    </p>\n                    {card.author ? (\n                      <div className=\"mt-4 flex items-center gap-3\">\n                        <div className=\"h-12 w-12 overflow-hidden rounded-full border bg-primary\">\n                          <img\n                            alt={card.author.name}\n                            className=\"h-full w-full object-cover\"\n                            draggable={false}\n                            height={48}\n                            src={card.author.image}\n                            width={48}\n                          />\n                        </div>\n                        <div>\n                          <p className=\"font-semibold text-foreground\">\n                            {card.author.name}\n                          </p>\n                          <p className=\"text-primary-foreground text-xs\">\n                            {card.author.role}\n                          </p>\n                        </div>\n                      </div>\n                    ) : null}\n                  </motion.div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </motion.div>\n        ))}\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/expandable-cards/index.tsx","type":"registry:ui"}],"name":"expandable-cards","registryDependencies":[],"title":"Expandable Cards","type":"registry:ui"}