{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toggle",
  "type": "registry:component",
  "title": "Toggle",
  "description": "Turns one persistent option on or off with aria-pressed. Default uses an ink fill when pressed; subtle uses a soft fill and stronger text.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/toggle.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\n\nexport interface ToggleProps\n  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\"> {\n  pressed?: boolean;\n  defaultPressed?: boolean;\n  onPressedChange?: (pressed: boolean) => void;\n  /** Subtle uses a quiet hover and a soft selected fill. */\n  variant?: \"default\" | \"subtle\";\n}\n\nconst styles = stylex.create({\n  toggle: {\n    boxSizing: \"border-box\",\n    display: \"inline-flex\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    gap: \"0.375rem\",\n    height: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n    minHeight: {\n      default: null,\n      [mq.phone]: vlak.hit,\n    },\n    minWidth: {\n      default: vlak.hit,\n      [mq.phone]: vlak.hit,\n    },\n    paddingBlock: 0,\n    paddingInline: {\n      default: \"1.25rem\",\n      [mq.phone]: \"1.25rem\",\n    },\n    fontSize: {\n      default: \"0.8125rem\",\n      [mq.phone]: vlak.controlFs,\n    },\n    fontWeight: 500,\n    letterSpacing: \"-0.01em\",\n    backgroundColor: {\n      default: \"transparent\",\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.controlFill, [mq.forcedColors]: \"ButtonFace\" } },\n      [mq.forcedColors]: \"ButtonFace\",\n    },\n    borderWidth: vlak.hairline,\n    borderStyle: \"solid\",\n    borderColor: {\n      default: vlak.controlBorder,\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.controlFill, [mq.forcedColors]: \"ButtonText\" } },\n      [mq.forcedColors]: \"ButtonText\",\n    },\n    borderRadius: {\n      default: vlak.radiusSm,\n      [mq.phone]: vlak.radiusSm,\n    },\n    color: {\n      default: vlak.gray,\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.ink, [mq.forcedColors]: \"ButtonText\" } },\n      [mq.forcedColors]: \"ButtonText\",\n    },\n    cursor: \"pointer\",\n    fontFamily: \"inherit\",\n    transition: {\n      default: vlak.transition,\n      [mq.reduce]: \"none\",\n    },\n    outlineWidth: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n    outlineStyle: {\n      default: null,\n      \":focus-visible\": \"solid\",\n    },\n    outlineColor: {\n      default: null,\n      \":focus-visible\": vlak.ink,\n    },\n    outlineOffset: {\n      default: null,\n      \":focus-visible\": 2,\n    },\n  },\n  pressed: {\n    backgroundColor: {\n      default: vlak.ink,\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.ink, [mq.forcedColors]: \"Highlight\" } },\n      [mq.forcedColors]: \"Highlight\",\n    },\n    borderColor: {\n      default: vlak.ink,\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.ink, [mq.forcedColors]: \"Highlight\" } },\n      [mq.forcedColors]: \"Highlight\",\n    },\n    color: {\n      default: vlak.paper,\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.paper, [mq.forcedColors]: \"HighlightText\" } },\n      [mq.forcedColors]: \"HighlightText\",\n    },\n    forcedColorAdjust: \"none\",\n  },\n  subtle: {\n    backgroundColor: {\n      default: \"transparent\",\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: \"transparent\", [mq.forcedColors]: \"ButtonFace\" } },\n      [mq.forcedColors]: \"ButtonFace\",\n    },\n    borderColor: {\n      default: \"transparent\",\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: \"transparent\", [mq.forcedColors]: \"ButtonText\" } },\n      [mq.forcedColors]: \"ButtonText\",\n    },\n    cursor: { default: \"pointer\", \":disabled\": \"not-allowed\" },\n    opacity: { default: 1, \":disabled\": { default: 0.5, [mq.forcedColors]: 1 } },\n  },\n  subtlePressed: {\n    backgroundColor: {\n      default: vlak.controlFill,\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.controlFill, [mq.forcedColors]: \"Highlight\" } },\n      [mq.forcedColors]: \"Highlight\",\n    },\n    borderColor: {\n      default: \"transparent\",\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: \"transparent\", [mq.forcedColors]: \"Highlight\" } },\n      [mq.forcedColors]: \"Highlight\",\n    },\n    color: {\n      default: vlak.ink,\n      \":hover:not(:disabled):not([aria-disabled='true'])\": { default: null, [mq.hover]: { default: vlak.ink, [mq.forcedColors]: \"HighlightText\" } },\n      [mq.forcedColors]: \"HighlightText\",\n    },\n    fontWeight: 600,\n  },\n  group: {\n    display: {\n      default: \"inline-flex\",\n      [mq.phone]: \"flex\",\n    },\n    alignItems: \"stretch\",\n    boxSizing: \"border-box\",\n    minHeight: `calc(${vlak.hit} + 2 * ${vlak.hairline})`,\n    width: {\n      default: null,\n      [mq.phone]: \"100%\",\n    },\n    borderWidth: vlak.hairline,\n    borderStyle: \"solid\",\n    /* The frame is the ink of the pressed toggle: one object, one colour. */\n    borderColor: {\n      default: vlak.ink,\n      [mq.forcedColors]: \"ButtonText\",\n    },\n    borderRadius: {\n      default: vlak.radiusSm,\n      [mq.phone]: vlak.radiusSm,\n    },\n    overflow: \"hidden\",\n  },\n  grouped: {\n    height: \"auto\",\n    minHeight: vlak.hit,\n    flexGrow: {\n      default: null,\n      [mq.phone]: 1,\n    },\n    borderWidth: 0,\n    borderRadius: 0,\n    margin: 0,\n    outlineOffset: {\n      default: null,\n      \":focus-visible\": -2,\n    },\n    borderInlineStartWidth: {\n      default: 0,\n      \":not(:first-child)\": vlak.hairline,\n    },\n    borderInlineStartStyle: {\n      default: \"none\",\n      \":not(:first-child)\": \"solid\",\n    },\n    borderInlineStartColor: {\n      default: \"transparent\",\n      \":not(:first-child)\": vlak.controlBorder,\n    },\n  },\n  groupedOn: {\n    position: \"relative\",\n    zIndex: 1,\n  },\n  groupSubtle: {\n    minWidth: 0,\n    maxWidth: \"100%\",\n    padding: 0,\n    gap: \"3px\",\n    backgroundColor: { default: vlak.tableAlt, [mq.forcedColors]: \"Canvas\" },\n    borderColor: { default: vlak.divider, [mq.forcedColors]: \"ButtonText\" },\n  },\n  groupedSubtle: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexBasis: { default: \"auto\", [mq.phone]: 0 },\n    minWidth: { default: vlak.hit, [mq.phone]: vlak.hit },\n    paddingInline: { default: \"0.75rem\", [mq.phone]: \"0.625rem\" },\n    borderRadius: { default: vlak.radiusSm, [mq.phone]: vlak.radiusSm },\n    borderStartStartRadius: { default: null, \":first-child\": 0 },\n    borderEndStartRadius: { default: null, \":first-child\": 0 },\n    borderStartEndRadius: { default: null, \":last-child\": 0 },\n    borderEndEndRadius: { default: null, \":last-child\": 0 },\n    borderInlineStartWidth: { default: 0, \":not(:first-child)\": 0 },\n    borderInlineStartStyle: { default: \"none\", \":not(:first-child)\": \"none\" },\n    borderInlineStartColor: { default: \"transparent\", \":not(:first-child)\": \"transparent\" },\n    overflowWrap: \"anywhere\",\n    whiteSpace: \"normal\",\n  },\n});\n\n/** Press switch; state lives in aria-pressed. */\nexport const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(function Toggle(\n  { pressed, defaultPressed, onPressedChange, variant = \"default\", className, style, onClick, ...props },\n  ref,\n) {\n  const [inner, setInner] = React.useState(defaultPressed ?? false);\n  const isControlled = pressed !== undefined;\n  const on = isControlled ? pressed : inner;\n  const subtle = variant === \"subtle\";\n  const sx = rs([\"rs-toggle\", className, subtle && \"rs-toggle-subtle\", on && \"rs-toggle-pressed\", subtle && on && \"rs-toggle-subtle-pressed\"], styles.toggle, subtle && styles.subtle, on && styles.pressed, subtle && on && styles.subtlePressed);\n  return (\n    <button\n      ref={ref}\n      type=\"button\"\n      aria-pressed={on}\n      onClick={(e) => {\n        if (!isControlled) setInner(!on);\n        onPressedChange?.(!on);\n        onClick?.(e);\n      }}\n      {...props}\n      className={sx.className}\n      style={{ ...sx.style, ...style }}\n    />\n  );\n});\n\nexport interface ToggleGroupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onChange\"> {\n  options: Array<{ value: string; label: React.ReactNode }>;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  /** Subtle groups use a soft rail and individually rounded selected items. */\n  variant?: \"default\" | \"subtle\";\n}\n\n/** One pressed at a time. */\nexport const ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>(function ToggleGroup({\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n  variant = \"default\",\n  className,\n  style,\n  ...props\n}, ref) {\n  const [inner, setInner] = React.useState(defaultValue);\n  const isControlled = value !== undefined;\n  const current = isControlled ? value : inner;\n  const subtle = variant === \"subtle\";\n  const group = rs([\"rs-toggle-group\", className, subtle && \"rs-toggle-group-subtle\"], styles.group, subtle && styles.groupSubtle);\n  const nest: React.CSSProperties = {\n    [\"--rs-out\" as string]: \"var(--radius-sm)\",\n    [\"--rs-gap\" as string]: \"0px\",\n    [\"--rs-in\" as string]: \"max(0px, calc(var(--rs-out) - var(--rs-gap)))\",\n  };\n  return (\n    <div ref={ref} role=\"group\" {...props} className={group.className} style={{ ...group.style, ...nest, ...style }}>\n      {options.map((option) => {\n        const on = option.value === current;\n        const btn = rs([\"rs-toggle\", \"rs-toggle-grouped\", subtle && \"rs-toggle-subtle\", subtle && \"rs-toggle-grouped-subtle\", on && \"rs-toggle-pressed\", subtle && on && \"rs-toggle-subtle-pressed\", on && \"rs-toggle-grouped-on\"], styles.toggle, styles.grouped, subtle && styles.subtle, subtle && styles.groupedSubtle, on && styles.pressed, subtle && on && styles.subtlePressed, on && styles.groupedOn);\n        return (\n          <button\n            key={option.value}\n            type=\"button\"\n            aria-pressed={on}\n            className={btn.className}\n            style={btn.style}\n            onClick={() => {\n              if (!isControlled) setInner(option.value);\n              onValueChange?.(option.value);\n            }}\n          >\n            {option.label}\n          </button>\n        );\n      })}\n    </div>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/vlak/toggle.tsx"
    },
    {
      "path": "vlak/styles/toggle.css",
      "content": "/* ── toggle: generated from packages/react/src/components/toggle.tsx ── */\n.rs-toggle{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:0.375rem;height:var(--hit);min-width:var(--hit);padding-block:0;padding-inline:1.25rem;font-size:0.8125rem;font-weight:500;letter-spacing:-0.01em;background-color:transparent;border-width:1px;border-style:solid;border-color:var(--control-border);border-radius:var(--radius-sm);color:var(--text-secondary);cursor:pointer;font-family:inherit;transition:var(--transition)}\n.rs-toggle:focus-visible{outline-width:2px;outline-style:solid;outline-color:var(--text);outline-offset:2px}\n@media (max-width: 640px){.rs-toggle{height:var(--hit);min-height:var(--hit);min-width:var(--hit);padding-inline:1.25rem;font-size:var(--control-fs);border-radius:var(--radius-sm)}}\n@media (hover: hover) and (pointer: fine){.rs-toggle:hover:not(:disabled):not([aria-disabled='true']){background-color:var(--control-fill);border-color:var(--control-fill);color:var(--text)}}\n@media (hover: hover) and (pointer: fine) and (forced-colors: active){.rs-toggle:hover:not(:disabled):not([aria-disabled='true']){background-color:ButtonFace;border-color:ButtonText;color:ButtonText}}\n@media (forced-colors: active){.rs-toggle{background-color:ButtonFace;border-color:ButtonText;color:ButtonText}}\n@media (prefers-reduced-motion: reduce){.rs-toggle{transition:none}}\n.rs-toggle-pressed{background-color:var(--text);border-color:var(--text);color:var(--bg);forced-color-adjust:none}\n@media (hover: hover) and (pointer: fine){.rs-toggle-pressed:hover:not(:disabled):not([aria-disabled='true']){background-color:var(--text);border-color:var(--text);color:var(--bg)}}\n@media (hover: hover) and (pointer: fine) and (forced-colors: active){.rs-toggle-pressed:hover:not(:disabled):not([aria-disabled='true']){background-color:Highlight;border-color:Highlight;color:HighlightText}}\n@media (forced-colors: active){.rs-toggle-pressed{background-color:Highlight;border-color:Highlight;color:HighlightText}}\n.rs-toggle-subtle{background-color:transparent;border-color:transparent;cursor:pointer;opacity:1}\n.rs-toggle-subtle:disabled{cursor:not-allowed;opacity:0.5}\n@media (hover: hover) and (pointer: fine){.rs-toggle-subtle:hover:not(:disabled):not([aria-disabled='true']){background-color:transparent;border-color:transparent}}\n@media (hover: hover) and (pointer: fine) and (forced-colors: active){.rs-toggle-subtle:hover:not(:disabled):not([aria-disabled='true']){background-color:ButtonFace;border-color:ButtonText}}\n@media (forced-colors: active){.rs-toggle-subtle{background-color:ButtonFace;border-color:ButtonText}.rs-toggle-subtle:disabled{opacity:1}}\n.rs-toggle-subtle-pressed{background-color:var(--control-fill);border-color:transparent;color:var(--text);font-weight:600}\n@media (hover: hover) and (pointer: fine){.rs-toggle-subtle-pressed:hover:not(:disabled):not([aria-disabled='true']){background-color:var(--control-fill);border-color:transparent;color:var(--text)}}\n@media (hover: hover) and (pointer: fine) and (forced-colors: active){.rs-toggle-subtle-pressed:hover:not(:disabled):not([aria-disabled='true']){background-color:Highlight;border-color:Highlight;color:HighlightText}}\n@media (forced-colors: active){.rs-toggle-subtle-pressed{background-color:Highlight;border-color:Highlight;color:HighlightText}}\n.rs-toggle-group{display:inline-flex;align-items:stretch;box-sizing:border-box;min-height:calc(var(--hit) + 2 * 1px);border-width:1px;border-style:solid;border-color:var(--text);border-radius:var(--radius-sm);overflow:hidden}\n@media (max-width: 640px){.rs-toggle-group{display:flex;width:100%;border-radius:var(--radius-sm)}}\n@media (forced-colors: active){.rs-toggle-group{border-color:ButtonText}}\n.rs-toggle-grouped{height:auto;min-height:var(--hit);border-width:0;border-radius:0;margin:0;border-inline-start-width:0;border-inline-start-style:none;border-inline-start-color:transparent}\n.rs-toggle-grouped:focus-visible{outline-offset:-2px}\n.rs-toggle-grouped:not(:first-child){border-inline-start-width:1px;border-inline-start-style:solid;border-inline-start-color:var(--control-border)}\n@media (max-width: 640px){.rs-toggle-grouped{flex-grow:1}}\n.rs-toggle-grouped-on{position:relative;z-index:1}\n.rs-toggle-group-subtle{min-width:0;max-width:100%;padding:0;gap:3px;background-color:var(--table-alt);border-color:var(--divider)}\n@media (forced-colors: active){.rs-toggle-group-subtle{background-color:Canvas;border-color:ButtonText}}\n.rs-toggle-grouped-subtle{flex-grow:1;flex-shrink:1;flex-basis:auto;min-width:var(--hit);padding-inline:0.75rem;border-radius:var(--radius-sm);border-inline-start-width:0;border-inline-start-style:none;border-inline-start-color:transparent;overflow-wrap:anywhere;white-space:normal}\n.rs-toggle-grouped-subtle:first-child{border-start-start-radius:0;border-end-start-radius:0}\n.rs-toggle-grouped-subtle:last-child{border-start-end-radius:0;border-end-end-radius:0}\n.rs-toggle-grouped-subtle:not(:first-child){border-inline-start-width:0;border-inline-start-style:none;border-inline-start-color:transparent}\n@media (max-width: 640px){.rs-toggle-grouped-subtle{flex-basis:0;min-width:var(--hit);padding-inline:0.625rem;border-radius:var(--radius-sm)}}\n",
      "type": "registry:file",
      "target": "styles/vlak/toggle.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "actions",
      "classes": [
        "rs-toggle",
        "rs-toggle-group",
        "rs-toggle-grouped",
        "rs-toggle-grouped-on",
        "rs-toggle-pressed",
        "rs-toggle-subtle",
        "rs-toggle-subtle-pressed",
        "rs-toggle-group-subtle",
        "rs-toggle-grouped-subtle"
      ],
      "snippet": "<button class=\"rs-toggle rs-toggle-pressed\" aria-pressed=\"true\">Bold</button>\n<button class=\"rs-toggle rs-toggle-subtle\" aria-pressed=\"false\">Italic</button>\n<button class=\"rs-toggle rs-toggle-pressed rs-toggle-subtle rs-toggle-subtle-pressed\" aria-pressed=\"true\">Underline</button>\n<button class=\"rs-toggle rs-toggle-subtle\" aria-pressed=\"false\" disabled>Strikethrough</button>",
      "cssOnly": false,
      "registryDependencies": [
        "vlak-lib"
      ],
      "aliases": [
        "Toggle",
        "Toggle button",
        "Press button",
        "Subtle toggle"
      ],
      "example": "import { useState } from \"react\";\nimport { Toggle } from \"@noorddev/vlak-react\";\n\nconst [bold, setBold] = useState(false);\nconst [italic, setItalic] = useState(false);\n\n<Toggle pressed={bold} onPressedChange={setBold}>Bold</Toggle>\n<Toggle variant=\"subtle\" pressed={italic} onPressedChange={setItalic}>Italic</Toggle>\n<Toggle variant=\"subtle\" defaultPressed>Underline</Toggle>\n<Toggle variant=\"subtle\" disabled>Strikethrough</Toggle>",
      "usage": {
        "use": [
          "A formatting or filter option that is on or off and stays in view.",
          "Default for a bordered toggle with an ink pressed state. Subtle stays transparent on hover while muted text turns to ink; pressing adds a soft fill and stronger text.",
          "Several independent toggles in a row."
        ],
        "avoid": [
          "A setting that applies immediately to the app; use Switch.",
          "One of several; use ToggleGroup."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Moves focus to the toggle"
        },
        {
          "keys": "Enter, Space",
          "does": "Presses or releases it"
        }
      ],
      "a11y": [
        "A native <button> with aria-pressed.",
        "Give icon-only toggles an aria-label. Controlled with pressed and onPressedChange, or uncontrolled with defaultPressed.",
        "Both variants keep a minimum 44px target, a visible keyboard focus ring, and system colors in forced colors. Native disabled prevents activation."
      ]
    }
  }
}
