{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mac-hud",
  "title": "Mac HUD",
  "description": "The macOS volume and brightness overlay: a frosted, click-through HUD in the classic 200×200 square or the macOS 26 pill. Repeat changes update it in place and restart its 1.1s hold; it never replays its entrance.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-mac.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/mac-hud/mac-hud.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useCallback, useEffect, useId, useRef, useState, type ReactNode } from \"react\";\nimport { MAC_RADIUS, macFont, macMaterial, type MacMaterialMode } from \"../lib/mac\";\nimport {\n  blur,\n  enter,\n  exit,\n  exitFast,\n  instant,\n  reducedFade,\n  smooth,\n  snappy,\n  swap,\n  swapIn,\n  swapOut,\n  swapShown,\n  useSafeReducedMotion,\n} from \"../lib/motion\";\n\nexport type MacHudKind = \"volume\" | \"brightness\" | \"keyboard\";\nexport type MacHudVariant = \"classic\" | \"pill\";\n\nexport type MacHudLabels = {\n  /** Announced before the level, e.g. \"Volume 60%\". Defaults: \"Volume\", \"Brightness\", \"Keyboard brightness\". */\n  volume: string;\n  brightness: string;\n  keyboard: string;\n  /** Announced instead of the level while muted: \"Volume, muted\". Default \"muted\". */\n  muted: string;\n};\n\nexport type MacHudProps = {\n  /** Show the HUD. Keep it mounted and toggle this so the exit animates; `useMacHud` drives it for you. */\n  open: boolean;\n  /** Which level it shows; picks the icon and the announcement. */\n  kind: MacHudKind;\n  /** The level, 0 to 1. Shown in quarter segments, the finest step macOS has (⌥⇧ + a media key). */\n  value: number;\n  /** Volume only: speaker with a slash, empty bar. */\n  muted?: boolean;\n  /** `classic` is the 200×200 square of macOS 15 and earlier; `pill` the top-right capsule of macOS 26. Default \"classic\". */\n  variant?: MacHudVariant;\n  /**\n   * `fixed` to the viewport (default), `absolute` in the nearest positioned parent, or `none`\n   * to place it yourself (a Tauri HUD window that is exactly the HUD's size).\n   */\n  position?: \"fixed\" | \"absolute\" | \"none\";\n  /** `css` draws the frosted material; `native` leaves it to a window with real vibrancy. Default \"css\". */\n  material?: MacMaterialMode;\n  /**\n   * Corner radius, px. Default 18 for classic, a full capsule for pill. In a native window, match\n   * the window's corner: Electron can't round vibrancy, and macOS gives a frameless window ~10.\n   */\n  radius?: number;\n  /** Called once the exit animation has finished, e.g. to hide a native HUD window. */\n  onExitComplete?: () => void;\n  labels?: Partial<MacHudLabels>;\n  /** Appended to the HUD's classes; position is kept. */\n  className?: string;\n};\n\nconst DEFAULT_LABELS: MacHudLabels = {\n  volume: \"Volume\",\n  brightness: \"Brightness\",\n  keyboard: \"Keyboard brightness\",\n  muted: \"muted\",\n};\n\nconst SEGMENTS = 16;\n\n// Classic sits centred 140px above the bottom edge, as on macOS; in a container shorter than\n// ~480px that would push it off the top, so it centres vertically instead.\nconst PLACE: Record<MacHudVariant, string> = {\n  classic: \"inset-x-0 mx-auto bottom-[max(0px,min(140px,calc(50%-100px)))]\",\n  pill: \"top-3 right-3\",\n};\nconst POSITION = { fixed: \"fixed z-50\", absolute: \"absolute z-10\", none: \"relative\" } as const;\n\nconst pct = new Intl.NumberFormat(undefined, { style: \"percent\", maximumFractionDigits: 0 });\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\n\n/**\n * The macOS volume and brightness overlay: a frosted, click-through HUD that updates in place\n * while it is up. Not anchored to anything, so it fades and scales in on its own and leaves\n * faster than it came; a new level never replays the entrance.\n */\nexport function MacHud({\n  open,\n  kind,\n  value,\n  muted = false,\n  variant = \"classic\",\n  position = \"fixed\",\n  material = \"css\",\n  radius,\n  onExitComplete,\n  labels,\n  className = \"\",\n}: MacHudProps) {\n  const reduce = useSafeReducedMotion();\n  const text = { ...DEFAULT_LABELS, ...labels };\n  const silent = kind === \"volume\" && muted;\n  const level = silent ? 0 : clamp01(value);\n  const pill = variant === \"pill\";\n\n  const announce = silent ? `${text[kind]}, ${text.muted}` : `${text[kind]} ${pct.format(clamp01(value))}`;\n\n  return (\n    <>\n      {/* A live region must exist before its text changes, so it stays mounted outside the HUD. */}\n      <span role=\"status\" className=\"sr-only\">\n        {open ? announce : \"\"}\n      </span>\n      {/* Only a real close counts: a variant swap while open also completes an exit. */}\n      <AnimatePresence onExitComplete={open ? undefined : onExitComplete}>\n        {open && (\n          <motion.div\n            // A new variant is a new HUD: the old one leaves while the new one enters, no morph.\n            key={variant}\n            aria-hidden\n            data-variant={variant}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.96, y: pill ? -10 : 0 }}\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            exit={\n              reduce\n                ? { opacity: 0, transition: reducedFade }\n                : { opacity: 0, scale: 0.98, y: pill ? -6 : 0, transition: exit }\n            }\n            transition={reduce ? reducedFade : { ...snappy, opacity: enter }}\n            style={{ borderRadius: radius ?? (pill ? MAC_RADIUS.pill : MAC_RADIUS.hud) }}\n            className={`pointer-events-none select-none ${POSITION[position]} ${position === \"none\" ? \"\" : PLACE[variant]} ${macFont} ${macMaterial(\"hud\", material)} ${\n              pill ? \"flex h-11 w-[200px] items-center gap-3 px-4\" : \"size-[200px]\"\n            } ${className}`}\n          >\n            {pill ? (\n              <>\n                <MacHudIcon kind={kind} value={value} muted={muted} className=\"size-5\" />\n                <PillBar level={level} reduce={reduce} />\n              </>\n            ) : (\n              <>\n                <MacHudIcon kind={kind} value={value} muted={muted} className=\"absolute inset-x-0 top-11 mx-auto size-20 opacity-70\" />\n                <Segments level={level} />\n              </>\n            )}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </>\n  );\n}\n\n/** Sixteen 7.5px squares, 2px apart (150px), 40px off the bottom. Each fills in quarters. */\nfunction Segments({ level }: { level: number }) {\n  return (\n    <div className=\"absolute inset-x-0 bottom-10 mx-auto flex w-[150px] gap-[2px]\">\n      {Array.from({ length: SEGMENTS }, (_, i) => {\n        // Quarter-segment resolution, like macOS. Fill and track never overlap, so the\n        // filled 70% and empty 20% stay exact rather than stacking.\n        const f = clamp01(Math.round((level * SEGMENTS - i) * 4) / 4);\n        return (\n          <span key={i} className=\"relative size-[7.5px] shrink-0\">\n            <span\n              className=\"absolute inset-0 bg-current/20 transition-[clip-path] duration-(--eased-duration-instant) ease-standard reduced:transition-none\"\n              style={{ clipPath: `inset(0 0 0 ${f * 100}%)` }}\n            />\n            <span\n              className=\"absolute inset-0 bg-current/70 transition-[clip-path] duration-(--eased-duration-instant) ease-standard reduced:transition-none\"\n              style={{ clipPath: `inset(0 ${(1 - f) * 100}% 0 0)` }}\n            />\n          </span>\n        );\n      })}\n    </div>\n  );\n}\n\n/** A 6px track whose fill slides on `smooth`: a translate, so no layout work per frame. */\nfunction PillBar({ level, reduce }: { level: number; reduce: boolean }) {\n  return (\n    <span className=\"relative h-1.5 min-w-0 flex-1 bg-current/20 [clip-path:inset(0_round_9999px)]\">\n      <motion.span\n        className=\"absolute inset-0 rounded-full bg-current/70\"\n        initial={false}\n        animate={{ x: `${(level - 1) * 100}%` }}\n        transition={reduce ? instant : smooth}\n      />\n    </span>\n  );\n}\n\n/* ---------- Icons: filled glyphs in the spirit of SF Symbols, on a 24-unit grid ---------- */\n\nconst STROKE = 1.8;\n\n/** Straight strokes from radius r1 to r2 around (cx, cy), one per angle in degrees (0 = right, 90 = up). */\nconst rays = (cx: number, cy: number, r1: number, r2: number, angles: number[]) =>\n  angles\n    .map((a) => {\n      const [c, s] = [Math.cos((a * Math.PI) / 180), -Math.sin((a * Math.PI) / 180)];\n      const p = (r: number) => `${+(cx + r * c).toFixed(2)} ${+(cy + r * s).toFixed(2)}`;\n      return `M${p(r1)}L${p(r2)}`;\n    })\n    .join(\"\");\n\n/** A wave of radius r around the speaker's mouth, spanning ±deg. */\nconst wave = (r: number, deg: number) => {\n  const [x, y] = [11 + r * Math.cos((deg * Math.PI) / 180), r * Math.sin((deg * Math.PI) / 180)];\n  return `M${x.toFixed(2)} ${(12 - y).toFixed(2)}A${r} ${r} 0 0 1 ${x.toFixed(2)} ${(12 + y).toFixed(2)}`;\n};\n\nconst SPEAKER = \"M3 9h3l4.3-3.6c.65-.55 1.7-.08 1.7.77v11.66c0 .85-1.05 1.32-1.7.77L6 15H3a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1Z\";\nconst WAVES = [wave(4, 45), wave(7, 50), wave(10, 52)];\nconst SLASH = \"M3.5 3.5 18.5 18.5\";\n// Shift (in grid units) that centres what is drawn: the speaker spans x 2–12, wave n ends at\n// 11 + r + half a stroke, the slash at 19.4. Three waves are centred as drawn.\nconst CENTRE = [5, 3.05, 1.55, 0];\nconst CENTRE_MUTED = 1.4;\nconst SUN_ALL = [0, 45, 90, 135, 180, 225, 270, 315];\nconst SUN = { min: rays(12, 12, 7, 7.6, SUN_ALL), max: rays(12, 12, 6.9, 9.4, SUN_ALL) };\nconst LIGHT_ALL = [0, 45, 90, 135, 180];\nconst LIGHT = { min: rays(12, 15.2, 7, 7.6, LIGHT_ALL), max: rays(12, 15.2, 6.9, 9.4, LIGHT_ALL) };\n\nexport type MacHudIconProps = {\n  kind: MacHudKind;\n  /** 0–1. Volume shows 0–3 waves; brightness and keyboard switch to longer rays from half. */\n  value: number;\n  muted?: boolean;\n  /** Size it here, e.g. `size-5`. Colour is `currentColor`. */\n  className?: string;\n};\n\n/**\n * The HUD's glyphs, also handy on your own media keys. Parts that come and go (a wave, the\n * mute slash, long rays) fade and un-blur on the text-swap tokens; a new kind swaps the whole\n * glyph with the text-swap rise.\n */\nexport function MacHudIcon({ kind, value, muted = false, className = \"\" }: MacHudIconProps) {\n  const reduce = useSafeReducedMotion();\n  const v = clamp01(value);\n  const waves = v === 0 ? 0 : Math.ceil(v * 3);\n  const bright = v >= 0.5;\n\n  return (\n    <span aria-hidden className={`relative block ${className}`}>\n      <AnimatePresence initial={false}>\n        <motion.span\n          key={kind}\n          className=\"absolute inset-0\"\n          initial={reduce ? { opacity: 0 } : swapIn}\n          animate={reduce ? { opacity: 1 } : swapShown}\n          exit={reduce ? { opacity: 0, transition: reducedFade } : { ...swapOut, transition: { ...swap, opacity: exitFast, filter: exitFast } }}\n          transition={reduce ? reducedFade : swap}\n        >\n          {kind === \"volume\" ? (\n            // Like separate SF Symbols, each state is centred on its own: the speaker slides\n            // over as waves come and go rather than hanging left of an empty space.\n            <motion.span\n              className=\"absolute inset-0\"\n              initial={false}\n              animate={{ x: `${((muted ? CENTRE_MUTED : CENTRE[waves]) / 24) * 100}%` }}\n              transition={reduce ? instant : snappy}\n            >\n              <Speaker muted={muted} reduce={reduce} />\n              {WAVES.map((d, i) => (\n                <Part key={d} shown={!muted && waves > i} reduce={reduce}>\n                  <path d={d} fill=\"none\" stroke=\"currentColor\" strokeWidth={STROKE} strokeLinecap=\"round\" />\n                </Part>\n              ))}\n              <Part shown={muted} reduce={reduce}>\n                <path d={SLASH} fill=\"none\" stroke=\"currentColor\" strokeWidth={STROKE} strokeLinecap=\"round\" />\n              </Part>\n            </motion.span>\n          ) : (\n            <>\n              <Part shown reduce={reduce}>\n                {kind === \"brightness\" ? (\n                  <circle cx=\"12\" cy=\"12\" r=\"4.4\" />\n                ) : (\n                  <>\n                    <path d=\"M7.2 15.2a4.8 4.8 0 0 1 9.6 0Z\" />\n                    <rect x=\"3\" y=\"17.4\" width=\"18\" height={STROKE} rx={STROKE / 2} />\n                  </>\n                )}\n              </Part>\n              {([\"min\", \"max\"] as const).map((size) => (\n                <Part key={size} shown={bright === (size === \"max\")} reduce={reduce}>\n                  <path\n                    d={(kind === \"brightness\" ? SUN : LIGHT)[size]}\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth={STROKE}\n                    strokeLinecap=\"round\"\n                  />\n                </Part>\n              ))}\n            </>\n          )}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\n/**\n * One layer of a glyph, as its own <svg> so blur works everywhere (Safari ignores CSS\n * filters on shapes inside an SVG). Always mounted: toggling `shown` cross-fades it.\n */\nfunction Part({ shown, reduce, children }: { shown: boolean; reduce: boolean; children: ReactNode }) {\n  return (\n    <motion.svg\n      viewBox=\"0 0 24 24\"\n      fill=\"currentColor\"\n      className=\"absolute inset-0 size-full overflow-visible\"\n      initial={false}\n      animate={shown ? (reduce ? { opacity: 1 } : swapShown) : reduce ? { opacity: 0 } : { opacity: 0, filter: `blur(${blur.soft})` }}\n      transition={reduce ? reducedFade : shown ? swap : { ...swap, opacity: exitFast, filter: exitFast }}\n    >\n      {children}\n    </motion.svg>\n  );\n}\n\n/** The speaker, with a gap cut along the mute slash so the slash reads on a filled shape. */\nfunction Speaker({ muted, reduce }: { muted: boolean; reduce: boolean }) {\n  const id = `mac-hud-cut-${useId().replace(/[^\\w-]/g, \"\")}`;\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" className=\"absolute inset-0 size-full overflow-visible\">\n      <mask id={id} maskUnits=\"userSpaceOnUse\" x=\"0\" y=\"0\" width=\"24\" height=\"24\">\n        <rect width=\"24\" height=\"24\" fill=\"white\" />\n        <motion.path\n          d={SLASH}\n          fill=\"none\"\n          stroke=\"black\"\n          strokeWidth={STROKE + 3}\n          strokeLinecap=\"round\"\n          initial={false}\n          animate={{ opacity: muted ? 1 : 0 }}\n          transition={reduce ? reducedFade : muted ? swap.opacity : exitFast}\n        />\n      </mask>\n      <path d={SPEAKER} mask={`url(#${id})`} />\n    </svg>\n  );\n}\n\nexport type MacHudState = { open: boolean; kind: MacHudKind; value: number; muted: boolean };\n\nexport type UseMacHudOptions = {\n  /** How long the HUD stays up after the last change, in ms. Default 1100, as on macOS. */\n  duration?: number;\n};\n\n/**\n * The HUD's state and its hold timer. `show(kind, value)` opens it, or updates it in place if\n * it is already up, and restarts the timer; it hides `duration` ms after the last call.\n * Spread `hud` onto `<MacHud>`.\n */\nexport function useMacHud({ duration = 1100 }: UseMacHudOptions = {}) {\n  const [hud, setHud] = useState<MacHudState>({ open: false, kind: \"volume\", value: 0, muted: false });\n  const timer = useRef<number | undefined>(undefined);\n\n  const hide = useCallback(() => {\n    window.clearTimeout(timer.current);\n    setHud((h) => (h.open ? { ...h, open: false } : h));\n  }, []);\n\n  const show = useCallback(\n    (kind: MacHudKind, value: number, { muted = false }: { muted?: boolean } = {}) => {\n      setHud({ open: true, kind, value: clamp01(value), muted });\n      window.clearTimeout(timer.current);\n      timer.current = window.setTimeout(() => setHud((h) => ({ ...h, open: false })), duration);\n    },\n    [duration],\n  );\n\n  useEffect(() => () => window.clearTimeout(timer.current), []);\n\n  return { hud, show, hide };\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/mac-hud/mac-hud.tsx"
    }
  ],
  "docs": "Import MacHud and useMacHud from components/eased/mac-hud/mac-hud. The Eased tokens were merged into your global CSS; nothing to import. Call show(kind, value) from useMacHud() and spread hud onto <MacHud>. Dark mode follows .dark or data-theme=\"dark\". In a Tauri or Electron HUD window with native vibrancy, pass material=\"native\" and position=\"none\", hide the window in onExitComplete, keep html and body transparent, and call useSystemAppearance() from components/eased/lib/mac so dark follows the system.",
  "type": "registry:ui"
}