{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "background-12",
  "title": "Threads",
  "description": "Hairline threads that gather through a call to action, tighten as the pointer nears it and part around the pointer, where a press sends a light out along every one.",
  "dependencies": [
    "not showing"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/demonstration.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-autoplay.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/background/background-12.tsx",
      "content": "'use client';\n\nimport {\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  type Ref,\n  type RefObject,\n} from 'react';\n\nimport {\n  useAutoplay,\n  type AutoplayStep,\n} from '@/registry/ndeko/hooks/use-autoplay';\nimport {\n  PRESS_LEAD_MS,\n  pressedSolidOf,\n  usePress,\n} from '@/registry/ndeko/lib/demonstration';\nimport { Button } from '@/registry/ndeko/ui/button';\n\nimport { DemoBand } from './_parts/demo-band';\nimport { AT_REST, NEXT_FRAME } from './_parts/frame-loop';\nimport { DEFAULT_HUE } from './_parts/hue';\nimport { clamp, shareOf, smoothstep, towards, type Point } from './_parts/math';\nimport { centreIn } from './_parts/pointer';\nimport { randomOf } from './_parts/seeded';\nimport type { Scene } from './_parts/stage';\nimport { StageCanvas, type StageHandle } from './_parts/stage-canvas';\nimport { wipeSurface, type Inks, type Surface } from './_parts/surface';\n\nconst DEFAULT_COUNT = 9;\n\nconst MIN_COUNT = 3;\n\nconst MAX_COUNT = 16;\n\n/** Points along one thread: enough for a thread bent around a hand to stay a smooth curve. */\nconst SAMPLES = 48;\n\nconst LAST_SAMPLE = SAMPLES - 1;\n\n/** A thread starts just outside the box, so its cut end is never seen. */\nconst OVERHANG_PX = 2;\n\n/** The threads leave each edge over this stretch of its height, so none starts in a corner. */\nconst EDGE_FIRST = 0.06;\n\nconst EDGE_SPAN = 0.88;\n\n/**\n * How far a thread strays from its even place on the edge, as a share of the gap between two. It\n * stays under half the gap, so the threads keep their order and never cross on their way in.\n */\nconst EDGE_JITTER = 0.35;\n\n/** Half the height of the bundle where it passes through the target: a share of the box at rest. */\nconst WAIST_SHARE = 0.075;\n\nconst TIGHT_WAIST_PX = 3;\n\n/** Drawn tight, the edge end of a thread comes this share of the way towards the height of the target. */\nconst EDGE_PULL = 0.16;\n\n/**\n * How long each end of a thread runs level before it bends, as shares of the way across. With the\n * most curl the two still add up to less than the way across, so a thread never bends back.\n */\nconst LEAD = 0.42;\n\nconst TIGHT_LEAD = 0.24;\n\nconst TRAIL = 0.5;\n\nconst TIGHT_TRAIL = 0.3;\n\n/** Each thread bends a little more or less than its neighbours, up to this share either way. */\nconst CURL = 0.12;\n\n/** The pointer starts drawing the threads tight this far from the target, and has them tight this near. */\nconst TIGHTEN_FAR_PX = 320;\n\nconst TIGHTEN_NEAR_PX = 40;\n\nconst TENSION_EASE_S = 0.32;\n\nconst PRESENCE_EASE_S = 0.2;\n\nconst SETTLED = 0.002;\n\nconst GONE = 0.01;\n\nconst PART_RADIUS_PX = 96;\n\n/** How far the hand pushes a thread aside at most. Under half the radius, so two threads never cross. */\nconst PART_PX = 22;\n\nconst THREAD_ALPHA = 0.24;\n\nconst PULSE_MS = 1300;\n\n/** Presses quicker than a light takes to fade do not pile up: past this many, the oldest goes. */\nconst MAX_PULSES = 3;\n\n/** A light is a short stretch of thread: its head leads, its tail fades back towards the target. */\nconst TAIL = 0.22;\n\nconst TAIL_STEPS = 6;\n\nconst TAIL_STEP = TAIL / TAIL_STEPS;\n\nconst LIGHT_ALPHA = 0.9;\n\nconst LIGHT_WIDTH_PX = 1.5;\n\n/** A light grows out of the target over the start of its run and fades over its end. */\nconst LIGHT_IN = 0.06;\n\nconst LIGHT_OUT = 0.72;\n\n/** Under reduced motion a press lights every thread at once and lets go: the colour changes, nothing travels. */\nconst STILL_PULSE_MS = 900;\n\nconst STILL_LIGHT_ALPHA = 0.5;\n\nconst SEED = 12;\n\nconst NO_INKS: Inks = { hue: 'transparent', line: 'transparent' };\n\ntype Loom = {\n  count: number;\n  isStill: boolean;\n  inks: Inks;\n  width: number;\n  height: number;\n  /** 1 for a thread that comes in from the left edge, -1 for one from the right edge. */\n  side: Int8Array;\n  /** Where a thread leaves its edge, from 0 at the top to 1 at the foot. */\n  edge: Float32Array;\n  /** Where a thread passes through the target, from -1 at the top of the bundle to 1 at its foot. */\n  slot: Float32Array;\n  curl: Float32Array;\n  /** Every point of every thread, from its edge end to the target. */\n  x: Float32Array;\n  y: Float32Array;\n  target: HTMLElement | null;\n  knot: Point;\n  isKnotStale: boolean;\n  pointer: Point;\n  hasPointer: boolean;\n  /** How much the pointer is there, from 0 to 1. */\n  presence: number;\n  /** How tight the threads are drawn, from 0 at rest to 1 with the pointer on the target. */\n  tension: number;\n  /** When each light still on its way was sent. */\n  pulses: number[];\n};\n\ntype Pose = {\n  waist: number;\n  lead: number;\n  trail: number;\n  pull: number;\n};\n\nfunction perSideOf(count: number) {\n  const whole = Math.round(count);\n  return clamp(whole, MIN_COUNT, MAX_COUNT);\n}\n\nfunction sideOf(isFromLeft: boolean) {\n  if (isFromLeft) {\n    return 1;\n  }\n  return -1;\n}\n\n/** An even place along the edge, moved a little off it so the threads do not look ruled. */\nfunction edgeOf(share: number, gap: number, random: () => number) {\n  const roll = random();\n  const doubled = roll * 2;\n  const centred = doubled - 1;\n  const reach = EDGE_JITTER * gap;\n  const stray = centred * reach;\n  const strayed = share + stray;\n  const placed = clamp(strayed, 0, 1);\n  const along = placed * EDGE_SPAN;\n  return EDGE_FIRST + along;\n}\n\nfunction curlOf(random: () => number) {\n  const roll = random();\n  const doubled = roll * 2;\n  const centred = doubled - 1;\n  const swing = centred * CURL;\n  return 1 + swing;\n}\n\n/**\n * The threads of each side, from the top of its edge to its foot. A thread keeps its rank in the\n * bundle, so the threads never cross on their way to the target.\n */\nfunction layThreads(loom: Loom, perSide: number) {\n  const random = randomOf(SEED);\n  const lastRank = perSide - 1;\n  const gap = 1 / lastRank;\n  for (let thread = 0; thread < loom.count; thread += 1) {\n    const isFromLeft = thread < perSide;\n    const rank = thread % perSide;\n    const share = rank * gap;\n    const spread = share * 2;\n    loom.side[thread] = sideOf(isFromLeft);\n    loom.edge[thread] = edgeOf(share, gap, random);\n    loom.slot[thread] = spread - 1;\n    loom.curl[thread] = curlOf(random);\n  }\n}\n\nfunction loomOf(count: number): Loom {\n  const perSide = perSideOf(count);\n  const total = perSide * 2;\n  const points = total * SAMPLES;\n  const loom: Loom = {\n    count: total,\n    isStill: false,\n    inks: NO_INKS,\n    width: 0,\n    height: 0,\n    side: new Int8Array(total),\n    edge: new Float32Array(total),\n    slot: new Float32Array(total),\n    curl: new Float32Array(total),\n    x: new Float32Array(points),\n    y: new Float32Array(points),\n    target: null,\n    knot: { x: 0, y: 0 },\n    isKnotStale: true,\n    pointer: { x: 0, y: 0 },\n    hasPointer: false,\n    presence: 0,\n    tension: 0,\n    pulses: [],\n  };\n  layThreads(loom, perSide);\n  return loom;\n}\n\nfunction fitLoom(loom: Loom, surface: Surface, isStill: boolean) {\n  loom.width = surface.width;\n  loom.height = surface.height;\n  loom.isStill = isStill;\n  loom.isKnotStale = true;\n  loom.hasPointer = false;\n  if (!isStill) {\n    return;\n  }\n  loom.presence = 0;\n  loom.tension = 0;\n}\n\nfunction placeKnot(loom: Loom, surface: Surface) {\n  const halfWidth = surface.width / 2;\n  const halfHeight = surface.height / 2;\n  loom.isKnotStale = false;\n  loom.knot = { x: halfWidth, y: halfHeight };\n  if (!loom.target) {\n    return;\n  }\n  const centre = centreIn(surface.canvas, loom.target);\n  if (centre) {\n    loom.knot = centre;\n  }\n}\n\nfunction keepKnotPlaced(loom: Loom, surface: Surface) {\n  if (loom.isKnotStale) {\n    placeKnot(loom, surface);\n  }\n}\n\nfunction tensionGoalOf(loom: Loom) {\n  if (!loom.hasPointer) {\n    return 0;\n  }\n  const offsetX = loom.pointer.x - loom.knot.x;\n  const offsetY = loom.pointer.y - loom.knot.y;\n  const distance = Math.hypot(offsetX, offsetY);\n  return smoothstep(TIGHTEN_FAR_PX, TIGHTEN_NEAR_PX, distance);\n}\n\nfunction easedOf(current: number, goal: number, share: number) {\n  const next = towards(current, goal, share);\n  const gap = goal - next;\n  const isClose = Math.abs(gap) < SETTLED;\n  if (isClose) {\n    return goal;\n  }\n  return next;\n}\n\n/** The presence of the pointer and the pull it puts on the threads ease after it. It answers whether either still moves. */\nfunction easeHand(loom: Loom, seconds: number) {\n  const presenceGoal = Number(loom.hasPointer);\n  const tensionGoal = tensionGoalOf(loom);\n  const presenceShare = shareOf(seconds, PRESENCE_EASE_S);\n  const tensionShare = shareOf(seconds, TENSION_EASE_S);\n  loom.presence = easedOf(loom.presence, presenceGoal, presenceShare);\n  loom.tension = easedOf(loom.tension, tensionGoal, tensionShare);\n  const isPresenceSettled = loom.presence === presenceGoal;\n  const isTensionSettled = loom.tension === tensionGoal;\n  const hasSettled = isPresenceSettled && isTensionSettled;\n  return !hasSettled;\n}\n\nfunction easeOutOf(progress: number) {\n  const left = 1 - progress;\n  const squared = left * left;\n  const cubed = squared * left;\n  return 1 - cubed;\n}\n\nfunction poseOf(loom: Loom): Pose {\n  const { tension } = loom;\n  const restWaist = loom.height * WAIST_SHARE;\n  return {\n    waist: towards(restWaist, TIGHT_WAIST_PX, tension),\n    lead: towards(LEAD, TIGHT_LEAD, tension),\n    trail: towards(TRAIL, TIGHT_TRAIL, tension),\n    pull: tension * EDGE_PULL,\n  };\n}\n\nfunction edgeXOf(loom: Loom, side: number) {\n  if (side > 0) {\n    return -OVERHANG_PX;\n  }\n  return loom.width + OVERHANG_PX;\n}\n\n/** A point of a cubic curve on one axis, `along` the way from its start to its end. */\nfunction cubicOf(\n  start: number,\n  first: number,\n  second: number,\n  end: number,\n  along: number\n) {\n  const back = 1 - along;\n  const backSquared = back * back;\n  const alongSquared = along * along;\n  const startWeight = backSquared * back;\n  const firstWeight = 3 * backSquared * along;\n  const secondWeight = 3 * back * alongSquared;\n  const endWeight = alongSquared * along;\n  const fromStart = startWeight * start;\n  const fromFirst = firstWeight * first;\n  const fromSecond = secondWeight * second;\n  const fromEnd = endWeight * end;\n  return fromStart + fromFirst + fromSecond + fromEnd;\n}\n\n/** One thread as a cubic curve that leaves its edge level and reaches the target level, cut into points. */\nfunction shapeThread(loom: Loom, thread: number, pose: Pose) {\n  const { knot } = loom;\n  const side = loom.side[thread];\n  const startX = edgeXOf(loom, side);\n  const restY = loom.edge[thread] * loom.height;\n  const startY = towards(restY, knot.y, pose.pull);\n  const lift = loom.slot[thread] * pose.waist;\n  const endY = knot.y + lift;\n  const across = knot.x - startX;\n  const span = Math.abs(across);\n  const curled = pose.lead * loom.curl[thread];\n  const leadLength = span * curled;\n  const trailLength = span * pose.trail;\n  const leadStep = side * leadLength;\n  const trailStep = side * trailLength;\n  const firstX = startX + leadStep;\n  const secondX = knot.x - trailStep;\n  const base = thread * SAMPLES;\n  for (let sample = 0; sample < SAMPLES; sample += 1) {\n    const along = sample / LAST_SAMPLE;\n    const index = base + sample;\n    loom.x[index] = cubicOf(startX, firstX, secondX, knot.x, along);\n    loom.y[index] = cubicOf(startY, startY, endY, endY, along);\n  }\n}\n\nfunction shapeThreads(loom: Loom) {\n  const pose = poseOf(loom);\n  for (let thread = 0; thread < loom.count; thread += 1) {\n    shapeThread(loom, thread, pose);\n  }\n}\n\nfunction pushAway(loom: Loom, index: number, strength: number) {\n  const offsetX = loom.x[index] - loom.pointer.x;\n  const offsetY = loom.y[index] - loom.pointer.y;\n  const isFarAcross = Math.abs(offsetX) > PART_RADIUS_PX;\n  const isFarDown = Math.abs(offsetY) > PART_RADIUS_PX;\n  if (isFarAcross || isFarDown) {\n    return;\n  }\n  const distance = Math.hypot(offsetX, offsetY);\n  const isOutside = distance >= PART_RADIUS_PX;\n  const isOnIt = distance === 0;\n  if (isOutside || isOnIt) {\n    return;\n  }\n  const farness = distance / PART_RADIUS_PX;\n  const nearness = 1 - farness;\n  const falloff = nearness * nearness;\n  const push = falloff * strength;\n  const share = push / distance;\n  const pushX = offsetX * share;\n  const pushY = offsetY * share;\n  loom.x[index] += pushX;\n  loom.y[index] += pushY;\n}\n\n/**\n * The threads part around the pointer, pushed straight away from it. Near the target they are\n * drawn tight instead, so they part less the tighter they are.\n */\nfunction partAround(loom: Loom) {\n  const isAway = loom.presence < GONE;\n  if (isAway) {\n    return;\n  }\n  const looseness = 1 - loom.tension;\n  const reach = PART_PX * loom.presence;\n  const strength = reach * looseness;\n  for (let index = 0; index < loom.x.length; index += 1) {\n    pushAway(loom, index, strength);\n  }\n}\n\nfunction pointAt(loom: Loom, base: number, place: number): Point {\n  const lower = Math.floor(place);\n  const next = lower + 1;\n  const upper = Math.min(next, LAST_SAMPLE);\n  const share = place - lower;\n  const lowerIndex = base + lower;\n  const upperIndex = base + upper;\n  const x = towards(loom.x[lowerIndex], loom.x[upperIndex], share);\n  const y = towards(loom.y[lowerIndex], loom.y[upperIndex], share);\n  return { x, y };\n}\n\n/** Adds the stretch of a thread between two shares of its length, counted from its edge end, to the path. */\nfunction traceBetween(\n  pen: CanvasRenderingContext2D,\n  loom: Loom,\n  thread: number,\n  from: number,\n  to: number\n) {\n  const start = from * LAST_SAMPLE;\n  const end = to * LAST_SAMPLE;\n  if (end <= start) {\n    return;\n  }\n  const base = thread * SAMPLES;\n  const first = pointAt(loom, base, start);\n  const last = pointAt(loom, base, end);\n  const firstWhole = Math.floor(start);\n  const firstInside = firstWhole + 1;\n  pen.moveTo(first.x, first.y);\n  for (let sample = firstInside; sample < end; sample += 1) {\n    const index = base + sample;\n    pen.lineTo(loom.x[index], loom.y[index]);\n  }\n  pen.lineTo(last.x, last.y);\n}\n\n/** Every thread in one path, in the neutral of the theme. */\nfunction drawThreads(\n  pen: CanvasRenderingContext2D,\n  loom: Loom,\n  surface: Surface\n) {\n  const hairline = 1 / surface.pixelRatio;\n  pen.globalAlpha = THREAD_ALPHA;\n  pen.strokeStyle = loom.inks.line;\n  pen.lineWidth = hairline;\n  pen.beginPath();\n  for (let thread = 0; thread < loom.count; thread += 1) {\n    traceBetween(pen, loom, thread, 0, 1);\n  }\n  pen.stroke();\n}\n\nfunction envelopeOf(progress: number) {\n  const rising = smoothstep(0, LIGHT_IN, progress);\n  const fading = smoothstep(LIGHT_OUT, 1, progress);\n  const remaining = 1 - fading;\n  return rising * remaining;\n}\n\n/** One step of the tail of a light: the same stretch of every thread, at one strength, in one path. */\nfunction drawTailStep(\n  pen: CanvasRenderingContext2D,\n  loom: Loom,\n  head: number,\n  step: number,\n  strength: number\n) {\n  const offset = step * TAIL_STEP;\n  const from = head + offset;\n  const to = from + TAIL_STEP;\n  const end = Math.min(to, 1);\n  const fading = step / TAIL_STEPS;\n  const remaining = 1 - fading;\n  pen.globalAlpha = strength * remaining;\n  pen.beginPath();\n  for (let thread = 0; thread < loom.count; thread += 1) {\n    traceBetween(pen, loom, thread, from, end);\n  }\n  pen.stroke();\n}\n\n/** A light runs from the target out to the edges, quick at first, and fades as it arrives. */\nfunction drawTravellingLight(\n  pen: CanvasRenderingContext2D,\n  loom: Loom,\n  sentAt: number,\n  now: number\n) {\n  const age = now - sentAt;\n  const raw = age / PULSE_MS;\n  const progress = clamp(raw, 0, 1);\n  const travelled = easeOutOf(progress);\n  const head = 1 - travelled;\n  const envelope = envelopeOf(progress);\n  const strength = envelope * LIGHT_ALPHA;\n  for (let step = 0; step < TAIL_STEPS; step += 1) {\n    drawTailStep(pen, loom, head, step, strength);\n  }\n}\n\nfunction drawStillLight(\n  pen: CanvasRenderingContext2D,\n  loom: Loom,\n  sentAt: number,\n  now: number\n) {\n  const age = now - sentAt;\n  const raw = age / STILL_PULSE_MS;\n  const progress = clamp(raw, 0, 1);\n  const turn = progress * Math.PI;\n  const swell = Math.sin(turn);\n  pen.globalAlpha = swell * STILL_LIGHT_ALPHA;\n  pen.beginPath();\n  for (let thread = 0; thread < loom.count; thread += 1) {\n    traceBetween(pen, loom, thread, 0, 1);\n  }\n  pen.stroke();\n}\n\nfunction drawLightOf(isStill: boolean) {\n  if (isStill) {\n    return drawStillLight;\n  }\n  return drawTravellingLight;\n}\n\nfunction drawLights(pen: CanvasRenderingContext2D, loom: Loom, now: number) {\n  const drawLight = drawLightOf(loom.isStill);\n  pen.strokeStyle = loom.inks.hue;\n  pen.lineWidth = LIGHT_WIDTH_PX;\n  for (const sentAt of loom.pulses) {\n    drawLight(pen, loom, sentAt, now);\n  }\n}\n\n/** The whole picture as it stands: every thread, and every light on its way. */\nfunction drawLoom(loom: Loom, surface: Surface, now: number) {\n  const { pen } = surface;\n  keepKnotPlaced(loom, surface);\n  shapeThreads(loom);\n  partAround(loom);\n  wipeSurface(surface);\n  pen.lineCap = 'butt';\n  pen.lineJoin = 'round';\n  drawThreads(pen, loom, surface);\n  drawLights(pen, loom, now);\n}\n\nfunction pulseMsOf(isStill: boolean) {\n  if (isStill) {\n    return STILL_PULSE_MS;\n  }\n  return PULSE_MS;\n}\n\nfunction liveOf(pulses: number[], now: number, lasts: number) {\n  const live: number[] = [];\n  for (const sentAt of pulses) {\n    const age = now - sentAt;\n    const isLive = age < lasts;\n    if (isLive) {\n      live.push(sentAt);\n    }\n  }\n  return live;\n}\n\nfunction sendLight(loom: Loom, now: number) {\n  const isFull = loom.pulses.length >= MAX_PULSES;\n  if (isFull) {\n    loom.pulses.shift();\n  }\n  loom.pulses.push(now);\n}\n\n/** Every step draws the picture again. Once the pointer has settled and the lights are gone, the loop sleeps. */\nfunction stepLoom(loom: Loom, surface: Surface, seconds: number, now: number) {\n  keepKnotPlaced(loom, surface);\n  const isHandMoving = easeHand(loom, seconds);\n  const lasts = pulseMsOf(loom.isStill);\n  loom.pulses = liveOf(loom.pulses, now, lasts);\n  drawLoom(loom, surface, now);\n  const hasLight = loom.pulses.length > 0;\n  const isMoving = isHandMoving || hasLight;\n  if (isMoving) {\n    return NEXT_FRAME;\n  }\n  return AT_REST;\n}\n\ntype ThreadScene = Scene & {\n  /** The element the threads gather through. It is measured again before the next picture. */\n  aimAt: (target: HTMLElement | null) => void;\n  pulse: () => void;\n};\n\nfunction createThreadScene(count: number): ThreadScene {\n  const loom = loomOf(count);\n  return {\n    ink(inks) {\n      loom.inks = inks;\n    },\n    fit(surface, isStill) {\n      fitLoom(loom, surface, isStill);\n    },\n    draw(surface) {\n      const now = performance.now();\n      drawLoom(loom, surface, now);\n    },\n    step(surface, seconds, now) {\n      return stepLoom(loom, surface, seconds, now);\n    },\n    pointerMoved(x, y) {\n      loom.pointer.x = x;\n      loom.pointer.y = y;\n      loom.hasPointer = true;\n    },\n    pointerLeft() {\n      loom.hasPointer = false;\n    },\n    aimAt(target) {\n      loom.target = target;\n      loom.isKnotStale = true;\n    },\n    pulse() {\n      const now = performance.now();\n      sendLight(loom, now);\n    },\n  };\n}\n\nexport type ThreadsHandle = {\n  /** Sends a light out along every thread, from the target to the edges. */\n  pulse: () => void;\n};\n\nexport type ThreadsProps = {\n  className?: string;\n  /** Any colour token, such as `var(--primary)` or `var(--series-2)`. The light of a pulse wears it. */\n  hue?: string;\n  /** How many threads come in from each edge, from 3 to 16. */\n  count?: number;\n  /** The element the threads gather through, such as a call to action. Without it they gather in the middle of the box. */\n  target?: RefObject<HTMLElement | null>;\n  /** The threads tighten as the pointer nears the target, and part around the pointer. */\n  interactive?: boolean;\n  handleRef?: Ref<ThreadsHandle>;\n};\n\n/**\n * Hairline threads that come in from the left and right edges and gather through one element.\n * They are in place from the start, tighten as the pointer nears the target and part around the\n * pointer, and `pulse()`, through `handleRef`, sends a light out along every one. One canvas,\n * drawn only while something moves. It fills its parent, which must be positioned. Under reduced\n * motion the threads stand still, and a pulse lights them all at once and fades.\n */\nexport function Threads({\n  className,\n  hue = DEFAULT_HUE,\n  count = DEFAULT_COUNT,\n  target,\n  interactive = true,\n  handleRef,\n}: ThreadsProps) {\n  const stage = useRef<StageHandle>(null);\n  const scene = useMemo(() => createThreadScene(count), [count]);\n\n  useEffect(() => {\n    const element = target?.current ?? null;\n\n    function aim() {\n      scene.aimAt(element);\n      stage.current?.refresh();\n    }\n\n    aim();\n    if (!element) {\n      return;\n    }\n    const observer = new ResizeObserver(aim);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [scene, target]);\n\n  useImperativeHandle(\n    handleRef,\n    () => ({\n      pulse() {\n        scene.pulse();\n        stage.current?.refresh();\n      },\n    }),\n    [scene]\n  );\n\n  return (\n    <StageCanvas\n      handleRef={stage}\n      scene={scene}\n      hue={hue}\n      isInteractive={interactive}\n      className={className}\n    />\n  );\n}\n\n/** Long enough for a light to reach the edges and fade before the demonstration presses again. */\nconst BETWEEN_PULSES_MS = 2600;\n\nexport default function Background12() {\n  const threads = useRef<ThreadsHandle>(null);\n  const button = useRef<HTMLButtonElement>(null);\n  const { pressed, press } = usePress<'light'>();\n\n  function lightThreads() {\n    threads.current?.pulse();\n  }\n\n  const steps: AutoplayStep[] = [\n    { after: BETWEEN_PULSES_MS, run: () => press('light') },\n    { after: PRESS_LEAD_MS, run: lightThreads },\n  ];\n  const { ref } = useAutoplay<HTMLElement>(steps, { loopAfter: 0 });\n\n  return (\n    <DemoBand\n      ref={ref}\n      title='Threads that gather through one button'\n      background={<Threads target={button} handleRef={threads} />}\n    >\n      <Button\n        ref={button}\n        onClick={lightThreads}\n        className={pressedSolidOf(pressed === 'light')}\n      >\n        Light the threads\n      </Button>\n    </DemoBand>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/background/background-12.tsx"
    },
    {
      "path": "registry/ndeko/components/background/_parts/demo-band.tsx",
      "content": "import { useId, type ReactNode, type Ref } from 'react';\n\nimport { cn } from '@/registry/ndeko/lib/utils';\n\ntype BandAlign = 'center' | 'end';\n\nconst ALIGNMENTS: Record<BandAlign, string> = {\n  center: 'justify-center',\n  end: 'justify-end pb-10',\n};\n\ntype DemoBandProps = {\n  ref?: Ref<HTMLElement>;\n  title: string;\n  /** The background under test. It fills the band, behind the headline and the actions. */\n  background: ReactNode;\n  /** `end` sets the headline at the foot of the band and leaves its middle to the background. */\n  align?: BandAlign;\n  children: ReactNode;\n};\n\n/**\n * The band every background is shown in: the page colour, a headline in the foreground colour and\n * the actions laid straight over the background, to prove that text stays readable above it.\n */\nexport function DemoBand({\n  ref,\n  title,\n  background,\n  align = 'center',\n  children,\n}: DemoBandProps) {\n  const titleId = useId();\n\n  return (\n    <section\n      ref={ref}\n      aria-labelledby={titleId}\n      className={cn(\n        'bg-background @container relative isolate flex h-90 w-full flex-col items-center overflow-hidden rounded-xl',\n        ALIGNMENTS[align]\n      )}\n    >\n      {background}\n      <div className='relative flex max-w-lg flex-col items-center gap-5 px-6 text-center'>\n        <h3\n          id={titleId}\n          className='text-2xl font-semibold text-balance @lg:text-3xl'\n        >\n          {title}\n        </h3>\n        <div className='flex flex-wrap items-center justify-center gap-2'>\n          {children}\n        </div>\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/demo-band.tsx"
    },
    {
      "path": "registry/ndeko/components/background/_parts/frame-loop.ts",
      "content": "const FRAMES_PER_SECOND = 30;\n\nconst FRAME_MS = 1000 / FRAMES_PER_SECOND;\n\n/**\n * The timer between two frames ends this long before the next one is due, so the frame it asks for\n * lands on the screen refresh that is due and not on the one after.\n */\nconst REFRESH_LEAD_MS = 5;\n\nconst TIMER_MS = FRAME_MS - REFRESH_LEAD_MS;\n\n/** A frame that comes late, after a long task, moves the picture no further than this long would. */\nconst MAX_STEP_S = 0.05;\n\n/** What a frame answers while the picture still moves: the next one comes as soon as the cap allows. */\nexport const NEXT_FRAME = 0;\n\n/** What a frame answers once the picture has come to rest: nothing is asked for until `wake`. */\nexport const AT_REST = Number.POSITIVE_INFINITY;\n\n/** Draws one frame, `seconds` after the last one, and answers how many milliseconds it can wait for the next. */\nexport type FrameHandler = (seconds: number, now: number) => number;\n\nexport type FrameLoop = {\n  start: () => void;\n  stop: () => void;\n  wake: () => void;\n};\n\n/**\n * Frames at thirty a second at most. Between two frames there is one timer and no animation frame,\n * so a 120 Hz screen does not run four rounds of main-thread work for one frame drawn. Stopped or\n * at rest, the loop holds no timer and asks for no frame.\n */\nexport function createFrameLoop(onFrame: FrameHandler): FrameLoop {\n  let frame = 0;\n  let timer: ReturnType<typeof setTimeout> | undefined;\n  let isStarted = false;\n  let lastFrameAt = 0;\n\n  function clearPending() {\n    cancelAnimationFrame(frame);\n    clearTimeout(timer);\n    frame = 0;\n    timer = undefined;\n  }\n\n  function askForFrame() {\n    timer = undefined;\n    frame = requestAnimationFrame(tick);\n  }\n\n  function schedule(wait: number) {\n    clearPending();\n    const now = performance.now();\n    const sinceFrame = now - lastFrameAt;\n    const untilCap = TIMER_MS - sinceFrame;\n    const delay = Math.max(wait, untilCap);\n    if (delay <= 0) {\n      askForFrame();\n      return;\n    }\n    timer = setTimeout(askForFrame, delay);\n  }\n\n  function tick(now: number) {\n    frame = 0;\n    const elapsedMs = now - lastFrameAt;\n    const elapsed = elapsedMs / 1000;\n    const forward = Math.max(0, elapsed);\n    const seconds = Math.min(forward, MAX_STEP_S);\n    lastFrameAt = now;\n    const wait = onFrame(seconds, now);\n    const isResting = wait === AT_REST;\n    if (isResting || !isStarted) {\n      return;\n    }\n    schedule(wait);\n  }\n\n  function start() {\n    if (isStarted) {\n      return;\n    }\n    const now = performance.now();\n    isStarted = true;\n    lastFrameAt = now - FRAME_MS;\n    schedule(NEXT_FRAME);\n  }\n\n  function stop() {\n    isStarted = false;\n    clearPending();\n  }\n\n  function wake() {\n    const isFrameComing = frame !== 0;\n    if (!isStarted || isFrameComing) {\n      return;\n    }\n    schedule(NEXT_FRAME);\n  }\n\n  return { start, stop, wake };\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/frame-loop.ts"
    },
    {
      "path": "registry/ndeko/components/background/_parts/hue.ts",
      "content": "'use client';\n\nimport { useState } from 'react';\n\n/** A background wears one hue: the brand unless it is told otherwise. */\nexport const DEFAULT_HUE = 'var(--primary)';\n\n/** The brand first, then the series hues that differ from it. */\nconst DEMO_HUES = [\n  DEFAULT_HUE,\n  'var(--series-2)',\n  'var(--series-3)',\n  'var(--series-4)',\n  'var(--series-5)',\n];\n\n/** The hue of a demo, and the step to the next one, to show a background in each hue it accepts. */\nexport function useHueCycle() {\n  const [index, setIndex] = useState(0);\n  const hue = DEMO_HUES[index] ?? DEFAULT_HUE;\n\n  function nextHue() {\n    setIndex((current) => {\n      const next = current + 1;\n      return next % DEMO_HUES.length;\n    });\n  }\n\n  return { hue, nextHue };\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/hue.ts"
    },
    {
      "path": "registry/ndeko/components/background/_parts/math.ts",
      "content": "export type Point = {\n  x: number;\n  y: number;\n};\n\nexport function clamp(value: number, low: number, high: number) {\n  const raised = Math.max(low, value);\n  return Math.min(high, raised);\n}\n\n/** How much of the way left is covered in `seconds`: the same glide whatever the frame rate. */\nexport function shareOf(seconds: number, easeSeconds: number) {\n  const ratio = seconds / easeSeconds;\n  const decay = Math.exp(-ratio);\n  return 1 - decay;\n}\n\nexport function towards(from: number, to: number, share: number) {\n  const gap = to - from;\n  const step = gap * share;\n  return from + step;\n}\n\n/** From 0 at `low` to 1 at `high`, leaving and arriving without a corner. */\nexport function smoothstep(low: number, high: number, value: number) {\n  const span = high - low;\n  const offset = value - low;\n  const raw = offset / span;\n  const share = clamp(raw, 0, 1);\n  const doubled = 2 * share;\n  const curve = 3 - doubled;\n  const squared = share * share;\n  return squared * curve;\n}\n\n/** The damping that brings a spring of this stiffness to its target without passing it. */\nexport function dampingOf(stiffness: number) {\n  const root = Math.sqrt(stiffness);\n  return 2 * root;\n}\n\n/**\n * The speed of a spring one step later. The caller then moves the value by that speed over the\n * same step, so the value follows its target and settles on it.\n */\nexport function springSpeedOf(\n  gap: number,\n  speed: number,\n  stiffness: number,\n  damping: number,\n  seconds: number\n) {\n  const pull = gap * stiffness;\n  const drag = speed * damping;\n  const push = pull - drag;\n  const gain = push * seconds;\n  return speed + gain;\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/math.ts"
    },
    {
      "path": "registry/ndeko/components/background/_parts/pointer.ts",
      "content": "import type { Point } from './math';\n\n/**\n * Where a point of the window falls inside `host`, in the pixels of its own box, or null when it\n * falls outside. A host that an ancestor scales still answers in its own pixels.\n */\nexport function placeIn(\n  host: HTMLElement,\n  clientX: number,\n  clientY: number\n): Point | null {\n  const box = host.getBoundingClientRect();\n  const isEmpty = box.width === 0 || box.height === 0;\n  if (isEmpty) {\n    return null;\n  }\n  const isInsideX = clientX >= box.left && clientX <= box.right;\n  const isInsideY = clientY >= box.top && clientY <= box.bottom;\n  if (!isInsideX || !isInsideY) {\n    return null;\n  }\n  const scaleX = host.offsetWidth / box.width;\n  const scaleY = host.offsetHeight / box.height;\n  const offsetX = clientX - box.left;\n  const offsetY = clientY - box.top;\n  const x = offsetX * scaleX;\n  const y = offsetY * scaleY;\n  return { x, y };\n}\n\n/** Where the centre of `element` falls inside `host`, or null when it falls outside. */\nexport function centreIn(host: HTMLElement, element: Element) {\n  const box = element.getBoundingClientRect();\n  const halfWidth = box.width / 2;\n  const halfHeight = box.height / 2;\n  const centreX = box.left + halfWidth;\n  const centreY = box.top + halfHeight;\n  return placeIn(host, centreX, centreY);\n}\n\n/**\n * Follows a pointer that can hover, from the window: content laid over a background takes the\n * pointer events of its own box, so a listener on the background would go blind under a headline.\n * The place is read once a frame at most, and only on a frame where the pointer moved. `onMove`\n * gets the place inside `host`; `onLeave` is called once when the pointer goes out of it. The\n * function returned ends the watch, and lets go of a pointer that was still over the host.\n */\nexport function watchPointer(\n  host: HTMLElement,\n  onMove: (x: number, y: number) => void,\n  onLeave: () => void\n) {\n  let clientX = 0;\n  let clientY = 0;\n  let frame = 0;\n  let isOver = false;\n\n  function leave() {\n    if (!isOver) {\n      return;\n    }\n    isOver = false;\n    onLeave();\n  }\n\n  function read() {\n    frame = 0;\n    const place = placeIn(host, clientX, clientY);\n    if (!place) {\n      leave();\n      return;\n    }\n    isOver = true;\n    onMove(place.x, place.y);\n  }\n\n  function onPointerMove(event: PointerEvent) {\n    const isTouch = event.pointerType === 'touch';\n    if (isTouch) {\n      return;\n    }\n    clientX = event.clientX;\n    clientY = event.clientY;\n    const isReadComing = frame !== 0;\n    if (isReadComing) {\n      return;\n    }\n    frame = requestAnimationFrame(read);\n  }\n\n  function onPointerGone() {\n    cancelAnimationFrame(frame);\n    frame = 0;\n    leave();\n  }\n\n  const root = document.documentElement;\n  window.addEventListener('pointermove', onPointerMove, { passive: true });\n  root.addEventListener('pointerleave', onPointerGone, { passive: true });\n\n  return () => {\n    window.removeEventListener('pointermove', onPointerMove);\n    root.removeEventListener('pointerleave', onPointerGone);\n    onPointerGone();\n  };\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/pointer.ts"
    },
    {
      "path": "registry/ndeko/components/background/_parts/seeded.ts",
      "content": "const STREAM_STEP = 0x6d2b79f5;\n\nconst UINT32_RANGE = 4294967296;\n\nconst FIRST_PRIME = 73856093;\n\nconst SECOND_PRIME = 19349663;\n\n/**\n * A stream of numbers from 0 to 1, the same for the same seed on the server and in the browser, so\n * a generated picture never changes between two renders.\n */\nexport function randomOf(seed: number) {\n  let state = seed >>> 0;\n\n  function next() {\n    const stepped = state + STREAM_STEP;\n    state = stepped >>> 0;\n    const shifted = state >>> 15;\n    const folded = state ^ shifted;\n    const odd = state | 1;\n    const first = Math.imul(folded, odd);\n    const reshifted = first >>> 7;\n    const refolded = first ^ reshifted;\n    const salted = first | 61;\n    const second = Math.imul(refolded, salted);\n    const summed = first + second;\n    const mixed = first ^ summed;\n    const lastShift = mixed >>> 14;\n    const settled = mixed ^ lastShift;\n    const positive = settled >>> 0;\n    return positive / UINT32_RANGE;\n  }\n\n  return next;\n}\n\n/** One seed out of two numbers, so each part of a picture draws from a stream of its own. */\nexport function seedOf(first: number, second: number) {\n  const firstHash = Math.imul(first, FIRST_PRIME);\n  const secondHash = Math.imul(second, SECOND_PRIME);\n  const mixed = firstHash ^ secondHash;\n  return mixed >>> 0;\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/seeded.ts"
    },
    {
      "path": "registry/ndeko/components/background/_parts/showing.ts",
      "content": "/**\n * A background sits behind content for as long as the page is open, so everything it runs hangs on\n * this answer: its box touches the screen and the tab is the one looked at. `onChange` is called\n * each time the answer changes, starting from \"not showing\". The function returned ends the watch.\n */\nexport function watchShowing(\n  element: Element,\n  onChange: (isShowing: boolean) => void\n) {\n  let isOnScreen = false;\n  let wasShowing = false;\n\n  function report() {\n    const isTabShown = document.visibilityState === 'visible';\n    const isShowing = isOnScreen && isTabShown;\n    if (isShowing === wasShowing) {\n      return;\n    }\n    wasShowing = isShowing;\n    onChange(isShowing);\n  }\n\n  function onCrossing(entries: IntersectionObserverEntry[]) {\n    const latest = entries.at(-1);\n    if (!latest) {\n      return;\n    }\n    isOnScreen = latest.isIntersecting;\n    report();\n  }\n\n  const observer = new IntersectionObserver(onCrossing);\n  observer.observe(element);\n  document.addEventListener('visibilitychange', report);\n\n  return () => {\n    observer.disconnect();\n    document.removeEventListener('visibilitychange', report);\n  };\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/showing.ts"
    },
    {
      "path": "registry/ndeko/components/background/_parts/stage-canvas.tsx",
      "content": "'use client';\n\nimport { useEffect, useImperativeHandle, useRef, type Ref } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { cn } from '@/registry/ndeko/lib/utils';\n\nimport type { Point } from './math';\nimport { mountStage, type Scene, type Stage } from './stage';\n\nexport type StageHandle = {\n  /** Something outside the loop changed the scene: it is drawn again, and its loop is woken. */\n  refresh: () => void;\n  /** Where the centre of an element sits on the canvas, or null when it is outside. */\n  centreOf: (element: Element) => Point | null;\n};\n\ntype StageCanvasProps = {\n  scene: Scene;\n  hue: string;\n  isInteractive?: boolean;\n  className?: string;\n  handleRef?: Ref<StageHandle>;\n};\n\n/**\n * The one canvas of a background. It fills its parent, lets every pointer event through and is\n * hidden from assistive technology, since it is decoration. A canvas cannot take a colour from a\n * class: the hue rides on its `color` and the neutral of lines on `decoration-foreground`, and the\n * stage reads both from the computed style. No transition may sit on those two, or the stage\n * would read a colour halfway through it.\n */\nexport function StageCanvas({\n  scene,\n  hue,\n  isInteractive = false,\n  className,\n  handleRef,\n}: StageCanvasProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const stage = useRef<Stage | null>(null);\n  const shouldReduceMotion = usePrefersReducedMotion();\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n    const mounted = mountStage(canvas, scene, {\n      isStill: shouldReduceMotion,\n      isInteractive,\n    });\n    stage.current = mounted;\n    return () => {\n      mounted?.dispose();\n      stage.current = null;\n    };\n  }, [scene, isInteractive, shouldReduceMotion]);\n\n  useEffect(() => {\n    stage.current?.reink();\n  }, [hue]);\n\n  useImperativeHandle(\n    handleRef,\n    () => ({\n      refresh: () => stage.current?.refresh(),\n      centreOf: (element) => stage.current?.centreOf(element) ?? null,\n    }),\n    []\n  );\n\n  return (\n    <canvas\n      ref={canvasRef}\n      aria-hidden='true'\n      style={{ color: hue }}\n      className={cn(\n        'decoration-foreground pointer-events-none absolute inset-0 size-full',\n        className\n      )}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/stage-canvas.tsx"
    },
    {
      "path": "registry/ndeko/components/background/_parts/stage.ts",
      "content": "import { createFrameLoop } from './frame-loop';\nimport type { Point } from './math';\nimport { centreIn, watchPointer } from './pointer';\nimport { watchShowing } from './showing';\nimport {\n  createSurface,\n  fitSurface,\n  hasSameInks,\n  inksOf,\n  isFitted,\n  watchSurface,\n  type Inks,\n  type Surface,\n} from './surface';\n\n/** What a background draws on its canvas. The stage decides when; the scene only knows how. */\nexport type Scene = {\n  /** The colours changed: the theme, or the hue. */\n  ink: (inks: Inks) => void;\n  /**\n   * The box changed, and the canvas was wiped: what depends on its size is built again. `isStill`\n   * is the reduced motion setting: the scene then holds its most telling picture and never moves.\n   */\n  fit: (surface: Surface, isStill: boolean) => void;\n  /** The whole picture as it stands. */\n  draw: (surface: Surface) => void;\n  /**\n   * Moves the picture on by `seconds` and draws what changed. It answers how long it can wait for\n   * its next frame: `NEXT_FRAME`, `AT_REST`, or a number of milliseconds.\n   */\n  step: (surface: Surface, seconds: number, now: number) => number;\n  pointerMoved?: (x: number, y: number) => void;\n  pointerLeft?: () => void;\n};\n\nexport type Stage = {\n  /** Something outside the loop changed the scene: it is drawn again, and its loop is woken. */\n  refresh: () => void;\n  /** Reads the colours again, and draws again when they changed. */\n  reink: () => void;\n  /** Where the centre of an element sits on the canvas, or null when it is outside. */\n  centreOf: (element: Element) => Point | null;\n  dispose: () => void;\n};\n\nexport type StageOptions = {\n  /** Reduced motion: no pointer is followed and nothing moves by itself. */\n  isStill: boolean;\n  isInteractive: boolean;\n};\n\nfunction idle() {}\n\nfunction stageOf(\n  surface: Surface,\n  scene: Scene,\n  { isStill, isInteractive }: StageOptions\n): Stage {\n  const { canvas } = surface;\n  const followsPointer = isInteractive && !isStill;\n  let isShowing = false;\n  let stopPointer = idle;\n  let inks = inksOf(canvas);\n\n  const loop = createFrameLoop((seconds, now) =>\n    scene.step(surface, seconds, now)\n  );\n\n  function wake() {\n    if (isShowing) {\n      loop.wake();\n    }\n  }\n\n  function lay() {\n    fitSurface(surface);\n    scene.fit(surface, isStill);\n    scene.draw(surface);\n  }\n\n  function onPointerMove(x: number, y: number) {\n    scene.pointerMoved?.(x, y);\n    wake();\n  }\n\n  function onPointerLeave() {\n    scene.pointerLeft?.();\n    wake();\n  }\n\n  function show() {\n    isShowing = true;\n    if (followsPointer) {\n      stopPointer = watchPointer(canvas, onPointerMove, onPointerLeave);\n    }\n    loop.start();\n  }\n\n  function hide() {\n    isShowing = false;\n    stopPointer();\n    stopPointer = idle;\n    loop.stop();\n  }\n\n  function onShowing(isNowShowing: boolean) {\n    if (isNowShowing) {\n      show();\n      return;\n    }\n    hide();\n  }\n\n  function onResize() {\n    if (isFitted(surface)) {\n      return;\n    }\n    lay();\n    wake();\n  }\n\n  function reink() {\n    const latest = inksOf(canvas);\n    if (hasSameInks(inks, latest)) {\n      return;\n    }\n    inks = latest;\n    scene.ink(inks);\n    scene.draw(surface);\n    wake();\n  }\n\n  /** In the loop, the next frame draws the change. Out of it, nothing would, so it is drawn here. */\n  function refresh() {\n    const isOutOfLoop = isStill || !isShowing;\n    if (isOutOfLoop) {\n      scene.draw(surface);\n    }\n    wake();\n  }\n\n  scene.ink(inks);\n  lay();\n  const stopSurface = watchSurface(canvas, onResize, reink);\n  const stopShowing = watchShowing(canvas, onShowing);\n\n  return {\n    refresh,\n    reink,\n    centreOf: (element) => centreIn(canvas, element),\n    dispose() {\n      stopShowing();\n      stopSurface();\n      hide();\n    },\n  };\n}\n\n/**\n * Puts a scene on a canvas. The stage sizes the canvas for the screen, reads its colours, and runs\n * the loop of the scene only while the canvas is on screen in a visible tab. It draws one frame\n * when it is set up, so the picture is there before anything moves.\n */\nexport function mountStage(\n  canvas: HTMLCanvasElement,\n  scene: Scene,\n  options: StageOptions\n) {\n  const surface = createSurface(canvas);\n  if (!surface) {\n    return null;\n  }\n  return stageOf(surface, scene, options);\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/stage.ts"
    },
    {
      "path": "registry/ndeko/components/background/_parts/surface.ts",
      "content": "/** A backing store above two device pixels a point costs four times the fill for a picture nobody sees sharper. */\nconst MAX_PIXEL_RATIO = 2;\n\nconst THEME_WATCH = {\n  attributes: true,\n  attributeFilter: ['class', 'data-theme'],\n};\n\nconst DARK_SCHEME = '(prefers-color-scheme: dark)';\n\nexport type Surface = {\n  canvas: HTMLCanvasElement;\n  pen: CanvasRenderingContext2D;\n  /** The box of the canvas in CSS pixels: one unit drawn is one of them, whatever the screen. */\n  width: number;\n  height: number;\n  pixelRatio: number;\n};\n\nexport type Inks = {\n  /** The hue of the background, resolved by the browser for the current theme. */\n  hue: string;\n  /** The neutral of lines: the foreground of the theme. */\n  line: string;\n};\n\nexport function createSurface(canvas: HTMLCanvasElement): Surface | null {\n  const pen = canvas.getContext('2d');\n  if (!pen) {\n    return null;\n  }\n  return { canvas, pen, width: 0, height: 0, pixelRatio: 1 };\n}\n\nfunction pixelRatioOf() {\n  const devicePixelRatio = window.devicePixelRatio || 1;\n  return Math.min(devicePixelRatio, MAX_PIXEL_RATIO);\n}\n\n/** Sizes the backing store to the box of the canvas and to the screen. It also wipes the canvas. */\nexport function fitSurface(surface: Surface) {\n  const { canvas, pen } = surface;\n  const pixelRatio = pixelRatioOf();\n  const width = canvas.clientWidth;\n  const height = canvas.clientHeight;\n  const deviceWidth = width * pixelRatio;\n  const deviceHeight = height * pixelRatio;\n  surface.width = width;\n  surface.height = height;\n  surface.pixelRatio = pixelRatio;\n  canvas.width = Math.round(deviceWidth);\n  canvas.height = Math.round(deviceHeight);\n  pen.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);\n}\n\nexport function isFitted(surface: Surface) {\n  const { canvas } = surface;\n  const hasWidth = surface.width === canvas.clientWidth;\n  const hasHeight = surface.height === canvas.clientHeight;\n  const hasRatio = surface.pixelRatio === pixelRatioOf();\n  const hasSize = hasWidth && hasHeight;\n  return hasSize && hasRatio;\n}\n\nexport function wipeSurface(surface: Surface) {\n  surface.pen.globalAlpha = 1;\n  surface.pen.clearRect(0, 0, surface.width, surface.height);\n}\n\n/**\n * A canvas cannot take a colour from a class, so it reads its colours from its own computed style,\n * where the browser has already resolved the tokens for the current theme. The hue rides on\n * `color` and the neutral of lines on `text-decoration-color`, which paints nothing on a canvas.\n */\nexport function inksOf(canvas: HTMLCanvasElement): Inks {\n  const style = getComputedStyle(canvas);\n  return { hue: style.color, line: style.textDecorationColor };\n}\n\nexport function hasSameInks(first: Inks, second: Inks) {\n  const hasSameHue = first.hue === second.hue;\n  const hasSameLine = first.line === second.line;\n  return hasSameHue && hasSameLine;\n}\n\nfunction densityQueryOf() {\n  return `(resolution: ${window.devicePixelRatio}dppx)`;\n}\n\n/**\n * Calls `onResize` when the box of the canvas or the density of the screen changes, and `onTheme`\n * when the page may have changed theme: a class or an attribute on the root or the body, or the\n * system setting. The function returned ends the watch.\n */\nexport function watchSurface(\n  canvas: HTMLCanvasElement,\n  onResize: () => void,\n  onTheme: () => void\n) {\n  const sizeObserver = new ResizeObserver(() => onResize());\n  const themeObserver = new MutationObserver(() => onTheme());\n  const scheme = window.matchMedia(DARK_SCHEME);\n  let density = window.matchMedia(densityQueryOf());\n\n  function onScheme() {\n    onTheme();\n  }\n\n  /** The query names one density: once the screen has left it, a new query watches the next one. */\n  function onDensity() {\n    density.removeEventListener('change', onDensity);\n    density = window.matchMedia(densityQueryOf());\n    density.addEventListener('change', onDensity);\n    onResize();\n  }\n\n  sizeObserver.observe(canvas);\n  themeObserver.observe(document.documentElement, THEME_WATCH);\n  themeObserver.observe(document.body, THEME_WATCH);\n  scheme.addEventListener('change', onScheme);\n  density.addEventListener('change', onDensity);\n\n  return () => {\n    sizeObserver.disconnect();\n    themeObserver.disconnect();\n    scheme.removeEventListener('change', onScheme);\n    density.removeEventListener('change', onDensity);\n  };\n}\n",
      "type": "registry:component",
      "target": "components/background/_parts/surface.ts"
    }
  ],
  "categories": [
    "components",
    "background"
  ],
  "type": "registry:component"
}