{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider-marks",
  "title": "Marks slider",
  "description": "Styled slider: the Marks set. 5 decorative slider variations (StepsSlider, LabelsSlider, SnapSlider, ScaleSlider, SegmentsSlider) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/slider-marks.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Slider marks family: 5 sliders with scale marks. A self-sufficient structure: a\n   track div + a fill div + a marks layer + a thumb button. Pointer dragging (clientX\n   within the track rect, with capture on the track) AND keyboard (arrows, Shift+arrow\n   for a large step, Home/End). The snap behavior runs off step; the marks are purely\n   visual and never swallow pointer events. Color comes ONLY from tokens. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nconst trackHeight: Record<StyledSize, string> = {\n  sm: \"h-1\",\n  md: \"h-1.5\",\n  lg: \"h-2.5\",\n  xl: \"h-3.5\",\n}\nconst thumbSize: Record<StyledSize, string> = {\n  sm: \"size-3.5\",\n  md: \"size-4\",\n  lg: \"size-5\",\n  xl: \"size-6\",\n}\nconst labelText: Record<StyledSize, string> = {\n  sm: \"text-[10px]\",\n  md: \"text-[11px]\",\n  lg: \"text-xs\",\n  xl: \"text-sm\",\n}\n\nconst rootBase =\n  \"relative flex w-56 max-w-full touch-none select-none items-center outline-none\"\n/* Basparmak 24px altinda: gorunmez dokunma alani genisletmesi zorunlu. */\nconst thumbBase =\n  \"absolute top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-background bg-primary shadow-sm outline-none after:absolute after:-inset-1.5 after:content-[''] focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n\nexport type MarksSliderProps = {\n  className?: string\n  size?: StyledSize\n  min?: number\n  max?: number\n  step?: number\n  value?: number\n  defaultValue?: number\n  onValueChange?: (v: number) => void\n}\n\nfunction clamp(v: number, min: number, max: number) {\n  return Math.max(min, Math.min(max, v))\n}\n\n/* Shared horizontal slider behaviour: controlled/uncontrolled value, pointer,\n   keyboard. */\nfunction useSlider(props: MarksSliderProps, fallbackStep = 1) {\n  const {\n    value,\n    defaultValue = 40,\n    onValueChange,\n    min = 0,\n    max = 100,\n    step = fallbackStep,\n  } = props\n  const trackRef = React.useRef<HTMLDivElement>(null)\n  const [internal, setInternal] = React.useState(() => clamp(defaultValue, min, max))\n  const [dragging, setDragging] = React.useState(false)\n  const current = value !== undefined ? clamp(value, min, max) : internal\n\n  const commit = React.useCallback(\n    (next: number) => {\n      const c = clamp(Math.round(next / step) * step, min, max)\n      if (value === undefined) setInternal(c)\n      onValueChange?.(c)\n    },\n    [max, min, onValueChange, step, value]\n  )\n\n  const fromClientX = React.useCallback(\n    (clientX: number) => {\n      const el = trackRef.current\n      if (!el) return current\n      const rect = el.getBoundingClientRect()\n      const ratio = rect.width > 0 ? (clientX - rect.left) / rect.width : 0\n      return min + clamp(ratio, 0, 1) * (max - min)\n    },\n    [current, max, min]\n  )\n\n  const onPointerDown = React.useCallback(\n    (e: React.PointerEvent) => {\n      e.preventDefault()\n      trackRef.current?.setPointerCapture?.(e.pointerId)\n      setDragging(true)\n      commit(fromClientX(e.clientX))\n    },\n    [commit, fromClientX]\n  )\n\n  const onThumbPointerDown = React.useCallback((e: React.PointerEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    trackRef.current?.setPointerCapture?.(e.pointerId)\n    setDragging(true)\n  }, [])\n\n  const onPointerMove = React.useCallback(\n    (e: React.PointerEvent) => {\n      if (!dragging) return\n      commit(fromClientX(e.clientX))\n    },\n    [commit, dragging, fromClientX]\n  )\n\n  const onPointerUp = React.useCallback((e: React.PointerEvent) => {\n    trackRef.current?.releasePointerCapture?.(e.pointerId)\n    setDragging(false)\n  }, [])\n\n  const onKeyDown = React.useCallback(\n    (e: React.KeyboardEvent) => {\n      const delta = e.shiftKey ? step * 10 : step\n      let next = current\n      if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") next = current - delta\n      else if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") next = current + delta\n      else if (e.key === \"Home\") next = min\n      else if (e.key === \"End\") next = max\n      else return\n      e.preventDefault()\n      commit(next)\n    },\n    [commit, current, max, min, step]\n  )\n\n  const pct = max > min ? ((current - min) / (max - min)) * 100 : 0\n\n  return {\n    trackRef,\n    current,\n    pct,\n    dragging,\n    min,\n    max,\n    onPointerDown,\n    onThumbPointerDown,\n    onPointerMove,\n    onPointerUp,\n    onKeyDown,\n  }\n}\n\ntype MarksState = ReturnType<typeof useSlider>\n\n/* Ortak thumb: rol, aria, klavye ve yay tek yerde. */\nfunction MarksThumb({\n  s,\n  size,\n  reduce,\n  className,\n}: {\n  s: MarksState\n  size: StyledSize\n  reduce: boolean | null\n  className?: string\n}) {\n  return (\n    <motion.button\n      type=\"button\"\n      data-slot=\"styled-slider-thumb\"\n      role=\"slider\"\n      aria-label=\"Value\"\n      aria-valuenow={Math.round(s.current)}\n      aria-valuemin={s.min}\n      aria-valuemax={s.max}\n      aria-orientation=\"horizontal\"\n      tabIndex={0}\n      onPointerDown={s.onThumbPointerDown}\n      onKeyDown={s.onKeyDown}\n      className={cn(thumbBase, thumbSize[size], className)}\n      style={{ left: `${s.pct}%` }}\n      animate={{ scale: !reduce && s.dragging ? 1.18 : 1 }}\n      transition={\n        reduce\n          ? { duration: 0 }\n          : { type: \"spring\" as const, stiffness: 500, damping: 30, mass: 0.6 }\n      }\n    />\n  )\n}\n\n/* Steps: short, evenly spaced step lines along the track. */\nexport function StepsSlider({ className, size = \"md\", ...rest }: MarksSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useSlider(rest)\n  const ticks = 11\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, className)}>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\n          \"relative w-full cursor-pointer rounded-full bg-secondary\",\n          trackHeight[size]\n        )}\n      >\n        <div\n          className=\"absolute inset-y-0 left-0 rounded-full bg-primary\"\n          style={{ width: `${s.pct}%` }}\n        />\n        <div className=\"pointer-events-none absolute inset-0\" aria-hidden=\"true\">\n          {Array.from({ length: ticks }).map((_, i) => (\n            <span\n              key={i}\n              className=\"absolute top-1/2 h-1.5 w-px -translate-x-1/2 -translate-y-1/2 rounded-full bg-border\"\n              style={{ left: `${(i / (ticks - 1)) * 100}%` }}\n            />\n          ))}\n        </div>\n        <MarksThumb s={s} size={size} reduce={reduce} />\n      </div>\n    </div>\n  )\n}\n\n/* Labels: adim cizgileri + altlarinda sayisal etiketler. */\nexport function LabelsSlider({ className, size = \"md\", ...rest }: MarksSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useSlider(rest)\n  const marks = 5\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, \"mb-5\", className)}>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\n          \"relative w-full cursor-pointer rounded-full bg-secondary\",\n          trackHeight[size]\n        )}\n      >\n        <div\n          className=\"absolute inset-y-0 left-0 rounded-full bg-primary\"\n          style={{ width: `${s.pct}%` }}\n        />\n        <div className=\"pointer-events-none absolute inset-0\" aria-hidden=\"true\">\n          {Array.from({ length: marks }).map((_, i) => {\n            const ratio = i / (marks - 1)\n            return (\n              <React.Fragment key={i}>\n                <span\n                  className=\"absolute top-1/2 h-2 w-px -translate-x-1/2 -translate-y-1/2 rounded-full bg-border\"\n                  style={{ left: `${ratio * 100}%` }}\n                />\n                <span\n                  className={cn(\n                    \"absolute top-full mt-2 -translate-x-1/2 tabular-nums text-muted-foreground\",\n                    labelText[size]\n                  )}\n                  style={{ left: `${ratio * 100}%` }}\n                >\n                  {Math.round(s.min + ratio * (s.max - s.min))}\n                </span>\n              </React.Fragment>\n            )\n          })}\n        </div>\n        <MarksThumb s={s} size={size} reduce={reduce} />\n      </div>\n    </div>\n  )\n}\n\n/* Snap: 20'lik adima kenetlenir, isaret noktalari doldukca vurgulanir. */\nexport function SnapSlider({ className, size = \"md\", ...rest }: MarksSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useSlider(rest, 20)\n  const stops = 6\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, className)}>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\n          \"relative w-full cursor-pointer rounded-full bg-secondary\",\n          trackHeight[size]\n        )}\n      >\n        <div\n          className=\"absolute inset-y-0 left-0 rounded-full bg-primary\"\n          style={{ width: `${s.pct}%` }}\n        />\n        <div className=\"pointer-events-none absolute inset-0\" aria-hidden=\"true\">\n          {Array.from({ length: stops }).map((_, i) => {\n            const ratio = i / (stops - 1)\n            const passed = ratio * 100 <= s.pct\n            return (\n              <span\n                key={i}\n                className={cn(\n                  \"absolute top-1/2 size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full\",\n                  passed ? \"bg-primary-foreground\" : \"bg-border\"\n                )}\n                style={{ left: `${ratio * 100}%` }}\n              />\n            )\n          })}\n        </div>\n        <MarksThumb s={s} size={size} reduce={reduce} />\n      </div>\n    </div>\n  )\n}\n\n/* Scale: cetvel gorunumu, izin altinda uzun/kisa cizgiler. */\nexport function ScaleSlider({ className, size = \"md\", ...rest }: MarksSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useSlider(rest)\n  const ticks = 21\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, \"mb-4\", className)}>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\n          \"relative w-full cursor-pointer rounded-full bg-secondary\",\n          trackHeight[size]\n        )}\n      >\n        <div\n          className=\"absolute inset-y-0 left-0 rounded-full bg-info\"\n          style={{ width: `${s.pct}%` }}\n        />\n        <div\n          className=\"pointer-events-none absolute inset-x-0 top-full mt-1.5\"\n          aria-hidden=\"true\"\n        >\n          {Array.from({ length: ticks }).map((_, i) => {\n            const major = i % 5 === 0\n            return (\n              <span\n                key={i}\n                className={cn(\n                  \"absolute top-0 w-px -translate-x-1/2 rounded-full\",\n                  major ? \"h-2.5 bg-muted-foreground\" : \"h-1.5 bg-border\"\n                )}\n                style={{ left: `${(i / (ticks - 1)) * 100}%` }}\n              />\n            )\n          })}\n        </div>\n        <MarksThumb s={s} size={size} reduce={reduce} className=\"bg-info\" />\n      </div>\n    </div>\n  )\n}\n\n/* Segments: iz aralikli parcalara bolunur, gecilen parcalar dolar. */\nexport function SegmentsSlider({ className, size = \"md\", ...rest }: MarksSliderProps) {\n  const reduce = useReducedMotion()\n  const s = useSlider(rest)\n  const segments = 5\n  return (\n    <div data-slot=\"styled-slider\" className={cn(rootBase, className)}>\n      <div\n        ref={s.trackRef}\n        onPointerDown={s.onPointerDown}\n        onPointerMove={s.onPointerMove}\n        onPointerUp={s.onPointerUp}\n        className={cn(\"relative flex w-full cursor-pointer gap-1\", trackHeight[size])}\n      >\n        {Array.from({ length: segments }).map((_, i) => {\n          const start = (i / segments) * 100\n          const end = ((i + 1) / segments) * 100\n          const local = clamp(((s.pct - start) / (end - start)) * 100, 0, 100)\n          return (\n            <span\n              key={i}\n              className=\"relative h-full flex-1 overflow-hidden rounded-full bg-secondary\"\n            >\n              <span\n                className=\"absolute inset-y-0 left-0 rounded-full bg-primary\"\n                style={{ width: `${local}%` }}\n              />\n            </span>\n          )\n        })}\n        <MarksThumb s={s} size={size} reduce={reduce} />\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/slider-marks.tsx"
    }
  ],
  "categories": [
    "styled",
    "slider"
  ],
  "type": "registry:component"
}