// RunhappySprinkles.tsx — Framer code component
// -----------------------------------------------------------------------------
// The brand's "sprinkle" background: a scattered pile of "Runhappy." sticker
// tags that drift up from the bottom edge — and, on load, FALL INTO PLACE one
// after another. Sibling of RunhappyToken.tsx: drop it on a frame, resize it
// like an image, tune everything in the right-hand panel.
//
// ZERO external dependencies. React + framer-motion + the `framer` property-
// control helpers are all built into Framer — nothing to install.
//
// The sticker is fixed artwork (static/sprinkle.svg): its shape, rounding,
// padding and "Runhappy." wordmark are baked in. We only place, rotate, scale
// and RECOLOR it — so every tag stays consistent. The layout math is identical
// to sprinkle-core.js, so the Lab preview, the exported PNG/SVG, and this
// animated component all produce the same pile for a given seed + settings.
//
// HOW TO USE IN FRAMER
//   1. In your project: Assets ▸ Code ▸ New Code File ▸ paste this whole file.
//   2. Drag "RunhappySprinkles" onto the canvas. Resize the frame like an image
//      (the pile re-fits to the frame's width/height).
//   3. Adjust Colors / Layout / Motion in the panel. Bump "Replay" to re-run
//      the fall-in animation while designing.
//
// The background is transparent by default, so it sits on whatever is behind it.
// -----------------------------------------------------------------------------

import * as React from "react"
import { motion } from "framer-motion"
import { addPropertyControls, ControlType, RenderTarget } from "framer"

// Per-page-session store: lets the entrance play EXACTLY ONCE per page load,
// surviving the remounts Framer triggers on variant / state changes (so opening
// an FAQ accordion no longer replays the background). Keyed by the optional
// "id" prop; only a full page reload resets it. Also pins the random seed so a
// remount re-lays the SAME pile (no layout jump) rather than re-rolling.
const SESSION: Record<string, { seed: number | null; played: boolean }> = {}
function sessionRec(key: string) {
    if (!SESSION[key]) SESSION[key] = { seed: null, played: false }
    return SESSION[key]
}

