{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "push-to-talk",
  "title": "Push to talk",
  "description": "A radio's push-to-talk key: it listens only while held, and letting go stops it. Nothing records by accident. For voice input to agents, walkie-talkie chat and dictation.",
  "dependencies": [
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/push-to-talk.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\n\ntype PushToTalkContextValue = { talking: boolean; disabled: boolean; setTalking: (talking: boolean) => void }\n\nconst PushToTalkContext = React.createContext<PushToTalkContextValue | null>(null)\n\nfunction usePushToTalk() {\n  const context = React.useContext(PushToTalkContext)\n  if (!context) throw new Error(\"PushToTalk parts must be inside <PushToTalk>\")\n  return context\n}\n\ntype PushToTalkProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  /** Whether the key is held right now. Set it from onTalkingChange. */\n  talking: boolean\n  /** Called with true when the key goes down and false when it's let go. */\n  onTalkingChange?: (talking: boolean) => void\n  disabled?: boolean\n}\n\n/**\n * A push-to-talk radio: it listens only while the key is held, with the\n * pointer or Space. Letting go, leaving, or losing focus stops it.\n */\nfunction PushToTalk({ className, talking, onTalkingChange, disabled = false, children, ...props }: PushToTalkProps) {\n  return (\n    <PushToTalkContext value={{ talking, disabled, setTalking: (t) => onTalkingChange?.(t) }}>\n      <div\n        data-slot=\"push-to-talk\"\n        data-talking={talking ? \"\" : undefined}\n        className={cn(\n          \"bevel-plastic bevel-object flex w-56 flex-col items-center gap-4 rounded-(--bevel-radius-xl) p-5 text-(--bevel-ivory)\",\n          className\n        )}\n        {...props}\n      >\n        <span aria-hidden className=\"push-to-talk-grille h-12 w-full rounded-(--bevel-radius-md) shadow-(--bevel-depth-recessed)\" />\n        {children}\n      </div>\n    </PushToTalkContext>\n  )\n}\n\n/** The key. A toggle button that's pressed only while held. */\nfunction PushToTalkKey({ className, children = \"Hold to talk\", ...props }: React.ComponentProps<\"button\">) {\n  const { talking, disabled, setTalking } = usePushToTalk()\n  // Pointer up, lost capture and blur can all arrive for one release: report it once.\n  const held = React.useRef(false)\n  const start = () => {\n    if (disabled || held.current) return\n    held.current = true\n    setTalking(true)\n  }\n  const stop = () => {\n    if (!held.current) return\n    held.current = false\n    setTalking(false)\n  }\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"push-to-talk-key\"\n      aria-pressed={talking}\n      disabled={disabled}\n      onPointerDown={(event) => {\n        if (event.button !== 0) return\n        event.currentTarget.setPointerCapture(event.pointerId)\n        start()\n      }}\n      onPointerUp={stop}\n      onPointerCancel={stop}\n      onLostPointerCapture={stop}\n      onKeyDown={(event) => {\n        if (event.key !== \" \") return\n        event.preventDefault()\n        if (!event.repeat) start()\n      }}\n      onKeyUp={(event) => {\n        if (event.key === \" \") stop()\n      }}\n      // Enter can't be held the way Space can, so it does nothing.\n      onClick={(event) => event.preventDefault()}\n      onBlur={stop}\n      onContextMenu={(event) => event.preventDefault()}\n      className={cn(\n        \"bevel-raised flex h-24 w-full touch-none flex-col items-center justify-center gap-1 rounded-(--bevel-radius-lg) text-sm font-semibold outline-none select-none\",\n        \"transition-[translate,box-shadow] duration-(--bevel-duration-press) ease-(--bevel-ease-out) motion-reduce:transition-none\",\n        \"aria-pressed:translate-y-1 aria-pressed:bevel-pressed\",\n        \"focus-visible:ring-3 focus-visible:ring-(--bevel-ivory)/50 disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    >\n      <span aria-hidden className=\"h-1 w-10 rounded-full bg-(--bevel-shade-strong)\" />\n      {children}\n      <span aria-hidden className=\"h-1 w-10 rounded-full bg-(--bevel-shade-strong)\" />\n    </button>\n  )\n}\n\n/** The on-air lamp and what it means, in words. */\nfunction PushToTalkStatus({ className, idle = \"Not listening\", live = \"Listening\", ...props }: React.ComponentProps<\"p\"> & { idle?: string; live?: string }) {\n  const { talking } = usePushToTalk()\n  return (\n    <p data-slot=\"push-to-talk-status\" className={cn(\"flex items-center gap-2 text-sm\", className)} {...props}>\n      <span\n        aria-hidden\n        className={cn(\n          \"size-2.5 rounded-full transition-opacity duration-(--bevel-duration-press) motion-reduce:transition-none\",\n          talking ? \"bevel-led\" : \"bg-(--bevel-shade-strong)\"\n        )}\n      />\n      <span role=\"status\">{talking ? live : idle}</span>\n    </p>\n  )\n}\n\nexport { PushToTalk, PushToTalkKey, PushToTalkStatus }\nexport type { PushToTalkProps }\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--push-to-talk-body": "oklch(0.26 0.01 260)",
      "--push-to-talk-body-edge": "oklch(0.18 0.01 260)",
      "--push-to-talk-key": "oklch(0.36 0.01 260)",
      "--push-to-talk-live": "oklch(0.62 0.22 27)",
      "--push-to-talk-grille": "oklch(0.14 0.01 260)"
    },
    "[data-slot=\"push-to-talk\"]": {
      "--bevel-material": "var(--push-to-talk-body)",
      "--bevel-material-edge": "var(--push-to-talk-body-edge)",
      "--bevel-led": "var(--push-to-talk-live)"
    },
    "[data-slot=\"push-to-talk-key\"]": {
      "--bevel-material": "var(--push-to-talk-key)"
    },
    "@utility push-to-talk-grille": {
      "background-image": "radial-gradient(circle, var(--push-to-talk-grille) 35%, transparent 40%)",
      "background-size": "0.5rem 0.5rem"
    }
  },
  "categories": [
    "instruments"
  ],
  "type": "registry:ui"
}