{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "venetian-blind",
  "type": "registry:lib",
  "title": "Venetian Blind",
  "description": "Two CSS mask transitions that split a slide into vertical strips — one collapses the outgoing image, the other reveals the incoming one. No JS animation library.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/venetian-blind/params.ts",
      "type": "registry:file",
      "target": "lib/vanilla-motion/venetian-blind/params.ts",
      "content": "// The one definition of every Venetian Blind value. The class merges options over `defaults`,\n// venetian-blind-controls.ts builds the Toolcraft panel from the same keys, and the embed reads\n// the same keys as kebab-case data attributes (data-direction, data-delay, data-columns…).\n// v1 hard-coded 5 columns, a 10% stagger and a .8s animation in CSS keyframes, and the two\n// tutorial pages (disappear / reveal) were two stylesheets; v2 is one effect with a direction.\n\nexport type VenetianBlindDirection = \"collapse\" | \"reveal\";\n\nexport type VenetianBlindParams = {\n  /** collapse: the outgoing slide's strips shrink to their centre. reveal: the incoming slide's strips grow from their centre. */\n  direction: VenetianBlindDirection;\n  /** Number of vertical strips (at least 2). */\n  columns: number;\n  /** Offset between neighbouring strips' start, as a percentage of the transition. 0 moves them together. */\n  stagger: number;\n  /** Length of one transition, in ms. */\n  duration: number;\n  /** Time between transitions, in ms. */\n  delay: number;\n};\n\nexport const defaults: VenetianBlindParams = {\n  direction: \"collapse\",\n  columns: 5,\n  stagger: 10,\n  duration: 800,\n  delay: 2000,\n};\n\n/** Params that restart the slide timer when they change. */\nexport const TIMER_KEYS: readonly (keyof VenetianBlindParams)[] = [\"delay\"];\n"
    },
    {
      "path": "registry/venetian-blind/styles.ts",
      "type": "registry:file",
      "target": "lib/vanilla-motion/venetian-blind/styles.ts",
      "content": "/** The CSS the effect needs wherever it's mounted. Injected once by the class. */\nexport const STYLE_ID = \"vm-venetian-blind-style\";\n\n// --vm-vb-p is the transition's overall progress. Each strip's own progress is derived from it\n// in the mask the class writes onto every slide (--vm-vb-mask), so the strip count is a param.\nexport const CSS = `\n@property --vm-vb-p {\n  syntax: \"<number>\";\n  inherits: false;\n  initial-value: 0;\n}\n@keyframes vm-venetian-blind {\n  from { --vm-vb-p: 0; }\n  to { --vm-vb-p: 1; }\n}\n.vm-venetian-blind {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n  height: 100%;\n}\n.vm-venetian-blind > .vm-venetian-blind__slide {\n  position: absolute;\n  inset: 0;\n}\n.vm-venetian-blind img {\n  display: block;\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n.vm-venetian-blind[data-vm-vb-direction=\"collapse\"] > .vm-venetian-blind__slide {\n  mask: var(--vm-vb-mask);\n}\n.vm-venetian-blind[data-vm-vb-direction=\"reveal\"] > .vm-venetian-blind__slide.active {\n  mask: var(--vm-vb-mask);\n}\n.vm-venetian-blind > .vm-venetian-blind__slide.active {\n  animation: vm-venetian-blind var(--vm-vb-duration) linear forwards;\n}\n`;\n"
    },
    {
      "path": "registry/venetian-blind/venetian-blind.ts",
      "type": "registry:file",
      "target": "lib/vanilla-motion/venetian-blind/venetian-blind.ts",
      "content": "import { defaults, TIMER_KEYS, type VenetianBlindParams } from \"./params\";\nimport { CSS, STYLE_ID } from \"./styles\";\n\nexport type { VenetianBlindDirection, VenetianBlindParams } from \"./params\";\nexport { defaults } from \"./params\";\n\nexport type VenetianBlindOptions = Partial<VenetianBlindParams> & {\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 VenetianBlindHandle {\n  setParams(next: Partial<VenetianBlindParams>): 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: VenetianBlindOptions = {}): VenetianBlindHandle {\n  return new VenetianBlind(container, options);\n}\n\nconst CLASS = \"vm-venetian-blind\";\n\nexport class VenetianBlind implements VenetianBlindHandle {\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: VenetianBlindParams;\n  private slides: HTMLElement[] = [];\n  private current = 0;\n  private busy: { slide: HTMLElement; done: () => void } | null = null;\n  private timer = 0;\n  private originalChildren: Node[];\n\n  constructor(private readonly el: HTMLElement, options: VenetianBlindOptions = {}) {\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(CLASS);\n    urls.forEach((src, i) => {\n      const slide = document.createElement(\"div\");\n      slide.className = `${CLASS}__slide`;\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.setZ(0, -1);\n    this.applyStyle();\n    if (this.slides.length >= 2) this.startTimer();\n  }\n\n  setParams(next: Partial<VenetianBlindParams>): void {\n    const changed = (Object.keys(next) as (keyof VenetianBlindParams)[]).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    this.busy?.done();\n    this.el.classList.remove(CLASS);\n    this.el.removeAttribute(\"data-vm-vb-direction\");\n    this.el.style.removeProperty(\"--vm-vb-duration\");\n    this.el.replaceChildren(...this.originalChildren);\n  }\n\n  private applyStyle(): void {\n    const p = this.params;\n    this.el.dataset.vmVbDirection = p.direction;\n    this.el.style.setProperty(\"--vm-vb-duration\", `${p.duration}ms`);\n    const mask = this.buildMask();\n    for (const slide of this.slides) slide.style.setProperty(\"--vm-vb-mask\", mask);\n  }\n\n  /**\n   * One gradient layer per strip. Strip i starts at i * stagger and travels for the rest of the\n   * transition (W = 100 - (columns - 1) * stagger); its progress eases in as t squared.\n   * collapse: visible between q*50% and 100% - q*50%. reveal: between 50% - q*50% and 50% + q*50%.\n   */\n  private buildMask(): string {\n    const n = Math.max(2, Math.round(this.params.columns));\n    const stagger = Math.max(0, this.params.stagger) / 100;\n    const window = Math.max(0.01, 1 - (n - 1) * stagger);\n    const layers: string[] = [];\n    for (let i = 0; i < n; i++) {\n      const t = `clamp(0, (var(--vm-vb-p) - ${+(i * stagger).toFixed(4)}) / ${+window.toFixed(4)}, 1)`;\n      const q = `(${t} * ${t})`;\n      const top = this.params.direction === \"reveal\" ? `calc(50% - ${q} * 50%)` : `calc(${q} * 50%)`;\n      const bottom = this.params.direction === \"reveal\" ? `calc(50% + ${q} * 50%)` : `calc(100% - ${q} * 50%)`;\n      const x = +((i / (n - 1)) * 100).toFixed(3);\n      layers.push(\n        `linear-gradient(to bottom, transparent 0 ${top}, #000 ${top} ${bottom}, transparent ${bottom}) ${x}% 0 / calc(100% / ${n} + 1px) 100% no-repeat`,\n      );\n    }\n    return layers.join(\", \");\n  }\n\n  private startTimer(): void {\n    clearInterval(this.timer);\n    this.timer = window.setInterval(() => this.advance(), this.params.delay);\n  }\n\n  /** The animated slide gets z 2, the one it uncovers or covers z 1, the rest 0. */\n  private setZ(top: number, below: number): void {\n    this.slides.forEach((s, i) => {\n      s.style.zIndex = i === top ? \"2\" : i === below ? \"1\" : \"0\";\n    });\n  }\n\n  private advance(): void {\n    if (this.busy) return;\n    const outgoing = this.current;\n    const incoming = (outgoing + 1) % this.slides.length;\n    const reveal = this.params.direction === \"reveal\";\n    const animated = this.slides[reveal ? incoming : outgoing];\n\n    if (reveal) this.setZ(incoming, outgoing);\n    else this.setZ(outgoing, incoming);\n    animated.classList.add(\"active\");\n\n    const done = () => {\n      animated.removeEventListener(\"animationend\", done);\n      animated.classList.remove(\"active\");\n      this.busy = null;\n      this.slides[outgoing].style.zIndex = \"0\";\n      this.slides[incoming].style.zIndex = \"2\";\n      this.current = incoming;\n    };\n    animated.addEventListener(\"animationend\", done);\n    this.busy = { slide: animated, done };\n  }\n}\n"
    }
  ],
  "meta": {
    "version": "1.0.0",
    "entry": "venetian-blind.ts",
    "docs": "https://vanilla-motion.com/venetian-blind/skill.md"
  }
}
