{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "annunciator",
  "title": "Annunciator",
  "description": "A panel of alarm lamps from a control room. A new alarm flashes until someone acknowledges it, then stays lit while its condition lasts; one that clears unseen keeps flashing, so nothing passes unnoticed. For agent, build and system alerts.",
  "dependencies": [
    "@base-ui/react",
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/annunciator.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\"\nimport { cn } from \"cn\"\n\ntype AnnunciatorContextValue = {\n  /** Alarms nobody has acknowledged yet, by id. */\n  unacked: Record<string, string>\n  raise: (id: string, label: string) => void\n  acknowledge: (id?: string) => void\n  message: string\n  announce: (message: string) => void\n}\n\nconst AnnunciatorContext = React.createContext<AnnunciatorContextValue | null>(null)\n\nfunction useAnnunciator() {\n  const context = React.useContext(AnnunciatorContext)\n  if (!context) throw new Error(\"Annunciator parts must be inside <Annunciator>\")\n  return context\n}\n\n/**\n * A panel of alarm lamps. Each new alarm flashes until acknowledged, then\n * stays lit while its condition lasts. Give it an aria-label.\n */\nfunction Annunciator({ className, children, ...props }: React.ComponentProps<\"div\">) {\n  const [unacked, setUnacked] = React.useState<Record<string, string>>({})\n  const [message, announce] = React.useState(\"\")\n  const raise = React.useCallback((id: string, label: string) => {\n    setUnacked((all) => ({ ...all, [id]: label }))\n  }, [])\n  const acknowledge = React.useCallback((id?: string) => {\n    setUnacked((all) => {\n      if (id === undefined) return {}\n      const next = { ...all }\n      delete next[id]\n      return next\n    })\n  }, [])\n  const context = React.useMemo(\n    () => ({ unacked, raise, acknowledge, message, announce }),\n    [unacked, raise, acknowledge, message]\n  )\n  return (\n    <AnnunciatorContext value={context}>\n      <div\n        role=\"group\"\n        data-slot=\"annunciator\"\n        data-alarm={Object.keys(unacked).length ? \"\" : undefined}\n        className={cn(\n          \"bevel-metal bevel-object flex w-full max-w-md flex-col gap-3 rounded-(--bevel-radius-md) p-3\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        <p role=\"status\" className=\"sr-only\">\n          {message}\n        </p>\n      </div>\n    </AnnunciatorContext>\n  )\n}\n\n/** The grid of lamp windows. */\nfunction AnnunciatorPanel({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"annunciator-panel\"\n      className={cn(\n        \"grid grid-cols-2 gap-1.5 rounded-(--bevel-radius-sm) bg-(--bevel-material-edge) p-1.5 shadow-(--bevel-depth-recessed) sm:grid-cols-3\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\ntype AnnunciatorTileProps = Omit<React.ComponentProps<\"button\">, \"children\"> & {\n  id: string\n  /** Whether the condition is happening now. */\n  active: boolean\n  /** Lamp colour. Red for warnings, amber for cautions. */\n  tone?: \"red\" | \"amber\" | \"green\" | \"white\"\n  /** The engraved legend. Keep it short. */\n  children: string\n}\n\n/** One lamp window. Press it to acknowledge its alarm. */\nfunction AnnunciatorTile({ className, id, active, tone = \"amber\", children, onClick, ...props }: AnnunciatorTileProps) {\n  const { unacked, raise, acknowledge, announce } = useAnnunciator()\n  const previous = React.useRef(false)\n  React.useEffect(() => {\n    if (active && !previous.current) {\n      raise(id, children)\n      announce(`Alarm: ${children}.`)\n    } else if (!active && previous.current) {\n      announce(`Cleared: ${children}.`)\n    }\n    previous.current = active\n  }, [active, id, children, raise, announce])\n\n  const isNew = id in unacked\n  const state = isNew ? \"new\" : active ? \"acknowledged\" : \"normal\"\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"annunciator-tile\"\n      data-tone={tone}\n      data-state={state}\n      aria-label={`${children}: ${state === \"new\" ? (active ? \"new alarm\" : \"cleared, not yet seen\") : state === \"acknowledged\" ? \"active, acknowledged\" : \"normal\"}`}\n      onClick={(event) => {\n        onClick?.(event)\n        if (isNew) acknowledge(id)\n      }}\n      className={cn(\n        \"relative flex min-h-14 items-center justify-center overflow-hidden rounded-(--bevel-radius-xs) bg-(--annunciator-off) px-2 py-2 text-center text-xs leading-tight font-bold tracking-(--bevel-tracking-label) text-(--annunciator-off-foreground) uppercase outline-none\",\n        \"focus-visible:ring-3 focus-visible:ring-(--annunciator-white)/60\",\n        className\n      )}\n      {...props}\n    >\n      {/* The lamp behind the legend: lit while active or unseen, flashing while unseen. */}\n      <span\n        aria-hidden\n        className={cn(\n          \"absolute inset-0 bg-(--annunciator-lamp) opacity-0 shadow-(--bevel-depth-recessed) transition-opacity duration-(--bevel-duration-small) motion-reduce:transition-none\",\n          \"in-data-[state=acknowledged]:opacity-100 in-data-[state=new]:opacity-100 in-data-[state=new]:annunciator-flash motion-reduce:in-data-[state=new]:animate-none\"\n        )}\n      />\n      <span className=\"relative in-data-[state=acknowledged]:text-(--annunciator-lit-foreground) in-data-[state=new]:text-(--annunciator-lit-foreground)\">\n        {children}\n      </span>\n      {/* Without flashing, a corner mark says it's new. */}\n      <span\n        aria-hidden\n        className=\"absolute top-1 right-1 hidden size-1.5 rounded-full bg-(--annunciator-lit-foreground) motion-reduce:in-data-[state=new]:block\"\n      />\n      <span aria-hidden className=\"bevel-glass absolute inset-0\" />\n    </button>\n  )\n}\n\n/** Acknowledges every unseen alarm at once. */\nfunction AnnunciatorAcknowledge({ className, children, onClick, ...props }: ButtonPrimitive.Props) {\n  const { unacked, acknowledge } = useAnnunciator()\n  const count = Object.keys(unacked).length\n  return (\n    <ButtonPrimitive\n      data-slot=\"annunciator-acknowledge\"\n      disabled={count === 0}\n      focusableWhenDisabled\n      onClick={(event) => {\n        onClick?.(event)\n        acknowledge()\n      }}\n      className={cn(\n        \"bevel-raised h-10 self-end rounded-(--bevel-radius-sm) px-4 text-sm font-semibold text-(--annunciator-white) outline-none [--bevel-material:var(--annunciator-off)]\",\n        \"transition-[translate,box-shadow] duration-(--bevel-duration-press) ease-(--bevel-ease-out) active:translate-y-0.5 active:bevel-pressed motion-reduce:transition-none\",\n        \"focus-visible:ring-3 focus-visible:ring-(--annunciator-white)/60 data-disabled:cursor-not-allowed data-disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      {children ?? (count ? `Acknowledge ${count}` : \"Nothing to acknowledge\")}\n    </ButtonPrimitive>\n  )\n}\n\nexport { Annunciator, AnnunciatorPanel, AnnunciatorTile, AnnunciatorAcknowledge }\nexport type { AnnunciatorTileProps }\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--annunciator-panel": "oklch(0.3 0.01 260)",
      "--annunciator-panel-edge": "oklch(0.2 0.01 260)",
      "--annunciator-off": "oklch(0.24 0.01 260)",
      "--annunciator-off-foreground": "oklch(0.5 0.01 260)",
      "--annunciator-red": "oklch(0.62 0.22 27)",
      "--annunciator-amber": "oklch(0.8 0.16 75)",
      "--annunciator-green": "oklch(0.74 0.17 150)",
      "--annunciator-white": "oklch(0.94 0.02 90)",
      "--annunciator-lit-foreground": "oklch(0.2 0.02 60)",
      "--annunciator-flash": "900ms"
    },
    "[data-slot=\"annunciator\"]": {
      "--bevel-material": "var(--annunciator-panel)",
      "--bevel-material-edge": "var(--annunciator-panel-edge)"
    },
    "[data-slot=\"annunciator-tile\"]": {
      "--annunciator-lamp": "var(--annunciator-amber)"
    },
    "[data-slot=\"annunciator-tile\"][data-tone=\"red\"]": {
      "--annunciator-lamp": "var(--annunciator-red)"
    },
    "[data-slot=\"annunciator-tile\"][data-tone=\"green\"]": {
      "--annunciator-lamp": "var(--annunciator-green)"
    },
    "[data-slot=\"annunciator-tile\"][data-tone=\"white\"]": {
      "--annunciator-lamp": "var(--annunciator-white)"
    },
    "@keyframes annunciator-flash": {
      "50%": {
        "opacity": "0.25"
      }
    },
    "@utility annunciator-flash": {
      "animation": "annunciator-flash var(--annunciator-flash) steps(1, end) infinite"
    }
  },
  "categories": [
    "instruments"
  ],
  "type": "registry:ui"
}