// The "Runhappy." sticker — verbatim inner markup of static/sprinkle.svg.
// Native box 264 × 65.7471. SPRITE_BG / SPRITE_TEXT are swapped at render time.
const SPRITE_W = 264
const SPRITE_H = 65.7471
const SPRITE_BG = "#FED54E"
const SPRITE_TEXT = "#202020"
const SPRITE_PATHS = [
    { d: "M0 24C0 12.6863 0 7.02944 3.51472 3.51472C7.02944 0 12.6863 0 24 0H240C251.314 0 256.971 0 260.485 3.51472C264 7.02944 264 12.6863 264 24V41.7472C264 53.0609 264 58.7177 260.485 62.2324C256.971 65.7471 251.314 65.7471 240 65.7471H24C12.6863 65.7471 7.02944 65.7471 3.51472 62.2324C0 58.7177 0 53.0608 0 41.7471V24Z", bg: true },
    { d: "M14.6437 49.6775C14.2062 49.6775 13.8516 49.3229 13.8516 48.8854V14.4282C13.8516 13.9907 14.2062 13.6361 14.6437 13.6361H32.0386C38.637 13.6361 43.3501 18.4047 43.3501 25.1694C43.3501 29.827 40.9104 33.5421 36.9735 35.3719L43.4534 48.5356C43.7125 49.062 43.3294 49.6775 42.7427 49.6775H34.811C34.4781 49.6775 34.1807 49.4694 34.0667 49.1566L29.488 36.5917H22.0579V48.8854C22.0579 49.3229 21.7033 49.6775 21.2658 49.6775H14.6437ZM22.0579 30.8251H29.9316C32.704 30.8251 34.7001 28.829 34.7001 25.9456C34.7001 23.0623 32.8149 21.0662 30.1534 21.0662H22.0579V30.8251Z", bg: false },
    { d: "M56.004 50.3429C50.1819 50.3429 46.0233 45.7407 46.0233 39.2532V22.7455C46.0233 22.308 46.378 21.9534 46.8154 21.9534H52.994C53.4314 21.9534 53.7861 22.308 53.7861 22.7455V37.4789C53.7861 40.5286 55.8377 42.691 58.6655 42.691C61.5489 42.691 63.6559 40.3068 63.6559 37.0353V22.7455C63.6559 22.308 64.0105 21.9534 64.448 21.9534H70.6265C71.064 21.9534 71.4187 22.308 71.4187 22.7455V44.1327L71.5134 48.8696C71.5223 49.3132 71.1651 49.6775 70.7214 49.6775H65.9665C65.6426 49.6775 65.3514 49.4803 65.2311 49.1796L63.8777 45.7962C62.5469 48.5131 59.4973 50.3429 56.004 50.3429Z", bg: false },
    { d: "M75.8595 49.6775C75.4221 49.6775 75.0674 49.3229 75.0674 48.8854V22.7455C75.0674 22.308 75.4221 21.9534 75.8595 21.9534H80.367C80.7114 21.9534 81.0162 22.1758 81.1213 22.5037L82.3312 26.2783C83.7728 23.395 87.0997 21.288 90.9257 21.288C96.7477 21.288 100.906 26.0011 100.906 32.5994V48.8854C100.906 49.3229 100.552 49.6775 100.114 49.6775H93.9357C93.4982 49.6775 93.1436 49.3229 93.1436 48.8854V34.152C93.1436 31.0469 91.1474 28.829 88.1532 28.829C85.0481 28.829 82.8302 31.2132 82.8302 34.5956V48.8854C82.8302 49.3229 82.4756 49.6775 82.0381 49.6775H75.8595Z", bg: false },
    { d: "M105.226 49.6775C104.788 49.6775 104.434 49.3229 104.434 48.8854V12.7648C104.434 12.3273 104.788 11.9727 105.226 11.9727H111.404C111.842 11.9727 112.197 12.3273 112.197 12.7648V25.2248C113.749 22.7851 116.632 21.288 120.07 21.288C126.003 21.288 130.273 26.0011 130.273 32.5994V48.8854C130.273 49.3229 129.918 49.6775 129.481 49.6775H123.302C122.865 49.6775 122.51 49.3229 122.51 48.8854V34.152C122.51 31.0469 120.514 28.829 117.52 28.829C114.415 28.829 112.197 31.2132 112.197 34.5956V48.8854C112.197 49.3229 111.842 49.6775 111.404 49.6775H105.226Z", bg: false },
    { d: "M141.56 50.3429C136.293 50.3429 132.467 46.9051 132.467 42.1366C132.467 38.5879 135.239 35.4273 139.121 34.5956L148.214 32.7103C148.88 32.5994 149.212 32.1559 149.212 31.7123C149.212 29.7716 147.604 28.3854 145.331 28.3854C143.481 28.3854 141.939 29.4272 141.35 30.9636C141.167 31.4407 140.655 31.7711 140.173 31.601L134.271 29.5178C133.879 29.3796 133.658 28.9606 133.798 28.5698C135.34 24.2838 139.894 21.288 144.998 21.288C151.929 21.288 156.975 25.6684 156.975 31.7123V44.1327L157.07 48.8696C157.079 49.3132 156.722 49.6775 156.278 49.6775H151.533C151.204 49.6775 150.909 49.4741 150.793 49.1663L149.767 46.4615C147.992 48.8458 145.054 50.3429 141.56 50.3429ZM140.673 41.0989C140.673 42.596 141.782 43.6495 143.778 43.6495C146.662 43.6495 149.212 41.3206 149.212 37.5502V37.0511C148.602 37.2729 147.882 37.4393 147.327 37.5502L143.113 38.4373C141.56 38.77 140.673 39.7681 140.673 41.0989Z", bg: false },
    { d: "M176.588 50.3429C173.15 50.3429 170.212 48.8458 168.16 46.2397V59.9751C168.16 60.4126 167.805 60.7672 167.368 60.7672H161.189C160.752 60.7672 160.397 60.4126 160.397 59.9751V22.7455C160.397 22.308 160.752 21.9534 161.189 21.9534H165.841C166.169 21.9534 166.463 22.1559 166.58 22.4626L167.827 25.7238C169.879 22.9514 172.929 21.288 176.477 21.288C183.353 21.288 188.232 27.2764 188.232 35.7046C188.232 44.2436 183.353 50.3429 176.588 50.3429ZM174.259 42.8019C177.808 42.8019 180.359 39.8632 180.359 35.76C180.359 31.6568 177.808 28.7181 174.259 28.7181C170.711 28.7181 168.16 31.6568 168.16 35.76C168.16 39.8632 170.711 42.8019 174.259 42.8019Z", bg: false },
    { d: "M206.873 50.3429C203.435 50.3429 200.496 48.8458 198.444 46.2397V59.9751C198.444 60.4126 198.09 60.7672 197.652 60.7672H191.474C191.036 60.7672 190.682 60.4126 190.682 59.9751V22.7455C190.682 22.308 191.036 21.9534 191.474 21.9534H196.125C196.453 21.9534 196.747 22.1559 196.865 22.4626L198.112 25.7238C200.163 22.9514 203.213 21.288 206.762 21.288C213.637 21.288 218.517 27.2764 218.517 35.7046C218.517 44.2436 213.637 50.3429 206.873 50.3429ZM204.544 42.8019C208.092 42.8019 210.643 39.8632 210.643 35.76C210.643 31.6568 208.092 28.7181 204.544 28.7181C200.995 28.7181 198.444 31.6568 198.444 35.76C198.444 39.8632 200.995 42.8019 204.544 42.8019Z", bg: false },
    { d: "M222.359 55.4903C222.087 54.9631 222.473 54.321 223.053 54.1981C225.006 53.7845 226.755 52.4728 227.53 50.7865L228.417 48.7904L217.885 23.0454C217.672 22.5243 218.055 21.9534 218.618 21.9534H225.509C225.854 21.9534 226.159 22.1762 226.264 22.5046L231.965 40.3622L236.906 22.5339C237.001 22.1909 237.313 21.9534 237.669 21.9534H244.471C245.022 21.9534 245.405 22.5015 245.215 23.0185L233.962 53.6698C232.32 58.1174 229.465 60.9976 225.928 61.3933C225.621 61.4276 225.332 61.2566 225.19 60.9819L222.359 55.4903Z", bg: false },
    { d: "M247.822 50.1211C245.049 50.1211 243.053 48.0695 243.053 45.2417C243.053 42.4138 245.049 40.3622 247.822 40.3622C250.594 40.3622 252.59 42.4138 252.59 45.2417C252.59 48.0695 250.594 50.1211 247.822 50.1211Z", bg: false },
]

