{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FlipCard-TS-CSS",
	"title": "FlipCard",
	"description": "Two-faced card that flips in 3D on a click, a drag or a flick, settling on a spring that carries your release velocity, with an optional cursor tilt, a sheen that follows the pointer and a shadow that narrows as it turns edge on.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "FlipCard.css",
			"target": "@components/FlipCard.css",
			"content": ".flip-card {\n  --fc-w: 300px;\n  --fc-h: 400px;\n  --fc-radius: 22px;\n  --fc-bg: #27272a;\n  --fc-ink: #f5f5f5;\n  --fc-shadow: #000000;\n  --fc-shadow-o: 0.45;\n  --fc-glare: 0.22;\n\n  position: relative;\n  display: inline-block;\n  width: var(--fc-w);\n  max-width: 100%;\n  height: var(--fc-h);\n  border-radius: var(--fc-radius);\n  outline: none;\n  cursor: pointer;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-touch-callout: none;\n  -webkit-tap-highlight-color: transparent;\n  touch-action: pan-y;\n}\n\n.flip-card[data-axis='x'] {\n  touch-action: pan-x;\n}\n\n.flip-card[data-draggable] {\n  cursor: grab;\n}\n\n.flip-card[data-dragging] {\n  cursor: grabbing;\n}\n\n.flip-card[data-disabled] {\n  cursor: default;\n  opacity: 0.6;\n}\n\n.flip-card__shadow {\n  position: absolute;\n  inset: 12% 9% -5%;\n  border-radius: var(--fc-radius);\n  background: color-mix(in srgb, var(--fc-shadow) calc(var(--fc-shadow-o) * 100%), transparent);\n  filter: blur(22px);\n  pointer-events: none;\n}\n\n.flip-card__rotor {\n  position: absolute;\n  inset: 0;\n  transform-style: preserve-3d;\n}\n\n.flip-card__face {\n  position: absolute;\n  inset: 0;\n  overflow: hidden;\n  border-radius: var(--fc-radius);\n  background: var(--fc-bg);\n  color: var(--fc-ink);\n  backface-visibility: hidden;\n  -webkit-backface-visibility: hidden;\n}\n\n.flip-card__face img {\n  -webkit-user-drag: none;\n}\n\n.flip-card__face--back {\n  transform: rotateY(180deg);\n}\n\n.flip-card[data-axis='x'] .flip-card__face--back {\n  transform: rotateX(180deg);\n}\n\n.flip-card__glare {\n  position: absolute;\n  inset: 0;\n  background: radial-gradient(\n    circle farthest-side at var(--fc-gx, 50%) var(--fc-gy, 50%),\n    rgba(255, 255, 255, var(--fc-glare)) 0%,\n    rgba(255, 255, 255, calc(var(--fc-glare) * 0.76)) 12%,\n    rgba(255, 255, 255, calc(var(--fc-glare) * 0.5)) 26%,\n    rgba(255, 255, 255, calc(var(--fc-glare) * 0.28)) 42%,\n    rgba(255, 255, 255, calc(var(--fc-glare) * 0.12)) 60%,\n    rgba(255, 255, 255, calc(var(--fc-glare) * 0.04)) 78%,\n    rgba(255, 255, 255, 0) 100%\n  );\n  opacity: var(--fc-sheen, 0);\n  pointer-events: none;\n}\n\n.flip-card[data-fade] .flip-card__face {\n  transform: none;\n  opacity: 0;\n  backface-visibility: visible;\n  -webkit-backface-visibility: visible;\n  transition: opacity 200ms ease;\n}\n\n.flip-card[data-fade='front'] .flip-card__face--front,\n.flip-card[data-fade='back'] .flip-card__face--back {\n  opacity: 1;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "FlipCard.tsx",
			"content": "'use client';\n\nimport React, {\n  useEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent,\n  type MouseEvent,\n  type PointerEvent,\n  type ReactNode\n} from 'react';\nimport {\n  animate,\n  motion,\n  useMotionTemplate,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n  type AnimationPlaybackControls,\n  type MotionStyle\n} from 'motion/react';\n\nimport './FlipCard.css';\n\nexport type FlipCardAxis = 'x' | 'y';\n\nexport interface FlipCardProps {\n  front?: ReactNode;\n  back?: ReactNode;\n  flipped?: boolean;\n  defaultFlipped?: boolean;\n  onFlipChange?: (flipped: boolean) => void;\n  axis?: FlipCardAxis;\n  flipOnClick?: boolean;\n  draggable?: boolean;\n  dragDistance?: number;\n  tilt?: boolean;\n  tiltMax?: number;\n  glare?: boolean;\n  glareOpacity?: number;\n  hoverScale?: number;\n  perspective?: number;\n  stiffness?: number;\n  damping?: number;\n  width?: number;\n  height?: number;\n  radius?: number;\n  background?: string;\n  color?: string;\n  shadow?: boolean;\n  shadowColor?: string;\n  shadowOpacity?: number;\n  disabled?: boolean;\n  ariaLabel?: string;\n  className?: string;\n}\n\ninterface Grip {\n  id: number;\n  x: number;\n  y: number;\n  base: number;\n  moved: boolean;\n  slop: number;\n  hist: { t: number; v: number }[];\n}\n\nconst SLOP = { fine: 4, coarse: 8 };\nconst TILT_SPRING = { stiffness: 240, damping: 24, mass: 0.6 };\nconst LIFT_SPRING = { stiffness: 320, damping: 26 };\nconst FLING = 0.16;\nconst HISTORY_MS = 90;\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst snap = (deg: number) => Math.round(deg / 180) * 180;\nconst isBack = (deg: number) => Math.abs(Math.round(deg / 180)) % 2 === 1;\n\nconst FlipCard: React.FC<FlipCardProps> = ({\n  front = null,\n  back = null,\n  flipped,\n  defaultFlipped = false,\n  onFlipChange,\n  axis = 'y',\n  flipOnClick = true,\n  draggable = true,\n  dragDistance = 0,\n  tilt = true,\n  tiltMax = 12,\n  glare = true,\n  glareOpacity = 0.22,\n  hoverScale = 1.03,\n  perspective = 1100,\n  stiffness = 170,\n  damping = 20,\n  width = 300,\n  height = 400,\n  radius = 22,\n  background = '#27272a',\n  color = '#f5f5f5',\n  shadow = true,\n  shadowColor = '#000000',\n  shadowOpacity = 0.45,\n  disabled = false,\n  ariaLabel = 'Flip card',\n  className = ''\n}) => {\n  const reduce = useReducedMotion();\n  const controlled = flipped !== undefined;\n  const [inner, setInner] = useState(defaultFlipped);\n  const [dragging, setDragging] = useState(false);\n  const shown = controlled ? flipped : inner;\n  const shownRef = useRef(shown);\n  shownRef.current = shown;\n  const rootRef = useRef<HTMLDivElement>(null);\n  const grip = useRef<Grip | null>(null);\n  const spin = useRef<AnimationPlaybackControls | null>(null);\n  const target = useRef(shown ? 180 : 0);\n\n  const turn = useMotionValue(shown ? 180 : 0);\n  const tiltX = useSpring(0, TILT_SPRING);\n  const tiltY = useSpring(0, TILT_SPRING);\n  const lift = useSpring(1, LIFT_SPRING);\n  const sheen = useSpring(0, LIFT_SPRING);\n  const gx = useMotionValue(50);\n  const gy = useMotionValue(50);\n\n  const sumX = useTransform([turn, tiltX], ([t, x]: number[]) => t + x);\n  const sumY = useTransform([turn, tiltY], ([t, y]: number[]) => t + y);\n  const turnY = useMotionTemplate`perspective(${perspective}px) scale(${lift}) rotateX(${tiltX}deg) rotateY(${sumY}deg)`;\n  const turnX = useMotionTemplate`perspective(${perspective}px) scale(${lift}) rotateY(${tiltY}deg) rotateX(${sumX}deg)`;\n  const facing = useTransform(turn, t => Math.abs(Math.cos((t * Math.PI) / 180)));\n  const spread = useTransform(facing, f => 0.08 + 0.92 * f);\n  const shade = useTransform(facing, f => 0.1 + 0.9 * f * f);\n  const gxPct = useMotionTemplate`${gx}%`;\n  const gyPct = useMotionTemplate`${gy}%`;\n\n  const settle = (to: number, velocity: number, instant: boolean) => {\n    spin.current?.stop();\n    target.current = to;\n    if (instant || reduce) turn.jump(to);\n    else spin.current = animate(turn, to, { type: 'spring', stiffness, damping, velocity, restDelta: 0.05 });\n    const next = isBack(to);\n    if (next === shownRef.current) return;\n    shownRef.current = next;\n    if (!controlled) setInner(next);\n    onFlipChange?.(next);\n  };\n  const flip = (instant: boolean) => {\n    const base = snap(turn.get());\n    settle(isBack(base) ? base - 180 : base + 180, 0, instant);\n  };\n  const rest = () => {\n    tiltX.set(0);\n    tiltY.set(0);\n    sheen.set(0);\n    lift.set(1);\n  };\n\n  useEffect(() => {\n    if (!controlled || isBack(target.current) === flipped) return;\n    const base = target.current;\n    spin.current?.stop();\n    target.current = isBack(base) ? base - 180 : base + 180;\n    if (reduce) turn.jump(target.current);\n    else spin.current = animate(turn, target.current, { type: 'spring', stiffness, damping, restDelta: 0.05 });\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [flipped]);\n  useEffect(() => () => spin.current?.stop(), []);\n  useEffect(() => {\n    if (disabled) rest();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [disabled]);\n\n  const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {\n    if (disabled || e.button !== 0 || grip.current) return;\n    try {\n      e.currentTarget.setPointerCapture(e.pointerId);\n    } catch {}\n    spin.current?.stop();\n    grip.current = {\n      id: e.pointerId,\n      x: e.clientX,\n      y: e.clientY,\n      base: turn.get(),\n      moved: false,\n      slop: e.pointerType === 'touch' ? SLOP.coarse : SLOP.fine,\n      hist: []\n    };\n    if (!reduce) lift.set(hoverScale);\n  };\n  const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {\n    const g = grip.current;\n    if (g && g.id === e.pointerId) {\n      const d = axis === 'x' ? e.clientY - g.y : e.clientX - g.x;\n      if (!g.moved) {\n        if (Math.abs(d) < g.slop || !draggable || reduce) return;\n        g.moved = true;\n        setDragging(true);\n        tiltX.set(0);\n        tiltY.set(0);\n        sheen.set(0);\n      }\n      const span = dragDistance > 0 ? dragDistance : axis === 'x' ? height : width;\n      const deg = g.base + (axis === 'x' ? -1 : 1) * (d / span) * 180;\n      turn.set(deg);\n      const now = performance.now();\n      g.hist.push({ t: now, v: deg });\n      while (g.hist.length > 2 && now - g.hist[0].t > HISTORY_MS) g.hist.shift();\n      return;\n    }\n    if (!tilt || reduce || disabled || e.pointerType === 'touch') return;\n    const r = e.currentTarget.getBoundingClientRect();\n    const px = clamp((e.clientX - r.left) / r.width, 0, 1);\n    const py = clamp((e.clientY - r.top) / r.height, 0, 1);\n    tiltX.set((0.5 - py) * 2 * tiltMax);\n    tiltY.set((px - 0.5) * 2 * tiltMax);\n    gx.set(px * 100);\n    gy.set(py * 100);\n    sheen.set(1);\n  };\n  const release = (e: PointerEvent<HTMLDivElement>, cancelled: boolean) => {\n    const g = grip.current;\n    if (!g || g.id !== e.pointerId) return;\n    grip.current = null;\n    try {\n      if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);\n    } catch {}\n    setDragging(false);\n    if (e.pointerType === 'touch' || !rootRef.current?.matches(':hover')) rest();\n    if (!g.moved) {\n      if (!cancelled && flipOnClick) flip(false);\n      else settle(target.current, 0, false);\n      return;\n    }\n    const here = turn.get();\n    let velocity = 0;\n    const a = g.hist[0];\n    const b = g.hist[g.hist.length - 1];\n    if (!cancelled && a && b && b.t > a.t && performance.now() - b.t < 60)\n      velocity = ((b.v - a.v) / (b.t - a.t)) * 1000;\n    const to = cancelled ? snap(g.base) : clamp(snap(here + velocity * FLING), snap(here) - 180, snap(here) + 180);\n    settle(to, velocity, false);\n  };\n  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (disabled || (e.key !== 'Enter' && e.key !== ' ')) return;\n    e.preventDefault();\n    if (!e.repeat) flip(true);\n  };\n  const onClick = (e: MouseEvent<HTMLDivElement>) => {\n    if (!disabled && e.detail === 0) flip(true);\n  };\n\n  const rotorStyle = {\n    transform: axis === 'x' ? turnX : turnY,\n    '--fc-gx': gxPct,\n    '--fc-gy': gyPct,\n    '--fc-sheen': sheen\n  } as MotionStyle;\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"button\"\n      tabIndex={disabled ? -1 : 0}\n      aria-pressed={shown}\n      aria-label={ariaLabel}\n      aria-disabled={disabled || undefined}\n      className={`flip-card${className ? ` ${className}` : ''}`}\n      data-axis={axis}\n      data-draggable={draggable && !disabled && !reduce ? '' : undefined}\n      data-dragging={dragging ? '' : undefined}\n      data-disabled={disabled ? '' : undefined}\n      data-fade={reduce ? (shown ? 'back' : 'front') : undefined}\n      onPointerDown={onPointerDown}\n      onPointerMove={onPointerMove}\n      onPointerUp={e => release(e, false)}\n      onPointerCancel={e => release(e, true)}\n      onLostPointerCapture={e => release(e, true)}\n      onPointerEnter={e => {\n        if (!reduce && !disabled && e.pointerType !== 'touch') lift.set(hoverScale);\n      }}\n      onPointerLeave={() => {\n        if (!grip.current) rest();\n      }}\n      onKeyDown={onKeyDown}\n      onClick={onClick}\n      onDragStart={e => e.preventDefault()}\n      style={\n        {\n          '--fc-w': `${width}px`,\n          '--fc-h': `${height}px`,\n          '--fc-radius': `${radius}px`,\n          '--fc-bg': background,\n          '--fc-ink': color,\n          '--fc-shadow': shadowColor,\n          '--fc-shadow-o': shadowOpacity,\n          '--fc-glare': glareOpacity\n        } as CSSProperties\n      }\n    >\n      {shadow ? (\n        <motion.span\n          className=\"flip-card__shadow\"\n          aria-hidden=\"true\"\n          style={axis === 'x' ? { scaleY: spread, opacity: shade } : { scaleX: spread, opacity: shade }}\n        />\n      ) : null}\n      <motion.div className=\"flip-card__rotor\" style={reduce ? undefined : rotorStyle}>\n        <div className=\"flip-card__face flip-card__face--front\" aria-hidden={shown} inert={shown}>\n          {front}\n          {glare ? <span className=\"flip-card__glare\" aria-hidden=\"true\" /> : null}\n        </div>\n        <div className=\"flip-card__face flip-card__face--back\" aria-hidden={!shown} inert={!shown}>\n          {back}\n          {glare ? <span className=\"flip-card__glare\" aria-hidden=\"true\" /> : null}\n        </div>\n      </motion.div>\n    </div>\n  );\n};\n\nexport default FlipCard;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}