{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "petal-reveal",
  "title": "Petal Reveal",
  "description": "A thick pile of real looking flower petals that blows away to reveal your content, then settles back on a loop.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/petal-reveal.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type PetalRevealPattern = \"random\" | \"top\" | \"wave\" | \"diagonal\" | \"center\";\nexport type PetalRevealTone = \"red\" | \"pink\" | \"white\" | \"yellow\" | \"peach\" | \"purple\";\nexport type PetalRevealColor = PetalRevealTone | \"mixed\";\n\nexport interface PetalRevealRef {\n  play: () => void;\n  cover: () => void;\n  reset: () => void;\n}\n\nexport interface PetalRevealProps extends Omit<React.ComponentProps<\"div\">, \"ref\"> {\n  ref?: React.Ref<PetalRevealRef>;\n  color?: PetalRevealColor;\n  size?: number;\n  pattern?: PetalRevealPattern;\n  duration?: number;\n  loop?: boolean;\n  loopDelay?: number;\n  autoStart?: boolean;\n  inView?: boolean;\n  once?: boolean;\n  onStart?: () => void;\n  onComplete?: () => void;\n}\n\ntype Rgb = [number, number, number];\ntype Phase = \"covered\" | \"revealing\" | \"revealed\" | \"covering\";\n\ninterface Tone {\n  base: Rgb;\n  tip: Rgb;\n}\n\ninterface Sprite {\n  canvas: HTMLCanvasElement;\n  width: number;\n  height: number;\n}\n\ninterface Petal {\n  front: string;\n  back: string;\n  x: number;\n  y: number;\n  turn: number;\n  flip: number;\n  tilt: number;\n  scale: number;\n  order: number;\n  pop: number;\n  drift: number;\n  spin: number;\n  flipRate: number;\n  tiltRate: number;\n  sway: number;\n  swayRate: number;\n  swayPhase: number;\n  height: number;\n  exit: number;\n}\n\ninterface Live {\n  color: PetalRevealColor;\n  size: number;\n  pattern: PetalRevealPattern;\n  duration: number;\n  loop: boolean;\n  loopDelay: number;\n  onStart?: () => void;\n  onComplete?: () => void;\n}\n\nconst TONES: Record<PetalRevealTone, Tone> = {\n  red: { base: [250, 214, 206], tip: [196, 18, 44] },\n  pink: { base: [255, 236, 238], tip: [238, 104, 128] },\n  white: { base: [255, 253, 246], tip: [244, 236, 228] },\n  yellow: { base: [255, 248, 214], tip: [245, 190, 24] },\n  peach: { base: [255, 240, 226], tip: [242, 132, 88] },\n  purple: { base: [248, 224, 248], tip: [178, 66, 196] },\n};\n\nconst COLORS = Object.keys(TONES) as PetalRevealTone[];\nconst SHAPES = 6;\nconst LAND = 0.85;\nconst SHARP = 1.3;\n\nfunction hash(n: number) {\n  const s = Math.sin(n * 127.1) * 43758.5453;\n  return s - Math.floor(s);\n}\n\nfunction mix(a: Rgb, b: Rgb, t: number) {\n  return a.map((v, i) => Math.round(v + (b[i] - v) * t)) as Rgb;\n}\n\nfunction rgba([r, g, b]: Rgb, a = 1) {\n  return `rgba(${r}, ${g}, ${b}, ${a})`;\n}\n\nfunction tracePetal(ctx: CanvasRenderingContext2D, len: number, half: number, seed: number) {\n  const base = len / 2;\n  const tip = -len / 2;\n  const notch = len * (0.02 + hash(seed + 3) * 0.07);\n  const lean = (hash(seed + 4) - 0.5) * 0.16;\n  const side = (s: number) => half * (1 + s * lean);\n  ctx.beginPath();\n  ctx.moveTo(0, base);\n  ctx.bezierCurveTo(\n    -side(-1) * 0.32,\n    base - len * 0.2,\n    -side(-1) * 1.12,\n    len * 0.14,\n    -side(-1),\n    -len * 0.12,\n  );\n  ctx.bezierCurveTo(-side(-1) * 0.9, -len * 0.36, -side(-1) * 0.5, tip, -side(-1) * 0.16, tip);\n  ctx.quadraticCurveTo(-side(-1) * 0.05, tip + notch * 0.2, 0, tip + notch);\n  ctx.quadraticCurveTo(side(1) * 0.05, tip + notch * 0.2, side(1) * 0.16, tip);\n  ctx.bezierCurveTo(side(1) * 0.5, tip, side(1) * 0.9, -len * 0.36, side(1), -len * 0.12);\n  ctx.bezierCurveTo(side(1) * 1.12, len * 0.14, side(1) * 0.32, base - len * 0.2, 0, base);\n  ctx.closePath();\n}\n\nfunction buildSprite(tone: Tone, shape: number, len: number, ratio: number, back: boolean) {\n  const seed = shape * 17.3 + 5;\n  const half = len * (0.3 + hash(seed + 1) * 0.1);\n  const pad = Math.ceil(len * 0.08) + 3;\n  const width = half * 2.3 + pad * 2;\n  const height = len + pad * 2;\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = Math.ceil(width * ratio);\n  canvas.height = Math.ceil(height * ratio);\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) return { canvas, width, height };\n\n  const paleBase = back ? mix(tone.base, [255, 255, 255], 0.35) : tone.base;\n  const paleTip = back ? mix(tone.tip, [255, 255, 255], 0.38) : tone.tip;\n  const vein = mix(tone.tip, [90, 20, 40], 0.35);\n  const heart = mix(paleBase, [236, 232, 170], 0.45);\n\n  ctx.scale(ratio, ratio);\n  ctx.translate(width / 2, height / 2);\n\n  tracePetal(ctx, len, half, seed);\n  ctx.save();\n  ctx.shadowColor = \"rgba(60, 20, 30, 0.22)\";\n  ctx.shadowBlur = 2.5;\n  ctx.shadowOffsetY = 1;\n  ctx.fillStyle = rgba(paleBase);\n  ctx.fill();\n  ctx.restore();\n\n  ctx.save();\n  ctx.clip();\n\n  const body = ctx.createLinearGradient(0, len / 2, 0, -len / 2);\n  body.addColorStop(0, rgba(heart));\n  body.addColorStop(0.16, rgba(paleBase));\n  body.addColorStop(0.55, rgba(mix(paleBase, paleTip, 0.5)));\n  body.addColorStop(1, rgba(paleTip));\n  ctx.fillStyle = body;\n  ctx.fillRect(-width, -height, width * 2, height * 2);\n\n  const light = ctx.createLinearGradient(-half, -len * 0.2, half, len * 0.2);\n  light.addColorStop(0, back ? \"rgba(255, 255, 255, 0.12)\" : \"rgba(255, 255, 255, 0.34)\");\n  light.addColorStop(0.45, \"rgba(255, 255, 255, 0)\");\n  light.addColorStop(1, back ? \"rgba(70, 20, 40, 0.06)\" : \"rgba(70, 20, 40, 0.16)\");\n  ctx.fillStyle = light;\n  ctx.fillRect(-width, -height, width * 2, height * 2);\n\n  const glow = ctx.createRadialGradient(0, len * 0.42, 0, 0, len * 0.42, len * 0.5);\n  glow.addColorStop(0, \"rgba(255, 255, 255, 0.35)\");\n  glow.addColorStop(1, \"rgba(255, 255, 255, 0)\");\n  ctx.fillStyle = glow;\n  ctx.fillRect(-width, -height, width * 2, height * 2);\n\n  ctx.lineCap = \"round\";\n  ctx.strokeStyle = rgba(vein, back ? 0.07 : 0.13);\n  ctx.lineWidth = Math.max(0.5, len * 0.008);\n  const veins = 7;\n  for (let i = 0; i < veins; i++) {\n    const t = i / (veins - 1) - 0.5;\n    const bend = (hash(seed + 20 + i) - 0.5) * half * 0.12;\n    ctx.beginPath();\n    ctx.moveTo(t * half * 0.12, len * 0.46);\n    ctx.quadraticCurveTo(t * half * 1.1 + bend, len * 0.05, t * half * 1.25, -len * 0.4);\n    ctx.stroke();\n  }\n  ctx.strokeStyle = back ? \"rgba(255, 255, 255, 0.1)\" : \"rgba(255, 255, 255, 0.22)\";\n  ctx.lineWidth = Math.max(0.6, len * 0.014);\n  ctx.beginPath();\n  ctx.moveTo(0, len * 0.46);\n  ctx.quadraticCurveTo(half * 0.04, 0, 0, -len * 0.36);\n  ctx.stroke();\n  ctx.restore();\n\n  tracePetal(ctx, len, half, seed);\n  ctx.strokeStyle = rgba(mix(paleTip, [80, 20, 40], 0.3), 0.22);\n  ctx.lineWidth = 0.7;\n  ctx.stroke();\n\n  return { canvas, width, height };\n}\n\nfunction getFactor(pattern: PetalRevealPattern, x: number, y: number, seed: number) {\n  switch (pattern) {\n    case \"top\":\n      return y * 0.85 + hash(seed) * 0.15;\n    case \"wave\":\n      return x * 0.85 + hash(seed) * 0.15;\n    case \"diagonal\":\n      return (x + y) * 0.43 + hash(seed) * 0.14;\n    case \"center\":\n      return Math.min(1, Math.hypot(x - 0.5, y - 0.5) * 1.25) * 0.86 + hash(seed) * 0.14;\n    case \"random\":\n    default:\n      return hash(seed);\n  }\n}\n\nfunction buildPetals(L: Live, W: number, H: number) {\n  const gx = L.size * 0.36;\n  const gy = L.size * 0.38;\n  const g = H * 2.2 + 300;\n  const petals: Petal[] = [];\n  for (let j = -1; j <= Math.ceil(H / gy) + 1; j++) {\n    for (let i = -1; i <= Math.ceil(W / gx) + 1; i++) {\n      const seed = petals.length * 7.31 + 11;\n      const r = (n: number) => hash(seed + n);\n      const sign = (n: number) => (r(n) > 0.5 ? 1 : -1);\n      const tone = L.color === \"mixed\" ? COLORS[Math.floor(r(1) * COLORS.length)] : L.color;\n      const shape = Math.floor(r(2) * SHAPES);\n      const x = (i + (j % 2 ? 0.5 : 0) + (r(3) - 0.5) * 0.7) * gx;\n      const y = (j + (r(4) - 0.5) * 0.7) * gy;\n      const scale = 0.85 + r(5) * 0.3;\n      const pop = 30 + r(6) * 70;\n      const fall = H + L.size * scale - y;\n      petals.push({\n        front: `${tone}:${shape}:0`,\n        back: `${tone}:${shape}:1`,\n        x,\n        y,\n        turn: r(7) * Math.PI * 2,\n        flip: r(8) < 0.1 ? Math.PI + (r(9) - 0.5) * 0.9 : (r(9) - 0.5) * 1.3,\n        tilt: (r(10) - 0.5) * 1.1,\n        scale,\n        order: getFactor(\n          L.pattern,\n          Math.min(1, Math.max(0, x / W)),\n          Math.min(1, Math.max(0, y / H)),\n          seed + 12,\n        ),\n        pop,\n        drift: (r(13) - 0.5) * 140,\n        spin: sign(14) * (1 + r(15) * 3),\n        flipRate: sign(16) * (3 + r(17) * 6),\n        tiltRate: sign(18) * (1 + r(19) * 2.5),\n        sway: 8 + r(20) * 18,\n        swayRate: 3 + r(21) * 4,\n        swayPhase: r(22) * Math.PI * 2,\n        height: H * 0.25 + y + L.size + r(23) * H * 0.3,\n        exit: (pop + Math.sqrt(pop * pop + 2 * g * Math.max(0, fall))) / g,\n      });\n    }\n  }\n  return petals.sort((a, b) => b.order - a.order);\n}\n\nfunction PetalReveal({\n  ref,\n  children,\n  color = \"pink\",\n  size = 44,\n  pattern = \"random\",\n  duration = 1,\n  loop = true,\n  loopDelay = 1.6,\n  autoStart = true,\n  inView = false,\n  once = true,\n  onStart,\n  onComplete,\n  className,\n  ...props\n}: PetalRevealProps) {\n  const hostRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const controls = React.useRef<PetalRevealRef | null>(null);\n  const syncRef = React.useRef<(() => void) | null>(null);\n  const live = React.useRef<Live>({\n    color,\n    size,\n    pattern,\n    duration,\n    loop,\n    loopDelay,\n    onStart,\n    onComplete,\n  });\n\n  React.useImperativeHandle(\n    ref,\n    () => ({\n      play: () => controls.current?.play(),\n      cover: () => controls.current?.cover(),\n      reset: () => controls.current?.reset(),\n    }),\n    [],\n  );\n\n  React.useEffect(() => {\n    live.current = { color, size, pattern, duration, loop, loopDelay, onStart, onComplete };\n    syncRef.current?.();\n  }, [color, size, pattern, duration, loop, loopDelay, onStart, onComplete]);\n\n  React.useEffect(() => {\n    const host = hostRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!host || !canvas || !ctx) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const sprites = new Map<string, Sprite>();\n    let petals: Petal[] = [];\n    let layoutKey = \"\";\n    let spriteKey = \"\";\n    let phase: Phase = \"covered\";\n    let clock = 0;\n    let looping = false;\n    let played = false;\n    let revealEnd = 0;\n    let raf = 0;\n    let last = 0;\n    let visible = false;\n    let dpr = 1;\n\n    const getSprite = (key: string) => {\n      const found = sprites.get(key);\n      if (found) return found;\n      const [tone, shape, side] = key.split(\":\");\n      const sprite = buildSprite(\n        TONES[tone as PetalRevealTone],\n        Number(shape),\n        live.current.size,\n        dpr * SHARP,\n        side === \"1\",\n      );\n      sprites.set(key, sprite);\n      return sprite;\n    };\n\n    const layout = () => {\n      const L = live.current;\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const W = host.clientWidth;\n      const H = host.clientHeight;\n      const w = Math.max(1, Math.round(W * dpr));\n      const h = Math.max(1, Math.round(H * dpr));\n      if (canvas.width !== w || canvas.height !== h) {\n        canvas.width = w;\n        canvas.height = h;\n      }\n      const nextSprites = `${L.size}:${dpr}`;\n      if (nextSprites !== spriteKey) {\n        spriteKey = nextSprites;\n        sprites.clear();\n      }\n      const nextLayout = `${W}:${H}:${L.color}:${L.size}:${L.pattern}:${L.duration}`;\n      if (nextLayout === layoutKey) return;\n      layoutKey = nextLayout;\n      petals = buildPetals(L, W, H);\n      revealEnd = petals.reduce((end, p) => Math.max(end, p.order * L.duration + p.exit), 0);\n    };\n\n    const place = (\n      p: Petal,\n      x: number,\n      y: number,\n      turn: number,\n      flip: number,\n      tilt: number,\n      grow: number,\n    ) => {\n      const flat = Math.cos(flip);\n      const sprite = getSprite(flat < 0 ? p.back : p.front);\n      const ratio = dpr * SHARP;\n      const k = (p.scale * grow * dpr) / ratio;\n      const sx = Math.sign(flat || 1) * Math.max(0.06, Math.abs(flat)) * k;\n      const sy = (0.6 + 0.4 * Math.abs(Math.cos(tilt))) * k;\n      const cos = Math.cos(turn);\n      const sin = Math.sin(turn);\n      ctx.setTransform(cos * sx, sin * sx, -sin * sy, cos * sy, x * dpr, y * dpr);\n      ctx.drawImage(sprite.canvas, -sprite.canvas.width / 2, -sprite.canvas.height / 2);\n    };\n\n    const draw = () => {\n      const L = live.current;\n      const H = host.clientHeight;\n      const g = H * 2.2 + 300;\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n      if (phase === \"revealed\") return;\n\n      if (phase === \"covered\") {\n        for (const p of petals) place(p, p.x, p.y, p.turn, p.flip, p.tilt, 1);\n        return;\n      }\n\n      if (phase === \"revealing\") {\n        for (const p of petals) {\n          if (clock <= p.order * L.duration) place(p, p.x, p.y, p.turn, p.flip, p.tilt, 1);\n        }\n        for (const p of petals) {\n          const age = clock - p.order * L.duration;\n          if (age <= 0 || age >= p.exit) continue;\n          const ramp = Math.min(1, age / 0.3);\n          const swing = Math.sin(p.swayPhase + age * p.swayRate) * p.sway * ramp;\n          place(\n            p,\n            p.x + p.drift * age * ramp + swing,\n            p.y - p.pop * age + 0.5 * g * age * age,\n            p.turn + p.spin * age * ramp,\n            p.flip + p.flipRate * age * ramp,\n            p.tilt + p.tiltRate * age * ramp,\n            1 + 0.15 * Math.min(1, age * 3),\n          );\n        }\n        return;\n      }\n\n      for (const p of petals) {\n        if (clock - (1 - p.order) * L.duration >= LAND) {\n          place(p, p.x, p.y, p.turn, p.flip, p.tilt, 1);\n        }\n      }\n      for (const p of petals) {\n        const age = clock - (1 - p.order) * L.duration;\n        if (age < 0 || age >= LAND) continue;\n        const rest = Math.pow(1 - age / LAND, 3);\n        const swing = Math.sin(p.swayPhase + age * p.swayRate) * p.sway * rest;\n        place(\n          p,\n          p.x - p.drift * 0.6 * rest + swing,\n          p.y - p.height * rest,\n          p.turn - p.spin * 0.7 * rest,\n          p.flip - p.flipRate * 0.7 * rest,\n          p.tilt - p.tiltRate * 0.7 * rest,\n          1 + 0.15 * rest,\n        );\n      }\n    };\n\n    const moving = () => !reduced.matches;\n\n    const advance = () => {\n      const L = live.current;\n      if (phase === \"revealing\" && clock >= revealEnd) {\n        phase = \"revealed\";\n        clock = 0;\n        L.onComplete?.();\n      } else if (phase === \"revealed\" && looping && clock >= L.loopDelay) {\n        phase = \"covering\";\n        clock = 0;\n      } else if (phase === \"covering\" && clock >= L.duration + LAND) {\n        phase = \"covered\";\n        clock = 0;\n      } else if (phase === \"covered\" && looping && clock >= L.loopDelay) {\n        phase = \"revealing\";\n        clock = 0;\n        L.onStart?.();\n      }\n    };\n\n    const active = () => phase === \"revealing\" || phase === \"covering\" || (looping && moving());\n\n    const tick = (now: number) => {\n      raf = 0;\n      const dt = last ? Math.min((now - last) / 1000, 0.05) : 0;\n      last = now;\n      clock += dt;\n      const before = phase;\n      advance();\n      if (phase === \"revealing\" || phase === \"covering\" || phase !== before) draw();\n      if (active() && visible && !document.hidden) raf = requestAnimationFrame(tick);\n    };\n\n    const schedule = () => {\n      if (raf) return;\n      last = 0;\n      raf = requestAnimationFrame(tick);\n    };\n\n    const play = () => {\n      const L = live.current;\n      played = true;\n      clock = 0;\n      L.onStart?.();\n      if (!moving()) {\n        looping = false;\n        phase = \"revealed\";\n        L.onComplete?.();\n        draw();\n        return;\n      }\n      looping = L.loop;\n      phase = \"revealing\";\n      schedule();\n    };\n\n    const cover = () => {\n      looping = false;\n      clock = 0;\n      phase = moving() ? \"covering\" : \"covered\";\n      if (phase === \"covered\") draw();\n      else schedule();\n    };\n\n    const reset = () => {\n      looping = false;\n      played = false;\n      clock = 0;\n      phase = \"covered\";\n      draw();\n    };\n\n    controls.current = { play, cover, reset };\n\n    syncRef.current = () => {\n      layout();\n      draw();\n      if (active()) schedule();\n    };\n\n    layout();\n    draw();\n    if (autoStart && !inView) play();\n\n    const ro = new ResizeObserver(() => syncRef.current?.());\n    ro.observe(host);\n\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible && active()) schedule();\n    });\n    io.observe(host);\n\n    const trigger = new IntersectionObserver(\n      ([entry]) => {\n        if (!inView || !entry.isIntersecting) return;\n        if (once && played) return;\n        play();\n      },\n      { rootMargin: \"-10% 0px -10% 0px\" },\n    );\n    trigger.observe(host);\n\n    const onVisibility = () => {\n      if (!document.hidden && active()) schedule();\n    };\n    const onMotion = () => {\n      if (!moving() && phase !== \"covered\") {\n        looping = false;\n        phase = \"revealed\";\n        draw();\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    reduced.addEventListener(\"change\", onMotion);\n\n    return () => {\n      if (raf) cancelAnimationFrame(raf);\n      controls.current = null;\n      syncRef.current = null;\n      ro.disconnect();\n      io.disconnect();\n      trigger.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n      reduced.removeEventListener(\"change\", onMotion);\n    };\n  }, [autoStart, inView, once]);\n\n  return (\n    <div ref={hostRef} className={cn(\"relative overflow-hidden\", className)} {...props}>\n      {children && <div className=\"relative h-full w-full\">{children}</div>}\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 block h-full w-full\"\n      />\n    </div>\n  );\n}\n\nexport { PetalReveal };\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "addedAt": "2026-10-26"
  },
  "type": "registry:component"
}