{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "dependencies": [],
  "description": "APG switch with a native form bridge: one visible role=switch button owns naming, focus, native Space/Enter activation, and composed handlers while an aria-hidden checkbox owns FormData, required validation, reset, and external form association. Controlled via checked/defaultChecked + onCheckedChange; cancellation and controlled owners are authoritative.",
  "files": [
    {
      "content": "\"use client\";\n\n/**\n * heidi-ui Switch — APG switch button with native form semantics.\n *\n * The visible control remains the sole accessible role=switch button. An\n * aria-hidden native checkbox sits beside it for FormData, constraint\n * validation, reset, and external `form` ownership. Native button activation\n * owns Space/Enter so every keyboard press produces exactly one click and one\n * checked-state request. Associated native `<label>`s are reflected into\n * `aria-labelledby`, since a button is not named by its label.\n *\n * RSC rule: named exports from Server Components; Switch.X is client sugar.\n */\n\nimport {\n  type ChangeEvent,\n  type ComponentPropsWithoutRef,\n  createContext,\n  useCallback,\n  useContext,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type MouseEvent,\n  type ReactNode,\n  type Ref\n} from \"react\";\nimport {\n  createNativeChangeEventDetails,\n  useFormResetSync,\n  VISUALLY_HIDDEN_INPUT_STYLE\n} from \"../_internal/form-bridge\";\nimport { nativeDisabledAttrs } from \"../_internal/disabled\";\nimport {\n  composeHeidiEventHandlers,\n  mergeHeidiRefs,\n  renderHeidiElement,\n  type HeidiIntrinsicHostProps\n} from \"../_internal/render-element\";\nimport { safeId } from \"../_internal/safe-id\";\nimport { useHeidiLayoutEffect } from \"../_internal/use-layout-effect\";\nimport { SWITCH_CLASSES } from \"./switch.classes.generated\";\n\nexport type SwitchRootChangeEventDetails = {\n  /** Prevents this interaction from changing uncontrolled state. */\n  cancel: () => void;\n  /** Native event produced by the form-associated checkbox input. */\n  event: Event;\n  /** Whether `cancel()` has been called. */\n  readonly isCanceled: boolean;\n  reason: \"none\";\n  /** Visible switch button that initiated the change, when mounted. */\n  trigger: HTMLButtonElement | undefined;\n};\n\nexport type SwitchRootState = {\n  checked: boolean;\n  disabled: boolean;\n  readOnly: boolean;\n  required: boolean;\n};\n\ntype SwitchContextValue = SwitchRootState;\n\nconst SwitchContext = createContext<SwitchContextValue | null>(null);\n\nfunction useSwitchContext(part: string): SwitchContextValue {\n  const context = useContext(SwitchContext);\n  if (!context) {\n    throw new Error(`Switch.${part} must be rendered inside Switch.Root.`);\n  }\n  return context;\n}\n\n/**\n * Labels that name this switch.\n *\n * ponytail: read from the ROOT, not from the hidden input — the opposite of\n * Checkbox. Checkbox's visible control is a `<span>`, which is not labelable, so\n * a wrapping `<label>` binds to the hidden input and `input.labels` is the right\n * source. Switch's visible control is a real `<button>` rendered BEFORE the\n * input, so it is the label's one labeled control and `input.labels` is always\n * empty; sourcing from the input here would have found nothing at all. The\n * input is still consulted second so a `render` host that is not labelable\n * (`.labels` undefined) degrades to Checkbox's behaviour instead of to no name.\n */\nfunction findSwitchLabels(\n  root: HTMLElement | null,\n  input: HTMLInputElement | null\n): HTMLLabelElement[] {\n  const rootLabels =\n    root instanceof HTMLButtonElement ? Array.from(root.labels ?? []) : [];\n  return [...new Set([...rootLabels, ...Array.from(input?.labels ?? [])])];\n}\n\n/**\n * Mirror of `useCheckboxLabelledBy` / `useRadioItemLabelledBy`, differing only\n * in its label source (above) and its generated-id prefix.\n *\n * ponytail: kept LOCAL rather than extracted into `_internal/`. form-bridge's\n * own header already records that these hooks were left un-shared because\n * Radio's takes an extra parameter; Switch adds a third source rule, so the\n * three are not one hook with a different prefix. A shared extraction is worth\n * doing, but as its own slice with all four call sites in front of it.\n *\n * Spec: APG Switch — \"the switch has an accessible label\". HTML-AAM does not\n * name a `<button>` from an associated `<label>` (Blink happens to; WebKit does\n * not), so the bridge has to be explicit rather than left to the UA.\n */\nfunction useSwitchLabelledBy(\n  explicit: string | undefined,\n  ariaLabel: string | undefined,\n  rootRef: { current: HTMLButtonElement | null },\n  inputRef: { current: HTMLInputElement | null }\n): string | undefined {\n  const generatedLabelId = `hui-switch-label-${safeId(useId())}`;\n  const generatedLabelSequenceRef = useRef(0);\n  const [fallback, setFallback] = useState<string | undefined>();\n\n  // Native label associations can change without a prop change (conditional\n  // label mount or a changed `htmlFor`), so re-check after every commit.\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useHeidiLayoutEffect(() => {\n    const labels =\n      explicit || ariaLabel\n        ? []\n        : findSwitchLabels(rootRef.current, inputRef.current);\n    for (const label of labels) {\n      if (!label.id) {\n        generatedLabelSequenceRef.current += 1;\n        label.id = `${generatedLabelId}-${generatedLabelSequenceRef.current}`;\n      }\n    }\n    const next = labels.map((label) => label.id).filter(Boolean).join(\" \") || undefined;\n    if (next !== fallback) {\n      setFallback(next);\n    }\n  });\n\n  return explicit ?? fallback;\n}\n\ntype NativeRootProps = Omit<\n  ComponentPropsWithoutRef<\"button\">,\n  | \"aria-checked\"\n  | \"aria-readonly\"\n  | \"aria-required\"\n  | \"children\"\n  | \"className\"\n  | \"disabled\"\n  | \"form\"\n  | \"name\"\n  | \"onClick\"\n  | \"ref\"\n  | \"role\"\n  | \"style\"\n  | \"type\"\n  | \"value\"\n>;\n\nexport type SwitchRootProps = HeidiIntrinsicHostProps<\n  SwitchRootState,\n  \"button\"\n> &\n  NativeRootProps & {\n    checked?: boolean;\n    children?: ReactNode;\n    defaultChecked?: boolean;\n    disabled?: boolean;\n    form?: string;\n    inputRef?: Ref<HTMLInputElement>;\n    name?: string;\n    onCheckedChange?: (\n      checked: boolean,\n      details: SwitchRootChangeEventDetails\n    ) => void;\n    onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n    readOnly?: boolean;\n    ref?: Ref<HTMLButtonElement>;\n    required?: boolean;\n    value?: string;\n  };\n\nexport function SwitchRoot({\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledByProp,\n  checked: checkedProp,\n  children,\n  className,\n  defaultChecked = false,\n  disabled = false,\n  form,\n  inputRef: inputRefProp,\n  name,\n  onCheckedChange,\n  onClick,\n  readOnly = false,\n  ref,\n  render,\n  required = false,\n  style,\n  value,\n  ...nativeProps\n}: SwitchRootProps) {\n  const controlled = checkedProp !== undefined;\n  const initialCheckedRef = useRef(defaultChecked);\n  const [internalChecked, setInternalChecked] = useState(defaultChecked);\n  const checked = checkedProp ?? internalChecked;\n  const rootRef = useRef<HTMLButtonElement | null>(null);\n  const nativeInputRef = useRef<HTMLInputElement | null>(null);\n  const mergedInputRef = mergeHeidiRefs(nativeInputRef, inputRefProp);\n  const ariaLabelledBy = useSwitchLabelledBy(\n    ariaLabelledByProp,\n    ariaLabel,\n    rootRef,\n    nativeInputRef\n  );\n  const state = useMemo<SwitchRootState>(\n    () => ({ checked, disabled, readOnly, required }),\n    [checked, disabled, readOnly, required]\n  );\n  const stateRef = useRef(state);\n  stateRef.current = state;\n\n  const syncNativeInput = useCallback(() => {\n    const input = nativeInputRef.current;\n    if (!input) {\n      return;\n    }\n    input.checked = stateRef.current.checked;\n    input.defaultChecked = initialCheckedRef.current;\n  }, []);\n\n  useHeidiLayoutEffect(syncNativeInput, [checked, syncNativeInput]);\n\n  useFormResetSync({\n    controlled,\n    form,\n    nativeInputRef,\n    resetToInitial: () => {\n      stateRef.current = {\n        ...stateRef.current,\n        checked: initialCheckedRef.current\n      };\n      setInternalChecked(initialCheckedRef.current);\n    },\n    syncNativeInput\n  });\n\n  const handleInputChange = useCallback(\n    (event: ChangeEvent<HTMLInputElement>) => {\n      if (disabled || readOnly) {\n        event.preventDefault();\n        queueMicrotask(syncNativeInput);\n        return;\n      }\n\n      const next = event.currentTarget.checked;\n      const details: SwitchRootChangeEventDetails = createNativeChangeEventDetails(\n        event.nativeEvent,\n        rootRef.current ?? undefined\n      );\n      onCheckedChange?.(next, details);\n      if (!details.isCanceled && !controlled) {\n        stateRef.current = { ...stateRef.current, checked: next };\n        setInternalChecked(next);\n      }\n      queueMicrotask(syncNativeInput);\n    },\n    [controlled, disabled, onCheckedChange, readOnly, syncNativeInput]\n  );\n\n  const activate = useCallback(() => {\n    if (!disabled && !readOnly) {\n      nativeInputRef.current?.click();\n    }\n  }, [disabled, readOnly]);\n\n  const dataState = checked ? \"checked\" : \"unchecked\";\n  const root = renderHeidiElement({\n    className: SWITCH_CLASSES.root,\n    dataPart: \"switch-root\",\n    element: \"button\",\n    props: {\n      ...nativeProps,\n      \"aria-checked\": checked,\n      \"aria-label\": ariaLabel,\n      \"aria-labelledby\": ariaLabelledBy,\n      \"aria-readonly\": readOnly || undefined,\n      \"aria-required\": required || undefined,\n      children,\n      \"data-checked\": checked ? \"\" : undefined,\n      \"data-readonly\": readOnly ? true : undefined,\n      \"data-required\": required ? true : undefined,\n      \"data-state\": dataState,\n      \"data-unchecked\": !checked ? \"\" : undefined,\n      onClick: composeHeidiEventHandlers(onClick, activate),\n      ref: mergeHeidiRefs(rootRef, ref),\n      role: \"switch\",\n      type: \"button\",\n      ...nativeDisabledAttrs(disabled)\n    },\n    renderProps: { className, render, style },\n    state\n  });\n\n  return (\n    <SwitchContext value={state}>\n      {root}\n      <input\n        {...nativeDisabledAttrs(disabled)}\n        aria-hidden=\"true\"\n        checked={checked}\n        data-hui-switch-input=\"\"\n        form={form}\n        name={name}\n        onChange={handleInputChange}\n        onFocus={() => rootRef.current?.focus({ preventScroll: true })}\n        readOnly={readOnly}\n        ref={mergedInputRef}\n        required={required}\n        style={VISUALLY_HIDDEN_INPUT_STYLE}\n        suppressHydrationWarning\n        tabIndex={-1}\n        type=\"checkbox\"\n        value={value}\n      />\n    </SwitchContext>\n  );\n}\n\nexport type SwitchThumbState = SwitchRootState;\n\ntype NativeThumbProps = Omit<\n  ComponentPropsWithoutRef<\"span\">,\n  \"aria-hidden\" | \"children\" | \"className\" | \"ref\" | \"style\"\n>;\n\nexport type SwitchThumbProps = HeidiIntrinsicHostProps<\n  SwitchThumbState,\n  \"span\"\n> &\n  NativeThumbProps & {\n    children?: ReactNode;\n    ref?: Ref<HTMLSpanElement>;\n  };\n\n/** Presentational knob; mirrors Root checked state. aria-hidden always. */\nexport function SwitchThumb({\n  children,\n  className,\n  ref,\n  render,\n  style,\n  ...nativeProps\n}: SwitchThumbProps) {\n  const state = useSwitchContext(\"Thumb\");\n  const dataState = state.checked ? \"checked\" : \"unchecked\";\n  return renderHeidiElement({\n    className: SWITCH_CLASSES.thumb,\n    dataPart: \"switch-thumb\",\n    element: \"span\",\n    props: {\n      ...nativeProps,\n      \"aria-hidden\": true,\n      children,\n      \"data-readonly\": state.readOnly ? true : undefined,\n      \"data-required\": state.required ? true : undefined,\n      \"data-state\": dataState,\n      ref\n    },\n    renderProps: { className, render, style },\n    state\n  });\n}\n\nexport const Switch = {\n  Root: SwitchRoot,\n  Thumb: SwitchThumb\n} as const;\n",
      "path": "packages/heidi-ui/src/switch/switch.tsx",
      "target": "components/ui/heidi/switch/switch.tsx",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui switch — STRUCTURAL CSS only. No --hui-*.\n */\n\n@layer heidi-ui-base {\n  .hui-switch-root {\n    align-items: center;\n    block-size: 1.25rem;\n    box-sizing: border-box;\n    display: inline-flex;\n    inline-size: 2.25rem;\n    margin: 0;\n    min-block-size: 0;\n    min-inline-size: 0;\n    position: relative;\n  }\n\n  /* The visible track stays 36×20; the hit target reaches the 24px minimum. */\n  .hui-switch-root::after {\n    content: \"\";\n    inset-block: -0.125rem;\n    inset-inline: 0;\n    position: absolute;\n  }\n\n  .hui-switch-thumb {\n    block-size: 1rem;\n    display: block;\n    inline-size: 1rem;\n    inset-block-start: 50%;\n    inset-inline-start: 0.125rem;\n    position: absolute;\n    transform: translateY(-50%);\n  }\n\n  .hui-switch-thumb[data-state=\"checked\"] {\n    inset-inline-start: auto;\n    inset-inline-end: 0.125rem;\n  }\n\n  .hui-switch-thumb[data-state=\"unchecked\"] {\n    inset-inline-start: 0.125rem;\n    inset-inline-end: auto;\n  }\n}\n",
      "path": "packages/heidi-ui/src/switch/switch.base.css",
      "target": "components/ui/heidi/switch/switch.base.css",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui switch — VISUAL theme (opt-in). Consumes --hui-*.\n */\n\n@layer heidi-ui {\n  .hui-switch-root {\n    background: var(--hui-color-border-default);\n    border: none;\n    border-radius: var(--hui-radius-full);\n    cursor: pointer;\n    flex-shrink: 0;\n    font: inherit;\n    padding: 0;\n    transition-duration: var(--hui-duration-fast);\n    transition-property: background-color;\n    transition-timing-function: var(--hui-ease-out);\n  }\n\n  .hui-switch-root:focus-visible {\n    outline: var(--hui-focus-ring-width) solid var(--hui-color-focus-ring);\n    outline-offset: var(--hui-focus-ring-offset);\n  }\n\n  .hui-switch-root[data-state=\"unchecked\"] {\n    background: var(--hui-color-border-default);\n  }\n\n  .hui-switch-root[data-state=\"checked\"] {\n    background: var(--hui-color-brand-primary);\n  }\n\n  /*\n   * ponytail: a knob, not a panel. bg-elevated put the thumb at neutral-900 in\n   * dark — DARKER than the unchecked track it slides in — and surface-lg hung a\n   * five-rung panel ambient off a 16px circle. bg-raised stays above the track\n   * in both schemes and pill-raised is the token authored for exactly this\n   * shape: an inset top highlight over one soft drop.\n  */\n  .hui-switch-thumb {\n    background: var(--hui-color-bg-raised);\n    border-radius: var(--hui-radius-full);\n    box-shadow: var(--hui-shadow-pill-raised);\n    transition-duration: var(--hui-duration-fast);\n    transition-property: inset-inline-start, inset-inline-end;\n    transition-timing-function: var(--hui-ease-out);\n  }\n\n  .hui-switch-root:disabled,\n  .hui-switch-root[data-disabled=\"true\"] {\n    cursor: not-allowed;\n    opacity: 0.5;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .hui-switch-root,\n    .hui-switch-thumb {\n      transition-duration: 0s;\n    }\n  }\n\n  @media (forced-colors: active) {\n    .hui-switch-root {\n      background: Canvas;\n      border: var(--hui-border-width) solid CanvasText;\n      forced-color-adjust: none;\n    }\n\n    .hui-switch-root[data-state=\"checked\"] {\n      background: Highlight;\n      border-color: Highlight;\n    }\n\n    .hui-switch-thumb {\n      background: CanvasText;\n      box-shadow: none;\n    }\n\n    .hui-switch-root[data-state=\"checked\"] .hui-switch-thumb {\n      background: HighlightText;\n    }\n\n    .hui-switch-root:focus-visible {\n      outline-color: Highlight;\n    }\n\n    .hui-switch-root:disabled,\n    .hui-switch-root[data-disabled=\"true\"] {\n      border-color: GrayText;\n      opacity: 1;\n    }\n\n    .hui-switch-root:disabled .hui-switch-thumb,\n    .hui-switch-root[data-disabled=\"true\"] .hui-switch-thumb {\n      background: GrayText;\n    }\n  }\n}\n",
      "path": "packages/heidi-ui/src/switch/switch.theme.css",
      "target": "components/ui/heidi/switch/switch.theme.css",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui switch — aggregator (base + Heidi adapter + theme).\n */\n\n@import \"./switch.base.css\";\n@import \"../../../../styles/heidi-ui-adapter.css\";\n@import \"./switch.theme.css\";\n",
      "path": "packages/heidi-ui/src/switch/switch.css",
      "target": "components/ui/heidi/switch/switch.css",
      "type": "registry:ui"
    },
    {
      "content": "/** GENERATED from packages/heidi-ui/src/switch/switch.base.css + packages/heidi-ui/src/switch/switch.theme.css — do not edit by hand.\n * Rebuild: `bun run hui:build`. Freshness: `bun run contracts:check`. */\nexport const SWITCH_CLASSES = {\n  root: \"hui-switch-root\",\n  thumb: \"hui-switch-thumb\",\n} as const;\n",
      "path": "packages/heidi-ui/src/switch/switch.classes.generated.ts",
      "target": "components/ui/heidi/switch/switch.classes.generated.ts",
      "type": "registry:ui"
    },
    {
      "content": "/** GENERATED from switch.anatomy.json + switch.base.css + switch.theme.css — do not edit by hand.\n * Rebuild: `bun run hui:build`. Freshness: `bun run contracts:check`. */\n\nexport type SwitchDisabled = \"true\";\nexport type SwitchState = \"checked\" | \"unchecked\";\n\nexport const SWITCH_ANATOMY = {\n  \"component\": \"switch\",\n  \"description\": \"APG switch with a native form bridge: one visible role=switch button owns naming, focus, native Space/Enter activation, and composed handlers while an aria-hidden checkbox owns FormData, required validation, reset, and external form association. Controlled via checked/defaultChecked + onCheckedChange; cancellation and controlled owners are authoritative.\",\n  \"schemaVersion\": 2,\n  \"parts\": {\n    \"root\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-checked\",\n          \"aria-label\",\n          \"aria-labelledby\",\n          \"aria-readonly\",\n          \"aria-required\"\n        ],\n        \"role\": \"switch\"\n      },\n      \"class\": \"hui-switch-root\",\n      \"css\": {\n        \"structural\": [\n          \"align-items\",\n          \"block-size\",\n          \"display\",\n          \"inline-size\",\n          \"position\"\n        ]\n      },\n      \"dataPart\": \"switch-root\",\n      \"description\": \"The sole visible role=switch button; track chrome lives here. A sibling visually-hidden native checkbox owns form behavior while Root owns aria-checked, native button activation, and synchronized state. Every associated native <label> is reflected into aria-labelledby, because HTML-AAM does not name a button from its label.\",\n      \"element\": \"button\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"checked\",\n        \"className\",\n        \"defaultChecked\",\n        \"disabled\",\n        \"form\",\n        \"inputRef\",\n        \"name\",\n        \"onCheckedChange\",\n        \"onClick\",\n        \"readOnly\",\n        \"ref\",\n        \"render\",\n        \"required\",\n        \"style\",\n        \"value\"\n      ],\n      \"pseudoStates\": [\n        \":disabled\",\n        \":focus-visible\"\n      ],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-state\": [\n          \"checked\",\n          \"unchecked\"\n        ]\n      }\n    },\n    \"thumb\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-hidden\"\n        ],\n        \"role\": null\n      },\n      \"class\": \"hui-switch-thumb\",\n      \"css\": {\n        \"structural\": [\n          \"block-size\",\n          \"display\",\n          \"inline-size\",\n          \"position\"\n        ]\n      },\n      \"dataPart\": \"switch-thumb\",\n      \"description\": \"Presentational knob; aria-hidden; slides via data-state on the root (thumb mirrors state for theming).\",\n      \"element\": \"span\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-state\": [\n          \"checked\",\n          \"unchecked\"\n        ]\n      }\n    }\n  },\n  \"rootProps\": [\n    \"checked\",\n    \"defaultChecked\",\n    \"disabled\",\n    \"form\",\n    \"inputRef\",\n    \"name\",\n    \"onCheckedChange\",\n    \"readOnly\",\n    \"required\",\n    \"value\"\n  ],\n  \"tokens\": [\n    \"--hui-border-width\",\n    \"--hui-color-bg-raised\",\n    \"--hui-color-border-default\",\n    \"--hui-color-brand-primary\",\n    \"--hui-color-focus-ring\",\n    \"--hui-duration-fast\",\n    \"--hui-ease-out\",\n    \"--hui-focus-ring-offset\",\n    \"--hui-focus-ring-width\",\n    \"--hui-radius-full\",\n    \"--hui-shadow-pill-raised\"\n  ]\n} as const;\n",
      "path": "packages/heidi-ui/src/switch/switch.anatomy.generated.ts",
      "target": "components/ui/heidi/switch/switch.anatomy.generated.ts",
      "type": "registry:ui"
    },
    {
      "content": "{\n  \"component\": \"switch\",\n  \"description\": \"APG switch with a native form bridge: one visible role=switch button owns naming, focus, native Space/Enter activation, and composed handlers while an aria-hidden checkbox owns FormData, required validation, reset, and external form association. Controlled via checked/defaultChecked + onCheckedChange; cancellation and controlled owners are authoritative.\",\n  \"schemaVersion\": 2,\n  \"parts\": {\n    \"root\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-checked\",\n          \"aria-label\",\n          \"aria-labelledby\",\n          \"aria-readonly\",\n          \"aria-required\"\n        ],\n        \"role\": \"switch\"\n      },\n      \"class\": \"hui-switch-root\",\n      \"css\": {\n        \"structural\": [\n          \"align-items\",\n          \"block-size\",\n          \"display\",\n          \"inline-size\",\n          \"position\"\n        ]\n      },\n      \"dataPart\": \"switch-root\",\n      \"description\": \"The sole visible role=switch button; track chrome lives here. A sibling visually-hidden native checkbox owns form behavior while Root owns aria-checked, native button activation, and synchronized state. Every associated native <label> is reflected into aria-labelledby, because HTML-AAM does not name a button from its label.\",\n      \"element\": \"button\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"checked\",\n        \"className\",\n        \"defaultChecked\",\n        \"disabled\",\n        \"form\",\n        \"inputRef\",\n        \"name\",\n        \"onCheckedChange\",\n        \"onClick\",\n        \"readOnly\",\n        \"ref\",\n        \"render\",\n        \"required\",\n        \"style\",\n        \"value\"\n      ],\n      \"pseudoStates\": [\n        \":disabled\",\n        \":focus-visible\"\n      ],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-state\": [\n          \"checked\",\n          \"unchecked\"\n        ]\n      }\n    },\n    \"thumb\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-hidden\"\n        ],\n        \"role\": null\n      },\n      \"class\": \"hui-switch-thumb\",\n      \"css\": {\n        \"structural\": [\n          \"block-size\",\n          \"display\",\n          \"inline-size\",\n          \"position\"\n        ]\n      },\n      \"dataPart\": \"switch-thumb\",\n      \"description\": \"Presentational knob; aria-hidden; slides via data-state on the root (thumb mirrors state for theming).\",\n      \"element\": \"span\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-state\": [\n          \"checked\",\n          \"unchecked\"\n        ]\n      }\n    }\n  },\n  \"rootProps\": [\n    \"checked\",\n    \"defaultChecked\",\n    \"disabled\",\n    \"form\",\n    \"inputRef\",\n    \"name\",\n    \"onCheckedChange\",\n    \"readOnly\",\n    \"required\",\n    \"value\"\n  ]\n}\n",
      "path": "packages/heidi-ui/src/switch/switch.anatomy.json",
      "target": "components/ui/heidi/switch/switch.anatomy.json",
      "type": "registry:ui"
    },
    {
      "content": "/**\n * Cross-cutting disabled attrs for heidi-ui interactive hosts.\n *\n * House convention (Phase 23):\n * - Button widgets → native `disabled` + `data-disabled=\"true\"` (CSS + a11y).\n * - Non-button options (Select.Item) → `aria-disabled` + `data-disabled=\"true\"`\n *   (native disabled is invalid on role=option divs).\n * - Group roots may cascade `disabled` to children and mirror `data-disabled`.\n * - Keyboard nav / activation always skip disabled hosts.\n */\n\nexport type NativeDisabledAttrs = {\n  \"data-disabled\"?: true;\n  disabled?: true;\n};\n\nexport type AriaDisabledAttrs = {\n  \"aria-disabled\"?: true;\n  \"data-disabled\"?: true;\n};\n\nexport type DataDisabledAttrs = {\n  \"data-disabled\"?: true;\n};\n\n/** Native button/input disabled + styling hook. */\nexport function nativeDisabledAttrs(disabled: boolean | undefined): NativeDisabledAttrs {\n  if (!disabled) {\n    return {};\n  }\n  return {\n    \"data-disabled\": true,\n    disabled: true\n  };\n}\n\n/** ARIA-disabled for non-native hosts (e.g. role=option). */\nexport function ariaDisabledAttrs(disabled: boolean | undefined): AriaDisabledAttrs {\n  if (!disabled) {\n    return {};\n  }\n  return {\n    \"aria-disabled\": true,\n    \"data-disabled\": true\n  };\n}\n\n/** Styling hook only (group roots that cascade disabled). */\nexport function dataDisabledAttrs(disabled: boolean | undefined): DataDisabledAttrs {\n  if (!disabled) {\n    return {};\n  }\n  return {\n    \"data-disabled\": true\n  };\n}\n\nexport function isDisabledElement(el: Element | null | undefined): boolean {\n  if (!el || !(el instanceof HTMLElement)) {\n    return false;\n  }\n  if (el.matches(\":disabled\")) {\n    return true;\n  }\n  return el.getAttribute(\"aria-disabled\") === \"true\" || el.getAttribute(\"data-disabled\") === \"true\";\n}\n",
      "path": "packages/heidi-ui/src/_internal/disabled.ts",
      "target": "components/ui/heidi/_internal/disabled.ts",
      "type": "registry:ui"
    },
    {
      "content": "/**\n * Shared form-association plumbing for the native-input primitives (P9).\n *\n * Checkbox, Switch and Radio each render a visually hidden, form-associated\n * `<input>` behind a styled control. Three pieces of that were copy-pasted.\n *\n * ponytail: as with P6, diffing first changed the slice. The style constant is\n * byte-identical in all three. The form-reset effect and the change-details\n * construction are byte-identical in TWO — Checkbox and Switch — and Radio has\n * neither: it is a GROUP, so N items share one form, and it refcounts\n * registrations per form instead of running one effect per control. Wrapping\n * that in a per-control hook would not be a thin wrapper, it would be a\n * rewrite of working group-registration logic. Radio therefore takes the style\n * constant only. The same is true of the labelled-by hooks the audit listed as\n * optional: Radio's takes an extra `initialFallback` parameter because a group\n * item inherits a name its sibling may supply, so they are not one hook with a\n * different prefix, and they stay where they are.\n */\n\nimport { useEffect, useRef, type CSSProperties } from \"react\";\n\n/**\n * Off-screen but focusable and form-associated. `position: fixed` (not\n * absolute) so an ancestor's transform cannot drag the input into view, and\n * `clip-path` rather than the legacy `clip`.\n */\nexport const VISUALLY_HIDDEN_INPUT_STYLE: CSSProperties = {\n  blockSize: 1,\n  border: 0,\n  clipPath: \"inset(50%)\",\n  inlineSize: 1,\n  insetBlockStart: 0,\n  insetInlineStart: 0,\n  margin: -1,\n  overflow: \"hidden\",\n  padding: 0,\n  position: \"fixed\",\n  whiteSpace: \"nowrap\"\n};\n\n/**\n * The details object a cancelable change callback receives. Structurally\n * identical to `CheckboxRootChangeEventDetails` and\n * `SwitchRootChangeEventDetails`, which stay declared in their own files so\n * the public type names — and their doc comments — remain per-component.\n */\n// Generic in the trigger element: Checkbox's is an `HTMLElement` (its root can\n// be re-rendered as any host) while Switch narrows to `HTMLButtonElement`. The\n// two constructions looked byte-identical because the difference lives in the\n// TYPE annotation above each one, not in the object literal.\nexport type NativeChangeEventDetails<TTrigger extends HTMLElement = HTMLElement> = {\n  cancel: () => void;\n  event: Event;\n  readonly isCanceled: boolean;\n  reason: \"none\";\n  trigger: TTrigger | undefined;\n};\n\nexport function createNativeChangeEventDetails<TTrigger extends HTMLElement>(\n  event: Event,\n  trigger: TTrigger | undefined\n): NativeChangeEventDetails<TTrigger> {\n  let canceled = false;\n  return {\n    cancel: () => {\n      canceled = true;\n    },\n    event,\n    get isCanceled() {\n      return canceled;\n    },\n    reason: \"none\",\n    trigger\n  };\n}\n\nexport type FormResetSyncOptions = {\n  /** Controlled inputs keep owner state; only the DOM input is re-synced. */\n  controlled: boolean;\n  /** The `form` prop. Present so re-parenting re-runs the subscription. */\n  form: string | undefined;\n  nativeInputRef: { current: HTMLInputElement | null };\n  /** Restore uncontrolled state to its initial value. */\n  resetToInitial: () => void;\n  /** Push current state back onto the DOM input. */\n  syncNativeInput: () => void;\n};\n\n/**\n * Re-sync a form-associated input when its owning form resets.\n *\n * The native reset lands on the input BEFORE any React state update, so the\n * push back onto the DOM is deferred a frame — and the pending frame is\n * cancelled on both re-reset and unmount, so a rapid double reset cannot leave\n * a stale frame writing over newer state.\n */\nexport function useFormResetSync({\n  controlled,\n  form,\n  nativeInputRef,\n  resetToInitial,\n  syncNativeInput\n}: FormResetSyncOptions): void {\n  // ponytail: `resetToInitial` is read through a ref rather than listed as a\n  // dependency. Both call sites' effects re-subscribed on exactly\n  // [controlled, form, syncNativeInput]; adding a fourth dependency would make\n  // them re-subscribe whenever the caller's closure changed identity, which is\n  // a behaviour change in a slice whose premise is changing none.\n  const resetToInitialRef = useRef(resetToInitial);\n  resetToInitialRef.current = resetToInitial;\n\n  useEffect(() => {\n    const input = nativeInputRef.current;\n    const ownerForm = input?.form;\n    if (!input || !ownerForm) {\n      return;\n    }\n    let resetFrame = 0;\n    const handleReset = () => {\n      if (!controlled) {\n        resetToInitialRef.current();\n      }\n      cancelAnimationFrame(resetFrame);\n      resetFrame = requestAnimationFrame(syncNativeInput);\n    };\n    ownerForm.addEventListener(\"reset\", handleReset);\n    return () => {\n      cancelAnimationFrame(resetFrame);\n      ownerForm.removeEventListener(\"reset\", handleReset);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps -- see ponytail above\n  }, [controlled, form, syncNativeInput]);\n}\n",
      "path": "packages/heidi-ui/src/_internal/form-bridge.ts",
      "target": "components/ui/heidi/_internal/form-bridge.ts",
      "type": "registry:ui"
    },
    {
      "content": "/**\n * Shared host rendering for heidi-ui parts — Base-UI-shaped composition\n * (docs/HEIDI-UI-HEADLESS.md § 4 / HEIDI-UI.md § 4.5):\n *   - className / style as value or (state) => value\n *   - render as ReactElement or (props, state) => ReactElement\n *   - ref forwarding\n *   - data-hui-part always set (stable unstyled hook)\n *\n * Structural inline styles (e.g. anchorName) are merged last so a consumer\n * style override cannot drop platform wiring.\n */\n\nimport {\n  cloneElement,\n  createElement,\n  isValidElement,\n  type CSSProperties,\n  type ComponentPropsWithRef,\n  type HTMLAttributes,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  type RefCallback\n} from \"react\";\n\nexport type HeidiClassName<State> = string | ((state: State) => string | undefined) | undefined;\n\nexport type HeidiStyle<State> =\n  | CSSProperties\n  | ((state: State) => CSSProperties | undefined)\n  | undefined;\n\nexport type HeidiRenderFn<State, Props> = (\n  props: Props,\n  state: State\n) => ReactElement;\n\nexport type HeidiRender<State, Props> =\n  | ReactElement\n  | HeidiRenderFn<State, Props>\n  | undefined;\n\ntype DefaultRenderProps = HTMLAttributes<HTMLElement> & { ref?: Ref<HTMLElement> };\n\nexport type HeidiHostProps<\n  State,\n  RenderProps extends object = DefaultRenderProps\n> = {\n  className?: HeidiClassName<State>;\n  render?: HeidiRender<State, RenderProps>;\n  style?: HeidiStyle<State>;\n};\n\nexport type HeidiIntrinsicHostProps<\n  State,\n  Tag extends keyof HTMLElementTagNameMap\n> = HeidiHostProps<State, ComponentPropsWithRef<Tag>>;\n\nfunction resolveClassName<State>(\n  className: HeidiClassName<State>,\n  state: State\n): string | undefined {\n  return typeof className === \"function\" ? className(state) : className;\n}\n\nfunction resolveStyle<State>(style: HeidiStyle<State>, state: State): CSSProperties | undefined {\n  return typeof style === \"function\" ? style(state) : style;\n}\n\nfunction mergeClassNames(...parts: Array<string | undefined>): string | undefined {\n  const merged = parts.filter(Boolean).join(\" \");\n  return merged.length > 0 ? merged : undefined;\n}\n\n/**\n * Compose a consumer event with library behavior. Consumers run first and may\n * cancel the component behavior with `event.preventDefault()`. Heidi keeps\n * this native-event convention intentionally: it avoids a second branded event\n * API while still making async/controlled actions vetoable.\n */\nexport function composeHeidiEventHandlers<Event extends { defaultPrevented: boolean }>(\n  consumer: ((event: Event) => void) | undefined,\n  library: (event: Event) => void\n): (event: Event) => void {\n  return (event) => {\n    consumer?.(event);\n    if (!event.defaultPrevented) {\n      library(event);\n    }\n  };\n}\n\n/** React 19-safe ref fan-out, including callback-ref cleanup functions. */\nexport function mergeHeidiRefs<Element>(\n  ...inputRefs: Array<Ref<Element> | undefined>\n): Ref<Element> | undefined {\n  const refs = Array.from(\n    new Set(\n      inputRefs.filter(\n        (ref): ref is Exclude<Ref<Element>, null> => ref != null\n      )\n    )\n  );\n  if (refs.length === 0) {\n    return undefined;\n  }\n  if (refs.length === 1) {\n    return refs[0];\n  }\n\n  let cache = mergedRefCache;\n  for (const ref of refs) {\n    const key = ref as object;\n    let child = cache.children.get(key);\n    if (!child) {\n      child = { children: new WeakMap() };\n      cache.children.set(key, child);\n    }\n    cache = child;\n  }\n  if (cache.callback) {\n    return cache.callback as RefCallback<Element>;\n  }\n\n  const callback: RefCallback<Element> = (node) => {\n    const cleanups: Array<() => void> = [];\n    for (const ref of refs) {\n      if (typeof ref === \"function\") {\n        const cleanup = ref(node);\n        if (node !== null) {\n          cleanups.push(typeof cleanup === \"function\" ? cleanup : () => ref(null));\n        }\n      } else if (ref) {\n        ref.current = node;\n        if (node !== null) {\n          cleanups.push(() => {\n            ref.current = null;\n          });\n        }\n      }\n    }\n    return cleanups.length > 0\n      ? () => {\n          for (const cleanup of cleanups) {\n            cleanup();\n          }\n        }\n      : undefined;\n  };\n  cache.callback = callback as RefCallback<unknown>;\n  return callback;\n}\n\ntype MergedRefCache = {\n  callback?: RefCallback<unknown>;\n  children: WeakMap<object, MergedRefCache>;\n};\n\nconst mergedRefCache: MergedRefCache = { children: new WeakMap() };\n\ntype UnknownHandler = (...args: never[]) => unknown;\n\nfunction isEventHandler(key: string, value: unknown): value is UnknownHandler {\n  return /^on[A-Z]/.test(key) && typeof value === \"function\";\n}\n\nfunction defaultPrevented(args: unknown[]): boolean {\n  const event = args[0];\n  return (\n    typeof event === \"object\" &&\n    event !== null &&\n    \"defaultPrevented\" in event &&\n    event.defaultPrevented === true\n  );\n}\n\nfunction composeUnknownHandlers(\n  consumer: UnknownHandler,\n  library: UnknownHandler\n): UnknownHandler {\n  if (consumer === library) {\n    return library;\n  }\n  return ((...args: unknown[]) => {\n    (consumer as (...handlerArgs: unknown[]) => unknown)(...args);\n    if (!defaultPrevented(args)) {\n      (library as (...handlerArgs: unknown[]) => unknown)(...args);\n    }\n  }) as UnknownHandler;\n}\n\n/** Library-owned host props — permissive so button `type`, `data-*`, `popover`, etc. type-check. */\nexport type HeidiElementProps<Tag extends keyof HTMLElementTagNameMap> = Record<\n  string,\n  unknown\n> & {\n  children?: ReactNode;\n  className?: string;\n  ref?: Ref<HTMLElementTagNameMap[Tag]>;\n  style?: CSSProperties;\n};\n\ntype RenderElementParams<\n  State,\n  Tag extends keyof HTMLElementTagNameMap,\n  RenderProps extends object = DefaultRenderProps\n> = {\n  /** Default class from the generated hui-* map (theme/base target). */\n  className?: string;\n  /** Stable machine hook — always emitted. */\n  dataPart: string;\n  /** Intrinsic tag when `render` is omitted. */\n  element: Tag;\n  /** Props the library owns (aria, ids, handlers, popover, …). */\n  props: HeidiElementProps<Tag>;\n  /** Optional consumer composition props. */\n  renderProps?: HeidiHostProps<State, RenderProps>;\n  /** Typed state passed to functional className/style/render. */\n  state: State;\n  /** Structural inline styles that must survive consumer style merges. */\n  structuralStyle?: CSSProperties;\n  /**\n   * Render-element handlers to remove instead of composing. This is reserved\n   * for states such as a focusable disabled composite item where the public\n   * contract requires press handlers to be completely inert.\n   */\n  suppressRenderedHandlers?: readonly string[];\n};\n\n/**\n * Render a heidi-ui host element with optional Base-shaped composition.\n */\nexport function renderHeidiElement<\n  State,\n  Tag extends keyof HTMLElementTagNameMap,\n  RenderProps extends object = DefaultRenderProps\n>(\n  params: RenderElementParams<State, Tag, RenderProps>\n): ReactElement {\n  const {\n    className,\n    dataPart,\n    element,\n    props,\n    renderProps,\n    state,\n    structuralStyle,\n    suppressRenderedHandlers\n  } = params;\n  const consumerClass = resolveClassName(renderProps?.className, state);\n  const consumerStyle = resolveStyle(renderProps?.style, state);\n  const mergedStyle: CSSProperties | undefined =\n    props.style || consumerStyle || structuralStyle\n      ? { ...props.style, ...consumerStyle, ...structuralStyle }\n      : undefined;\n\n  const outProps: HTMLAttributes<HTMLElement> & {\n    \"data-hui-part\": string;\n    ref?: Ref<HTMLElement>;\n  } = {\n    ...(props as HTMLAttributes<HTMLElement> & { ref?: Ref<HTMLElement> }),\n    className: mergeClassNames(consumerClass, className),\n    \"data-hui-part\": dataPart,\n    style: mergedStyle\n  };\n\n  const render = renderProps?.render;\n  if (typeof render === \"function\") {\n    // Post-merge the returned element as well. This deliberately makes the\n    // stable data hook, owned semantics, refs, and internal handlers survive\n    // even when a render function forgets to spread one of the supplied props.\n    return mergeRenderedElement(\n      render(outProps as unknown as RenderProps, state),\n      outProps,\n      dataPart,\n      structuralStyle,\n      suppressRenderedHandlers\n    );\n  }\n  if (isValidElement(render)) {\n    return mergeRenderedElement(\n      render,\n      outProps,\n      dataPart,\n      structuralStyle,\n      suppressRenderedHandlers\n    );\n  }\n\n  return createElement(element, outProps as never, props.children);\n}\n\ntype RenderedProps = Record<string, unknown> & {\n  children?: ReactNode;\n  className?: string;\n  ref?: Ref<HTMLElement>;\n  style?: CSSProperties;\n};\n\nfunction mergeRenderedElement(\n  element: ReactElement,\n  libraryProps: HTMLAttributes<HTMLElement> & {\n    \"data-hui-part\": string;\n    ref?: Ref<HTMLElement>;\n  },\n  dataPart: string,\n  structuralStyle: CSSProperties | undefined,\n  suppressRenderedHandlers: readonly string[] | undefined\n): ReactElement {\n  const rendered = element as ReactElement<RenderedProps>;\n  const renderedProps = rendered.props;\n  const renderedClass =\n    renderedProps.className === libraryProps.className\n      ? undefined\n      : renderedProps.className;\n  const merged: RenderedProps = {\n    ...renderedProps,\n    ...libraryProps,\n    className: mergeClassNames(renderedClass, libraryProps.className),\n    \"data-hui-part\": dataPart,\n    ref: mergeHeidiRefs(libraryProps.ref, renderedProps.ref),\n    style: {\n      ...libraryProps.style,\n      ...renderedProps.style,\n      ...structuralStyle\n    }\n  };\n\n  if (Object.prototype.hasOwnProperty.call(renderedProps, \"children\")) {\n    merged.children = renderedProps.children;\n  }\n\n  const suppressedHandlers = suppressRenderedHandlers\n    ? new Set(suppressRenderedHandlers)\n    : null;\n  for (const key of new Set([...Object.keys(renderedProps), ...Object.keys(libraryProps)])) {\n    const consumer = renderedProps[key];\n    const library = (libraryProps as unknown as Record<string, unknown>)[key];\n    if (suppressedHandlers?.has(key)) {\n      merged[key] = library;\n      continue;\n    }\n    if (isEventHandler(key, consumer) && isEventHandler(key, library)) {\n      merged[key] = composeUnknownHandlers(consumer, library);\n    }\n  }\n\n  return cloneElement(rendered, merged as never);\n}\n",
      "path": "packages/heidi-ui/src/_internal/render-element.ts",
      "target": "components/ui/heidi/_internal/render-element.ts",
      "type": "registry:ui"
    },
    {
      "content": "/**\n * Shared id sanitizer for heidi-ui hosts.\n *\n * React's useId() emits characters that are invalid in HTML id / attribute\n * selectors and in CSS identifiers (`«r0»` in React 19, `:r0:` earlier).\n * heidi-ui interpolates these ids into element ids, aria relationships, and\n * dashed-ident anchor names (`--hui-*-anchor-…`), so everything outside\n * [a-zA-Z0-9_-] is stripped.\n */\n\nexport function safeId(id: string): string {\n  return id.replace(/[^a-zA-Z0-9_-]/g, \"\");\n}\n",
      "path": "packages/heidi-ui/src/_internal/safe-id.ts",
      "target": "components/ui/heidi/_internal/safe-id.ts",
      "type": "registry:ui"
    },
    {
      "content": "import { useEffect, useLayoutEffect } from \"react\";\n\n/**\n * `useLayoutEffect` that does not warn on the server.\n *\n * ponytail: fifteen primitives imported `useLayoutEffect` straight from React.\n * Every one of them carries `\"use client\"`, which is easy to misread as\n * \"client-only\" — in the App Router it means \"hydrated on the client\", and the\n * component is still rendered to HTML on the server first. React logs\n * \"useLayoutEffect does nothing on the server\" for each one, so a consumer\n * doing SSR saw a wall of warnings the app that ships this library never saw,\n * because it renders these routes on the client path.\n *\n * The branch is evaluated ONCE at module scope, not per render, and it is\n * therefore stable across a component's lifetime — swapping which hook is\n * called between renders would violate the rules of hooks. `typeof document`\n * rather than `typeof window`: both work, but `document` is the thing the\n * effect actually needs, and it keeps the check honest in exotic runtimes that\n * define a partial `window`.\n *\n * Rejected: `useInsertionEffect`, which runs earlier but is specified for\n * style injection and is not a general layout hook; and per-file guards, which\n * is what the 15 copies would have become.\n */\nexport const useHeidiLayoutEffect =\n  typeof document === \"undefined\" ? useEffect : useLayoutEffect;\n",
      "path": "packages/heidi-ui/src/_internal/use-layout-effect.ts",
      "target": "components/ui/heidi/_internal/use-layout-effect.ts",
      "type": "registry:ui"
    }
  ],
  "name": "switch",
  "registryDependencies": [
    "@heidi-ui/heidi-tokens"
  ],
  "title": "Switch",
  "type": "registry:ui"
}
