{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "expandable-cards",
  "type": "registry:ui",
  "title": "Expandable Cards",
  "author": "Rishabh Bose <https://theb0se.vercel.app/>",
  "description": "Interactive horizontal accordion showcase cards highlighting UI library features and architectural pillars with spring flex-grow expansions, gradient backdrops, and animated quotes.",
  "dependencies": [
    "framer-motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/ui/expandable-cards.tsx",
      "content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport { motion, AnimatePresence, useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ExpandableCardItem {\n  id: number | string;\n  name: string;\n  tagline?: string;\n  description?: string;\n  quote?: string | string[];\n  author?: string;\n  role?: string;\n  initials?: string;\n  href?: string;\n  ctaLabel?: string;\n  ctaText?: string;\n  bg?: string; // tailwind background classes\n  border?: string; // tailwind border classes\n  logo?: React.ReactNode | React.ComponentType;\n  Logo?: React.ReactNode | React.ComponentType;\n  className?: string;\n}\n\nexport type ExpandableStory = ExpandableCardItem;\nexport type Story = ExpandableCardItem;\n\nexport interface ExpandableCardsProps\n  extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n  /** Array of expandable card items with logos and contents passed as props */\n  stories?: ExpandableCardItem[];\n  /** Optional alias for stories */\n  items?: ExpandableCardItem[];\n  /** Active card ID on initial render */\n  defaultActiveId?: number | string;\n  /** Custom headline */\n  title?: React.ReactNode;\n  /** Eyebrow subtitle */\n  subtitle?: React.ReactNode;\n  /** Optional custom right-aligned header action */\n  headerAction?: React.ReactNode;\n  /** Label for the default header link */\n  moreStoriesText?: string;\n  /** Destination URL for the default header link */\n  moreStoriesHref?: string;\n  /** Custom container class name */\n  className?: string;\n}\n\nfunction renderLogo(logo: React.ReactNode | React.ComponentType | undefined) {\n  if (!logo) return null;\n  if (typeof logo === \"function\") {\n    const Component = logo as React.ComponentType;\n    return <Component />;\n  }\n  return <>{logo}</>;\n}\n\nexport function ExpandableCards({\n  stories,\n  items,\n  defaultActiveId,\n  title,\n  subtitle,\n  headerAction,\n  moreStoriesText,\n  moreStoriesHref,\n  className,\n  ...props\n}: ExpandableCardsProps) {\n  const cardList = stories || items || [];\n  const initialActive = defaultActiveId ?? cardList[0]?.id;\n  const [active, setActive] = useState<number | string | undefined>(initialActive);\n  const reduceMotion = useReducedMotion();\n\n  if (cardList.length === 0) {\n    return null;\n  }\n\n  const hasHeader = Boolean(title || subtitle || headerAction || moreStoriesText);\n\n  return (\n    <section\n      className={cn(\"w-full bg-[#141414] px-6 py-16 text-white rounded-2xl\", className)}\n      {...props}\n    >\n      <div className=\"mx-auto max-w-6xl\">\n        {hasHeader && (\n          <div className=\"mb-10 flex flex-col sm:flex-row sm:items-end justify-between gap-4\">\n            <div>\n              {subtitle && (\n                <span className=\"block text-sm text-neutral-400 mb-1\">\n                  {subtitle}\n                </span>\n              )}\n              {title && (\n                <h2 className=\"text-3xl font-medium leading-tight tracking-tight sm:text-4xl\">\n                  {title}\n                </h2>\n              )}\n            </div>\n            {headerAction ? (\n              headerAction\n            ) : moreStoriesText ? (\n              <a\n                href={moreStoriesHref || \"#\"}\n                className=\"shrink-0 pb-1 text-sm text-neutral-300 underline underline-offset-4 transition-colors hover:text-white\"\n              >\n                {moreStoriesText}\n              </a>\n            ) : null}\n          </div>\n        )}\n\n        {/* Cards: horizontal accordion on md+, stacked on mobile */}\n        <div className=\"flex h-[640px] flex-col gap-2 md:h-[480px] md:flex-row\">\n          {cardList.map((s) => {\n            const isActive = active === s.id;\n            const logoElement = renderLogo(s.logo ?? s.Logo);\n            const quoteLines = Array.isArray(s.quote)\n              ? s.quote\n              : typeof s.quote === \"string\"\n                ? [s.quote]\n                : [];\n            const initials =\n              s.initials ||\n              (s.author\n                ? s.author\n                  .split(\" \")\n                  .filter(Boolean)\n                  .map((w) => w[0])\n                  .join(\"\")\n                  .slice(0, 2)\n                  .toUpperCase()\n                : \"\");\n            const ctaText = s.ctaLabel || s.ctaText;\n            const tagline = s.tagline || s.description;\n\n            return (\n              <motion.article\n                key={s.id}\n                layout={!reduceMotion}\n                tabIndex={0}\n                role=\"button\"\n                aria-expanded={isActive}\n                aria-label={`${s.name}${tagline ? ` - ${tagline}` : \"\"}`}\n                onFocus={() => setActive(s.id)}\n                onClick={() => setActive(s.id)}\n                onKeyDown={(e) => {\n                  if (e.key === \"Enter\" || e.key === \" \") {\n                    e.preventDefault();\n                    setActive(s.id);\n                  }\n                }}\n                animate={{ flexGrow: isActive ? 10 : 0 }}\n                transition={\n                  reduceMotion\n                    ? { duration: 0 }\n                    : { type: \"spring\", stiffness: 180, damping: 26 }\n                }\n                className={cn(\n                  \"relative min-h-0 min-w-0 cursor-pointer overflow-hidden rounded-xl border p-6 outline-none focus-visible:ring-2 focus-visible:ring-white/70\",\n                  s.bg || \"bg-neutral-900\",\n                  s.border || \"border-white/10\",\n                  isActive ? \"basis-0\" : \"basis-14 md:basis-[84px]\",\n                  s.className\n                )}\n              >\n                {/* Logo: pinned top-left if provided */}\n                {logoElement && (\n                  <div className=\"flex h-8 items-center\">\n                    {logoElement}\n                  </div>\n                )}\n\n                <AnimatePresence mode=\"wait\">\n                  {isActive && (\n                    <motion.div\n                      key=\"content\"\n                      initial={{ opacity: 0 }}\n                      animate={{ opacity: 1 }}\n                      exit={{ opacity: 0, transition: { duration: 0.1 } }}\n                      transition={{ delay: reduceMotion ? 0 : 0.2, duration: 0.3 }}\n                      className=\"absolute inset-0 flex flex-col p-6\"\n                    >\n                      {/* Spacer for logo */}\n                      {logoElement && <div className=\"h-8\" />}\n\n                      <div className=\"mt-8\">\n                        <p className=\"text-sm font-semibold\">{s.name}</p>\n                        {tagline && (\n                          <p className=\"mt-1 max-w-md text-xs text-white/70\">\n                            {tagline}\n                          </p>\n                        )}\n                      </div>\n\n                      <div className=\"mt-auto\">\n                        {quoteLines.length > 0 && (\n                          <blockquote className=\"max-w-xl text-xl font-medium leading-snug md:text-[22px]\">\n                            {quoteLines.map((line) => (\n                              <span key={line} className=\"block\">\n                                {line}\n                              </span>\n                            ))}\n                          </blockquote>\n                        )}\n\n                        {(s.author || s.role) && (\n                          <div className=\"mt-5 flex items-center gap-3\">\n                            {initials && (\n                              <span className=\"flex h-9 w-9 items-center justify-center rounded-full bg-black/40 text-xs font-semibold ring-1 ring-white/20\">\n                                {initials}\n                              </span>\n                            )}\n                            <p className=\"text-xs font-medium text-white/80\">\n                              {s.author}\n                              {s.role ? `, ${s.role}` : \"\"}\n                            </p>\n                          </div>\n                        )}\n\n                        {s.href && (\n                          <a\n                            href={s.href}\n                            onClick={(e) => e.stopPropagation()}\n                            className=\"mt-6 inline-flex items-center gap-1.5 text-xs font-medium underline underline-offset-4 hover:opacity-80\"\n                          >\n                            <span>{ctaText || \"Explore \\u2192\"}</span>\n                          </a>\n                        )}\n                      </div>\n                    </motion.div>\n                  )}\n                </AnimatePresence>\n              </motion.article>\n            );\n          })}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default ExpandableCards;\n",
      "type": "registry:ui",
      "target": "components/ui/expandable-cards.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport async function copyToClipboard(text: string): Promise<boolean> {\n  if (typeof window === \"undefined\") return false;\n\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(text);\n      return true;\n    }\n\n    const textArea = document.createElement(\"textarea\");\n    textArea.value = text;\n    textArea.style.position = \"fixed\";\n    textArea.style.left = \"-999999px\";\n    textArea.style.top = \"-999999px\";\n    document.body.appendChild(textArea);\n    textArea.focus();\n    textArea.select();\n    const succeeded = document.execCommand(\"copy\");\n    textArea.remove();\n    return succeeded;\n  } catch {\n    return false;\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/utils.ts"
    }
  ]
}
