{
  "id": "fx_vue_page_transition",
  "name": "Slide and Fade Route Transition",
  "width": 480,
  "height": 720,
  "cornerRadius": 0,
  "fit": "Layout",
  "backgroundColor": "#ffffff",
  "opacity": 1,
  "x": -240,
  "y": -360,
  "responsive": {
    "enabled": true,
    "mode": "constraint",
    "width": {
      "type": "fixed"
    },
    "height": {
      "type": "fixed"
    },
    "maintainAspectRatio": false,
    "scaleObjects": false,
    "breakpoints": [
      {
        "id": "bp_mobile",
        "name": "Mobile",
        "minWidth": 0,
        "maxWidth": 599
      },
      {
        "id": "bp_tablet",
        "name": "Tablet",
        "minWidth": 600,
        "maxWidth": 1023
      },
      {
        "id": "bp_desktop",
        "name": "Desktop",
        "minWidth": 1024
      }
    ]
  },
  "layoutProps": {
    "mode": "linear",
    "direction": "vertical",
    "gap": 0,
    "padding": {
      "top": 0,
      "right": 0,
      "bottom": 0,
      "left": 0
    },
    "alignItems": "stretch",
    "layoutSizingHorizontal": "fixed",
    "layoutSizingVertical": "fixed"
  },
  "seo": {
    "title": "Slide and Fade Route Transition — ExodeUI",
    "description": "Direction-aware route transitions: pages slide and fade toward the tab you chose, with a sliding indicator."
  },
  "stateMachine": {
    "inputs": [
      {
        "id": "in_tabs",
        "name": "tabs",
        "value": {
          "type": "Text",
          "value": "Feed,Search,Library,Profile",
          "defaultValue": "Feed,Search,Library,Profile"
        }
      },
      {
        "id": "in_accent",
        "name": "accent",
        "value": {
          "type": "Text",
          "value": "#42b883",
          "defaultValue": "#42b883"
        }
      },
      {
        "id": "in_ink",
        "name": "ink",
        "value": {
          "type": "Text",
          "value": "#1f2937",
          "defaultValue": "#1f2937"
        }
      },
      {
        "id": "in_bg",
        "name": "bg",
        "value": {
          "type": "Text",
          "value": "#ffffff",
          "defaultValue": "#ffffff"
        }
      }
    ],
    "layers": [
      {
        "name": "Base",
        "entryStateId": "s_idle",
        "states": [
          {
            "id": "s_idle",
            "name": "Idle",
            "loop": true,
            "transitions": []
          }
        ]
      }
    ],
    "logicNodes": []
  },
  "animations": [],
  "physics": {
    "enabled": false,
    "gravity": {
      "x": 0,
      "y": 9.8
    }
  },
  "animationFlow": [],
  "navigation": [],
  "stateVariables": [],
  "objects": [
    {
      "id": "stage",
      "name": "Stage",
      "type": "Group",
      "transform": {
        "x": 0,
        "y": 0,
        "rotation": 0,
        "scale_x": 1,
        "scale_y": 1,
        "opacity": 1,
        "skew_x": 0,
        "skew_y": 0
      },
      "width": 480,
      "height": 720,
      "geometry": {
        "type": "Rectangle",
        "width": 480,
        "height": 720,
        "cornerRadius": 0
      },
      "cornerRadius": 0,
      "children": [],
      "clipContent": true,
      "style": {
        "fill": {
          "type": "Solid",
          "color": "#ffffff",
          "opacity": 1
        },
        "effects": [
          {
            "id": "fx_vue-page-transition",
            "type": "CustomEffect",
            "name": "Slide and Fade Route Transition",
            "isEnabled": true,
            "params": {},
            "code": "/* Shared by every motion-ui effect (prepended to each one).\n   Rules for this file and the effects: the .bexode compiler minifies effect\n   code, so use explicit semicolons, block comments only, and never write a\n   minus followed by a negative value (it collapses to a decrement). */\n\n/* Read a live state-machine input, falling back to a default. */\nfunction inp(env, name, fallback) {\n  const v = env.inputs ? env.inputs[name] : undefined;\n  if (v === undefined || v === null || v === '') return fallback;\n  if (typeof fallback === 'number') {\n    const n = Number(v);\n    return isFinite(n) ? n : fallback;\n  }\n  if (typeof fallback === 'boolean') return v === true || v === 1 || v === 'true';\n  return v;\n}\n\n/* '#rgb' / '#rrggbb' to [r, g, b]; anything else falls back to white. */\nfunction rgbOf(hex) {\n  let h = String(hex || '').trim().replace('#', '');\n  if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n  if (!/^[0-9a-fA-F]{6}$/.test(h)) return [255, 255, 255];\n  const n = parseInt(h, 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nfunction rgba(hex, a) {\n  const c = rgbOf(hex);\n  return 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + Math.max(0, Math.min(1, a)) + ')';\n}\n\nfunction mix(a, b, t) {\n  const x = rgbOf(a), y = rgbOf(b);\n  const r = Math.round(x[0] + (y[0] - x[0]) * t);\n  const g = Math.round(x[1] + (y[1] - x[1]) * t);\n  const bl = Math.round(x[2] + (y[2] - x[2]) * t);\n  return '#' + ((1 << 24) + (r << 16) + (g << 8) + bl).toString(16).slice(1);\n}\n\n/* A soft round glow, rendered once per colour and blitted with drawImage\n   (much cheaper than shadowBlur on every particle). */\nconst __glows = {};\nfunction glowSprite(hex) {\n  if (__glows[hex]) return __glows[hex];\n  const s = 64;\n  const c = document.createElement('canvas');\n  c.width = s; c.height = s;\n  const g = c.getContext('2d');\n  const gr = g.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);\n  gr.addColorStop(0, rgba(hex, 1));\n  gr.addColorStop(0.18, rgba(hex, 0.55));\n  gr.addColorStop(0.45, rgba(hex, 0.14));\n  gr.addColorStop(1, rgba(hex, 0));\n  g.fillStyle = gr;\n  g.fillRect(0, 0, s, s);\n  __glows[hex] = c;\n  return c;\n}\n\nfunction glow(ctx, hex, x, y, r, a) {\n  if (r <= 0 || a <= 0) return;\n  ctx.globalAlpha = Math.min(1, a);\n  ctx.drawImage(glowSprite(hex), x - r, y - r, r * 2, r * 2);\n  ctx.globalAlpha = 1;\n}\n\n/* Frame clock: seconds since start and a clamped delta. */\nfunction tick(S) {\n  const now = performance.now();\n  if (!S.t0) { S.t0 = now; S.last = now; }\n  S.dt = Math.min(0.05, Math.max(0.001, (now - S.last) / 1000));\n  S.last = now;\n  S.age = (now - S.t0) / 1000;\n  return S.dt;\n}\n\n/* Deterministic pseudo-random so layouts are stable across resizes. */\nfunction rng(seed) {\n  let s = seed >>> 0 || 1;\n  return function () {\n    s = (s * 1664525 + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\n\nfunction ease(x) { return x < 0.5 ? 4 * x * x * x : 1 + (Math.pow(2 * x - 2, 3)) / 2; }\nfunction clamp(v, a, b) { return v < a ? a : v > b ? b : v; }\n\nconst SANS = 'Inter, Helvetica, Arial, sans-serif';\nconst MONO = 'ui-monospace, Menlo, monospace';\n\nfunction roundRect(ctx, x, y, w, h, r) {\n  r = Math.min(r, w / 2, h / 2);\n  ctx.beginPath();\n  ctx.moveTo(x + r, y);\n  ctx.arcTo(x + w, y, x + w, y + h, r);\n  ctx.arcTo(x + w, y + h, x, y + h, r);\n  ctx.arcTo(x, y + h, x, y, r);\n  ctx.arcTo(x, y, x + w, y, r);\n  ctx.closePath();\n}\n\n/* Shared backdrop: flat colour, soft vignette and fine grain. */\nfunction backdrop(ctx, W, H, bg, tint, amt) {\n  ctx.fillStyle = bg;\n  ctx.fillRect(0, 0, W, H);\n  if (tint && amt > 0) {\n    const gr = ctx.createRadialGradient(W / 2, H * 0.45, 0, W / 2, H * 0.45, Math.max(W, H) * 0.7);\n    gr.addColorStop(0, rgba(tint, 0.10 * amt));\n    gr.addColorStop(1, rgba(tint, 0));\n    ctx.fillStyle = gr;\n    ctx.fillRect(0, 0, W, H);\n  }\n  const vg = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.3, W / 2, H / 2, Math.max(W, H) * 0.8);\n  vg.addColorStop(0, 'rgba(0,0,0,0)');\n  vg.addColorStop(1, 'rgba(0,0,0,0.45)');\n  ctx.fillStyle = vg;\n  ctx.fillRect(0, 0, W, H);\n}\n\n/* Greedy word wrap with the current ctx.font. */\nfunction wrap(ctx, text, maxW) {\n  const out = [];\n  String(text).split('\\n').forEach(function (para) {\n    const words = para.split(' ');\n    let cur = '';\n    for (let i = 0; i < words.length; i++) {\n      const next = cur ? cur + ' ' + words[i] : words[i];\n      if (cur && ctx.measureText(next).width > maxW) { out.push(cur); cur = words[i]; } else cur = next;\n    }\n    out.push(cur);\n  });\n  return out;\n}\n\n/* Largest font size (up to max) at which text wraps into maxLines within maxW. */\nfunction fitFont(ctx, text, weight, family, maxW, maxLines, max, min) {\n  let fs = max;\n  while (fs > min) {\n    ctx.font = weight + ' ' + fs + 'px ' + family;\n    if (wrap(ctx, text, maxW).length <= maxLines) break;\n    fs = fs * 0.92;\n  }\n  ctx.font = weight + ' ' + fs + 'px ' + family;\n  return fs;\n}\n\n/* Smoothed pointer that idles along a slow figure-eight when the cursor is away. */\nfunction follow(S, env, W, H, k) {\n  const pt = env.pointer || {};\n  const t = S.age || 0;\n  const tx = pt.isHovered ? pt.localX : W / 2 + Math.sin(t * 0.37) * W * 0.3;\n  const ty = pt.isHovered ? pt.localY : H / 2 + Math.sin(t * 0.74) * H * 0.22;\n  if (S.px === undefined) { S.px = tx; S.py = ty; }\n  S.px += (tx - S.px) * Math.min(1, S.dt * (k || 6));\n  S.py += (ty - S.py) * Math.min(1, S.dt * (k || 6));\n  S.inside = !!pt.isHovered;\n  return pt;\n}\n\n/* Optional centred headline over a background effect. */\nfunction title(ctx, W, H, text, sub, ink, accent, bg) {\n  if (!text) return;\n  if (bg) {\n    /* Soft scrim so the headline reads over busy motion. */\n    const sc = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, Math.min(W * 0.5, 560));\n    sc.addColorStop(0, rgba(bg, 0.72));\n    sc.addColorStop(1, rgba(bg, 0));\n    ctx.fillStyle = sc;\n    ctx.fillRect(0, 0, W, H);\n  }\n  ctx.textAlign = 'center';\n  ctx.textBaseline = 'alphabetic';\n  const maxW = Math.min(W * 0.86, 900);\n  const fs = fitFont(ctx, text, 600, SANS, maxW, 2, clamp(W * 0.065, 26, 72), 16);\n  const lines = wrap(ctx, text, maxW);\n  const lh = fs * 1.08;\n  const y0 = H / 2 - (lines.length * lh) / 2 + fs * 0.8 - (sub ? fs * 0.35 : 0);\n  ctx.fillStyle = ink;\n  lines.forEach(function (l, i) { ctx.fillText(l, W / 2, y0 + i * lh); });\n  if (sub) {\n    ctx.font = '500 ' + clamp(fs * 0.24, 11, 15) + 'px ' + MONO;\n    ctx.fillStyle = rgba(accent, 0.85);\n    ctx.fillText(String(sub).toUpperCase(), W / 2, y0 + (lines.length - 1) * lh + fs * 0.9);\n  }\n}\n\n/* Helpers shared by the framework-examples effects (prepended after the\n   motion-ui _kit.js). Same rules: explicit semicolons, block comments only,\n   and never write a minus followed by a negative value. */\n\n/* Pointer in artboard space with press and release edges. */\nfunction ptr(S, env) {\n  const pt = env.pointer || {};\n  const down = !!(pt.isDown || pt.down);\n  const p = {\n    x: pt.isHovered ? pt.localX : -9999,\n    y: pt.isHovered ? pt.localY : -9999,\n    over: !!pt.isHovered,\n    down: down,\n    pressed: down && !S.__down,\n    released: !down && !!S.__down,\n  };\n  S.__down = down;\n  return p;\n}\n\n/* Damped spring on o[key] toward target; velocity lives in o[key + 'V']. */\nfunction spring(o, key, target, k, c, dt) {\n  const vk = key + 'V';\n  const v = o[vk] || 0;\n  const a = (target - o[key]) * k - v * c;\n  o[vk] = v + a * dt;\n  o[key] += o[vk] * dt;\n}\n\nfunction approach(cur, target, rate, dt) { return cur + (target - cur) * Math.min(1, dt * rate); }\nfunction hit(p, x, y, w, h) { return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; }\nfunction near(p, x, y, r) { return Math.hypot(p.x - x, p.y - y) <= r; }\nfunction emit(env, name) { if (typeof env.emit === 'function') env.emit(name); }\nfunction lerp(a, b, t) { return a + (b - a) * t; }\nfunction easeOut(t) { t = clamp(t, 0, 1); return 1 - Math.pow(1 - t, 3); }\nfunction easeInOut(t) { t = clamp(t, 0, 1); return t < 0.5 ? 4 * t * t * t : 1 + Math.pow(2 * t - 2, 3) / 2; }\nfunction backOut(t) { t = clamp(t, 0, 1); const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); }\n\n/* Start a frame: artboard-space origin and a flat background. */\nfunction begin(ctx, W, H, bg) {\n  ctx.save();\n  ctx.translate(-W / 2, -H / 2);\n  ctx.fillStyle = bg;\n  ctx.fillRect(0, 0, W, H);\n}\n\n/* Size unit that scales with the container. */\nfunction unit(W, H) { return clamp(Math.min(W / 720, H / 440), 0.55, 1.6); }\n\nfunction text(ctx, str, x, y, size, weight, color, align, base) {\n  ctx.font = (weight || 500) + ' ' + size + 'px ' + SANS;\n  ctx.textAlign = align || 'center';\n  ctx.textBaseline = base || 'middle';\n  ctx.fillStyle = color;\n  ctx.fillText(str, x, y);\n}\n\n/* Small caption at the bottom telling the visitor what to do. */\nfunction hint(ctx, W, H, str, ink) {\n  ctx.font = '500 ' + clamp(W * 0.018, 11, 13) + 'px ' + MONO;\n  ctx.textAlign = 'center';\n  ctx.textBaseline = 'alphabetic';\n  ctx.fillStyle = rgba(ink, 0.45);\n  ctx.fillText(String(str).toUpperCase(), W / 2, H - clamp(H * 0.05, 14, 26));\n}\n\n/* Rounded panel with optional stroke. */\nfunction panel(ctx, x, y, w, h, r, fill, stroke, lw) {\n  roundRect(ctx, x, y, w, h, r);\n  if (fill) { ctx.fillStyle = fill; ctx.fill(); }\n  if (stroke) { ctx.strokeStyle = stroke; ctx.lineWidth = lw || 1; ctx.stroke(); }\n}\n\nfunction circle(ctx, x, y, r, fill) {\n  ctx.beginPath();\n  ctx.arc(x, y, Math.max(0, r), 0, Math.PI * 2);\n  ctx.fillStyle = fill;\n  ctx.fill();\n}\n\nfunction ring(ctx, x, y, r, a0, a1, color, lw, cap) {\n  ctx.beginPath();\n  ctx.arc(x, y, Math.max(0, r), a0, a1);\n  ctx.strokeStyle = color;\n  ctx.lineWidth = lw;\n  ctx.lineCap = cap || 'round';\n  ctx.stroke();\n}\n\n/* Simple burst particles: push with burst(), draw with particles(). */\nfunction burst(S, x, y, n, colors, speed) {\n  S.parts = S.parts || [];\n  for (let i = 0; i < n; i++) {\n    const a = Math.random() * Math.PI * 2;\n    const v = (0.4 + Math.random() * 0.8) * (speed || 220);\n    S.parts.push({ x: x, y: y, vx: Math.cos(a) * v, vy: Math.sin(a) * v, r: 2 + Math.random() * 3, life: 1, c: colors[i % colors.length] });\n  }\n}\nfunction particles(ctx, S, dt, gravity) {\n  const P = S.parts || [];\n  for (let i = P.length - 1; i >= 0; i--) {\n    const q = P[i];\n    q.vy += (gravity || 0) * dt;\n    q.x += q.vx * dt;\n    q.y += q.vy * dt;\n    q.vx *= 0.985;\n    q.life -= dt * 1.4;\n    if (q.life <= 0) { P.splice(i, 1); continue; }\n    ctx.globalAlpha = q.life;\n    circle(ctx, q.x, q.y, q.r, q.c);\n  }\n  ctx.globalAlpha = 1;\n}\n\n/* Slide and Fade Route Transition: direction from index change; outgoing\n   page moves 30% and fades, incoming moves the full width; tab indicator slides. */\nlet S = null;\n\nfunction screen(ctx, i, x, W, H, u, ink, accent, title) {\n  const top = 70 * u;\n  text(ctx, title, x + 22 * u, 40 * u, 26 * u, 800, ink, 'left');\n  if (i % 4 === 0) {\n    for (let r = 0; r < 3; r++) { const y = top + r * 150 * u; panel(ctx, x + 18 * u, y, W - 36 * u, 136 * u, 16 * u, mix(accent, '#ffffff', 0.8 - r * 0.1), null); panel(ctx, x + 34 * u, y + 100 * u, W * 0.4, 10 * u, 5 * u, rgba(ink, 0.25), null); }\n  } else if (i % 4 === 1) {\n    panel(ctx, x + 18 * u, top, W - 36 * u, 46 * u, 23 * u, rgba(ink, 0.06), null);\n    text(ctx, 'Search', x + 46 * u, top + 23 * u, 15 * u, 500, rgba(ink, 0.4), 'left');\n    for (let g = 0; g < 6; g++) { const cw = (W - 48 * u) / 2; panel(ctx, x + 18 * u + (g % 2) * (cw + 12 * u), top + 64 * u + Math.floor(g / 2) * (cw + 12 * u), cw, cw, 14 * u, mix(accent, ['#fde68a', '#bfdbfe', '#fbcfe8'][g % 3], 0.75), null); }\n  } else if (i % 4 === 2) {\n    for (let r = 0; r < 6; r++) { const y = top + r * 72 * u; panel(ctx, x + 18 * u, y, 56 * u, 56 * u, 10 * u, mix(accent, '#000000', r * 0.08), null); panel(ctx, x + 88 * u, y + 14 * u, W * 0.45, 10 * u, 5 * u, rgba(ink, 0.2), null); panel(ctx, x + 88 * u, y + 34 * u, W * 0.3, 8 * u, 4 * u, rgba(ink, 0.1), null); }\n  } else {\n    circle(ctx, x + W / 2, top + 70 * u, 50 * u, mix(accent, '#ffffff', 0.4));\n    text(ctx, 'Ada Lovelace', x + W / 2, top + 150 * u, 20 * u, 750, ink);\n    for (let r = 0; r < 4; r++) panel(ctx, x + 18 * u, top + 190 * u + r * 60 * u, W - 36 * u, 48 * u, 12 * u, rgba(ink, 0.05), null);\n  }\n}\n\nfunction render(ctx, env) {\n  const W = env.width, H = env.height;\n  if (!S) S = { a: 0, prev: 0, t: 1, dir: 1, ind: 0, auto: 0 };\n  const dt = tick(S);\n  const p = ptr(S, env);\n  const tabs = String(inp(env, 'tabs', 'Feed,Search,Library,Profile')).split(',').map(function (s) { return s.trim(); }).filter(Boolean);\n  const n = Math.max(1, tabs.length);\n  const accent = inp(env, 'accent', '#42b883');\n  const ink = inp(env, 'ink', '#1f2937');\n  const bg = inp(env, 'bg', '#ffffff');\n  const u = unit(W, H);\n  begin(ctx, W, H, bg);\n  const go = function (i) { if (i === S.a) return; S.dir = i > S.a ? 1 : 0 - 1; S.prev = S.a; S.a = i; S.t = 0; emit(env, 'navigate'); };\n  const barH = 72 * u, barY = H - barH;\n  S.auto += dt;\n  if (p.over) S.auto = 0;\n  if (S.auto > 2.2) { S.auto = 0; go((S.a + 1) % n); }\n  for (let i = 0; i < n; i++) if (p.released && hit(p, i * W / n, barY, W / n, barH)) go(i);\n  S.t = Math.min(1, S.t + dt * 2.4);\n  const e = easeInOut(S.t);\n  ctx.save();\n  ctx.beginPath(); ctx.rect(0, 0, W, barY); ctx.clip();\n  if (S.t < 1) { ctx.globalAlpha = 1 - e; screen(ctx, S.prev, 0 - S.dir * e * W * 0.3, W, H, u, ink, accent, tabs[S.prev]); }\n  ctx.globalAlpha = S.t < 1 ? e : 1;\n  screen(ctx, S.a, S.dir * (1 - e) * W, W, H, u, ink, accent, tabs[S.a]);\n  ctx.restore();\n  ctx.globalAlpha = 1;\n  ctx.fillStyle = bg; ctx.fillRect(0, barY, W, barH);\n  ctx.fillStyle = rgba(ink, 0.08); ctx.fillRect(0, barY, W, 1);\n  S.ind = approach(S.ind, S.a, 12, dt);\n  panel(ctx, S.ind * W / n + W / n * 0.25, barY + 6 * u, W / n * 0.5, 4 * u, 2 * u, accent, null);\n  for (let i = 0; i < n; i++) {\n    const cx = (i + 0.5) * W / n;\n    circle(ctx, cx, barY + 30 * u, 7 * u, i === S.a ? accent : rgba(ink, 0.25));\n    text(ctx, tabs[i], cx, barY + 52 * u, 11 * u, 600, i === S.a ? accent : rgba(ink, 0.5));\n  }\n  ctx.restore();\n}\n"
          }
        ]
      },
      "layoutProps": {
        "layoutSizingHorizontal": "fill",
        "layoutSizingVertical": "fill"
      },
      "zIndex": 1,
      "interactions": []
    }
  ]
}
