{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "guarded-switch",
  "title": "Guarded switch",
  "description": "A critical switch under a flip-up cover, as in a cockpit. Lift the cover, then flip it; closing the cover returns it to safe, and the cover closes itself if left.",
  "dependencies": [
    "@base-ui/react",
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/guarded-switch.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Switch as SwitchPrimitive } from \"@base-ui/react/switch\"\nimport { cn } from \"cn\"\n\ntype GuardedSwitchContextValue = {\n  open: boolean\n  setOpen: (open: boolean) => void\n  disabled: boolean\n  checked: boolean\n  setChecked: (checked: boolean) => void\n  touch: () => void\n  ids: { control: string; label: string }\n}\n\nconst GuardedSwitchContext = React.createContext<GuardedSwitchContextValue | null>(null)\n\nfunction useGuardedSwitch() {\n  const context = React.useContext(GuardedSwitchContext)\n  if (!context) throw new Error(\"GuardedSwitch parts must be inside <GuardedSwitch>\")\n  return context\n}\n\n/** Browsers may report a custom-property duration as \"8000ms\" or \"8s\". */\nfunction toMilliseconds(value: string, fallback: number) {\n  const n = parseFloat(value)\n  if (Number.isNaN(n)) return fallback\n  return /ms\\s*$/.test(value) ? n : /s\\s*$/.test(value) ? n * 1000 : n\n}\n\ntype GuardedSwitchProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  checked?: boolean\n  defaultChecked?: boolean\n  onCheckedChange?: (checked: boolean) => void\n  /** Closing the cover turns the switch off. Leave on for anything dangerous to leave running. */\n  safeOnClose?: boolean\n  /** Close the cover after --guarded-switch-close-after of no interaction. */\n  autoClose?: boolean\n  /** Locks the cover shut. Say why with aria-describedby on the root. */\n  disabled?: boolean\n}\n\n/**\n * A critical switch under a flip-up cover. Lift the cover, then flip the\n * switch: two deliberate steps. Closing the cover returns it to safe.\n */\nfunction GuardedSwitch({\n  className,\n  checked: checkedProp,\n  defaultChecked = false,\n  onCheckedChange,\n  safeOnClose = true,\n  autoClose = true,\n  disabled = false,\n  children,\n  ...props\n}: GuardedSwitchProps) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultChecked)\n  const checked = checkedProp ?? uncontrolled\n  const [open, setOpenState] = React.useState(false)\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const timer = React.useRef<number | undefined>(undefined)\n  const control = React.useId()\n  const label = React.useId()\n\n  const setChecked = React.useCallback(\n    (next: boolean) => {\n      if (checkedProp === undefined) setUncontrolled(next)\n      onCheckedChange?.(next)\n    },\n    [checkedProp, onCheckedChange]\n  )\n\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      setOpenState(next)\n      if (!next && safeOnClose && checked) setChecked(false)\n    },\n    [safeOnClose, checked, setChecked]\n  )\n\n  const touch = React.useCallback(() => {\n    window.clearTimeout(timer.current)\n    if (!autoClose || !rootRef.current) return\n    const ms = toMilliseconds(\n      getComputedStyle(rootRef.current).getPropertyValue(\"--guarded-switch-close-after\"),\n      8000\n    )\n    timer.current = window.setTimeout(() => setOpen(false), ms)\n  }, [autoClose, setOpen])\n\n  React.useEffect(() => {\n    if (open) touch()\n    else window.clearTimeout(timer.current)\n    return () => window.clearTimeout(timer.current)\n  }, [open, touch])\n\n  const context = React.useMemo(\n    () => ({ open, setOpen, disabled, checked, setChecked, touch, ids: { control, label } }),\n    [open, setOpen, disabled, checked, setChecked, touch, control, label]\n  )\n\n  return (\n    <GuardedSwitchContext value={context}>\n      <div\n        ref={rootRef}\n        role=\"group\"\n        aria-labelledby={label}\n        data-slot=\"guarded-switch\"\n        data-open={open ? \"\" : undefined}\n        data-checked={checked ? \"\" : undefined}\n        data-disabled={disabled ? \"\" : undefined}\n        className={cn(\n          \"bevel-metal bevel-object inline-flex w-44 flex-col items-center gap-3 rounded-(--bevel-radius-md) p-4 text-(--guarded-switch-lever)\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </GuardedSwitchContext>\n  )\n}\n\nfunction GuardedSwitchLabel({ className, ...props }: React.ComponentProps<\"span\">) {\n  const { ids } = useGuardedSwitch()\n  return (\n    <span\n      id={ids.label}\n      data-slot=\"guarded-switch-label\"\n      className={cn(\n        \"bevel-deboss text-center text-(length:--bevel-label) font-semibold tracking-(--bevel-tracking-label) uppercase\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\n/** The cover and the switch beneath it. */\nfunction GuardedSwitchControl({ className }: { className?: string }) {\n  const { open, setOpen, disabled, checked, setChecked, touch, ids } = useGuardedSwitch()\n  return (\n    <div\n      data-slot=\"guarded-switch-housing\"\n      className={cn(\"relative mt-5 grid h-28 w-20 place-items-center perspective-(--bevel-perspective)\", className)}\n    >\n      {/* Cover first: it is the first step, in focus order too. */}\n      <button\n        type=\"button\"\n        data-slot=\"guarded-switch-cover\"\n        aria-expanded={open}\n        aria-controls={ids.control}\n        disabled={disabled && !open}\n        onClick={() => setOpen(!open)}\n        className={cn(\n          \"guarded-switch-cover absolute inset-x-0 top-0 z-10 h-24 origin-top cursor-pointer rounded-(--bevel-radius-sm) outline-none\",\n          \"transition-[rotate] duration-(--bevel-duration-move) ease-(--bevel-ease-out) motion-reduce:transition-none\",\n          \"focus-visible:ring-3 focus-visible:ring-(--guarded-switch-on)/60 disabled:cursor-not-allowed\",\n          open ? \"[rotate:x_var(--guarded-switch-open-angle)]\" : \"[rotate:x_0deg]\"\n        )}\n      >\n        <span className=\"sr-only\">{open ? \"Close the cover\" : \"Lift the cover\"}</span>\n      </button>\n      <SwitchPrimitive.Root\n        id={ids.control}\n        data-slot=\"guarded-switch-switch\"\n        checked={checked}\n        disabled={!open || disabled}\n        aria-labelledby={ids.label}\n        onCheckedChange={(next) => {\n          setChecked(next)\n          touch()\n        }}\n        className=\"bevel-recessed relative flex h-20 w-10 items-center justify-center rounded-(--bevel-radius-sm) outline-none focus-visible:ring-3 focus-visible:ring-(--guarded-switch-on)/60 data-disabled:cursor-not-allowed\"\n      >\n        <SwitchPrimitive.Thumb\n          data-slot=\"guarded-switch-lever\"\n          className=\"bevel-raised block h-8 w-5 translate-y-(--guarded-switch-throw) rounded-full transition-[translate] duration-(--bevel-duration-small) ease-(--bevel-ease-spring) data-checked:-translate-y-(--guarded-switch-throw) motion-reduce:transition-none\"\n        />\n      </SwitchPrimitive.Root>\n    </div>\n  )\n}\n\n/** Lamp and words: Off, Armed (cover up), On. */\nfunction GuardedSwitchStatus({ className, ...props }: React.ComponentProps<\"span\">) {\n  const { open, checked, disabled } = useGuardedSwitch()\n  return (\n    <span\n      role=\"status\"\n      data-slot=\"guarded-switch-status\"\n      className={cn(\n        \"bevel-deboss flex items-center gap-1.5 text-(length:--bevel-fine) font-semibold tracking-(--bevel-tracking-print) uppercase\",\n        className\n      )}\n      {...props}\n    >\n      <span aria-hidden className=\"bevel-led size-1.5 rounded-full\" />\n      {checked ? \"On\" : disabled ? \"Locked\" : open ? \"Cover open\" : \"Off, guarded\"}\n    </span>\n  )\n}\n\nexport { GuardedSwitch, GuardedSwitchLabel, GuardedSwitchControl, GuardedSwitchStatus }\nexport type { GuardedSwitchProps }\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--guarded-switch-plate": "oklch(0.3 0.008 260)",
      "--guarded-switch-plate-edge": "oklch(0.2 0.008 260)",
      "--guarded-switch-cover": "oklch(0.58 0.21 27 / 0.72)",
      "--guarded-switch-cover-edge": "oklch(0.42 0.18 27)",
      "--guarded-switch-lever": "oklch(0.9 0.006 260)",
      "--guarded-switch-lever-edge": "oklch(0.62 0.01 260)",
      "--guarded-switch-on": "oklch(0.72 0.19 27)",
      "--guarded-switch-off": "oklch(0.45 0.01 260)",
      "--guarded-switch-open-angle": "-112deg",
      "--guarded-switch-throw": "0.75rem",
      "--guarded-switch-close-after": "8s"
    },
    "[data-slot=\"guarded-switch\"]": {
      "--bevel-material": "var(--guarded-switch-plate)",
      "--bevel-material-edge": "var(--guarded-switch-plate-edge)",
      "--bevel-material-recess": "var(--guarded-switch-plate-edge)",
      "--bevel-led": "var(--guarded-switch-off)"
    },
    "[data-slot=\"guarded-switch\"][data-checked]": {
      "--bevel-led": "var(--guarded-switch-on)"
    },
    "[data-slot=\"guarded-switch-lever\"]": {
      "--bevel-material": "var(--guarded-switch-lever)",
      "--bevel-material-edge": "var(--guarded-switch-lever-edge)"
    },
    "@utility guarded-switch-cover": {
      "background-color": "var(--guarded-switch-cover)",
      "background-image": "linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade))",
      "box-shadow": "inset 0 0 0 1px var(--guarded-switch-cover-edge), 0 4px 8px -2px var(--bevel-cast)"
    }
  },
  "categories": [
    "coordination"
  ],
  "type": "registry:ui"
}