{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mondrian-split",
  "title": "Mondrian Split",
  "description": "Black lines cut the frame into a seeded De Stijl grid, primaries wipe into its cells, the grid reflows like rubber, and one cell pushes its own lines out past the edges to become a clean color field or the next scene.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/mondrian-split/index.tsx",
      "content": "\"use client\";\n\nimport { type CSSProperties, type ReactNode, useMemo } from \"react\";\nimport { Easing, random, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport type MondrianSplitAxis = \"x\" | \"y\";\nexport type MondrianSplitSlot = \"red\" | \"yellow\" | \"blue\";\nexport type MondrianSplitSide = \"left\" | \"top\" | \"right\" | \"bottom\";\n\nexport interface MondrianSplitProps {\n  children?: ReactNode;\n  seed?: number;\n  splits?: number;\n  lineWeight?: number;\n  expandCell?: number;\n  lineColor?: string;\n  redColor?: string;\n  yellowColor?: string;\n  blueColor?: string;\n  fieldColor?: string;\n  speed?: number;\n  className?: string;\n}\n\nexport type MondrianSplitOptions = Pick<\n  MondrianSplitProps,\n  \"seed\" | \"splits\" | \"lineWeight\" | \"expandCell\"\n> & {\n  width?: number;\n  height?: number;\n};\n\nexport interface MondrianSplitBox {\n  x0: number;\n  y0: number;\n  x1: number;\n  y1: number;\n}\n\nexport interface MondrianSplitRect {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\ninterface GridNode {\n  box: MondrianSplitBox;\n  bounds: Record<MondrianSplitSide, number>;\n  line: number;\n}\n\ninterface GridCut {\n  axis: MondrianSplitAxis;\n  position: number;\n  node: number;\n}\n\ninterface GridMove {\n  track: number;\n  from: number;\n  to: number;\n  start: number;\n}\n\ninterface GridWipe {\n  axis: MondrianSplitAxis;\n  fromEnd: boolean;\n}\n\nexport const mondrianSplitDefaults = {\n  seed: 7,\n  splits: 7,\n  lineWeight: 14,\n  expandCell: 0,\n  lineColor: \"#121212\",\n  redColor: \"#d62d20\",\n  yellowColor: \"#f4c20d\",\n  blueColor: \"#1f4e9e\",\n  fieldColor: \"#d62d20\",\n};\n\nconst LINE_START = 3;\nconst LINE_GAP = 5;\nconst LINE_DRAW = 10;\nconst FILL_LAG = 7;\nconst FILL_GAP = 4;\nconst FILL_WIPE = 9;\nconst FIELD_LAG = 6;\nconst FIELD_WIPE = 12;\nconst HOLD = 14;\nconst TRACK_COUNT = 3;\nconst TRACK_GAP = 5;\nconst TRACK_SLIDE = 20;\nconst EXPAND_LEAD = 4;\nconst SIDE_GAP = 3;\nconst SIDE_PUSH = 22;\nconst TAIL = 12;\nconst SLIDE_PEAK = 0.72;\nconst SLIDE_OVER = 0.035;\nconst RESERVE = 0.045;\nconst WIND = 0.22;\nconst DIP = 0.035;\nconst DIP_SHARE = 0.2;\nconst SIDES: MondrianSplitSide[] = [\"left\", \"top\", \"right\", \"bottom\"];\nconst SLOTS: MondrianSplitSlot[] = [\"blue\", \"yellow\", \"red\", \"blue\", \"yellow\"];\nconst FALLBACK_CUTS = [0.38, 0.62, 0.3, 0.7, 0.5];\n\nconst drawEase = Easing.bezier(0.16, 1, 0.3, 1);\nconst wipeEase = Easing.bezier(0.7, 0, 0.2, 1);\nconst riseEase = Easing.bezier(0.65, 0, 0.3, 1);\nconst settleEase = Easing.bezier(0.4, 0, 0.6, 1);\nconst windEase = Easing.bezier(0.45, 0, 0.55, 1);\nconst pushEase = Easing.poly(2.5);\nconst layer: CSSProperties = { position: \"absolute\", inset: 0 };\n\nconst finite = (value: number, fallback: number) =>\n  Number.isFinite(value) ? value : fallback;\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(max, Math.max(min, value));\nconst mix = (from: number, to: number, amount: number) =>\n  from + (to - from) * amount;\nconst progress = (time: number, start: number, duration: number) =>\n  clamp((time - start) / duration, 0, 1);\nconst other = (axis: MondrianSplitAxis): MondrianSplitAxis =>\n  axis === \"x\" ? \"y\" : \"x\";\nconst lowEdge = (box: MondrianSplitBox, axis: MondrianSplitAxis) =>\n  axis === \"x\" ? box.x0 : box.y0;\nconst extent = (box: MondrianSplitBox, axis: MondrianSplitAxis) =>\n  axis === \"x\" ? box.x1 - box.x0 : box.y1 - box.y0;\nconst area = (box: MondrianSplitBox) => extent(box, \"x\") * extent(box, \"y\");\nconst splitCount = (splits: number) =>\n  Math.round(clamp(finite(splits, mondrianSplitDefaults.splits), 3, 12));\nconst coloredCount = (count: number) => clamp(Math.round(count * 0.3), 1, 5);\n\nconst touches = (a: MondrianSplitBox, b: MondrianSplitBox) => {\n  const overlapX = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);\n  const overlapY = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);\n  const beside = Math.abs(a.x1 - b.x0) < 0.5 || Math.abs(b.x1 - a.x0) < 0.5;\n  const stacked = Math.abs(a.y1 - b.y0) < 0.5 || Math.abs(b.y1 - a.y0) < 0.5;\n  return (beside && overlapY > 0.5) || (stacked && overlapX > 0.5);\n};\n\nconst choose = (weights: readonly number[], roll: number) => {\n  let remaining = roll * weights.reduce((sum, value) => sum + value, 0);\n  for (let index = 0; index < weights.length; index++) {\n    remaining -= weights[index];\n    if (remaining <= 0) return index;\n  }\n  return weights.length - 1;\n};\n\nconst slide = (amount: number) => {\n  if (amount >= 1) return 1;\n  if (amount <= SLIDE_PEAK) {\n    return (1 + SLIDE_OVER) * riseEase(amount / SLIDE_PEAK);\n  }\n  const settle = settleEase((amount - SLIDE_PEAK) / (1 - SLIDE_PEAK));\n  return mix(1 + SLIDE_OVER, 1, settle);\n};\n\nconst push = (amount: number, dip: number) => {\n  if (amount >= 1) return 1;\n  if (amount <= 0) return 0;\n  if (amount < WIND) return -dip * windEase(amount / WIND);\n  return mix(-dip, 1, pushEase((amount - WIND) / (1 - WIND)));\n};\n\nconst stretch = (\n  from0: number,\n  from1: number,\n  to0: number,\n  to1: number,\n  min: number,\n  max: number,\n) => {\n  if (from0 === to0 && from1 === to1) return (value: number) => value;\n  return (value: number) => {\n    if (value <= from0) {\n      return min + (value - min) * ((to0 - min) / (from0 - min));\n    }\n    if (value >= from1) {\n      return max - (max - value) * ((max - to1) / (max - from1));\n    }\n    return to0 + (value - from0) * ((to1 - to0) / (from1 - from0));\n  };\n};\n\nconst wipe = (\n  box: MondrianSplitBox,\n  { axis, fromEnd }: GridWipe,\n  amount: number,\n): MondrianSplitRect => {\n  const width = box.x1 - box.x0;\n  const height = box.y1 - box.y0;\n  if (axis === \"x\") {\n    const span = width * amount;\n    return {\n      x: fromEnd ? box.x1 - span : box.x0,\n      y: box.y0,\n      width: span,\n      height,\n    };\n  }\n  const span = height * amount;\n  return {\n    x: box.x0,\n    y: fromEnd ? box.y1 - span : box.y0,\n    width,\n    height: span,\n  };\n};\n\nconst inFrame = (rect: MondrianSplitRect, width: number, height: number) =>\n  rect.width > 0.01 &&\n  rect.height > 0.01 &&\n  rect.x < width &&\n  rect.y < height &&\n  rect.x + rect.width > 0 &&\n  rect.y + rect.height > 0;\n\nconst placeBoxes = (\n  cuts: readonly Pick<GridCut, \"axis\" | \"node\">[],\n  positions: readonly number[],\n  width: number,\n  height: number,\n) => {\n  const boxes: MondrianSplitBox[] = [{ x0: 0, y0: 0, x1: width, y1: height }];\n  for (let id = 0; id < cuts.length; id++) {\n    const { axis, node } = cuts[id];\n    const box = boxes[node];\n    const at = positions[id];\n    boxes.push(\n      axis === \"x\" ? { ...box, x1: at } : { ...box, y1: at },\n      axis === \"x\" ? { ...box, x0: at } : { ...box, y0: at },\n    );\n  }\n  return boxes;\n};\n\nconst percent = (value: number, size: number) =>\n  `${(clamp(value / size, 0, 1) * 100).toFixed(4)}%`;\n\nexport function getMondrianSplitTimeline({\n  splits = mondrianSplitDefaults.splits,\n}: Pick<MondrianSplitProps, \"splits\"> = {}) {\n  const count = splitCount(splits);\n  const fill = LINE_START + (count - 1) * LINE_GAP + FILL_LAG;\n  const field = fill + (coloredCount(count) - 1) * FILL_GAP + FIELD_LAG;\n  const reflow = field + FIELD_WIPE + HOLD;\n  const settled = reflow + (TRACK_COUNT - 1) * TRACK_GAP + TRACK_SLIDE;\n  const expand = settled - EXPAND_LEAD;\n  const end = expand + (SIDES.length - 1) * SIDE_GAP + SIDE_PUSH;\n  return { lines: LINE_START, fill, field, reflow, expand, end };\n}\n\nexport const mondrianSplitLength = getMondrianSplitTimeline().end;\n\nexport function getMondrianSplitDuration({\n  speed = 1,\n  splits = mondrianSplitDefaults.splits,\n}: Pick<MondrianSplitProps, \"speed\" | \"splits\"> = {}) {\n  const rate = Math.max(0, finite(speed, 1));\n  const frames = getMondrianSplitTimeline({ splits }).end + TAIL;\n  return rate === 0 ? 1 : Math.max(1, Math.ceil(frames / rate));\n}\n\nexport function getMondrianSplitLayout({\n  seed = mondrianSplitDefaults.seed,\n  splits = mondrianSplitDefaults.splits,\n  lineWeight = mondrianSplitDefaults.lineWeight,\n  expandCell = mondrianSplitDefaults.expandCell,\n  width = 1280,\n  height = 720,\n}: MondrianSplitOptions = {}) {\n  const w = Math.max(1, finite(width, 1280));\n  const h = Math.max(1, finite(height, 720));\n  const short = Math.min(w, h);\n  const weight = clamp(finite(lineWeight, 14), 2, 48) * (short / 720);\n  const count = splitCount(splits);\n  const timeline = getMondrianSplitTimeline({ splits: count });\n  const salt = Math.round(finite(seed, mondrianSplitDefaults.seed));\n  const roll = (name: string) => random(`mondrian-split-${salt}-${name}`);\n  const minSide = Math.max(short * 0.1, weight * 3);\n  const snap = short * 0.045;\n  const align = short * 0.075;\n  const gap = Math.max(short * 0.07, weight * 2.5);\n  const minMove = short * 0.06;\n  const overscan = weight + short * 0.04;\n  const nodes: GridNode[] = [\n    {\n      box: { x0: 0, y0: 0, x1: w, y1: h },\n      bounds: { left: -1, top: -1, right: -1, bottom: -1 },\n      line: -1,\n    },\n  ];\n  const cuts: GridCut[] = [];\n\n  const lean = (\n    box: MondrianSplitBox,\n    ratio: number,\n    name: string,\n  ): MondrianSplitAxis => {\n    const wide = extent(box, \"x\");\n    const tall = extent(box, \"y\");\n    if (wide > tall * ratio) return \"x\";\n    if (tall > wide * ratio) return \"y\";\n    return roll(name) < 0.5 ? \"x\" : \"y\";\n  };\n\n  const cut = (node: number, axis: MondrianSplitAxis, position: number) => {\n    const id = cuts.length;\n    const { box, bounds: edges } = nodes[node];\n    const vertical = axis === \"x\";\n    nodes[node].line = id;\n    cuts.push({ axis, position, node });\n    nodes.push(\n      {\n        box: vertical ? { ...box, x1: position } : { ...box, y1: position },\n        bounds: vertical ? { ...edges, right: id } : { ...edges, bottom: id },\n        line: -1,\n      },\n      {\n        box: vertical ? { ...box, x0: position } : { ...box, y0: position },\n        bounds: vertical ? { ...edges, left: id } : { ...edges, top: id },\n        line: -1,\n      },\n    );\n  };\n\n  const snapTo = (axis: MondrianSplitAxis, position: number) => {\n    let best = position;\n    let distance = snap;\n    for (const line of cuts) {\n      const offset = Math.abs(line.position - position);\n      if (line.axis === axis && offset < distance) {\n        best = line.position;\n        distance = offset;\n      }\n    }\n    return best;\n  };\n\n  const nearMiss = (axis: MondrianSplitAxis, position: number) =>\n    cuts.some(\n      (line) =>\n        line.axis === axis &&\n        line.position !== position &&\n        Math.abs(line.position - position) < snap,\n    );\n\n  const tryCut = (\n    node: number,\n    axis: MondrianSplitAxis,\n    fraction: number,\n    strict: boolean,\n  ) => {\n    const { box } = nodes[node];\n    const low = lowEdge(box, axis);\n    const length = extent(box, axis);\n    if (length < minSide * 2) return false;\n    const position = clamp(\n      snapTo(axis, low + length * fraction),\n      low + minSide,\n      low + length - minSide,\n    );\n    if (strict && nearMiss(axis, position)) return false;\n    cut(node, axis, position);\n    return true;\n  };\n\n  let anchor = -1;\n  const longAxis = lean(nodes[0].box, 1.05, \"square\");\n  const rootAxis = roll(\"root-axis\") < 0.75 ? longAxis : other(longAxis);\n  const rootFraction =\n    roll(\"root-side\") < 0.5\n      ? mix(0.3, 0.4, roll(\"root-at\"))\n      : mix(0.6, 0.7, roll(\"root-at\"));\n  if (tryCut(0, rootAxis, rootFraction, false)) {\n    const share = mix(0.6, 0.76, roll(\"anchor-at\"));\n    const anchorLow = roll(\"anchor-side\") < 0.5;\n    const big = rootFraction < 0.5 ? 2 : 1;\n    if (tryCut(big, other(rootAxis), anchorLow ? share : 1 - share, false)) {\n      anchor = anchorLow ? nodes.length - 2 : nodes.length - 1;\n    }\n  }\n\n  const splittable = (index: number) => {\n    const { box, line } = nodes[index];\n    return (\n      line === -1 &&\n      index !== anchor &&\n      Math.max(extent(box, \"x\"), extent(box, \"y\")) >= minSide * 2\n    );\n  };\n\n  const budget = count * 24;\n  for (let attempt = 0; cuts.length < count && attempt < budget; attempt++) {\n    const tag = `${cuts.length}-${attempt}`;\n    const pool = nodes.flatMap((_, index) =>\n      splittable(index) ? [index] : [],\n    );\n    if (pool.length === 0) break;\n    const weights = pool.map((index) => area(nodes[index].box) ** 1.2);\n    const node = pool[choose(weights, roll(`${tag}-cell`))];\n    const { box } = nodes[node];\n    const preferred = lean(box, 1.25, `${tag}-axis`);\n    const fits = extent(box, preferred) >= minSide * 2;\n    const at = roll(`${tag}-at`);\n    const fraction = at < 0.5 ? 0.28 + at * 0.34 : 0.55 + (at - 0.5) * 0.34;\n    tryCut(node, fits ? preferred : other(preferred), fraction, true);\n  }\n\n  const byPriority = (a: number, b: number) =>\n    Number(a === anchor) - Number(b === anchor) ||\n    area(nodes[b].box) - area(nodes[a].box);\n  const fallback = () => {\n    const pool = nodes.flatMap((node, index) =>\n      node.line === -1 ? [index] : [],\n    );\n    pool.sort(byPriority);\n    for (const strict of [true, false]) {\n      for (const node of pool) {\n        const { box } = nodes[node];\n        const first: MondrianSplitAxis =\n          extent(box, \"x\") >= extent(box, \"y\") ? \"x\" : \"y\";\n        for (const axis of [first, other(first)]) {\n          for (const fraction of FALLBACK_CUTS) {\n            if (tryCut(node, axis, fraction, strict)) return true;\n          }\n        }\n      }\n    }\n    return false;\n  };\n  while (cuts.length < count) {\n    if (!fallback()) break;\n  }\n\n  const trackKeys: string[] = [];\n  const lines = cuts.map((draft, id) => {\n    const key = `${draft.axis}:${draft.position}`;\n    if (!trackKeys.includes(key)) trackKeys.push(key);\n    const { bounds } = nodes[draft.node];\n    const vertical = draft.axis === \"x\";\n    const near = vertical ? bounds.top : bounds.left;\n    const far = vertical ? bounds.bottom : bounds.right;\n    return {\n      id,\n      axis: draft.axis,\n      position: draft.position,\n      node: draft.node,\n      track: trackKeys.indexOf(key),\n      lowBound: vertical ? bounds.left : bounds.top,\n      highBound: vertical ? bounds.right : bounds.bottom,\n      fromEnd: near === far ? roll(`draw-${id}`) < 0.5 : far < near,\n      start: LINE_START + id * LINE_GAP,\n    };\n  });\n\n  const leaves = nodes.flatMap((node, index) =>\n    node.line === -1 ? [index] : [],\n  );\n  const ranked = [...leaves].sort(\n    (a, b) => area(nodes[b].box) - area(nodes[a].box) || a - b,\n  );\n  const pick = clamp(Math.round(finite(expandCell, 0)), 0, ranked.length - 1);\n  const target = ranked[pick];\n  const targetBox = nodes[target].box;\n\n  const others = leaves.filter((index) => index !== target);\n  const largest = Math.max(1, ...others.map((index) => area(nodes[index].box)));\n  const candidates = others\n    .map((index) => ({\n      index,\n      score:\n        roll(`fill-${index}`) +\n        (touches(nodes[index].box, targetBox) ? 0.45 : 0) +\n        (0.3 * area(nodes[index].box)) / largest,\n    }))\n    .sort((a, b) => a.score - b.score);\n  const painted: { index: number; slot: MondrianSplitSlot }[] = [];\n  const quota = Math.min(coloredCount(count), candidates.length);\n  for (let order = 0; order < quota; order++) {\n    const slot = SLOTS[order];\n    const free = candidates.filter(\n      ({ index }) => !painted.some((cell) => cell.index === index),\n    );\n    const clean = free.find(\n      ({ index }) =>\n        !painted.some(\n          (cell) =>\n            cell.slot === slot &&\n            touches(nodes[cell.index].box, nodes[index].box),\n        ) && !(slot === \"red\" && touches(nodes[index].box, targetBox)),\n    );\n    const choice = clean ?? free[0];\n    if (choice) painted.push({ index: choice.index, slot });\n  }\n\n  const wipeOf = (index: number): GridWipe => {\n    const { box } = nodes[index];\n    const axis = extent(box, \"x\") >= extent(box, \"y\") ? \"x\" : \"y\";\n    const middle = axis === \"x\" ? box.x0 + box.x1 : box.y0 + box.y1;\n    return { axis, fromEnd: middle < (axis === \"x\" ? w : h) };\n  };\n  const diagonal = (index: number) => {\n    const { box } = nodes[index];\n    return (box.x0 + box.x1) / w + (box.y0 + box.y1) / h;\n  };\n  const fills = [...painted]\n    .sort((a, b) => diagonal(a.index) - diagonal(b.index))\n    .map((cell, order) => ({\n      node: cell.index,\n      slot: cell.slot,\n      ...wipeOf(cell.index),\n      start: timeline.fill + order * FILL_GAP,\n    }));\n  const field = { node: target, ...wipeOf(target), start: timeline.field };\n\n  const tracks = trackKeys.map((_, track) => {\n    const members = lines.filter((line) => line.track === track);\n    return {\n      track,\n      axis: members[0].axis,\n      position: members[0].position,\n      length: members.reduce(\n        (sum, line) => sum + extent(nodes[line.node].box, other(line.axis)),\n        0,\n      ),\n    };\n  });\n  const reserveLow = lines.map((line) => line.position);\n  const reserveHigh = lines.map((line) => line.position);\n  const limits = (track: number) => {\n    let low = Number.NEGATIVE_INFINITY;\n    let high = Number.POSITIVE_INFINITY;\n    for (const line of lines) {\n      if (line.track === track) {\n        const size = line.axis === \"x\" ? w : h;\n        const floor = line.lowBound === -1 ? 0 : reserveHigh[line.lowBound];\n        const ceiling =\n          line.highBound === -1 ? size : reserveLow[line.highBound];\n        low = Math.max(low, floor + gap);\n        high = Math.min(high, ceiling - gap);\n      } else {\n        if (line.lowBound !== -1 && lines[line.lowBound].track === track) {\n          high = Math.min(high, reserveLow[line.id] - gap);\n        }\n        if (line.highBound !== -1 && lines[line.highBound].track === track) {\n          low = Math.max(low, reserveHigh[line.id] + gap);\n        }\n      }\n    }\n    return { low, high };\n  };\n  const moves: GridMove[] = [];\n  const settledAt = (track: number, position: number) =>\n    moves.find((move) => move.track === track)?.to ?? position;\n  const borders = (side: MondrianSplitSide, track: number) => {\n    const id = nodes[target].bounds[side];\n    return id !== -1 && lines[id].track === track;\n  };\n  const inward = (track: number) => {\n    if (borders(\"left\", track) || borders(\"top\", track)) return 1;\n    if (borders(\"right\", track) || borders(\"bottom\", track)) return -1;\n    return 0;\n  };\n  const heading = (track: number, down: number, up: number) => {\n    const squeeze = inward(track);\n    if (squeeze !== 0) {\n      return (squeeze > 0 ? up : down) >= minMove ? squeeze : 0;\n    }\n    if (down < minMove) return up >= minMove ? 1 : 0;\n    if (up < minMove) return -1;\n    return roll(`track-way-${track}`) < up / (up + down) ? 1 : -1;\n  };\n  const order = tracks\n    .map((track) => ({\n      ...track,\n      weight: track.length * (0.6 + 0.8 * roll(`track-${track.track}`)),\n    }))\n    .sort((a, b) => b.weight - a.weight);\n  for (const track of order) {\n    if (moves.length >= TRACK_COUNT) break;\n    const { low, high } = limits(track.track);\n    const down = (track.position - low) / (1 + RESERVE);\n    const up = (high - track.position) / (1 + RESERVE);\n    const direction = heading(track.track, down, up);\n    if (direction === 0) continue;\n    const span = track.axis === \"x\" ? w : h;\n    const room = Math.min(direction > 0 ? up : down, span * 0.2);\n    const stride = mix(0.6, 0.95, roll(`track-far-${track.track}`));\n    const reach = track.position + direction * Math.max(minMove, room * stride);\n    const nearest = tracks\n      .filter((rival) => rival.axis === track.axis)\n      .filter((rival) => rival.track !== track.track)\n      .map((rival) => settledAt(rival.track, rival.position))\n      .reduce(\n        (best, position) =>\n          Math.abs(position - reach) < Math.abs(best - reach) ? position : best,\n        Number.POSITIVE_INFINITY,\n      );\n    const to = Math.abs(nearest - reach) < align ? nearest : reach;\n    const margin = Math.abs(to - track.position) * RESERVE;\n    if (\n      Math.abs(to - track.position) < minMove ||\n      to - margin < low ||\n      to + margin > high\n    ) {\n      continue;\n    }\n    for (const line of lines) {\n      if (line.track === track.track) {\n        reserveLow[line.id] = Math.min(track.position, to) - margin;\n        reserveHigh[line.id] = Math.max(track.position, to) + margin;\n      }\n    }\n    moves.push({\n      track: track.track,\n      from: track.position,\n      to,\n      start: timeline.reflow + moves.length * TRACK_GAP,\n    });\n  }\n\n  const settled = lines.map((line) => settledAt(line.track, line.position));\n  const home = placeBoxes(lines, settled, w, h)[target];\n  const travel: Record<MondrianSplitSide, number> = {\n    left: home.x0 + overscan,\n    top: home.y0 + overscan,\n    right: w + overscan - home.x1,\n    bottom: h + overscan - home.y1,\n  };\n  const ranking = [...SIDES].sort((a, b) => travel[a] - travel[b]);\n  const side = (name: MondrianSplitSide) => {\n    const across = name === \"left\" || name === \"right\" ? \"x\" : \"y\";\n    const share = (extent(home, across) * DIP_SHARE) / travel[name];\n    return {\n      start: timeline.expand + ranking.indexOf(name) * SIDE_GAP,\n      dip: nodes[target].bounds[name] === -1 ? 0 : Math.min(DIP, share),\n    };\n  };\n\n  return {\n    width: w,\n    height: h,\n    weight,\n    minSide,\n    gap,\n    minMove,\n    align,\n    overscan,\n    timeline,\n    nodes,\n    lines,\n    leaves,\n    target,\n    fills,\n    field,\n    moves,\n    sides: {\n      left: side(\"left\"),\n      top: side(\"top\"),\n      right: side(\"right\"),\n      bottom: side(\"bottom\"),\n    },\n  };\n}\n\nexport type MondrianSplitLayout = ReturnType<typeof getMondrianSplitLayout>;\n\nexport function getMondrianSplitState(\n  frame: number,\n  layout: MondrianSplitLayout = getMondrianSplitLayout(),\n) {\n  const time = Math.max(0, finite(frame, 0));\n  const { width, height, weight, overscan } = layout;\n  const positions = layout.lines.map((line) => {\n    const move = layout.moves.find((item) => item.track === line.track);\n    if (!move) return line.position;\n    const amount = slide(progress(time, move.start, TRACK_SLIDE));\n    return line.position + (move.to - move.from) * amount;\n  });\n  const boxes = placeBoxes(layout.lines, positions, width, height);\n  const home = boxes[layout.target];\n  const reach = (name: MondrianSplitSide) => {\n    const { start, dip } = layout.sides[name];\n    return push(progress(time, start, SIDE_PUSH), dip);\n  };\n  const mapX = stretch(\n    home.x0,\n    home.x1,\n    mix(home.x0, -overscan, reach(\"left\")),\n    mix(home.x1, width + overscan, reach(\"right\")),\n    -overscan,\n    width + overscan,\n  );\n  const mapY = stretch(\n    home.y0,\n    home.y1,\n    mix(home.y0, -overscan, reach(\"top\")),\n    mix(home.y1, height + overscan, reach(\"bottom\")),\n    -overscan,\n    height + overscan,\n  );\n  const project = (box: MondrianSplitBox): MondrianSplitBox => ({\n    x0: mapX(box.x0),\n    y0: mapY(box.y0),\n    x1: mapX(box.x1),\n    y1: mapY(box.y1),\n  });\n\n  const lines = layout.lines.flatMap((line) => {\n    const drawn = drawEase(progress(time, line.start, LINE_DRAW));\n    if (drawn <= 0) return [];\n    const box = boxes[line.node];\n    const vertical = line.axis === \"x\";\n    const low = vertical ? box.y0 : box.x0;\n    const high = vertical ? box.y1 : box.x1;\n    const along = vertical ? mapY : mapX;\n    const start = along(line.fromEnd ? high - (high - low) * drawn : low);\n    const end = along(line.fromEnd ? high : low + (high - low) * drawn);\n    const position = (vertical ? mapX : mapY)(positions[line.id]);\n    const half = weight / 2;\n    const length = end - start;\n    const rect: MondrianSplitRect = vertical\n      ? { x: position - half, y: start, width: weight, height: length }\n      : { x: start, y: position - half, width: length, height: weight };\n    return inFrame(rect, width, height)\n      ? [{ id: line.id, axis: line.axis, position, drawn, ...rect }]\n      : [];\n  });\n\n  const fills = layout.fills.flatMap((fill) => {\n    const amount = wipeEase(progress(time, fill.start, FILL_WIPE));\n    if (amount <= 0) return [];\n    const rect = wipe(project(boxes[fill.node]), fill, amount);\n    return inFrame(rect, width, height)\n      ? [{ id: fill.node, slot: fill.slot, ...rect }]\n      : [];\n  });\n\n  const reveal = wipeEase(progress(time, layout.field.start, FIELD_WIPE));\n  const field = reveal > 0 ? wipe(project(home), layout.field, reveal) : null;\n\n  return {\n    time,\n    lines,\n    fills,\n    field: field && inFrame(field, width, height) ? field : null,\n    cells: layout.leaves.map((id) => ({ id, ...project(boxes[id]) })),\n  };\n}\n\nexport type MondrianSplitState = ReturnType<typeof getMondrianSplitState>;\n\nexport function getMondrianSplitClip(\n  field: MondrianSplitRect | null,\n  width: number,\n  height: number,\n) {\n  if (!field) return \"inset(50%)\";\n  const top = percent(field.y, height);\n  const right = percent(width - field.x - field.width, width);\n  const bottom = percent(height - field.y - field.height, height);\n  const left = percent(field.x, width);\n  return `inset(${top} ${right} ${bottom} ${left})`;\n}\n\nexport function MondrianSplit({\n  children,\n  seed = mondrianSplitDefaults.seed,\n  splits = mondrianSplitDefaults.splits,\n  lineWeight = mondrianSplitDefaults.lineWeight,\n  expandCell = mondrianSplitDefaults.expandCell,\n  lineColor = mondrianSplitDefaults.lineColor,\n  redColor = mondrianSplitDefaults.redColor,\n  yellowColor = mondrianSplitDefaults.yellowColor,\n  blueColor = mondrianSplitDefaults.blueColor,\n  fieldColor = mondrianSplitDefaults.fieldColor,\n  speed = 1,\n  className,\n}: MondrianSplitProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const layout = useMemo(\n    () =>\n      getMondrianSplitLayout({\n        seed,\n        splits,\n        lineWeight,\n        expandCell,\n        width,\n        height,\n      }),\n    [seed, splits, lineWeight, expandCell, width, height],\n  );\n  const time = frame * (30 / fps) * Math.max(0, finite(speed, 1));\n  const state = getMondrianSplitState(time, layout);\n  const palette: Record<MondrianSplitSlot, string> = {\n    red: redColor,\n    yellow: yellowColor,\n    blue: blueColor,\n  };\n  const viewBox = `0 0 ${width} ${height}`;\n\n  return (\n    <div\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\" }}\n    >\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox={viewBox}\n        preserveAspectRatio=\"none\"\n        aria-hidden=\"true\"\n        style={layer}\n      >\n        {state.fills.map((fill) => (\n          <rect\n            key={fill.id}\n            x={fill.x}\n            y={fill.y}\n            width={fill.width}\n            height={fill.height}\n            fill={palette[fill.slot]}\n          />\n        ))}\n        {state.field ? (\n          <rect\n            x={state.field.x}\n            y={state.field.y}\n            width={state.field.width}\n            height={state.field.height}\n            fill={fieldColor}\n          />\n        ) : null}\n      </svg>\n      {children == null ? null : (\n        <div\n          style={{\n            ...layer,\n            clipPath: getMondrianSplitClip(state.field, width, height),\n          }}\n        >\n          {children}\n        </div>\n      )}\n      <svg\n        width=\"100%\"\n        height=\"100%\"\n        viewBox={viewBox}\n        preserveAspectRatio=\"none\"\n        role=\"img\"\n        aria-label=\"De Stijl grid\"\n        style={layer}\n      >\n        {state.lines.map((line) => (\n          <rect\n            key={line.id}\n            x={line.x}\n            y={line.y}\n            width={line.width}\n            height={line.height}\n            fill={lineColor}\n          />\n        ))}\n      </svg>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/mondrian-split.tsx"
    }
  ],
  "type": "registry:component"
}
