{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dip-switch",
  "title": "DIP switches",
  "description": "A bank of tiny numbered slide switches, ON at the top, as on a circuit board. Every flag is visible at once and the bank reads as one value. For feature flags, permission bits and config words.",
  "dependencies": [
    "@base-ui/react",
    "cn"
  ],
  "registryDependencies": [
    "https://bevel-ui.vercel.app/r/bevel.json",
    "https://bevel-ui.vercel.app/r/feedback.json"
  ],
  "files": [
    {
      "path": "registry/bevel/ui/dip-switch.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Switch as SwitchPrimitive } from \"@base-ui/react/switch\"\nimport { cn } from \"cn\"\nimport { useBevelFeedback } from \"@/lib/feedback\"\n\ntype Bit = { value: string; label: string; id: string }\n\ntype DipSwitchContextValue = {\n  bits: Bit[]\n  on: string[]\n  disabled: boolean\n  register: (bit: Bit) => () => void\n  set: (value: string, checked: boolean) => void\n}\n\nconst DipSwitchContext = React.createContext<DipSwitchContextValue | null>(null)\n\nfunction useDipSwitch() {\n  const context = React.useContext(DipSwitchContext)\n  if (!context) throw new Error(\"DipSwitch parts must be inside <DipSwitch>\")\n  return context\n}\n\ntype DipSwitchProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n  /** The switches that are on, by value. */\n  value?: string[]\n  defaultValue?: string[]\n  onValueChange?: (value: string[]) => void\n  /** Locks every switch. Say why with aria-describedby. */\n  disabled?: boolean\n  /** Housing colour. */\n  variant?: \"red\" | \"blue\" | \"black\"\n}\n\n/**\n * A bank of DIP switches: tiny numbered slide switches for flags and config\n * bits, ON at the top. List them as DipSwitchToggle children inside\n * DipSwitchBank; switch 1 is the lowest bit. Give it an aria-label.\n */\nfunction DipSwitch({\n  className,\n  value: valueProp,\n  defaultValue = [],\n  onValueChange,\n  disabled = false,\n  variant = \"red\",\n  children,\n  ...props\n}: DipSwitchProps) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue)\n  const on = valueProp ?? uncontrolled\n  const [bits, setBits] = React.useState<Bit[]>([])\n\n  const register = React.useCallback((bit: Bit) => {\n    // Switches keep the order they mounted in, which is their order on the bank.\n    setBits((all) =>\n      all.some((b) => b.value === bit.value) ? all.map((b) => (b.value === bit.value ? bit : b)) : [...all, bit]\n    )\n    return () => setBits((all) => all.filter((b) => b.value !== bit.value))\n  }, [])\n\n  const set = React.useCallback(\n    (value: string, checked: boolean) => {\n      const next = checked ? [...on.filter((v) => v !== value), value] : on.filter((v) => v !== value)\n      if (valueProp === undefined) setUncontrolled(next)\n      onValueChange?.(next)\n    },\n    [on, valueProp, onValueChange]\n  )\n\n  const context = React.useMemo(() => ({ bits, on, disabled, register, set }), [bits, on, disabled, register, set])\n  return (\n    <DipSwitchContext value={context}>\n      <div\n        role=\"group\"\n        data-slot=\"dip-switch\"\n        data-variant={variant}\n        data-disabled={disabled ? \"\" : undefined}\n        className={cn(\"inline-flex flex-col gap-4\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </DipSwitchContext>\n  )\n}\n\n/** The plastic housing the switches sit in, with ON printed above them. */\nfunction DipSwitchBank({ className, children, ...props }: React.ComponentProps<\"div\">) {\n  const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    // Arrow keys move along the bank; Space flips the switch in focus.\n    const step = event.key === \"ArrowRight\" ? 1 : event.key === \"ArrowLeft\" ? -1 : 0\n    if (!step) return\n    const switches = [...event.currentTarget.querySelectorAll<HTMLElement>('[role=\"switch\"]:not([aria-disabled=\"true\"])')]\n    const i = switches.indexOf(document.activeElement as HTMLElement)\n    if (i < 0) return\n    switches[Math.min(switches.length - 1, Math.max(0, i + step))]?.focus()\n    event.preventDefault()\n  }\n  return (\n    <div\n      data-slot=\"dip-switch-bank\"\n      onKeyDown={onKeyDown}\n      className={cn(\n        \"bevel-plastic relative inline-flex w-fit flex-col gap-1 rounded-(--bevel-radius-xs) px-2 pt-1.5 pb-1 text-(--dip-switch-print) in-data-disabled:opacity-60\",\n        className\n      )}\n      {...props}\n    >\n      <span\n        aria-hidden\n        className=\"flex items-center gap-1 text-(length:--dip-switch-print-size) leading-none font-bold tracking-(--bevel-tracking-print)\"\n      >\n        <span className=\"dip-switch-arrow\" />\n        ON\n      </span>\n      <div className=\"flex gap-(--dip-switch-gap)\">{children}</div>\n    </div>\n  )\n}\n\ntype DipSwitchToggleProps = Omit<SwitchPrimitive.Root.Props, \"checked\" | \"defaultChecked\" | \"onCheckedChange\" | \"value\" | \"children\"> & {\n  value: string\n  /** What the switch controls; it's named by its number and this. */\n  children: string\n}\n\n/** One switch. Its number is its position on the bank; its label shows in DipSwitchLegend. */\nfunction DipSwitchToggle({ className, value, children, disabled: disabledProp, ...props }: DipSwitchToggleProps) {\n  const { bits, on, disabled, register, set } = useDipSwitch()\n  const feedback = useBevelFeedback()\n  const id = React.useId()\n  React.useEffect(() => register({ value, label: children, id }), [value, children, id, register])\n  const index = bits.findIndex((b) => b.value === value)\n  const number = index + 1\n  const checked = on.includes(value)\n  return (\n    <span data-slot=\"dip-switch-position\" className=\"flex flex-col items-center gap-0.5\">\n      <SwitchPrimitive.Root\n        id={id}\n        data-slot=\"dip-switch-toggle\"\n        checked={checked}\n        disabled={disabled || disabledProp}\n        aria-label={number > 0 ? `${number}: ${children}` : children}\n        onCheckedChange={(next) => {\n          feedback.toggle()\n          set(value, next)\n        }}\n        className={cn(\n          \"bevel-recessed relative flex h-(--dip-switch-slot-height) w-(--dip-switch-slot-width) cursor-pointer justify-center rounded-(--bevel-radius-xs) py-0.5\",\n          \"outline-none focus-visible:ring-2 focus-visible:ring-(--dip-switch-focus) focus-visible:ring-offset-1 focus-visible:ring-offset-(--dip-switch-body) data-disabled:cursor-not-allowed\",\n          className\n        )}\n        {...props}\n      >\n        <SwitchPrimitive.Thumb\n          data-slot=\"dip-switch-actuator\"\n          className={cn(\n            \"bevel-raised block h-(--dip-switch-actuator-height) w-full rounded-(--bevel-radius-xs)\",\n            \"translate-y-(--dip-switch-throw) data-checked:translate-y-0\",\n            \"transition-[translate] duration-(--bevel-duration-press) ease-(--bevel-ease-out) motion-reduce:transition-none\"\n          )}\n        />\n      </SwitchPrimitive.Root>\n      <span aria-hidden className=\"text-(length:--dip-switch-print-size) leading-none font-bold tabular-nums\">\n        {number > 0 ? number : \"\"}\n      </span>\n    </span>\n  )\n}\n\n/** The key printed beside the bank: each switch's number, what it does, and whether it's on. */\nfunction DipSwitchLegend({ className, ...props }: React.ComponentProps<\"ol\">) {\n  const { bits, on } = useDipSwitch()\n  return (\n    <ol\n      aria-hidden\n      data-slot=\"dip-switch-legend\"\n      className={cn(\"flex flex-col gap-1 text-sm\", className)}\n      {...props}\n    >\n      {bits.map((bit, i) => {\n        const checked = on.includes(bit.value)\n        return (\n          <li key={bit.value} data-checked={checked ? \"\" : undefined} className=\"flex items-baseline gap-2\">\n            <span className=\"w-4 text-right font-mono text-xs text-muted-foreground tabular-nums\">{i + 1}</span>\n            <span className=\"min-w-0 flex-1 truncate\">{bit.label}</span>\n            <span className=\"font-mono text-xs font-semibold text-muted-foreground in-data-checked:text-foreground\">\n              {checked ? \"ON\" : \"OFF\"}\n            </span>\n          </li>\n        )\n      })}\n    </ol>\n  )\n}\n\ntype DipSwitchReadoutProps = React.ComponentProps<\"span\"> & {\n  /** How to show the bits: binary, hex or decimal. */\n  format?: \"binary\" | \"hex\" | \"decimal\"\n}\n\n/** The bank as a number. Switch 1 is the lowest bit; binary reads highest first, as written. */\nfunction DipSwitchReadout({ className, format = \"hex\", ...props }: DipSwitchReadoutProps) {\n  const { bits, on } = useDipSwitch()\n  const n = bits.reduce((sum, bit, i) => (on.includes(bit.value) ? sum + 2 ** i : sum), 0)\n  const binary = bits\n    .map((bit) => (on.includes(bit.value) ? \"1\" : \"0\"))\n    .reverse()\n    .join(\"\")\n  const text =\n    format === \"binary\"\n      ? `0b${binary}`\n      : format === \"decimal\"\n        ? String(n)\n        : `0x${n.toString(16).toUpperCase().padStart(Math.max(1, Math.ceil(bits.length / 4)), \"0\")}`\n  return (\n    <span\n      data-slot=\"dip-switch-readout\"\n      className={cn(\"font-mono text-sm tabular-nums\", className)}\n      {...props}\n    >\n      {text}\n    </span>\n  )\n}\n\nexport { DipSwitch, DipSwitchBank, DipSwitchToggle, DipSwitchLegend, DipSwitchReadout }\nexport type { DipSwitchProps, DipSwitchToggleProps, DipSwitchReadoutProps }\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ":root": {
      "--dip-switch-red": "oklch(0.5 0.17 27)",
      "--dip-switch-red-edge": "oklch(0.34 0.12 27)",
      "--dip-switch-blue": "oklch(0.45 0.13 258)",
      "--dip-switch-blue-edge": "oklch(0.3 0.09 258)",
      "--dip-switch-black": "oklch(0.26 0.01 260)",
      "--dip-switch-black-edge": "oklch(0.16 0.01 260)",
      "--dip-switch-slot": "oklch(0.18 0.01 260)",
      "--dip-switch-actuator": "oklch(0.96 0.005 90)",
      "--dip-switch-actuator-edge": "oklch(0.72 0.01 90)",
      "--dip-switch-print": "oklch(0.97 0.01 90)",
      "--dip-switch-focus": "oklch(0.97 0.01 90)",
      "--dip-switch-print-size": "0.625rem",
      "--dip-switch-slot-width": "1.25rem",
      "--dip-switch-slot-height": "2.75rem",
      "--dip-switch-actuator-height": "1.125rem",
      "--dip-switch-throw": "1.375rem",
      "--dip-switch-gap": "0.5rem"
    },
    "[data-slot=\"dip-switch\"]": {
      "--dip-switch-body": "var(--dip-switch-red)",
      "--dip-switch-body-edge": "var(--dip-switch-red-edge)"
    },
    "[data-slot=\"dip-switch\"][data-variant=\"blue\"]": {
      "--dip-switch-body": "var(--dip-switch-blue)",
      "--dip-switch-body-edge": "var(--dip-switch-blue-edge)"
    },
    "[data-slot=\"dip-switch\"][data-variant=\"black\"]": {
      "--dip-switch-body": "var(--dip-switch-black)",
      "--dip-switch-body-edge": "var(--dip-switch-black-edge)"
    },
    "[data-slot=\"dip-switch-bank\"]": {
      "--bevel-material": "var(--dip-switch-body)",
      "--bevel-material-edge": "var(--dip-switch-body-edge)",
      "--bevel-material-recess": "var(--dip-switch-slot)"
    },
    "[data-slot=\"dip-switch-actuator\"]": {
      "--bevel-material": "var(--dip-switch-actuator)",
      "--bevel-material-edge": "var(--dip-switch-actuator-edge)"
    },
    "@utility dip-switch-arrow": {
      "width": "0",
      "height": "0",
      "border-inline": "0.1875rem solid transparent",
      "border-bottom": "0.3125rem solid currentColor"
    },
    "@media (forced-colors: active)": {
      "[data-slot=\"dip-switch-toggle\"]": {
        "border": "1px solid CanvasText"
      },
      "[data-slot=\"dip-switch-actuator\"]": {
        "background": "ButtonText"
      }
    }
  },
  "categories": [
    "instruments"
  ],
  "type": "registry:ui"
}