// ---- deterministic layout engine (identical math to sprinkle-core.js) ----

function mulberry32(a: number) {
    return function () {
        a |= 0
        a = (a + 0x6d2b79f5) | 0
        let t = Math.imul(a ^ (a >>> 15), 1 | a)
        t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
        return ((t ^ (t >>> 14)) >>> 0) / 4294967296
    }
}

function pickWeighted(colors: string[], weights: number[], r: number) {
    const w = weights && weights.length === colors.length ? weights : colors.map(() => 1)
    const total = w.reduce((a, b) => a + b, 0)
    let x = r * total
    for (let i = 0; i < colors.length; i++) if ((x -= w[i]) <= 0) return colors[i]
    return colors[colors.length - 1]
}

interface Tag {
    cx: number
    cy: number
    rot: number
    w: number
    h: number
    color: string
    order: number
    index: number
}

function buildLayout(o: any) {
    const W = o.width
    const H = o.height
    const aspect = SPRITE_W / SPRITE_H

    // height is fixed (the artwork's size); width follows its baked-in aspect.
    const tagH = o.tagHeight
    const tagW = aspect * tagH

    // SPREAD sets how airy density=1 is (must match sprinkle-core.js).
    const SPREAD = 1.3
    const density = Math.max(0.2, o.density)
    const stepX = (tagW * (1 - 0.5 * o.overlap) * SPREAD) / density
    const stepY = (tagH * (1 - 0.55 * o.overlap) * SPREAD) / density

    const rnd = mulberry32((o.seed | 0) || 1)
    const phaseA = rnd() * Math.PI * 2
    const phaseB = rnd() * Math.PI * 2

    const bandPx = H * o.bandHeight
    const baseRows = bandPx / stepY

    const rowsAt = (xFrac: number) => {
        const w1 = Math.sin(xFrac * Math.PI * 3 + phaseA)
        const w2 = Math.sin(xFrac * Math.PI * 7.3 + phaseB) * 0.5
        const lift = 0.5 + 0.5 * ((w1 + w2) / 1.5)
        return baseRows * (1 - o.wave * 0.55) + baseRows * o.wave * 0.55 * (lift * 2)
    }

    // Columns spread evenly across the width (no fixed bleed padding) so per-width
    // density stays constant — must match sprinkle-core.js.
    const cols = Math.max(1, Math.round(W / stepX))
    const cellW = W / cols
    const tags: Tag[] = []
    const totalRows = Math.ceil(baseRows * (1 + o.wave)) + 2

    for (let r = 0; r < totalRows; r++) {
        const rowY = H - tagH * 0.35 - r * stepY + (rnd() - 0.5) * stepY * 0.3
        const rowShift = (r % 2) * cellW * 0.5
        for (let c = 0; c <= cols; c++) {
            const baseX = c * cellW + rowShift + (rnd() - 0.5) * cellW * 0.35
            const edge = rowsAt(baseX / W) - r
            if (edge <= 0) continue
            if (edge < 1 && rnd() > edge) continue
            const scale = 1 + (rnd() - 0.5) * 2 * o.sizeJitter
            tags.push({
                cx: baseX + (rnd() - 0.5) * cellW * 0.25,
                cy: rowY + (rnd() - 0.5) * tagH * 0.2,
                rot: (rnd() - 0.5) * 2 * o.rotation,
                w: tagW * scale,
                h: tagH * scale,
                color: pickWeighted(o.colors, o.weights, rnd()),
                order: 0,
                index: 0,
            })
        }
    }

    // draw order fixed here (back-to-front); independent of WHEN each tag animates.
    tags.sort((a, b) => a.cy - b.cy)
    tags.forEach((t, i) => (t.index = i))
    const maxCy = tags.length ? tags[tags.length - 1].cy : 0
    const minCy = tags.length ? tags[0].cy : 0
    const span = maxCy - minCy || 1
    // Fill order: bottom-biased but spread piece-by-piece. Rank by (bottom-first
    // position + per-piece hash), then map rank to an EVEN 0..1 spread so landings
    // are continuous, one-at-a-time, not row-by-row waves (matches sprinkle-core.js).
    const BIAS = 1.3
    const keyed = tags.map((t) => {
        const h = Math.sin((t.index + 1) * 12.9898) * 43758.5453
        const hash = h - Math.floor(h)
        const base = (maxCy - t.cy) / span
        return { t, key: base * BIAS + hash }
    })
    keyed.sort((a, b) => a.key - b.key)
    const last = keyed.length - 1 || 1
    keyed.forEach((k, rank) => (k.t.order = rank / last))

    return tags
}

