{"version":1,"id":"expandable-card","framework":"next","kind":"block","files":[{"path":"src/blocks/gfa/expandable-card.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useOutsideClick } from \"@/hooks/use-outside-click\";\n\nexport type ExpandableCardData = {\n  id: string;\n  description: string;\n  title: string;\n  src: string;\n  ctaText: string;\n  ctaLink: string;\n  content: React.ReactNode | (() => React.ReactNode);\n};\n\nfunction CloseIcon() {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"size-3 text-neutral-900 dark:text-neutral-100\"\n      aria-hidden\n    >\n      <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n      <path d=\"M18 6l-12 12\" />\n      <path d=\"M6 6l12 12\" />\n    </svg>\n  );\n}\n\nexport type ExpandableCardsProps = {\n  cards: ExpandableCardData[];\n  variant: \"list\" | \"grid\";\n  className?: string;\n};\n\nfunction ExpandableCardShell({ cards, variant, className }: ExpandableCardsProps) {\n  const [active, setActive] = React.useState<ExpandableCardData | null>(null);\n  const ref = React.useRef<HTMLDivElement>(null);\n  const uid = React.useId();\n\n  const close = React.useCallback(() => setActive(null), []);\n\n  React.useEffect(() => {\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key === \"Escape\") setActive(null);\n    }\n\n    const prevOverflow = document.body.style.overflow;\n    if (active) document.body.style.overflow = \"hidden\";\n    else document.body.style.overflow = \"auto\";\n\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => {\n      window.removeEventListener(\"keydown\", onKeyDown);\n      document.body.style.overflow = prevOverflow;\n    };\n  }, [active]);\n\n  useOutsideClick(ref, close);\n\n  const layoutKey = (key: string, card: ExpandableCardData) => `${key}-${card.title}-${uid}`;\n\n  return (\n    <>\n      <AnimatePresence>\n        {active ? (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            className=\"fixed inset-0 z-10 h-full w-full bg-black/20\"\n            aria-hidden\n          />\n        ) : null}\n      </AnimatePresence>\n\n      <AnimatePresence>\n        {active ? (\n          <div className=\"fixed inset-0 z-[100] grid place-items-center p-3 sm:p-4\">\n            <motion.button\n              key={`close-${active.id}-${uid}`}\n              type=\"button\"\n              layout\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0, transition: { duration: 0.05 } }}\n              className=\"absolute right-1.5 top-1.5 flex size-7 items-center justify-center rounded-full bg-white lg:hidden dark:bg-neutral-800\"\n              onClick={close}\n              aria-label=\"Close\"\n            >\n              <CloseIcon />\n            </motion.button>\n\n            <motion.div\n              layoutId={layoutKey(\"card\", active)}\n              ref={ref}\n              role=\"dialog\"\n              aria-modal=\"true\"\n              aria-labelledby={`expandable-modal-title-${variant}-${active.id}`}\n              className=\"flex h-full max-h-[85dvh] w-full max-w-[min(100%,22rem)] flex-col overflow-hidden bg-white sm:rounded-2xl md:h-fit dark:bg-neutral-900\"\n            >\n              <motion.div layoutId={layoutKey(\"image\", active)}>\n                {/* eslint-disable-next-line @next/next/no-img-element -- layout morph; local or remote images */}\n                <img\n                  width={200}\n                  height={200}\n                  src={active.src}\n                  alt=\"\"\n                  className=\"h-40 w-full object-cover object-top sm:h-44 sm:rounded-tl-md sm:rounded-tr-md\"\n                />\n              </motion.div>\n\n              <div>\n                <div className=\"flex items-start justify-between gap-2 p-3\">\n                  <div className=\"min-w-0\">\n                    <motion.h3\n                      id={`expandable-modal-title-${variant}-${active.id}`}\n                      layoutId={layoutKey(\"title\", active)}\n                      className={cn(\n                        \"text-sm font-bold leading-snug text-neutral-800 dark:text-neutral-200\",\n                        variant === \"grid\" && \"font-medium\",\n                      )}\n                    >\n                      {active.title}\n                    </motion.h3>\n                    <motion.p\n                      layoutId={layoutKey(\"description\", active)}\n                      className=\"text-xs text-neutral-600 dark:text-neutral-400\"\n                    >\n                      {active.description}\n                    </motion.p>\n                  </div>\n\n                  {variant === \"list\" ? (\n                    <motion.a\n                      layoutId={layoutKey(\"button\", active)}\n                      href={active.ctaLink}\n                      target=\"_blank\"\n                      rel=\"noreferrer\"\n                      className=\"shrink-0 rounded-full bg-green-500 px-2.5 py-1.5 text-xs font-bold text-white\"\n                    >\n                      {active.ctaText}\n                    </motion.a>\n                  ) : (\n                    <motion.a\n                      layout\n                      initial={{ opacity: 0 }}\n                      animate={{ opacity: 1 }}\n                      exit={{ opacity: 0 }}\n                      href={active.ctaLink}\n                      target=\"_blank\"\n                      rel=\"noreferrer\"\n                      className=\"shrink-0 rounded-full bg-green-500 px-2.5 py-1.5 text-xs font-bold text-white\"\n                    >\n                      {active.ctaText}\n                    </motion.a>\n                  )}\n                </div>\n\n                <div className=\"relative px-3 pb-2 pt-0\">\n                  <motion.div\n                    layout\n                    initial={{ opacity: 0 }}\n                    animate={{ opacity: 1 }}\n                    exit={{ opacity: 0 }}\n                    className=\"flex max-h-32 flex-col items-start gap-2 overflow-auto pb-4 text-xs leading-relaxed text-neutral-600 [mask:linear-gradient(to_bottom,white,white,transparent)] [-ms-overflow-style:none] [-webkit-overflow-scrolling:touch] [scrollbar-width:none] md:max-h-none md:text-[0.8125rem] dark:text-neutral-400 [&::-webkit-scrollbar]:hidden\"\n                  >\n                    {typeof active.content === \"function\" ? active.content() : active.content}\n                  </motion.div>\n                </div>\n              </div>\n            </motion.div>\n          </div>\n        ) : null}\n      </AnimatePresence>\n\n      <ul\n        className={cn(\n          \"mx-auto w-full gap-2\",\n          variant === \"list\" && \"max-w-xl\",\n          variant === \"grid\" && \"grid max-w-xl grid-cols-1 items-start gap-2 sm:grid-cols-2\",\n          className,\n        )}\n      >\n        {cards.map((card) => (\n          <motion.li\n            key={card.id}\n            layoutId={layoutKey(\"card\", card)}\n            onClick={() => setActive(card)}\n            className={cn(\n              \"cursor-pointer list-none rounded-lg p-2.5 hover:bg-neutral-50 md:p-3 dark:hover:bg-neutral-800\",\n              variant === \"list\" &&\n                \"flex flex-col items-center justify-between gap-2 md:flex-row md:items-center md:gap-3\",\n              variant === \"grid\" && \"flex flex-col\",\n            )}\n          >\n            <div\n              className={cn(\n                \"flex w-full gap-2.5 md:gap-3\",\n                variant === \"list\" && \"min-w-0 flex-col md:flex-1 md:flex-row\",\n                variant === \"grid\" && \"flex-col\",\n              )}\n            >\n              <motion.div layoutId={layoutKey(\"image\", card)} className={cn(variant === \"grid\" && \"w-full\")}>\n                {/* eslint-disable-next-line @next/next/no-img-element */}\n                <img\n                  width={100}\n                  height={100}\n                  src={card.src}\n                  alt=\"\"\n                  className={cn(\n                    \"rounded-md object-cover object-top\",\n                    variant === \"list\" && \"size-24 shrink-0 md:size-11\",\n                    variant === \"grid\" && \"aspect-[4/3] h-auto max-h-28 w-full sm:max-h-32\",\n                  )}\n                />\n              </motion.div>\n              <div\n                className={cn(\n                  variant === \"list\" && \"text-center md:text-left\",\n                  variant === \"grid\" && \"flex flex-col items-center justify-center\",\n                )}\n              >\n                <motion.h3\n                  layoutId={layoutKey(\"title\", card)}\n                  className=\"text-sm font-medium leading-tight text-neutral-800 dark:text-neutral-200\"\n                >\n                  {card.title}\n                </motion.h3>\n                <motion.p\n                  layoutId={layoutKey(\"description\", card)}\n                  className=\"text-xs text-neutral-600 dark:text-neutral-400\"\n                >\n                  {card.description}\n                </motion.p>\n              </div>\n            </div>\n\n            {variant === \"list\" ? (\n              <motion.button\n                type=\"button\"\n                layoutId={layoutKey(\"button\", card)}\n                className=\"mt-2 shrink-0 rounded-full bg-neutral-100 px-2.5 py-1 text-xs font-bold text-black hover:bg-green-500 hover:text-white md:mt-0 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-green-500\"\n              >\n                {card.ctaText}\n              </motion.button>\n            ) : null}\n          </motion.li>\n        ))}\n      </ul>\n    </>\n  );\n}\n\nexport function ExpandableCards({ cards, variant, className }: ExpandableCardsProps) {\n  return <ExpandableCardShell cards={cards} variant={variant} className={className} />;\n}\n\nexport { ExpandableCards as ExpandableCardBlock };\nexport type { ExpandableCardsProps as ExpandableCardBlockProps };\n"}],"meta":{"studioName":"GFA Development & Design Studio","siteUrl":"https://ui.gkhn.dev"}}