{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "noise-gate",
  "type": "registry:ui",
  "title": "Noise Gate",
  "description": "A before-and-after section. Alerts, pings and complaints drift into the seam between two panels and slide under the calm one, and each card that goes through lights the seam and ticks a counter, or lands as a tidy line in a log.",
  "author": "KeyframeUI <https://keyframeui.com>",
  "categories": [
    "section",
    "comparison",
    "counter",
    "marketing"
  ],
  "docs": "Live preview, examples and props: https://keyframeui.com/components/noise-gate",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "components/ui/noise-gate.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Keep:\n * - One rAF loop moves every card by writing `transform` and `opacity` straight to the DOM and\n *   redraws the traces; the stream never re-renders, and the loop sleeps when stopped or off\n *   screen.\n * - Arrivals are scheduled: each new card starts where it reaches the seam about one beat after\n *   the card before it, give or take the jitter, so ticks keep a rhythm instead of bunching.\n * - Hover and `paused` ease the whole stream to a stop through one shared factor, never a snap,\n *   and the card under the pointer comes forward on top of the rest.\n * - Digits are keyed from the right and roll in `popLayout`, so a new leading digit doesn't re-roll\n *   the rest; the count reads from an `sr-only` span while the moving parts are `aria-hidden`.\n * - Reduced motion shows a seeded, still scatter of cards and traces: no stream and no ticks.\n */\nconst TIMING = {\n  /** A mid-depth card crossing the noise panel, in seconds (the `duration` prop). */\n  duration: 15,\n  /** How fast far, mid and near cards drift, as multiples of a mid-depth card's pace. */\n  depthSpeed: [0.72, 1, 1.28],\n  /** The stream easing to a stop and starting again, a time constant in seconds (the `settle` prop). */\n  settle: 0.32,\n  /** A new card appearing, in seconds. */\n  fadeIn: 0.9,\n  /** Between the first cards appearing, in seconds. */\n  stagger: 0.11,\n  /** A hovered card coming forward, as a time constant in seconds. */\n  emphasis: 0.12,\n  /** The least a new card drifts before it reaches the seam, in seconds. */\n  minRun: 2.2,\n  /** Between arrivals: this share of a full crossing, split between the cards. */\n  beat: 0.9,\n  /** The shortest beat, in seconds. */\n  minBeat: 0.35,\n  /** ± share of a beat each arrival moves by, so ticks don't land like a metronome. */\n  jitter: 0.4,\n  /** One bob of a card across its lane: the shortest period in seconds, and up to this many more. */\n  bobPeriod: 5,\n  bobVary: 3,\n  /** Each trace's three waves moving, in rad/s. */\n  traceSpeed: [\n    [1.1, -1.9, 3.3],\n    [-0.8, 2.4, -3.9],\n    [1.6, -2.8, 4.6],\n  ],\n  /** The seam lighting up where a card went through: seconds, keyframe times and curve. */\n  flash: { duration: 0.9, times: [0, 0.14, 1], ease: \"easeOut\" },\n  /** The routed dot running along the seam and the horizon, in px/s. */\n  route: 460,\n  /** The shortest and longest the routed dot takes, in seconds. */\n  routeDuration: [0.4, 1.1],\n  /** The routed dot speeding up down the seam and easing out along the horizon, per leg. */\n  routeEase: [\"easeIn\", \"easeOut\"],\n  /** From the edge light to the tick when stacked, in seconds. */\n  tickDelay: 0.42,\n  /** The pill brightening once on a tick: seconds, keyframe times and curve. */\n  glow: { duration: 0.9, times: [0, 0.15, 1], ease: \"easeOut\" },\n  /** The fainter dot leaving on the far side: this many times as long as the routed dot would take. */\n  exitPace: 1.6,\n  /** The shortest and longest the fainter dot takes, in seconds. */\n  exitDuration: [0.4, 1.2],\n  /** The fainter dot's curve, as a motion easing. */\n  exitEase: \"easeOut\",\n  /** A tick with no fainter dot lingers this long before it's cleared, in seconds. */\n  linger: 0.6,\n  /** The counter's changed digits rolling in, in seconds (the `rollDuration` prop). */\n  rollDuration: 0.42,\n  /** Counter digits, as a spring over `rollDuration`. */\n  roll: { type: \"spring\", bounce: 0 },\n  /** Log rows sliding, as a spring. */\n  row: { type: \"spring\", visualDuration: 0.45, bounce: 0 },\n} as const;\n\n/* ─────────────────────────────────────────────────────────\n * NOISE GATE STORYBOARD\n *\n *  enter     cards fade in one after another across the noise\n *            panel, at three depths: far ones small, faint and\n *            slow, near ones crisp and quicker\n *  drift     every card heads for the seam on its own beat,\n *            bobbing a few px across its lane. Near cards pass\n *            over far ones\n *  gate      a card's leading edge touches the seam: the calm\n *            panel's edge lights up along it, and the card slides\n *            on under the calm panel and is gone. A new card\n *            fades in somewhere behind\n *  route     a dot runs along the seam to the horizon and turns\n *            into the counter (or the log)\n *  tick      the counter's changed digits roll up and the pill\n *            brightens once; in the log, the card's resolved line\n *            slides in on top. A fainter dot leaves on the far side\n *  traces    behind the cards three jittery traces converge on\n *            the horizon at the seam, where the calm side's single\n *            dotted line takes over\n *  hover     pointing at the noise eases everything to a stop so\n *            a card can be read; the card under the pointer comes\n *            forward at full strength\n *  stacked   under 40rem the panels stack: cards fall into the\n *            calm panel's top edge, which lights up, and the tick\n *            follows without the routed dot\n *  reduced   a still scatter of cards and traces; nothing ticks\n * ───────────────────────────────────────────────────────── */\n\nconst GATE = {\n  // far, mid, near\n  depths: [\n    { scale: 0.86, opacity: 0.38 },\n    { scale: 0.94, opacity: 0.72 },\n    { scale: 1, opacity: 1 },\n  ],\n  depthOrder: [2, 0, 1, 2, 1, 0, 2, 1, 0],\n  horizon: 0.6, // share of the panel's height the horizon sits at, side by side\n  overhang: 0.45, // share of a card that may start past the far edge (under the title, stacked)\n  edge: 16, // px cards keep from the panel's outer edges\n  crowd: 0.95, // cards on screen per card-sized area of the field\n  logRows: 6,\n} as const;\n\n// Three traces, each a sum of three waves: wavenumber (rad/px). Their speeds are `TIMING.traceSpeed`.\nconst TRACES = [\n  { amp: 1, k: [0.034, 0.092, 0.21], phase: 0.3, tone: \"text-rose-500/30 dark:text-rose-400/25\" },\n  { amp: 0.74, k: [0.05, 0.13, 0.27], phase: 2.1, tone: \"text-amber-500/35 dark:text-amber-400/25\" },\n  { amp: 0.56, k: [0.028, 0.16, 0.33], phase: 4.4, tone: \"text-zinc-400/55 dark:text-zinc-500/55\" },\n] as const;\n\nconst TONE = {\n  critical: {\n    tag: \"bg-rose-500/10 text-rose-700 dark:bg-rose-400/15 dark:text-rose-300\",\n    icon: \"text-rose-500 dark:text-rose-400\",\n  },\n  warning: {\n    tag: \"bg-amber-500/15 text-amber-800 dark:bg-amber-400/15 dark:text-amber-200\",\n    icon: \"text-amber-500 dark:text-amber-400\",\n  },\n  neutral: {\n    tag: \"bg-zinc-950/[0.06] text-zinc-600 dark:bg-white/10 dark:text-zinc-300\",\n    icon: \"text-zinc-400 dark:text-zinc-500\",\n  },\n} as const;\n\nconst SURFACE =\n  \"rounded-xl border border-zinc-950/[0.08] bg-white/85 shadow-[0_1px_2px_rgb(9_9_11/0.05),0_12px_28px_-16px_rgb(9_9_11/0.3)] dark:border-white/10 dark:bg-zinc-900/85 dark:shadow-[0_1px_2px_rgb(0_0_0/0.4),0_12px_28px_-16px_rgb(0_0_0/0.8)]\";\n\nconst DOTTED: React.CSSProperties = {\n  backgroundImage: \"radial-gradient(circle, currentColor 1px, transparent 1.3px)\",\n  backgroundSize: \"7px 3px\",\n  backgroundRepeat: \"repeat-x\",\n};\n\nconst TAU = Math.PI * 2;\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\nconst smoothstep = (a: number, b: number, v: number) => {\n  const t = clamp((v - a) / (b - a), 0, 1);\n  return t * t * (3 - 2 * t);\n};\nconst easeOut = (t: number) => 1 - (1 - t) ** 3;\n\n/** Small seeded generator, so the still scatter is the same on every visit. */\nfunction seeded(seed: number) {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nexport type NoiseGateTone = \"critical\" | \"warning\" | \"neutral\";\n\nexport interface NoiseGateItem {\n  /** The card's main line. */\n  title: React.ReactNode;\n  /** A smaller, muted second line. */\n  detail?: React.ReactNode;\n  /** A short label in a pill before the title, e.g. \"P1\" or \"Overdue\". */\n  tag?: React.ReactNode;\n  /** Colors the tag and the icon. */\n  tone?: NoiseGateTone;\n  /** A leading icon or avatar, about 20px. */\n  icon?: React.ReactNode;\n  /** Set the title in monospace, for log lines, file names and codes. */\n  mono?: boolean;\n  /** Stack this many copies, like an alert that keeps firing. */\n  repeat?: number;\n  /** How much the counter goes up when this card reaches the gate. */\n  weight?: number;\n  /** With `display=\"log\"`: the line this card becomes once it's through. */\n  resolved?: React.ReactNode;\n  /** With `display=\"log\"`: a right-aligned value beside `resolved`, in tabular figures. */\n  resolvedDetail?: React.ReactNode;\n}\n\nexport interface NoiseGateProps {\n  /** The cards that drift into the gate, cycled in order. */\n  items: NoiseGateItem[];\n  /** Heading of the noise side. */\n  noiseTitle: React.ReactNode;\n  /** Heading of the calm side. */\n  calmTitle: React.ReactNode;\n  /** What the calm side shows: a counter pill, or a log of resolved lines. */\n  display?: \"count\" | \"log\";\n  /** The counter's starting value. Each card adds its `weight` (1) when it reaches the gate. */\n  count?: number;\n  /** Text before the number in the pill. */\n  countPrefix?: React.ReactNode;\n  /** Text after the number in the pill. */\n  countSuffix?: React.ReactNode;\n  /** Pinned to the bottom of the calm side: a call to action or a footnote. */\n  calmFooter?: React.ReactNode;\n  /** Most cards on screen at once. Fewer show when the panel is small. */\n  density?: number;\n  /** Seconds a mid-depth card takes to cross the noise panel. */\n  duration?: number;\n  /** How far each card floats across its lane as it drifts, in px. */\n  bob?: number;\n  /** How quickly the stream eases to a stop under the pointer and gets going again: a time constant in seconds. */\n  settle?: number;\n  /** Seconds the counter's changed digits take to roll in. */\n  rollDuration?: number;\n  /** Ease the stream to a stop. It also stops while the pointer is over the noise. */\n  paused?: boolean;\n  /** Element for both headings. */\n  titleAs?: \"h2\" | \"h3\" | \"h4\" | \"p\";\n  /** Height while the panels sit side by side. Stacked, each panel sizes itself. */\n  height?: string;\n  /** Classes for the root. Set `[--noise-gate-radius:…]` to change the corner radius. */\n  className?: string;\n  /** Classes for the noise panel. */\n  noiseClassName?: string;\n  /** Classes for the calm panel: its background and text color. */\n  calmClassName?: string;\n}\n\ntype Slot = {\n  item: number;\n  depth: number;\n  /** px along the drift, from the far edge toward the seam (the box's start). */\n  a: number;\n  /** px across the drift (the box's start). */\n  c: number;\n  /** Stream seconds the card was born at; it fades in from there. */\n  born: number;\n  phase: number;\n  period: number;\n  gated: boolean;\n  /** 0..1, how far the card has come forward under the pointer. */\n  emph: number;\n};\n\ntype Geometry = {\n  stacked: boolean;\n  w: number;\n  h: number;\n  /** Along coordinate of the seam. */\n  gate: number;\n  /** Along coordinate where cards may start. */\n  start: number;\n  /** Cross range for a card's box. */\n  lo: number;\n  hi: number;\n  horizon: number;\n  /** Per item: along and cross size. */\n  sizes: { a: number; c: number }[];\n};\n\ntype GateEvent = { item: number; from: number; to: number; stacked: boolean };\ntype CalmApi = { gate: (event: GateEvent) => void };\n\ntype Pulse = {\n  id: number;\n  item: number;\n  weight: number;\n  stacked: boolean;\n  from: number;\n  to: number;\n  route: { x: number[]; y: number[]; times: number[]; duration: number } | null;\n  exit: { x0: number; x1: number; y: number } | null;\n};\n\nexport function NoiseGate({\n  items,\n  noiseTitle,\n  calmTitle,\n  display = \"count\",\n  count = 0,\n  countPrefix,\n  countSuffix,\n  calmFooter,\n  density = 9,\n  duration = TIMING.duration,\n  bob = 3,\n  settle = TIMING.settle,\n  rollDuration = TIMING.rollDuration,\n  paused = false,\n  titleAs = \"h3\",\n  height = \"28rem\",\n  className,\n  noiseClassName,\n  calmClassName,\n}: NoiseGateProps) {\n  const reduced = useReducedMotion() ?? false;\n  const noiseRef = React.useRef<HTMLDivElement>(null);\n  const calmRef = React.useRef<HTMLDivElement>(null);\n  const titleRef = React.useRef<HTMLHeadingElement>(null);\n  const layerRef = React.useRef<HTMLDivElement>(null);\n  const cardRefs = React.useRef<(HTMLDivElement | null)[]>([]);\n  const traceRefs = React.useRef<(SVGPathElement | null)[]>([]);\n  const calmApi = React.useRef<CalmApi>(null);\n\n  const hovering = React.useRef(false);\n  const hovered = React.useRef(-1);\n  const pausedRef = React.useRef(paused);\n  const feel = React.useRef({ bob, settle });\n  const wake = React.useRef<() => void>(() => {});\n\n  React.useEffect(() => {\n    pausedRef.current = paused;\n    wake.current();\n  }, [paused]);\n\n  // Read by the frame loop, so changing them doesn't restart the stream.\n  React.useEffect(() => {\n    feel.current = { bob, settle };\n    wake.current();\n  }, [bob, settle]);\n\n  const itemCount = items.length;\n  const Title = titleAs as React.ElementType;\n\n  // The stream: one frame loop moves every card and redraws the traces.\n  React.useEffect(() => {\n    const panel = noiseRef.current;\n    const calm = calmRef.current;\n    const title = titleRef.current;\n    const layer = layerRef.current;\n    const cards = cardRefs.current;\n    const traces = traceRefs.current;\n    if (!panel || !calm || !title || !layer || itemCount === 0) return;\n\n    const rand = seeded(7);\n\n    const sizeOf = (el: HTMLDivElement | null | undefined, stacked: boolean) =>\n      el\n        ? stacked\n          ? { a: el.offsetHeight, c: el.offsetWidth }\n          : { a: el.offsetWidth, c: el.offsetHeight }\n        : { a: 0, c: 0 };\n\n    const measure = (): Geometry => {\n      const w = panel.clientWidth;\n      const h = panel.clientHeight;\n      const stacked = calm.offsetTop > 1;\n      const titleBottom = title.offsetTop + title.offsetHeight;\n      const sizes = Array.from({ length: itemCount }, (_, i) => sizeOf(cards[i], stacked));\n      // Stacked, cards start under the title and come out from beneath it.\n      const mask = stacked ? `linear-gradient(to bottom, transparent ${titleBottom - 6}px, #000 ${titleBottom + 30}px)` : \"\";\n      layer.style.setProperty(\"mask-image\", mask);\n      layer.style.setProperty(\"-webkit-mask-image\", mask);\n      return stacked\n        ? { stacked, w, h, gate: h, start: titleBottom + 12, lo: GATE.edge, hi: w - GATE.edge, horizon: 0, sizes }\n        : { stacked, w, h, gate: w, start: GATE.edge, lo: titleBottom + 16, hi: h - GATE.edge, horizon: h * GATE.horizon, sizes };\n    };\n\n    let geo = measure();\n\n    // How many cards the field holds without turning into a wall.\n    const capacity = () => {\n      const along = Math.max(1, geo.gate - geo.start);\n      const across = Math.max(1, geo.hi - geo.lo);\n      const card = geo.sizes.reduce((sum, s) => sum + s.a * s.c, 0) / itemCount || 1;\n      return clamp(Math.round(((along * across) / card) * GATE.crowd), Math.min(3, itemCount), Math.min(density, itemCount));\n    };\n\n    const slots: Slot[] = [];\n    const lanes: number[] = [];\n    let cursor = 0;\n    let spawned = 0;\n    let clock = 0;\n    // Arrivals are scheduled, not left to chance: each new card starts where it\n    // will reach the seam about one beat after the card before it.\n    let lastArrival = 0;\n    let beat = 1;\n\n    const nextItem = () => {\n      for (let i = 0; i < itemCount; i++) {\n        const idx = (cursor + i) % itemCount;\n        if (!slots.some((s) => s.item === idx)) {\n          cursor = idx + 1;\n          return idx;\n        }\n      }\n      return cursor++ % itemCount;\n    };\n\n    // Of a few random lanes, take the one furthest from the last few cards.\n    const pickLane = (size: number) => {\n      const lo = geo.lo;\n      const hi = Math.max(lo, geo.hi - size);\n      let best = lo;\n      let bestScore = -1;\n      for (let i = 0; i < 6; i++) {\n        const v = lerp(lo, hi, rand());\n        const score = lanes.length ? Math.min(...lanes.map((l) => Math.abs(l - v))) : 1;\n        if (score > bestScore) {\n          bestScore = score;\n          best = v;\n        }\n      }\n      lanes.push(best);\n      if (lanes.length > 3) lanes.shift();\n      return best;\n    };\n\n    const show = (slot: Slot) => {\n      const el = cards[slot.item];\n      if (el) el.style.visibility = \"visible\";\n    };\n    const hide = (item: number) => {\n      const el = cards[item];\n      if (!el) return;\n      el.style.visibility = \"hidden\";\n      el.style.opacity = \"0\";\n    };\n\n    const spawn = (slot: Slot, born = clock, along?: number) => {\n      slot.item = nextItem();\n      slot.depth = GATE.depthOrder[spawned++ % GATE.depthOrder.length];\n      geo.sizes[slot.item] = sizeOf(cards[slot.item], geo.stacked);\n      const size = geo.sizes[slot.item];\n      const d = GATE.depths[slot.depth];\n      const speed = (geo.gate / duration) * TIMING.depthSpeed[slot.depth];\n      const lead = (size.a * (1 + d.scale)) / 2; // box start to the visible leading edge\n      if (along == null) {\n        const earliest = Math.max(clock, born) + TIMING.minRun;\n        const target = Math.max(earliest, lastArrival + beat * (1 + TIMING.jitter * (2 * rand() - 1)));\n        const lo = geo.start - size.a * GATE.overhang;\n        const hi = Math.max(lo, geo.gate - lead - speed * (earliest - clock));\n        along = clamp(geo.gate - lead - speed * (target - clock), lo, hi);\n        lastArrival = Math.max(lastArrival, clock + (geo.gate - lead - along) / speed);\n      }\n      slot.a = along;\n      slot.c = pickLane(size.c);\n      slot.born = born;\n      slot.phase = rand() * TAU;\n      slot.period = TIMING.bobPeriod + rand() * TIMING.bobVary;\n      slot.gated = false;\n      slot.emph = 0;\n      show(slot);\n    };\n\n    // Fill the field at once. Live, on the arrival schedule; still, spread\n    // evenly from the far edge to just short of the seam.\n    const seed = (mode: \"enter\" | \"resume\" | \"still\") => {\n      for (const slot of slots) hide(slot.item);\n      slots.length = 0;\n      lanes.length = 0;\n      cursor = 0;\n      spawned = 0;\n      const n = capacity();\n      const along = geo.sizes.reduce((sum, s) => sum + s.a, 0) / itemCount;\n      const run = geo.gate - geo.start - along * (1 - GATE.overhang);\n      beat = Math.max(TIMING.minBeat, (TIMING.beat * Math.max(1, run) * duration) / geo.gate / n);\n      lastArrival = clock;\n      const order = Array.from({ length: n }, (_, i) => i).sort(() => rand() - 0.5);\n      for (let i = 0; i < n; i++) {\n        const slot = { item: -1 } as Slot;\n        slots.push(slot);\n        if (mode === \"still\") {\n          const size = sizeOf(cards[i % itemCount], geo.stacked);\n          spawn(slot, clock - TIMING.fadeIn, lerp(geo.start, geo.gate - size.a * 1.15, (order[i] + 0.5) / n));\n        } else {\n          spawn(slot, mode === \"enter\" ? clock + i * TIMING.stagger : clock - TIMING.fadeIn);\n        }\n      }\n    };\n\n    const traceAt = (k: number, x: number, t: number) => {\n      const tr = TRACES[k];\n      const w = TIMING.traceSpeed[k];\n      return (\n        tr.amp *\n        (0.55 * Math.sin(tr.k[0] * x + w[0] * t + tr.phase) +\n          0.3 * Math.sin(tr.k[1] * x + w[1] * t + 2 * tr.phase) +\n          0.15 * Math.sin(tr.k[2] * x + w[2] * t + 3 * tr.phase))\n      );\n    };\n\n    // Side by side the traces converge on the horizon at the seam; stacked\n    // they run across the middle of the field and swell in its center.\n    const drawTraces = (t: number) => {\n      const { w, h } = geo;\n      const y0 = geo.stacked ? (geo.start + h) / 2 : geo.horizon;\n      const amp = Math.min(geo.stacked ? 48 : 60, (geo.stacked ? h - geo.start : h - geo.lo) * 0.3);\n      for (let k = 0; k < TRACES.length; k++) {\n        const path = traces[k];\n        if (!path) continue;\n        let d = \"\";\n        for (let x = 0; ; x = Math.min(w, x + 6)) {\n          const env = geo.stacked ? Math.sin((Math.PI * x) / w) ** 0.6 : 1 - smoothstep(w * 0.38, w, x);\n          const y = y0 + amp * env * traceAt(k, x, t);\n          d += `${x ? \"L\" : \"M\"}${x.toFixed(1)} ${y.toFixed(1)}`;\n          if (x >= w) break;\n        }\n        path.setAttribute(\"d\", d);\n      }\n    };\n\n    const place = (slot: Slot, opacityOnly = false) => {\n      const el = cards[slot.item];\n      if (!el) return;\n      const d = GATE.depths[slot.depth];\n      const fade = easeOut(clamp((clock - slot.born) / TIMING.fadeIn, 0, 1));\n      const scale = lerp(d.scale, Math.max(d.scale, 1), slot.emph) * lerp(0.96, 1, fade);\n      const bob = feel.current.bob * Math.sin((clock / slot.period) * TAU + slot.phase);\n      el.style.opacity = String(lerp(d.opacity, 1, slot.emph) * fade);\n      el.style.zIndex = String(slot.emph > 0.5 ? 100 : slot.depth * 10 + 1);\n      if (opacityOnly) return;\n      el.style.transform = geo.stacked\n        ? `translate3d(${(slot.c + bob).toFixed(2)}px, ${slot.a.toFixed(2)}px, 0) scale(${scale.toFixed(4)})`\n        : `translate3d(${slot.a.toFixed(2)}px, ${(slot.c + bob).toFixed(2)}px, 0) scale(${scale.toFixed(4)})`;\n    };\n\n    // Reduced motion: a still scatter, no stream, no ticks.\n    if (reduced) {\n      const still = () => {\n        geo = measure();\n        clock = 0;\n        seed(\"still\");\n        for (const slot of slots) place(slot);\n        drawTraces(0);\n      };\n      still();\n      const ro = new ResizeObserver(still);\n      ro.observe(panel);\n      return () => {\n        ro.disconnect();\n        for (const slot of slots) hide(slot.item);\n      };\n    }\n\n    seed(\"enter\");\n    for (const slot of slots) place(slot);\n    drawTraces(0);\n\n    let factor = pausedRef.current ? 0 : 1;\n    let raf = 0;\n    let last = 0;\n    let running = false;\n    let visible = false;\n\n    const step = (now: number) => {\n      const dt = Math.min(0.05, (now - last) / 1000);\n      last = now;\n      const target = pausedRef.current || hovering.current ? 0 : 1;\n      factor += (target - factor) * (1 - Math.exp(-dt / feel.current.settle));\n      if (target === 1 && factor > 0.999) factor = 1;\n      if (target === 0 && factor < 0.01) factor = 0;\n      clock += dt * factor;\n\n      for (const slot of slots) {\n        const d = GATE.depths[slot.depth];\n        slot.a += (geo.gate / duration) * TIMING.depthSpeed[slot.depth] * dt * factor;\n        slot.emph += ((hovered.current === slot.item ? 1 : 0) - slot.emph) * (1 - Math.exp(-dt / TIMING.emphasis));\n\n        const size = geo.sizes[slot.item];\n        const scale = lerp(d.scale, Math.max(d.scale, 1), slot.emph);\n        const lead = slot.a + (size.a * (1 + scale)) / 2;\n        const trail = slot.a + (size.a * (1 - scale)) / 2;\n\n        if (!slot.gated && lead >= geo.gate) {\n          slot.gated = true;\n          const bob = feel.current.bob * Math.sin((clock / slot.period) * TAU + slot.phase);\n          const mid = slot.c + bob + size.c / 2;\n          const half = (size.c * scale) / 2;\n          calmApi.current?.gate({ item: slot.item, from: mid - half, to: mid + half, stacked: geo.stacked });\n        }\n\n        if (trail >= geo.gate + 12) {\n          const old = slot.item;\n          spawn(slot);\n          hide(old);\n        }\n        place(slot);\n      }\n      drawTraces(clock);\n\n      // Fully stopped and nothing coming forward or back: sleep until woken.\n      const settling = slots.some((s) => s.emph > 0.002 && s.emph < 0.998);\n      if (target === 0 && factor === 0 && !settling) {\n        running = false;\n        return;\n      }\n      raf = requestAnimationFrame(step);\n    };\n\n    const start = () => {\n      if (running || !visible) return;\n      running = true;\n      last = performance.now();\n      raf = requestAnimationFrame(step);\n    };\n    const stop = () => {\n      running = false;\n      cancelAnimationFrame(raf);\n    };\n    wake.current = start;\n\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) start();\n      else stop();\n    });\n    io.observe(panel);\n\n    let lastStacked = geo.stacked;\n    let lastGate = geo.gate;\n    const ro = new ResizeObserver(() => {\n      geo = measure();\n      if (geo.stacked !== lastStacked) {\n        seed(\"resume\");\n      } else {\n        // Keep the stream going: rescale along the drift, clamp across it.\n        const ratio = geo.gate / (lastGate || geo.gate);\n        for (const slot of slots) {\n          slot.a *= ratio;\n          slot.c = clamp(slot.c, geo.lo, Math.max(geo.lo, geo.hi - geo.sizes[slot.item].c));\n        }\n      }\n      lastStacked = geo.stacked;\n      lastGate = geo.gate;\n      for (const slot of slots) place(slot);\n      drawTraces(clock);\n    });\n    ro.observe(panel);\n    ro.observe(calm);\n\n    return () => {\n      stop();\n      io.disconnect();\n      ro.disconnect();\n      wake.current = () => {};\n      for (const slot of slots) hide(slot.item);\n    };\n  }, [itemCount, reduced, density, duration]);\n\n  const titleClass =\n    \"relative z-20 mx-auto max-w-[17ch] px-6 pt-9 text-center text-[clamp(1.625rem,7.4cqi,2.25rem)] leading-[1.04] font-semibold tracking-[-0.035em] text-balance @[40rem]/noise-gate:pt-12 @[40rem]/noise-gate:text-[clamp(1.75rem,3.9cqi,2.75rem)]\";\n\n  return (\n    <div\n      className={cn(\"@container/noise-gate w-full [--noise-gate-radius:1.5rem]\", className)}\n      style={{ \"--noise-gate-height\": height } as React.CSSProperties}\n    >\n      <div className=\"relative isolate grid overflow-hidden rounded-[var(--noise-gate-radius)] @[40rem]/noise-gate:h-[var(--noise-gate-height)] @[40rem]/noise-gate:grid-cols-2\">\n        <div\n          ref={noiseRef}\n          className={cn(\n            \"relative h-[25rem] overflow-hidden bg-white text-zinc-950 @[40rem]/noise-gate:h-auto dark:bg-zinc-950 dark:text-zinc-50\",\n            noiseClassName,\n          )}\n          onPointerEnter={(e) => {\n            if (e.pointerType !== \"mouse\") return;\n            hovering.current = true;\n          }}\n          onPointerLeave={() => {\n            hovering.current = false;\n            hovered.current = -1;\n            wake.current();\n          }}\n        >\n          <div\n            aria-hidden\n            className=\"absolute inset-0 text-zinc-950/[0.09] [mask-image:linear-gradient(to_bottom,transparent,#000_45%)] dark:text-white/[0.08]\"\n            style={{\n              backgroundImage: \"radial-gradient(circle, currentColor 1px, transparent 1.3px)\",\n              backgroundSize: \"16px 16px\",\n              backgroundPosition: \"8px 8px\",\n            }}\n          />\n          <svg aria-hidden className=\"absolute inset-0 size-full overflow-visible\">\n            {TRACES.map((trace, k) => (\n              <path\n                key={k}\n                ref={(el) => {\n                  traceRefs.current[k] = el;\n                }}\n                className={trace.tone}\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth={1}\n                strokeLinejoin=\"round\"\n                strokeLinecap=\"round\"\n              />\n            ))}\n          </svg>\n\n          <Title ref={titleRef} className={titleClass}>\n            {noiseTitle}\n          </Title>\n\n          <div ref={layerRef} aria-hidden className=\"pointer-events-none absolute inset-0\">\n            {items.map((item, i) => (\n              <div\n                key={i}\n                ref={(el) => {\n                  cardRefs.current[i] = el;\n                }}\n                className=\"pointer-events-auto absolute top-0 left-0 w-[min(15rem,80%)] will-change-transform\"\n                style={{ opacity: 0, visibility: \"hidden\" }}\n                onPointerEnter={(e) => {\n                  if (e.pointerType !== \"mouse\") return;\n                  hovered.current = i;\n                  wake.current();\n                }}\n                onPointerLeave={() => {\n                  if (hovered.current === i) hovered.current = -1;\n                  wake.current();\n                }}\n              >\n                <NoiseCard item={item} />\n              </div>\n            ))}\n          </div>\n\n          <div\n            aria-hidden\n            className=\"pointer-events-none absolute inset-0 z-30 rounded-t-[var(--noise-gate-radius)] border border-b-0 border-zinc-950/10 @[40rem]/noise-gate:rounded-tr-none @[40rem]/noise-gate:rounded-l-[var(--noise-gate-radius)] @[40rem]/noise-gate:border-r-0 @[40rem]/noise-gate:border-b dark:border-white/10\"\n          />\n        </div>\n\n        <CalmPanel\n          ref={calmRef}\n          api={calmApi}\n          items={items}\n          display={display}\n          count={count}\n          rollDuration={rollDuration}\n          countPrefix={countPrefix}\n          countSuffix={countSuffix}\n          footer={calmFooter}\n          className={calmClassName}\n        >\n          <Title className={titleClass}>{calmTitle}</Title>\n        </CalmPanel>\n      </div>\n    </div>\n  );\n}\n\nfunction NoiseCard({ item }: { item: NoiseGateItem }) {\n  const tone = TONE[item.tone ?? \"neutral\"];\n  const copies = clamp(Math.round(item.repeat ?? 1), 1, 4) - 1;\n  return (\n    <div className=\"relative\">\n      {Array.from({ length: copies }, (_, i) => (\n        <div\n          key={i}\n          className={cn(SURFACE, \"absolute inset-0 origin-top\")}\n          style={{\n            transform: `translateY(${-(copies - i) * 6}px) scale(${1 - (copies - i) * 0.05})`,\n            opacity: 0.45 + (i / copies) * 0.35,\n          }}\n        />\n      ))}\n      <div className={cn(SURFACE, \"relative flex items-start gap-2.5 px-3 py-2.5\")}>\n        {item.icon ? (\n          <span className={cn(\"grid size-5 shrink-0 place-items-center [&_svg]:size-4\", tone.icon)}>\n            {item.icon}\n          </span>\n        ) : null}\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"flex items-start gap-1.5\">\n            {item.tag ? (\n              <span className={cn(\"shrink-0 rounded-[5px] px-1.5 text-[11px] leading-5 font-medium\", tone.tag)}>\n                {item.tag}\n              </span>\n            ) : null}\n            <span\n              className={cn(\n                \"line-clamp-2 min-w-0 flex-1 text-[13px] leading-5 font-medium text-zinc-900 dark:text-zinc-100\",\n                item.mono && \"font-mono text-[12px] tracking-tight\",\n              )}\n            >\n              {item.title}\n            </span>\n            {copies > 0 ? (\n              <span className=\"shrink-0 text-[11px] leading-5 text-zinc-500 tabular-nums dark:text-zinc-400\">\n                ×{copies + 1}\n              </span>\n            ) : null}\n          </div>\n          {item.detail ? (\n            <div className=\"mt-0.5 truncate text-[12px] leading-[18px] text-zinc-500 dark:text-zinc-400\">\n              {item.detail}\n            </div>\n          ) : null}\n        </div>\n      </div>\n    </div>\n  );\n}\n\ntype Row = { id: number; item: number };\n\n/** The last few items that have a resolved line, newest first, so the log never starts empty. */\nconst initialRows = (items: NoiseGateItem[]): Row[] =>\n  items\n    .map((item, i) => ({ id: -1 - i, item: i, ok: item.resolved != null }))\n    .filter((row) => row.ok)\n    .slice(-3)\n    .reverse()\n    .map(({ id, item }) => ({ id, item }));\n\nfunction CalmPanel({\n  ref,\n  api,\n  items,\n  display,\n  count: start,\n  rollDuration,\n  countPrefix,\n  countSuffix,\n  footer,\n  className,\n  children,\n}: {\n  ref: React.Ref<HTMLDivElement>;\n  api: React.Ref<CalmApi>;\n  items: NoiseGateItem[];\n  display: \"count\" | \"log\";\n  count: number;\n  rollDuration: number;\n  countPrefix?: React.ReactNode;\n  countSuffix?: React.ReactNode;\n  footer?: React.ReactNode;\n  className?: string;\n  children: React.ReactNode;\n}) {\n  const panelRef = React.useRef<HTMLDivElement>(null);\n  const anchorRef = React.useRef<HTMLElement>(null);\n  const glowRef = React.useRef<HTMLSpanElement>(null);\n  const [count, setCount] = React.useState(start);\n  const [rows, setRows] = React.useState(() => initialRows(items));\n  const [pulses, setPulses] = React.useState<Pulse[]>([]);\n  const nextId = React.useRef(0);\n\n  React.useImperativeHandle(ref, () => panelRef.current as HTMLDivElement);\n\n  React.useImperativeHandle(\n    api,\n    () => ({\n      gate({ item, from, to, stacked }) {\n        const panel = panelRef.current;\n        const anchor = anchorRef.current;\n        if (!panel || !anchor) return;\n        const p = panel.getBoundingClientRect();\n        const a = anchor.getBoundingClientRect();\n        let route: Pulse[\"route\"] = null;\n        let exit: Pulse[\"exit\"] = null;\n        if (!stacked) {\n          // Down (or up) the seam to the horizon, then along it to the pill or the log.\n          const y = display === \"log\" ? a.top + 20 - p.top : a.top + a.height / 2 - p.top;\n          const x = a.left - p.left;\n          const yc = clamp((from + to) / 2, 8, p.height - 8);\n          const leg1 = Math.abs(y - yc);\n          const leg2 = Math.max(0, x - 4);\n          const total = Math.max(1, leg1 + leg2);\n          route = {\n            x: [4, 4, x],\n            y: [yc, y, y],\n            times: [0, leg1 / total, 1],\n            duration: clamp(total / TIMING.route, TIMING.routeDuration[0], TIMING.routeDuration[1]),\n          };\n          exit = { x0: a.right - p.left, x1: p.width - 4, y };\n        }\n        setPulses((list) => [\n          ...list,\n          { id: nextId.current++, item, weight: items[item]?.weight ?? 1, stacked, from, to, route, exit },\n        ]);\n      },\n    }),\n    [display, items],\n  );\n\n  const arrive = React.useCallback((pulse: Pulse) => {\n    setCount((c) => c + pulse.weight);\n    setRows((list) => [{ id: pulse.id, item: pulse.item }, ...list].slice(0, GATE.logRows));\n    const glow = glowRef.current;\n    if (glow)\n      animate(\n        glow,\n        { opacity: [0, 1, 0] },\n        { duration: TIMING.glow.duration, times: [...TIMING.glow.times], ease: TIMING.glow.ease },\n      );\n  }, []);\n\n  const done = React.useCallback((id: number) => {\n    setPulses((list) => list.filter((p) => p.id !== id));\n  }, []);\n\n  return (\n    <div\n      ref={panelRef}\n      className={cn(\n        \"relative z-10 flex flex-col bg-[#3245ff] text-white shadow-[0_-24px_40px_-32px_rgb(9_9_11/0.3)] @[40rem]/noise-gate:shadow-[-24px_0_40px_-32px_rgb(9_9_11/0.3)]\",\n        className,\n      )}\n    >\n      {children}\n\n      <div\n        className={cn(\n          \"relative mt-7 flex flex-col pb-9 @[40rem]/noise-gate:absolute @[40rem]/noise-gate:inset-x-0 @[40rem]/noise-gate:top-[60%] @[40rem]/noise-gate:bottom-0 @[40rem]/noise-gate:mt-0 @[40rem]/noise-gate:pb-0\",\n          footer && \"pb-8\",\n        )}\n      >\n        {display === \"count\" ? (\n          <div className=\"flex items-center @[40rem]/noise-gate:-mt-[22px]\">\n            <span aria-hidden className=\"h-[3px] min-w-6 flex-1 opacity-50\" style={DOTTED} />\n            <div\n              ref={anchorRef as React.Ref<HTMLDivElement>}\n              className=\"relative mx-2 inline-flex h-11 max-w-[calc(100%-4.5rem)] shrink-0 items-center gap-2 rounded-full border border-current/25 bg-current/[0.08] px-4 text-[clamp(0.8125rem,3.9cqi,0.9375rem)] font-medium whitespace-nowrap @[40rem]/noise-gate:text-[clamp(0.8125rem,1.75cqi,0.9375rem)]\"\n            >\n              <span ref={glowRef} aria-hidden className=\"absolute inset-0 rounded-full bg-current/15 opacity-0\" />\n              <CheckIcon className=\"relative size-4 shrink-0\" />\n              <span aria-hidden className=\"relative inline-flex min-w-0 items-baseline gap-[0.3em]\">\n                {countPrefix != null ? <span>{countPrefix}</span> : null}\n                <Digits value={count} duration={rollDuration} />\n                {countSuffix != null ? <span className=\"truncate\">{countSuffix}</span> : null}\n              </span>\n              <span className=\"sr-only\">\n                {countPrefix} {count.toLocaleString(\"en-US\")} {countSuffix}\n              </span>\n            </div>\n            <span aria-hidden className=\"h-[3px] min-w-6 flex-1 opacity-50\" style={DOTTED} />\n          </div>\n        ) : (\n          <div className=\"flex items-start @[40rem]/noise-gate:-mt-5 @[40rem]/noise-gate:min-h-0 @[40rem]/noise-gate:flex-1\">\n            <span aria-hidden className=\"mt-[18.5px] h-[3px] min-w-6 flex-1 opacity-50\" style={DOTTED} />\n            <ul\n              ref={anchorRef as React.Ref<HTMLUListElement>}\n              className=\"relative mx-2 h-[12.5rem] w-[min(19rem,calc(100%-4.5rem))] shrink-0 overflow-hidden text-left [mask-image:linear-gradient(to_bottom,#000_45%,transparent)] @[40rem]/noise-gate:h-full\"\n            >\n              <AnimatePresence initial={false}>\n                {rows.map((row) => {\n                  const item = items[row.item];\n                  if (!item) return null;\n                  return (\n                    // A new line opens at the top and pushes the rest down; the oldest fades out under the mask.\n                    <motion.li\n                      key={row.id}\n                      initial={{ height: 0, opacity: 0 }}\n                      animate={{ height: 40, opacity: 1 }}\n                      exit={{ opacity: 0 }}\n                      transition={TIMING.row}\n                      className=\"flex items-center gap-2.5 overflow-hidden border-b border-current/15 text-[14px]\"\n                    >\n                      <CheckIcon className=\"size-4 shrink-0 opacity-85\" />\n                      <span className=\"min-w-0 flex-1 truncate font-medium\">{item.resolved ?? item.title}</span>\n                      {item.resolvedDetail != null ? (\n                        <span className=\"shrink-0 tabular-nums opacity-85\">{item.resolvedDetail}</span>\n                      ) : null}\n                    </motion.li>\n                  );\n                })}\n              </AnimatePresence>\n            </ul>\n            <span aria-hidden className=\"mt-[18.5px] h-[3px] min-w-6 flex-1 opacity-50\" style={DOTTED} />\n          </div>\n        )}\n\n        {footer ? (\n          <div className=\"relative mt-7 px-6 text-center @[40rem]/noise-gate:mt-auto @[40rem]/noise-gate:pt-6 @[40rem]/noise-gate:pb-8\">\n            {footer}\n          </div>\n        ) : null}\n      </div>\n\n      {pulses.map((pulse) => (\n        <PulseView key={pulse.id} pulse={pulse} onArrive={arrive} onDone={done} />\n      ))}\n    </div>\n  );\n}\n\n/** The seam lighting up where a card went through, and the dot routed to the counter. */\nfunction PulseView({\n  pulse,\n  onArrive,\n  onDone,\n}: {\n  pulse: Pulse;\n  onArrive: (pulse: Pulse) => void;\n  onDone: (id: number) => void;\n}) {\n  const flashRef = React.useRef<HTMLSpanElement>(null);\n  const dotRef = React.useRef<HTMLSpanElement>(null);\n  const exitRef = React.useRef<HTMLSpanElement>(null);\n\n  React.useEffect(() => {\n    const flash = flashRef.current;\n    if (!flash) return;\n    let live = true;\n    const running: { stop: () => void }[] = [];\n\n    running.push(\n      animate(\n        flash,\n        { opacity: [0, 1, 0] },\n        { duration: TIMING.flash.duration, times: [...TIMING.flash.times], ease: TIMING.flash.ease },\n      ),\n    );\n\n    const settle = (seconds: number) =>\n      running.push(animate(0, 1, { duration: seconds, onComplete: () => live && onDone(pulse.id) }));\n\n    const arrived = () => {\n      if (!live) return;\n      onArrive(pulse);\n      const exit = exitRef.current;\n      if (pulse.exit && exit && pulse.exit.x1 > pulse.exit.x0) {\n        const { x0, x1, y } = pulse.exit;\n        running.push(\n          animate(\n            exit,\n            { x: [x0, x1], y: [y, y], opacity: [0.6, 0] },\n            {\n              duration: clamp(\n                ((x1 - x0) / TIMING.route) * TIMING.exitPace,\n                TIMING.exitDuration[0],\n                TIMING.exitDuration[1],\n              ),\n              ease: TIMING.exitEase,\n              onComplete: () => live && onDone(pulse.id),\n            },\n          ),\n        );\n      } else {\n        settle(TIMING.linger);\n      }\n    };\n\n    const dot = dotRef.current;\n    if (pulse.route && dot) {\n      const { x, y, times, duration } = pulse.route;\n      running.push(\n        animate(dot, { x, y, opacity: [0, 1, 1] }, { duration, times, ease: [...TIMING.routeEase], onComplete: arrived }),\n      );\n    } else {\n      running.push(animate(0, 1, { duration: TIMING.tickDelay, onComplete: arrived }));\n    }\n\n    return () => {\n      live = false;\n      for (const r of running) r.stop();\n    };\n  }, [pulse, onArrive, onDone]);\n\n  const dotClass =\n    \"pointer-events-none absolute top-0 left-0 -mt-[3px] -ml-[3px] size-1.5 rounded-full bg-current opacity-0 ring-[3px] ring-current/20\";\n\n  return (\n    <>\n      <span\n        ref={flashRef}\n        aria-hidden\n        className={cn(\n          \"pointer-events-none absolute opacity-0\",\n          pulse.stacked\n            ? \"top-0 h-8 [mask-image:linear-gradient(to_right,transparent,#000_30%,#000_70%,transparent)]\"\n            : \"left-0 w-8 [mask-image:linear-gradient(to_bottom,transparent,#000_30%,#000_70%,transparent)]\",\n        )}\n        style={pulse.stacked ? { left: pulse.from, width: pulse.to - pulse.from } : { top: pulse.from, height: pulse.to - pulse.from }}\n      >\n        <span\n          className={cn(\n            \"absolute bg-current\",\n            pulse.stacked ? \"inset-x-0 top-0 h-0.5\" : \"inset-y-0 left-0 w-0.5\",\n          )}\n        />\n        <span\n          className={cn(\n            \"absolute inset-0 from-current/20 to-transparent\",\n            pulse.stacked ? \"bg-linear-to-b\" : \"bg-linear-to-r\",\n          )}\n        />\n      </span>\n      {pulse.route ? <span ref={dotRef} aria-hidden className={dotClass} /> : null}\n      {pulse.exit ? <span ref={exitRef} aria-hidden className={dotClass} /> : null}\n    </>\n  );\n}\n\nfunction Digits({ value, duration }: { value: number; duration: number }) {\n  const chars = value.toLocaleString(\"en-US\").split(\"\");\n  return (\n    <span className=\"inline-flex font-semibold tabular-nums\">\n      {chars.map((char, i) => (\n        // Keyed from the right, so a new leading digit doesn't re-roll the rest.\n        <Digit key={chars.length - i} char={char} duration={duration} />\n      ))}\n    </span>\n  );\n}\n\nfunction Digit({ char, duration }: { char: string; duration: number }) {\n  return (\n    <span className=\"relative inline-flex overflow-hidden\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          key={char}\n          initial={{ y: \"100%\", opacity: 0 }}\n          animate={{ y: \"0%\", opacity: 1 }}\n          exit={{ y: \"-100%\", opacity: 0 }}\n          transition={{ ...TIMING.roll, visualDuration: duration }}\n          className=\"inline-block\"\n        >\n          {char}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction CheckIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      aria-hidden\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.75}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={className}\n    >\n      <path d=\"M3.5 8.5 6.5 11.5 12.5 4.5\" />\n    </svg>\n  );\n}\n"
    }
  ]
}