{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "domain-switch",
  "type": "registry:ui",
  "title": "Domain Switch",
  "description": "A toggle that expands its new state as a domain across the page. A sphere grows from the switch, everything inside it changes theme or mode, and its edge is a thin seam of stars where the two worlds meet.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "toggle",
    "theme",
    "transition",
    "canvas"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/domain-switch",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/domain-switch.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { animate, useMotionValue, useReducedMotion, type MotionValue } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - Every frame is written straight to the DOM from one motion value, `progress`: the inside's\n *   `clip-path` circle, the lens scale and the canvas seam. Nothing in a frame re-renders React.\n * - Both worlds are real markup stacked in one grid cell, the inside clipped on top. The world not\n *   in charge is `inert` and `aria-hidden`, and focus moves to the same toggle in the other world.\n * - A toggle mid-flight turns around from the current progress, never from the start; the move\n *   scales with the square root of the distance left and never drops under `TIMING.shortest`.\n * - The sphere opens from the pressed toggle, measured in the unmagnified outside world, and grows\n *   past the farthest corner by the seam's width so the seam leaves with it.\n * - Reduced motion drops the clip, lens and seam and crossfades the inside world's opacity.\n */\nconst TIMING = {\n  /** The domain growing across the scope, in seconds (the `duration` prop). */\n  duration: 1.1,\n  /** A collapse, as a share of `duration` (the `collapseRatio` prop). */\n  collapseRatio: 0.8,\n  /** The curve it grows on: a heavy ease-out, fast start, long settle. */\n  grow: [0.2, 0.72, 0.12, 1],\n  /** The curve it collapses on: it gathers, then draws in to the toggle. */\n  collapse: [0.5, 0, 0.18, 1],\n  /** The shortest a move takes, so a reversal near the end still takes this long, in seconds. */\n  shortest: 0.35,\n  /** The reduced-motion crossfade, in seconds. */\n  fade: 0.15,\n  /** The stars sliding along the seam, in rad per ms. */\n  drift: 0.00004,\n  /** A star's twinkle, in rad per ms: the slowest, plus up to `spread` more. */\n  twinkle: { base: 0.003, spread: 0.007 },\n  /** The toggle's press scale, as Tailwind classes. */\n  press: \"duration-150 ease-out\",\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * DOMAIN SWITCH STORYBOARD\n *\n *  press     the toggle flips and a domain opens from its center\n *  grow      a sphere of the new world swells across the scope:\n *            fast at first, then a long settle out to the\n *            farthest corner. Everything inside it is already the\n *            other world, real markup, so text stays crisp\n *  seam      the edge is a thin band of space, dark with star\n *            points, where the two worlds meet. The inside is\n *            magnified a touch and relaxes as it grows, so the\n *            edge reads as a lens, not a wipe\n *  settle    the sphere passes the last corner, the seam leaves\n *            with it and the old world is put away\n *  collapse  toggle back and the domain draws in to the toggle\n *            on the same seam\n *  reverse   toggle mid-flight and it turns around from where it\n *            is, never from the start\n *  reduced   with reduced motion the worlds crossfade in 150ms\n * ───────────────────────────────────────────────────────── */\n\nconst SEAM = {\n  stars: 1.7, // px of seam per star\n  maxStars: 4800,\n  starFade: 48, // stars that fade in as the seam lengthens\n} as const;\n\ntype World = \"inside\" | \"outside\";\n\ntype Toggle = { element: HTMLElement; world: World | null };\n\ntype Scope = {\n  measure: (origin: { x: number; y: number } | null) => void;\n  render: (p: number) => void;\n  inside: HTMLElement;\n  outside: HTMLElement;\n  /** The element in the scope that last had focus, kept through the blur that `inert` causes. */\n  focused: () => Element | null;\n};\n\n/** Keeps the toggles and scopes out of React, so a frame never re-renders anything. */\nfunction createDomain(progress: MotionValue<number>) {\n  const toggles = new Set<Toggle>();\n  const scopes = new Set<Scope>();\n  let pressed: { world: World | null; element: HTMLElement; index: number } | null = null;\n  let options = {\n    duration: TIMING.duration as number,\n    collapseRatio: TIMING.collapseRatio as number,\n    reduced: false,\n    onChange: undefined as ((inside: boolean) => void) | undefined,\n  };\n  let target = progress.get();\n\n  // Toggles of one world in document order, so the second toggle outside\n  // pairs with the second toggle inside.\n  const inWorld = (world: World | null) =>\n    [...toggles]\n      .filter((toggle) => toggle.world === world)\n      .map((toggle) => toggle.element)\n      .sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));\n\n  // The sphere opens from the toggle that was pressed, measured in the\n  // world that isn't magnified: a toggle outside the scopes, else its twin\n  // in the outside world.\n  const origin = () => {\n    const index = pressed?.index ?? 0;\n    const element =\n      (pressed?.world === null && pressed.element.isConnected ? pressed.element : null) ??\n      inWorld(\"outside\")[index] ??\n      inWorld(null)[0] ??\n      inWorld(\"outside\")[0] ??\n      inWorld(\"inside\")[index] ??\n      inWorld(\"inside\")[0];\n    if (!element) return null;\n    const rect = element.getBoundingClientRect();\n    return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };\n  };\n\n  // The world that was left goes inert, which drops focus on the body. If\n  // focus was in it, hand it to the same toggle in the world taking over.\n  // A task later: an Enter that closed the domain from a keydown would\n  // otherwise go on to click the toggle it lands on, and the app may want\n  // to focus something itself.\n  const handoff = (inside: boolean) => {\n    const active = document.activeElement;\n    for (const scope of scopes) {\n      const was = active && active !== document.body ? active : scope.focused();\n      if (!was || !(inside ? scope.outside : scope.inside).contains(was)) continue;\n      const index = pressed?.index ?? 0;\n      const next = inWorld(inside ? \"inside\" : \"outside\");\n      const toggle = next[index] ?? next[0] ?? inWorld(null)[0];\n      setTimeout(() => {\n        const now = document.activeElement;\n        if (now && now !== document.body && !now.closest(\"[inert]\")) return;\n        toggle?.focus({ preventScroll: true });\n      });\n      return;\n    }\n  };\n\n  return {\n    progress,\n    setOptions(next: typeof options) {\n      options = next;\n    },\n    reduced: () => options.reduced,\n    target: () => target,\n    addToggle(element: HTMLElement, world: World | null) {\n      const toggle = { element, world };\n      toggles.add(toggle);\n      return () => void toggles.delete(toggle);\n    },\n    press(element: HTMLElement, world: World | null) {\n      pressed = { element, world, index: Math.max(0, inWorld(world).indexOf(element)) };\n    },\n    addScope(scope: Scope) {\n      scopes.add(scope);\n      scope.measure(origin());\n      scope.render(progress.get());\n      return () => void scopes.delete(scope);\n    },\n    remeasure(scope: Scope) {\n      scope.measure(origin());\n      scope.render(progress.get());\n    },\n    // Runs in the commit that flips `inert`, so the incoming world is shown\n    // (clipped to nothing) before anything tries to focus inside it.\n    go(inside: boolean) {\n      const to = inside ? 1 : 0;\n      if (to === target) return;\n      target = to;\n      const at = origin();\n      scopes.forEach((scope) => {\n        scope.measure(at);\n        scope.render(progress.get());\n      });\n      handoff(inside);\n      const distance = Math.abs(to - progress.get());\n      const transition = options.reduced\n        ? { duration: TIMING.fade * distance, ease: \"linear\" as const }\n        : {\n            duration: Math.max(\n              TIMING.shortest,\n              options.duration * (inside ? 1 : options.collapseRatio) * Math.sqrt(distance),\n            ),\n            ease: inside ? TIMING.grow : TIMING.collapse,\n          };\n      animate(progress, to, transition).then(() => {\n        if (target === to) options.onChange?.(inside);\n      });\n    },\n  };\n}\n\ntype Domain = ReturnType<typeof createDomain>;\n\ntype DomainSwitchContextValue = {\n  checked: boolean;\n  setChecked: (checked: boolean) => void;\n  domain: Domain;\n};\n\nconst DomainSwitchContext = React.createContext<DomainSwitchContextValue | null>(null);\nconst WorldContext = React.createContext<World | null>(null);\n\nfunction useDomainSwitchContext(part: string) {\n  const context = React.useContext(DomainSwitchContext);\n  if (!context) throw new Error(`<${part}> must be used inside <DomainSwitch>.`);\n  return context;\n}\n\n/** The switch's state, and which world the caller is rendered in (`null` outside a scope). */\nexport function useDomainSwitch() {\n  const { checked, setChecked } = useDomainSwitchContext(\"useDomainSwitch\");\n  const world = React.useContext(WorldContext);\n  return { checked, setChecked, world };\n}\n\nexport interface DomainSwitchProps {\n  /** Whether the inside world has taken over. Control it with `onCheckedChange`. */\n  checked?: boolean;\n  defaultChecked?: boolean;\n  /** Called the moment the toggle is pressed. */\n  onCheckedChange?: (checked: boolean) => void;\n  /** Called once the domain has finished growing or collapsing, with the world that now covers the scope. Not called when a toggle reverses it mid-flight. */\n  onChange?: (inside: boolean) => void;\n  /** Seconds the domain takes to grow across the scope. Collapsing is a little quicker. */\n  duration?: number;\n  /** How long collapsing takes, as a share of `duration`. `1` draws the domain in as slowly as it grew. */\n  collapseRatio?: number;\n  children?: React.ReactNode;\n}\n\nexport function DomainSwitch({\n  checked: checkedProp,\n  defaultChecked = false,\n  onCheckedChange,\n  onChange,\n  duration = TIMING.duration,\n  collapseRatio = TIMING.collapseRatio,\n  children,\n}: DomainSwitchProps) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultChecked);\n  const checked = checkedProp ?? uncontrolled;\n  const progress = useMotionValue(checked ? 1 : 0);\n  const [domain] = React.useState(() => createDomain(progress));\n  const reduced = useReducedMotion() ?? false;\n\n  React.useEffect(() => {\n    domain.setOptions({ duration, collapseRatio, reduced, onChange });\n  }, [domain, duration, collapseRatio, reduced, onChange]);\n\n  React.useLayoutEffect(() => {\n    domain.go(checked);\n  }, [domain, checked]);\n\n  const setChecked = React.useCallback(\n    (next: boolean) => {\n      if (checkedProp === undefined) setUncontrolled(next);\n      onCheckedChange?.(next);\n    },\n    [checkedProp, onCheckedChange],\n  );\n\n  const value = React.useMemo(() => ({ checked, setChecked, domain }), [checked, setChecked, domain]);\n\n  return <DomainSwitchContext.Provider value={value}>{children}</DomainSwitchContext.Provider>;\n}\n\nexport interface DomainSwitchToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  children?: React.ReactNode;\n}\n\n/**\n * The switch itself. Put it outside the scope, or inside the worlds: one in\n * each world at the same spot, and the domain opens from it.\n */\nexport function DomainSwitchToggle({ className, children, onClick, ...props }: DomainSwitchToggleProps) {\n  const { checked, setChecked, domain } = useDomainSwitchContext(\"DomainSwitchToggle\");\n  const world = React.useContext(WorldContext);\n  const ref = React.useRef<HTMLButtonElement>(null);\n\n  React.useLayoutEffect(() => {\n    if (!ref.current) return;\n    return domain.addToggle(ref.current, world);\n  }, [domain, world]);\n\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={checked}\n      data-state={checked ? \"on\" : \"off\"}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented) return;\n        domain.press(event.currentTarget, world);\n        setChecked(!checked);\n      }}\n      className={cn(\n        `relative inline-flex cursor-pointer items-center justify-center transition-transform ${TIMING.press} outline-none select-none focus-visible:ring-2 focus-visible:ring-current/40 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent active:scale-[0.94]`,\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n}\n\nexport interface DomainSwitchScopeProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** The world the domain brings in. */\n  inside?: React.ReactNode;\n  /** The world it takes over. */\n  outside?: React.ReactNode;\n  /** Rendered in whichever world `inside` or `outside` leaves out, e.g. one page that reads `useDomainSwitch().world` or uses `DomainSwitchAside`. */\n  children?: React.ReactNode;\n  /** Width of the seam of stars at the edge of the domain, in px. `0` gives a clean edge. */\n  seam?: number;\n  /** How much the inside is magnified while the domain is small, e.g. `0.05` for 5%. It relaxes to none as it grows. */\n  lens?: number;\n  /** Classes that set the seam's color (a `text-*` color). Its stars take the opposite shade. */\n  seamClassName?: string;\n}\n\n/**\n * The area the domain takes over. Both worlds are real markup stacked in\n * one cell; the inside one is clipped to the sphere on top.\n */\nexport function DomainSwitchScope({\n  inside,\n  outside,\n  children,\n  seam = 10,\n  lens = 0.05,\n  className,\n  seamClassName,\n  ...props\n}: DomainSwitchScopeProps) {\n  const { checked, domain } = useDomainSwitchContext(\"DomainSwitchScope\");\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const outsideRef = React.useRef<HTMLDivElement>(null);\n  const insideRef = React.useRef<HTMLDivElement>(null);\n  const lensRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  // The first paint, before the effect takes over: only the world in charge shows.\n  const [initial] = React.useState(checked);\n\n  React.useLayoutEffect(() => {\n    const root = rootRef.current;\n    const outsideEl = outsideRef.current;\n    const insideEl = insideRef.current;\n    const lensEl = lensRef.current;\n    const canvas = canvasRef.current;\n    if (!root || !outsideEl || !insideEl || !lensEl || !canvas) return;\n    const context = canvas.getContext(\"2d\");\n\n    let width = 0;\n    let height = 0;\n    let dpr = 1;\n    let cx = 0;\n    let cy = 0;\n    let radius = 0;\n    let band: [number, number, number] = [9, 9, 11];\n    let star = \"#fff\";\n    let drawn = false;\n    let focused: Element | null = null;\n\n    const scope: Scope = {\n      inside: insideEl,\n      outside: outsideEl,\n      focused: () => focused,\n      measure(origin) {\n        const box = canvas.getBoundingClientRect();\n        width = box.width;\n        height = box.height;\n        dpr = Math.min(window.devicePixelRatio || 1, 2);\n        const w = Math.round(width * dpr);\n        const h = Math.round(height * dpr);\n        if (canvas.width !== w || canvas.height !== h) {\n          canvas.width = w;\n          canvas.height = h;\n        }\n        cx = origin ? origin.x - box.left : width / 2;\n        cy = origin ? origin.y - box.top : height / 2;\n        // Out to the farthest corner, plus the seam, so the seam leaves too.\n        radius =\n          Math.max(\n            Math.hypot(cx, cy),\n            Math.hypot(width - cx, cy),\n            Math.hypot(cx, height - cy),\n            Math.hypot(width - cx, height - cy),\n          ) +\n          seam +\n          2;\n        band = toRgb(getComputedStyle(canvas).color);\n        star = luminance(band) < 0.5 ? \"#f4f6ff\" : \"#0b0c10\";\n      },\n      render(p) {\n        const reduced = domain.reduced();\n        const target = domain.target();\n        const settledIn = p >= 1 && target === 1;\n        const settledOut = p <= 0 && target === 0;\n        insideEl.style.visibility = settledOut ? \"hidden\" : \"\";\n        outsideEl.style.visibility = settledIn ? \"hidden\" : \"\";\n\n        if (reduced || settledIn || settledOut) {\n          insideEl.style.clipPath = \"\";\n          insideEl.style.opacity = reduced && !settledIn ? String(p) : \"\";\n          lensEl.style.transform = \"\";\n          if (drawn && context) {\n            context.clearRect(0, 0, canvas.width, canvas.height);\n            drawn = false;\n          }\n          return;\n        }\n\n        const r = p * radius;\n        insideEl.style.opacity = \"\";\n        insideEl.style.clipPath = `circle(${r}px at ${cx}px ${cy}px)`;\n        lensEl.style.transformOrigin = `${cx}px ${cy}px`;\n        lensEl.style.transform = lens ? `scale(${1 + lens * (1 - p) * (1 - p)})` : \"\";\n        if (context && seam > 0) {\n          drawSeam(context, { width, height, dpr, cx, cy, r, seam, band, star, t: performance.now() });\n          drawn = true;\n        }\n      },\n    };\n\n    const onFocusIn = (event: FocusEvent) => {\n      focused = event.target as Element;\n    };\n    const onFocusOut = (event: FocusEvent) => {\n      // Focus going somewhere else on the page, not just dropped.\n      if (event.relatedTarget && !root.contains(event.relatedTarget as Node)) focused = null;\n    };\n    root.addEventListener(\"focusin\", onFocusIn);\n    root.addEventListener(\"focusout\", onFocusOut);\n    const remove = domain.addScope(scope);\n    const unsubscribe = domain.progress.on(\"change\", scope.render);\n    const observer = new ResizeObserver(() => domain.remeasure(scope));\n    observer.observe(root);\n    return () => {\n      root.removeEventListener(\"focusin\", onFocusIn);\n      root.removeEventListener(\"focusout\", onFocusOut);\n      remove();\n      unsubscribe();\n      observer.disconnect();\n    };\n  }, [domain, seam, lens]);\n\n  const outsideWorld = outside ?? children;\n  const insideWorld = inside ?? children;\n\n  return (\n    <div\n      ref={rootRef}\n      className={cn(\n        \"relative isolate grid grid-cols-[minmax(0,1fr)] grid-rows-[minmax(0,1fr)] overflow-hidden\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        ref={outsideRef}\n        inert={checked}\n        aria-hidden={checked || undefined}\n        className=\"relative [grid-area:1/1]\"\n        style={initial ? { visibility: \"hidden\" } : undefined}\n      >\n        <WorldContext.Provider value=\"outside\">{outsideWorld}</WorldContext.Provider>\n      </div>\n      <div\n        ref={insideRef}\n        inert={!checked}\n        aria-hidden={!checked || undefined}\n        className=\"relative [grid-area:1/1]\"\n        style={initial ? undefined : { visibility: \"hidden\" }}\n      >\n        <div ref={lensRef} className=\"h-full w-full\">\n          <WorldContext.Provider value=\"inside\">{insideWorld}</WorldContext.Provider>\n        </div>\n      </div>\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className={cn(\"pointer-events-none absolute inset-0 z-10 size-full text-zinc-950\", seamClassName)}\n      />\n    </div>\n  );\n}\n\nexport interface DomainSwitchAsideProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Classes it takes inside the domain. Replaces the default dim and blur. */\n  insideClassName?: string;\n}\n\n/** Something non-essential: inside the domain it dims and blurs away, and can't be reached. */\nexport function DomainSwitchAside({ className, insideClassName, ...props }: DomainSwitchAsideProps) {\n  const world = React.useContext(WorldContext);\n  const isInside = world === \"inside\";\n  return (\n    <div\n      inert={isInside || undefined}\n      className={cn(\n        className,\n        isInside && (insideClassName ?? \"scale-[0.98] opacity-[0.14] blur-[5px] saturate-50\"),\n      )}\n      {...props}\n    />\n  );\n}\n\nexport interface DomainSwitchMainProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Classes it takes inside the domain. Replaces the default scale. */\n  insideClassName?: string;\n}\n\n/** The content that matters: inside the domain it comes forward a touch. */\nexport function DomainSwitchMain({ className, insideClassName, ...props }: DomainSwitchMainProps) {\n  const world = React.useContext(WorldContext);\n  return (\n    <div\n      className={cn(className, world === \"inside\" && (insideClassName ?? \"scale-[1.04]\"))}\n      {...props}\n    />\n  );\n}\n\n/* ── the seam ─────────────────────────────────────────────── */\n\ntype SeamFrame = {\n  width: number;\n  height: number;\n  dpr: number;\n  cx: number;\n  cy: number;\n  r: number;\n  seam: number;\n  band: [number, number, number];\n  star: string;\n  t: number;\n};\n\nconst TAU = Math.PI * 2;\nconst PHI = 0.6180339887498949;\n\n// A stable random number per star, so a star keeps its place and size.\nfunction hash(k: number, salt: number) {\n  const x = Math.sin(k * 127.1 + salt * 311.7) * 43758.5453;\n  return x - Math.floor(x);\n}\n\nfunction drawSeam(ctx: CanvasRenderingContext2D, f: SeamFrame) {\n  const { width, height, cx, cy, r, seam, t } = f;\n  ctx.setTransform(f.dpr, 0, 0, f.dpr, 0, 0);\n  ctx.clearRect(0, 0, width, height);\n  if (r <= 0.5) return;\n  // A sphere the size of a toggle is all seam; let it come in.\n  const grown = Math.min(1, r / (seam * 4));\n  const [cr, cg, cb] = f.band;\n  const rgba = (a: number) => `rgba(${cr},${cg},${cb},${a * grown})`;\n\n  // The band: a strip of space across the edge, with a soft shadow\n  // reaching in so the inside sits a little deeper.\n  const inner = Math.max(0, r - seam * 3.2);\n  const outer = r + seam * 0.75;\n  const at = (x: number) => Math.min(1, Math.max(0, (x - inner) / (outer - inner)));\n  const gradient = ctx.createRadialGradient(cx, cy, inner, cx, cy, outer);\n  gradient.addColorStop(0, rgba(0));\n  gradient.addColorStop(at(r - seam * 1.2), rgba(0.12));\n  gradient.addColorStop(at(r - seam * 0.4), rgba(0.85));\n  gradient.addColorStop(at(r + seam * 0.3), rgba(0.85));\n  gradient.addColorStop(1, rgba(0));\n  ctx.fillStyle = gradient;\n  ctx.beginPath();\n  ctx.arc(cx, cy, outer, 0, TAU);\n  ctx.arc(cx, cy, inner, 0, TAU, true);\n  ctx.fill();\n\n  // Stars on a low-discrepancy sequence of angles: any first n of them are\n  // spread evenly, so the seam fills in as it lengthens and none jump.\n  const count = Math.min(SEAM.maxStars, (TAU * r) / SEAM.stars);\n  const last = Math.min(SEAM.maxStars, Math.ceil(count + SEAM.starFade));\n  const spin = t * TIMING.drift;\n  ctx.fillStyle = f.star;\n  for (let k = 0; k < last; k++) {\n    const u = hash(k, 1);\n    const v = hash(k, 2);\n    const angle = ((k * PHI) % 1) * TAU + spin;\n    // Across the strip, thickest at the edge itself.\n    const d = r + (u - 0.5) * (0.4 + 0.6 * hash(k, 4)) * seam * 1.3;\n    const x = cx + Math.cos(angle) * d;\n    const y = cy + Math.sin(angle) * d;\n    if (x < -2 || y < -2 || x > width + 2 || y > height + 2) continue;\n    const twinkle = 0.55 + 0.45 * Math.sin(t * (TIMING.twinkle.base + TIMING.twinkle.spread * v) + u * TAU);\n    let alpha = (0.45 + 0.55 * hash(k, 3)) * twinkle * grown;\n    if (k > count) alpha *= 1 - (k - count) / SEAM.starFade;\n    if (alpha <= 0.02) continue;\n    const size = 0.7 + v * v * 1.3;\n    ctx.globalAlpha = alpha;\n    ctx.fillRect(x - size / 2, y - size / 2, size, size);\n    // One in thirty is a glint with a faint halo.\n    if (v > 0.967) {\n      ctx.globalAlpha = alpha * 0.22;\n      ctx.fillRect(x - 1.75, y - 1.75, 3.5, 3.5);\n    }\n  }\n  ctx.globalAlpha = 1;\n}\n\n// Canvas gradients need rgb. Computed colors can be oklch, so let the\n// canvas resolve it.\nlet probe: CanvasRenderingContext2D | null = null;\nfunction toRgb(color: string): [number, number, number] {\n  probe ??= document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n  if (!probe) return [9, 9, 11];\n  probe.clearRect(0, 0, 1, 1);\n  probe.fillStyle = \"#000\";\n  probe.fillStyle = color;\n  probe.fillRect(0, 0, 1, 1);\n  const [r, g, b] = probe.getImageData(0, 0, 1, 1).data;\n  return [r, g, b];\n}\n\nfunction luminance([r, g, b]: [number, number, number]) {\n  return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;\n}\n"
    }
  ]
}