{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select-caustic",
  "title": "Select Caustic",
  "description": "Single-select whose frosted-glass trigger and listbox sit over drifting caustic light pools; hovering or arrow-keying an option bends the caustics toward that row so the highlight reads as pressed into the glass.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/select-caustic/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// CausticSelect — single-select whose closed trigger and open listbox are\n// frosted glass over live caustic light: 6-8 drifting radial blobs summed\n// additively on a canvas BENEATH each surface's backdrop-filter layer, so the\n// blur turns them into pooled light. Opening scale-springs the panel from the\n// trigger while blob anchors spring-redistribute down the list height; the\n// active row (cursor OR keyboard — one target) carries a lens that bends\n// nearby caustics up to 6px toward the row so the highlight reads as pressed\n// into the glass. Direct-DOM rAF: fully asleep while closed, paused offscreen\n// and on hidden tabs, lens has a 1s forced-settle deadline. All drawn ink is\n// derived from CSS tokens at mount and re-derived on documentElement class\n// changes. Full listbox keyboard pattern with aria-activedescendant.\n// ---------------------------------------------------------------------------\n\nconst TRIGGER_BLOBS = 6;\nconst PANEL_BLOBS = 8;\nconst DRIFT_MIN = 6; // px/s peak drift speed per blob\nconst DRIFT_MAX = 10;\nconst OPEN_MS = 180;\nconst ANCHOR_K = 60; // s^-2 — blob anchor redistribution spring\nconst ANCHOR_ZETA = 0.9;\nconst LENS_K = 140; // s^-2 — lens follow spring\nconst LENS_ZETA = 0.8;\nconst LENS_R = 56; // px displacement field radius\nconst LENS_PULL = 6; // px max pull toward the active row center\nconst SETTLE_MS = 1000; // forced-settle deadline after last input\nconst TYPEAHEAD_MS = 500;\n\ntype Vec3 = readonly [number, number, number];\n\ninterface CausticBlob {\n  ax: number; // anchor position (spring state)\n  ay: number;\n  vx: number;\n  vy: number;\n  tx: number; // anchor target\n  ty: number;\n  amp: number; // drift amplitude px\n  wx: number; // drift angular frequency rad/s (amp*wx = peak px/s)\n  wy: number;\n  phase: number;\n  phase2: number;\n  r: number;\n  rf: number; // persistent per-blob randoms (radius / layout jitter)\n  jx: number;\n  jy: number;\n  mixT: number; // 0 = --ns-accent, 1 = --foreground\n  alpha: number; // 0.06-0.10\n}\n\ninterface Lens {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  tx: number;\n  ty: number;\n  s: number; // strength 0..1\n  ts: number;\n  on: boolean;\n}\n\nexport interface CausticSelectOption {\n  value: string;\n  label: string;\n  /** mono hint rendered right of the label (e.g. a region code) */\n  hint?: string;\n  disabled?: boolean;\n}\n\nconst DEFAULT_OPTIONS: CausticSelectOption[] = [\n  { value: \"iad1\", label: \"Washington, D.C.\", hint: \"iad1\" },\n  { value: \"sfo1\", label: \"San Francisco\", hint: \"sfo1\" },\n  { value: \"fra1\", label: \"Frankfurt\", hint: \"fra1\" },\n  { value: \"hnd1\", label: \"Tokyo\", hint: \"hnd1\" },\n  { value: \"syd1\", label: \"Sydney\", hint: \"syd1\" },\n  { value: \"gru1\", label: \"São Paulo\", hint: \"gru1\" },\n];\n\n// deterministic PRNG — blob character is stable across opens in-session\nfunction mulberry32(seed: number) {\n  let a = seed >>> 0;\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nfunction parseColor(raw: string): Vec3 | null {\n  const s = raw.trim();\n  if (s.startsWith(\"#\")) {\n    const hex = s.slice(1);\n    if (hex.length === 3) {\n      const r = parseInt(hex.slice(0, 1) + hex.slice(0, 1), 16);\n      const g = parseInt(hex.slice(1, 2) + hex.slice(1, 2), 16);\n      const b = parseInt(hex.slice(2, 3) + hex.slice(2, 3), 16);\n      return Number.isNaN(r + g + b) ? null : [r, g, b];\n    }\n    if (hex.length >= 6) {\n      const r = parseInt(hex.slice(0, 2), 16);\n      const g = parseInt(hex.slice(2, 4), 16);\n      const b = parseInt(hex.slice(4, 6), 16);\n      return Number.isNaN(r + g + b) ? null : [r, g, b];\n    }\n    return null;\n  }\n  const m = s.match(/rgba?\\(\\s*([\\d.]+)[,\\s]+([\\d.]+)[,\\s]+([\\d.]+)/);\n  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;\n}\n\nfunction mix(a: Vec3, b: Vec3, t: number): Vec3 {\n  return [\n    Math.round(a[0] + (b[0] - a[0]) * t),\n    Math.round(a[1] + (b[1] - a[1]) * t),\n    Math.round(a[2] + (b[2] - a[2]) * t),\n  ];\n}\n\nexport function CausticSelect({\n  options = DEFAULT_OPTIONS,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  label = \"Region\",\n  placeholder = \"Select a region\",\n  disabled = false,\n  name,\n  className = \"\",\n}: {\n  /** the selectable options */\n  options?: CausticSelectOption[];\n  /** controlled value; omit for uncontrolled */\n  value?: string;\n  /** uncontrolled initial value */\n  defaultValue?: string;\n  /** called with the new value when a different option is selected */\n  onValueChange?: (value: string) => void;\n  /** field label rendered above the trigger */\n  label?: string;\n  /** shown on the trigger while no option is selected */\n  placeholder?: string;\n  /** blocks the trigger and closes the popover if open */\n  disabled?: boolean;\n  /** when set, renders a hidden input for form posts */\n  name?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}) {\n  const uid = useId();\n  const labelId = `${uid}-label`;\n  const valueId = `${uid}-value`;\n  const listboxId = `${uid}-listbox`;\n  const optId = (i: number) => `${uid}-opt-${i}`;\n\n  const rootRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const trigCanvasRef = useRef<HTMLCanvasElement>(null);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const panelCanvasRef = useRef<HTMLCanvasElement>(null);\n  const listboxRef = useRef<HTMLUListElement>(null);\n  const engineRef = useRef<{\n    opened: () => void;\n    closed: () => void;\n    setLens: (i: number) => void;\n  } | null>(null);\n\n  const [open, setOpen] = useState(false);\n  const [activeIndex, setActiveIndex] = useState(-1);\n  const openRef = useRef(open);\n\n  const isControlled = value !== undefined;\n  const [internal, setInternal] = useState(defaultValue);\n  const val = isControlled ? value : internal;\n  const selected = options.find((o) => o.value === val) ?? null;\n\n  const typeRef = useRef<{ buf: string; timer: number }>({ buf: \"\", timer: 0 });\n\n  // -- caustic engine: one mount effect, direct-DOM hot path -----------------\n  useEffect(() => {\n    const root = rootRef.current;\n    const trigger = triggerRef.current;\n    const tCanvas = trigCanvasRef.current;\n    const panel = panelRef.current;\n    const pCanvas = panelCanvasRef.current;\n    const listbox = listboxRef.current;\n    if (!root || !trigger || !tCanvas || !panel || !pCanvas || !listbox) return;\n    const tCtx = tCanvas.getContext(\"2d\");\n    const pCtx = pCanvas.getContext(\"2d\");\n    if (!tCtx || !pCtx) return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    // token-derived ink: read at mount, re-derived on theme class change\n    let accent: Vec3 = [0, 107, 255];\n    let fg: Vec3 = [237, 237, 237];\n    const derive = () => {\n      const cs = getComputedStyle(document.documentElement);\n      accent = parseColor(cs.getPropertyValue(\"--ns-accent\")) ?? accent;\n      fg = parseColor(cs.getPropertyValue(\"--foreground\")) ?? fg;\n    };\n    derive();\n\n    const rand = mulberry32(0x5eaf00d);\n    const makeBlobs = (n: number): CausticBlob[] => {\n      const arr: CausticBlob[] = [];\n      for (let i = 0; i < n; i++) {\n        const amp = 10 + rand() * 6;\n        const speed = DRIFT_MIN + rand() * (DRIFT_MAX - DRIFT_MIN);\n        arr.push({\n          ax: 0,\n          ay: 0,\n          vx: 0,\n          vy: 0,\n          tx: 0,\n          ty: 0,\n          amp,\n          wx: speed / amp,\n          wy: (speed / amp) * 0.83,\n          phase: rand() * Math.PI * 2,\n          phase2: rand() * Math.PI * 2,\n          r: 40,\n          rf: rand(),\n          jx: rand(),\n          jy: rand(),\n          mixT: i % 2 === 0 ? 0.12 + rand() * 0.2 : 0.65 + rand() * 0.3,\n          alpha: 0.06 + rand() * 0.04,\n        });\n      }\n      return arr;\n    };\n    const tBlobs = makeBlobs(TRIGGER_BLOBS);\n    const pBlobs = makeBlobs(PANEL_BLOBS);\n\n    // hot-path state: locals only, never React state\n    let dpr = 1;\n    let tW = 0;\n    let tH = 0;\n    let pW = 0;\n    let pH = 0;\n    let tSized = false;\n    let pSized = false;\n    let raf = 0;\n    let last = 0;\n    let visible = true;\n    let lastInput = 0;\n    let anim: Animation | null = null;\n    const lens: Lens = {\n      x: 0,\n      y: 0,\n      vx: 0,\n      vy: 0,\n      tx: 0,\n      ty: 0,\n      s: 0,\n      ts: 0,\n      on: false,\n    };\n\n    // <canvas> is a replaced element — inset never sizes it; set BOTH the CSS\n    // size and the backing store explicitly. offsetWidth/Height ignore the\n    // open-scale transform, so layout stays honest mid-animation.\n    const sizeCanvas = (\n      host: HTMLElement,\n      canvas: HTMLCanvasElement\n    ): { w: number; h: number; ok: boolean } => {\n      const w = host.offsetWidth;\n      const h = host.offsetHeight;\n      if (w < 8 || h < 8) return { w: 0, h: 0, ok: false }; // zero-size guard\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n      return { w, h, ok: true };\n    };\n\n    const layoutTrigger = () => {\n      const n = tBlobs.length;\n      for (let i = 0; i < n; i++) {\n        const b = tBlobs[i];\n        if (!b) continue;\n        b.tx = tW * ((i + 0.5) / n) + (b.jx - 0.5) * tW * 0.25;\n        b.ty = tH * (0.2 + b.jy * 0.6);\n        b.r = 20 + b.rf * 22;\n        b.ax = b.tx; // trigger anchors never spring — targets ARE positions\n        b.ay = b.ty;\n      }\n    };\n    // redistribute anchors evenly down the list height, jittered laterally\n    const layoutPanel = () => {\n      const n = pBlobs.length;\n      for (let i = 0; i < n; i++) {\n        const b = pBlobs[i];\n        if (!b) continue;\n        b.ty = pH * ((i + 0.5) / n) + (b.jy - 0.5) * pH * 0.12;\n        b.tx = pW * (0.12 + b.jx * 0.76);\n        b.r = 42 + b.rf * 40;\n      }\n    };\n\n    // full clear + full redraw every frame — additive blobs must never\n    // accumulate (destination-in style fades quantize and pile up forever)\n    const drawSurface = (\n      ctx: CanvasRenderingContext2D,\n      blobs: CausticBlob[],\n      w: number,\n      h: number,\n      t: number,\n      lensRef: Lens | null\n    ) => {\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.clearRect(0, 0, w, h);\n      ctx.globalCompositeOperation = \"lighter\";\n      for (const b of blobs) {\n        let x = b.ax + Math.sin(t * b.wx + b.phase) * b.amp;\n        let y = b.ay + Math.cos(t * b.wy + b.phase2) * b.amp;\n        let boost = 0;\n        if (lensRef && lensRef.s > 0.01) {\n          const dx = lensRef.x - x;\n          const dy = lensRef.y - y;\n          const dist = Math.hypot(dx, dy);\n          if (dist > 0.01 && dist < LENS_R) {\n            const f = (1 - dist / LENS_R) * lensRef.s;\n            x += (dx / dist) * LENS_PULL * f;\n            y += (dy / dist) * LENS_PULL * f;\n            boost = 0.05 * f;\n          }\n        }\n        const c = mix(accent, fg, b.mixT);\n        const a = b.alpha + boost;\n        const g = ctx.createRadialGradient(x, y, 0, x, y, b.r);\n        g.addColorStop(0, `rgba(${c[0]},${c[1]},${c[2]},${a.toFixed(3)})`);\n        g.addColorStop(1, `rgba(${c[0]},${c[1]},${c[2]},0)`);\n        ctx.fillStyle = g;\n        ctx.beginPath();\n        ctx.arc(x, y, b.r, 0, Math.PI * 2);\n        ctx.fill();\n      }\n      // the lens itself: a tight accent pool pressed under the active row\n      if (lensRef && lensRef.s > 0.01) {\n        const a = 0.09 * lensRef.s;\n        const g = ctx.createRadialGradient(\n          lensRef.x,\n          lensRef.y,\n          0,\n          lensRef.x,\n          lensRef.y,\n          LENS_R\n        );\n        g.addColorStop(\n          0,\n          `rgba(${accent[0]},${accent[1]},${accent[2]},${a.toFixed(3)})`\n        );\n        g.addColorStop(1, `rgba(${accent[0]},${accent[1]},${accent[2]},0)`);\n        ctx.fillStyle = g;\n        ctx.beginPath();\n        ctx.arc(lensRef.x, lensRef.y, LENS_R, 0, Math.PI * 2);\n        ctx.fill();\n      }\n      ctx.globalCompositeOperation = \"source-over\";\n    };\n\n    const drawTriggerStatic = () => {\n      if (tSized) drawSurface(tCtx, tBlobs, tW, tH, 0, null);\n    };\n    const drawPanelStatic = () => {\n      if (!pSized) return;\n      for (const b of pBlobs) {\n        b.ax = b.tx;\n        b.ay = b.ty;\n        b.vx = 0;\n        b.vy = 0;\n      }\n      drawSurface(pCtx, pBlobs, pW, pH, 0, null);\n    };\n\n    const loop = (now: number) => {\n      raf = 0;\n      if (!openRef.current) return; // canvas fully asleep while closed\n      const dt = last ? Math.min(0.033, (now - last) / 1000) : 1 / 60;\n      last = now;\n      const t = now / 1000;\n\n      // anchor redistribution springs (k=60, zeta=0.9, ~450ms settle)\n      const cA = 2 * ANCHOR_ZETA * Math.sqrt(ANCHOR_K);\n      for (const b of pBlobs) {\n        b.vx += (-ANCHOR_K * (b.ax - b.tx) - cA * b.vx) * dt;\n        b.vy += (-ANCHOR_K * (b.ay - b.ty) - cA * b.vy) * dt;\n        b.ax += b.vx * dt;\n        b.ay += b.vy * dt;\n      }\n\n      // lens follow spring (k=140, zeta=0.8) + forced-settle deadline —\n      // never trusts epsilon alone\n      if (lens.on) {\n        if (now - lastInput > SETTLE_MS) {\n          lens.x = lens.tx;\n          lens.y = lens.ty;\n          lens.vx = 0;\n          lens.vy = 0;\n          lens.s = lens.ts;\n        } else {\n          const cL = 2 * LENS_ZETA * Math.sqrt(LENS_K);\n          lens.vx += (-LENS_K * (lens.x - lens.tx) - cL * lens.vx) * dt;\n          lens.vy += (-LENS_K * (lens.y - lens.ty) - cL * lens.vy) * dt;\n          lens.x += lens.vx * dt;\n          lens.y += lens.vy * dt;\n          lens.s += (lens.ts - lens.s) * Math.min(1, dt * 12);\n        }\n      }\n\n      if (tSized) drawSurface(tCtx, tBlobs, tW, tH, t, null);\n      if (pSized) drawSurface(pCtx, pBlobs, pW, pH, t, lens.on ? lens : null);\n\n      // idle simmer is the ambient default while open; sleeps offscreen/hidden\n      if (visible && !document.hidden) raf = requestAnimationFrame(loop);\n    };\n    const wake = () => {\n      if (!raf && !reduced && openRef.current && visible && !document.hidden) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const resizeTrigger = () => {\n      const s = sizeCanvas(trigger, tCanvas);\n      tW = s.w;\n      tH = s.h;\n      tSized = s.ok;\n      if (tSized) layoutTrigger();\n      if (!openRef.current || reduced) drawTriggerStatic();\n    };\n    const resizePanel = () => {\n      const s = sizeCanvas(panel, pCanvas);\n      pW = s.w;\n      pH = s.h;\n      pSized = s.ok;\n      if (pSized) layoutPanel();\n    };\n    resizeTrigger();\n\n    const opened = () => {\n      resizePanel();\n      lens.on = false;\n      lens.s = 0;\n      lens.ts = 0;\n      if (reduced) {\n        // static frost texture, instant open — no drift, no lens\n        drawTriggerStatic();\n        drawPanelStatic();\n        return;\n      }\n      // anchors start clustered at the transform origin, spring outward\n      for (const b of pBlobs) {\n        b.ax = pW / 2 + (b.jx - 0.5) * pW * 0.3;\n        b.ay = b.jy * 18;\n        b.vx = 0;\n        b.vy = 0;\n      }\n      anim?.cancel();\n      anim = panel.animate(\n        [\n          { transform: \"scale(0.96)\", opacity: 0 },\n          { transform: \"scale(1)\", opacity: 1 },\n        ],\n        { duration: OPEN_MS, easing: \"cubic-bezier(0.16, 1, 0.3, 1)\" }\n      );\n      wake();\n    };\n\n    const closed = () => {\n      anim?.cancel();\n      anim = null;\n      if (raf) {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n      lens.on = false;\n      lens.s = 0;\n      drawTriggerStatic();\n    };\n\n    const setLens = (i: number) => {\n      if (!openRef.current || reduced) return;\n      const row = listbox.children.item(i) as HTMLElement | null;\n      if (!row) return;\n      // panel is DOM-anchored: lens works in panel-local offsets, never\n      // absolute canvas/page coordinates\n      const pr = panel.getBoundingClientRect();\n      const rr = row.getBoundingClientRect();\n      const cx = rr.left - pr.left + rr.width / 2;\n      const cy = rr.top - pr.top + rr.height / 2;\n      if (!lens.on) {\n        lens.x = cx;\n        lens.y = cy;\n        lens.vx = 0;\n        lens.vy = 0;\n        lens.on = true;\n      }\n      lens.tx = cx;\n      lens.ty = cy;\n      lens.ts = 1;\n      lastInput = performance.now();\n      wake();\n    };\n\n    engineRef.current = { opened, closed, setLens };\n\n    // -- observers ----------------------------------------------------------\n    const ro = new ResizeObserver(() => {\n      resizeTrigger();\n      if (openRef.current) {\n        resizePanel();\n        if (reduced) drawPanelStatic();\n        wake();\n      }\n    });\n    ro.observe(trigger);\n    ro.observe(panel);\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) wake();\n    });\n    io.observe(root);\n    // live theme re-derive: watch documentElement class flips\n    const mo = new MutationObserver(() => {\n      derive();\n      if (!openRef.current) drawTriggerStatic();\n      else if (reduced) {\n        drawTriggerStatic();\n        drawPanelStatic();\n      }\n      // open + animating: next loop frame repaints with the new ink\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n    const onVis = () => {\n      if (!document.hidden) wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      anim?.cancel();\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n      engineRef.current = null;\n    };\n  }, []);\n\n  // -- open/close orchestration ----------------------------------------------\n  useEffect(() => {\n    openRef.current = open;\n    const eng = engineRef.current;\n    if (!eng) return;\n    if (open) {\n      eng.opened();\n      listboxRef.current?.focus({ preventScroll: true });\n    } else {\n      eng.closed();\n    }\n  }, [open]);\n\n  useEffect(() => {\n    if (!open || activeIndex < 0) return;\n    const el = listboxRef.current?.children.item(activeIndex) as\n      | HTMLElement\n      | null;\n    el?.scrollIntoView({ block: \"nearest\" });\n    engineRef.current?.setLens(activeIndex);\n  }, [open, activeIndex]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onDown = (e: PointerEvent) => {\n      if (!rootRef.current?.contains(e.target as Node)) {\n        setOpen(false);\n        triggerRef.current?.focus();\n      }\n    };\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n  }, [open]);\n\n  useEffect(() => {\n    if (disabled && open) setOpen(false);\n  }, [disabled, open]);\n\n  useEffect(() => {\n    const t = typeRef.current;\n    return () => window.clearTimeout(t.timer);\n  }, []);\n\n  // -- selection + keyboard --------------------------------------------------\n  const firstEnabled = () => options.findIndex((o) => !o.disabled);\n  const lastEnabled = () => {\n    for (let i = options.length - 1; i >= 0; i--) {\n      const o = options[i];\n      if (o && !o.disabled) return i;\n    }\n    return -1;\n  };\n  const step = (from: number, dir: 1 | -1) => {\n    let i = from;\n    for (let k = 0; k < options.length; k++) {\n      i += dir;\n      if (i < 0 || i >= options.length) break;\n      const o = options[i];\n      if (o && !o.disabled) return i;\n    }\n    return from;\n  };\n\n  const openList = () => {\n    if (disabled || open || options.length === 0) return;\n    const sel = options.findIndex((o) => o.value === val && !o.disabled);\n    setActiveIndex(sel >= 0 ? sel : firstEnabled());\n    setOpen(true);\n  };\n  const closeList = (restoreFocus: boolean) => {\n    typeRef.current.buf = \"\";\n    setOpen(false);\n    if (restoreFocus) triggerRef.current?.focus();\n  };\n  const commit = (i: number) => {\n    const o = options[i];\n    if (!o || o.disabled) return;\n    if (!isControlled) setInternal(o.value);\n    onValueChange?.(o.value);\n    closeList(true);\n  };\n\n  const typeahead = (ch: string) => {\n    const t = typeRef.current;\n    t.buf += ch.toLowerCase();\n    window.clearTimeout(t.timer);\n    t.timer = window.setTimeout(() => {\n      t.buf = \"\";\n    }, TYPEAHEAD_MS);\n    const n = options.length;\n    const start = activeIndex >= 0 ? activeIndex : 0;\n    const offset = t.buf.length > 1 ? 0 : 1;\n    for (let k = 0; k < n; k++) {\n      const i = (start + offset + k) % n;\n      const o = options[i];\n      if (o && !o.disabled && o.label.toLowerCase().startsWith(t.buf)) {\n        setActiveIndex(i);\n        return;\n      }\n    }\n  };\n\n  const onTriggerKeyDown = (e: React.KeyboardEvent) => {\n    if (disabled) return;\n    if (\n      e.key === \"Enter\" ||\n      e.key === \" \" ||\n      e.key === \"ArrowDown\" ||\n      e.key === \"ArrowUp\"\n    ) {\n      e.preventDefault();\n      openList();\n    }\n  };\n\n  const onListKeyDown = (e: React.KeyboardEvent) => {\n    const key = e.key;\n    if (key === \"ArrowDown\") {\n      e.preventDefault();\n      setActiveIndex((a) => step(a, 1));\n    } else if (key === \"ArrowUp\") {\n      e.preventDefault();\n      setActiveIndex((a) => step(a, -1));\n    } else if (key === \"Home\") {\n      e.preventDefault();\n      const i = firstEnabled();\n      if (i >= 0) setActiveIndex(i);\n    } else if (key === \"End\") {\n      e.preventDefault();\n      const i = lastEnabled();\n      if (i >= 0) setActiveIndex(i);\n    } else if (key === \"Enter\" || (key === \" \" && typeRef.current.buf === \"\")) {\n      e.preventDefault();\n      commit(activeIndex);\n    } else if (key === \"Escape\") {\n      e.preventDefault();\n      closeList(true);\n    } else if (key === \"Tab\") {\n      closeList(false);\n    } else if (key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey) {\n      e.preventDefault();\n      typeahead(key);\n    }\n  };\n\n  return (\n    <div ref={rootRef} className={`relative ${className}`}>\n      <span\n        id={labelId}\n        className=\"mb-1.5 block font-mono text-xs uppercase tracking-[0.14em] text-ns-muted\"\n      >\n        {label}\n      </span>\n      {name ? <input type=\"hidden\" name={name} value={val} /> : null}\n\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        aria-haspopup=\"listbox\"\n        aria-expanded={open}\n        aria-controls={open ? listboxId : undefined}\n        aria-labelledby={`${labelId} ${valueId}`}\n        disabled={disabled}\n        onClick={() => (open ? closeList(true) : openList())}\n        onKeyDown={onTriggerKeyDown}\n        className=\"relative flex h-10 w-full items-center justify-between overflow-hidden rounded-sm border border-border text-left outline-none transition-colors hover:border-foreground/25 focus-visible:ring-2 focus-visible:ring-ns-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none disabled:pointer-events-none\"\n      >\n        {/* caustic light beneath the frost */}\n        <canvas\n          ref={trigCanvasRef}\n          aria-hidden\n          className=\"pointer-events-none absolute left-0 top-0\"\n        />\n        {/* the frost itself: backdrop-filter blurs the canvas under it */}\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 bg-surface/55 backdrop-blur-md backdrop-saturate-150\"\n        />\n        <span\n          id={valueId}\n          className={`relative z-[1] truncate px-3 text-sm ${\n            selected ? \"text-foreground\" : \"text-ns-muted\"\n          }`}\n        >\n          {selected ? selected.label : placeholder}\n        </span>\n        <svg\n          aria-hidden\n          viewBox=\"0 0 16 16\"\n          fill=\"none\"\n          className={`relative z-[1] mr-3 h-4 w-4 shrink-0 text-ns-muted transition-transform duration-200 motion-reduce:transition-none ${\n            open ? \"rotate-180\" : \"\"\n          }`}\n        >\n          <path\n            d=\"M4 6l4 4 4-4\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      </button>\n\n      <div\n        ref={panelRef}\n        className={`absolute left-0 right-0 top-full z-30 mt-2 overflow-hidden rounded-md border border-border shadow-[0_8px_16px_-4px_rgba(0,0,0,0.2),0_20px_48px_-12px_rgba(0,0,0,0.35)] ${\n          open ? \"\" : \"hidden\"\n        }`}\n        style={{ transformOrigin: \"50% 0%\" }}\n      >\n        <canvas\n          ref={panelCanvasRef}\n          aria-hidden\n          className=\"pointer-events-none absolute left-0 top-0\"\n        />\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 bg-surface/70 backdrop-blur-xl backdrop-saturate-150\"\n        />\n        <ul\n          ref={listboxRef}\n          id={listboxId}\n          role=\"listbox\"\n          tabIndex={-1}\n          aria-labelledby={labelId}\n          aria-activedescendant={\n            open && activeIndex >= 0 ? optId(activeIndex) : undefined\n          }\n          onKeyDown={onListKeyDown}\n          onScroll={() => {\n            if (activeIndex >= 0) engineRef.current?.setLens(activeIndex);\n          }}\n          className=\"relative z-[1] max-h-72 overflow-y-auto py-1.5 outline-none\"\n        >\n          {options.map((opt, i) => {\n            const isActive = i === activeIndex;\n            const isSelected = opt.value === val;\n            return (\n              <li\n                key={opt.value}\n                id={optId(i)}\n                role=\"option\"\n                aria-selected={isSelected}\n                aria-disabled={opt.disabled || undefined}\n                onPointerMove={() => {\n                  if (!opt.disabled && i !== activeIndex) setActiveIndex(i);\n                }}\n                onPointerDown={(e) => e.preventDefault()}\n                onClick={() => {\n                  if (!opt.disabled) commit(i);\n                }}\n                className={`flex items-center justify-between gap-3 px-3 py-2 text-sm transition-colors duration-100 motion-reduce:transition-none ${\n                  opt.disabled\n                    ? \"cursor-not-allowed text-ns-muted/60\"\n                    : isActive\n                      ? \"cursor-pointer bg-foreground/[0.07] text-foreground\"\n                      : \"cursor-pointer text-foreground/85\"\n                }`}\n              >\n                <span className=\"flex min-w-0 items-baseline gap-2\">\n                  <span className=\"truncate\">{opt.label}</span>\n                  {opt.hint ? (\n                    <span className=\"shrink-0 font-mono text-[11px] text-ns-muted\">\n                      {opt.hint}\n                    </span>\n                  ) : null}\n                </span>\n                {isSelected ? (\n                  <svg\n                    aria-hidden\n                    viewBox=\"0 0 16 16\"\n                    fill=\"none\"\n                    className=\"h-4 w-4 shrink-0 text-ns-accent\"\n                  >\n                    <path\n                      d=\"M3.5 8.5l3 3 6-6.5\"\n                      stroke=\"currentColor\"\n                      strokeWidth=\"1.5\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                    />\n                  </svg>\n                ) : null}\n              </li>\n            );\n          })}\n        </ul>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/select-caustic.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "select",
      "listbox",
      "form",
      "canvas",
      "glass",
      "keyboard",
      "micro-interaction"
    ],
    "instruction": "Build a single-select (DOM trigger button + listbox panel) whose surfaces are real frosted glass: each surface stacks a canvas 2D layer, then a translucent bg-surface span with backdrop-blur + saturate that frosts the canvas beneath it, then the content. The canvases draw caustics as 6 (trigger) / 8 (panel) radial-gradient blobs summed with globalCompositeOperation 'lighter' at 0.06-0.10 alpha, colors mixed between --ns-accent and --foreground, with EXPLICIT canvas style.width/height plus a DPR-2-clamped backing store, and a full clear + redraw every frame (no destination-in accumulation). Idle simmer: each blob drifts on independent sin/cos phase offsets at a 6-10 px/s peak (amplitude 10-16px with frequency = speed/amplitude), running only while the panel is open, onscreen (IntersectionObserver), and the tab is visible; the closed trigger shows one static frame and the rAF is fully asleep. Open: panel with transform-origin at the trigger top scales 0.96 to 1 over 180ms on cubic-bezier(0.16,1,0.3,1) via WAAPI while blob anchors start clustered at the origin and spring (k=60 s^-2, zeta=0.9, ~450ms settle) to positions redistributed evenly down the list height. Active-row lens: a radial displacement field of radius 56px pulls blob centers up to 6px toward the active row center plus a tight accent pool under the row; the lens position follows cursor-hover or keyboard-active targets (one unified target) on a spring k=140 s^-2, zeta=0.8, with a forced-settle deadline snapping it 1.0s after the last input; lens coordinates are panel-local offsets from getBoundingClientRect deltas, never absolute page coords. Keyboard: full listbox pattern — trigger aria-haspopup=listbox aria-expanded, Enter/Space/ArrowDown/ArrowUp open, focus moves to the listbox which tracks aria-activedescendant, Up/Down skip disabled options, Home/End, 500ms typeahead buffer with startsWith matching, Enter (or Space with an empty buffer) commits, Esc and outside pointerdown close and restore focus to the trigger, hover and keyboard drive the same lens. Ink is getComputedStyle-derived from --ns-accent/--foreground at mount and re-derived live by a MutationObserver on documentElement class so both themes render correctly. prefers-reduced-motion: static frost texture at anchor targets, no drift or lens, instant open/close. Guard zero-size hosts before sizing canvases (the panel is display:none while closed), and tear down every rAF, WAAPI animation, observer, listener, and typeahead timer on unmount. Demo: a deployment-settings card with a labeled project input, a Region select over believable region/code option data, a Failover select, and a footer status line."
  },
  "type": "registry:ui"
}