{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "expandable-card",
  "type": "registry:item",
  "title": "Expandable Card",
  "description": "A card component that can expand to show more content.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/brook/ui/expandable-card/expandable-card.tsx",
      "content": "\"use client\";\nimport { Dialog } from \"@base-ui/react/dialog\";\nimport { ScrollArea } from \"@base-ui/react/scroll-area\";\nimport { Plus, X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport styles from \"./expandable-card.module.css\";\n\ntype ExpandableCardItem = {\n  id: string | number;\n  imageSrc: string;\n  alt: string;\n  cardHeading: string;\n  content?: React.ReactNode;\n};\n\ntype ExpandableCardProps = {\n  item: ExpandableCardItem;\n  className?: string;\n};\n\nfunction ExpandableCard({ item, className }: ExpandableCardProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const [isElevated, setIsElevated] = useState(false);\n  const prefersReducedMotion = useReducedMotion();\n\n  return (\n    <div className={cn(styles.wrapper, className)} style={{ zIndex: isElevated ? 100 : undefined }}>\n      <Dialog.Root\n        onOpenChange={(open) => {\n          setIsOpen(open);\n          if (open) {\n            setIsElevated(true);\n          }\n        }}\n        open={isOpen}\n      >\n        <AnimatePresence>\n          {isOpen ? (\n            <Dialog.Backdrop\n              hidden={undefined}\n              key=\"overlay\"\n              render={\n                <motion.div\n                  animate={{\n                    opacity: 1,\n                  }}\n                  className={styles.overlay}\n                  exit={{ opacity: 0 }}\n                  initial={{ opacity: 0 }}\n                  transition={{\n                    duration: prefersReducedMotion ? 0 : 0.25,\n                    ease: [0.455, 0.03, 0.515, 0.955],\n                  }}\n                />\n              }\n            />\n          ) : null}\n        </AnimatePresence>\n        <Dialog.Portal keepMounted>\n          <AnimatePresence>\n            {isOpen ? (\n              <div className={styles.modalPositioner} key=\"positioner\">\n                <Dialog.Popup\n                  hidden={undefined}\n                  render={\n                    <motion.div\n                      className={styles.expandedCard}\n                      layoutId={`card-${item.id}`}\n                      style={{\n                        borderRadius: \"32px\",\n                        overflow: \"hidden\",\n                      }}\n                    />\n                  }\n                >\n                  <ScrollArea.Root className={styles.scrollAreaRoot}>\n                    <ScrollArea.Viewport className={styles.scrollAreaViewport}>\n                      <ScrollArea.Content className={styles.scrollableContent}>\n                        <div className={styles.closeButtonContainer}>\n                          <Dialog.Close\n                            aria-label=\"Close\"\n                            render={\n                              <motion.button\n                                animate={{ opacity: 1 }}\n                                className={styles.closeButton}\n                                exit={{ opacity: 0, display: \"flex\" }}\n                                initial={{ opacity: prefersReducedMotion ? 1 : 0 }}\n                                transition={{\n                                  type: \"spring\",\n                                  duration: prefersReducedMotion ? 0 : 0.3,\n                                  delay: prefersReducedMotion ? 0 : 0.1,\n                                }}\n                              />\n                            }\n                          >\n                            <X height={21} strokeWidth={2} width={21} />\n                          </Dialog.Close>\n                        </div>\n\n                        <motion.img\n                          alt={item.alt}\n                          className={styles.expandedImage}\n                          height={600}\n                          layoutId={`image-${item.id}`}\n                          src={item.imageSrc}\n                          style={{ borderRadius: \"24px\" }}\n                          width={600}\n                        />\n\n                        <motion.div className={styles.contentExpanded}>\n                          <motion.div layoutId={`heading-${item.id}`}>\n                            <h3 className={styles.expandedHeading}>{item.cardHeading}</h3>\n                          </motion.div>\n\n                          <motion.div\n                            animate={{ opacity: 1, y: 0, scale: 1 }}\n                            className={styles.paragraphWrapper}\n                            exit={{\n                              opacity: 0,\n                              display: \"block\",\n                              y: prefersReducedMotion ? 0 : -40,\n                              scale: prefersReducedMotion ? 1 : 0.92,\n                            }}\n                            initial={{\n                              opacity: prefersReducedMotion ? 1 : 0,\n                              y: prefersReducedMotion ? 0 : -40,\n                              scale: prefersReducedMotion ? 1 : 0.92,\n                            }}\n                            transition={{\n                              delay: prefersReducedMotion ? 0 : 0.1,\n                              duration: prefersReducedMotion ? 0 : 0.3,\n                              type: \"spring\",\n                              bounce: 0,\n                            }}\n                          >\n                            {item.content}\n                          </motion.div>\n                        </motion.div>\n                      </ScrollArea.Content>\n                    </ScrollArea.Viewport>\n                    <ScrollArea.Scrollbar className={styles.scrollbar} orientation=\"vertical\">\n                      <ScrollArea.Thumb className={styles.scrollbarThumb} />\n                    </ScrollArea.Scrollbar>\n                  </ScrollArea.Root>\n                </Dialog.Popup>\n              </div>\n            ) : null}\n          </AnimatePresence>\n        </Dialog.Portal>\n        <Dialog.Trigger\n          render={\n            <motion.button\n              aria-label={`Expand ${item.cardHeading}`}\n              className={styles.card}\n              layoutId={`card-${item.id}`}\n              onLayoutAnimationComplete={() => {\n                if (!isOpen) {\n                  setIsElevated(false);\n                }\n              }}\n              style={{ borderRadius: \"24px\" }}\n            />\n          }\n        >\n          <motion.img\n            alt={item.alt}\n            className={styles.image}\n            height={300}\n            layoutId={`image-${item.id}`}\n            src={item.imageSrc}\n            style={{ borderRadius: \"24px\" }}\n            width={300}\n          />\n\n          <div className={styles.contentContainer}>\n            <motion.div layoutId={`heading-${item.id}`}>\n              <h3 className={styles.heading}>{item.cardHeading}</h3>\n            </motion.div>\n\n            <motion.div className={styles.expandIcon}>\n              <Plus height={21} strokeWidth={2} width={21} />\n            </motion.div>\n          </div>\n        </Dialog.Trigger>\n      </Dialog.Root>\n    </div>\n  );\n}\n\nexport { ExpandableCard };\nexport type { ExpandableCardItem, ExpandableCardProps };\n",
      "type": "registry:file",
      "target": "~/components/ui/expandable-card/expandable-card.tsx"
    },
    {
      "path": "registry/brook/ui/expandable-card/expandable-card.module.css",
      "content": ".wrapper {\n  position: relative;\n  width: fit-content;\n  height: fit-content;\n}\n\n.overlay {\n  position: fixed;\n  min-height: 100dvh;\n  z-index: 101;\n  inset: 0;\n  background-color: var(--background);\n}\n\n.card {\n  all: unset;\n  cursor: pointer;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 320px;\n  overflow: hidden;\n  position: relative;\n}\n\n.card::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  border-radius: inherit;\n  box-shadow: inset 0 0 0 0.5px oklch(from var(--border) l c h / 0.7);\n  pointer-events: none;\n}\n\n.card:focus-visible {\n  outline: 2px solid var(--ring);\n  outline-offset: 2px;\n}\n\n.image {\n  width: 100%;\n  height: 320px;\n  object-fit: cover;\n}\n\n.heading {\n  font-size: var(--font-size-2xl);\n  font-weight: 500;\n  color: var(--secondary-foreground);\n  margin: 0;\n  transition: color 150ms ease-out;\n  line-height: 1.5;\n  letter-spacing: -0.02em;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.card:hover .heading {\n  color: var(--foreground);\n}\n\n.modalPositioner {\n  position: fixed;\n  inset: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  z-index: 9999;\n  pointer-events: none;\n  overflow-y: auto;\n  max-height: 100%;\n}\n\n.expandedCard {\n  position: fixed;\n  top: 5vh;\n  width: 100%;\n  max-width: 960px;\n  height: 100dvh;\n  overflow: hidden;\n  background-color: var(--mix-card-15-bg);\n  box-shadow: inset 0 0 0 0.5px oklch(from var(--border) l c h / 0.6);\n  padding: 0;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 16px;\n  pointer-events: auto;\n  transition: none;\n  animation: none;\n  opacity: 1;\n  transform: none;\n  scrollbar-width: thin;\n  scrollbar-color: var(--border) transparent;\n}\n\n.scrollAreaRoot {\n  width: 100%;\n  height: 100%;\n  position: relative;\n}\n\n.scrollAreaViewport {\n  width: 100%;\n  height: 100%;\n  overscroll-behavior: contain;\n}\n\n.scrollAreaViewport::before,\n.scrollAreaViewport::after {\n  content: \"\";\n  display: block;\n  left: 0;\n  width: 100%;\n  position: absolute;\n  pointer-events: none;\n  border-radius: 0.375rem;\n  transition: height 100ms ease-out;\n  z-index: 10;\n}\n\n.scrollAreaViewport::before {\n  --scroll-area-overflow-y-start: inherit;\n  top: 0;\n  height: min(40px, var(--scroll-area-overflow-y-start));\n  background: linear-gradient(\n    to bottom,\n    oklch(from var(--background) l c h / 0.7),\n    transparent\n  );\n}\n\n.scrollAreaViewport::after {\n  --scroll-area-overflow-y-end: inherit;\n  bottom: 0;\n  height: min(10vh, var(--scroll-area-overflow-y-end, 10vh));\n  background: linear-gradient(\n    to top,\n    var(--background) 0%,\n    var(--background) 30%,\n    transparent 100%\n  );\n}\n\n.scrollableContent {\n  width: 100%;\n  padding: 24px;\n  padding-top: max(0vh, env(safe-area-inset-top));\n  padding-bottom: max(12vh, env(safe-area-inset-bottom));\n  padding-left: max(24px, env(safe-area-inset-left));\n  padding-right: max(24px, env(safe-area-inset-right));\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 16px;\n}\n\n.scrollbar {\n  display: flex;\n  justify-content: center;\n  width: 6px;\n  border-radius: 3px;\n  margin: 4px;\n  margin-top: calc(32px + 0.5vh);\n  margin-left: 0;\n  touch-action: none;\n  user-select: none;\n  opacity: 0;\n  transition: opacity 150ms ease-out 800ms;\n  pointer-events: none;\n}\n\n.scrollbar[data-scrolling] {\n  opacity: 1;\n  transition: opacity 150ms ease-out 0ms;\n  pointer-events: auto;\n}\n\n.scrollbarThumb {\n  width: 100%;\n  border-radius: inherit;\n  background-color: oklch(from var(--border) l c h / 0.5);\n  transition: background-color 150ms ease-out;\n}\n\n.scrollbarThumb:hover {\n  background-color: oklch(from var(--border) l c h / 0.8);\n}\n\n.contentExpanded {\n  max-width: 700px;\n  line-height: 2;\n  margin: 0 auto;\n  padding-top: 28px;\n  padding-left: 0;\n  padding-right: 0;\n  padding-bottom: 0;\n  width: 100%;\n  height: auto;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  gap: 36px;\n  text-align: left;\n}\n\n.expandedImage {\n  width: 100%;\n  max-width: 700px;\n  height: auto;\n  object-fit: contain;\n}\n\n.expandedHeading {\n  font-size: 48px;\n  font-weight: 500;\n  color: var(--foreground);\n  margin: 0;\n  align-self: flex-start;\n  width: 100%;\n  line-height: 1.5;\n  letter-spacing: -0.02em;\n}\n\n.paragraphWrapper {\n  color: oklch(from var(--secondary-foreground) l c h / 0.8);\n}\n\n.closeButtonContainer {\n  position: sticky;\n  align-self: flex-end;\n  width: 44px;\n  height: 44px;\n  top: 32px;\n  border-radius: 50%;\n  z-index: 20;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n}\n\n.closeButton {\n  padding: 0.5rem;\n  border-radius: 999px;\n  box-shadow: inset 0 0 0 0.5px oklch(from var(--border) l c h / 0.7);\n  z-index: 20;\n  background: transparent;\n  color: var(--muted-foreground);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  transition:\n    background-color 150ms ease-out,\n    color 150ms ease-out;\n}\n\n.closeButton:hover {\n  color: var(--foreground);\n  background-color: var(--muted);\n}\n\n.closeButton:focus-visible {\n  outline: 2px solid var(--ring);\n  outline-offset: 2px;\n}\n\n.contentContainer {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 16px;\n}\n\n.expandIcon {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: var(--muted-foreground);\n  width: 36px;\n  height: 36px;\n  min-width: 36px;\n  min-height: 36px;\n  border-radius: 50%;\n  box-shadow: inset 0 0 0 0.5px oklch(from var(--border) l c h / 0.7);\n  margin-left: auto;\n  flex-shrink: 0;\n  transition:\n    background-color 150ms ease-out,\n    color 150ms ease-out;\n}\n.expandIcon:hover,\n.card:hover .expandIcon {\n  background-color: var(--card);\n  color: var(--foreground);\n}\n\n@media (max-width: 768px) {\n  .card {\n    max-width: 300px;\n  }\n  .expandedHeading {\n    font-size: 32px;\n  }\n  .expandedContent {\n    font-size: 14px;\n  }\n\n  .scrollableContent {\n    padding: 0px 16px;\n    padding-left: max(16px, env(safe-area-inset-left));\n    padding-right: max(16px, env(safe-area-inset-right));\n    padding-bottom: max(8vh, env(safe-area-inset-bottom));\n    justify-content: space-between;\n    gap: 0px;\n  }\n\n  .closeButton {\n    background-color: var(--background);\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .card,\n  .closeButton,\n  .expandIcon,\n  .heading,\n  .scrollbar,\n  .scrollbarThumb {\n    transition: none;\n  }\n}\n",
      "type": "registry:file",
      "target": "~/components/ui/expandable-card/expandable-card.module.css"
    }
  ]
}