{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "dependencies": [],
  "description": "APG slider (single thumb). Thumb is role=slider with aria-valuemin/max/now + aria-orientation. Arrow/Home/End/PageUp/PageDown adjust value by step. Controlled via value/defaultValue + onValueChange, with onValueCommitted fired once per interaction. Requires label. Track click focuses the thumb and jumps the value; grabbing the thumb keeps its grab offset instead of re-anchoring. A name enables form submission and native reset. Composition via renderHeidiElement.",
  "files": [
    {
      "content": "\"use client\";\n\n/**\n * heidi-ui Slider — APG slider (single thumb)\n * (docs/HEIDI-UI-HEADLESS.md § P4 / Slice G). Thumb is role=slider with\n * aria-valuemin/max/now + aria-orientation. Arrow/Home/End/PageUp/PageDown.\n * Track pointer (which also focuses the thumb) + offset-preserving thumb drag.\n * Controlled: value / defaultValue + onValueChange, with onValueCommitted fired\n * once per interaction. A `name` renders a hidden form-associated input so the\n * value submits and survives a native form reset.\n * Requires label. Root `disabled` → data-disabled on root; thumb native\n * disabled; track/thumb ignore pointer + keyboard.\n *\n * RSC rule: named exports from Server Components; Slider.X is client sugar.\n */\n\nimport {\n  type ComponentPropsWithoutRef,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n  type Ref\n} from \"react\";\nimport { dataDisabledAttrs, nativeDisabledAttrs } from \"../_internal/disabled\";\nimport { useFormResetSync } from \"../_internal/form-bridge\";\nimport {\n  composeHeidiEventHandlers,\n  mergeHeidiRefs,\n  renderHeidiElement,\n  type HeidiIntrinsicHostProps\n} from \"../_internal/render-element\";\nimport { type SliderOrientation } from \"./slider.anatomy.generated\";\nimport { SLIDER_CLASSES } from \"./slider.classes.generated\";\n\nexport type { SliderOrientation };\n\ntype SliderContextValue = {\n  /** Flush one `onValueCommitted` for the interaction that just ended. */\n  commitValue: () => void;\n  disabled: boolean;\n  label: string;\n  max: number;\n  min: number;\n  orientation: SliderOrientation;\n  percent: number;\n  /** Unsnapped value under the pointer; the drag grab-offset needs sub-step precision. */\n  rawValueAtPointer: (clientX: number, clientY: number, track: HTMLElement) => number;\n  setFromPointer: (clientX: number, clientY: number, track: HTMLElement) => void;\n  setValue: (next: number) => void;\n  step: number;\n  value: number;\n};\n\nconst SliderContext = createContext<SliderContextValue | null>(null);\n\nfunction useSliderContext(part: string): SliderContextValue {\n  const context = useContext(SliderContext);\n  if (!context) {\n    throw new Error(`Slider.${part} must be rendered inside Slider.Root.`);\n  }\n  return context;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n  return Math.min(max, Math.max(min, value));\n}\n\nfunction snap(value: number, min: number, max: number, step: number): number {\n  // step <= 0 (or negative/zero from the caller) degrades to a plain clamp —\n  // a divide-by-zero snap would emit NaN into geometry and aria-valuenow.\n  if (step <= 0) {\n    return clamp(value, min, max);\n  }\n  const snapped = Math.round((value - min) / step) * step + min;\n  return clamp(Number(snapped.toFixed(10)), min, max);\n}\n\nfunction toPercent(value: number, min: number, max: number): number {\n  if (max <= min) {\n    return 0;\n  }\n  return ((value - min) / (max - min)) * 100;\n}\n\n/**\n * Value under the pointer, UNSNAPPED.\n *\n * ponytail: this used to snap before returning, and every caller then snapped\n * again inside `setValue`. Snapping here is not just redundant, it destroys the\n * sub-step precision the drag grab-offset needs: an offset measured between two\n * already-quantized values loses the fraction of a step the user actually\n * grabbed, so the thumb still drifts under the cursor mid-drag. Snapping stays\n * where the value enters state (`setValue`), which is the single place that has\n * to enforce it.\n */\nfunction rawValueFromPointer(\n  clientX: number,\n  clientY: number,\n  track: HTMLElement,\n  orientation: SliderOrientation,\n  min: number,\n  max: number\n): number {\n  const rect = track.getBoundingClientRect();\n  const ratio =\n    orientation === \"vertical\"\n      ? rect.height <= 0\n        ? 0\n        : (rect.bottom - clientY) / rect.height\n      : rect.width <= 0\n        ? 0\n        : (clientX - rect.left) / rect.width;\n  const logicalRatio =\n    orientation === \"horizontal\" &&\n    getComputedStyle(track).direction === \"rtl\"\n      ? 1 - ratio\n      : ratio;\n  return min + clamp(logicalRatio, 0, 1) * (max - min);\n}\n\n/** Sibling part inside the same Slider.Root, resolved by its data-hui-part. */\nfunction findSliderPart(from: HTMLElement, part: string): HTMLElement | null {\n  return (\n    from\n      .closest('[data-hui-part=\"slider-root\"]')\n      ?.querySelector<HTMLElement>(`[data-hui-part=\"${part}\"]`) ?? null\n  );\n}\n\nexport type SliderRootState = {\n  disabled: boolean;\n  orientation: SliderOrientation;\n  value: number;\n};\n\ntype SliderRootNativeProps = Omit<\n  ComponentPropsWithoutRef<\"div\">,\n  | \"children\"\n  | \"className\"\n  | \"defaultValue\"\n  | \"ref\"\n  | \"style\"\n  | \"value\"\n>;\n\nexport type SliderRootProps = HeidiIntrinsicHostProps<SliderRootState, \"div\"> &\n  SliderRootNativeProps & {\n    children?: ReactNode;\n    defaultValue?: number;\n    disabled?: boolean;\n    /** Id of a form elsewhere in the document that owns the submitted value. */\n    form?: string;\n    /** Accessible name for the slider thumb (aria-label). Required. */\n    label: string;\n    max?: number;\n    min?: number;\n    /** Submits the current value under this key; also enables native form reset. */\n    name?: string;\n    onValueChange?: (value: number) => void;\n    /** Fires once when an interaction ends, not on every dragged frame. */\n    onValueCommitted?: (value: number) => void;\n    orientation?: SliderOrientation;\n    ref?: Ref<HTMLDivElement>;\n    step?: number;\n    value?: number;\n  };\n\nexport function SliderRoot({\n  children,\n  className,\n  defaultValue = 0,\n  disabled = false,\n  form,\n  label,\n  max = 100,\n  min = 0,\n  name,\n  onValueChange,\n  onValueCommitted,\n  orientation = \"horizontal\",\n  ref,\n  render,\n  step = 1,\n  style,\n  value: valueProp,\n  ...nativeProps\n}: SliderRootProps) {\n  const [internal, setInternal] = useState(defaultValue);\n  const initialValueRef = useRef(defaultValue);\n  const hiddenInputRef = useRef<HTMLInputElement | null>(null);\n  // ponytail: Slider resolves only NON-FINITENESS, and nothing else. It still\n  // does not adopt _internal/range normalizeRange (unlike Meter, whose\n  // adoption was equivalence-verified): rebasing `max <= min` onto a synthetic\n  // min+100 span would position the thumb inside an invented range, so an\n  // inverted-but-finite range is left exactly as the caller passed it and\n  // toPercent's `max <= min` guard pins the thumb at 0. A finite controlled\n  // value is likewise never snapped or clamped at render — the caller sees\n  // back the value it emitted, and snapping happens only on change.\n  //\n  // What that original rule got wrong is that NaN and ±Infinity are not\n  // values a caller can have meant. Leaving them raw emitted\n  // aria-valuemin=\"NaN\" / aria-valuemax=\"-Infinity\" / aria-valuenow=\"Infinity\"\n  // — not numbers, so not a valid ARIA range for any screen reader to read —\n  // and left `insetInlineStart` empty, so the thumb silently detached from its\n  // own value. A degenerate `step` was the same story one level down: snap's\n  // `step <= 0 → clamp only` guard stops the divide-by-zero, but it also makes\n  // every arrow key a no-op, which is a keyboard-operability failure (WCAG\n  // 2.1.1) on a control whose entire APG contract is arrow keys. The APG\n  // default step is 1, so that is what a nonsense step degrades to.\n  //\n  // For every finite input, `resolved* === ` the raw prop, so this changes\n  // nothing for real consumers. Spec: `slider--invalid-values` in the Lab plus\n  // `slider: invalid values normalize to a finite ordered ARIA range` in\n  // scripts/smoke-heidi-ui-behavior/widgets.ts.\n  const resolvedMax = Number.isFinite(max) ? max : 100;\n  const resolvedMin = Number.isFinite(min) ? min : 0;\n  const resolvedStep = Number.isFinite(step) && step > 0 ? step : 1;\n  const controlled = valueProp !== undefined;\n  const rawValue = valueProp ?? internal;\n  const value = Number.isFinite(rawValue) ? rawValue : resolvedMin;\n  const percent = toPercent(value, resolvedMin, resolvedMax);\n\n  // ponytail: the pending commit holds the last value this interaction EMITTED,\n  // not `value`. A controlled owner may apply the change a tick later (or veto\n  // it), so reading `value` at pointer-up would commit a stale number. Null\n  // means \"this interaction never changed anything\", which is why a bare click\n  // on the thumb — which no longer repositions — stays silent instead of\n  // firing a no-op commit.\n  const pendingCommitRef = useRef<number | null>(null);\n\n  const setValue = useCallback(\n    (next: number) => {\n      if (disabled) {\n        return;\n      }\n      const snapped = snap(next, resolvedMin, resolvedMax, resolvedStep);\n      pendingCommitRef.current = snapped;\n      if (!controlled) {\n        setInternal(snapped);\n      }\n      onValueChange?.(snapped);\n    },\n    [controlled, disabled, onValueChange, resolvedMax, resolvedMin, resolvedStep]\n  );\n\n  const commitValue = useCallback(() => {\n    const pending = pendingCommitRef.current;\n    if (pending === null) {\n      return;\n    }\n    pendingCommitRef.current = null;\n    onValueCommitted?.(pending);\n  }, [onValueCommitted]);\n\n  const rawValueAtPointer = useCallback(\n    (clientX: number, clientY: number, track: HTMLElement) =>\n      rawValueFromPointer(\n        clientX,\n        clientY,\n        track,\n        orientation,\n        resolvedMin,\n        resolvedMax\n      ),\n    [orientation, resolvedMax, resolvedMin]\n  );\n\n  const setFromPointer = useCallback(\n    (clientX: number, clientY: number, track: HTMLElement) => {\n      if (disabled) {\n        return;\n      }\n      setValue(rawValueAtPointer(clientX, clientY, track));\n    },\n    [disabled, rawValueAtPointer, setValue]\n  );\n\n  const syncHiddenInput = useCallback(() => {\n    const input = hiddenInputRef.current;\n    if (input) {\n      input.defaultValue = String(initialValueRef.current);\n    }\n  }, []);\n\n  // The hidden input's `value` is React-controlled, so its RESET value has to\n  // be written imperatively — exactly how Checkbox seeds `defaultChecked`.\n  useEffect(syncHiddenInput, [syncHiddenInput]);\n\n  useFormResetSync({\n    controlled,\n    form,\n    nativeInputRef: hiddenInputRef,\n    resetToInitial: () => setInternal(initialValueRef.current),\n    syncNativeInput: syncHiddenInput\n  });\n\n  const context: SliderContextValue = {\n    commitValue,\n    disabled,\n    label,\n    max: resolvedMax,\n    min: resolvedMin,\n    orientation,\n    percent,\n    rawValueAtPointer,\n    setFromPointer,\n    setValue,\n    step: resolvedStep,\n    value\n  };\n\n  return (\n    <SliderContext value={context}>\n      {renderHeidiElement({\n        className: SLIDER_CLASSES.root,\n        dataPart: \"slider-root\",\n        element: \"div\",\n        props: {\n          ...nativeProps,\n          children,\n          \"data-orientation\": orientation,\n          ref,\n          ...dataDisabledAttrs(disabled)\n        },\n        renderProps: { className, render, style },\n        state: { disabled, orientation, value }\n      })}\n      {name === undefined ? null : (\n        // ponytail: a hidden input, not a visually hidden `<input type=\"range\">`.\n        // A real range input would be a SECOND role=slider in the accessibility\n        // tree competing with the thumb for the same accessible name, and the\n        // thumb is the APG host. `readOnly` is what suppresses React's\n        // controlled-without-onChange warning; the value is pushed from state,\n        // and native form reset is reconciled through useFormResetSync above.\n        <input\n          data-hui-slider-input=\"\"\n          disabled={disabled}\n          form={form}\n          name={name}\n          readOnly\n          ref={hiddenInputRef}\n          suppressHydrationWarning\n          type=\"hidden\"\n          value={String(value)}\n        />\n      )}\n    </SliderContext>\n  );\n}\n\nexport type SliderTrackState = { orientation: SliderOrientation };\n\ntype SliderTrackNativeProps = Omit<\n  ComponentPropsWithoutRef<\"div\">,\n  | \"children\"\n  | \"className\"\n  | \"onLostPointerCapture\"\n  | \"onPointerDown\"\n  | \"onPointerMove\"\n  | \"ref\"\n  | \"style\"\n>;\n\nexport type SliderTrackProps = HeidiIntrinsicHostProps<SliderTrackState, \"div\"> &\n  SliderTrackNativeProps & {\n    children?: ReactNode;\n    onLostPointerCapture?: ComponentPropsWithoutRef<\"div\">[\"onLostPointerCapture\"];\n    onPointerDown?: ComponentPropsWithoutRef<\"div\">[\"onPointerDown\"];\n    onPointerMove?: ComponentPropsWithoutRef<\"div\">[\"onPointerMove\"];\n    ref?: Ref<HTMLDivElement>;\n  };\n\nexport function SliderTrack({\n  children,\n  className,\n  onLostPointerCapture: onLostPointerCaptureProp,\n  onPointerDown: onPointerDownProp,\n  onPointerMove: onPointerMoveProp,\n  ref,\n  render,\n  style,\n  ...nativeProps\n}: SliderTrackProps) {\n  const { commitValue, disabled, orientation, setFromPointer } =\n    useSliderContext(\"Track\");\n  const trackRef = useRef<HTMLDivElement | null>(null);\n\n  const onPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (disabled || event.button !== 0) {\n      return;\n    }\n    const track = trackRef.current ?? event.currentTarget;\n    // ponytail: the Thumb is rendered INSIDE the Track, so a press on the thumb\n    // bubbles here — and this handler then re-derived the value from it. Two\n    // bugs from one line: grabbing the thumb 7px off-centre snapped the value\n    // (~2 points on a 320px 0..100 track) and fired onValueChange before the\n    // drag began, and `setPointerCapture` below stole the capture the Thumb had\n    // just taken one dispatch earlier, so the Thumb's own pointermove went dead\n    // and the rest of the drag re-anchored the thumb centre under the cursor\n    // instead of preserving the grab offset. The Thumb owns its press\n    // (capture + focus + offset); the Track handles only presses on itself.\n    // Radix makes the same test (`thumbs.has(target)`) at the same point.\n    // Rejected: stopPropagation inside the Thumb — it would also silence a\n    // consumer's own onPointerDown passed to Track, which is public API.\n    if (\n      event.target instanceof Element &&\n      event.target.closest('[data-hui-part=\"slider-thumb\"]') !== null\n    ) {\n      return;\n    }\n    // ponytail: cancel the press's default action, or the browser undoes the\n    // focus on the very next line. mousedown's default focus behaviour runs\n    // AFTER pointerdown and, with no focusable ancestor of the track, it moved\n    // focus back off the thumb — measured as focusin(thumb) → mousedown →\n    // focusout(thumb) on a plain track click. Rejected preventing default on\n    // mousedown instead: it fixes the mouse and leaves the touch compatibility\n    // path re-stealing focus the same way.\n    event.preventDefault();\n    track.setPointerCapture(event.pointerId);\n    // ponytail: focus moves to the thumb BEFORE the value changes. APG puts\n    // focus on the thumb (\"focus is placed on the slider thumb\"), and without\n    // this a track click changed the value while activeElement stayed <body> —\n    // so the user's next ArrowRight scrolled the page instead of nudging the\n    // control they had just operated (WCAG 2.1.1 / 2.4.7). Rejected focusing on\n    // pointerUP: the focus ring must be visible during the drag, not after it.\n    findSliderPart(track, \"slider-thumb\")?.focus({ preventScroll: true });\n    setFromPointer(event.clientX, event.clientY, track);\n  };\n\n  const onPointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (disabled) {\n      return;\n    }\n    const track = trackRef.current ?? event.currentTarget;\n    if (!track.hasPointerCapture(event.pointerId)) {\n      return;\n    }\n    setFromPointer(event.clientX, event.clientY, track);\n  };\n\n  return renderHeidiElement({\n    className: SLIDER_CLASSES.track,\n    dataPart: \"slider-track\",\n    element: \"div\",\n    props: {\n      ...nativeProps,\n      children,\n      \"data-orientation\": orientation,\n      // `lostpointercapture`, not `pointerup`: it is the one event that fires\n      // exactly once per drag, on cancel as well as on release.\n      onLostPointerCapture: composeHeidiEventHandlers(\n        onLostPointerCaptureProp,\n        commitValue\n      ),\n      onPointerDown: composeHeidiEventHandlers(\n        onPointerDownProp,\n        onPointerDown\n      ),\n      onPointerMove: composeHeidiEventHandlers(\n        onPointerMoveProp,\n        onPointerMove\n      ),\n      ref: mergeHeidiRefs(trackRef, ref),\n      ...dataDisabledAttrs(disabled)\n    },\n    renderProps: { className, render, style },\n    state: { orientation }\n  });\n}\n\nexport type SliderRangeState = { orientation: SliderOrientation };\n\ntype SliderRangeNativeProps = Omit<\n  ComponentPropsWithoutRef<\"div\">,\n  \"children\" | \"className\" | \"ref\" | \"style\"\n>;\n\nexport type SliderRangeProps = HeidiIntrinsicHostProps<SliderRangeState, \"div\"> &\n  SliderRangeNativeProps & {\n    children?: ReactNode;\n    ref?: Ref<HTMLDivElement>;\n  };\n\nexport function SliderRange({\n  children,\n  className,\n  ref,\n  render,\n  style,\n  ...nativeProps\n}: SliderRangeProps) {\n  const { orientation, percent } = useSliderContext(\"Range\");\n  const structuralStyle: CSSProperties =\n    orientation === \"vertical\"\n      ? { blockSize: `${percent}%` }\n      : { inlineSize: `${percent}%` };\n\n  return renderHeidiElement({\n    className: SLIDER_CLASSES.range,\n    dataPart: \"slider-range\",\n    element: \"div\",\n    props: {\n      ...nativeProps,\n      children,\n      \"data-orientation\": orientation,\n      ref\n    },\n    renderProps: { className, render, style },\n    state: { orientation },\n    structuralStyle\n  });\n}\n\nexport type SliderThumbState = { orientation: SliderOrientation };\n\ntype SliderThumbNativeProps = Omit<\n  ComponentPropsWithoutRef<\"button\">,\n  | \"aria-label\"\n  | \"aria-orientation\"\n  | \"aria-valuemax\"\n  | \"aria-valuemin\"\n  | \"aria-valuenow\"\n  | \"children\"\n  | \"className\"\n  | \"disabled\"\n  | \"onKeyDown\"\n  | \"onKeyUp\"\n  | \"onLostPointerCapture\"\n  | \"onPointerDown\"\n  | \"onPointerMove\"\n  | \"ref\"\n  | \"role\"\n  | \"style\"\n  | \"type\"\n>;\n\nexport type SliderThumbProps = HeidiIntrinsicHostProps<SliderThumbState, \"button\"> &\n  SliderThumbNativeProps & {\n    children?: ReactNode;\n    onKeyDown?: ComponentPropsWithoutRef<\"button\">[\"onKeyDown\"];\n    onKeyUp?: ComponentPropsWithoutRef<\"button\">[\"onKeyUp\"];\n    onLostPointerCapture?: ComponentPropsWithoutRef<\"button\">[\"onLostPointerCapture\"];\n    onPointerDown?: ComponentPropsWithoutRef<\"button\">[\"onPointerDown\"];\n    onPointerMove?: ComponentPropsWithoutRef<\"button\">[\"onPointerMove\"];\n    ref?: Ref<HTMLButtonElement>;\n  };\n\nexport function SliderThumb({\n  children,\n  className,\n  onKeyDown: onKeyDownProp,\n  onKeyUp: onKeyUpProp,\n  onLostPointerCapture: onLostPointerCaptureProp,\n  onPointerDown: onPointerDownProp,\n  onPointerMove: onPointerMoveProp,\n  ref,\n  render,\n  style,\n  ...nativeProps\n}: SliderThumbProps) {\n  const {\n    commitValue,\n    disabled,\n    label,\n    max,\n    min,\n    orientation,\n    percent,\n    rawValueAtPointer,\n    setValue,\n    step,\n    value\n  } = useSliderContext(\"Thumb\");\n  const thumbRef = useRef<HTMLButtonElement | null>(null);\n  // Distance between the pointer's value and the thumb's value at grab time.\n  const grabOffsetRef = useRef(0);\n\n  const structuralStyle: CSSProperties =\n    orientation === \"vertical\"\n      ? { insetBlockEnd: `${percent}%` }\n      : { insetInlineStart: `${percent}%` };\n\n  const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n    if (disabled) {\n      return;\n    }\n    const large = step * 10;\n    // ponytail: the POINTER path has always inverted for RTL\n    // (`rawValueFromPointer` reads `direction === \"rtl\"`), and the thumb has\n    // always positioned with the logical `insetInlineStart`. The keyboard did\n    // neither, so under `dir=\"rtl\"` ArrowRight raised the value while moving\n    // the thumb visibly LEFT — drag and arrows disagreed inside one control.\n    // Gated on horizontal, matching the radio/toggle-group idiom: on a\n    // vertical slider the inline direction says nothing about which way is\n    // \"more\". Up/Down, PageUp/PageDown and Home/End are logical, not\n    // directional, so RTL leaves all of them alone.\n    const rtl =\n      orientation === \"horizontal\" &&\n      getComputedStyle(event.currentTarget).direction === \"rtl\";\n    const increaseKey = rtl ? \"ArrowLeft\" : \"ArrowRight\";\n    const decreaseKey = rtl ? \"ArrowRight\" : \"ArrowLeft\";\n    let next: number | null = null;\n    if (event.key === increaseKey || event.key === \"ArrowUp\") {\n      event.preventDefault();\n      setValue(value + step);\n      return;\n    }\n    if (event.key === decreaseKey || event.key === \"ArrowDown\") {\n      event.preventDefault();\n      setValue(value - step);\n      return;\n    }\n    switch (event.key) {\n      case \"PageUp\":\n        next = value + large;\n        break;\n      case \"PageDown\":\n        next = value - large;\n        break;\n      case \"Home\":\n        next = min;\n        break;\n      case \"End\":\n        next = max;\n        break;\n      default:\n        return;\n    }\n    event.preventDefault();\n    setValue(next);\n  };\n\n  const onPointerDown = (event: ReactPointerEvent<HTMLButtonElement>) => {\n    if (disabled || event.button !== 0) {\n      return;\n    }\n    event.currentTarget.setPointerCapture(event.pointerId);\n    // The thumb is a <button>: Safari does not focus one on mousedown, so the\n    // APG \"focus is placed on the slider thumb\" rule has to be explicit.\n    event.currentTarget.focus({ preventScroll: true });\n    const track = findSliderPart(event.currentTarget, \"slider-track\");\n    // ponytail: grabbing the thumb records an OFFSET; it must not re-derive the\n    // value from the pointer. The thumb is a whole space unit wide and centred\n    // on its value, so the old unconditional `setFromPointer` here snapped the\n    // value by up to half a thumb width — on a 320px 0..100 track, a silent\n    // ~2.5-point jump, and an `onValueChange` the user never asked for, before\n    // the drag even started. Radix explicitly skips repositioning when the\n    // press target is a thumb; Base UI drives a native range input, which no UA\n    // jumps. Rejected clamping the offset to half a step: that still moves the\n    // thumb on grab, just less visibly.\n    grabOffsetRef.current = track\n      ? rawValueAtPointer(event.clientX, event.clientY, track) - value\n      : 0;\n  };\n\n  const onPointerMove = (event: ReactPointerEvent<HTMLButtonElement>) => {\n    if (disabled || !event.currentTarget.hasPointerCapture(event.pointerId)) {\n      return;\n    }\n    const track = findSliderPart(event.currentTarget, \"slider-track\");\n    if (track) {\n      setValue(\n        rawValueAtPointer(event.clientX, event.clientY, track) -\n          grabOffsetRef.current\n      );\n    }\n  };\n\n  return renderHeidiElement({\n    className: SLIDER_CLASSES.thumb,\n    dataPart: \"slider-thumb\",\n    element: \"button\",\n    props: {\n      ...nativeProps,\n      \"aria-label\": label,\n      \"aria-orientation\": orientation,\n      \"aria-valuemax\": max,\n      \"aria-valuemin\": min,\n      \"aria-valuenow\": value,\n      children,\n      \"data-orientation\": orientation,\n      onKeyDown: composeHeidiEventHandlers(onKeyDownProp, onKeyDown),\n      onKeyUp: composeHeidiEventHandlers(onKeyUpProp, commitValue),\n      onLostPointerCapture: composeHeidiEventHandlers(\n        onLostPointerCaptureProp,\n        commitValue\n      ),\n      onPointerDown: composeHeidiEventHandlers(\n        onPointerDownProp,\n        onPointerDown\n      ),\n      onPointerMove: composeHeidiEventHandlers(\n        onPointerMoveProp,\n        onPointerMove\n      ),\n      ref: mergeHeidiRefs(thumbRef, ref),\n      role: \"slider\",\n      type: \"button\",\n      ...nativeDisabledAttrs(disabled)\n    },\n    renderProps: { className, render, style },\n    state: { orientation },\n    structuralStyle\n  });\n}\n\nexport const Slider = {\n  Range: SliderRange,\n  Root: SliderRoot,\n  Thumb: SliderThumb,\n  Track: SliderTrack\n} as const;\n",
      "path": "packages/heidi-ui/src/slider/slider.tsx",
      "target": "components/ui/heidi/slider/slider.tsx",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui slider — STRUCTURAL CSS only. No --hui-*.\n */\n\n@layer heidi-ui-base {\n  /*\n   * ponytail: the app CSS transform rewrites :dir(rtl) into language\n   * selectors, which does not match an explicit dir=\"rtl\". This inherited\n   * custom property follows the nearest authored direction boundary without\n   * incorrectly treating language as direction.\n  */\n  :where([dir=\"ltr\"]) {\n    --_hui-slider-thumb-inline-translate: -50%;\n  }\n\n  :where([dir=\"rtl\"]) {\n    --_hui-slider-thumb-inline-translate: 50%;\n  }\n\n  .hui-slider-root[data-orientation=\"horizontal\"] {\n    align-items: center;\n    display: flex;\n    position: relative;\n    touch-action: none;\n    inline-size: 100%;\n  }\n\n  .hui-slider-root[data-orientation=\"vertical\"] {\n    align-items: center;\n    display: flex;\n    flex-direction: column;\n    position: relative;\n    touch-action: none;\n    block-size: 8rem;\n  }\n\n  .hui-slider-track[data-orientation=\"horizontal\"] {\n    display: block;\n    flex-grow: 1;\n    position: relative;\n    block-size: 0.25rem;\n  }\n\n  .hui-slider-track[data-orientation=\"vertical\"] {\n    display: block;\n    flex-grow: 1;\n    position: relative;\n    inline-size: 0.25rem;\n  }\n\n  .hui-slider-range[data-orientation=\"horizontal\"] {\n    block-size: 100%;\n    inline-size: 0;\n    position: absolute;\n    inset-block-start: 0;\n    inset-inline-start: 0;\n  }\n\n  .hui-slider-range[data-orientation=\"vertical\"] {\n    block-size: 0;\n    inline-size: 100%;\n    position: absolute;\n    inset-block-end: 0;\n    inset-inline-start: 0;\n  }\n\n  .hui-slider-thumb {\n    box-sizing: border-box;\n    margin: 0;\n    min-block-size: 0;\n    min-inline-size: 0;\n    padding: 0;\n    position: absolute;\n    touch-action: none;\n  }\n\n  /* Preserve the compact 16px thumb while exposing a 24px pointer target. */\n  .hui-slider-thumb::after {\n    content: \"\";\n    inset: -0.25rem;\n    position: absolute;\n  }\n\n  .hui-slider-thumb[data-orientation=\"horizontal\"] {\n    inset-block-start: 50%;\n    transform: translate(\n      var(--_hui-slider-thumb-inline-translate, -50%),\n      -50%\n    );\n  }\n\n  .hui-slider-thumb[data-orientation=\"vertical\"] {\n    inset-inline-start: 50%;\n    transform: translate(\n      var(--_hui-slider-thumb-inline-translate, -50%),\n      50%\n    );\n  }\n}\n",
      "path": "packages/heidi-ui/src/slider/slider.base.css",
      "target": "components/ui/heidi/slider/slider.base.css",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui slider — VISUAL theme (opt-in). Consumes --hui-*.\n */\n\n@layer heidi-ui {\n  .hui-slider-track {\n    background: var(--hui-color-border-default);\n    border-radius: var(--hui-radius-md);\n  }\n\n  .hui-slider-range {\n    background: var(--hui-color-brand-primary);\n    border-radius: inherit;\n  }\n\n  .hui-slider-thumb {\n    background: var(--hui-color-bg-elevated);\n    border: var(--hui-border-width) var(--hui-border-style) var(--hui-color-brand-primary);\n    border-radius: var(--hui-radius-md);\n    block-size: var(--hui-space-4);\n    cursor: grab;\n    inline-size: var(--hui-space-4);\n  }\n\n  .hui-slider-thumb: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-slider-thumb:active {\n    cursor: grabbing;\n  }\n\n  .hui-slider-root[data-disabled=\"true\"] {\n    opacity: 0.5;\n  }\n\n  .hui-slider-track[data-disabled=\"true\"] {\n    cursor: not-allowed;\n  }\n\n  .hui-slider-thumb:disabled,\n  .hui-slider-thumb[data-disabled=\"true\"] {\n    cursor: not-allowed;\n  }\n\n  @media (forced-colors: active) {\n    .hui-slider-track {\n      background: Canvas;\n      border: var(--hui-border-width) solid CanvasText;\n      box-sizing: border-box;\n      forced-color-adjust: none;\n    }\n\n    .hui-slider-range {\n      background: Highlight;\n    }\n\n    .hui-slider-thumb {\n      background: Canvas;\n      border-color: CanvasText;\n      forced-color-adjust: none;\n    }\n\n    .hui-slider-thumb:focus-visible {\n      outline-color: Highlight;\n    }\n\n    .hui-slider-root[data-disabled=\"true\"] {\n      opacity: 1;\n    }\n\n    .hui-slider-root[data-disabled=\"true\"] .hui-slider-range,\n    .hui-slider-thumb:disabled,\n    .hui-slider-thumb[data-disabled=\"true\"] {\n      border-color: GrayText;\n    }\n\n    .hui-slider-root[data-disabled=\"true\"] .hui-slider-range {\n      background: GrayText;\n    }\n  }\n}\n",
      "path": "packages/heidi-ui/src/slider/slider.theme.css",
      "target": "components/ui/heidi/slider/slider.theme.css",
      "type": "registry:ui"
    },
    {
      "content": "/*\n * heidi-ui slider — aggregator.\n */\n\n@import \"./slider.base.css\";\n@import \"../../../../styles/heidi-ui-adapter.css\";\n@import \"./slider.theme.css\";\n",
      "path": "packages/heidi-ui/src/slider/slider.css",
      "target": "components/ui/heidi/slider/slider.css",
      "type": "registry:ui"
    },
    {
      "content": "/** GENERATED from packages/heidi-ui/src/slider/slider.base.css + packages/heidi-ui/src/slider/slider.theme.css — do not edit by hand.\n * Rebuild: `bun run hui:build`. Freshness: `bun run contracts:check`. */\nexport const SLIDER_CLASSES = {\n  range: \"hui-slider-range\",\n  root: \"hui-slider-root\",\n  thumb: \"hui-slider-thumb\",\n  track: \"hui-slider-track\",\n} as const;\n",
      "path": "packages/heidi-ui/src/slider/slider.classes.generated.ts",
      "target": "components/ui/heidi/slider/slider.classes.generated.ts",
      "type": "registry:ui"
    },
    {
      "content": "/** GENERATED from slider.anatomy.json + slider.base.css + slider.theme.css — do not edit by hand.\n * Rebuild: `bun run hui:build`. Freshness: `bun run contracts:check`. */\n\nexport type SliderDisabled = \"true\";\nexport type SliderOrientation = \"horizontal\" | \"vertical\";\n\nexport const SLIDER_ANATOMY = {\n  \"component\": \"slider\",\n  \"description\": \"APG slider (single thumb). Thumb is role=slider with aria-valuemin/max/now + aria-orientation. Arrow/Home/End/PageUp/PageDown adjust value by step. Controlled via value/defaultValue + onValueChange, with onValueCommitted fired once per interaction. Requires label. Track click focuses the thumb and jumps the value; grabbing the thumb keeps its grab offset instead of re-anchoring. A name enables form submission and native reset. Composition via renderHeidiElement.\",\n  \"schemaVersion\": 2,\n  \"parts\": {\n    \"range\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-slider-range\",\n      \"css\": {\n        \"structural\": [\n          \"block-size\",\n          \"inline-size\",\n          \"position\"\n        ]\n      },\n      \"dataPart\": \"slider-range\",\n      \"description\": \"Filled portion of the track; size set structurally from value/min/max.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    },\n    \"root\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-slider-root\",\n      \"css\": {\n        \"structural\": [\n          \"align-items\",\n          \"display\",\n          \"position\",\n          \"touch-action\"\n        ]\n      },\n      \"dataPart\": \"slider-root\",\n      \"description\": \"Slider container; owns value context, commit batching, and track pointer hit-testing. With a name it also renders the hidden form-associated input. Orientation via data-orientation.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"defaultValue\",\n        \"disabled\",\n        \"form\",\n        \"label\",\n        \"max\",\n        \"min\",\n        \"name\",\n        \"onValueChange\",\n        \"onValueCommitted\",\n        \"orientation\",\n        \"ref\",\n        \"render\",\n        \"step\",\n        \"style\",\n        \"value\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    },\n    \"thumb\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-label\",\n          \"aria-orientation\",\n          \"aria-valuemax\",\n          \"aria-valuemin\",\n          \"aria-valuenow\"\n        ],\n        \"role\": \"slider\"\n      },\n      \"class\": \"hui-slider-thumb\",\n      \"css\": {\n        \"structural\": [\n          \"position\",\n          \"touch-action\"\n        ]\n      },\n      \"dataPart\": \"slider-thumb\",\n      \"description\": \"role=slider thumb; keyboard + offset-preserving pointer drag. Owns the accessible name and value attrs, and takes focus when the track is clicked.\",\n      \"element\": \"button\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"onKeyDown\",\n        \"onKeyUp\",\n        \"onLostPointerCapture\",\n        \"onPointerDown\",\n        \"onPointerMove\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [\n        \":disabled\",\n        \":focus-visible\"\n      ],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    },\n    \"track\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-slider-track\",\n      \"css\": {\n        \"structural\": [\n          \"display\",\n          \"flex-grow\",\n          \"position\"\n        ]\n      },\n      \"dataPart\": \"slider-track\",\n      \"description\": \"Track rail; pointer down focuses the thumb and sets the value. Holds Range.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"onLostPointerCapture\",\n        \"onPointerDown\",\n        \"onPointerMove\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    }\n  },\n  \"rootProps\": [\n    \"defaultValue\",\n    \"disabled\",\n    \"form\",\n    \"label\",\n    \"max\",\n    \"min\",\n    \"name\",\n    \"onValueChange\",\n    \"onValueCommitted\",\n    \"orientation\",\n    \"step\",\n    \"value\"\n  ],\n  \"tokens\": [\n    \"--hui-border-style\",\n    \"--hui-border-width\",\n    \"--hui-color-bg-elevated\",\n    \"--hui-color-border-default\",\n    \"--hui-color-brand-primary\",\n    \"--hui-color-focus-ring\",\n    \"--hui-focus-ring-offset\",\n    \"--hui-focus-ring-width\",\n    \"--hui-radius-md\",\n    \"--hui-space-4\"\n  ]\n} as const;\n",
      "path": "packages/heidi-ui/src/slider/slider.anatomy.generated.ts",
      "target": "components/ui/heidi/slider/slider.anatomy.generated.ts",
      "type": "registry:ui"
    },
    {
      "content": "{\n  \"component\": \"slider\",\n  \"description\": \"APG slider (single thumb). Thumb is role=slider with aria-valuemin/max/now + aria-orientation. Arrow/Home/End/PageUp/PageDown adjust value by step. Controlled via value/defaultValue + onValueChange, with onValueCommitted fired once per interaction. Requires label. Track click focuses the thumb and jumps the value; grabbing the thumb keeps its grab offset instead of re-anchoring. A name enables form submission and native reset. Composition via renderHeidiElement.\",\n  \"schemaVersion\": 2,\n  \"parts\": {\n    \"range\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-slider-range\",\n      \"css\": {\n        \"structural\": [\n          \"block-size\",\n          \"inline-size\",\n          \"position\"\n        ]\n      },\n      \"dataPart\": \"slider-range\",\n      \"description\": \"Filled portion of the track; size set structurally from value/min/max.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    },\n    \"root\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-slider-root\",\n      \"css\": {\n        \"structural\": [\n          \"align-items\",\n          \"display\",\n          \"position\",\n          \"touch-action\"\n        ]\n      },\n      \"dataPart\": \"slider-root\",\n      \"description\": \"Slider container; owns value context, commit batching, and track pointer hit-testing. With a name it also renders the hidden form-associated input. Orientation via data-orientation.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"defaultValue\",\n        \"disabled\",\n        \"form\",\n        \"label\",\n        \"max\",\n        \"min\",\n        \"name\",\n        \"onValueChange\",\n        \"onValueCommitted\",\n        \"orientation\",\n        \"ref\",\n        \"render\",\n        \"step\",\n        \"style\",\n        \"value\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    },\n    \"thumb\": {\n      \"aria\": {\n        \"owns\": [\n          \"aria-label\",\n          \"aria-orientation\",\n          \"aria-valuemax\",\n          \"aria-valuemin\",\n          \"aria-valuenow\"\n        ],\n        \"role\": \"slider\"\n      },\n      \"class\": \"hui-slider-thumb\",\n      \"css\": {\n        \"structural\": [\n          \"position\",\n          \"touch-action\"\n        ]\n      },\n      \"dataPart\": \"slider-thumb\",\n      \"description\": \"role=slider thumb; keyboard + offset-preserving pointer drag. Owns the accessible name and value attrs, and takes focus when the track is clicked.\",\n      \"element\": \"button\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"onKeyDown\",\n        \"onKeyUp\",\n        \"onLostPointerCapture\",\n        \"onPointerDown\",\n        \"onPointerMove\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [\n        \":disabled\",\n        \":focus-visible\"\n      ],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    },\n    \"track\": {\n      \"aria\": {\n        \"owns\": [],\n        \"role\": null\n      },\n      \"class\": \"hui-slider-track\",\n      \"css\": {\n        \"structural\": [\n          \"display\",\n          \"flex-grow\",\n          \"position\"\n        ]\n      },\n      \"dataPart\": \"slider-track\",\n      \"description\": \"Track rail; pointer down focuses the thumb and sets the value. Holds Range.\",\n      \"element\": \"div\",\n      \"nativeProps\": true,\n      \"props\": [\n        \"className\",\n        \"onLostPointerCapture\",\n        \"onPointerDown\",\n        \"onPointerMove\",\n        \"ref\",\n        \"render\",\n        \"style\"\n      ],\n      \"pseudoStates\": [],\n      \"states\": {\n        \"data-disabled\": [\n          \"true\"\n        ],\n        \"data-orientation\": [\n          \"horizontal\",\n          \"vertical\"\n        ]\n      }\n    }\n  },\n  \"rootProps\": [\n    \"defaultValue\",\n    \"disabled\",\n    \"form\",\n    \"label\",\n    \"max\",\n    \"min\",\n    \"name\",\n    \"onValueChange\",\n    \"onValueCommitted\",\n    \"orientation\",\n    \"step\",\n    \"value\"\n  ]\n}\n",
      "path": "packages/heidi-ui/src/slider/slider.anatomy.json",
      "target": "components/ui/heidi/slider/slider.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"
    }
  ],
  "name": "slider",
  "registryDependencies": [
    "@heidi-ui/heidi-tokens"
  ],
  "title": "Slider",
  "type": "registry:ui"
}
