{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider",
  "title": "Slider",
  "description": "A ruler with a hand. The value is drawn as a dimension from zero with your number in italic in the gap, or runs through the label's spread letters, or makes the label as loud as the value.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/slider.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype SliderProps = Omit<React.ComponentProps<\"input\">, \"type\" | \"value\" | \"defaultValue\" | \"onChange\" | \"min\" | \"max\" | \"step\" | \"children\"> & {\n  /** What is being measured. It labels the range; spread and dynamics set it as the picture, so pass a string there. */\n  label: React.ReactNode\n  /**\n   * dimension: the value is a dimension line from zero to the hand, your number in its gap.\n   * spread: the label's letters are spread across the ruler as its marks, and the hand inks them as it passes.\n   * dynamics: the label is as loud as the value (its weight follows the hand), over a scale from pp to ff.\n   */\n  variant?: \"dimension\" | \"spread\" | \"dynamics\"\n  min?: number\n  max?: number\n  step?: number\n  value?: number\n  defaultValue?: number\n  onValueChange?: (value: number) => void\n  /** Set after the number, as written: pass \" kg\" with its space, \"%\" without. */\n  unit?: string\n  /** Classes for the root. */\n  className?: string\n  /** Pins a state for documentation (\"focus\"); set on the root. */\n  \"data-force\"?: string\n}\n\n/** The value a quarter-mark stands for, snapped to a step and free of float dust. */\nconst at = (min: number, max: number, step: number, fraction: number) => {\n  const digits = (String(step).split(\".\")[1] ?? \"\").length\n  return Number((min + Math.round(((max - min) * fraction) / step) * step).toFixed(digits))\n}\n\nconst dynamics = [\"pp\", \"p\", \"mp\", \"mf\", \"f\", \"ff\"]\n\n/**\n * A ruler with a hand. Native range, so arrows step, Page keys jump and Home / End go to the ends.\n * dimension (the default) draws the value as a dimension, after Paul Rand: a line from zero to the hand with your\n * number, in italic, in the gap; when the gap is too short for it, the number stands outside, past the far tick,\n * as a draughtsman sets it. spread and dynamics make the label the picture (see the variant prop).\n */\nfunction Slider({\n  label,\n  variant = \"dimension\",\n  min = 0,\n  max = 100,\n  step = 1,\n  value: controlled,\n  defaultValue,\n  onValueChange,\n  unit = \"\",\n  className,\n  id,\n  \"data-force\": force,\n  ...props\n}: SliderProps) {\n  const generated = React.useId()\n  const inputId = id ?? generated\n  const [uncontrolled, setUncontrolled] = React.useState(defaultValue ?? min)\n  const value = controlled ?? uncontrolled\n  const p = max > min ? (value - min) / (max - min) : 0\n  const word = typeof label === \"string\" && variant !== \"dimension\" ? label : null\n  const shape = word ? variant : \"dimension\"\n  const out = React.useRef<HTMLOutputElement>(null)\n  const figure = React.useRef<HTMLSpanElement>(null)\n\n  // The dimension must stay true to the hand, so a figure that doesn't fit its gap is set outside it.\n  React.useLayoutEffect(() => {\n    const o = out.current\n    const f = figure.current\n    if (shape !== \"dimension\" || !o || !f) return\n    const place = () => o.toggleAttribute(\"data-outside\", f.offsetWidth + parseFloat(getComputedStyle(o).fontSize) * 0.9 > o.clientWidth)\n    place()\n    const watch = new ResizeObserver(place)\n    watch.observe(o)\n    document.fonts?.ready.then(place, () => {})\n    return () => watch.disconnect()\n  }, [shape, value])\n\n  return (\n    <div data-slot=\"slider\" data-variant={shape} data-force={force} className={cn(\"ot-ruler\", className)} style={{ \"--p\": p } as React.CSSProperties}>\n      <label className={word ? \"ot-sr\" : \"ot-label\"} htmlFor={inputId}>\n        {label}\n      </label>\n      {shape === \"dynamics\" ? (\n        <output className=\"ot-ruler-loud\" htmlFor={inputId} style={{ \"--chars\": word?.length } as React.CSSProperties}>\n          <span aria-hidden=\"true\">{word}</span>{\" \"}\n          <span className=\"ot-ruler-yours\">\n            {`${value}${unit}`}\n          </span>\n        </output>\n      ) : shape === \"spread\" ? (\n        <output className=\"ot-ruler-yours ot-ruler-corner\" htmlFor={inputId}>\n          {`${value}${unit}`}\n        </output>\n      ) : (\n        <output ref={out} className=\"ot-ruler-value\" htmlFor={inputId}>\n          <span ref={figure}>\n            {`${value}${unit}`}\n          </span>\n        </output>\n      )}\n      <div className=\"ot-ruler-hand\">\n        {shape === \"spread\" ? (\n          <span className=\"ot-ruler-letters\" aria-hidden=\"true\">\n            {[...(word ?? \"\")].map((l, i) => (\n              <span key={i}>{l === \" \" ? \" \" : l}</span>\n            ))}\n          </span>\n        ) : null}\n        <input\n          type=\"range\"\n          id={inputId}\n          min={min}\n          max={max}\n          step={step}\n          value={value}\n          aria-valuetext={unit ? `${value}${unit}` : undefined}\n          onChange={(e) => {\n            const next = Number(e.target.value)\n            if (controlled === undefined) setUncontrolled(next)\n            onValueChange?.(next)\n          }}\n          {...props}\n        />\n      </div>\n      <div className=\"ot-ruler-scale\" aria-hidden=\"true\">\n        {shape === \"dynamics\"\n          ? dynamics.map((d, i) => (\n              <span key={d} className=\"ot-term\" style={{ \"--n\": i / 5 } as React.CSSProperties} data-on={Math.round(p * 5) === i || undefined}>\n                {d}\n              </span>\n            ))\n          : [0, 0.25, 0.5, 0.75, 1].map((n) => (\n              <span key={n} style={{ \"--n\": n } as React.CSSProperties}>\n                {at(min, max, step, n)}\n              </span>\n            ))}\n      </div>\n    </div>\n  )\n}\n\nexport { Slider, type SliderProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/slider.css",
      "content": "@layer components {\n  /* ── ot-ruler ── slider: a ruler with a hand; the value is yours */\n  .ot-ruler {\n    --p: 0.5;\n    --thumb: 20px;\n    --h: 40px; /* the hand's height; the track is as tall */\n    --ot-ruler-bg:\n      linear-gradient(var(--ot-ink), var(--ot-ink)) calc(var(--thumb) / 2) 100% / calc((100% - var(--thumb)) * var(--p)) var(--ot-stroke) no-repeat,\n      linear-gradient(var(--ot-pencil), var(--ot-pencil)) calc(100% - var(--thumb) / 2) 100% / 1px 16px no-repeat,\n      repeating-linear-gradient(90deg, var(--ot-pencil) 0 1px, transparent 1px 25%) calc(var(--thumb) / 2) 100% / calc(100% - var(--thumb)) 16px no-repeat,\n      repeating-linear-gradient(90deg, var(--ot-rule-strong) 0 1px, transparent 1px 5%) calc(var(--thumb) / 2) 100% / calc(100% - var(--thumb)) 8px no-repeat,\n      linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) calc(var(--thumb) / 2) 100% / calc(100% - var(--thumb)) 1px no-repeat;\n    position: relative; display: block;\n  }\n  /* The value is a dimension, as Paul Rand drew them: a measured line from zero\n     to the hand, ticked at both ends, with your number in the gap. */\n  .ot-ruler-value {\n    display: flex; align-items: center; gap: 0.3em; position: relative;\n    left: calc(var(--thumb) / 2); width: calc((100% - var(--thumb)) * var(--p));\n    margin-top: var(--ot-space-3);\n    font-family: var(--ot-expression); font-style: italic; font-size: var(--ot-f); line-height: 1.05;\n    color: var(--ot-ink); font-variant-numeric: lining-nums;\n    background:\n      linear-gradient(var(--ot-ink), var(--ot-ink)) 0 55% / var(--ot-hairline) 12px no-repeat,\n      linear-gradient(var(--ot-ink), var(--ot-ink)) 100% 55% / var(--ot-hairline) 12px no-repeat;\n    transition: translate var(--ot-andante) var(--ot-spiccato);\n  }\n  .ot-ruler-value > span { white-space: nowrap; direction: ltr; unicode-bidi: isolate; }\n  /* Too short a gap for the figure: the draughtsman's rule. The line stays true between its ticks and the figure\n     stands outside, past the far one. */\n  .ot-ruler-value[data-outside] { gap: 0; }\n  .ot-ruler-value[data-outside] > span { position: absolute; inset-inline-start: calc(100% + 0.28em); }\n  .ot-ruler-value[data-outside]::after { display: none; }\n  .ot-ruler:has(input:active:not(:disabled)) :is(.ot-ruler-value, .ot-ruler-corner, .ot-ruler-loud > .ot-ruler-yours) { translate: 0 -5px; transition-duration: var(--ot-moderato); }\n  .ot-ruler-value::before, .ot-ruler-value::after { content: \"\"; flex: 1; height: var(--ot-hairline); margin-top: 0.1em; background: var(--ot-ink); }\n  .ot-ruler-hand { position: relative; }\n  .ot-ruler input[type=\"range\"] {\n    -webkit-appearance: none; appearance: none; display: block; position: relative;\n    width: 100%; height: var(--h); margin: 0; background: transparent; cursor: pointer;\n  }\n  .ot-ruler input[type=\"range\"]::-webkit-slider-runnable-track { height: var(--h); background: var(--ot-ruler-bg); }\n  .ot-ruler input[type=\"range\"]::-moz-range-track { height: var(--h); background: var(--ot-ruler-bg); }\n  .ot-ruler input[type=\"range\"]::-moz-range-progress { background: transparent; }\n  .ot-ruler input[type=\"range\"]::-webkit-slider-thumb {\n    -webkit-appearance: none; appearance: none; width: var(--thumb); height: var(--h); border: 0; border-radius: 0;\n    background:\n      radial-gradient(circle at 50% 5px, var(--ot-accent) 3.6px, transparent 4.2px),\n      linear-gradient(var(--ot-ink), var(--ot-ink)) 50% 100% / var(--ot-stroke) calc(var(--h) - 10px) no-repeat;\n  }\n  .ot-ruler input[type=\"range\"]::-moz-range-thumb {\n    width: var(--thumb); height: var(--h); border: 0; border-radius: 0;\n    background:\n      radial-gradient(circle at 50% 5px, var(--ot-accent) 3.6px, transparent 4.2px),\n      linear-gradient(var(--ot-ink), var(--ot-ink)) 50% 100% / var(--ot-stroke) calc(var(--h) - 10px) no-repeat;\n  }\n  .ot-ruler input[type=\"range\"]:focus-visible { outline-offset: 8px; }\n  .ot-ruler[data-force~=\"focus\"] { outline: 0; }\n  .ot-ruler[data-force~=\"focus\"] input { outline: var(--ot-stroke) solid var(--ot-accent); outline-offset: 8px; }\n  .ot-ruler-scale { position: relative; height: 1.6em; margin-top: var(--ot-space-2); font-size: var(--ot-pp); color: var(--ot-pencil); font-variant-numeric: tabular-nums; }\n  .ot-ruler-scale > span { position: absolute; top: 0; left: calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--n)); translate: -50% 0; }\n  .ot-ruler:has(input:disabled) { opacity: 0.45; }\n  .ot-ruler:has(input:disabled) input { cursor: not-allowed; }\n  .ot-ruler-yours { direction: ltr; unicode-bidi: isolate; font-family: var(--ot-expression); font-style: italic; font-size: calc(var(--ot-expression-scale) * var(--ot-mf)); line-height: 1; color: var(--ot-ink); font-variant-numeric: lining-nums tabular-nums; transition: translate var(--ot-andante) var(--ot-spiccato); }\n\n  /* ── spread ── after \"Renaissance.\", whose words are spread to the edges: the label's letters are the ruler's marks,\n     set out across its whole length. The hand stands through them and inks every letter it has passed, splitting the\n     one it stands in, as \"the uncreative\" splits its word. Your number waits in the far corner. */\n  .ot-ruler[data-variant=\"spread\"] {\n    --h: 64px;\n    --ot-ruler-bg:\n      linear-gradient(var(--ot-ink), var(--ot-ink)) calc(var(--thumb) / 2) 100% / calc((100% - var(--thumb)) * var(--p)) var(--ot-stroke) no-repeat,\n      linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) calc(var(--thumb) / 2) 100% / calc(100% - var(--thumb)) 1px no-repeat;\n  }\n  .ot-ruler-corner { display: block; text-align: end; }\n  .ot-ruler-letters {\n    position: absolute; inset-inline: calc(var(--thumb) / 2); bottom: 8px; display: flex; justify-content: space-between;\n    font-size: var(--ot-mf); line-height: 1; font-weight: 500; text-transform: uppercase; letter-spacing: 0;\n    color: transparent; -webkit-background-clip: text; background-clip: text;\n    background-image: linear-gradient(90deg, var(--ot-ink) calc(var(--p) * 100%), var(--ot-pencil) 0);\n    pointer-events: none;\n  }\n  .ot-ruler[data-variant=\"spread\"] .ot-ruler-scale > span:not(:first-child, :last-child) { display: none; }\n\n  /* ── dynamics ── the score marks loudness pp to ff in italic under the notes. Here the label is as loud as the value:\n     its weight and width follow the hand, from a whisper at zero to its heaviest at the top, and the nearest marking inks. */\n  .ot-ruler[data-variant=\"dynamics\"] { container-type: inline-size; }\n  .ot-ruler-loud { display: flex; align-items: baseline; gap: var(--ot-space-3); margin-bottom: var(--ot-space-2); white-space: nowrap; }\n  /* Sized from its letter count (--chars) so that at its heaviest it still fits one line: nothing reflows while you drag. */\n  .ot-ruler-loud > :first-child {\n    font-size: max(var(--ot-p), min(var(--ot-f), calc((100cqi - 72px) / (var(--chars) * 0.76)))); line-height: 1.05; letter-spacing: var(--ot-f-tr); color: var(--ot-ink);\n    font-weight: calc(100 + 800 * var(--p)); font-stretch: calc(62% + 63% * var(--p));\n    transition: font-weight var(--ot-allegro) var(--ot-breath), font-stretch var(--ot-allegro) var(--ot-breath);\n  }\n  .ot-ruler-loud > .ot-ruler-yours { color: var(--ot-graphite); }\n  .ot-ruler[data-variant=\"dynamics\"] .ot-ruler-scale { font-size: var(--ot-p); height: 1.4em; }\n  .ot-ruler[data-variant=\"dynamics\"] .ot-ruler-scale > span { font-variant-numeric: normal; transition: color var(--ot-allegro); }\n  .ot-ruler[data-variant=\"dynamics\"] .ot-ruler-scale > [data-on] { color: var(--ot-ink); }\n\n  /* Right to left the ruler turns with the text: zero is on the right and the hand fills leftward. */\n  .ot-ruler:is([dir=\"rtl\"], [dir=\"rtl\"] *) {\n    --ot-ruler-bg:\n      linear-gradient(var(--ot-ink), var(--ot-ink)) right calc(var(--thumb) / 2) bottom 0 / calc((100% - var(--thumb)) * var(--p)) var(--ot-stroke) no-repeat,\n      linear-gradient(var(--ot-pencil), var(--ot-pencil)) left calc(var(--thumb) / 2) bottom 0 / 1px 16px no-repeat,\n      repeating-linear-gradient(270deg, var(--ot-pencil) 0 1px, transparent 1px 25%) right calc(var(--thumb) / 2) bottom 0 / calc(100% - var(--thumb)) 16px no-repeat,\n      repeating-linear-gradient(270deg, var(--ot-rule-strong) 0 1px, transparent 1px 5%) right calc(var(--thumb) / 2) bottom 0 / calc(100% - var(--thumb)) 8px no-repeat,\n      linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) right calc(var(--thumb) / 2) bottom 0 / calc(100% - var(--thumb)) 1px no-repeat;\n  }\n  .ot-ruler[data-variant=\"spread\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) {\n    --ot-ruler-bg:\n      linear-gradient(var(--ot-ink), var(--ot-ink)) right calc(var(--thumb) / 2) bottom 0 / calc((100% - var(--thumb)) * var(--p)) var(--ot-stroke) no-repeat,\n      linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) right calc(var(--thumb) / 2) bottom 0 / calc(100% - var(--thumb)) 1px no-repeat;\n  }\n  .ot-ruler:is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-ruler-letters { background-image: linear-gradient(270deg, var(--ot-ink) calc(var(--p) * 100%), var(--ot-pencil) 0); }\n  .ot-ruler:is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-ruler-value { left: auto; right: calc(var(--thumb) / 2); }\n  .ot-ruler:is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-ruler-value[data-outside] > span { inset-inline-start: auto; inset-inline-end: calc(100% + 0.28em); }\n  .ot-ruler:is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-ruler-corner { text-align: start; }\n  .ot-ruler:is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-ruler-scale > span { left: auto; right: calc(var(--thumb) / 2 + (100% - var(--thumb)) * var(--n)); translate: 50% 0; }\n\n  /* Forced colours: the ruler restates its tokens in system colours, so every drawn tick and the hand survive. */\n  @media (forced-colors: active) {\n    .ot-ruler { forced-color-adjust: none; color: CanvasText; --ot-ink: CanvasText; --ot-graphite: CanvasText; --ot-pencil: GrayText; --ot-rule-strong: GrayText; --ot-accent: Highlight; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/slider.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/slider.css\"": {}
  },
  "meta": {
    "movement": "VI",
    "underneath": "hook",
    "contract": "ot-ruler"
  },
  "type": "registry:ui"
}
