{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"datamosh-transition","type":"registry:ui","title":"DatamoshTransition","description":"Page-transition overlay that looks like a corrupted video decode: a fixed grid of columns, narrow on the left and wide on the right, fills with flat blocks of saturated colour that fall, snap open through the middle of the frame and squeeze shut again; cover() drops the columns over the page right to left and reveal() lets them fall away, both as promises for any router's navigation hook","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["effects"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/datamosh-transition/DatamoshTransition.tsx","content":"\"use client\";\n\nimport {\n\tforwardRef,\n\tuseCallback,\n\tuseEffect,\n\tuseImperativeHandle,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport type { HTMLAttributes } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { useReducedMotion } from \"../../internals/motion/media-query\";\nimport {\n\tBLEED,\n\tCYCLE,\n\tDEFAULT_COLUMNS,\n\tDEFAULT_POWER,\n\tDEFAULT_TILES,\n\tFALL_STAGGER,\n\tbuildStrip,\n\tcolumnBand,\n\tcolumnDelays,\n\tcolumnEdges,\n\tcolumnFlow,\n\tcolumnTiles,\n\tcoverFit,\n\tsampleSource,\n\tsourceSlot,\n\tcreateCoverage,\n\teaseEdge,\n\toddTiles,\n\tpaletteRoles,\n\tresolvePalette,\n\tsettleCoverage,\n\tstepCoverage,\n\tstripIndex,\n\ttype Coverage,\n\ttype DatamoshEffect,\n\ttype DatamoshPaletteName,\n\ttype DatamoshSweep,\n\ttype Direction,\n} from \"./datamosh-transition-core\";\n\nexport type DatamoshTransitionPhase = \"idle\" | \"covering\" | \"covered\" | \"revealing\";\n\n/**\n * What the forwarded ref carries. The Svelte source exposes `cover()`,\n * `reveal()` and `play()` on the component instance and binds the overlay\n * element through `ref`; React has no instance, so both travel through one\n * handle: the three methods plus `element`.\n */\nexport interface DatamoshTransitionHandle {\n\t/** Cover the page. Resolves once every column is down. */\n\tcover: () => Promise<void>;\n\t/** Clear the page. Resolves once every column has fallen away. */\n\treveal: () => Promise<void>;\n\t/** Cover, then reveal — a one-shot flash. */\n\tplay: () => Promise<void>;\n\t/** The overlay element. */\n\treadonly element: HTMLDivElement | null;\n}\n\n/**\n * DatamoshTransition — a page-transition overlay that looks like a corrupted\n * video decode.\n *\n * A fixed grid of columns (narrow on the left, wide on the right) fills with\n * flat blocks of saturated colour. Inside each column a stack of tiles falls\n * along one sigmoid: slivers at the edges, one tile snapping open through\n * the middle. `cover()` drops the columns over the page like a curtain,\n * right to left; `reveal()` lets them fall away. Nothing moves sideways.\n *\n * Mount once (root layout) and drive it from your router's navigation hook.\n */\nexport interface DatamoshTransitionProps extends Omit<HTMLAttributes<HTMLDivElement>, \"color\"> {\n\t/** Additional classes on the overlay */\n\tclassName?: string;\n\t/**\n\t * Palette name or colour list (hex or rgb()). The lightest and darkest\n\t * colours recur most often.\n\t */\n\tcolors?: string[] | DatamoshPaletteName;\n\t/** Shape of the covering band: curtain, rise, split or interlace */\n\tvariant?: DatamoshEffect;\n\t/** Order in which the columns move */\n\tsweep?: DatamoshSweep;\n\t/**\n\t * Picture to decode: tiles take their colours from it instead of the\n\t * palette. A URL (same-origin or CORS-enabled), an image or a canvas.\n\t */\n\tsource?: string | HTMLImageElement | HTMLCanvasElement;\n\t/** Seed for the colour order — same seed, same strip */\n\tseed?: number;\n\t/** Number of columns */\n\tcolumns?: number;\n\t/** Column edge exponent: 1 = uniform grid, higher = wider columns to the right */\n\tpower?: number;\n\t/** Tiles per column stack (forced odd) */\n\ttiles?: number;\n\t/** Time for one column to cover, ms */\n\tcoverDuration?: number;\n\t/** Time for one column to clear, ms */\n\trevealDuration?: number;\n\t/** Delay between neighbouring columns, ms (rightmost first) */\n\tstagger?: number;\n\t/** Speed of the falling tiles, 1 = default */\n\tspeed?: number;\n\t/** Fill the positioned parent instead of the viewport */\n\tcontained?: boolean;\n\t/** Stacking order of the overlay */\n\tzIndex?: number;\n\t/** Called with every phase change (the Svelte `bind:phase`) */\n\tonPhaseChange?: (phase: DatamoshTransitionPhase) => void;\n\t/** Called once the page is fully covered */\n\toncovered?: () => void;\n\t/** Called once the page is fully revealed */\n\tonrevealed?: () => void;\n}\n\nfunction currentDpr() {\n\treturn Math.min(typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1, 2);\n}\n\nexport const DatamoshTransition = forwardRef<DatamoshTransitionHandle, DatamoshTransitionProps>(\n\tfunction DatamoshTransition(\n\t\t{\n\t\t\tclassName,\n\t\t\tcolors = \"broadcast\",\n\t\t\tvariant = \"curtain\",\n\t\t\tsweep = \"right\",\n\t\t\tsource,\n\t\t\tseed = 1,\n\t\t\tcolumns = DEFAULT_COLUMNS,\n\t\t\tpower = DEFAULT_POWER,\n\t\t\ttiles = DEFAULT_TILES,\n\t\t\tcoverDuration = 380,\n\t\t\trevealDuration = 480,\n\t\t\tstagger = 28,\n\t\t\tspeed = 1,\n\t\t\tcontained = false,\n\t\t\tzIndex = 9999,\n\t\t\tonPhaseChange,\n\t\t\toncovered,\n\t\t\tonrevealed,\n\t\t\tstyle,\n\t\t\t...restProps\n\t\t},\n\t\tref\n\t) {\n\t\tconst reduced = useReducedMotion();\n\n\t\tconst palette = useMemo(() => resolvePalette(colors), [colors]);\n\t\tconst columnsC = Math.max(1, Math.round(Number.isFinite(columns) ? columns : DEFAULT_COLUMNS));\n\t\tconst powerC = Number.isFinite(power) && power > 0 ? power : DEFAULT_POWER;\n\t\tconst tilesC = oddTiles(tiles);\n\t\tconst speedC = Number.isFinite(speed) && speed > 0 ? speed : 1;\n\t\tconst strip = useMemo(() => buildStrip(seed, palette), [seed, palette]);\n\t\tconst delays = useMemo(\n\t\t\t() =>\n\t\t\t\tcolumnDelays(\n\t\t\t\t\tcolumnsC,\n\t\t\t\t\tsweep,\n\t\t\t\t\tMath.max(0, Number.isFinite(stagger) ? stagger : 0) / 1000,\n\t\t\t\t\tseed\n\t\t\t\t),\n\t\t\t[columnsC, sweep, stagger, seed]\n\t\t);\n\t\t/** Darkest colour as a backing fill, so rounding seams never show the page through. */\n\t\tconst backing = palette[paletteRoles(palette).darks[0] ?? 0] ?? \"#14101f\";\n\n\t\tconst [phase, setPhaseState] = useState<DatamoshTransitionPhase>(\"idle\");\n\n\t\tconst hostRef = useRef<HTMLDivElement | null>(null);\n\t\tconst canvasRef = useRef<HTMLCanvasElement | null>(null);\n\n\t\t// Everything the loop reads, always current, without re-creating the engine.\n\t\tconst live = useLiveRef({\n\t\t\treduced,\n\t\t\tpalette,\n\t\t\tcolumnsC,\n\t\t\tpowerC,\n\t\t\ttilesC,\n\t\t\tspeedC,\n\t\t\tstrip,\n\t\t\tdelays,\n\t\t\tbacking,\n\t\t\tvariant,\n\t\t\tcoverDuration,\n\t\t\trevealDuration,\n\t\t\tonPhaseChange,\n\t\t\toncovered,\n\t\t\tonrevealed,\n\t\t});\n\n\t\t// --- plain closure state (touched every frame, never rendered) ---------------\n\t\tconst s = useRef({\n\t\t\tctx: null as CanvasRenderingContext2D | null,\n\t\t\tw: 0,\n\t\t\th: 0,\n\t\t\tedges: [] as number[],\n\t\t\tcov: createCoverage(DEFAULT_COLUMNS) as Coverage,\n\t\t\tdirection: \"reveal\" as Direction,\n\t\t\tphase: \"idle\" as DatamoshTransitionPhase,\n\t\t\tphaseTime: 0,\n\t\t\telapsed: 0,\n\t\t\trafId: null as number | null,\n\t\t\tlastT: 0,\n\t\t\tpending: [] as Array<() => void>,\n\t\t\tsourceImage: null as CanvasImageSource | null,\n\t\t\tsourceSize: { w: 0, h: 0 },\n\t\t\tsourceTable: null as string[][] | null,\n\t\t}).current;\n\n\t\t// The engine: stable functions over `s` and `live`, built once.\n\t\tconst engine = useMemo(() => {\n\t\t\tfunction setPhase(next: DatamoshTransitionPhase) {\n\t\t\t\tif (s.phase === next) return;\n\t\t\t\ts.phase = next;\n\t\t\t\tsetPhaseState(next);\n\t\t\t\tlive.current.onPhaseChange?.(next);\n\t\t\t}\n\n\t\t\tfunction settlePending() {\n\t\t\t\tconst list = s.pending;\n\t\t\t\ts.pending = [];\n\t\t\t\tfor (const resolve of list) resolve();\n\t\t\t}\n\n\t\t\tfunction clear() {\n\t\t\t\ts.ctx?.clearRect(0, 0, s.w, s.h);\n\t\t\t}\n\n\t\t\tfunction draw() {\n\t\t\t\tconst c = s.ctx;\n\t\t\t\tconst p = live.current;\n\t\t\t\tif (!c || !s.edges.length) return;\n\t\t\t\tconst { w, h } = s;\n\t\t\t\tc.clearRect(0, 0, w, h);\n\t\t\t\tconst bleed = Math.round(BLEED * h);\n\t\t\t\tconst len = p.palette.length;\n\t\t\t\t// Reduced motion paints one still frame part-way into a step.\n\t\t\t\tconst t = p.reduced ? CYCLE * 0.45 + p.columnsC * FALL_STAGGER : s.elapsed;\n\n\t\t\t\tfor (let i = 0; i < p.columnsC; i++) {\n\t\t\t\t\tconst x0 = s.edges[i]!;\n\t\t\t\t\tconst cw = s.edges[i + 1]! - x0;\n\t\t\t\t\tif (cw <= 0) continue;\n\t\t\t\t\tconst band = columnBand(\n\t\t\t\t\t\tp.variant,\n\t\t\t\t\t\ti,\n\t\t\t\t\t\teaseEdge(s.cov.top[i] ?? 0),\n\t\t\t\t\t\teaseEdge(s.cov.bot[i] ?? 0)\n\t\t\t\t\t);\n\t\t\t\t\tconst bandTop = Math.round(band.y0 * h);\n\t\t\t\t\tconst bandBot = Math.round(band.y1 * h);\n\t\t\t\t\tif (bandBot <= bandTop) continue;\n\n\t\t\t\t\tc.fillStyle = p.backing;\n\t\t\t\t\tc.fillRect(x0, bandTop, cw, bandBot - bandTop);\n\n\t\t\t\t\tconst flow = columnFlow(t, i, p.columnsC, p.speedC);\n\t\t\t\t\tfor (const tile of columnTiles(flow, p.tilesC, h, bleed)) {\n\t\t\t\t\t\t// Mirrored stacks rise instead of fall; the overhang still laps the next tile.\n\t\t\t\t\t\tconst top = band.mirror ? h - tile.bot : tile.top;\n\t\t\t\t\t\tconst bot = band.mirror ? h - tile.top : tile.bot;\n\t\t\t\t\t\tconst y0 = Math.max(bandTop, top);\n\t\t\t\t\t\tconst y1 = Math.min(bandBot, bot);\n\t\t\t\t\t\tif (y1 <= y0) continue;\n\t\t\t\t\t\tc.fillStyle =\n\t\t\t\t\t\t\ts.sourceTable?.[i]?.[sourceSlot(tile.id, i, p.tilesC)] ??\n\t\t\t\t\t\t\tp.palette[stripIndex(p.strip, tile.id, i) % len]!;\n\t\t\t\t\t\tc.fillRect(x0, y0, cw, y1 - y0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction stopLoop() {\n\t\t\t\tif (s.rafId !== null && typeof cancelAnimationFrame === \"function\")\n\t\t\t\t\tcancelAnimationFrame(s.rafId);\n\t\t\t\ts.rafId = null;\n\t\t\t}\n\n\t\t\tfunction requestFrame() {\n\t\t\t\tif (s.rafId !== null || !s.ctx || typeof requestAnimationFrame !== \"function\") return;\n\t\t\t\ts.lastT = 0;\n\t\t\t\ts.rafId = requestAnimationFrame(tick);\n\t\t\t}\n\n\t\t\tfunction finish(dir: Direction) {\n\t\t\t\tsettleCoverage(s.cov, dir);\n\t\t\t\tsetPhase(dir === \"cover\" ? \"covered\" : \"idle\");\n\t\t\t\tif (dir === \"reveal\") {\n\t\t\t\t\tstopLoop();\n\t\t\t\t\tclear();\n\t\t\t\t} else if (!live.current.reduced) {\n\t\t\t\t\trequestFrame();\n\t\t\t\t} else {\n\t\t\t\t\tdraw();\n\t\t\t\t}\n\t\t\t\tsettlePending();\n\t\t\t\tif (dir === \"cover\") live.current.oncovered?.();\n\t\t\t\telse live.current.onrevealed?.();\n\t\t\t}\n\n\t\t\tfunction tick(now: number) {\n\t\t\t\ts.rafId = null;\n\t\t\t\tif (!s.ctx) return;\n\t\t\t\t// Time-based: identical pacing at 60 Hz and 120 Hz; long stalls clamp.\n\t\t\t\tconst dt = s.lastT ? Math.min(0.05, Math.max(0, (now - s.lastT) / 1000)) : 0;\n\t\t\t\ts.lastT = now;\n\t\t\t\ts.elapsed += dt;\n\n\t\t\t\tif (s.phase === \"covering\" || s.phase === \"revealing\") {\n\t\t\t\t\ts.phaseTime += dt;\n\t\t\t\t\tconst p = live.current;\n\t\t\t\t\tconst duration = (s.direction === \"cover\" ? p.coverDuration : p.revealDuration) / 1000;\n\t\t\t\t\tconst done = stepCoverage(s.cov, s.direction, s.phaseTime, dt, duration, p.delays);\n\t\t\t\t\tif (done) {\n\t\t\t\t\t\tfinish(s.direction);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tdraw();\n\t\t\t\tif (s.phase !== \"idle\") s.rafId = requestAnimationFrame(tick);\n\t\t\t}\n\n\t\t\tfunction run(dir: Direction): Promise<void> {\n\t\t\t\t// A superseded call resolves now: its caller moves on, the new phase takes over.\n\t\t\t\tsettlePending();\n\t\t\t\tconst promise = new Promise<void>((resolve) => s.pending.push(resolve));\n\n\t\t\t\t// Already there: nothing to animate, no callback.\n\t\t\t\tif (\n\t\t\t\t\t(dir === \"reveal\" && s.phase === \"idle\") ||\n\t\t\t\t\t(dir === \"cover\" && s.phase === \"covered\")\n\t\t\t\t) {\n\t\t\t\t\tsettlePending();\n\t\t\t\t\treturn promise;\n\t\t\t\t}\n\n\t\t\t\ts.direction = dir;\n\t\t\t\ts.phaseTime = 0;\n\t\t\t\tsetPhase(dir === \"cover\" ? \"covering\" : \"revealing\");\n\n\t\t\t\tconst instant =\n\t\t\t\t\t!s.ctx ||\n\t\t\t\t\tlive.current.reduced ||\n\t\t\t\t\ttypeof requestAnimationFrame !== \"function\" ||\n\t\t\t\t\t(typeof document !== \"undefined\" && document.hidden);\n\t\t\t\tif (instant) finish(dir);\n\t\t\t\telse requestFrame();\n\t\t\t\treturn promise;\n\t\t\t}\n\n\t\t\t/** Sample the loaded picture onto the current grid; any failure falls back to the palette. */\n\t\t\tfunction buildSourceTable() {\n\t\t\t\ts.sourceTable = null;\n\t\t\t\tif (!s.sourceImage || !s.edges.length || typeof document === \"undefined\") return;\n\t\t\t\tconst pw = 96;\n\t\t\t\tconst ph = Math.max(8, Math.round((pw * s.h) / Math.max(1, s.w)));\n\t\t\t\ttry {\n\t\t\t\t\tconst scratch = document.createElement(\"canvas\");\n\t\t\t\t\tscratch.width = pw;\n\t\t\t\t\tscratch.height = ph;\n\t\t\t\t\tconst sctx = scratch.getContext(\"2d\", { willReadFrequently: true });\n\t\t\t\t\tif (!sctx) return;\n\t\t\t\t\tconst fit = coverFit(s.sourceSize.w, s.sourceSize.h, pw, ph);\n\t\t\t\t\tsctx.drawImage(s.sourceImage, fit.sx, fit.sy, fit.sw, fit.sh, 0, 0, pw, ph);\n\t\t\t\t\tconst { data } = sctx.getImageData(0, 0, pw, ph);\n\t\t\t\t\tconst norm = s.edges.map((e) => e / Math.max(1, s.w));\n\t\t\t\t\ts.sourceTable = sampleSource(data, pw, ph, norm, live.current.tilesC);\n\t\t\t\t} catch {\n\t\t\t\t\t// Cross-origin picture without CORS taints the canvas: keep the palette.\n\t\t\t\t\ts.sourceTable = null;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfunction measure() {\n\t\t\t\tconst host = hostRef.current;\n\t\t\t\tconst canvas = canvasRef.current;\n\t\t\t\tif (!host || !canvas || !s.ctx) return;\n\t\t\t\tconst p = live.current;\n\t\t\t\tconst dpr = currentDpr();\n\t\t\t\ts.w = Math.max(1, Math.round(host.clientWidth * dpr));\n\t\t\t\ts.h = Math.max(1, Math.round(host.clientHeight * dpr));\n\t\t\t\tcanvas.width = s.w;\n\t\t\t\tcanvas.height = s.h;\n\t\t\t\ts.ctx.imageSmoothingEnabled = false;\n\t\t\t\ts.edges = columnEdges(s.w, p.columnsC, p.powerC);\n\t\t\t\tbuildSourceTable();\n\t\t\t\tif (s.cov.top.length !== p.columnsC) {\n\t\t\t\t\tconst covered = s.phase === \"covered\" || s.phase === \"covering\";\n\t\t\t\t\ts.cov = createCoverage(p.columnsC, covered);\n\t\t\t\t}\n\t\t\t\tif (s.phase !== \"idle\") draw();\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\trun,\n\t\t\t\tdraw,\n\t\t\t\tfinish,\n\t\t\t\tmeasure,\n\t\t\t\tstopLoop,\n\t\t\t\trequestFrame,\n\t\t\t\tsettlePending,\n\t\t\t\tbuildSourceTable,\n\t\t\t};\n\t\t}, [live, s]);\n\n\t\tconst cover = useCallback(() => engine.run(\"cover\"), [engine]);\n\t\tconst reveal = useCallback(() => engine.run(\"reveal\"), [engine]);\n\t\tconst play = useCallback(async () => {\n\t\t\tawait engine.run(\"cover\");\n\t\t\tawait engine.run(\"reveal\");\n\t\t}, [engine]);\n\n\t\tuseImperativeHandle(\n\t\t\tref,\n\t\t\t() => ({\n\t\t\t\tcover,\n\t\t\t\treveal,\n\t\t\t\tplay,\n\t\t\t\tget element() {\n\t\t\t\t\treturn hostRef.current;\n\t\t\t\t},\n\t\t\t}),\n\t\t\t[cover, reveal, play]\n\t\t);\n\n\t\t// --- mount: context, observers, hidden tab ---------------------------------\n\t\tuseEffect(() => {\n\t\t\tconst host = hostRef.current;\n\t\t\tconst canvas = canvasRef.current;\n\t\t\tif (!host || !canvas) return;\n\t\t\ts.ctx = canvas.getContext(\"2d\");\n\t\t\tif (!s.ctx) return;\n\t\t\tengine.measure();\n\n\t\t\tlet ro: ResizeObserver | undefined;\n\t\t\tif (typeof ResizeObserver !== \"undefined\") {\n\t\t\t\tro = new ResizeObserver(() => engine.measure());\n\t\t\t\tro.observe(host);\n\t\t\t}\n\n\t\t\t// A hidden tab stops rAF: finish the phase so an awaiting navigation never hangs.\n\t\t\tconst onVisibility = () => {\n\t\t\t\tif (!document.hidden) {\n\t\t\t\t\tif (s.phase !== \"idle\") engine.requestFrame();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tengine.stopLoop();\n\t\t\t\tif (s.phase === \"covering\") engine.finish(\"cover\");\n\t\t\t\telse if (s.phase === \"revealing\") engine.finish(\"reveal\");\n\t\t\t};\n\t\t\tdocument.addEventListener(\"visibilitychange\", onVisibility);\n\n\t\t\treturn () => {\n\t\t\t\tengine.stopLoop();\n\t\t\t\tro?.disconnect();\n\t\t\t\tdocument.removeEventListener(\"visibilitychange\", onVisibility);\n\t\t\t\tengine.settlePending();\n\t\t\t\ts.ctx = null;\n\t\t\t};\n\t\t}, [engine, s]);\n\n\t\t// Grid-shaping props: re-measure (column count may change the coverage arrays).\n\t\tconst gridInit = useRef(false);\n\t\tuseEffect(() => {\n\t\t\tif (!gridInit.current) {\n\t\t\t\tgridInit.current = true;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tengine.measure();\n\t\t}, [columnsC, powerC, engine]);\n\n\t\t// Reduced motion flips mid-phase: settle instantly rather than animate.\n\t\tuseEffect(() => {\n\t\t\tif (!reduced) return;\n\t\t\tif (s.phase === \"covering\") engine.finish(\"cover\");\n\t\t\telse if (s.phase === \"revealing\") engine.finish(\"reveal\");\n\t\t\telse if (s.phase === \"covered\") {\n\t\t\t\tengine.stopLoop();\n\t\t\t\tengine.draw();\n\t\t\t}\n\t\t}, [reduced, engine, s]);\n\n\t\t// Load the picture source; a stale load never overwrites a newer one.\n\t\tuseEffect(() => {\n\t\t\ts.sourceImage = null;\n\t\t\ts.sourceTable = null;\n\t\t\tconst src = source;\n\t\t\tif (!src || typeof window === \"undefined\") return;\n\n\t\t\tlet cancelled = false;\n\t\t\tconst ready = (img: CanvasImageSource, iw: number, ih: number) => {\n\t\t\t\tif (cancelled || iw <= 0 || ih <= 0) return;\n\t\t\t\ts.sourceImage = img;\n\t\t\t\ts.sourceSize = { w: iw, h: ih };\n\t\t\t\tengine.buildSourceTable();\n\t\t\t\tif (s.phase !== \"idle\") engine.draw();\n\t\t\t};\n\n\t\t\tif (typeof src !== \"string\") {\n\t\t\t\tif (src instanceof HTMLCanvasElement) ready(src, src.width, src.height);\n\t\t\t\telse if (src.complete && src.naturalWidth) ready(src, src.naturalWidth, src.naturalHeight);\n\t\t\t\telse\n\t\t\t\t\tsrc.addEventListener(\"load\", () => ready(src, src.naturalWidth, src.naturalHeight), {\n\t\t\t\t\t\tonce: true,\n\t\t\t\t\t});\n\t\t\t\treturn () => {\n\t\t\t\t\tcancelled = true;\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst img = new Image();\n\t\t\timg.decoding = \"async\";\n\t\t\tif (!src.startsWith(\"data:\")) img.crossOrigin = \"anonymous\";\n\t\t\timg.onload = () => ready(img, img.naturalWidth || 300, img.naturalHeight || 150);\n\t\t\timg.src = src;\n\t\t\treturn () => {\n\t\t\t\tcancelled = true;\n\t\t\t\timg.onload = null;\n\t\t\t};\n\t\t}, [source, tilesC, engine, s]);\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tref={hostRef}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"datamosh-transition inset-0\",\n\t\t\t\t\tcontained ? \"absolute\" : \"fixed\",\n\t\t\t\t\tphase === \"idle\" ? \"pointer-events-none invisible\" : \"pointer-events-auto\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\tstyle={{ zIndex, ...style }}\n\t\t\t\tdata-state={phase}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t\t{...restProps}\n\t\t\t>\n\t\t\t\t<canvas ref={canvasRef} className=\"block h-full w-full\" />\n\t\t\t</div>\n\t\t);\n\t}\n);\n","type":"registry:ui","target":"@ui/fancy/components/datamosh-transition/DatamoshTransition.tsx"},{"path":"react/src/components/datamosh-transition/datamosh-transition-core.ts","content":"\"use client\";\n\n/**\n * DatamoshTransition core — pure math, no DOM.\n *\n * The look: flat blocks of saturated colour in a FIXED column grid, each column\n * a stack of tiles falling along one sigmoid curve — slivers at the top and\n * bottom, one fat tile snapping open through the middle. Nothing ever moves\n * horizontally; only the vertical tiling animates.\n *\n * On top of that sits the transition itself: every column owns a covered band\n * [top, bot] (0..1 of the height). Cover drops the band's bottom edge like a\n * curtain, reveal lets its top edge fall away, columns staggered right to left.\n */\n\n// --- defaults ------------------------------------------------------------------\n\nexport const DEFAULT_COLORS = [\n\t\"#ffffff\",\n\t\"#3566ff\",\n\t\"#ffcf00\",\n\t\"#192aff\",\n\t\"#d62036\",\n\t\"#282142\",\n\t\"#ec4978\",\n\t\"#ff9900\",\n\t\"#14101f\",\n\t\"#00dd33\",\n\t\"#00ef82\",\n] as const;\n\n/** Column edges sit at W·(i/N)^power: narrow left, wide right — reads as receding perspective. */\nexport const DEFAULT_COLUMNS = 11;\nexport const DEFAULT_POWER = 1.65;\n/** Odd so exactly one tile sits dead centre and takes the whole bulge. */\nexport const DEFAULT_TILES = 15;\n/** Seconds per tile step at speed 1. */\nexport const CYCLE = 0.2;\n/** Seconds between neighbouring columns starting to fall (right column first). */\nexport const FALL_STAGGER = 0.006;\n/** Sigmoid exponent — steepness at the centre is tile height. */\nexport const STRETCH = 8.5;\n/** Exponential arrival rate inside a tile step (the \"spring\"). */\nexport const SPRING_K = 1.6;\n/** Each tile overhangs the one below by this fraction of the height. */\nexport const BLEED = 0.012;\n/** Phase offset per column, in tiles — the zig-zag. Negative: stacks ride higher to the right. */\nexport const COL_PHASE = -0.4;\n/** Slope of the curve past both ends; clamping would pile off-screen tiles onto y = 0. */\nexport const TAIL_SLOPE = 0.05;\n/** Length of the cyclic colour strip every column reads. */\nexport const STRIP_LENGTH = 61;\n\n// --- randomness ----------------------------------------------------------------\n\n/** Small, fast, seedable PRNG (32-bit state). Same seed → same sequence. */\nexport function mulberry32(seed: number): () => number {\n\tlet a = seed >>> 0;\n\treturn () => {\n\t\ta = (a + 0x6d2b79f5) >>> 0;\n\t\tlet t = a;\n\t\tt = Math.imul(t ^ (t >>> 15), t | 1);\n\t\tt ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n\t\treturn ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n\t};\n}\n\n// --- palette -------------------------------------------------------------------\n\n/** Relative luminance of a hex (#rgb / #rrggbb) or rgb() colour; null when unparseable. */\nexport function luminance(color: string): number | null {\n\tconst s = color.trim().toLowerCase();\n\tlet rgb: number[] | null = null;\n\tconst hex = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);\n\tif (hex) {\n\t\tconst h = hex[1]!;\n\t\tconst full =\n\t\t\th.length === 3\n\t\t\t\t? h\n\t\t\t\t\t\t.split(\"\")\n\t\t\t\t\t\t.map((c) => c + c)\n\t\t\t\t\t\t.join(\"\")\n\t\t\t\t: h;\n\t\trgb = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));\n\t} else {\n\t\tconst m = s.match(/^rgba?\\(\\s*(\\d+)[\\s,]+(\\d+)[\\s,]+(\\d+)/);\n\t\tif (m) rgb = [Number(m[1]), Number(m[2]), Number(m[3])];\n\t}\n\tif (!rgb) return null;\n\tconst [r, g, b] = rgb.map((v) => {\n\t\tconst c = Math.min(255, v) / 255;\n\t\treturn c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;\n\t}) as [number, number, number];\n\treturn 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\nexport interface PaletteRoles {\n\t/** The lightest colour — recurs most often (~a quarter of the strip). */\n\tlight: number;\n\t/** The darkest one or two colours — the other contrast anchor. */\n\tdarks: number[];\n\t/** Everything else. */\n\thues: number[];\n}\n\n/** Split a palette into contrast anchors and mid-tone hues by luminance. */\nexport function paletteRoles(colors: readonly string[]): PaletteRoles {\n\tconst ranked = colors\n\t\t.map((c, i) => ({ i, l: luminance(c) ?? 0.5 }))\n\t\t.sort((a, b) => a.l - b.l || a.i - b.i);\n\tconst light = ranked[ranked.length - 1]!.i;\n\tconst darkCount = colors.length >= 5 ? 2 : 1;\n\tconst darks = ranked.slice(0, darkCount).map((r) => r.i);\n\tconst hues = ranked.slice(darkCount, -1).map((r) => r.i);\n\treturn { light, darks, hues };\n}\n\nfunction pickDifferent(from: number, avoid: number[], len: number): number {\n\tfor (let i = 1; i <= len; i++) {\n\t\tconst c = (from + i) % len;\n\t\tif (!avoid.includes(c)) return c;\n\t}\n\treturn from;\n}\n\n/**\n * The cyclic colour strip, as palette indices. Light and dark anchors recur\n * often on purpose: a fixed palette stride walks past them and leaves flat\n * mid-tone corduroy. No two neighbours repeat, including across the wrap.\n */\nexport function buildStrip(seed: number, colors: readonly string[], len = STRIP_LENGTH): number[] {\n\tconst n = colors.length;\n\tif (n < 2) return Array.from({ length: len }, () => 0);\n\tconst { light, darks, hues } = paletteRoles(colors);\n\tconst pool = hues.length ? hues : darks;\n\tconst rand = mulberry32(seed);\n\tconst strip: number[] = [];\n\tlet lastHue = -1;\n\n\tfor (let k = 0; k < len; k++) {\n\t\tconst r = rand();\n\t\tlet next: number;\n\t\tif (r < 0.26) {\n\t\t\tnext = light;\n\t\t} else if (r < 0.42) {\n\t\t\tnext = darks[Math.floor(rand() * darks.length)]!;\n\t\t} else {\n\t\t\tlet h = pool[Math.floor(rand() * pool.length)]!;\n\t\t\tif (h === lastHue && pool.length > 1) h = pool[(pool.indexOf(h) + 1) % pool.length]!;\n\t\t\tlastHue = h;\n\t\t\tnext = h;\n\t\t}\n\t\tif (k > 0 && next === strip[k - 1]) next = pickDifferent(next, [strip[k - 1]!], n);\n\t\tstrip.push(next);\n\t}\n\n\tif (len > 2 && strip[len - 1] === strip[0]) {\n\t\tstrip[len - 1] = pickDifferent(strip[len - 1]!, [strip[len - 2]!, strip[0]!], n);\n\t}\n\treturn strip;\n}\n\n/** Colour for a tile, keyed by its IDENTITY (never its on-screen slot) — colours stay put. */\nexport function stripIndex(strip: readonly number[], id: number, column: number): number {\n\tconst len = strip.length;\n\tconst s = id - column;\n\treturn strip[((s % len) + len) % len]!;\n}\n\n// --- geometry ------------------------------------------------------------------\n\n/** Column edges in device px: W·(i/N)^power, rounded. Length = columns + 1. */\nexport function columnEdges(width: number, columns: number, power: number): number[] {\n\tconst edges: number[] = [];\n\tfor (let i = 0; i <= columns; i++) {\n\t\tedges.push(Math.round(width * Math.pow(i / columns, power)));\n\t}\n\treturn edges;\n}\n\n/** Force a usable odd tile count (≥ 3). */\nexport function oddTiles(tiles: number): number {\n\tconst t = Math.max(3, Math.round(Number.isFinite(tiles) ? tiles : DEFAULT_TILES));\n\treturn t % 2 === 0 ? t + 1 : t;\n}\n\n/**\n * Where tile boundary `k` sits, 0..1 of the height. Sigmoid across [0, tiles],\n * extended LINEARLY (shallow slope) past both ends rather than clamped.\n */\nexport function tileEdge(k: number, tiles: number, stretch = STRETCH): number {\n\tconst u = k / tiles;\n\tif (u < 0) return u * TAIL_SLOPE;\n\tif (u > 1) return 1 + (u - 1) * TAIL_SLOPE;\n\tconst a = Math.pow(u, stretch);\n\treturn a / (a + Math.pow(1 - u, stretch));\n}\n\nconst SPRING_NORM = 1 - Math.exp(-SPRING_K);\n\n/**\n * Re-time the fraction of one tile step: exponential arrival mirrored at the\n * halfway point. Rushes in, hangs at the centre (fattest), releases. Monotonic,\n * exact at 0 and 1, so the stack never drifts off schedule.\n */\nexport function springStep(p: number): number {\n\tconst half = (t: number) => (1 - Math.exp(-SPRING_K * t)) / SPRING_NORM;\n\treturn p < 0.5 ? 0.5 * half(2 * p) : 1 - 0.5 * half(2 * (1 - p));\n}\n\n/**\n * Unbounded phase of column `i` at `elapsed` seconds. Never wrapped: tile\n * identity is derived from it, so a wrap would teleport every boundary.\n */\nexport function columnFlow(elapsed: number, column: number, columns: number, speed = 1): number {\n\tconst delay = (columns - 1 - column) * FALL_STAGGER;\n\tconst t = elapsed - delay;\n\tconst raw = t <= 0 ? 0 : (t * speed) / CYCLE;\n\tconst linear = raw + column * COL_PHASE;\n\tconst step = Math.floor(linear);\n\treturn step + springStep(linear - step);\n}\n\nexport interface TileRect {\n\tid: number;\n\t/** Device px, unclamped. */\n\ttop: number;\n\t/** Device px, bleed included, unclamped. */\n\tbot: number;\n}\n\n/**\n * The tiles of one column at `flow`, ordered BOTTOM-UP (paint order): each\n * tile overhangs the one below it by `bleed` px, so painting upward laps the\n * overhang over the lower tile instead of hiding it under it.\n */\nexport function columnTiles(\n\tflow: number,\n\ttiles: number,\n\theight: number,\n\tbleed: number\n): TileRect[] {\n\tconst out: TileRect[] = [];\n\tconst base = -Math.floor(flow);\n\tfor (let n = tiles + 2; n >= -2; n--) {\n\t\tconst id = base + n;\n\t\tconst k = id + flow;\n\t\tconst top = Math.round(tileEdge(k, tiles) * height);\n\t\tconst bot = Math.round(tileEdge(k + 1, tiles) * height) + bleed;\n\t\tif (bot <= top) continue;\n\t\tout.push({ id, top, bot });\n\t}\n\treturn out;\n}\n\n// --- coverage ------------------------------------------------------------------\n\nexport type Direction = \"cover\" | \"reveal\";\n\nexport interface Coverage {\n\t/** Covered band per column, linear 0..1 (eased only when drawn). */\n\ttop: Float32Array;\n\tbot: Float32Array;\n}\n\nexport function createCoverage(columns: number, covered = false): Coverage {\n\tconst top = new Float32Array(columns);\n\tconst bot = new Float32Array(columns);\n\tif (covered) bot.fill(1);\n\treturn { top, bot };\n}\n\n/** Order in which the columns move. */\nexport type DatamoshSweep = \"right\" | \"left\" | \"center\" | \"edges\" | \"random\";\n\n/**\n * Delay before each column moves, in seconds. `right` starts at the rightmost\n * column, `center` fans out from the middle, `edges` closes in from both\n * sides, `random` is a seeded shuffle.\n */\nexport function columnDelays(\n\tcolumns: number,\n\tsweep: DatamoshSweep,\n\tstagger: number,\n\tseed = 1\n): number[] {\n\tconst mid = (columns - 1) / 2;\n\tconst rank = (i: number): number => {\n\t\tswitch (sweep) {\n\t\t\tcase \"left\":\n\t\t\t\treturn i;\n\t\t\tcase \"center\":\n\t\t\t\treturn Math.abs(i - mid);\n\t\t\tcase \"edges\":\n\t\t\t\treturn mid - Math.abs(i - mid);\n\t\t\tcase \"random\":\n\t\t\t\treturn 0;\n\t\t\tdefault:\n\t\t\t\treturn columns - 1 - i;\n\t\t}\n\t};\n\tif (sweep === \"random\") {\n\t\tconst order = Array.from({ length: columns }, (_, i) => i);\n\t\tconst rand = mulberry32(seed ^ 0x5eed);\n\t\tfor (let i = order.length - 1; i > 0; i--) {\n\t\t\tconst j = Math.floor(rand() * (i + 1));\n\t\t\t[order[i], order[j]] = [order[j]!, order[i]!];\n\t\t}\n\t\tconst delays = new Array<number>(columns);\n\t\torder.forEach((col, k) => (delays[col] = k * stagger));\n\t\treturn delays;\n\t}\n\treturn Array.from({ length: columns }, (_, i) => rank(i) * stagger);\n}\n\n/**\n * Advance every column one step toward `direction`. Cover drops the band's\n * bottom edge to 1 (and pulls a half-fallen top back up); reveal lets the top\n * edge fall onto the bottom one. Monotonic per column, so an interruption just\n * reverses from wherever each column is. Returns true once every column is done.\n */\nexport function stepCoverage(\n\tcov: Coverage,\n\tdirection: Direction,\n\tphaseTime: number,\n\tdt: number,\n\tduration: number,\n\tdelays: ArrayLike<number>\n): boolean {\n\tconst n = cov.top.length;\n\tconst rate = duration > 0 ? dt / duration : Infinity;\n\tlet done = true;\n\tfor (let i = 0; i < n; i++) {\n\t\tconst started = phaseTime >= (delays[i] ?? 0);\n\t\tif (direction === \"cover\") {\n\t\t\t// An emptied column restarts from the top as a fresh curtain.\n\t\t\tif (cov.bot[i]! - cov.top[i]! <= 1e-6 && cov.bot[i]! < 1) {\n\t\t\t\tcov.top[i] = 0;\n\t\t\t\tcov.bot[i] = 0;\n\t\t\t}\n\t\t\tif (started) {\n\t\t\t\tcov.bot[i] = Math.min(1, cov.bot[i]! + rate);\n\t\t\t\tcov.top[i] = Math.max(0, cov.top[i]! - rate);\n\t\t\t}\n\t\t\tif (cov.bot[i]! < 1 || cov.top[i]! > 0) done = false;\n\t\t} else {\n\t\t\tif (started) cov.top[i] = Math.min(cov.bot[i]!, cov.top[i]! + rate);\n\t\t\tif (cov.top[i]! < cov.bot[i]!) done = false;\n\t\t}\n\t}\n\treturn done;\n}\n\n/** Snap every column to the end of `direction`. */\nexport function settleCoverage(cov: Coverage, direction: Direction) {\n\tif (direction === \"cover\") {\n\t\tcov.top.fill(0);\n\t\tcov.bot.fill(1);\n\t} else {\n\t\tcov.top.fill(0);\n\t\tcov.bot.fill(0);\n\t}\n}\n\n/** Ease for a band edge: cubic in-out, exact at 0 and 1. */\nexport function easeEdge(a: number): number {\n\tconst x = Math.min(1, Math.max(0, a));\n\treturn x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;\n}\n\n// --- effects ---------------------------------------------------------------------\n\n/**\n * Shape of the covered band. `curtain` drops from the top and falls away,\n * `rise` climbs from the bottom and lifts off, `split` opens from the centre\n * line and squeezes shut onto it, `interlace` alternates curtain and rise\n * column by column. Everything stays vertical.\n */\nexport type DatamoshEffect = \"curtain\" | \"rise\" | \"split\" | \"interlace\";\n\nexport interface Band {\n\t/** 0..1 of the height. */\n\ty0: number;\n\ty1: number;\n\t/** Draw the tile stack upside down (tiles rise instead of fall). */\n\tmirror: boolean;\n}\n\n/**\n * Map a column's curtain-space band `[top, bot]` (already eased) onto the\n * screen for `effect`. Curtain space is what `stepCoverage` animates; every\n * effect is a pure re-mapping of it, so interruptions behave the same.\n */\nexport function columnBand(effect: DatamoshEffect, column: number, top: number, bot: number): Band {\n\tconst rise = effect === \"rise\" || (effect === \"interlace\" && column % 2 === 1);\n\tif (effect === \"split\") {\n\t\tconst half = Math.max(0, bot - top) / 2;\n\t\treturn { y0: 0.5 - half, y1: 0.5 + half, mirror: false };\n\t}\n\tif (rise) return { y0: 1 - bot, y1: 1 - top, mirror: true };\n\treturn { y0: top, y1: bot, mirror: false };\n}\n\n// --- palettes --------------------------------------------------------------------\n\n/** Ready-made palettes. Each keeps one light anchor and a couple of near-blacks. */\nexport const DATAMOSH_PALETTES = {\n\tbroadcast: DEFAULT_COLORS,\n\tthermal: [\n\t\t\"#fff4d6\",\n\t\t\"#ffd23f\",\n\t\t\"#ff9f1c\",\n\t\t\"#ff5a1f\",\n\t\t\"#e0282e\",\n\t\t\"#b0125b\",\n\t\t\"#6a0f63\",\n\t\t\"#2a0a3d\",\n\t\t\"#0d0614\",\n\t],\n\tsunset: [\n\t\t\"#fff6d8\",\n\t\t\"#fbc56b\",\n\t\t\"#f59a3a\",\n\t\t\"#e97a1d\",\n\t\t\"#c85a14\",\n\t\t\"#8a4a22\",\n\t\t\"#5a3b25\",\n\t\t\"#2d1c16\",\n\t\t\"#140b08\",\n\t],\n\tmono: [\"#ffffff\", \"#e5e5e5\", \"#b5b5b5\", \"#8a8a8a\", \"#5c5c5c\", \"#2e2e2e\", \"#141414\", \"#050505\"],\n\tacid: [\n\t\t\"#f4ffe8\",\n\t\t\"#e4ff1a\",\n\t\t\"#00ff9c\",\n\t\t\"#00e0ff\",\n\t\t\"#3d5afe\",\n\t\t\"#8c3dff\",\n\t\t\"#ff2bd6\",\n\t\t\"#1b0b36\",\n\t\t\"#070707\",\n\t],\n} as const satisfies Record<string, readonly string[]>;\n\nexport type DatamoshPaletteName = keyof typeof DATAMOSH_PALETTES;\n\n/** Resolve a palette name or colour list; falls back to the default for anything unusable. */\nexport function resolvePalette(\n\tcolors: readonly string[] | DatamoshPaletteName | undefined\n): string[] {\n\tif (typeof colors === \"string\") return [...(DATAMOSH_PALETTES[colors] ?? DEFAULT_COLORS)];\n\treturn colors && colors.length >= 2 ? [...colors] : [...DEFAULT_COLORS];\n}\n\n// --- picture source --------------------------------------------------------------\n\n/** Largest brightness gain the `boost` auto-levels may apply. */\nexport const MAX_GAIN = 1.6;\n\n/** Share of the picture's height the slots sample from, centred. */\nexport const SOURCE_SPAN = 0.6;\n\n/**\n * Height (0..1) a slot samples the picture at: spread linearly over the middle\n * `SOURCE_SPAN` of the picture. Not along the fall's sigmoid, which packs\n * almost every slot against the edges, and not over the full height, where\n * frames and borders (the darkest, dullest parts of most pictures) sit.\n */\nexport function sourceSlotY(slot: number, tiles: number): number {\n\treturn 0.5 + ((slot + 0.5) / tiles - 0.5) * SOURCE_SPAN;\n}\n\n/**\n * Which rest slot a tile's colour is sampled from. Keyed by IDENTITY, like the\n * colour strip: a tile keeps its piece of the picture as it falls, so the\n * image gets dragged down in chunks instead of sliding under the grid.\n */\nexport function sourceSlot(id: number, column: number, tiles: number): number {\n\tconst s = id - column;\n\treturn ((s % tiles) + tiles) % tiles;\n}\n\n/** Source rect that fits an image over a box like `object-fit: cover`. */\nexport function coverFit(\n\timgW: number,\n\timgH: number,\n\tboxW: number,\n\tboxH: number\n): { sx: number; sy: number; sw: number; sh: number } {\n\tif (imgW <= 0 || imgH <= 0 || boxW <= 0 || boxH <= 0) return { sx: 0, sy: 0, sw: imgW, sh: imgH };\n\tconst scale = Math.max(boxW / imgW, boxH / imgH);\n\tconst sw = boxW / scale;\n\tconst sh = boxH / scale;\n\treturn { sx: (imgW - sw) / 2, sy: (imgH - sh) / 2, sw, sh };\n}\n\n/**\n * Colour table `[column][slot]` sampled from an RGBA buffer that already maps\n * onto the overlay box. Each cell averages a 3×3 neighbourhood at the column's\n * centre and the slot's height. `edges` are column edges normalised to 0..1.\n *\n * With `boost` (default), the table is auto-levelled (darkest cell → black,\n * brightest → white) and saturated, so a moody picture still decodes into the\n * flat, saturated blocks the effect is about.\n */\nexport function sampleSource(\n\tpixels: ArrayLike<number>,\n\tpw: number,\n\tph: number,\n\tedges: readonly number[],\n\ttiles: number,\n\tboost = true\n): string[][] {\n\tconst at = (x: number, y: number) => {\n\t\tconst cx = Math.min(pw - 1, Math.max(0, x));\n\t\tconst cy = Math.min(ph - 1, Math.max(0, y));\n\t\treturn (cy * pw + cx) * 4;\n\t};\n\tconst cells: [number, number, number][][] = [];\n\tlet lo = Infinity;\n\tlet hi = -Infinity;\n\tfor (let i = 0; i + 1 < edges.length; i++) {\n\t\tconst x = Math.round(((edges[i]! + edges[i + 1]!) / 2) * (pw - 1));\n\t\tconst row: [number, number, number][] = [];\n\t\tfor (let s = 0; s < tiles; s++) {\n\t\t\tconst y = Math.round(sourceSlotY(s, tiles) * (ph - 1));\n\t\t\tlet r = 0;\n\t\t\tlet g = 0;\n\t\t\tlet b = 0;\n\t\t\tfor (let dy = -1; dy <= 1; dy++) {\n\t\t\t\tfor (let dx = -1; dx <= 1; dx++) {\n\t\t\t\t\tconst k = at(x + dx, y + dy);\n\t\t\t\t\tr += pixels[k]!;\n\t\t\t\t\tg += pixels[k + 1]!;\n\t\t\t\t\tb += pixels[k + 2]!;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst cell: [number, number, number] = [r / 9, g / 9, b / 9];\n\t\t\tconst l = 0.2126 * cell[0] + 0.7152 * cell[1] + 0.0722 * cell[2];\n\t\t\tlo = Math.min(lo, l);\n\t\t\thi = Math.max(hi, l);\n\t\t\trow.push(cell);\n\t\t}\n\t\tcells.push(row);\n\t}\n\n\tconst span = hi - lo;\n\t// Gain capped: a dark, low-contrast picture (a dusk scene) would otherwise be\n\t// blown out into one flat neon colour.\n\tconst level = boost && span > 8 ? Math.min(MAX_GAIN, 255 / (hi - lo * 0.5)) : 1;\n\t// Lift the floor only halfway, so shadows stay dark without crushing to black.\n\tconst offset = boost && span > 8 ? lo * 0.5 : 0;\n\tconst saturation = boost ? 1.35 : 1;\n\tconst clamp = (v: number) => Math.round(Math.min(255, Math.max(0, v)));\n\n\treturn cells.map((row) =>\n\t\trow.map(([r, g, b]) => {\n\t\t\tlet [nr, ng, nb] = [(r - offset) * level, (g - offset) * level, (b - offset) * level];\n\t\t\tconst l = 0.2126 * nr + 0.7152 * ng + 0.0722 * nb;\n\t\t\tnr = l + (nr - l) * saturation;\n\t\t\tng = l + (ng - l) * saturation;\n\t\t\tnb = l + (nb - l) * saturation;\n\t\t\treturn `rgb(${clamp(nr)}, ${clamp(ng)}, ${clamp(nb)})`;\n\t\t})\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/datamosh-transition/datamosh-transition-core.ts"},{"path":"react/src/components/datamosh-transition/index.ts","content":"\"use client\";\n\nexport { DatamoshTransition } from \"./DatamoshTransition\";\nexport type {\n\tDatamoshTransitionProps,\n\tDatamoshTransitionPhase,\n\tDatamoshTransitionHandle,\n} from \"./DatamoshTransition\";\nexport {\n\tDATAMOSH_PALETTES,\n\ttype DatamoshEffect,\n\ttype DatamoshSweep,\n\ttype DatamoshPaletteName,\n} from \"./datamosh-transition-core\";\n","type":"registry:ui","target":"@ui/fancy/components/datamosh-transition/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/datamosh-transition?framework=react"}}