{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"channel-switch","title":"Channel Switch","description":"Content changes like a CRT changing channel — collapse to a line and a dot, a phosphor feedback buffer decaying as e^(−Δt/τ), and a damped vertical hold rolling the new picture into lock. Children stay live DOM.","dependencies":[],"files":[{"path":"src/components/ChannelSwitch/index.js","content":"export { default } from './ChannelSwitch.jsx'\n","type":"registry:component","target":"components/channel-switch/index.js"},{"path":"src/components/ChannelSwitch/ChannelSwitch.jsx","content":"'use client'\n\nimport { useEffect, useRef, useState } from 'react'\nimport './ChannelSwitch.css'\n\n/* =========================================================\n   ChannelSwitch\n   Content changes the way a cathode-ray set changes channel:\n   the picture collapses to a bright line, the line to a dot,\n   the dot glows and fades; then the new picture opens out of\n   a line and rolls vertically until the set locks on to it.\n\n   Each stage is what the tube physically did.\n\n   The collapse is the deflection failing. The beam is swept\n   by two magnetic fields; the vertical one goes first, so\n   every row lands on the same line, and all the picture's\n   energy lands there with it — the line is brighter than any\n   part of the picture was. The horizontal one holds a beat\n   longer, then goes too, and the line becomes a dot, brighter\n   again. Each scan is a coil and its amplifier, a second-\n   order system, x″ = −ω²(x − target) − 2ω·x′, critically\n   damped so the picture never swings past the bezel. It is\n   stepped exactly, so it draws the same curve at 60 Hz and\n   120 Hz, and it has a velocity: nothing about a switch is a\n   timeline that could be restarted.\n\n   That is what makes a second change of channel cheap. While\n   the picture is going, or the tube is dark, there is nothing\n   to do — whatever channel is newest when the tube goes dark\n   is the one that goes in. If the picture is already opening\n   on the new one, the scans are simply driven off again from\n   where they are, at the speed they have: a half-open picture\n   folds from half open, never from the full one, and a line\n   caught still growing turns round at once. And if the\n   channel goes back to what is still on show before the tube\n   is dark, nothing is owed any more; the picture opens again\n   from wherever it got to, with nothing swapped and no roll.\n\n   The dot is the beam left undeflected. It lingers while the\n   EHT drains, about a fifth of the duration, and as the next\n   channel comes in the beam returns first: the dot flares,\n   then streaks out into a line, then opens into the picture.\n   Coming on takes a little longer than going off — the eye\n   wants time with what arrives, not with what leaves — and\n   the new picture is not shown until it is actually in the\n   document, however long its render takes.\n\n   The glow is phosphor persistence. The screen keeps emitting\n   after the beam has gone, decaying exponentially. All of the\n   overlay's light lies on the one line the beam is confined\n   to, so the overlay is a feedback buffer along that line:\n   every frame the glow left from before is kept at e^(−Δt/τ),\n   and wherever the beam is now the phosphor shines at least as\n   brightly as it drives it. A dot does not blink out; it\n   fades, and a shrinking line leaves a trail. The buffer is\n   held in floats, so a long τ fades all the way to black\n   instead of stalling a few levels above it, and it is light,\n   so it is screened over the picture rather than painted on.\n\n   The roll is the vertical hold. A new signal's sync pulses\n   arrive at an arbitrary phase, and once the vertical scan is\n   running again the set's oscillator pulls toward them like a\n   damped spring, y″ = −ω²y − 2ζω·y′, with ζ ≈ 0.7 — where a\n   sync loop is usually set, locking quickly with one small\n   overshoot. The offset is a phase of the vertical scan, so it\n   is scaled with it: the line always sits where the beam is,\n   and the roll grows as the picture opens. While it is out of\n   lock the vertical blanking interval shows as the black bar\n   it always was. Every rate in the switch, the hold's\n   included, is a multiple of 1/duration, so a slow switch\n   is the same switch slowed; only the phosphor keeps its own\n   τ.\n\n   The browser gives no access to DOM pixels, so the picture\n   itself is only ever transformed, never re-rendered; the\n   light — the line, the dot, the glow — is drawn on the\n   overlay. Children stay real, accessible DOM throughout. The\n   picture refuses the pointer only while it is not there to\n   be pointed at: from the moment it starts to fold until it\n   is open again. The roll is already the new channel, live.\n\n   Under reduced motion, in a hidden tab, or with the box off\n   screen, the content is swapped at once and onSwitched still\n   fires; a switch already under way finishes on the spot.\n   ========================================================= */\n\nconst V_OFF = 44        // ω × duration (s) of the vertical scan failing\nconst H_OFF = 60        // ω × duration of the horizontal following it\nconst H_ON = 42         // ω × duration of the line growing out of the dot\nconst V_ON = 20         // ω × duration of the picture opening out of the line\nconst HOLD = 8.5        // ω × duration of the vertical hold\nconst HOLD_ZETA = 0.7   // its damping\nconst LINE_HOLD = 0.06  // of duration: the line holds this long before the horizontal goes\nconst DWELL = 0.2       // of duration: the tube dark between channels, the dot lingering\nconst BEAM_ON = 0.028   // of duration: τ of the beam coming back\nconst EHT = 0.08        // of duration: τ of the beam fading as the EHT drains\nconst ROLL = 0.62       // of the height: how far out of hold the picture arrives at roll = 1\nconst LINE_PX = 4       // a band this thin is a line\nconst OPEN = 0.75       // of the width: how far the line has grown before the picture opens\nconst DARK = 0.01       // of the width: a line this short is only the dot\nconst BEAM_PX = 2       // the line is never thinner than the beam\nconst STEP = 1 / 240    // s: a frame is taken in steps no longer than this\n\nconst clamp = (n, lo, hi) => (n < lo ? lo : n > hi ? hi : n)\n// A number, or a string that is one — a range input hands its value over as text.\nconst finite = (n, d) => {\n  const x = typeof n === 'string' ? Number(n) : n\n  return Number.isFinite(x) ? x : d\n}\nconst ramp = (n, lo, hi) => clamp((n - lo) / (hi - lo), 0, 1)\nconst smooth = (n, lo, hi) => {\n  const t = ramp(n, lo, hi)\n  return t * t * (3 - 2 * t)\n}\n\n/* One exact step of x″ = −ω²(x − to) − 2ζω·x′, from whatever x and x′ are.\n   ζ = 1 (critically damped) or ζ < 1. Exact, so the frame rate changes\n   nothing; continuous, so a new target bends the motion instead of\n   restarting it. */\nfunction spring(s, to, w, zeta, dt) {\n  const e = s.x - to\n  const k = Math.exp(-zeta * w * dt)\n  if (zeta >= 1) {\n    const c = s.v + w * e\n    s.x = to + (e + c * dt) * k\n    s.v = (s.v - w * c * dt) * k\n    return\n  }\n  const wd = w * Math.sqrt(1 - zeta * zeta)\n  const cos = Math.cos(wd * dt), sin = Math.sin(wd * dt)\n  const b = (s.v + zeta * w * e) / wd\n  s.x = to + k * (e * cos + b * sin)\n  s.v = k * ((b * wd - zeta * w * e) * cos - (e * wd + zeta * w * b) * sin)\n}\n\n// The phosphor as r, g, b. A colour that does not parse falls back instead of throwing mid-frame.\nfunction rgbOf(color) {\n  const c = document.createElement('canvas')\n  c.width = c.height = 1\n  const x = c.getContext('2d', { willReadFrequently: true })\n  if (!x) return [217, 241, 255]\n  x.fillStyle = '#d9f1ff'\n  x.fillStyle = typeof color === 'string' ? color : '#d9f1ff'\n  x.fillRect(0, 0, 1, 1)\n  const d = x.getImageData(0, 0, 1, 1).data\n  return [d[0], d[1], d[2]]\n}\n\nexport default function ChannelSwitch({\n  channel,\n  duration = 900,          // ms, off and on again; the hold locks a moment after\n  color = '#d9f1ff',       // phosphor\n  persistence = 0.18,      // s, phosphor decay constant τ\n  roll = 1,                // how far out of vertical hold the new picture arrives; 0 locks at once\n  onSwitched,              // () => void, when the new content goes in\n  children,\n  className = '',\n  style,\n  ...rest\n}) {\n  const rootRef = useRef(null)\n  const screenRef = useRef(null)\n  const canvasRef = useRef(null)\n  const api = useRef(null)\n\n  // What is on screen: follows children, but holds the old channel until the tube is dark.\n  const [view, setView] = useState({ channel, children })\n  const onShow = Object.is(channel, view.channel)    // not ===: a NaN channel must still equal itself\n  if (onShow && children !== view.children) {\n    setView({ channel, children })\n  }\n\n  const live = useRef(null)\n  // Synced after every render and before the effects below, so a switch reads\n  // current props — and the newest children — without restarting.\n  useEffect(() => {\n    live.current = { channel, children, duration, color, persistence, roll, onSwitched, shown: view.channel }\n  })\n\n  useEffect(() => {\n    const root = rootRef.current\n    const screen = screenRef.current\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext('2d')\n    const row = document.createElement('canvas')\n    const rctx = row.getContext('2d')\n    if (!root || !screen || !ctx || !rctx) {\n      // Nothing to draw on. The channel still changes, only without the tube.\n      api.current = {\n        run: () => {\n          const p = live.current\n          setView({ channel: p.channel, children: p.children })\n          p.onSwitched?.()\n        },\n        land: () => {},\n      }\n      return () => {\n        api.current = null\n      }\n    }\n\n    const calm = window.matchMedia('(prefers-reduced-motion: reduce)')\n    let W = 1, H = 1, dpr = 1\n    let raf = 0, last = 0\n    let seen = true\n    let shown = live.current.shown    // the channel whose content is in the screen\n    let landed = true                 // and that content has reached the document\n    let phase = 'on'                  // 'on' (scanning, or coming on) | 'off' (folding) | 'dark'\n    let lineFor = 0, darkFor = 0      // s spent as a line, and dark\n    let beam = 1                      // beam current: 1 driven, falling as the EHT drains\n    let dot = 0                       // the spot's glow\n    let glow = new Float32Array(1)    // the phosphor along the line, a value per CSS px\n    let pixels = rctx.createImageData(1, 1)\n    let rgb = null, rgbFor\n    let band = null, spot = null      // the line's vertical profile and the dot, at this size and colour\n    const sx = { x: 1, v: 0 }         // horizontal scan, of the width\n    const sy = { x: 1, v: 0 }         // vertical scan, of the height\n    const hold = { x: 0, v: 0 }       // vertical hold, out of lock by this much of the height\n    row.height = 1\n\n    const swap = (p) => {\n      shown = p.channel\n      landed = false\n      hold.x = -ROLL * clamp(finite(p.roll, 1), 0, 3)    // a new signal, at its own phase\n      hold.v = 0\n      setView({ channel: p.channel, children: p.children })\n      p.onSwitched?.()\n    }\n\n    const sleep = () => {\n      if (raf) cancelAnimationFrame(raf)\n      raf = 0\n      last = 0\n      root.classList.remove('is-switching', 'is-blanked')\n      screen.style.transform = ''\n      screen.style.filter = ''\n      screen.style.opacity = ''\n      ctx.setTransform(1, 0, 0, 1, 0, 0)\n      ctx.clearRect(0, 0, canvas.width, canvas.height)\n    }\n\n    const paints = (p) => {\n      if (!rgb || rgbFor !== p.color) {\n        rgb = rgbOf(p.color)\n        rgbFor = p.color\n        spot = null\n      }\n      const [r, g, b] = rgb\n      const half = clamp(H * 0.035, 6, 20)\n      if (!band) {\n        band = ctx.createLinearGradient(0, H / 2 - half, 0, H / 2 + half)\n        ;[[0, 0], [0.3, 0.06], [0.42, 0.25], [0.48, 0.75], [0.5, 1], [0.52, 0.75], [0.58, 0.25], [0.7, 0.06], [1, 0]]\n          .forEach(([at, a]) => band.addColorStop(at, `rgba(0,0,0,${a})`))\n      }\n      const R = clamp(Math.min(W, H) * 0.045, 8, 24)\n      if (!spot) {\n        spot = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, R)\n        spot.addColorStop(0, `rgba(${r},${g},${b},1)`)\n        spot.addColorStop(0.3, `rgba(${r},${g},${b},0.55)`)\n        spot.addColorStop(1, `rgba(${r},${g},${b},0)`)\n      }\n      return { r, g, b, half, R }\n    }\n\n    const draw = (p, most) => {\n      // The picture: squashed by the scans, brightened by the energy they concentrate, rolled by the hold.\n      const ax = Math.max(sx.x, Math.min(1, BEAM_PX / W))\n      const ay = Math.max(sy.x, Math.min(1, BEAM_PX / H))\n      const bright = Math.min(3, (ax * ay) ** -0.175)\n      const shade = ramp(sx.x, 0.005, 0.02)              // shorter than this, the line is only the dot\n      screen.style.transform =\n        `scale(${ax.toFixed(4)}, ${ay.toFixed(4)}) translateY(${(hold.x * H).toFixed(2)}px)`\n      screen.style.filter = bright > 1.005 ? `brightness(${bright.toFixed(3)})` : ''\n      screen.style.opacity = shade < 1 ? shade.toFixed(3) : ''\n\n      ctx.setTransform(1, 0, 0, 1, 0, 0)\n      ctx.clearRect(0, 0, canvas.width, canvas.height)\n      if (!most && !dot) return\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n      const { r, g, b, half, R } = paints(p)\n      if (most) {\n        const d = pixels.data\n        for (let i = 0; i < glow.length; i++) {\n          const o = i * 4\n          d[o] = r\n          d[o + 1] = g\n          d[o + 2] = b\n          d[o + 3] = Math.min(255, glow[i] * 255)\n        }\n        rctx.putImageData(pixels, 0, 0)\n        // The line's light along its length, then shaped across it: a hot core and a soft bloom.\n        ctx.globalCompositeOperation = 'source-over'\n        ctx.drawImage(row, 0, H / 2 - half, W, half * 2)\n        ctx.globalCompositeOperation = 'destination-in'\n        ctx.fillStyle = band\n        ctx.fillRect(0, H / 2 - half, W, half * 2)\n      }\n      if (dot) {\n        ctx.globalCompositeOperation = 'lighter'\n        ctx.globalAlpha = Math.min(1, dot)\n        ctx.fillStyle = spot\n        ctx.fillRect(W / 2 - R, H / 2 - R, R * 2, R * 2)\n        ctx.globalAlpha = 1\n      }\n      ctx.globalCompositeOperation = 'source-over'\n    }\n\n    const frame = (now) => {\n      // Asked for first: if a consumer's onSwitched throws, it costs this frame, not the picture.\n      raf = requestAnimationFrame(frame)\n      const p = live.current\n      const span = clamp((now - last) / 1000, 0, 1 / 30)\n      last = now\n      const T = Math.max(0.2, finite(p.duration, 900) / 1000)\n      const tau = clamp(finite(p.persistence, 0.18), 0.01, 2.5)\n\n      /* The frame is taken in short steps. The curves are exact at any step,\n         but what turns on a threshold — the line's beat, the tube going dark,\n         the beam back, the picture opening — can only turn at the edge of\n         one, and a 60 Hz frame is four times a 240 Hz one. */\n      const steps = Math.max(1, Math.ceil(span / STEP))\n      const dt = span / steps\n      let owed = false, most = 0\n      for (let step = 0; step < steps; step++) {\n        // What the set is doing. Nothing restarts: each change only re-aims the scans.\n        owed = !Object.is(p.channel, shown)\n        if (phase === 'on' && owed) phase = 'off'\n        else if (phase === 'off' && !owed) phase = 'on'          // back to what is on show: open again\n        if (phase === 'off' && sx.x < DARK) {\n          phase = 'dark'\n          darkFor = 0\n        }\n        if (phase === 'dark') {\n          if (owed) swap(p)                                      // the newest channel, in the dark\n          owed = false\n          darkFor += dt\n          if (darkFor >= DWELL * T && landed) phase = 'on'\n        }\n\n        // The beam comes back with the drive, and fails with the EHT once the tube is dark.\n        const to = phase === 'dark' ? 0 : 1\n        beam = to + (beam - to) * Math.exp(-dt / ((to ? BEAM_ON : EHT) * T))\n\n        // Off: vertical first, the line holds, then the horizontal. On: the dot flares, the line, the picture.\n        // The line's beat is counted both ways, so a line caught still growing has had\n        // it already: switched off again, it turns round at once instead of running on.\n        if (phase === 'on') {\n          spring(sx, beam > 0.7 ? 1 : 0, H_ON / T, 1, dt)\n          spring(sy, sx.x > OPEN ? 1 : 0, V_ON / T, 1, dt)\n          lineFor = sy.x * H < LINE_PX ? lineFor + dt : 0\n        } else {\n          spring(sy, 0, V_OFF / T, 1, dt)\n          lineFor = sy.x * H < LINE_PX ? lineFor + dt : 0\n          spring(sx, lineFor >= LINE_HOLD * T ? 0 : 1, H_OFF / T, 1, dt)\n        }\n        for (const s of [sx, sy]) {\n          if (s.x < 0) {\n            s.x = 0\n            s.v = Math.max(0, s.v)\n          }\n        }\n        if (phase === 'on' && Math.abs(1 - sx.x) * W < 0.3 && Math.abs(sx.v) * W < 30) {\n          sx.x = 1\n          sx.v = 0\n        }\n        if (phase === 'on' && Math.abs(1 - sy.x) * H < 0.3 && Math.abs(sy.v) * H < 30) {\n          sy.x = 1\n          sy.v = 0\n        }\n\n        // The hold pulls only while there is a vertical scan for it to time.\n        if (sy.x > 0.003) spring(hold, 0, HOLD / T, HOLD_ZETA, dt)\n        if (Math.abs(hold.x) * H < 0.3 && Math.abs(hold.v) * H < 12) {\n          hold.x = 0\n          hold.v = 0\n        }\n\n        /* Phosphor: keep what was glowing at e^(−Δt/τ); where the beam is, at\n           least what it drives. Brighter as the line shortens, the same energy\n           in less of it; softer towards its ends. */\n        const f = Math.exp(-dt / tau)\n        const lit = beam * (1 - smooth(sy.x, 0.004, 0.03))    // how much of the light is on the line\n        const n = glow.length, mid = n / 2, reach = (sx.x * n) / 2\n        const peak = lit * Math.min(1, 0.45 / Math.sqrt(Math.max(sx.x, 1e-3)))\n        most = 0\n        for (let i = 0; i < n; i++) {\n          let g = glow[i] * f\n          const u = Math.abs(i + 0.5 - mid) / reach\n          if (u < 1 && peak * (1 - u) > g) g = peak * (1 - u)\n          if (g < 0.002) g = 0\n          glow[i] = g\n          if (g > most) most = g\n        }\n        dot = Math.max(dot * f, lit * (1 - smooth(sx.x, 0, 0.08)))\n        if (dot < 0.002) dot = 0\n      }\n\n      draw(p, most)\n      root.classList.add('is-switching')    // each frame: a new className prop rewrites the attribute\n      root.classList.toggle('is-blanked', phase !== 'on' || sy.x < 0.95)\n\n      if (phase === 'on' && !owed && sx.x === 1 && sy.x === 1 && !hold.x && !most && !dot) sleep()\n    }\n\n    // Nobody is watching, or nobody wants the motion: make the change and be done.\n    const settle = () => {\n      const p = live.current\n      try {\n        if (!Object.is(p.channel, shown)) swap(p)\n      } finally {    // whatever a consumer's onSwitched does, the tube is left at rest\n        phase = 'on'\n        sx.x = sy.x = beam = 1\n        sx.v = sy.v = hold.x = hold.v = dot = lineFor = darkFor = 0\n        glow.fill(0)\n        sleep()\n      }\n    }\n\n    const run = () => {\n      if (calm.matches || !seen || document.hidden) {\n        settle()\n        return\n      }\n      root.classList.add('is-switching', 'is-blanked')    // the old channel is going: hands off it now\n      if (!raf) {\n        last = performance.now()    // the first frame advances by the time really since the change\n        raf = requestAnimationFrame(frame)\n      }\n    }\n\n    /* The canvas is measured rather than the root: it fills the root's padding\n       box, and its content box is that size exactly, whatever border the root\n       has and whatever transform it sits under. */\n    const ro = new ResizeObserver((entries) => {\n      const box = entries[entries.length - 1].contentRect\n      W = Math.max(1, box.width)\n      H = Math.max(1, box.height)\n      dpr = Math.min(2, window.devicePixelRatio || 1)\n      canvas.width = Math.round(W * dpr)\n      canvas.height = Math.round(H * dpr)\n      const n = Math.max(1, Math.ceil(W))\n      if (n !== glow.length) {\n        const was = glow    // the afterglow is on the glass: it is resampled, not lost\n        glow = new Float32Array(n)\n        for (let i = 0; i < n; i++) glow[i] = was[Math.floor(((i + 0.5) / n) * was.length)]\n        row.width = n\n        pixels = rctx.createImageData(n, 1)\n      }\n      band = spot = null\n      if (raf) draw(live.current, 1)    // sizing a canvas wipes it; put the light straight back\n    })\n    ro.observe(canvas)\n\n    const io = new IntersectionObserver((entries) => {\n      seen = entries[entries.length - 1].isIntersecting\n      if (!seen && raf) settle()\n    })\n    io.observe(root)\n\n    const onHide = () => {\n      if (document.hidden && raf) settle()\n    }\n    const onCalm = () => {\n      if (calm.matches && raf) settle()\n    }\n    document.addEventListener('visibilitychange', onHide)\n    calm.addEventListener?.('change', onCalm)\n\n    api.current = { run, land: () => { landed = true } }\n\n    return () => {\n      api.current = null\n      ro.disconnect()\n      io.disconnect()\n      document.removeEventListener('visibilitychange', onHide)\n      calm.removeEventListener?.('change', onCalm)\n      sleep()\n    }\n  }, [])\n\n  // What was swapped in has reached the document: the picture may open on it.\n  // Declared before the switch below, so a channel that changes in the same\n  // render as a swap lands asks for its own switch after this.\n  useEffect(() => {\n    api.current?.land()\n  }, [view])\n\n  // A new channel starts the switch; the content changes inside it, in the dark.\n  // One that returns to the channel on show is noticed by the switch itself.\n  useEffect(() => {\n    if (!onShow) api.current?.run()\n  }, [onShow, channel])\n\n  return (\n    <div\n      ref={rootRef}\n      className={`channel-switch ${className}`.trim()}\n      aria-busy={!onShow || undefined}\n      style={style}\n      {...rest}\n    >\n      <div ref={screenRef} className=\"channel-switch__screen\">{view.children}</div>\n      <canvas ref={canvasRef} className=\"channel-switch__phosphor\" aria-hidden=\"true\" />\n    </div>\n  )\n}\n","type":"registry:component","target":"components/channel-switch/ChannelSwitch.jsx"},{"path":"src/components/ChannelSwitch/ChannelSwitch.css","content":"/* ChannelSwitch — the screen is the content, transformed; the phosphor canvas\n   is the light, screened over it. While switching the box goes black behind\n   the picture, which is what shows through as the blanking bar during the\n   roll, and is isolated so the light blends with the tube and nothing else.\n   The picture refuses the pointer only while it is folded away. */\n\n.channel-switch {\n  position: relative;\n  overflow: hidden;\n}\n\n.channel-switch.is-switching {\n  background: #000;\n  isolation: isolate;\n}\n\n.channel-switch__screen {\n  position: relative;\n  height: 100%;\n  transform-origin: 50% 50%;\n}\n\n.channel-switch.is-switching .channel-switch__screen {\n  will-change: transform, filter, opacity;\n}\n\n.channel-switch.is-blanked .channel-switch__screen {\n  pointer-events: none;\n}\n\n.channel-switch__phosphor {\n  position: absolute;\n  inset: 0;\n  width: 100%;\n  height: 100%;\n  display: block;\n  pointer-events: none;\n}\n\n.channel-switch.is-switching .channel-switch__phosphor {\n  mix-blend-mode: screen;\n}\n","type":"registry:file","target":"components/channel-switch/ChannelSwitch.css"},{"path":"src/components/ChannelSwitch/ChannelSwitch.d.ts","content":"import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from 'react'\n\nexport interface ChannelSwitchProps extends ComponentPropsWithoutRef<'div'> {\n  /**\n   * Changing this switches channel. The new children go in while the tube is\n   * dark. A change mid-switch carries on from wherever the picture is; one\n   * back to the channel still on show reopens it with nothing swapped.\n   */\n  channel?: string | number\n  /**\n   * Off and on again, ms, never under 200: about a third going off, a fifth\n   * dark, the rest coming on. The vertical hold locks a moment after.\n   */\n  duration?: number\n  /**\n   * Phosphor colour. Any colour a canvas can parse — not `currentColor` or a\n   * `var()`; one that does not parse falls back to the default.\n   */\n  color?: string\n  /** Phosphor decay constant τ, seconds. Longer leaves more afterglow. */\n  persistence?: number\n  /** How far out of vertical hold the new picture arrives. 0 locks at once. */\n  roll?: number\n  /** Called as the new content goes in — once per swap, at once under reduced motion. */\n  onSwitched?: () => void\n  /** What is showing on the current `channel`. New children go in while the tube is dark. */\n  children?: ReactNode\n}\n\ndeclare const ChannelSwitch: (props: ChannelSwitchProps) => ReactElement\nexport default ChannelSwitch\n","type":"registry:lib","target":"components/channel-switch/ChannelSwitch.d.ts"}],"type":"registry:component"}