{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ripple-reveal",
  "type": "registry:lib",
  "title": "Ripple Reveal",
  "description": "Slideshow transition using CSS mask + radial-gradient to expand slides outward from a configurable origin point with a soft liquid ripple edge",
  "dependencies": [],
  "files": [
    {
      "path": "registry/ripple-reveal/params.ts",
      "type": "registry:file",
      "target": "lib/vanilla-motion/ripple-reveal/params.ts",
      "content": "// The one definition of every Ripple Reveal value. The class merges options over `defaults`,\n// ripple-reveal-controls.ts builds the Toolcraft panel from the same keys, and the embed reads\n// the same keys as kebab-case data attributes (data-origin-x, data-delay…).\n// v1 read these from CSS custom properties on the container (--origin-x, --feather, --duration,\n// --delay, --ease); v2 takes them from params only.\n\nexport type RippleRevealParams = {\n  /** Time between slides, in ms. */\n  delay: number;\n  /** Length of one reveal, in ms. */\n  duration: number;\n  /** Width of the soft edge, as a percentage of the element. 0 is a hard circle. */\n  feather: number;\n  /** Where the ripple starts, horizontally: 0 is the left edge, 100 the right. */\n  originX: number;\n  /** Where the ripple starts, vertically: 0 is the top edge, 100 the bottom. */\n  originY: number;\n  /** CSS timing function of the expansion. */\n  ease: string;\n  /** Scale the active slide's image drifts to (1 turns the drift off). */\n  zoom: number;\n  /** Colour behind the slides. */\n  background: string;\n};\n\nexport const defaults: RippleRevealParams = {\n  delay: 5000,\n  duration: 1000,\n  feather: 20,\n  originX: 50,\n  originY: 50,\n  ease: \"cubic-bezier(0.4, 0, 1, 1)\",\n  zoom: 1.1,\n  background: \"#0a372a\",\n};\n\n/** Params that restart the slide timer when they change. */\nexport const TIMER_KEYS: readonly (keyof RippleRevealParams)[] = [\"delay\"];\n"
    },
    {
      "path": "registry/ripple-reveal/ripple-reveal.ts",
      "type": "registry:file",
      "target": "lib/vanilla-motion/ripple-reveal/ripple-reveal.ts",
      "content": "import { defaults, TIMER_KEYS, type RippleRevealParams } from \"./params\";\nimport { CSS, STYLE_ID } from \"./styles\";\n\nexport type { RippleRevealParams } from \"./params\";\nexport { defaults } from \"./params\";\n\nexport type RippleRevealOptions = Partial<RippleRevealParams> & {\n  /** Slide image URLs. Without it the images already inside the element are used. */\n  images?: string[];\n};\n\n/** The public surface wrappers use (Toolcraft canvas, embed). */\nexport interface RippleRevealHandle {\n  setParams(next: Partial<RippleRevealParams>): void;\n  dispose(): void;\n}\n\n/**\n * Fill an element with the slideshow. Slides come from `options.images`, or from the `<img>`\n * elements already inside the element (any nesting, in order). Used by the Toolcraft app,\n * the embed page and the embed script.\n */\nexport function mount(container: HTMLElement, options: RippleRevealOptions = {}): RippleRevealHandle {\n  return new RippleReveal(container, options);\n}\n\nexport class RippleReveal implements RippleRevealHandle {\n  /** Same as meta.json \"version\"; bump both when the class or params change. */\n  static readonly version = \"1.0.0\";\n\n  private params: RippleRevealParams;\n  private slides: HTMLElement[] = [];\n  private current = 0;\n  private zCounter = 1;\n  private timer = 0;\n  private raf = 0;\n  private cleanups = new Set<number>();\n  private originalChildren: Node[];\n\n  constructor(private readonly el: HTMLElement, options: RippleRevealOptions = {}) {\n    const { images, ...params } = options;\n    this.params = { ...defaults, ...params };\n\n    const urls = images ?? Array.from(el.querySelectorAll(\"img\"), (img) => img.currentSrc || img.src).filter(Boolean);\n    this.originalChildren = Array.from(el.childNodes);\n\n    if (!document.getElementById(STYLE_ID)) {\n      const style = document.createElement(\"style\");\n      style.id = STYLE_ID;\n      style.textContent = CSS;\n      document.head.appendChild(style);\n    }\n\n    el.replaceChildren();\n    el.classList.add(\"vm-ripple-reveal\");\n    urls.forEach((src, i) => {\n      const slide = document.createElement(\"div\");\n      slide.className = \"vm-ripple-reveal__slide\";\n      if (i === 0) slide.style.zIndex = \"1\";\n      const img = document.createElement(\"img\");\n      img.src = src;\n      img.alt = \"\";\n      if (i === 0) {\n        img.fetchPriority = \"high\";\n        img.loading = \"eager\";\n      }\n      slide.appendChild(img);\n      el.appendChild(slide);\n      this.slides.push(slide);\n    });\n\n    this.applyStyle();\n    if (this.slides.length < 2) return;\n    this.raf = requestAnimationFrame(() => this.slides[0].classList.add(\"active\"));\n    this.startTimer();\n  }\n\n  setParams(next: Partial<RippleRevealParams>): void {\n    const changed = (Object.keys(next) as (keyof RippleRevealParams)[]).filter((k) => next[k] !== undefined && next[k] !== this.params[k]);\n    this.params = { ...this.params, ...next };\n    this.applyStyle();\n    if (this.slides.length >= 2 && changed.some((k) => TIMER_KEYS.includes(k))) this.startTimer();\n  }\n\n  dispose(): void {\n    clearInterval(this.timer);\n    cancelAnimationFrame(this.raf);\n    this.cleanups.forEach((id) => clearTimeout(id));\n    this.cleanups.clear();\n    this.el.classList.remove(\"vm-ripple-reveal\");\n    for (const prop of [\"--vm-rr-bg\", \"--vm-rr-x\", \"--vm-rr-y\", \"--vm-rr-feather\", \"--vm-rr-duration\", \"--vm-rr-ease\", \"--vm-rr-zoom\"]) {\n      this.el.style.removeProperty(prop);\n    }\n    this.el.replaceChildren(...this.originalChildren);\n  }\n\n  private applyStyle(): void {\n    const p = this.params;\n    const s = this.el.style;\n    s.setProperty(\"--vm-rr-bg\", p.background);\n    s.setProperty(\"--vm-rr-x\", `${p.originX}%`);\n    s.setProperty(\"--vm-rr-y\", `${p.originY}%`);\n    s.setProperty(\"--vm-rr-feather\", `${p.feather}%`);\n    s.setProperty(\"--vm-rr-duration\", `${p.duration}ms`);\n    s.setProperty(\"--vm-rr-ease\", p.ease);\n    s.setProperty(\"--vm-rr-zoom\", String(p.zoom));\n  }\n\n  private startTimer(): void {\n    clearInterval(this.timer);\n    this.timer = window.setInterval(() => this.setActive((this.current + 1) % this.slides.length), this.params.delay);\n  }\n\n  private setActive(idx: number): void {\n    const prev = this.current;\n    this.current = idx;\n    const slide = this.slides[idx];\n    slide.style.zIndex = String(++this.zCounter);\n    // remove + add restarts the keyframes even if the slide was active before\n    slide.classList.remove(\"active\");\n    void slide.offsetWidth;\n    slide.classList.add(\"active\");\n\n    const id = window.setTimeout(() => {\n      this.cleanups.delete(id);\n      if (prev === idx) return;\n      this.slides[prev].classList.remove(\"active\");\n      this.slides[prev].style.zIndex = \"0\";\n    }, this.params.duration + 200);\n    this.cleanups.add(id);\n  }\n}\n"
    },
    {
      "path": "registry/ripple-reveal/styles.ts",
      "type": "registry:file",
      "target": "lib/vanilla-motion/ripple-reveal/styles.ts",
      "content": "/** The CSS the effect needs wherever it's mounted. Injected once by the class. */\nexport const STYLE_ID = \"vm-ripple-reveal-style\";\n\nexport const CSS = `\n@property --vm-rr-p {\n  syntax: \"<number>\";\n  inherits: false;\n  initial-value: 0;\n}\n@keyframes vm-ripple-reveal {\n  from { --vm-rr-p: 0; }\n  to { --vm-rr-p: 1; }\n}\n.vm-ripple-reveal {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n  height: 100%;\n  background-color: var(--vm-rr-bg);\n}\n.vm-ripple-reveal > .vm-ripple-reveal__slide {\n  position: absolute;\n  inset: 0;\n}\n.vm-ripple-reveal img {\n  display: block;\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  transform: scale(1);\n  transition: transform 10s linear;\n}\n.vm-ripple-reveal > .active img {\n  transform: scale(var(--vm-rr-zoom));\n}\n.vm-ripple-reveal > .active {\n  --vm-rr-radius: calc(var(--vm-rr-p) * 150%);\n  --vm-rr-edge-in: calc(var(--vm-rr-radius) - var(--vm-rr-feather) / 2);\n  --vm-rr-edge-out: calc(var(--vm-rr-radius) + var(--vm-rr-feather) / 2);\n  mask: radial-gradient(circle at var(--vm-rr-x) var(--vm-rr-y), #000 var(--vm-rr-edge-in), transparent var(--vm-rr-edge-out));\n  animation: vm-ripple-reveal var(--vm-rr-duration) var(--vm-rr-ease) forwards;\n}\n`;\n"
    }
  ],
  "meta": {
    "version": "1.0.0",
    "entry": "ripple-reveal.ts",
    "docs": "https://vanilla-motion.com/ripple-reveal/skill.md"
  }
}
