{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider-loupe",
  "title": "Slider Loupe",
  "description": "Slider read through a circular magnifying loupe riding the thumb: the tick beneath sits sharp at 1.8x inside the lens while the rest of the scale rests soft-focus; rubber-banding past the bounds wobbles the magnification on release.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/slider-loupe/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// LoupeSlider — single-value slider read through a circular magnifying loupe\n// riding the thumb. The tick scale is drawn twice from cached layers: a base\n// pass pre-blurred (ctx.filter blur(1.5px), rebuilt only on size/theme change)\n// at 55% alpha, and a sharp 1.8x-magnified pass inside a 44px circular clip\n// centered on the loupe — lens focus IS the readout. The loupe springs after\n// the thumb (k=180, zeta=0.75, slight optical lag); dragging past the bounds\n// rubber-bands at x0.3 (capped 24px) and release springs home (k=260,\n// zeta=0.7) while magnification wobbles 1.8 -> 2.0 -> 1.8 over 350ms.\n// Direct-DOM rAF that sleeps at a velocity epsilon, pauses offscreen, and\n// carries a 1s forced-settle deadline. All canvas ink is derived from CSS\n// tokens at mount and re-derived live on documentElement class changes.\n// ---------------------------------------------------------------------------\n\nconst LENS_R = 22; // px lens radius (44px loupe)\nconst PAD = 28; // track inset so the lens never clips at the ends\nconst MAG_BASE = 1.8;\nconst MAG_PEAK = 2.0;\nconst WOBBLE_MS = 350;\nconst LOUPE_K = 180; // loupe chase spring, s^-2\nconst LOUPE_ZETA = 0.75;\nconst HOME_K = 260; // rubber-band return / glide spring, s^-2\nconst HOME_ZETA = 0.7;\nconst RUBBER = 0.3; // overshoot gain past the bounds\nconst RUBBER_MAX = 24; // px overshoot cap\nconst SETTLE_MS = 1000; // forced-settle deadline\n\ntype Vec3 = readonly [number, number, number];\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 rgba(c: Vec3, a: number) {\n  return `rgba(${c[0]},${c[1]},${c[2]},${a})`;\n}\n\n// round a raw interval to a 1/2/5 decade so tick labels land on clean values\nfunction niceStep(raw: number) {\n  const mag = 10 ** Math.floor(Math.log10(Math.max(1e-9, raw)));\n  const n = raw / mag;\n  return (n <= 1 ? 1 : n <= 2 ? 2 : n <= 5 ? 5 : 10) * mag;\n}\n\nexport function LoupeSlider({\n  value,\n  defaultValue = 50,\n  min = 0,\n  max = 100,\n  step = 1,\n  tickStep,\n  majorEvery = 5,\n  formatLabel,\n  formatValue,\n  onValueChange,\n  className = \"\",\n  \"aria-label\": ariaLabel = \"Value\",\n}: {\n  /** controlled value; omit for uncontrolled */\n  value?: number;\n  /** uncontrolled initial value */\n  defaultValue?: number;\n  /** minimum allowed value */\n  min?: number;\n  /** maximum allowed value */\n  max?: number;\n  /** increment per keyboard/drag step */\n  step?: number;\n  /** minor tick interval in value units; auto-derived when omitted */\n  tickStep?: number;\n  /** every Nth minor tick is major and labeled */\n  majorEvery?: number;\n  /** tick label text (drawn on canvas) */\n  formatLabel?: (v: number) => string;\n  /** aria-valuetext for the current value */\n  formatValue?: (v: number) => string;\n  /** called with the new value on every drag/keyboard change */\n  onValueChange?: (v: number) => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n  /** accessible name for the slider. Default \"Value\". */\n  \"aria-label\"?: string;\n}) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const loupeRef = useRef<HTMLDivElement>(null);\n  const thumbRef = useRef<HTMLDivElement>(null);\n  const engineRef = useRef<{\n    dragStart: (clientX: number) => void;\n    dragMove: (clientX: number) => void;\n    dragEnd: () => void;\n    glideTo: (v: number) => void;\n    dragging: () => boolean;\n  } | null>(null);\n\n  const isControlled = value !== undefined;\n  const [internal, setInternal] = useState(() =>\n    Math.min(max, Math.max(min, defaultValue))\n  );\n  const current = isControlled ? value : internal;\n  const currentRef = useRef(current);\n  currentRef.current = current;\n  // mirrors :hover into data-hover — synthetic/dispatched pointer events never\n  // flip Chromium's real hover pseudo-class, so an autoplay driver using them\n  // needs an attribute to target (additive, doesn't change real-pointer feel)\n  const [hover, setHover] = useState(false);\n\n  const defaultLabel = (v: number) =>\n    step < 1 ? v.toFixed(1) : String(Math.round(v));\n  const fmtLabel = formatLabel ?? defaultLabel;\n  const fmtValue = formatValue ?? fmtLabel;\n  const labelRef = useRef(fmtLabel);\n  labelRef.current = fmtLabel;\n\n  const commitRef = useRef<(v: number) => void>(() => {});\n  commitRef.current = (v: number) => {\n    if (!isControlled) setInternal(v);\n    onValueChange?.(v);\n  };\n\n  useEffect(() => {\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    const loupe = loupeRef.current;\n    const thumb = thumbRef.current;\n    if (!container || !canvas || !loupe || !thumb) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) 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 fg: Vec3 = [237, 237, 237];\n    let bd: Vec3 = [46, 46, 46];\n    let monoFam = \"ui-monospace, SFMono-Regular, Menlo, monospace\";\n    const derive = () => {\n      const cs = getComputedStyle(document.documentElement);\n      fg =\n        parseColor(cs.getPropertyValue(\"--foreground\")) ??\n        parseColor(getComputedStyle(container).color) ??\n        fg;\n      bd = parseColor(cs.getPropertyValue(\"--border\")) ?? bd;\n      const fam = getComputedStyle(container).fontFamily;\n      if (fam) monoFam = fam;\n    };\n    derive();\n\n    // -- hot-path state: locals only, never React state ---------------------\n    let w = 0;\n    let h = 0;\n    let cy = 32;\n    let dpr = 1;\n    let sized = false;\n    let raf = 0;\n    let last = 0;\n    let visible = true;\n    let tx = 0; // thumb x (canvas coords)\n    let tv = 0;\n    let lx = 0; // loupe x, springs after the thumb\n    let lv = 0;\n    let targetX = 0;\n    let mag = MAG_BASE;\n    let wobbleAt = -1; // performance.now() when the wobble started\n    let dragging = false;\n    let committed = currentRef.current;\n    let deadline = 0; // forced-settle deadline (performance.now())\n\n    // cached layers: sharp scale at 2x supersample (stays crisp when the lens\n    // magnifies it), blurred base derived from it once per size/theme change\n    const sharp = document.createElement(\"canvas\");\n    const soft = document.createElement(\"canvas\");\n\n    const span = () => Math.max(1e-9, max - min);\n    const xFor = (v: number) =>\n      PAD + ((v - min) / span()) * Math.max(1, w - PAD * 2);\n    const quantize = (v: number) => {\n      const q = min + Math.round((v - min) / step) * step;\n      return Math.min(max, Math.max(min, Number(q.toFixed(6))));\n    };\n\n    // geometry hugs the lens center: the clip radius is LENS_R*mag/MAG_BASE\n    // and the draw pass is scaled by `mag` around the same center, so a\n    // pre-mag point at offset d from (lx,cy) lands at d*mag post-transform —\n    // the mag-INDEPENDENT safe envelope is |d| <= LENS_R/MAG_BASE (R0),\n    // regardless of the 1.8->2.0 wobble. That only bounds height though:\n    // labels also need their magnified WIDTH inside the chord at that\n    // offset, so they sit at the lens's vertical center (dy small, near 0)\n    // for the widest available chord, AND get measured + font-shrunk per\n    // label so arbitrary formatLabel strings (not just 2-digit numbers)\n    // never get sliced by the rim.\n    const drawScale = (g: CanvasRenderingContext2D) => {\n      let minor = tickStep && tickStep > 0 ? tickStep : niceStep(span() / 60);\n      while (span() / minor > 240) minor *= 2; // cap the tick count\n      const every = majorEvery > 0 ? Math.round(majorEvery) : 5;\n      const axisY = cy - 12;\n      g.strokeStyle = rgba(bd, 1);\n      g.lineWidth = 1;\n      g.beginPath();\n      g.moveTo(PAD, axisY + 0.5);\n      g.lineTo(w - PAD, axisY + 0.5);\n      g.stroke();\n      const count = Math.floor(span() / minor + 1e-6);\n      // minor ticks\n      g.strokeStyle = rgba(fg, 0.45);\n      g.beginPath();\n      for (let i = 0; i <= count; i++) {\n        if (i % every === 0) continue;\n        const x = Math.round(xFor(min + i * minor)) + 0.5;\n        g.moveTo(x, axisY);\n        g.lineTo(x, axisY + 6);\n      }\n      g.stroke();\n      // major ticks + labels\n      g.strokeStyle = rgba(fg, 0.8);\n      g.beginPath();\n      for (let i = 0; i <= count; i += every) {\n        const x = Math.round(xFor(min + i * minor)) + 0.5;\n        g.moveTo(x, axisY);\n        g.lineTo(x, axisY + 10);\n      }\n      g.stroke();\n      // labels: near lens-center for max chord width, font shrunk per-label\n      // when it would still overflow that chord (see comment above drawScale)\n      const R0 = LENS_R / MAG_BASE;\n      const labelDy = 2; // small clearance below the major tick tips (-2)\n      const maxWidth =\n        2 * Math.sqrt(Math.max(0, R0 * R0 - labelDy * labelDy)) * 0.92;\n      const baseFontPx = 9;\n      g.textAlign = \"center\";\n      g.textBaseline = \"middle\";\n      g.fillStyle = rgba(fg, 0.9);\n      for (let i = 0; i <= count; i += every) {\n        const v = Number((min + i * minor).toFixed(6));\n        const text = labelRef.current(v);\n        g.font = `500 ${baseFontPx}px ${monoFam}`;\n        const measured = g.measureText(text).width;\n        const fontPx =\n          measured > maxWidth && measured > 0\n            ? Math.max(5, baseFontPx * (maxWidth / measured))\n            : baseFontPx;\n        if (fontPx !== baseFontPx) g.font = `500 ${fontPx}px ${monoFam}`;\n        g.fillText(text, xFor(v), cy + labelDy);\n      }\n    };\n\n    const buildLayers = () => {\n      if (!sized) return;\n      const S = dpr * 2; // supersample so the 1.8-2.0x pass stays sharp\n      sharp.width = Math.ceil(w * S);\n      sharp.height = Math.ceil(h * S);\n      const sctx = sharp.getContext(\"2d\");\n      if (!sctx) return;\n      sctx.setTransform(S, 0, 0, S, 0, 0);\n      sctx.clearRect(0, 0, w, h);\n      drawScale(sctx);\n      soft.width = Math.ceil(w * dpr);\n      soft.height = Math.ceil(h * dpr);\n      const bctx = soft.getContext(\"2d\");\n      if (!bctx) return;\n      bctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      bctx.clearRect(0, 0, w, h);\n      bctx.filter = \"blur(1.5px)\";\n      if (bctx.filter.indexOf(\"blur\") !== -1) {\n        bctx.drawImage(sharp, 0, 0, w, h);\n        bctx.filter = \"none\";\n      } else {\n        // no ctx.filter support: approximate with a 9-tap ring average\n        bctx.globalAlpha = 1 / 9;\n        bctx.drawImage(sharp, 0, 0, w, h);\n        for (let k = 0; k < 8; k++) {\n          const a = (k / 8) * Math.PI * 2;\n          bctx.drawImage(sharp, Math.cos(a) * 1.3, Math.sin(a) * 1.3, w, h);\n        }\n        bctx.globalAlpha = 1;\n      }\n    };\n\n    // full clear + composite from cached layers every frame — no filter work\n    // on the hot path, no alpha accumulation\n    const render = () => {\n      if (!sized) return;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.clearRect(0, 0, w, h);\n      ctx.globalAlpha = 0.55;\n      ctx.drawImage(soft, 0, 0, w, h);\n      ctx.globalAlpha = 1;\n      const r = LENS_R * (mag / MAG_BASE);\n      ctx.save();\n      ctx.beginPath();\n      ctx.arc(lx, cy, r, 0, Math.PI * 2);\n      ctx.clip();\n      ctx.clearRect(0, 0, w, h); // knock the soft pass out of the lens\n      ctx.translate(lx, cy);\n      ctx.scale(mag, mag);\n      ctx.translate(-lx, -cy);\n      ctx.drawImage(sharp, 0, 0, w, h);\n      ctx.restore();\n      // loupe + thumb are DOM nodes on OFFSET transforms from the container's\n      // top-left anchor — never absolute canvas coords\n      const ringScale = mag / MAG_BASE;\n      loupe.style.transform = `translate3d(${(lx - LENS_R).toFixed(2)}px, ${(\n        cy - LENS_R\n      ).toFixed(2)}px, 0) scale(${ringScale.toFixed(4)})`;\n      thumb.style.transform = `translate3d(${(tx - 12).toFixed(2)}px, ${(\n        cy - 12\n      ).toFixed(2)}px, 0)`;\n    };\n\n    const resize = () => {\n      const rect = container.getBoundingClientRect();\n      if (rect.width < PAD * 2 + 8 || rect.height < 8) {\n        sized = false; // zero-size guard — never draw into nothing\n        return;\n      }\n      w = rect.width;\n      h = rect.height;\n      cy = h / 2;\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      // a <canvas> is a replaced element: size the CSS box EXPLICITLY, or it\n      // renders at dpr scale\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      sized = true;\n      buildLayers();\n      targetX = xFor(currentRef.current);\n      if (!raf) {\n        tx = targetX;\n        tv = 0;\n        lx = targetX;\n        lv = 0;\n      }\n      render();\n    };\n    resize();\n\n    const loop = (now: number) => {\n      raf = 0;\n      const dt = last ? Math.min(0.033, (now - last) / 1000) : 1 / 60;\n      last = now;\n      let active = false;\n\n      if (dragging) {\n        active = true; // thumb is pinned to the pointer; loupe still chases\n      } else {\n        const c = 2 * HOME_ZETA * Math.sqrt(HOME_K);\n        tv += (-HOME_K * (tx - targetX) - c * tv) * dt;\n        tx += tv * dt;\n        if (Math.abs(tx - targetX) < 0.05 && Math.abs(tv) < 0.5) {\n          tx = targetX;\n          tv = 0;\n        } else active = true;\n      }\n\n      const cl = 2 * LOUPE_ZETA * Math.sqrt(LOUPE_K);\n      lv += (-LOUPE_K * (lx - tx) - cl * lv) * dt;\n      lx += lv * dt;\n      if (!dragging && Math.abs(lx - tx) < 0.05 && Math.abs(lv) < 0.5) {\n        lx = tx;\n        lv = 0;\n      } else if (Math.abs(lx - tx) >= 0.05 || Math.abs(lv) >= 0.5) {\n        active = true;\n      }\n\n      if (wobbleAt >= 0) {\n        const p = (now - wobbleAt) / WOBBLE_MS;\n        if (p >= 1) {\n          wobbleAt = -1;\n          mag = MAG_BASE;\n        } else {\n          // half-sine out-and-back, tail damped: 1.8 -> 2.0 -> 1.8\n          mag =\n            MAG_BASE +\n            (MAG_PEAK - MAG_BASE) * Math.sin(Math.PI * p) * (1 - 0.25 * p);\n          active = true;\n        }\n      }\n\n      // forced-settle deadline — springs AND wobble both answer to it\n      if (active && !dragging && now > deadline) {\n        tx = targetX;\n        tv = 0;\n        lx = targetX;\n        lv = 0;\n        mag = MAG_BASE;\n        wobbleAt = -1;\n        active = false;\n      }\n\n      render();\n      if (active && visible && !document.hidden) {\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const wake = () => {\n      if (!raf && sized) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    // -- interaction (called from React handlers via engineRef) -------------\n    const dragMove = (clientX: number) => {\n      if (!sized) return;\n      const rect = canvas.getBoundingClientRect();\n      const x = clientX - rect.left;\n      const lo = PAD;\n      const hi = w - PAD;\n      let px = x;\n      if (x < lo) px = lo + Math.max(-RUBBER_MAX, (x - lo) * RUBBER);\n      else if (x > hi) px = hi + Math.min(RUBBER_MAX, (x - hi) * RUBBER);\n      const clamped = Math.min(hi, Math.max(lo, x));\n      const v = quantize(min + ((clamped - lo) / Math.max(1, hi - lo)) * span());\n      targetX = xFor(v); // release springs home to the committed value\n      if (v !== committed) {\n        committed = v;\n        commitRef.current(v);\n      }\n      deadline = performance.now() + SETTLE_MS;\n      if (reduced) {\n        tx = px;\n        lx = px;\n        tv = 0;\n        lv = 0;\n        render();\n        return;\n      }\n      tx = px;\n      wake();\n    };\n\n    const dragStart = (clientX: number) => {\n      if (!sized) return;\n      dragging = true;\n      committed = currentRef.current;\n      dragMove(clientX);\n    };\n\n    const dragEnd = () => {\n      if (!dragging) return;\n      dragging = false;\n      const past = tx < PAD - 0.5 || tx > w - PAD + 0.5;\n      deadline = performance.now() + SETTLE_MS;\n      if (reduced) {\n        tx = targetX;\n        lx = targetX;\n        tv = 0;\n        lv = 0;\n        render(); // no wobble, instant reposition — magnification itself stays\n        return;\n      }\n      if (past) wobbleAt = performance.now(); // refractive wobble on release\n      wake();\n    };\n\n    const glideTo = (v: number) => {\n      if (dragging || !sized) return;\n      committed = v;\n      targetX = xFor(v);\n      deadline = performance.now() + SETTLE_MS;\n      if (reduced) {\n        tx = targetX;\n        lx = targetX;\n        tv = 0;\n        lv = 0;\n        render();\n        return;\n      }\n      wake();\n    };\n\n    engineRef.current = {\n      dragStart,\n      dragMove,\n      dragEnd,\n      glideTo,\n      dragging: () => dragging,\n    };\n\n    // -- observers ----------------------------------------------------------\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) wake();\n    });\n    io.observe(container);\n    // live theme re-derive: rebuild the blur layer on documentElement flips\n    const mo = new MutationObserver(() => {\n      derive();\n      buildLayers();\n      render();\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      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n      engineRef.current = null;\n    };\n  }, [min, max, step, tickStep, majorEvery]);\n\n  // keyboard + external value changes glide the loupe on the same spring\n  useEffect(() => {\n    engineRef.current?.glideTo(current);\n  }, [current]);\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    let next: number | null = null;\n    const v = currentRef.current;\n    switch (e.key) {\n      case \"ArrowRight\":\n      case \"ArrowUp\":\n        next = v + step;\n        break;\n      case \"ArrowLeft\":\n      case \"ArrowDown\":\n        next = v - step;\n        break;\n      case \"PageUp\":\n        next = v + step * 10;\n        break;\n      case \"PageDown\":\n        next = v - step * 10;\n        break;\n      case \"Home\":\n        next = min;\n        break;\n      case \"End\":\n        next = max;\n        break;\n      default:\n        return;\n    }\n    e.preventDefault();\n    next = Math.min(max, Math.max(min, Number(next.toFixed(6))));\n    if (next !== v) commitRef.current(next);\n  };\n\n  return (\n    <div\n      ref={containerRef}\n      data-hover={hover}\n      className={`group relative h-16 w-full cursor-ew-resize touch-none select-none font-mono ${className}`}\n      onPointerDown={(e) => {\n        e.preventDefault();\n        e.currentTarget.setPointerCapture(e.pointerId);\n        thumbRef.current?.focus({ preventScroll: true });\n        engineRef.current?.dragStart(e.clientX);\n      }}\n      onPointerMove={(e) => {\n        if (engineRef.current?.dragging()) engineRef.current.dragMove(e.clientX);\n      }}\n      onPointerUp={() => engineRef.current?.dragEnd()}\n      onPointerCancel={() => engineRef.current?.dragEnd()}\n      onPointerEnter={() => setHover(true)}\n      onPointerLeave={() => setHover(false)}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className=\"pointer-events-none absolute left-0 top-0\"\n      />\n      <div\n        ref={loupeRef}\n        aria-hidden\n        className=\"pointer-events-none absolute left-0 top-0 h-11 w-11 rounded-full border border-border shadow-[0_2px_10px_-4px_rgba(0,0,0,0.4)] transition-colors duration-200 will-change-transform group-hover:border-foreground/40 group-data-[hover=true]:border-foreground/40\"\n      />\n      <div\n        ref={thumbRef}\n        role=\"slider\"\n        tabIndex={0}\n        aria-label={ariaLabel}\n        aria-orientation=\"horizontal\"\n        aria-valuemin={min}\n        aria-valuemax={max}\n        aria-valuenow={current}\n        aria-valuetext={fmtValue(current)}\n        onKeyDown={onKeyDown}\n        className=\"absolute left-0 top-0 h-6 w-6 rounded-full outline-none will-change-transform focus-visible:ring-2 focus-visible:ring-ns-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n      >\n        <span\n          aria-hidden\n          className=\"absolute left-1/2 top-1/2 h-4 w-0.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground/80 transition-colors duration-200 group-hover:bg-foreground group-data-[hover=true]:bg-foreground\"\n        />\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/slider-loupe.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-accent": "#006bff"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "slider",
      "input",
      "form",
      "canvas",
      "magnifier",
      "micro-interaction"
    ],
    "instruction": "Build a single-value slider whose readout is a 44px circular magnifying loupe riding the thumb. RENDER: canvas 2D draws the tick scale + labels twice from cached layers rebuilt only on size/theme change — a sharp layer supersampled at 2x dpr (so magnified pixels stay crisp) and a soft base derived from it once via ctx.filter blur(1.5px) into an offscreen canvas (9-tap ring-average fallback when ctx.filter is unsupported). Per frame: full clear, soft layer at 55% alpha, then a circular clip centered on the loupe (radius 22 x mag/1.8), clearRect inside the clip to knock out the soft pass, and the sharp layer drawn through translate(lens) scale(mag) translate(-lens) at 1.8x. No per-frame filter work, no alpha accumulation. Canvas gets EXPLICIT style.width/height (replaced element — inset does not size it); DPR clamp 2. Thumb (2x16px foreground hairline in a 24px focus circle) and loupe ring (44px, border-border, hover border-foreground/40) are DOM nodes on offset transforms from the container's top-left anchor, never absolute canvas coords. MOTION: loupe position springs after the thumb at k=180 s^-2, zeta=0.75 for slight optical lag; the ring also scales mag/1.8 so the wobble reads refractive. Dragging past min/max maps overshoot x0.3 capped 24px; release springs home k=260, zeta=0.7 while magnification wobbles 1.8 -> 2.0 -> 1.8 on a damped half-sine over 350ms. Forced-settle deadline 1.0s covers springs AND wobble. Direct-DOM rAF: sleeps at velocity epsilon (0.05px / 0.5px/s), pauses offscreen via IntersectionObserver and on document.hidden, zero-size containers guarded before any draw. INTERACTION: focusable thumb with role=slider, aria-valuemin/max/now/valuetext, aria-orientation; ArrowLeft/Right/Up/Down +-step, PageUp/Down +-10 steps, Home/End; keyboard and external value changes glide on the same k=260 spring with the loupe chasing; pointer drag with setPointerCapture pins the thumb while the loupe lags; accent focus-visible ring with background ring-offset. TOKENS: all canvas ink (--foreground, --border) and the mono label font are read via getComputedStyle at mount and re-derived by a MutationObserver on documentElement class changes, rebuilding the blur layer on theme flip — works in both themes. REDUCED MOTION: thumb and loupe reposition instantly, no wobble; the 1.8x magnification itself stays (informational, not decorative). Tick generation: minor interval prop or auto 1/2/5-decade nice step, every Nth tick major + labeled, count capped at 240, custom formatLabel. Controlled/uncontrolled value, typed props with defaults so the bare component mounts. Tear down rAF, ResizeObserver, IntersectionObserver, MutationObserver, and the visibilitychange listener on unmount. DEMO: audio playback card on a padded surface — loupe scrub slider over an m:ss timestamp scale with live mono readout, skip/play/pause transport, plus a second instance as a waveform zoom-level control labeled in percent."
  },
  "type": "registry:ui"
}