{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "card-stack-motion",
  "title": "Card Stack (Motion)",
  "author": "AlgorizeTech",
  "description": "A layered deck of cards featuring fluid drag-to-discard gestures, button-driven directional slide animations, and realistic depth perspective.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/motion/CardStackBlock.tsx",
      "content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport { AnimatePresence, motion, useMotionValue, useTransform } from \"motion/react\";\nimport { ArrowLeft, ArrowRight, ArrowUpRight, RotateCcw } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CardStackItem {\n  id: string | number;\n  title: string;\n  description?: string;\n  category?: string;\n  date?: string;\n  meta?: string;\n  icon?: React.ReactNode | string;\n  href?: string;\n  [key: string]: unknown;\n}\n\nexport interface CardStackProps extends React.HTMLAttributes<HTMLDivElement> {\n  items: CardStackItem[];\n  title?: string;\n  /** Maximum number of cards visually stacked (front card + cards behind). Defaults to 3. */\n  visibleCards?: number;\n  /** Direction the card slides off on next: \"right\" | \"left\". Defaults to \"right\". */\n  slideDirection?: \"right\" | \"left\";\n  /** Whether the stack loops infinitely or stops at ends. Defaults to false. */\n  loop?: boolean;\n  /** Initial active card index. Defaults to 0. */\n  defaultIndex?: number;\n  /** Callback fired when the active card index changes. */\n  onCardChange?: (index: number) => void;\n  /** Callback fired when a card is dismissed/swiped. */\n  onDismiss?: (item: CardStackItem, direction: \"left\" | \"right\") => void;\n  /** Custom render function for card content. */\n  renderCard?: (item: CardStackItem, index: number, isTop: boolean) => React.ReactNode;\n  /** Custom class for individual cards. */\n  cardClassName?: string;\n  /** Enable dragging and swiping the top card to discard (left or right). Defaults to false. */\n  swipeToDismiss?: boolean;\n  /** Whether to show header navigation buttons (arrows). Defaults to true. Set to false for pure swipe-to-discard mode. */\n  showNavigation?: boolean;\n  /** Whether to render the header bar (title, counter, and navigation). Defaults to true. */\n  showHeader?: boolean;\n  /** Whether the last card can be dismissed in swipe mode. Defaults to false. */\n  canDismissLastCard?: boolean;\n}\n\ninterface StackCardItemProps {\n  item: CardStackItem;\n  offset: number;\n  isTop: boolean;\n  count: number;\n  direction: number;\n  slideMultiplier: number;\n  swipeToDismiss: boolean;\n  canDismiss: boolean;\n  onDismiss: (dir: \"left\" | \"right\") => void;\n  renderCard?: (item: CardStackItem, index: number, isTop: boolean) => React.ReactNode;\n  cardClassName?: string;\n}\n\nfunction StackCardItem({\n  item,\n  offset,\n  isTop,\n  count,\n  direction,\n  slideMultiplier,\n  swipeToDismiss,\n  canDismiss,\n  onDismiss,\n  renderCard,\n  cardClassName,\n}: StackCardItemProps) {\n  const x = useMotionValue(0);\n  const dragRotate = useTransform(x, [-260, 260], [-18, 18]);\n\n  const rotate = offset * 1.6;\n  const offsetX = offset * 8;\n  const offsetY = -offset * 4;\n  const zIndex = count - offset;\n\n  const isDraggable = isTop && swipeToDismiss && canDismiss;\n\n  return (\n    <motion.div\n      key={item.id}\n      layout\n      custom={direction}\n      drag={isDraggable ? \"x\" : false}\n      dragConstraints={{ left: 0, right: 0 }}\n      dragElastic={0.8}\n      onDragEnd={(_, info) => {\n        if (!isDraggable) return;\n        const threshold = 80;\n        const velocityThreshold = 280;\n        if (info.offset.x > threshold || info.velocity.x > velocityThreshold) {\n          onDismiss(\"right\");\n        } else if (info.offset.x < -threshold || info.velocity.x < -velocityThreshold) {\n          onDismiss(\"left\");\n        }\n      }}\n      variants={{\n        initial: (dir: number) =>\n          isTop && dir < 0\n            ? {\n                x: 220 * slideMultiplier,\n                y: -10,\n                rotate: 8 * slideMultiplier,\n                opacity: 0,\n                scale: 0.98,\n                zIndex: count + 2,\n              }\n            : {\n                x: offsetX,\n                y: offsetY,\n                rotate,\n                opacity: 0,\n                zIndex,\n              },\n        animate: {\n          x: offsetX,\n          y: offsetY,\n          rotate,\n          opacity: 1,\n          scale: 1,\n          zIndex,\n        },\n        exit: (dir: number) => ({\n          x: (dir > 0 ? 320 : -320) * (swipeToDismiss ? 1 : slideMultiplier),\n          y: -10,\n          rotate: (dir > 0 ? 14 : -14) * (swipeToDismiss ? 1 : slideMultiplier),\n          opacity: 0,\n          scale: 0.96,\n          zIndex: count + 2,\n          transition: {\n            duration: 0.26,\n            ease: [0.25, 1, 0.5, 1],\n          },\n        }),\n      }}\n      initial=\"initial\"\n      animate=\"animate\"\n      exit=\"exit\"\n      transition={{\n        type: \"spring\",\n        stiffness: 280,\n        damping: 26,\n        mass: 0.8,\n      }}\n      style={{\n        transformOrigin: \"bottom center\",\n        ...(isDraggable ? { x, rotate: dragRotate } : {}),\n      }}\n      className={cn(\n        \"absolute inset-x-0 bottom-0 top-0 rounded-[26px] border border-foreground/10 dark:border-foreground/15 bg-card p-6 shadow-[0_10px_25px_-5px_rgba(0,0,0,0.06),0_2px_6px_-2px_rgba(0,0,0,0.04)] dark:shadow-[0_12px_28px_-6px_rgba(0,0,0,0.6),0_2px_6px_-2px_rgba(0,0,0,0.4)] flex flex-col justify-between overflow-hidden\",\n        isDraggable\n          ? \"cursor-grab active:cursor-grabbing touch-none select-none\"\n          : isTop\n          ? \"cursor-default select-none\"\n          : \"pointer-events-none select-none\",\n        cardClassName,\n      )}\n    >\n      {renderCard ? (\n        renderCard(item, offset, isTop)\n      ) : (\n        <>\n          {/* Card Header: Icon, Title, and Action Link */}\n          <div className=\"flex items-start justify-between gap-3\">\n            <div className=\"flex items-center gap-3.5 min-w-0\">\n              {item.icon && (\n                <div className=\"size-12 shrink-0 rounded-2xl overflow-hidden flex items-center justify-center pointer-events-none\">\n                  {typeof item.icon === \"string\" ? (\n                    <img\n                      src={item.icon}\n                      alt={item.title}\n                      className=\"size-full object-cover\"\n                    />\n                  ) : (\n                    item.icon\n                  )}\n                </div>\n              )}\n              <h3 className=\"font-bold text-xl text-foreground tracking-tight truncate pointer-events-none\">\n                {item.title}\n              </h3>\n            </div>\n            {item.href && (\n              <a\n                href={item.href}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                aria-label={`Open ${item.title}`}\n                onClick={(e) => {\n                  // Prevent navigation if user was dragging\n                  if (Math.abs(x.get()) > 5) {\n                    e.preventDefault();\n                  }\n                }}\n                className=\"text-foreground hover:text-primary transition-colors p-0.5 shrink-0 z-20\"\n              >\n                <ArrowUpRight className=\"size-6 stroke-[2.2]\" />\n              </a>\n            )}\n          </div>\n\n          {/* Description */}\n          {item.description && (\n            <p className=\"mt-3 text-[15px] text-muted-foreground line-clamp-2 leading-relaxed font-normal pointer-events-none\">\n              {item.description}\n            </p>\n          )}\n\n          {/* Footer Meta */}\n          {(item.category || item.date || item.meta) && (\n            <div className=\"mt-4 flex items-center gap-2 text-xs font-medium text-muted-foreground/80 pointer-events-none\">\n              {item.category && <span>{item.category}</span>}\n              {item.category && (item.date || item.meta) && (\n                <span>·</span>\n              )}\n              {item.date && <span>{item.date}</span>}\n              {!item.date && item.meta && <span>{item.meta}</span>}\n            </div>\n          )}\n        </>\n      )}\n    </motion.div>\n  );\n}\n\nexport const CardStack = React.forwardRef<HTMLDivElement, CardStackProps>(\n  (\n    {\n      items,\n      title,\n      visibleCards = 3,\n      slideDirection = \"right\",\n      loop = false,\n      defaultIndex = 0,\n      onCardChange,\n      onDismiss,\n      renderCard,\n      className,\n      cardClassName,\n      swipeToDismiss = false,\n      showNavigation = true,\n      showHeader = true,\n      canDismissLastCard = false,\n      ...props\n    },\n    ref,\n  ) => {\n    const [currentIndex, setCurrentIndex] = useState(\n      Math.min(Math.max(0, defaultIndex), Math.max(0, items.length - 1)),\n    );\n    const [direction, setDirection] = useState<1 | -1>(1);\n\n    if (!items || items.length === 0) {\n      return null;\n    }\n\n    const total = items.length;\n    const isCompleted = currentIndex >= total;\n    const canPrev = loop ? true : currentIndex > 0;\n    const canNext = loop ? true : currentIndex < total - 1;\n    const canDismiss = loop || canDismissLastCard || currentIndex < total - 1;\n\n    const handlePrev = () => {\n      if (!canPrev) return;\n      setDirection(-1);\n      const nextIdx = currentIndex > 0 ? currentIndex - 1 : total - 1;\n      setCurrentIndex(nextIdx);\n      onCardChange?.(nextIdx);\n    };\n\n    const handleNext = () => {\n      if (!canNext) return;\n      setDirection(1);\n      const nextIdx = currentIndex < total - 1 ? currentIndex + 1 : 0;\n      setCurrentIndex(nextIdx);\n      onCardChange?.(nextIdx);\n    };\n\n    const handleDismissCard = (dismissDir: \"left\" | \"right\") => {\n      if (!canDismiss) return;\n      const currentItem = items[currentIndex % total];\n      setDirection(dismissDir === \"right\" ? 1 : -1);\n      const nextIdx = loop ? (currentIndex + 1) % total : currentIndex + 1;\n      setCurrentIndex(nextIdx);\n      onDismiss?.(currentItem, dismissDir);\n      onCardChange?.(nextIdx % total);\n    };\n\n    // Calculate visible cards from current index\n    const visibleItems: { item: CardStackItem; offset: number }[] = [];\n    const count = Math.min(visibleCards, total);\n\n    if (!isCompleted) {\n      for (let i = 0; i < count; i++) {\n        const itemIndex = (currentIndex + i) % total;\n        if (!loop && currentIndex + i >= total) {\n          break;\n        }\n        visibleItems.push({\n          item: items[itemIndex],\n          offset: i,\n        });\n      }\n    }\n\n    const slideMultiplier = slideDirection === \"left\" ? -1 : 1;\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"w-full max-w-[500px] select-none\", className)}\n        {...props}\n      >\n        {/* Header: Title/Counter on left, Navigation Arrows on right */}\n        {showHeader && (\n          <div className=\"flex items-center justify-between mb-4 px-1\">\n            {title ? (\n              <h2 className=\"text-xl font-bold tracking-tight text-foreground\">\n                {title}\n              </h2>\n            ) : (\n              <span className=\"text-sm font-medium text-muted-foreground/80 tabular-nums\">\n                {isCompleted ? \"0 left\" : `${Math.min(currentIndex + 1, total)} of ${total}`}\n              </span>\n            )}\n            <div className=\"flex items-center gap-3\">\n              {title && (\n                <span className=\"text-sm font-medium text-muted-foreground/80 tabular-nums\">\n                  {isCompleted ? \"0 left\" : `${Math.min(currentIndex + 1, total)} of ${total}`}\n                </span>\n              )}\n              {showNavigation && (\n                <div className=\"flex items-center gap-1.5\">\n                  <button\n                    type=\"button\"\n                    onClick={handlePrev}\n                    disabled={!canPrev}\n                    aria-label=\"Previous card\"\n                    className=\"size-9 rounded-xl border border-foreground/10 dark:border-foreground/15 bg-card hover:bg-muted active:scale-95 transition-all flex items-center justify-center text-foreground disabled:opacity-30 disabled:pointer-events-none shadow-xs\"\n                  >\n                    <ArrowLeft className=\"size-4\" />\n                  </button>\n                  <button\n                    type=\"button\"\n                    onClick={handleNext}\n                    disabled={!canNext}\n                    aria-label=\"Next card\"\n                    className=\"size-9 rounded-xl border border-foreground/10 dark:border-foreground/15 bg-card hover:bg-muted active:scale-95 transition-all flex items-center justify-center text-foreground disabled:opacity-30 disabled:pointer-events-none shadow-xs\"\n                  >\n                    <ArrowRight className=\"size-4\" />\n                  </button>\n                </div>\n              )}\n            </div>\n          </div>\n        )}\n\n        {/* Stack Container with generous padding so stacked cards and shadows never clip */}\n        <div className=\"relative w-full overflow-hidden pt-7 pr-12 pb-8 pl-2\">\n          <div className=\"relative h-[195px] sm:h-[200px] w-full\">\n            {isCompleted ? (\n              <div className=\"size-full rounded-[26px] border border-dashed border-foreground/15 dark:border-foreground/20 flex flex-col items-center justify-center gap-3 p-6 text-center bg-muted/20\">\n                <p className=\"text-sm font-medium text-muted-foreground\">\n                  All cards have been reviewed!\n                </p>\n                <button\n                  type=\"button\"\n                  onClick={() => setCurrentIndex(0)}\n                  className=\"inline-flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold bg-primary text-primary-foreground hover:opacity-90 active:scale-95 transition-all shadow-xs\"\n                >\n                  <RotateCcw className=\"size-3.5\" />\n                  Replay deck\n                </button>\n              </div>\n            ) : (\n              <AnimatePresence custom={direction} initial={false}>\n                {visibleItems\n                  .slice()\n                  .reverse()\n                  .map(({ item, offset }) => (\n                    <StackCardItem\n                      key={item.id}\n                      item={item}\n                      offset={offset}\n                      isTop={offset === 0}\n                      count={count}\n                      direction={direction}\n                      slideMultiplier={slideMultiplier}\n                      swipeToDismiss={swipeToDismiss}\n                      canDismiss={canDismiss}\n                      onDismiss={handleDismissCard}\n                      renderCard={renderCard}\n                      cardClassName={cardClassName}\n                    />\n                  ))}\n              </AnimatePresence>\n            )}\n          </div>\n        </div>\n      </div>\n    );\n  },\n);\n\nCardStack.displayName = \"CardStack\";\n\nexport default CardStack;\n",
      "type": "registry:component",
      "target": "components/custom/blocks/CardStackBlock.tsx"
    }
  ],
  "docs": "Motion build of `card-stack` — same props, markup and install path; install one or the other. Named export: CardStack (also default). Accepts items, visibleCards, swipeToDismiss, canDismissLastCard, loop, showHeader, and showNavigation.",
  "categories": [
    "data-display",
    "marketing",
    "actions"
  ],
  "type": "registry:component"
}
