{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "core-toggle-switch",
  "type": "registry:ui",
  "title": "Toggle Switch",
  "description": "On-off in three looks: a sliding rocker, a round stomp footswitch with a status LED, or a vertical hardware lever in a recessed metal slot.",
  "dependencies": [
    "maruaudio@>=0.0.3 <0.1.0"
  ],
  "registryDependencies": [
    "https://pluginui.com/r/core-cc-binding-editor.json",
    "https://pluginui.com/r/core-control-label.json",
    "https://pluginui.com/r/core-toggle-headless.json"
  ],
  "files": [
    {
      "path": "components/ui/toggle-switch.tsx",
      "content": "/**\n * Source: https://maru.audio/ui-kit/toggle-switch\n * Copyright © [Maru](https://maru.audio)\n *\n * Maru UI Kit — https://maru.audio/ui-kit\n *\n * Installed from the Maru UI registry. This file is yours now: edit it\n * freely, it will not be overwritten unless you re-run the add command.\n *\n * Keep this item’s companion files and CSS imports together. Dependencies\n * declared by the registry are installed with the source.\n */\n/**\n * ToggleSwitch — a stomp/rocker-style on-off.\n *\n * Three *shapes* from one component: \"rocker\" is a sliding switch with a\n * travelling cap, \"stomp\" is a round footswitch with a status LED, and\n * \"lever\" is a vertical hardware toggle in a recessed metal slot. Each shape\n * then wears any of the kit's shared skins (`lib/control-skin`), so shape and\n * finish are two independent choices: a vintage stomp and a vintage knob are\n * the same cream, a neon rocker glows the way a neon fader does.\n *\n * Behaviour lives in `ToggleHeadless` — state, the `role=\"switch\"` contract,\n * the keyboard path, the pressed flag — the same split `Knob` has from\n * `KnobHeadless`. What is left here is looks, the label and readout, and the\n * MIDI CC layer.\n */\n\nimport { useId, useState } from \"react\"\nimport type { CSSProperties, ReactNode } from \"react\"\nimport { token } from \"maruaudio/ui/shared/theme\"\nimport {\n  CONTROL_SKINS,\n  indicatorFace,\n  litGlow,\n  type ControlSkin,\n  type SkinTraits,\n} from \"maruaudio/ui/shared/control-skin\"\nimport { useControlSkin } from \"maruaudio/ui/shared/control-skin-context\"\nimport {\n  ToggleHeadless,\n  type ToggleRenderState,\n} from \"@/components/ui/toggle-headless\"\nimport {\n  CcBadge,\n  ccBadgeSlot,\n  ccDropTargetStyle,\n  useCcActivityPulse,\n  useCcBlink,\n  useCcDropTarget,\n  useCcMappingFlash,\n  useControlMidi,\n  type ControlMidiProps,\n} from \"maruaudio/ui/shared/cc-dnd\"\nimport { ControlLabel } from \"@/components/ui/control-label\"\nimport { CcControlMenuTarget } from \"@/components/ui/cc-binding-editor\"\n\n/** The three switch shapes. Orthogonal to the skin. */\nexport type ToggleShape = \"rocker\" | \"stomp\" | \"lever\"\n\nexport interface ToggleSwitchProps {\n  /** Optional author-supplied hover text; no default tooltip. */\n  title?: string\n  /** Controlled state. Omit for uncontrolled operation. */\n  checked?: boolean\n  /** Initial state when uncontrolled, and the double-click reset target. */\n  defaultChecked?: boolean\n  onChange?: (checked: boolean) => void\n\n  label?: string\n  path?: string\n  midi?: ControlMidiProps\n  /** Text shown in place of a value readout. Defaults to \"ON\"/\"OFF\". */\n  onLabel?: string\n  offLabel?: string\n  /** Hide the state readout entirely. */\n  hideValue?: boolean\n\n  /** The switch's shape. Independent of `skin`, which is its finish. */\n  variant?: ToggleShape\n  /**\n   * Visual finish, shared with every other control in the kit. Omitted, the\n   * switch takes whatever a surrounding `ControlSkinProvider` declares.\n   */\n  skin?: ControlSkin\n  size?: number\n  /** Glyph inside a stomp switch — a power symbol by default. */\n  icon?: ReactNode\n\n  disabled?: boolean\n  className?: string\n  style?: CSSProperties\n  \"aria-label\"?: string\n}\n\nexport function ToggleSwitch({\n  title,\n  checked,\n  defaultChecked = false,\n  onChange,\n  label,\n  path,\n  midi,\n  onLabel = \"ON\",\n  offLabel = \"OFF\",\n  hideValue = false,\n  variant = \"rocker\",\n  skin,\n  size = 1,\n  icon,\n  disabled = false,\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n}: ToggleSwitchProps) {\n  const labelId = useId()\n  const traits = CONTROL_SKINS[useControlSkin(skin)] ?? CONTROL_SKINS.default\n  // The readout and the CC range need the state, and in controlled mode the\n  // prop is it. Uncontrolled, the primitive owns it and reports it back.\n  const [uncontrolled, setUncontrolled] = useState(defaultChecked)\n  const current = checked !== undefined ? checked : uncontrolled\n  const cc = useControlMidi({\n    path,\n    label: label ?? ariaLabel,\n    range: { min: 0, max: 1 },\n    midi,\n  })\n  const pulse = useCcActivityPulse(cc?.activity)\n  const target = useCcDropTarget({\n    id: cc?.targetId,\n    path,\n    label: cc?.targetLabel ?? label ?? ariaLabel,\n    onCcDrop: cc?.onAssign,\n    accepts: cc?.accepts,\n    enabled: Boolean(cc) && !cc?.disabled,\n    controller: cc?.controller,\n    onLearnRequest: cc?.onLearn,\n    learning: cc?.learning ?? false,\n  })\n  const blink = useCcBlink(target.learning)\n  const flash = useCcMappingFlash(cc?.assignment, target, cc?.controller)\n\n  const control = (\n    <ToggleHeadless\n      className={`uikit-control${className ? ` ${className}` : \"\"}`}\n      {...(checked === undefined ? { defaultChecked } : { checked })}\n      onChange={(next) => {\n        if (checked === undefined) setUncontrolled(next)\n        onChange?.(next)\n      }}\n      disabled={disabled}\n      title={label ? undefined : title}\n      aria-description={label ? title : undefined}\n      aria-label={label ? undefined : (ariaLabel ?? \"Toggle\")}\n      aria-labelledby={label ? labelId : undefined}\n      // Kept from the old markup: panels style the lever through it.\n      data-variant={variant}\n      style={shapeButtonStyle(variant, size, disabled)}\n    >\n      {(state) =>\n        variant === \"stomp\" ? (\n          <StompFace {...state} traits={traits} scale={size} icon={icon} />\n        ) : variant === \"lever\" ? (\n          <LeverFace {...state} traits={traits} scale={size} />\n        ) : (\n          <RockerFace {...state} traits={traits} scale={size} />\n        )\n      }\n    </ToggleHeadless>\n  )\n\n  const wrapped = cc ? (\n    <CcControlMenuTarget\n      midi={cc}\n      target={target}\n      {...(flash ? { \"data-uikit-cc-flash\": \"\" } : {})}\n      presentation={{\n        label: label ?? ariaLabel ?? path ?? \"Toggle\",\n        value: current ? onLabel : offLabel,\n        ...(path ? { path } : {}),\n        range: `${offLabel}–${onLabel}`,\n      }}\n      style={{\n        ...ccDropTargetStyle({ ...target, blink, flash }),\n        ...style,\n      }}\n    >\n      {control}\n      {cc.assignment ? (\n        <span style={{ position: \"absolute\", top: -5, right: -7, zIndex: 1 }}>\n          {cc.renderBadge?.(ccBadgeSlot(cc, cc.assignment, pulse)) ?? (\n            <CcBadge\n              assignment={cc.assignment}\n              onClear={cc.onClear}\n              active={pulse}\n              controller={cc.controller}\n              sourceTargetId={cc.targetId}\n              onMove={cc.onClear}\n              movesAtomically={cc.movesAtomically}\n              designer={cc.designer === true}\n              {...(cc.binding ? { binding: cc.binding } : {})}\n              {...(cc.range ? { range: cc.range } : {})}\n            />\n          )}\n        </span>\n      ) : null}\n    </CcControlMenuTarget>\n  ) : (\n    <div style={style}>{control}</div>\n  )\n\n  if (!label && hideValue) return wrapped\n\n  return (\n    <ControlLabel\n      label={label}\n      labelId={labelId}\n      value={hideValue ? undefined : current ? onLabel : offLabel}\n    >\n      {wrapped}\n    </ControlLabel>\n  )\n}\n\n/**\n * What a face is handed: the primitive's render state, the resolved skin, and\n * the size scale. Faces are pure — no state, no listeners, no DOM reads.\n */\ninterface FaceProps extends ToggleRenderState {\n  traits: SkinTraits\n  scale: number\n  icon?: ReactNode\n}\n\n/**\n * The button's own box. The primitive renders the `<button>`, so its geometry\n * and cursor live here while every painted surface belongs to the face — that\n * is what keeps the faces swappable and the primitive appearance-free.\n */\nfunction shapeButtonStyle(\n  shape: ToggleShape,\n  scale: number,\n  disabled: boolean\n): CSSProperties {\n  const box =\n    shape === \"stomp\"\n      ? { width: 44 * scale, height: 44 * scale }\n      : shape === \"lever\"\n        ? { width: 34 * scale, height: 62 * scale }\n        : { width: 46 * scale, height: 24 * scale }\n  return {\n    position: \"relative\",\n    padding: 0,\n    border: 0,\n    background: \"transparent\",\n    cursor: disabled ? \"not-allowed\" : \"pointer\",\n    ...box,\n  }\n}\n\n/** A dark ink means a pale face, which wants a rim to sit on a dark panel. */\nfunction inkRing(traits: SkinTraits): string | undefined {\n  return traits.ink ? `0 0 0 1px ${traits.ink}` : undefined\n}\n\n/** Join the box-shadow layers a face has built, dropping the absent ones. */\nfunction shadows(...layers: Array<string | undefined>): string | undefined {\n  const used = layers.filter(Boolean)\n  return used.length > 0 ? used.join(\", \") : undefined\n}\n\nfunction RockerFace({ on, pressed, traits, scale }: FaceProps) {\n  const height = 24 * scale\n  const width = 46 * scale\n  const cap = height - 6\n\n  return (\n    <>\n      {/* Track */}\n      <span\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          border: `1px solid ${token.border}`,\n          borderRadius: 999,\n          background: on ? token.accent : token.track,\n          boxShadow: shadows(\n            pressed\n              ? \"inset 0 2px 5px rgba(0,0,0,0.5)\"\n              : \"inset 0 1px 3px rgba(0,0,0,0.35)\",\n            // A glow-at-rest skin keeps a halo even in the off position; the\n            // others stay dark, exactly as this switch always has.\n            traits.glowAtRest\n              ? litGlow(token.accent, 12 * scale, on, true)\n              : undefined\n          ),\n          transition: \"background 140ms ease-out\",\n        }}\n      />\n      {/* Travelling cap */}\n      <span\n        aria-hidden\n        className=\"uikit-animated\"\n        style={{\n          position: \"absolute\",\n          top: 2,\n          left: 2,\n          width: cap,\n          height: cap,\n          borderRadius: 999,\n          background:\n            traits.cap === \"moulded\"\n              ? token.text\n              : indicatorFace(traits.cap, token.cap),\n          boxShadow: shadows(\n            pressed\n              ? \"0 1px 2px rgba(0,0,0,0.5)\"\n              : \"0 2px 5px rgba(0,0,0,0.45)\",\n            // The cap *is* the state indicator, so it has to separate from the\n            // track it rides. A moulded face does that with its white top and a\n            // cream one with its ink; a flat face needs a rim or it disappears\n            // against a dark track.\n            inkRing(traits) ??\n              (traits.cap === \"moulded\"\n                ? undefined\n                : `0 0 0 1px ${token.border}`)\n          ),\n          transform: `translateX(${on ? width - cap - 6 : 0}px) scale(${pressed ? 0.92 : 1})`,\n        }}\n      />\n    </>\n  )\n}\n\nfunction StompFace({ on, pressed, traits, scale, icon }: FaceProps) {\n  const diameter = 44 * scale\n  // Default power switches use a quiet solid cap; vintage keeps its material.\n  const face =\n    traits.cap === \"moulded\" ? token.cap : indicatorFace(traits.cap, token.cap)\n\n  return (\n    <>\n      <span\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          borderRadius: 999,\n          border: `1px solid ${token.border}`,\n          background: face,\n          boxShadow: shadows(\n            pressed\n              ? `inset 0 3px 8px rgba(0,0,0,0.65), 0 0 0 1px ${token.accent}`\n              : \"0 3px 8px rgba(0,0,0,0.5)\",\n            // A stomp has always glowed when engaged; a glow-at-rest skin also\n            // keeps a softer halo while it is off.\n            pressed\n              ? undefined\n              : on || traits.glowAtRest\n                ? `0 0 ${on ? 14 : 10}px color-mix(in srgb, ${token.accent} ${on ? 45 : 30}%, transparent)`\n                : undefined\n          ),\n          transform: pressed ? \"translateY(1px) scale(0.97)\" : \"none\",\n          transition: \"transform 70ms ease-out, box-shadow 140ms ease-out\",\n        }}\n      />\n      {/* Status LED */}\n      <span\n        aria-hidden\n        className=\"uikit-animated\"\n        style={{\n          position: \"absolute\",\n          top: diameter * 0.12,\n          left: \"50%\",\n          marginLeft: -3,\n          width: 6,\n          height: 6,\n          borderRadius: 999,\n          background: on ? token.accent : \"rgba(148,163,184,0.3)\",\n          boxShadow: on ? `0 0 8px 2px ${token.accent}` : \"none\",\n        }}\n      />\n      <span\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          // On a cream face the muted off-state text disappears, so a skin\n          // that carries an ink lends it to the glyph.\n          color: on ? token.accent : (traits.ink ?? token.textMuted),\n          pointerEvents: \"none\",\n        }}\n      >\n        <span style={{ marginTop: diameter * 0.14, lineHeight: 0 }}>\n          {icon ?? <PowerGlyph size={diameter * 0.34} />}\n        </span>\n      </span>\n    </>\n  )\n}\n\nfunction LeverFace({ on, pressed, traits, scale }: FaceProps) {\n  const slotWidth = 10 * scale\n  const handleWidth = 15 * scale\n  const handleHeight = 27 * scale\n  const travel = 17 * scale\n  // The moulded lever's brushed-metal handle is its identity; the other skins\n  // replace it with the shared face so it matches the rest of the panel.\n  const handleFace =\n    traits.cap === \"moulded\"\n      ? \"linear-gradient(to right, #202220 0%, #73736e 32%, #2d302e 58%, #111312 100%)\"\n      : indicatorFace(traits.cap, token.cap, \"to right\")\n\n  return (\n    <>\n      {/* Recessed slot */}\n      <span\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          left: \"50%\",\n          top: \"50%\",\n          width: slotWidth,\n          height: 43 * scale,\n          borderRadius: 2 * scale,\n          border: \"1px solid rgba(65,63,57,0.68)\",\n          background:\n            \"linear-gradient(to right, #eceae3 0%, #77756e 16%, #242524 42%, #0e1010 58%, #87847b 84%, #f1eee6 100%)\",\n          boxShadow:\n            \"inset 0 2px 4px rgba(0,0,0,0.7), 0 1px 0 rgba(255,255,255,0.7)\",\n          transform: \"translate(-50%, -50%)\",\n        }}\n      />\n      {/* Handle */}\n      <span\n        aria-hidden\n        className=\"uikit-animated\"\n        style={{\n          position: \"absolute\",\n          left: \"50%\",\n          top: \"50%\",\n          width: handleWidth,\n          height: handleHeight,\n          borderRadius: `${3 * scale}px ${3 * scale}px ${7 * scale}px ${7 * scale}px`,\n          border: \"1px solid rgba(28,29,28,0.75)\",\n          background: handleFace,\n          boxShadow: shadows(\n            pressed\n              ? \"inset 0 2px 4px rgba(0,0,0,0.65)\"\n              : `0 ${3 * scale}px ${5 * scale}px rgba(24,25,23,0.48), inset 1px 0 1px rgba(255,255,255,0.22)`,\n            traits.glowAtRest\n              ? litGlow(token.accent, 10 * scale, on, true)\n              : undefined\n          ),\n          transform: `translate(-50%, -50%) translateY(${on ? -travel : travel}px) scale(${pressed ? 0.96 : 1})`,\n          transformOrigin: on ? \"50% 100%\" : \"50% 0%\",\n          transition: \"transform 110ms cubic-bezier(0.2, 0.8, 0.2, 1)\",\n        }}\n      >\n        <span\n          style={{\n            position: \"absolute\",\n            inset: `${4 * scale}px ${3 * scale}px auto`,\n            height: 1,\n            background: traits.ink ?? \"rgba(255,255,255,0.28)\",\n          }}\n        />\n      </span>\n    </>\n  )\n}\n\nfunction PowerGlyph({ size }: { size: number }) {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n      <path\n        d=\"M12 3v9\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2.5\"\n        strokeLinecap=\"round\"\n      />\n      <path\n        d=\"M6.3 6.3a8 8 0 1 0 11.4 0\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2.5\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  )\n}\n",
      "type": "registry:component",
      "target": "@ui/toggle-switch.tsx"
    }
  ]
}
