{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FlipCard-TS-TW",
	"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:component",
			"path": "FlipCard/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\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={`group relative inline-block max-w-full cursor-pointer touch-pan-y select-none outline-none [width:var(--fc-w)] [height:var(--fc-h)] [border-radius:var(--fc-radius)] [-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none] data-[axis=x]:touch-pan-x data-[draggable]:cursor-grab data-[dragging]:cursor-grabbing data-[disabled]:cursor-default data-[disabled]:opacity-60${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=\"pointer-events-none absolute [inset:12%_9%_-5%] [border-radius:var(--fc-radius)] [background:color-mix(in_srgb,var(--fc-shadow)_calc(var(--fc-shadow-o)*100%),transparent)] [filter:blur(22px)]\"\n          aria-hidden=\"true\"\n          style={axis === 'x' ? { scaleY: spread, opacity: shade } : { scaleX: spread, opacity: shade }}\n        />\n      ) : null}\n      <motion.div className=\"absolute inset-0 [transform-style:preserve-3d]\" style={reduce ? undefined : rotorStyle}>\n        <div\n          className=\"absolute inset-0 overflow-hidden [border-radius:var(--fc-radius)] [background:var(--fc-bg)] [color:var(--fc-ink)] [backface-visibility:hidden] [-webkit-backface-visibility:hidden] [&_img]:[-webkit-user-drag:none] group-data-[fade]:opacity-0 group-data-[fade]:[backface-visibility:visible] group-data-[fade]:[-webkit-backface-visibility:visible] group-data-[fade]:[transition:opacity_200ms_ease] group-data-[fade=front]:opacity-100!\"\n          aria-hidden={shown}\n          inert={shown}\n        >\n          {front}\n          {glare ? (\n            <span\n              className=\"pointer-events-none absolute inset-0 [opacity:var(--fc-sheen,0)] [background:radial-gradient(circle_farthest-side_at_var(--fc-gx,50%)_var(--fc-gy,50%),rgba(255,255,255,var(--fc-glare))_0%,rgba(255,255,255,calc(var(--fc-glare)*0.76))_12%,rgba(255,255,255,calc(var(--fc-glare)*0.5))_26%,rgba(255,255,255,calc(var(--fc-glare)*0.28))_42%,rgba(255,255,255,calc(var(--fc-glare)*0.12))_60%,rgba(255,255,255,calc(var(--fc-glare)*0.04))_78%,rgba(255,255,255,0)_100%)]\"\n              aria-hidden=\"true\"\n            />\n          ) : null}\n        </div>\n        <div\n          className=\"absolute inset-0 overflow-hidden [border-radius:var(--fc-radius)] [background:var(--fc-bg)] [color:var(--fc-ink)] [backface-visibility:hidden] [-webkit-backface-visibility:hidden] [&_img]:[-webkit-user-drag:none] group-data-[fade]:opacity-0 group-data-[fade]:[backface-visibility:visible] group-data-[fade]:[-webkit-backface-visibility:visible] group-data-[fade]:[transition:opacity_200ms_ease] [transform:rotateY(180deg)] group-data-[axis=x]:[transform:rotateX(180deg)] group-data-[fade]:[transform:none]! group-data-[fade=back]:opacity-100!\"\n          aria-hidden={!shown}\n          inert={!shown}\n        >\n          {back}\n          {glare ? (\n            <span\n              className=\"pointer-events-none absolute inset-0 [opacity:var(--fc-sheen,0)] [background:radial-gradient(circle_farthest-side_at_var(--fc-gx,50%)_var(--fc-gy,50%),rgba(255,255,255,var(--fc-glare))_0%,rgba(255,255,255,calc(var(--fc-glare)*0.76))_12%,rgba(255,255,255,calc(var(--fc-glare)*0.5))_26%,rgba(255,255,255,calc(var(--fc-glare)*0.28))_42%,rgba(255,255,255,calc(var(--fc-glare)*0.12))_60%,rgba(255,255,255,calc(var(--fc-glare)*0.04))_78%,rgba(255,255,255,0)_100%)]\"\n              aria-hidden=\"true\"\n            />\n          ) : null}\n        </div>\n      </motion.div>\n    </div>\n  );\n};\n\nexport default FlipCard;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"motion@^12.23.12"
	]
}