{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"flip-flow","type":"registry:ui","title":"Flip Flow","description":"Split-flap tiles that flip through their items like a flip clock, the top half falling over a hinge; stagger several to make a flipping stream.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/flip-flow.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\n\nexport interface FlipFlowProps {\n  /** Items to cycle through; each fills the whole tile. */\n  items: ReactNode[];\n  /** Ms between flips. */\n  interval?: number;\n  /** Ms one flip takes. */\n  duration?: number;\n  /** Ms before the first flip. Stagger this across tiles to make them flip one after another. */\n  delay?: number;\n  /** Tile width in px. */\n  width?: number;\n  /** Tile height in px. */\n  height?: number;\n  /** Corner radius in px. */\n  radius?: number;\n  /** Stop cycling. */\n  paused?: boolean;\n  className?: string;\n}\n\nconst css = `\n@keyframes ff-top { from { transform: rotateX(0deg); } to { transform: rotateX(-90deg); } }\n@keyframes ff-bot { from { transform: rotateX(90deg); } 70% { transform: rotateX(-7deg); } to { transform: rotateX(0deg); } }\n@keyframes ff-shade-in { from { opacity: 0; } to { opacity: .55; } }\n@keyframes ff-shade-out { from { opacity: .45; } to { opacity: 0; } }\n`;\n\ntype Part = \"top\" | \"bottom\";\n\nfunction Half({ part, children, style, shade }: { part: Part; children: ReactNode; style?: CSSProperties; shade?: string }) {\n  return (\n    <div\n      aria-hidden\n      className=\"absolute inset-0 overflow-hidden rounded-[inherit] bg-card [backface-visibility:hidden]\"\n      style={{\n        clipPath: part === \"top\" ? \"inset(0 0 50% 0)\" : \"inset(50% 0 0 0)\",\n        transformOrigin: \"50% 50%\",\n        ...style,\n      }}\n    >\n      {children}\n      {/* Light falloff towards the hinge, like a real split-flap. */}\n      <div\n        className=\"pointer-events-none absolute inset-0\"\n        style={{\n          background:\n            part === \"top\"\n              ? \"linear-gradient(to bottom, transparent 30%, rgba(0,0,0,.28) 50%)\"\n              : \"linear-gradient(to bottom, rgba(0,0,0,.18) 50%, transparent 62%)\",\n        }}\n      />\n      {shade && <div className=\"pointer-events-none absolute inset-0 bg-black\" style={{ animation: shade }} />}\n    </div>\n  );\n}\n\n/**\n * A split-flap tile that flips through its items like a flip clock: the top half falls\n * over the hinge and the next item's bottom half lands under it. Put several side by side\n * with increasing `delay` to get a stream of tiles flipping one after another.\n */\nexport function FlipFlow({\n  items,\n  interval = 2600,\n  duration = 800,\n  delay = 0,\n  width = 180,\n  height = 240,\n  radius = 16,\n  paused = false,\n  className,\n}: FlipFlowProps) {\n  const [index, setIndex] = useState(0);\n  const [flip, setFlip] = useState<{ from: number; to: number; n: number } | null>(null);\n  const [visible, setVisible] = useState(false);\n  const root = useRef<HTMLDivElement>(null);\n  const cur = useRef(0);\n\n  useEffect(() => {\n    const el = root.current;\n    if (!el) return;\n    const io = new IntersectionObserver(([e]) => setVisible(e.isIntersecting));\n    io.observe(el);\n    return () => io.disconnect();\n  }, []);\n\n  useEffect(() => {\n    const n = items.length;\n    if (!visible || paused || n < 2) return;\n    const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let next: ReturnType<typeof setTimeout>;\n    let land: ReturnType<typeof setTimeout> | undefined;\n    let pending: number | null = null;\n    let count = 0;\n\n    const settle = (to: number) => {\n      cur.current = to;\n      pending = null;\n      setIndex(to);\n      setFlip(null);\n    };\n    const step = () => {\n      const from = cur.current % n;\n      const to = (from + 1) % n;\n      if (reduce) settle(to);\n      else {\n        pending = to;\n        setFlip({ from, to, n: count++ });\n        land = setTimeout(() => settle(to), duration);\n      }\n      next = setTimeout(step, interval);\n    };\n    next = setTimeout(step, delay + Math.min(interval, 1200));\n\n    return () => {\n      clearTimeout(next);\n      clearTimeout(land);\n      // Never leave a tile frozen half-flipped.\n      if (pending !== null) settle(pending);\n    };\n  }, [visible, paused, items.length, interval, duration, delay]);\n\n  const n = Math.max(items.length, 1);\n  const shown = index % n;\n  const half = duration / 2;\n\n  return (\n    <div\n      ref={root}\n      className={`relative shrink-0 rounded-2xl shadow-[0_18px_40px_-18px_rgba(0,0,0,.8)] ${className ?? \"\"}`}\n      style={{ width, height, borderRadius: radius, perspective: height * 3.5 }}\n    >\n      <style href=\"flip-flow\" precedence=\"default\">\n        {css}\n      </style>\n      <Half part=\"top\">{items[flip ? flip.to : shown]}</Half>\n      <Half part=\"bottom\">{items[shown]}</Half>\n      {flip && (\n        <>\n          <Half\n            key={`t${flip.n}`}\n            part=\"top\"\n            shade={`ff-shade-in ${half}ms ease-in both`}\n            style={{ animation: `ff-top ${half}ms cubic-bezier(.55,.05,.8,.4) both`, zIndex: 2 }}\n          >\n            {items[flip.from]}\n          </Half>\n          <Half\n            key={`b${flip.n}`}\n            part=\"bottom\"\n            shade={`ff-shade-out ${half}ms ease-out ${half}ms both`}\n            style={{ animation: `ff-bot ${half}ms cubic-bezier(.25,.8,.4,1) ${half}ms both`, zIndex: 2 }}\n          >\n            {items[flip.to]}\n          </Half>\n        </>\n      )}\n      {/* Hinge. */}\n      <div aria-hidden className=\"pointer-events-none absolute inset-x-0 top-1/2 z-10 h-px -translate-y-1/2 bg-black/60\" />\n      <div aria-live=\"off\" className=\"sr-only\">\n        {items[shown]}\n      </div>\n    </div>\n  );\n}\n"}]}