{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "flip-card",
  "type": "registry:ui",
  "title": "Flip Card",
  "description": "A card that flips in 3D on hover or tap — the front rotates out while the back rotates in with its description, feature list and CTA staggering into place.",
  "dependencies": [
    "gsap",
    "@gsap/react"
  ],
  "registryDependencies": [],
  "cssVars": {
    "theme": {}
  },
  "files": [
    {
      "path": "components/tweenui/flip-card.tsx",
      "type": "registry:component",
      "target": "components/tweenui/flip-card.tsx",
      "content": "'use client';\n\nimport { useEffect, useRef, useState, type ComponentPropsWithoutRef, type FocusEvent } from 'react';\nimport { useGSAP } from '@gsap/react';\nimport gsap from 'gsap';\nimport { cn } from '@/lib/utils';\n\ngsap.registerPlugin(useGSAP);\n\nconst prefersReducedMotion = () =>\n  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\nconst Arrow = ({ className }: { className?: string }) => {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      className={className}\n      fill=\"none\"\n      stroke=\"currentColor\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M5 12h14M13 6l6 6-6 6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n};\n\nexport interface FlipCardProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {\n  /** Card heading, shown on both faces. */\n  title: string;\n  /** Small eyebrow / index above the title, e.g. \"(01)\". */\n  eyebrow?: string;\n  /** Front-face supporting line under the title. */\n  subtitle?: string;\n  /** Back-face body text revealed on flip. */\n  description?: string;\n  /** Front-face image URL. */\n  image?: string;\n  /** Alt text for the image (defaults to the title). */\n  imageAlt?: string;\n  /** Bulleted points revealed on the back, staggered in. */\n  features?: string[];\n  /** CTA link on the back. Omit to hide the CTA. */\n  href?: string;\n  /** CTA label. */\n  ctaText?: string;\n  /** Controlled flip state. When set, hover/tap no longer flips it. */\n  flipped?: boolean;\n  /** Fires when the card flips (interaction-driven). */\n  onFlippedChange?: (flipped: boolean) => void;\n}\n\nconst FlipCard = ({\n  title,\n  eyebrow,\n  subtitle,\n  description,\n  image,\n  imageAlt,\n  features = [],\n  href,\n  ctaText = 'Learn more',\n  flipped: flippedProp,\n  onFlippedChange,\n  className,\n  ...props\n}: FlipCardProps) => {\n  const innerRef = useRef<HTMLDivElement>(null);\n  const frontRef = useRef<HTMLDivElement>(null);\n  const backRef = useRef<HTMLDivElement>(null);\n\n  const isControlled = flippedProp !== undefined;\n  const [internal, setInternal] = useState(false);\n  const flipped = isControlled ? flippedProp : internal;\n\n  // Only hover-capable, fine-pointer devices flip on hover; touch falls back to tap.\n  const [canHover, setCanHover] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia('(hover: hover) and (pointer: fine)');\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener('change', update);\n    return () => mq.removeEventListener('change', update);\n  }, []);\n\n  const setFlipped = (next: boolean) => {\n    if (!isControlled) setInternal(next);\n    onFlippedChange?.(next);\n  };\n\n  const items = () =>\n    backRef.current ? gsap.utils.toArray<HTMLElement>('[data-flip-item]', backRef.current) : [];\n\n  // Establish the 3D faces once.\n  useGSAP(() => {\n    const front = frontRef.current;\n    const back = backRef.current;\n    if (!front || !back) return;\n    gsap.set(innerRef.current, { transformStyle: 'preserve-3d' });\n    gsap.set([front, back], { backfaceVisibility: 'hidden' });\n    gsap.set(front, { rotateY: 0 });\n    gsap.set(back, { rotateY: 180 });\n    gsap.set(items(), { opacity: 0, x: -24 });\n  }, []);\n\n  // Flip + stagger the back content.\n  useGSAP(\n    () => {\n      const front = frontRef.current;\n      const back = backRef.current;\n      if (!front || !back) return;\n      const its = items();\n\n      if (prefersReducedMotion()) {\n        gsap.set(front, { rotateY: flipped ? 180 : 0 });\n        gsap.set(back, { rotateY: flipped ? 0 : 180 });\n        gsap.set(its, flipped ? { opacity: 1, x: 0 } : { opacity: 0, x: -24 });\n        return;\n      }\n\n      gsap.to(front, {\n        rotateY: flipped ? 180 : 0,\n        duration: 0.6,\n        ease: 'power2.out',\n        overwrite: true,\n      });\n      gsap.to(back, {\n        rotateY: flipped ? 0 : 180,\n        duration: 0.6,\n        ease: 'power2.out',\n        overwrite: true,\n      });\n\n      if (!its.length) return;\n      if (flipped) {\n        gsap.fromTo(\n          its,\n          { opacity: 0, x: -24 },\n          {\n            opacity: 1,\n            x: 0,\n            duration: 0.45,\n            stagger: 0.08,\n            delay: 0.18,\n            ease: 'power2.out',\n            overwrite: true,\n          }\n        );\n      } else {\n        gsap.to(its, {\n          opacity: 0,\n          x: -16,\n          duration: 0.2,\n          stagger: 0.03,\n          ease: 'power2.in',\n          overwrite: true,\n        });\n      }\n    },\n    { dependencies: [flipped] }\n  );\n\n  const onBlur = (event: FocusEvent<HTMLDivElement>) => {\n    if (canHover && !event.currentTarget.contains(event.relatedTarget as Node | null)) {\n      setFlipped(false);\n    }\n  };\n\n  return (\n    <div\n      {...props}\n      data-flip-card\n      data-flipped={flipped ? 'true' : 'false'}\n      tabIndex={0}\n      onMouseEnter={() => canHover && setFlipped(true)}\n      onMouseLeave={() => canHover && setFlipped(false)}\n      onFocus={() => canHover && setFlipped(true)}\n      onBlur={onBlur}\n      onClick={() => !canHover && setFlipped(!flipped)}\n      style={{ perspective: 1000 }}\n      className={cn(\n        'relative h-[360px] w-full max-w-[320px] cursor-pointer rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-[#045f64]',\n        className\n      )}\n    >\n      <div ref={innerRef} className=\"relative h-full w-full\">\n        {/* Front */}\n        <div\n          ref={frontRef}\n          data-flip-front\n          className=\"absolute inset-0 flex flex-col overflow-hidden rounded-2xl border border-[#045f64]/20 bg-white dark:border-[#045f64]/40 dark:bg-[#12161F]\"\n        >\n          <div className=\"space-y-4 p-5\">\n            {eyebrow ? (\n              <span className=\"text-xs text-[#045f64]/60 dark:text-[#9fd4d6]/80\">{eyebrow}</span>\n            ) : null}\n            <div className=\"space-y-1\">\n              <h3 className=\"text-lg font-medium text-[#12161F] dark:text-white\">{title}</h3>\n              {subtitle ? (\n                <p className=\"text-sm text-[#045f64]/70 dark:text-[#9fd4d6]/80\">{subtitle}</p>\n              ) : null}\n            </div>\n          </div>\n          {image ? (\n            <figure className=\"relative min-h-0 flex-1 overflow-hidden\">\n              <img src={image} alt={imageAlt ?? title} className=\"size-full object-cover\" />\n            </figure>\n          ) : null}\n        </div>\n\n        {/* Back */}\n        <div\n          ref={backRef}\n          data-flip-back\n          className=\"absolute inset-0 flex flex-col rounded-2xl border border-[#045f64]/20 bg-white p-5 dark:border-[#045f64]/40 dark:bg-[#12161F]\"\n        >\n          <div className=\"flex-1 space-y-5\">\n            <div className=\"space-y-2\">\n              {eyebrow ? (\n                <span\n                  data-flip-item\n                  className=\"block text-xs text-[#045f64]/60 dark:text-[#9fd4d6]/80\"\n                >\n                  {eyebrow}\n                </span>\n              ) : null}\n              <div className=\"space-y-1\">\n                <h3 data-flip-item className=\"text-lg font-medium text-[#12161F] dark:text-white\">\n                  {title}\n                </h3>\n                {description ? (\n                  <p data-flip-item className=\"text-sm text-[#045f64]/70 dark:text-[#9fd4d6]/80\">\n                    {description}\n                  </p>\n                ) : null}\n              </div>\n            </div>\n            {features.length > 0 ? (\n              <ul className=\"space-y-2.5\">\n                {features.map((feature) => (\n                  <li\n                    key={feature}\n                    data-flip-item\n                    className=\"flex items-center gap-2 text-sm text-[#045f64] dark:text-[#9fd4d6]\"\n                  >\n                    <Arrow className=\"size-3.5 shrink-0 stroke-[#045f64] dark:stroke-[#9fd4d6]\" />\n                    <span>{feature}</span>\n                  </li>\n                ))}\n              </ul>\n            ) : null}\n          </div>\n\n          {href ? (\n            <a\n              data-flip-item\n              href={href}\n              className=\"group/link mt-5 flex items-center justify-between rounded-xl p-3 transition-colors hover:bg-[#045f64]/10 motion-reduce:transition-none\"\n            >\n              <span className=\"text-sm font-medium text-[#12161F] dark:text-white\">{ctaText}</span>\n              <Arrow className=\"size-4 stroke-[#045f64] transition-transform duration-300 group-hover/link:translate-x-1 motion-reduce:transition-none dark:stroke-[#9fd4d6]\" />\n            </a>\n          ) : null}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default FlipCard;\n"
    }
  ],
  "docs": "Docs, props and examples: https://tween-ui.vercel.app/component/flip-card"
}