// One sticker, recolored, centered on its own origin.
function Sticker({ color, textColor }: { color: string; textColor: string }) {
    return (
        <g transform={`translate(${-SPRITE_W / 2} ${-SPRITE_H / 2})`}>
            {SPRITE_PATHS.map((p, i) => (
                <path key={i} d={p.d} fill={p.bg ? color : textColor} />
            ))}
        </g>
    )
}

// =============================================================================
// ====  FRAMER REACT COMPONENT  ===============================================
// =============================================================================

/**
 * @framerIntrinsicWidth 1200
 * @framerIntrinsicHeight 675
 * @framerSupportedLayoutWidth any
 * @framerSupportedLayoutHeight any
 */
export default function RunhappySprinkles(props: any) {
    const {
        color1, color2, color3, yellowBias, textColor,
        randomize, seed, tagHeight, density, bandHeight, overlap, rotation, sizeJitter, wave,
        fall, cascade, duration, bounce, tumble, animate, id, replay,
        style,
    } = props

    const containerRef = React.useRef<HTMLDivElement | null>(null)
    const [size, setSize] = React.useState({ w: 1200, h: 675 })

    // On the Framer canvas (editor) show the settled pile — no scroll-trigger and
    // no constant motion while designing. The entrance only runs in preview / live.
    const onCanvas = RenderTarget.current() === RenderTarget.canvas

    // Session record for this instance (persists across remounts). Pin the random
    // seed here so a remount re-lays the SAME pile instead of re-rolling.
    const idKey = id ? String(id) : "default"
    const rec = React.useRef(sessionRec(idKey)).current
    if (randomize && rec.seed == null) rec.seed = Math.floor(Math.random() * 2_000_000_000)
    const effSeed = randomize ? (rec.seed as number) : seed

    // Does THIS mount perform the entrance? Only if animation is on, we're not on
    // the canvas, and it hasn't already played this page session. Captured once so
    // a remount from a variant/state change renders the settled pile (no replay).
    const willPlay = React.useRef<boolean | null>(null)
    if (willPlay.current === null) willPlay.current = !!animate && !onCanvas && !rec.played

    // Play when scrolled into view — and immediately if already on screen, so an
    // above-the-fold background still plays on load.
    const [inView, setInView] = React.useState(false)
    React.useEffect(() => {
        if (!willPlay.current) return
        const el = containerRef.current
        if (!el || typeof IntersectionObserver === "undefined") {
            rec.played = true
            setInView(true)
            return
        }
        const io = new IntersectionObserver(
            (entries) => {
                if (entries[0].isIntersecting) {
                    rec.played = true // claim now so any remount mid-fall won't replay
                    setInView(true)
                    io.disconnect()
                }
            },
            { threshold: 0.15 }
        )
        io.observe(el)
        return () => io.disconnect()
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [])

    // Replay = manual re-trigger for designing in Framer. Skips the first run so it
    // never fires on a plain remount; re-arms and restarts the fall (and re-rolls
    // the seed if Random each load is on).
    const firstReplay = React.useRef(true)
    const [replayNonce, setReplayNonce] = React.useState(0)
    React.useEffect(() => {
        if (firstReplay.current) {
            firstReplay.current = false
            return
        }
        if (randomize) rec.seed = Math.floor(Math.random() * 2_000_000_000)
        rec.played = true
        willPlay.current = !!animate
        setInView(true)
        setReplayNonce((n) => n + 1)
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [replay])

    // re-fit the pile to the actual rendered frame size
    React.useEffect(() => {
        if (!containerRef.current || typeof ResizeObserver === "undefined") return
        const el = containerRef.current
        const ro = new ResizeObserver((entries) => {
            const r = entries[0].contentRect
            if (r.width > 0 && r.height > 0)
                setSize({ w: Math.round(r.width), h: Math.round(r.height) })
        })
        ro.observe(el)
        return () => ro.disconnect()
    }, [])

    const colors = [color1, color2, color3].filter(Boolean)
    const weights = [Math.max(0.1, yellowBias), 1, 1].slice(0, colors.length)

    const tags = React.useMemo(
        () =>
            buildLayout({
                width: size.w, height: size.h, colors, weights,
                seed: effSeed, tagHeight, density, bandHeight, overlap, rotation, sizeJitter, wave,
            }),
        // eslint-disable-next-line react-hooks/exhaustive-deps
        [size.w, size.h, color1, color2, color3, yellowBias, effSeed,
         tagHeight, density, bandHeight, overlap, rotation, sizeJitter, wave]
    )

    const playing = !!willPlay.current && inView

    return (
        <div
            ref={containerRef}
            style={{ width: "100%", height: "100%", overflow: "hidden", ...style }}
        >
            <svg
                width="100%"
                height="100%"
                viewBox={`0 0 ${size.w} ${size.h}`}
                preserveAspectRatio="xMidYMid slice"
                style={{ display: "block" }}
            >
                {/* replayNonce remounts the subtree on a manual Replay; size changes
                    do NOT remount (they relayout at rest) so resizing never replays. */}
                <g key={replayNonce}>
                    {tags.map((t, i) => {
                        const s = t.h / SPRITE_H
                        const place = `translate(${t.cx} ${t.cy}) rotate(${t.rot}) scale(${s})`

                        // settled pile — not animating this mount (already played,
                        // animation off, or on the editor canvas)
                        if (!willPlay.current) {
                            return (
                                <g key={i} transform={place}>
                                    <Sticker color={t.color} textColor={textColor} />
                                </g>
                            )
                        }

                        // entrance: hidden until in view, then gravity drop + one small
                        // bounce, tumbling into its final tilt. Outer group falls
                        // (vertically); inner group rotates about the sticker's centre.
                        const startY = -(t.cy + t.h) - fall
                        const delay = t.order * cascade
                        const amp1 = t.h * 0.45 * bounce
                        const times = [0, 0.68, 0.84, 1]
                        const ease = ["easeIn", "easeOut", "easeIn"]
                        // deterministic per-piece spin (matches sprinkle-lab.html)
                        const sh = Math.sin((t.index + 1) * 78.233) * 43758.5453
                        const dir = (sh - Math.floor(sh)) * 2 - 1
                        const spin0 = t.rot + dir * tumble
                        const wob = -dir * tumble * 0.12
                        return (
                            <motion.g
                                key={i}
                                initial={{ y: startY, opacity: 0 }}
                                animate={playing ? { y: [startY, 0, -amp1, 0], opacity: 1 } : { y: startY, opacity: 0 }}
                                transition={
                                    playing
                                        ? {
                                              delay,
                                              duration,
                                              times,
                                              ease,
                                              opacity: { delay, duration: Math.min(duration * 0.4, 0.22) },
                                          }
                                        : { duration: 0 }
                                }
                            >
                                <g transform={`translate(${t.cx} ${t.cy})`}>
                                    <motion.g
                                        initial={{ rotate: spin0 }}
                                        animate={playing ? { rotate: [spin0, t.rot, t.rot + wob, t.rot] } : { rotate: spin0 }}
                                        transition={playing ? { delay, duration, times, ease } : { duration: 0 }}
                                    >
                                        <g transform={`scale(${s})`}>
                                            <Sticker color={t.color} textColor={textColor} />
                                        </g>
                                    </motion.g>
                                </g>
                            </motion.g>
                        )
                    })}
                </g>
            </svg>
        </div>
    )
}

RunhappySprinkles.defaultProps = {
    color1: "#ffd13a",
    color2: "#f78dc8",
    color3: "#3bd18b",
    yellowBias: 2,
    textColor: "#000000",
    randomize: true,
    seed: 840716320,
    tagHeight: 60,
    density: 1,
    bandHeight: 0.1,
    overlap: 0.95,
    rotation: 20,
    sizeJitter: 0,
    wave: 0,
    fall: 160,
    cascade: 1.6,
    duration: 0.85,
    bounce: 0.35,
    tumble: 22,
    animate: true,
    id: "",
    replay: false,
}

addPropertyControls(RunhappySprinkles, {
    // ---- Colors ----
    color1: { type: ControlType.Color, title: "Color 1", defaultValue: "#ffd13a" },
    color2: { type: ControlType.Color, title: "Color 2", defaultValue: "#f78dc8" },
    color3: { type: ControlType.Color, title: "Color 3", defaultValue: "#3bd18b" },
    yellowBias: { type: ControlType.Number, title: "Color 1 bias", min: 0.1, max: 6, step: 0.1, defaultValue: 2 },
    textColor: { type: ControlType.Color, title: "Text ink", defaultValue: "#000000" },

    // ---- Layout ----
    randomize: { type: ControlType.Boolean, title: "Random each load", defaultValue: true },
    seed: {
        type: ControlType.Number, title: "Seed", min: 0, max: 2000000000, step: 1, defaultValue: 840716320,
        hidden: (p: any) => p.randomize,
    },
    tagHeight: { type: ControlType.Number, title: "Tag height", min: 10, max: 260, step: 1, defaultValue: 60 },
    density: { type: ControlType.Number, title: "Density", min: 0.4, max: 2.2, step: 0.05, defaultValue: 1 },
    bandHeight: { type: ControlType.Number, title: "Pile height", min: 0.1, max: 1, step: 0.01, defaultValue: 0.1 },
    overlap: { type: ControlType.Number, title: "Overlap", min: 0, max: 0.95, step: 0.01, defaultValue: 0.95 },
    rotation: { type: ControlType.Number, title: "Tilt (±°)", min: 0, max: 35, step: 1, defaultValue: 20 },
    sizeJitter: { type: ControlType.Number, title: "Size variance", min: 0, max: 0.6, step: 0.01, defaultValue: 0 },
    wave: { type: ControlType.Number, title: "Top wobble", min: 0, max: 1, step: 0.01, defaultValue: 0 },

    // ---- Motion (fall into place) ----
    animate: { type: ControlType.Boolean, title: "Animate in", defaultValue: true },
    fall: {
        type: ControlType.Number, title: "Drop distance", min: 0, max: 600, step: 10, defaultValue: 160,
        hidden: (p: any) => !p.animate,
    },
    cascade: {
        type: ControlType.Number, title: "Cascade (s)", min: 0, max: 3, step: 0.05, defaultValue: 1.6,
        hidden: (p: any) => !p.animate,
    },
    duration: {
        type: ControlType.Number, title: "Drop time", min: 0.2, max: 2.5, step: 0.05, defaultValue: 0.85,
        hidden: (p: any) => !p.animate,
    },
    bounce: {
        type: ControlType.Number, title: "Bounce", min: 0, max: 0.8, step: 0.05, defaultValue: 0.35,
        hidden: (p: any) => !p.animate,
    },
    tumble: {
        type: ControlType.Number, title: "Tumble (°)", min: 0, max: 90, step: 1, defaultValue: 22,
        hidden: (p: any) => !p.animate,
    },
    // Set a unique id per instance ONLY if you place more than one of these on a
    // page — it keeps each one's "play once" state separate. Leave blank otherwise.
    id: {
        type: ControlType.String, title: "Instance id", defaultValue: "", placeholder: "optional",
        hidden: (p: any) => !p.animate,
    },
    replay: {
        type: ControlType.Boolean, title: "Replay", defaultValue: false,
        hidden: (p: any) => !p.animate,
    },
})
