{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "meter-quota-meniscus",
  "title": "Meter Quota Meniscus",
  "description": "A quota (storage, API budget, seats) rendered as liquid in a thin vessel where surface curvature, not level, carries the reading: concave under the soft limit, flat at it, convex and overfull-but-held in the grace zone, and a bead that breaks off past the hard limit leaving a permanent stain.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/meter-quota-meniscus/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// MeniscusMeter — a quota (storage, API budget, seats) rendered as liquid in\n// a thin open-top vessel, where the fill's top-edge CURVATURE, not its level,\n// carries the reading. Under the soft limit the level rises normally and the\n// edge is concave (wets the walls, dips at center — climbs higher at the\n// walls than the middle, same physical tell as water in a narrow glass tube).\n// Exactly at the soft limit it flattens. Between soft and hard the level\n// pins at the rim and the edge inverts to convex, bulging progressively\n// higher above the rim as the value approaches the hard cap — visibly\n// overfull, still held by surface tension, no color change required to read\n// \"tolerated but over.\" The instant value crosses the hard limit a bead\n// detaches from the outer wall and eases down under gravity, then a static\n// 1px --ns-muted stain remains — the crossing becomes a permanent mark, not\n// just a momentary state. The whole shape is one SVG cubic bezier: fixed\n// endpoints at the current level, two shared control points whose y-offset\n// from that level is the single scalar that walks concave -> flat -> convex.\n// A CSS transition on `d` with a back-out (overshoot) timing function is the\n// \"soft spring\" — one wobble past the target, then it settles, no JS\n// simulation loop needed. `role=meter` carries the real semantics (SVG is\n// aria-hidden); a Geist Mono numeric readout beside the vessel means\n// curvature is never the only signal. Every stroke/fill is a token-driven\n// Tailwind class (text-border/text-ns-muted/text-foreground/text-ns-accent) at\n// low opacity — no hex, no canvas, no getComputedStyle needed since nothing\n// here is a raster surface.\n// ---------------------------------------------------------------------------\n\nexport interface MeniscusMeterProps {\n  /** current usage, same unit as softLimit/hardLimit */\n  value: number;\n  /** the soft threshold — the vessel's rim; reaching it flattens the surface */\n  softLimit: number;\n  /** the hard cap — crossing it detaches a bead and leaves a stain */\n  hardLimit: number;\n  /** unit suffix for every readout, e.g. \"%\", \"GB\", \"seats\" (default \"%\") */\n  unit?: string;\n  /** what's being metered, shown above the vessel, e.g. \"Storage\" */\n  label?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\ntype Zone = \"headroom\" | \"grace\" | \"spill\";\n\n// vessel geometry, SVG viewBox units\nconst VIEW_W = 96;\nconst VIEW_H = 168;\nconst WALL_L = 30;\nconst WALL_R = 66;\nconst RIM_Y = 34;\nconst FLOOR_Y = 142;\nconst MAX_CONCAVE = 9; // control-point dip at value = 0 (deep headroom)\nconst MAX_CONVEX = 20; // control-point rise at value = hardLimit (max dome)\n// a two-control-point cubic's midpoint sits at 0.25*end + 0.75*control, so a\n// concave dip of MAX_CONCAVE bows the visible trough ~0.75*MAX_CONCAVE below\n// the wall-anchor point — reserve that much clearance above FLOOR_Y so an\n// empty vessel's dipped meniscus never renders below the drawn floor line.\nconst LEVEL_FLOOR_Y = FLOOR_Y - Math.ceil(MAX_CONCAVE * 0.75) - 3;\nconst BEAD_X = WALL_R + 4;\nconst DRIP_LEN = 46; // stain length below the rim\n\nfunction clamp(n: number, lo: number, hi: number) {\n  return Math.max(lo, Math.min(hi, n));\n}\n\nfunction fmt(n: number) {\n  if (!Number.isFinite(n)) return \"0\";\n  const abs = Math.abs(n);\n  if (Number.isInteger(n) || abs >= 100) return Math.round(n).toString();\n  return n.toFixed(1);\n}\n\nfunction useReducedMotion() {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\nfunction zoneAnnounce(zone: Zone) {\n  if (zone === \"spill\") return \"over hard limit — overflowing\";\n  if (zone === \"grace\") return \"over soft limit — still held\";\n  return \"within soft limit\";\n}\n\nfunction zoneShort(zone: Zone) {\n  if (zone === \"spill\") return \"over hard limit\";\n  if (zone === \"grace\") return \"over soft limit\";\n  return \"under soft limit\";\n}\n\nfunction surface(value: number, softLimit: number, hardLimit: number) {\n  const soft = Math.max(1e-6, softLimit);\n  const hard = Math.max(soft + 1e-6, hardLimit);\n  let levelY: number;\n  let curveOffset: number; // positive = convex dome, negative = concave dip\n  let zone: Zone;\n  if (value <= soft) {\n    const t = clamp(value / soft, 0, 1); // 0 empty -> 1 at rim\n    levelY = LEVEL_FLOOR_Y - t * (LEVEL_FLOOR_Y - RIM_Y);\n    curveOffset = -MAX_CONCAVE * (1 - t);\n    zone = \"headroom\";\n  } else {\n    levelY = RIM_Y; // pinned — the level itself never rises further\n    const t = clamp((value - soft) / (hard - soft), 0, 1);\n    curveOffset = MAX_CONVEX * t;\n    zone = value >= hard ? \"spill\" : \"grace\";\n  }\n  return { levelY, curveOffset, zone };\n}\n\nfunction surfacePath(levelY: number, curveOffset: number) {\n  const cpY = levelY - curveOffset;\n  const cp1x = WALL_L + (WALL_R - WALL_L) * 0.33;\n  const cp2x = WALL_L + (WALL_R - WALL_L) * 0.67;\n  return `M ${WALL_L} ${levelY} C ${cp1x} ${cpY} ${cp2x} ${cpY} ${WALL_R} ${levelY}`;\n}\n\nfunction fillPath(levelY: number, curveOffset: number) {\n  return `${surfacePath(levelY, curveOffset)} L ${WALL_R} ${FLOOR_Y} L ${WALL_L} ${FLOOR_Y} Z`;\n}\n\nexport function MeniscusMeter({\n  value,\n  softLimit,\n  hardLimit,\n  unit = \"%\",\n  label = \"Usage\",\n  className = \"\",\n}: MeniscusMeterProps) {\n  const uid = useId();\n  const reduced = useReducedMotion();\n  const { levelY, curveOffset, zone } = useMemo(\n    () => surface(value, softLimit, hardLimit),\n    [value, softLimit, hardLimit]\n  );\n\n  const prevZoneRef = useRef<Zone>(zone);\n  const [stained, setStained] = useState(zone === \"spill\");\n  const [beadTick, setBeadTick] = useState(0);\n  const [announce, setAnnounce] = useState(() => zoneAnnounce(zone));\n\n  useEffect(() => {\n    const prev = prevZoneRef.current;\n    if (prev !== zone) {\n      setAnnounce(zoneAnnounce(zone));\n      if (zone === \"spill\") {\n        setStained(true);\n        if (prev !== \"spill\") setBeadTick((k) => k + 1);\n      } else if (zone === \"headroom\") {\n        setStained(false);\n      }\n      prevZoneRef.current = zone;\n    }\n  }, [zone]);\n\n  const labelId = `${uid}-label`;\n  const liveId = `${uid}-live`;\n  const unitSuffix = unit === \"%\" ? \"%\" : unit ? ` ${unit}` : \"\";\n  const valueText = `${fmt(value)}${unitSuffix}, ${zoneShort(zone)}`;\n  const d = fillPath(levelY, curveOffset);\n  const surfaceD = surfacePath(levelY, curveOffset);\n\n  return (\n    <div className={className}>\n      <style>{`\n.ns-meniscus-fill,.ns-meniscus-surface{transition:d 700ms cubic-bezier(0.34,1.56,0.64,1)}\n@keyframes ns-meniscus-drip{\n  0%{transform:translateY(0);opacity:1}\n  70%{opacity:1}\n  100%{transform:translateY(${DRIP_LEN}px);opacity:0}\n}\n.ns-meniscus-bead{animation:ns-meniscus-drip 900ms cubic-bezier(0.55,0,0.85,0.35) 1}\n@media (prefers-reduced-motion: reduce){\n  .ns-meniscus-fill,.ns-meniscus-surface{transition-duration:140ms;transition-timing-function:linear}\n  .ns-meniscus-bead{animation:none}\n}\n`}</style>\n\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <span id={labelId} className=\"font-mono text-[11px] tracking-wide text-ns-muted\">\n          {label.toUpperCase()}\n        </span>\n        <span\n          className={\n            \"font-mono text-[11px] tracking-wide text-foreground \" +\n            (zone !== \"headroom\" ? \"font-semibold\" : \"\")\n          }\n        >\n          {zoneShort(zone)}\n        </span>\n      </div>\n\n      <div className=\"mt-1 flex items-baseline gap-1.5\">\n        <span className=\"text-2xl font-semibold tabular-nums text-foreground\">\n          {fmt(value)}\n          {unit === \"%\" ? \"%\" : \"\"}\n        </span>\n        <span className=\"font-mono text-xs text-ns-muted\">\n          / {fmt(hardLimit)}\n          {unit !== \"%\" ? ` ${unit}` : \"\"} cap\n        </span>\n      </div>\n\n      <div\n        role=\"meter\"\n        aria-labelledby={labelId}\n        aria-valuemin={0}\n        aria-valuemax={hardLimit}\n        aria-valuenow={value}\n        aria-valuetext={valueText}\n        className=\"mt-3 flex justify-center\"\n      >\n        <svg\n          viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}\n          className=\"h-[168px] w-[96px]\"\n          aria-hidden\n          focusable=\"false\"\n        >\n          {/* floor */}\n          <line\n            x1={WALL_L}\n            x2={WALL_R}\n            y1={FLOOR_Y}\n            y2={FLOOR_Y}\n            className=\"stroke-current text-border\"\n            strokeWidth={1.5}\n          />\n          {/* liquid body, fill under the meniscus curve */}\n          <path d={d} className=\"ns-meniscus-fill fill-current text-foreground opacity-[0.14]\" />\n          {/* surface line, more opaque than the body fill */}\n          <path\n            d={surfaceD}\n            fill=\"none\"\n            className=\"ns-meniscus-surface stroke-current text-foreground\"\n            strokeWidth={1.5}\n            strokeLinecap=\"round\"\n          />\n          {/* walls, stop exactly at the rim so a grace-zone dome visibly rises\n              into open space above them, unconfined */}\n          <line\n            x1={WALL_L}\n            x2={WALL_L}\n            y1={RIM_Y}\n            y2={FLOOR_Y}\n            className=\"stroke-current text-border\"\n            strokeWidth={2}\n          />\n          <line\n            x1={WALL_R}\n            x2={WALL_R}\n            y1={RIM_Y}\n            y2={FLOOR_Y}\n            className=\"stroke-current text-border\"\n            strokeWidth={2}\n          />\n          {/* rim lip ticks */}\n          <line\n            x1={WALL_L - 3}\n            x2={WALL_L + 3}\n            y1={RIM_Y}\n            y2={RIM_Y}\n            className=\"stroke-current text-border\"\n            strokeWidth={1.5}\n          />\n          <line\n            x1={WALL_R - 3}\n            x2={WALL_R + 3}\n            y1={RIM_Y}\n            y2={RIM_Y}\n            className=\"stroke-current text-border\"\n            strokeWidth={1.5}\n          />\n          {/* stain — static, left by the hard-limit crossing */}\n          {stained ? (\n            <line\n              x1={BEAD_X}\n              x2={BEAD_X}\n              y1={RIM_Y}\n              y2={RIM_Y + DRIP_LEN}\n              className=\"stroke-current text-ns-muted opacity-60\"\n              strokeWidth={1}\n            />\n          ) : null}\n          {/* bead — one-shot drop animation replayed via key on each crossing */}\n          {!reduced && beadTick > 0 ? (\n            <circle\n              key={beadTick}\n              cx={BEAD_X}\n              cy={RIM_Y}\n              r={2.25}\n              className=\"ns-meniscus-bead fill-current text-ns-muted\"\n            />\n          ) : null}\n        </svg>\n      </div>\n\n      <p className=\"mt-2 text-center font-mono text-[11px] text-ns-muted\">\n        {zone === \"spill\"\n          ? \"held — overflow marked on the glass\"\n          : zone === \"grace\"\n            ? \"over the soft limit, still tolerated\"\n            : `${fmt(Math.max(0, softLimit - value))}${unitSuffix} of headroom left`}\n      </p>\n\n      <span id={liveId} role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/meter-quota-meniscus.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "meter",
      "quota",
      "threshold",
      "gauge",
      "svg",
      "spring",
      "status",
      "accessibility"
    ],
    "instruction": "Renders one quota reading — `value` against a `softLimit` and a `hardLimit`, all in the same `unit` (default \"%\") — as liquid in an open-top SVG vessel (two vertical wall lines plus a floor line, all `stroke-current text-border`) whose fill's top edge is a single cubic bezier: fixed endpoints at the current `levelY`, two control points sharing one `curveOffset` scalar that is the entire mechanism. Below `softLimit`, `levelY` rises linearly from the floor toward the rim as `value` goes 0 -> softLimit, and `curveOffset` is negative and shrinking toward zero — control points sit below the endpoints, pulling the middle of the curve down so the edges read higher than the center: a concave meniscus wetting the walls, deepest at value=0, flattening as `value` approaches `softLimit`. At `softLimit` the curve is exactly flat (`curveOffset` = 0) and `levelY` reaches the rim exactly. Between `softLimit` and `hardLimit`, `levelY` stops rising — pinned at the rim, because the vessel is nominally full — and `curveOffset` instead goes positive and grows toward `hardLimit`: control points rise above the endpoints, bulging the middle upward past the two wall-line tops (which stop exactly at the rim y, leaving open space above them) into a visible convex dome, unconfined by any wall above that height — over budget, physically overfull, still held by the curve, no color change required to read the zone. The instant `value` reaches or passes `hardLimit`, `curveOffset` clamps at its maximum (the dome stays pinned at its fullest rather than growing further) and, edge-triggered on that specific crossing (a ref tracks the previous zone so re-renders at the same over-hard value don't replay it), a small `text-ns-muted` circle appears at a fixed point just outside the right wall and runs one `900ms` gravity-eased (`cubic-bezier(0.55,0,0.85,0.35)`, i.e. accelerating) keyframe animation down the outside of the glass, fading out as it lands; immediately behind it a static `1px` `text-ns-muted` line at 60% opacity remains from the rim down a fixed drip length — the stain, a permanent record of the crossing that persists through further value changes and only clears once `value` drops back to a clear headroom read (`<= softLimit`), not merely below `hardLimit`, so a value hovering just under the hard cap after a spill still shows the mark. Both `d`-valued SVG paths (the filled liquid body and the stroked surface line) share one CSS `transition: d 700ms cubic-bezier(0.34,1.56,0.64,1)` — a back-out timing function whose control points push the eased fraction past 1 before settling back to it, which is the 'soft spring': no JS simulation loop, the curve genuinely overshoots its new target once and stills on every `value`, `softLimit`, or `hardLimit` change. A Geist Mono readout sits above the vessel: the metered label and a bold zone chip ('under soft limit' / 'over soft limit' / 'over hard limit') on one line, the live value (with a trailing '%' when `unit` is '%', a space-prefixed unit otherwise) and '/ {hardLimit} cap' on the next, and a status caption below the vessel spelling out remaining headroom, 'over the soft limit, still tolerated', or 'held — overflow marked on the glass' depending on zone — so the reading is never dependent on perceiving curvature alone. The outer element is `role=meter` with `aria-labelledby` on the visible label, `aria-valuemin=0`, `aria-valuemax=hardLimit`, `aria-valuenow=value`, and `aria-valuetext` spelling e.g. '84%, over soft limit' (value, unit suffix, zone) — the exact sentence a screen reader gets on every read, not just at a boundary crossing. A separate visually-hidden `role=status`/`aria-live=polite` span announces a fuller zone-change sentence ('over soft limit — still held', 'over hard limit — overflowing', 'within soft limit') only when the zone itself changes, not on every value tick, so assistive tech isn't spammed mid-fill. The SVG itself is `aria-hidden` — decorative once the meter node and the two text readouts carry the real semantics — and being a passive display component with no exposed control, it is correctly exempt from Tab-reachability on its own (the demo's cycle button is what Tab actually reaches). Under `prefers-reduced-motion: reduce` (checked via `matchMedia` with a live change listener) the bead's keyframe animation is stripped entirely via a media-query CSS block (the stain still appears, just without the drop), but the `d` transition is shortened to a plain 140ms linear ease rather than removed — curvature IS the reading, so a value/zone change still visibly settles into its new shape instead of teleporting there, it just loses the back-out spring overshoot. Every stroke and fill is a `stroke-current`/`fill-current` Tailwind class tinted `text-border`, `text-ns-muted`, or `text-foreground` at low opacity — never a hex literal or `getComputedStyle` read, since nothing here is a raster surface — so both themes restyle for free. No canvas: two SVG `<path>` elements, three `<line>` walls/floor/lip-ticks, one conditional stain `<line>`, and one conditional bead `<circle>` are the entire visual, DOM+SVG+CSS only."
  },
  "type": "registry:ui"
}