{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bounce-cards","type":"registry:component","title":"Bounce Cards","description":"A stacked deck of cards fans open with a staggered spring bounce when it enters the view. Spacing, rotation and stagger are tunable; reduced-motion keeps the fanned layout and only fades in.","author":"UI Lab","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/bounce-cards.tsx","type":"registry:component","target":"@components/motion/bounce-cards.tsx","content":"// ui-lab-ten.vercel.app/components/motion/bounce-cards\n// Ported from motion-anything (nexu-io, Apache-2.0); upstream: reactbits.dev \"Bounce Cards\", redistributed with permission.\n\"use client\";\n\nimport { motion, useInView, useReducedMotion, type Transition } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface BounceCardsProps {\n  /** Card contents, rendered as an absolutely stacked deck that fans open. */\n  children: ReactNode[];\n  /** Stagger between each card's entrance, in ms. */\n  stagger?: number;\n  /** Horizontal offset between adjacent cards at rest, in px. */\n  spacing?: number;\n  /** Rotation step between adjacent cards at rest, in degrees. */\n  rotation?: number;\n  className?: string;\n}\n\nconst CARD_WIDTH = 132;\nconst CARD_HEIGHT = 176;\n\n// None of the shared spring tokens in lib/ease.ts overshoot enough to read as\n// a \"bounce\" — upstream animates with cubic-bezier(.34,1.56,.64,1), a springy\n// ease with ~56% overshoot. Low damping here reproduces that snap as the deck\n// fans open, instead of copying the CSS keyframe directly.\nconst SPRING_FAN: Transition = {\n  type: \"spring\",\n  stiffness: 280,\n  damping: 16,\n  mass: 0.9,\n};\n\nexport function BounceCards({\n  children,\n  stagger = 90,\n  spacing = 46,\n  rotation = 9,\n  className,\n}: BounceCardsProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const inView = useInView(ref, { once: true, amount: 0.3 });\n\n  const count = children.length;\n  const center = (count - 1) / 2;\n  const maxOffset = center * spacing;\n  const containerWidth = CARD_WIDTH + maxOffset * 2 + 32;\n\n  return (\n    <div\n      ref={ref}\n      style={{ width: containerWidth, height: CARD_HEIGHT + 32 }}\n      className={cn(\"relative\", className)}\n    >\n      {children.map((card, index) => {\n        const offset = index - center;\n        const x = offset * spacing;\n        const rotate = offset * rotation;\n        const delay = (index * stagger) / 1000;\n\n        const rest = { opacity: 1, x, y: 0, rotate, scale: 1 };\n        // Reduced motion: pin transform to its resting value so only opacity\n        // animates — the fanned layout still appears, just without the leap.\n        const hidden = reduce\n          ? { opacity: 0, x, y: 0, rotate, scale: 1 }\n          : { opacity: 0, x: 0, y: 20, rotate: 0, scale: 0.9 };\n\n        return (\n          <motion.div\n            // biome-ignore lint/suspicious/noArrayIndexKey: fixed-length card deck, position is the slot identity.\n            key={index}\n            initial={hidden}\n            animate={inView ? rest : hidden}\n            transition={{\n              default: { ...SPRING_FAN, delay },\n              opacity: { duration: 0.5, ease: EASE_OUT, delay },\n            }}\n            style={{ width: CARD_WIDTH, height: CARD_HEIGHT }}\n            className=\"absolute inset-0 m-auto overflow-hidden rounded-2xl shadow-2xl shadow-black/40\"\n          >\n            {card}\n          </motion.div>\n        );\n      })}\n    </div>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}