{
  "slug": "segmented-progress",
  "name": "Segmented Progress",
  "description": "Metric card with segmented progress bars, hover value preview, a proximity wave and staggered GSAP fill.",
  "summary": "A dark card with a title, an icon, a more-options button, one or more segmented progress metrics with trend badges and rolling-number readouts, and an optional footer banner. Bars can be scrubbed by pointer or keyboard, and hovering previews values.",
  "category": "feedback",
  "tags": [
    "progress",
    "segmented",
    "dashboard",
    "gsap",
    "animation",
    "hover",
    "metrics",
    "telemetry",
    "physics",
    "interactive"
  ],
  "dependencies": [
    "clsx",
    "tailwind-merge",
    "gsap",
    "@gsap/react",
    "lucide-react",
    "motion"
  ],
  "version": "1.0.0",
  "createdDate": "2026-10-01",
  "updatedDate": "2026-10-01",
  "interactive": true,
  "supportsColor": true,
  "highlights": [
    "Segmented vertical capsule progress tracking with configurable segment resolution",
    "Hovering a segment previews its value in the metric readout",
    "Proximity wave physics applying organic Gaussian scaleY expansion to neighboring segments",
    "Staggered entrance choreographing sequential segment illumination via GSAP",
    "Rolling-digit readout that shows the hovered value and returns to the stored value on leave",
    "Complete dark obsidian card layout with metallic squircle progress icon, trend badges, and footer banner",
    "Full keyboard accessibility with Arrow keys, PageUp, PageDown, Home, End, and ARIA slider semantics"
  ],
  "anatomy": [
    "<div> (Card Shell: dark container with inner shadow and subtle border)",
    "<div> (Card Header: progress icon, title, subtitle, and more-options button)",
    "<div> (Metric Row: label, trend badge, and rolling percentage readout)",
    "<div> (Segmented Track: row of vertical rounded capsule segments)",
    "<div> (Footer Banner: tinted with the card color, with an arrow icon)"
  ],
  "physics": {
    "engine": "GSAP Staggered Fill & Gaussian Proximity Wave",
    "description": "GSAP staggered scaleY and opacity fill on value change, plus a Gaussian scaleY wave over segments near the pointer.",
    "parameters": [
      {
        "label": "Stagger Duration",
        "value": "0.012s per segment"
      },
      {
        "label": "Wave Peak Scale",
        "value": "1.16 scaleY"
      },
      {
        "label": "Proximity Radius",
        "value": "exp(-d^2 / 3.5), segments within 3 of the pointer"
      },
      {
        "label": "Fill Tween",
        "value": "0.45s power2.out from scaleY 0.35 / opacity 0.2"
      }
    ]
  },
  "accessibility": {
    "role": "slider",
    "aria": "role='slider' with aria-valuenow, aria-valuemin, aria-valuemax, aria-valuetext, and aria-label when interactive; falls back to role='progressbar' when read-only.",
    "reducedMotion": "Bypasses stagger and scale animations when prefers-reduced-motion is active.",
    "keyboard": [
      {
        "key": "ArrowRight / ArrowUp",
        "description": "Increase progress value by 1%."
      },
      {
        "key": "ArrowLeft / ArrowDown",
        "description": "Decrease progress value by 1%."
      },
      {
        "key": "PageUp / PageDown",
        "description": "Increase or decrease progress by one segment."
      },
      {
        "key": "Home / End",
        "description": "Jump to 0% or 100% progress."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Project completion dashboards, milestone trackers, and sprint goal monitors",
      "Sales targets, revenue quota cards, and financial performance overviews",
      "System health, disk usage, or CPU capacity indicators in dev tools"
    ],
    "bestPractices": [
      "Use 24 to 32 segments for standard card widths to ensure clear segment separation",
      "Pair emerald with blue for contrasting positive progress metrics",
      "Enable interactive scrubbing when user adjustment or goal setting is allowed"
    ]
  },
  "props": [
    {
      "name": "title",
      "type": "string",
      "defaultValue": "\"Project Progress\"",
      "description": "Card heading."
    },
    {
      "name": "subtitle",
      "type": "string",
      "defaultValue": "\"Overall completion rate all projects.\"",
      "description": "Text under the heading."
    },
    {
      "name": "icon",
      "type": "ReactNode",
      "description": "Icon next to the heading. Defaults to a progress pie icon."
    },
    {
      "name": "color",
      "type": "\"emerald\" | \"blue\" | \"violet\" | \"amber\" | \"rose\" | \"cyan\" | string",
      "defaultValue": "\"emerald\"",
      "description": "Preset name or CSS color for the filled segments. A metric's own color overrides it."
    },
    {
      "name": "metrics",
      "type": "SegmentedProgressMetric[]",
      "defaultValue": "two sample metrics",
      "description": "Rows to render. Each metric has id, label, value (0 to 100) and optional trend, trendDirection, color, unit and formatValue."
    },
    {
      "name": "segments",
      "type": "number",
      "defaultValue": "32",
      "description": "Number of segments per bar."
    },
    {
      "name": "footerText",
      "type": "string",
      "defaultValue": "\"Up by 6% compared to last week, great momentum!\"",
      "description": "Text in the card footer."
    },
    {
      "name": "footerHref",
      "type": "string",
      "description": "Turns the footer into a link to this URL."
    },
    {
      "name": "footerAction",
      "type": "() => void",
      "description": "Makes the footer clickable and calls this handler."
    },
    {
      "name": "onMoreClick",
      "type": "() => void",
      "description": "Called when the header's more button is pressed."
    },
    {
      "name": "interactive",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Lets users click, drag or use arrow keys on the bars to change values."
    },
    {
      "name": "onMetricChange",
      "type": "(metricId: string, newValue: number) => void",
      "description": "Called when the user changes a metric value."
    },
    {
      "name": "className",
      "type": "string",
      "description": "Additional CSS classes for the card."
    }
  ],
  "usage": "import { SegmentedProgress } from \"@/components/ui/segmented-progress\";\n\nconst metrics = [\n  { id: \"design\", label: \"Design\", value: 82 },\n  { id: \"build\", label: \"Build\", value: 54 },\n];\n\nexport function SegmentedProgressDemo() {\n  return <SegmentedProgress title=\"Sprint progress\" metrics={metrics} />;\n}",
  "files": [
    {
      "name": "segmented-progress.tsx",
      "path": "registry/ui/segmented-progress.tsx",
      "code": "\"use client\";\n\nimport React, {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n  type MotionValue,\n  type Variants,\n} from \"motion/react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { ArrowRight, MoreHorizontal } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type SegmentedProgressColor =\n  | \"emerald\"\n  | \"blue\"\n  | \"violet\"\n  | \"amber\"\n  | \"rose\"\n  | \"cyan\";\n\nexport interface ColorConfig {\n  active: string;\n  glow: string;\n  badgeBg: string;\n  badgeText: string;\n  badgeBorder: string;\n  inactive: string;\n}\n\nconst COLOR_MAP: Record<SegmentedProgressColor, ColorConfig> = {\n  emerald: {\n    active: \"#4ade80\",\n    glow: \"rgba(74, 222, 128, 0.38)\",\n    badgeBg: \"rgba(16, 185, 129, 0.12)\",\n    badgeText: \"#4ade80\",\n    badgeBorder: \"rgba(74, 222, 128, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  blue: {\n    active: \"#60a5fa\",\n    glow: \"rgba(96, 165, 250, 0.38)\",\n    badgeBg: \"rgba(59, 130, 246, 0.12)\",\n    badgeText: \"#60a5fa\",\n    badgeBorder: \"rgba(96, 165, 250, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  violet: {\n    active: \"#a78bfa\",\n    glow: \"rgba(167, 139, 250, 0.38)\",\n    badgeBg: \"rgba(139, 92, 246, 0.12)\",\n    badgeText: \"#a78bfa\",\n    badgeBorder: \"rgba(167, 139, 250, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  amber: {\n    active: \"#fbbf24\",\n    glow: \"rgba(251, 191, 36, 0.38)\",\n    badgeBg: \"rgba(245, 158, 11, 0.12)\",\n    badgeText: \"#fbbf24\",\n    badgeBorder: \"rgba(251, 191, 36, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  rose: {\n    active: \"#fb7185\",\n    glow: \"rgba(251, 113, 133, 0.38)\",\n    badgeBg: \"rgba(244, 63, 94, 0.12)\",\n    badgeText: \"#fb7185\",\n    badgeBorder: \"rgba(251, 113, 133, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  cyan: {\n    active: \"#22d3ee\",\n    glow: \"rgba(34, 211, 238, 0.38)\",\n    badgeBg: \"rgba(6, 182, 212, 0.12)\",\n    badgeText: \"#22d3ee\",\n    badgeBorder: \"rgba(34, 211, 238, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n};\n\nconst COLOR_ALIAS_MAP: Record<string, SegmentedProgressColor> = {\n  green: \"emerald\",\n  purple: \"violet\",\n  red: \"rose\",\n  orange: \"amber\",\n};\n\nfunction resolveColor(color: SegmentedProgressColor | string): ColorConfig {\n  const normalized = COLOR_ALIAS_MAP[color] || color;\n  if (normalized in COLOR_MAP) {\n    return COLOR_MAP[normalized as SegmentedProgressColor];\n  }\n  return {\n    active: color,\n    glow: `color-mix(in srgb, ${color} 40%, transparent)`,\n    badgeBg: `color-mix(in srgb, ${color} 15%, transparent)`,\n    badgeText: color,\n    badgeBorder: `color-mix(in srgb, ${color} 28%, transparent)`,\n    inactive: \"#1e1e22\",\n  };\n}\n\nexport function ProgressPieIcon({\n  className,\n  style,\n}: {\n  className?: string;\n  style?: React.CSSProperties;\n}) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      style={style}\n      className={cn(\"w-5 h-5 text-white\", className)}\n      aria-hidden=\"true\"\n    >\n      <circle\n        cx=\"12\"\n        cy=\"12\"\n        r=\"9\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        className=\"opacity-40\"\n      />\n      <path\n        d=\"M12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12H12V3Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\ntype Part = { key: string; digit: number } | { key: string; text: string };\n\nconst DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];\n\nconst rollMotionTokens = {\n  spring: {\n    snappy: { visualDuration: 0.32, bounce: 0.12 },\n    morph: { visualDuration: 0.4, bounce: 0.08 },\n    smooth: { visualDuration: 0.35, bounce: 0 },\n  },\n  duration: {\n    instant: 0.08,\n    fast: 0.18,\n    normal: 0.3,\n  },\n  blur: {\n    subtle: 1.5,\n    soft: 2.5,\n  },\n  ease: {\n    enter: [0.16, 1, 0.3, 1] as const,\n    standard: [0.2, 0, 0, 1] as const,\n  },\n};\n\nconst enterEase = [...rollMotionTokens.ease.enter] as [\n  number,\n  number,\n  number,\n  number,\n];\nconst exitEase = [...rollMotionTokens.ease.standard] as [\n  number,\n  number,\n  number,\n  number,\n];\n\nconst physicalSpring = ({\n  visualDuration,\n  bounce,\n}: {\n  visualDuration: number;\n  bounce: number;\n}) => {\n  const root = (2 * Math.PI) / (visualDuration * 1.2);\n  return {\n    type: \"spring\" as const,\n    stiffness: root * root,\n    damping: 2 * (1 - bounce) * root,\n    restDelta: 0.002,\n    restSpeed: 0.02,\n  };\n};\n\nconst rollSpring = physicalSpring(rollMotionTokens.spring.snappy);\n\nfunction partsOf(text: string): Part[] {\n  const chars = [...text];\n  const first = chars.findIndex((c) => c >= \"0\" && c <= \"9\");\n  const last =\n    chars.length -\n    1 -\n    [...chars].reverse().findIndex((c) => c >= \"0\" && c <= \"9\");\n  let place = chars.filter((c) => c >= \"0\" && c <= \"9\").length;\n  return chars.map((char, index): Part => {\n    if (first < 0 || index < first)\n      return { key: `p${index}${char}`, text: char };\n    if (index > last)\n      return { key: `s${chars.length - index}${char}`, text: char };\n    if (char >= \"0\" && char <= \"9\")\n      return { key: `d${--place}`, digit: Number(char) };\n    return { key: `g${place}${char}`, text: char };\n  });\n}\n\nconst slotVariants: Variants = {\n  enter: (direction: number) => ({\n    width: 0,\n    scale: 0.6,\n    opacity: 0,\n    y: `${direction * 0.3}em`,\n    filter: `blur(${rollMotionTokens.blur.soft}px)`,\n  }),\n  center: {\n    width: \"auto\",\n    scale: 1,\n    opacity: 1,\n    y: 0,\n    filter: \"blur(0px)\",\n    transitionEnd: { filter: \"none\" },\n    transition: {\n      width: rollMotionTokens.spring.morph,\n      scale: rollMotionTokens.spring.morph,\n      opacity: rollMotionTokens.spring.morph,\n      y: rollMotionTokens.spring.snappy,\n      filter: { duration: 0.22, ease: enterEase },\n    },\n  },\n  exit: (direction: number) => ({\n    width: 0,\n    scale: 0.6,\n    opacity: 0,\n    y: `${direction * -0.3}em`,\n    filter: `blur(${rollMotionTokens.blur.subtle}px)`,\n    transition: {\n      width: rollMotionTokens.spring.smooth,\n      scale: rollMotionTokens.spring.smooth,\n      y: { duration: rollMotionTokens.duration.fast, ease: exitEase },\n      opacity: { duration: rollMotionTokens.duration.instant },\n      filter: { duration: rollMotionTokens.duration.instant },\n    },\n  }),\n};\n\nconst stillVariants: Variants = {\n  enter: { width: \"auto\", scale: 1, opacity: 0, y: 0, filter: \"none\" },\n  center: {\n    width: \"auto\",\n    scale: 1,\n    opacity: 1,\n    y: 0,\n    filter: \"none\",\n    transition: { duration: rollMotionTokens.duration.instant },\n  },\n  exit: { width: 0, opacity: 0, transition: { duration: 0 } },\n};\n\nfunction Glyph({\n  position,\n  digit,\n}: {\n  position: MotionValue<number>;\n  digit: number;\n}) {\n  const offset = (current: number) => {\n    const diff = (((digit - current) % 10) + 10) % 10;\n    return diff > 5 ? diff - 10 : diff;\n  };\n  const y = useTransform(position, (current) => `${offset(current) * 1.05}em`);\n  const opacity = useTransform(position, (current) =>\n    Math.max(0, 1 - Math.abs(offset(current)) ** 1.5 * 1.1),\n  );\n  const visibility = useTransform(position, (current) =>\n    Math.abs(offset(current)) >= 1 ? \"hidden\" : \"visible\",\n  );\n  const filter = useTransform(position, (current) => {\n    const distance = Math.abs(offset(current));\n    return distance < 0.02 || distance >= 1\n      ? \"none\"\n      : `blur(${(distance * rollMotionTokens.blur.soft * 0.75).toFixed(2)}px)`;\n  });\n\n  return (\n    <motion.span\n      className=\"absolute inset-0 flex items-center justify-center pointer-events-none select-none will-change-transform\"\n      style={{ y, opacity, filter, visibility }}\n    >\n      {digit}\n    </motion.span>\n  );\n}\n\nfunction Wheel({ digit, direction }: { digit: number; direction: number }) {\n  const reduced = useReducedMotion();\n  const position = useMotionValue(digit);\n  const wheel = useRef({ digit, target: digit });\n\n  useEffect(() => {\n    const state = wheel.current;\n    if (state.digit === digit) return;\n    state.target +=\n      direction > 0\n        ? (digit - state.digit + 10) % 10\n        : -((state.digit - digit + 10) % 10);\n    state.digit = digit;\n    if (reduced) position.jump(state.target);\n    else animate(position, state.target, rollSpring);\n  }, [digit, direction, position, reduced]);\n\n  return (\n    <>\n      <span className=\"invisible pointer-events-none select-none tabular-nums font-semibold\">\n        0\n      </span>\n      {DIGITS.map((item) => (\n        <Glyph key={item} position={position} digit={item} />\n      ))}\n    </>\n  );\n}\n\nexport function RollingNumber({\n  value,\n  text,\n  className,\n}: {\n  value: number;\n  text: string;\n  className?: string;\n}) {\n  const reduced = useReducedMotion();\n  const [trail, setTrail] = useState({ value, direction: 1 });\n\n  if (trail.value !== value) {\n    setTrail({ value, direction: value >= trail.value ? 1 : -1 });\n  }\n  const direction =\n    trail.value === value ? trail.direction : value >= trail.value ? 1 : -1;\n\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-baseline overflow-hidden tabular-nums leading-none select-none\",\n        className,\n      )}\n    >\n      <AnimatePresence initial={false} custom={direction}>\n        {partsOf(text).map((part) => (\n          <motion.span\n            key={part.key}\n            className={\n              \"digit\" in part\n                ? \"relative inline-flex items-center justify-center overflow-hidden h-[1.15em] align-baseline\"\n                : \"inline-flex items-center justify-center align-baseline\"\n            }\n            custom={direction}\n            variants={reduced ? stillVariants : slotVariants}\n            initial=\"enter\"\n            animate=\"center\"\n            exit=\"exit\"\n          >\n            {\"digit\" in part ? (\n              <Wheel digit={part.digit} direction={direction} />\n            ) : (\n              part.text\n            )}\n          </motion.span>\n        ))}\n      </AnimatePresence>\n    </span>\n  );\n}\n\nexport interface SegmentedProgressBarProps {\n  value: number;\n  segments?: number;\n  color?: SegmentedProgressColor | string;\n  height?: number;\n  interactive?: boolean;\n  onChange?: (value: number) => void;\n  onHoverChange?: (hoveredValue: number | null, index: number | null) => void;\n  className?: string;\n  disabled?: boolean;\n  ariaLabel?: string;\n}\n\nexport function SegmentedProgressBar({\n  value,\n  segments = 32,\n  color = \"emerald\",\n  height = 26,\n  interactive = true,\n  onChange,\n  onHoverChange,\n  className,\n  disabled = false,\n  ariaLabel,\n}: SegmentedProgressBarProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const trackRef = useRef<HTMLDivElement>(null);\n  const segmentRefs = useRef<(HTMLDivElement | null)[]>([]);\n\n  const colorConfig = useMemo(() => resolveColor(color), [color]);\n  const clampedValue = Math.max(0, Math.min(100, Math.round(value)));\n  const baselineCount = Math.round((clampedValue / 100) * segments);\n\n  const isAdjustable = interactive && !disabled && Boolean(onChange);\n\n  const [hoverIndex, setHoverIndex] = useState<number | null>(null);\n  const isDraggingRef = useRef(false);\n\n  const activeSegmentsCount =\n    hoverIndex !== null ? hoverIndex + 1 : baselineCount;\n\n  const isReducedMotion = useRef(false);\n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      isReducedMotion.current = window.matchMedia(\n        \"(prefers-reduced-motion: reduce)\",\n      ).matches;\n    }\n  }, []);\n\n  useGSAP(\n    () => {\n      if (isReducedMotion.current) return;\n      const elements = segmentRefs.current.slice(0, baselineCount);\n      if (elements.length === 0) return;\n\n      gsap.fromTo(\n        elements,\n        {\n          scaleY: 0.35,\n          opacity: 0.2,\n        },\n        {\n          scaleY: 1,\n          opacity: 1,\n          duration: 0.45,\n          stagger: {\n            each: 0.012,\n            from: \"start\",\n          },\n          ease: \"power2.out\",\n        },\n      );\n    },\n    {\n      scope: containerRef,\n      dependencies: [baselineCount],\n      revertOnUpdate: false,\n    },\n  );\n\n  const updateHoverEffect = useCallback((targetIndex: number | null) => {\n    if (!segmentRefs.current.length) return;\n\n    segmentRefs.current.forEach((seg, idx) => {\n      if (!seg) return;\n      if (targetIndex === null) {\n        gsap.to(seg, {\n          scaleY: 1,\n          duration: 0.25,\n          ease: \"power2.out\",\n          overwrite: \"auto\",\n        });\n      } else {\n        const dist = Math.abs(idx - targetIndex);\n        if (dist < 4) {\n          const scale = 1 + 0.16 * Math.exp(-(dist * dist) / 3.5);\n          gsap.to(seg, {\n            scaleY: scale,\n            duration: 0.16,\n            ease: \"power1.out\",\n            overwrite: \"auto\",\n          });\n        } else {\n          gsap.to(seg, {\n            scaleY: 1,\n            duration: 0.2,\n            ease: \"power2.out\",\n            overwrite: \"auto\",\n          });\n        }\n      }\n    });\n  }, []);\n\n  const handlePointerMove = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (disabled || !trackRef.current) return;\n      const rect = trackRef.current.getBoundingClientRect();\n      const pointerX = Math.max(0, Math.min(rect.width, e.clientX - rect.left));\n      const ratio = pointerX / rect.width;\n      const index = Math.min(segments - 1, Math.floor(ratio * segments));\n      const hoveredVal = Math.round(((index + 1) / segments) * 100);\n\n      setHoverIndex(index);\n      onHoverChange?.(hoveredVal, index);\n      updateHoverEffect(index);\n\n      if (isDraggingRef.current && interactive && onChange) {\n        onChange(hoveredVal);\n      }\n    },\n    [\n      disabled,\n      segments,\n      onHoverChange,\n      updateHoverEffect,\n      interactive,\n      onChange,\n    ],\n  );\n\n  const handlePointerLeave = useCallback(() => {\n    isDraggingRef.current = false;\n    setHoverIndex(null);\n    onHoverChange?.(null, null);\n    updateHoverEffect(null);\n  }, [onHoverChange, updateHoverEffect]);\n\n  const handlePointerDown = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (disabled || !interactive) return;\n      isDraggingRef.current = true;\n      e.currentTarget.setPointerCapture(e.pointerId);\n      handlePointerMove(e);\n    },\n    [disabled, interactive, handlePointerMove],\n  );\n\n  const handlePointerUp = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (isDraggingRef.current) {\n        isDraggingRef.current = false;\n        try {\n          e.currentTarget.releasePointerCapture(e.pointerId);\n        } catch {}\n      }\n    },\n    [],\n  );\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLDivElement>) => {\n      if (disabled || !interactive || !onChange) return;\n      if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n        e.preventDefault();\n        onChange(Math.min(100, clampedValue + 1));\n      } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n        e.preventDefault();\n        onChange(Math.max(0, clampedValue - 1));\n      } else if (e.key === \"PageUp\") {\n        e.preventDefault();\n        onChange(\n          Math.round((Math.min(segments, baselineCount + 1) / segments) * 100),\n        );\n      } else if (e.key === \"PageDown\") {\n        e.preventDefault();\n        onChange(Math.round((Math.max(0, baselineCount - 1) / segments) * 100));\n      } else if (e.key === \"Home\") {\n        e.preventDefault();\n        onChange(0);\n      } else if (e.key === \"End\") {\n        e.preventDefault();\n        onChange(100);\n      }\n    },\n    [disabled, interactive, onChange, clampedValue, baselineCount, segments],\n  );\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"relative w-full select-none touch-none py-1\",\n        disabled && \"opacity-40 pointer-events-none\",\n        className,\n      )}\n    >\n      <div\n        ref={trackRef}\n        role={isAdjustable ? \"slider\" : \"progressbar\"}\n        tabIndex={isAdjustable ? 0 : -1}\n        aria-valuenow={clampedValue}\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuetext={`${clampedValue}%`}\n        aria-label={ariaLabel || \"Progress bar\"}\n        onPointerMove={handlePointerMove}\n        onPointerDown={handlePointerDown}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerLeave}\n        onPointerLeave={handlePointerLeave}\n        onKeyDown={handleKeyDown}\n        className={cn(\n          \"relative flex w-full items-center justify-between gap-0.75 sm:gap-1 outline-none\",\n          interactive &&\n            \"cursor-pointer focus-visible:ring-2 focus-visible:ring-white/40 focus-visible:ring-offset-2 focus-visible:ring-offset-black rounded-md\",\n        )}\n        style={{ height }}\n      >\n        {Array.from({ length: segments }).map((_, idx) => {\n          const isActive = idx < activeSegmentsCount;\n\n          const activeStyle = isActive\n            ? {\n                backgroundColor: colorConfig.active,\n                boxShadow: `0 0 10px ${colorConfig.glow}`,\n                opacity: 1,\n              }\n            : {\n                backgroundColor: colorConfig.inactive,\n                opacity: 1,\n              };\n\n          return (\n            <div\n              key={idx}\n              ref={(el) => {\n                segmentRefs.current[idx] = el;\n              }}\n              style={{\n                height: \"100%\",\n                ...activeStyle,\n              }}\n              className=\"flex-1 min-w-0.75 rounded-full transition-colors duration-150 will-change-transform\"\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nexport interface SegmentedProgressMetric {\n  id: string;\n  label: string;\n  value: number;\n  trend?: string;\n  trendDirection?: \"up\" | \"down\" | \"neutral\";\n  color?: SegmentedProgressColor | string;\n  unit?: string;\n  formatValue?: (val: number) => string;\n}\n\nexport interface SegmentedProgressItemProps {\n  metric: SegmentedProgressMetric;\n  segments?: number;\n  interactive?: boolean;\n  onChange?: (val: number) => void;\n}\n\nexport function SegmentedProgressItem({\n  metric,\n  segments = 32,\n  interactive = true,\n  onChange,\n}: SegmentedProgressItemProps) {\n  const {\n    label,\n    value,\n    trend,\n    trendDirection = \"up\",\n    color = \"emerald\",\n    unit = \"%\",\n    formatValue,\n  } = metric;\n\n  const colorConfig = useMemo(() => resolveColor(color), [color]);\n  const [hoveredValue, setHoveredValue] = useState<number | null>(null);\n\n  const displayVal = hoveredValue !== null ? hoveredValue : value;\n  const formattedText = formatValue\n    ? formatValue(displayVal)\n    : `${displayVal}${unit}`;\n\n  return (\n    <div className=\"flex flex-col gap-2.5 w-full\">\n      <div className=\"flex items-center justify-between\">\n        <span className=\"text-[13px] sm:text-sm font-medium text-zinc-400\">\n          {label}\n        </span>\n        <div className=\"flex items-center gap-2.5\">\n          {trend && (\n            <div\n              className=\"flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium\"\n              style={{\n                backgroundColor: colorConfig.badgeBg,\n                color: colorConfig.badgeText,\n                border: `1px solid ${colorConfig.badgeBorder}`,\n              }}\n            >\n              <span className=\"text-xs\">\n                {trendDirection === \"up\"\n                  ? \"↑\"\n                  : trendDirection === \"down\"\n                    ? \"↓\"\n                    : \"•\"}\n              </span>\n              <span>{trend}</span>\n            </div>\n          )}\n          <span className=\"text-lg sm:text-xl font-semibold tabular-nums text-white min-w-[3.5ch] text-right inline-flex items-baseline justify-end\">\n            <RollingNumber value={displayVal} text={formattedText} />\n          </span>\n        </div>\n      </div>\n\n      <SegmentedProgressBar\n        value={value}\n        segments={segments}\n        color={color}\n        interactive={interactive}\n        onChange={onChange}\n        onHoverChange={(hoverVal) => setHoveredValue(hoverVal)}\n        ariaLabel={`${label} progress`}\n      />\n    </div>\n  );\n}\n\nexport interface SegmentedProgressProps {\n  title?: string;\n  subtitle?: string;\n  icon?: React.ReactNode;\n  color?: SegmentedProgressColor | string;\n  metrics?: SegmentedProgressMetric[];\n  segments?: number;\n  footerText?: string;\n  footerHref?: string;\n  footerAction?: () => void;\n  onMoreClick?: () => void;\n  interactive?: boolean;\n  onMetricChange?: (metricId: string, newValue: number) => void;\n  className?: string;\n}\n\nconst DEFAULT_METRICS: SegmentedProgressMetric[] = [\n  {\n    id: \"performing-progress\",\n    label: \"Performing Progress\",\n    value: 89,\n  },\n  {\n    id: \"target-sales\",\n    label: \"Target Sales\",\n    value: 67,\n  },\n];\n\nexport function SegmentedProgress({\n  title = \"Project Progress\",\n  subtitle = \"Overall completion rate all projects.\",\n  icon,\n  color = \"emerald\",\n  metrics = DEFAULT_METRICS,\n  segments = 32,\n  footerText = \"Up by 6% compared to last week, great momentum!\",\n  footerHref,\n  footerAction,\n  onMoreClick,\n  interactive = true,\n  onMetricChange,\n  className,\n}: SegmentedProgressProps) {\n  const cardRef = useRef<HTMLDivElement>(null);\n  const [valueOverrides, setValueOverrides] = useState<Record<string, number>>(\n    {},\n  );\n\n  const colorConfig = useMemo(() => resolveColor(color), [color]);\n\n  const activeMetrics = useMemo(() => {\n    return metrics.map((m) => ({\n      ...m,\n      color: m.color || color,\n      value:\n        valueOverrides[m.id] !== undefined ? valueOverrides[m.id] : m.value,\n    }));\n  }, [metrics, valueOverrides, color]);\n\n  const handleMetricChange = useCallback(\n    (id: string, val: number) => {\n      setValueOverrides((prev) => ({ ...prev, [id]: val }));\n      onMetricChange?.(id, val);\n    },\n    [onMetricChange],\n  );\n\n  return (\n    <div\n      ref={cardRef}\n      className={cn(\n        \"relative w-full max-w-105 rounded-2xl bg-[#141416] p-5 sm:p-6 text-white border border-white/8 shadow-[0_24px_50px_-12px_rgba(0,0,0,0.7)] flex flex-col gap-5 overflow-hidden\",\n        className,\n      )}\n    >\n      <div className=\"flex items-start justify-between gap-3\">\n        <div className=\"flex items-center gap-3.5\">\n          <div className=\"relative flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-linear-to-b from-[#2a2a2e] to-[#141416] border border-white/15 shadow-[inset_0_1px_1px_rgba(255,255,255,0.25),0_4px_12px_rgba(0,0,0,0.5)]\">\n            {icon || (\n              <ProgressPieIcon\n                className=\"w-5 h-5 transition-colors duration-200\"\n                style={{ color: colorConfig.active }}\n              />\n            )}\n          </div>\n          <div className=\"flex flex-col\">\n            <h3 className=\"text-[15px] sm:text-base font-semibold text-white tracking-tight leading-snug\">\n              {title}\n            </h3>\n            <p className=\"text-xs sm:text-[13px] text-zinc-400 font-normal leading-normal\">\n              {subtitle}\n            </p>\n          </div>\n        </div>\n\n        <button\n          type=\"button\"\n          onClick={onMoreClick}\n          aria-label=\"More options\"\n          className=\"rounded-lg p-1.5 text-zinc-400 hover:text-white hover:bg-white/6 transition-colors cursor-pointer outline-none focus-visible:ring-1 focus-visible:ring-white/20\"\n        >\n          <MoreHorizontal className=\"w-4 h-4\" />\n        </button>\n      </div>\n\n      <div className=\"flex flex-col gap-4\">\n        {activeMetrics.map((metric) => (\n          <SegmentedProgressItem\n            key={metric.id}\n            metric={metric}\n            segments={segments}\n            interactive={interactive}\n            onChange={(newVal) => handleMetricChange(metric.id, newVal)}\n          />\n        ))}\n      </div>\n\n      {footerText && (\n        <div\n          role={footerAction || footerHref ? \"button\" : undefined}\n          tabIndex={footerAction || footerHref ? 0 : undefined}\n          onClick={footerAction}\n          style={{\n            backgroundColor: colorConfig.badgeBg,\n            borderColor: colorConfig.badgeBorder,\n            color: colorConfig.badgeText,\n          }}\n          className={cn(\n            \"group relative flex items-center justify-between rounded-xl px-4 py-3 border transition-colors duration-200\",\n            (footerAction || footerHref) &&\n              \"cursor-pointer hover:brightness-110\",\n          )}\n        >\n          {footerHref ? (\n            <a\n              href={footerHref}\n              className=\"flex items-center justify-between w-full text-xs sm:text-[13px] font-medium leading-relaxed\"\n            >\n              <span>{footerText}</span>\n              <ArrowRight className=\"w-4 h-4 shrink-0 transition-transform duration-200 group-hover:translate-x-1\" />\n            </a>\n          ) : (\n            <>\n              <span className=\"text-xs sm:text-[13px] font-medium leading-relaxed\">\n                {footerText}\n              </span>\n              <ArrowRight className=\"w-4 h-4 shrink-0 transition-transform duration-200 group-hover:translate-x-1\" />\n            </>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default SegmentedProgress;\n",
      "content": "\"use client\";\n\nimport React, {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n  type MotionValue,\n  type Variants,\n} from \"motion/react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { ArrowRight, MoreHorizontal } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type SegmentedProgressColor =\n  | \"emerald\"\n  | \"blue\"\n  | \"violet\"\n  | \"amber\"\n  | \"rose\"\n  | \"cyan\";\n\nexport interface ColorConfig {\n  active: string;\n  glow: string;\n  badgeBg: string;\n  badgeText: string;\n  badgeBorder: string;\n  inactive: string;\n}\n\nconst COLOR_MAP: Record<SegmentedProgressColor, ColorConfig> = {\n  emerald: {\n    active: \"#4ade80\",\n    glow: \"rgba(74, 222, 128, 0.38)\",\n    badgeBg: \"rgba(16, 185, 129, 0.12)\",\n    badgeText: \"#4ade80\",\n    badgeBorder: \"rgba(74, 222, 128, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  blue: {\n    active: \"#60a5fa\",\n    glow: \"rgba(96, 165, 250, 0.38)\",\n    badgeBg: \"rgba(59, 130, 246, 0.12)\",\n    badgeText: \"#60a5fa\",\n    badgeBorder: \"rgba(96, 165, 250, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  violet: {\n    active: \"#a78bfa\",\n    glow: \"rgba(167, 139, 250, 0.38)\",\n    badgeBg: \"rgba(139, 92, 246, 0.12)\",\n    badgeText: \"#a78bfa\",\n    badgeBorder: \"rgba(167, 139, 250, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  amber: {\n    active: \"#fbbf24\",\n    glow: \"rgba(251, 191, 36, 0.38)\",\n    badgeBg: \"rgba(245, 158, 11, 0.12)\",\n    badgeText: \"#fbbf24\",\n    badgeBorder: \"rgba(251, 191, 36, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  rose: {\n    active: \"#fb7185\",\n    glow: \"rgba(251, 113, 133, 0.38)\",\n    badgeBg: \"rgba(244, 63, 94, 0.12)\",\n    badgeText: \"#fb7185\",\n    badgeBorder: \"rgba(251, 113, 133, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n  cyan: {\n    active: \"#22d3ee\",\n    glow: \"rgba(34, 211, 238, 0.38)\",\n    badgeBg: \"rgba(6, 182, 212, 0.12)\",\n    badgeText: \"#22d3ee\",\n    badgeBorder: \"rgba(34, 211, 238, 0.25)\",\n    inactive: \"#1e1e22\",\n  },\n};\n\nconst COLOR_ALIAS_MAP: Record<string, SegmentedProgressColor> = {\n  green: \"emerald\",\n  purple: \"violet\",\n  red: \"rose\",\n  orange: \"amber\",\n};\n\nfunction resolveColor(color: SegmentedProgressColor | string): ColorConfig {\n  const normalized = COLOR_ALIAS_MAP[color] || color;\n  if (normalized in COLOR_MAP) {\n    return COLOR_MAP[normalized as SegmentedProgressColor];\n  }\n  return {\n    active: color,\n    glow: `color-mix(in srgb, ${color} 40%, transparent)`,\n    badgeBg: `color-mix(in srgb, ${color} 15%, transparent)`,\n    badgeText: color,\n    badgeBorder: `color-mix(in srgb, ${color} 28%, transparent)`,\n    inactive: \"#1e1e22\",\n  };\n}\n\nexport function ProgressPieIcon({\n  className,\n  style,\n}: {\n  className?: string;\n  style?: React.CSSProperties;\n}) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      style={style}\n      className={cn(\"w-5 h-5 text-white\", className)}\n      aria-hidden=\"true\"\n    >\n      <circle\n        cx=\"12\"\n        cy=\"12\"\n        r=\"9\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        className=\"opacity-40\"\n      />\n      <path\n        d=\"M12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12H12V3Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\ntype Part = { key: string; digit: number } | { key: string; text: string };\n\nconst DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];\n\nconst rollMotionTokens = {\n  spring: {\n    snappy: { visualDuration: 0.32, bounce: 0.12 },\n    morph: { visualDuration: 0.4, bounce: 0.08 },\n    smooth: { visualDuration: 0.35, bounce: 0 },\n  },\n  duration: {\n    instant: 0.08,\n    fast: 0.18,\n    normal: 0.3,\n  },\n  blur: {\n    subtle: 1.5,\n    soft: 2.5,\n  },\n  ease: {\n    enter: [0.16, 1, 0.3, 1] as const,\n    standard: [0.2, 0, 0, 1] as const,\n  },\n};\n\nconst enterEase = [...rollMotionTokens.ease.enter] as [\n  number,\n  number,\n  number,\n  number,\n];\nconst exitEase = [...rollMotionTokens.ease.standard] as [\n  number,\n  number,\n  number,\n  number,\n];\n\nconst physicalSpring = ({\n  visualDuration,\n  bounce,\n}: {\n  visualDuration: number;\n  bounce: number;\n}) => {\n  const root = (2 * Math.PI) / (visualDuration * 1.2);\n  return {\n    type: \"spring\" as const,\n    stiffness: root * root,\n    damping: 2 * (1 - bounce) * root,\n    restDelta: 0.002,\n    restSpeed: 0.02,\n  };\n};\n\nconst rollSpring = physicalSpring(rollMotionTokens.spring.snappy);\n\nfunction partsOf(text: string): Part[] {\n  const chars = [...text];\n  const first = chars.findIndex((c) => c >= \"0\" && c <= \"9\");\n  const last =\n    chars.length -\n    1 -\n    [...chars].reverse().findIndex((c) => c >= \"0\" && c <= \"9\");\n  let place = chars.filter((c) => c >= \"0\" && c <= \"9\").length;\n  return chars.map((char, index): Part => {\n    if (first < 0 || index < first)\n      return { key: `p${index}${char}`, text: char };\n    if (index > last)\n      return { key: `s${chars.length - index}${char}`, text: char };\n    if (char >= \"0\" && char <= \"9\")\n      return { key: `d${--place}`, digit: Number(char) };\n    return { key: `g${place}${char}`, text: char };\n  });\n}\n\nconst slotVariants: Variants = {\n  enter: (direction: number) => ({\n    width: 0,\n    scale: 0.6,\n    opacity: 0,\n    y: `${direction * 0.3}em`,\n    filter: `blur(${rollMotionTokens.blur.soft}px)`,\n  }),\n  center: {\n    width: \"auto\",\n    scale: 1,\n    opacity: 1,\n    y: 0,\n    filter: \"blur(0px)\",\n    transitionEnd: { filter: \"none\" },\n    transition: {\n      width: rollMotionTokens.spring.morph,\n      scale: rollMotionTokens.spring.morph,\n      opacity: rollMotionTokens.spring.morph,\n      y: rollMotionTokens.spring.snappy,\n      filter: { duration: 0.22, ease: enterEase },\n    },\n  },\n  exit: (direction: number) => ({\n    width: 0,\n    scale: 0.6,\n    opacity: 0,\n    y: `${direction * -0.3}em`,\n    filter: `blur(${rollMotionTokens.blur.subtle}px)`,\n    transition: {\n      width: rollMotionTokens.spring.smooth,\n      scale: rollMotionTokens.spring.smooth,\n      y: { duration: rollMotionTokens.duration.fast, ease: exitEase },\n      opacity: { duration: rollMotionTokens.duration.instant },\n      filter: { duration: rollMotionTokens.duration.instant },\n    },\n  }),\n};\n\nconst stillVariants: Variants = {\n  enter: { width: \"auto\", scale: 1, opacity: 0, y: 0, filter: \"none\" },\n  center: {\n    width: \"auto\",\n    scale: 1,\n    opacity: 1,\n    y: 0,\n    filter: \"none\",\n    transition: { duration: rollMotionTokens.duration.instant },\n  },\n  exit: { width: 0, opacity: 0, transition: { duration: 0 } },\n};\n\nfunction Glyph({\n  position,\n  digit,\n}: {\n  position: MotionValue<number>;\n  digit: number;\n}) {\n  const offset = (current: number) => {\n    const diff = (((digit - current) % 10) + 10) % 10;\n    return diff > 5 ? diff - 10 : diff;\n  };\n  const y = useTransform(position, (current) => `${offset(current) * 1.05}em`);\n  const opacity = useTransform(position, (current) =>\n    Math.max(0, 1 - Math.abs(offset(current)) ** 1.5 * 1.1),\n  );\n  const visibility = useTransform(position, (current) =>\n    Math.abs(offset(current)) >= 1 ? \"hidden\" : \"visible\",\n  );\n  const filter = useTransform(position, (current) => {\n    const distance = Math.abs(offset(current));\n    return distance < 0.02 || distance >= 1\n      ? \"none\"\n      : `blur(${(distance * rollMotionTokens.blur.soft * 0.75).toFixed(2)}px)`;\n  });\n\n  return (\n    <motion.span\n      className=\"absolute inset-0 flex items-center justify-center pointer-events-none select-none will-change-transform\"\n      style={{ y, opacity, filter, visibility }}\n    >\n      {digit}\n    </motion.span>\n  );\n}\n\nfunction Wheel({ digit, direction }: { digit: number; direction: number }) {\n  const reduced = useReducedMotion();\n  const position = useMotionValue(digit);\n  const wheel = useRef({ digit, target: digit });\n\n  useEffect(() => {\n    const state = wheel.current;\n    if (state.digit === digit) return;\n    state.target +=\n      direction > 0\n        ? (digit - state.digit + 10) % 10\n        : -((state.digit - digit + 10) % 10);\n    state.digit = digit;\n    if (reduced) position.jump(state.target);\n    else animate(position, state.target, rollSpring);\n  }, [digit, direction, position, reduced]);\n\n  return (\n    <>\n      <span className=\"invisible pointer-events-none select-none tabular-nums font-semibold\">\n        0\n      </span>\n      {DIGITS.map((item) => (\n        <Glyph key={item} position={position} digit={item} />\n      ))}\n    </>\n  );\n}\n\nexport function RollingNumber({\n  value,\n  text,\n  className,\n}: {\n  value: number;\n  text: string;\n  className?: string;\n}) {\n  const reduced = useReducedMotion();\n  const [trail, setTrail] = useState({ value, direction: 1 });\n\n  if (trail.value !== value) {\n    setTrail({ value, direction: value >= trail.value ? 1 : -1 });\n  }\n  const direction =\n    trail.value === value ? trail.direction : value >= trail.value ? 1 : -1;\n\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-baseline overflow-hidden tabular-nums leading-none select-none\",\n        className,\n      )}\n    >\n      <AnimatePresence initial={false} custom={direction}>\n        {partsOf(text).map((part) => (\n          <motion.span\n            key={part.key}\n            className={\n              \"digit\" in part\n                ? \"relative inline-flex items-center justify-center overflow-hidden h-[1.15em] align-baseline\"\n                : \"inline-flex items-center justify-center align-baseline\"\n            }\n            custom={direction}\n            variants={reduced ? stillVariants : slotVariants}\n            initial=\"enter\"\n            animate=\"center\"\n            exit=\"exit\"\n          >\n            {\"digit\" in part ? (\n              <Wheel digit={part.digit} direction={direction} />\n            ) : (\n              part.text\n            )}\n          </motion.span>\n        ))}\n      </AnimatePresence>\n    </span>\n  );\n}\n\nexport interface SegmentedProgressBarProps {\n  value: number;\n  segments?: number;\n  color?: SegmentedProgressColor | string;\n  height?: number;\n  interactive?: boolean;\n  onChange?: (value: number) => void;\n  onHoverChange?: (hoveredValue: number | null, index: number | null) => void;\n  className?: string;\n  disabled?: boolean;\n  ariaLabel?: string;\n}\n\nexport function SegmentedProgressBar({\n  value,\n  segments = 32,\n  color = \"emerald\",\n  height = 26,\n  interactive = true,\n  onChange,\n  onHoverChange,\n  className,\n  disabled = false,\n  ariaLabel,\n}: SegmentedProgressBarProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const trackRef = useRef<HTMLDivElement>(null);\n  const segmentRefs = useRef<(HTMLDivElement | null)[]>([]);\n\n  const colorConfig = useMemo(() => resolveColor(color), [color]);\n  const clampedValue = Math.max(0, Math.min(100, Math.round(value)));\n  const baselineCount = Math.round((clampedValue / 100) * segments);\n\n  const isAdjustable = interactive && !disabled && Boolean(onChange);\n\n  const [hoverIndex, setHoverIndex] = useState<number | null>(null);\n  const isDraggingRef = useRef(false);\n\n  const activeSegmentsCount =\n    hoverIndex !== null ? hoverIndex + 1 : baselineCount;\n\n  const isReducedMotion = useRef(false);\n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      isReducedMotion.current = window.matchMedia(\n        \"(prefers-reduced-motion: reduce)\",\n      ).matches;\n    }\n  }, []);\n\n  useGSAP(\n    () => {\n      if (isReducedMotion.current) return;\n      const elements = segmentRefs.current.slice(0, baselineCount);\n      if (elements.length === 0) return;\n\n      gsap.fromTo(\n        elements,\n        {\n          scaleY: 0.35,\n          opacity: 0.2,\n        },\n        {\n          scaleY: 1,\n          opacity: 1,\n          duration: 0.45,\n          stagger: {\n            each: 0.012,\n            from: \"start\",\n          },\n          ease: \"power2.out\",\n        },\n      );\n    },\n    {\n      scope: containerRef,\n      dependencies: [baselineCount],\n      revertOnUpdate: false,\n    },\n  );\n\n  const updateHoverEffect = useCallback((targetIndex: number | null) => {\n    if (!segmentRefs.current.length) return;\n\n    segmentRefs.current.forEach((seg, idx) => {\n      if (!seg) return;\n      if (targetIndex === null) {\n        gsap.to(seg, {\n          scaleY: 1,\n          duration: 0.25,\n          ease: \"power2.out\",\n          overwrite: \"auto\",\n        });\n      } else {\n        const dist = Math.abs(idx - targetIndex);\n        if (dist < 4) {\n          const scale = 1 + 0.16 * Math.exp(-(dist * dist) / 3.5);\n          gsap.to(seg, {\n            scaleY: scale,\n            duration: 0.16,\n            ease: \"power1.out\",\n            overwrite: \"auto\",\n          });\n        } else {\n          gsap.to(seg, {\n            scaleY: 1,\n            duration: 0.2,\n            ease: \"power2.out\",\n            overwrite: \"auto\",\n          });\n        }\n      }\n    });\n  }, []);\n\n  const handlePointerMove = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (disabled || !trackRef.current) return;\n      const rect = trackRef.current.getBoundingClientRect();\n      const pointerX = Math.max(0, Math.min(rect.width, e.clientX - rect.left));\n      const ratio = pointerX / rect.width;\n      const index = Math.min(segments - 1, Math.floor(ratio * segments));\n      const hoveredVal = Math.round(((index + 1) / segments) * 100);\n\n      setHoverIndex(index);\n      onHoverChange?.(hoveredVal, index);\n      updateHoverEffect(index);\n\n      if (isDraggingRef.current && interactive && onChange) {\n        onChange(hoveredVal);\n      }\n    },\n    [\n      disabled,\n      segments,\n      onHoverChange,\n      updateHoverEffect,\n      interactive,\n      onChange,\n    ],\n  );\n\n  const handlePointerLeave = useCallback(() => {\n    isDraggingRef.current = false;\n    setHoverIndex(null);\n    onHoverChange?.(null, null);\n    updateHoverEffect(null);\n  }, [onHoverChange, updateHoverEffect]);\n\n  const handlePointerDown = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (disabled || !interactive) return;\n      isDraggingRef.current = true;\n      e.currentTarget.setPointerCapture(e.pointerId);\n      handlePointerMove(e);\n    },\n    [disabled, interactive, handlePointerMove],\n  );\n\n  const handlePointerUp = useCallback(\n    (e: React.PointerEvent<HTMLDivElement>) => {\n      if (isDraggingRef.current) {\n        isDraggingRef.current = false;\n        try {\n          e.currentTarget.releasePointerCapture(e.pointerId);\n        } catch {}\n      }\n    },\n    [],\n  );\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLDivElement>) => {\n      if (disabled || !interactive || !onChange) return;\n      if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n        e.preventDefault();\n        onChange(Math.min(100, clampedValue + 1));\n      } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n        e.preventDefault();\n        onChange(Math.max(0, clampedValue - 1));\n      } else if (e.key === \"PageUp\") {\n        e.preventDefault();\n        onChange(\n          Math.round((Math.min(segments, baselineCount + 1) / segments) * 100),\n        );\n      } else if (e.key === \"PageDown\") {\n        e.preventDefault();\n        onChange(Math.round((Math.max(0, baselineCount - 1) / segments) * 100));\n      } else if (e.key === \"Home\") {\n        e.preventDefault();\n        onChange(0);\n      } else if (e.key === \"End\") {\n        e.preventDefault();\n        onChange(100);\n      }\n    },\n    [disabled, interactive, onChange, clampedValue, baselineCount, segments],\n  );\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        \"relative w-full select-none touch-none py-1\",\n        disabled && \"opacity-40 pointer-events-none\",\n        className,\n      )}\n    >\n      <div\n        ref={trackRef}\n        role={isAdjustable ? \"slider\" : \"progressbar\"}\n        tabIndex={isAdjustable ? 0 : -1}\n        aria-valuenow={clampedValue}\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuetext={`${clampedValue}%`}\n        aria-label={ariaLabel || \"Progress bar\"}\n        onPointerMove={handlePointerMove}\n        onPointerDown={handlePointerDown}\n        onPointerUp={handlePointerUp}\n        onPointerCancel={handlePointerLeave}\n        onPointerLeave={handlePointerLeave}\n        onKeyDown={handleKeyDown}\n        className={cn(\n          \"relative flex w-full items-center justify-between gap-0.75 sm:gap-1 outline-none\",\n          interactive &&\n            \"cursor-pointer focus-visible:ring-2 focus-visible:ring-white/40 focus-visible:ring-offset-2 focus-visible:ring-offset-black rounded-md\",\n        )}\n        style={{ height }}\n      >\n        {Array.from({ length: segments }).map((_, idx) => {\n          const isActive = idx < activeSegmentsCount;\n\n          const activeStyle = isActive\n            ? {\n                backgroundColor: colorConfig.active,\n                boxShadow: `0 0 10px ${colorConfig.glow}`,\n                opacity: 1,\n              }\n            : {\n                backgroundColor: colorConfig.inactive,\n                opacity: 1,\n              };\n\n          return (\n            <div\n              key={idx}\n              ref={(el) => {\n                segmentRefs.current[idx] = el;\n              }}\n              style={{\n                height: \"100%\",\n                ...activeStyle,\n              }}\n              className=\"flex-1 min-w-0.75 rounded-full transition-colors duration-150 will-change-transform\"\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nexport interface SegmentedProgressMetric {\n  id: string;\n  label: string;\n  value: number;\n  trend?: string;\n  trendDirection?: \"up\" | \"down\" | \"neutral\";\n  color?: SegmentedProgressColor | string;\n  unit?: string;\n  formatValue?: (val: number) => string;\n}\n\nexport interface SegmentedProgressItemProps {\n  metric: SegmentedProgressMetric;\n  segments?: number;\n  interactive?: boolean;\n  onChange?: (val: number) => void;\n}\n\nexport function SegmentedProgressItem({\n  metric,\n  segments = 32,\n  interactive = true,\n  onChange,\n}: SegmentedProgressItemProps) {\n  const {\n    label,\n    value,\n    trend,\n    trendDirection = \"up\",\n    color = \"emerald\",\n    unit = \"%\",\n    formatValue,\n  } = metric;\n\n  const colorConfig = useMemo(() => resolveColor(color), [color]);\n  const [hoveredValue, setHoveredValue] = useState<number | null>(null);\n\n  const displayVal = hoveredValue !== null ? hoveredValue : value;\n  const formattedText = formatValue\n    ? formatValue(displayVal)\n    : `${displayVal}${unit}`;\n\n  return (\n    <div className=\"flex flex-col gap-2.5 w-full\">\n      <div className=\"flex items-center justify-between\">\n        <span className=\"text-[13px] sm:text-sm font-medium text-zinc-400\">\n          {label}\n        </span>\n        <div className=\"flex items-center gap-2.5\">\n          {trend && (\n            <div\n              className=\"flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium\"\n              style={{\n                backgroundColor: colorConfig.badgeBg,\n                color: colorConfig.badgeText,\n                border: `1px solid ${colorConfig.badgeBorder}`,\n              }}\n            >\n              <span className=\"text-xs\">\n                {trendDirection === \"up\"\n                  ? \"↑\"\n                  : trendDirection === \"down\"\n                    ? \"↓\"\n                    : \"•\"}\n              </span>\n              <span>{trend}</span>\n            </div>\n          )}\n          <span className=\"text-lg sm:text-xl font-semibold tabular-nums text-white min-w-[3.5ch] text-right inline-flex items-baseline justify-end\">\n            <RollingNumber value={displayVal} text={formattedText} />\n          </span>\n        </div>\n      </div>\n\n      <SegmentedProgressBar\n        value={value}\n        segments={segments}\n        color={color}\n        interactive={interactive}\n        onChange={onChange}\n        onHoverChange={(hoverVal) => setHoveredValue(hoverVal)}\n        ariaLabel={`${label} progress`}\n      />\n    </div>\n  );\n}\n\nexport interface SegmentedProgressProps {\n  title?: string;\n  subtitle?: string;\n  icon?: React.ReactNode;\n  color?: SegmentedProgressColor | string;\n  metrics?: SegmentedProgressMetric[];\n  segments?: number;\n  footerText?: string;\n  footerHref?: string;\n  footerAction?: () => void;\n  onMoreClick?: () => void;\n  interactive?: boolean;\n  onMetricChange?: (metricId: string, newValue: number) => void;\n  className?: string;\n}\n\nconst DEFAULT_METRICS: SegmentedProgressMetric[] = [\n  {\n    id: \"performing-progress\",\n    label: \"Performing Progress\",\n    value: 89,\n  },\n  {\n    id: \"target-sales\",\n    label: \"Target Sales\",\n    value: 67,\n  },\n];\n\nexport function SegmentedProgress({\n  title = \"Project Progress\",\n  subtitle = \"Overall completion rate all projects.\",\n  icon,\n  color = \"emerald\",\n  metrics = DEFAULT_METRICS,\n  segments = 32,\n  footerText = \"Up by 6% compared to last week, great momentum!\",\n  footerHref,\n  footerAction,\n  onMoreClick,\n  interactive = true,\n  onMetricChange,\n  className,\n}: SegmentedProgressProps) {\n  const cardRef = useRef<HTMLDivElement>(null);\n  const [valueOverrides, setValueOverrides] = useState<Record<string, number>>(\n    {},\n  );\n\n  const colorConfig = useMemo(() => resolveColor(color), [color]);\n\n  const activeMetrics = useMemo(() => {\n    return metrics.map((m) => ({\n      ...m,\n      color: m.color || color,\n      value:\n        valueOverrides[m.id] !== undefined ? valueOverrides[m.id] : m.value,\n    }));\n  }, [metrics, valueOverrides, color]);\n\n  const handleMetricChange = useCallback(\n    (id: string, val: number) => {\n      setValueOverrides((prev) => ({ ...prev, [id]: val }));\n      onMetricChange?.(id, val);\n    },\n    [onMetricChange],\n  );\n\n  return (\n    <div\n      ref={cardRef}\n      className={cn(\n        \"relative w-full max-w-105 rounded-2xl bg-[#141416] p-5 sm:p-6 text-white border border-white/8 shadow-[0_24px_50px_-12px_rgba(0,0,0,0.7)] flex flex-col gap-5 overflow-hidden\",\n        className,\n      )}\n    >\n      <div className=\"flex items-start justify-between gap-3\">\n        <div className=\"flex items-center gap-3.5\">\n          <div className=\"relative flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-linear-to-b from-[#2a2a2e] to-[#141416] border border-white/15 shadow-[inset_0_1px_1px_rgba(255,255,255,0.25),0_4px_12px_rgba(0,0,0,0.5)]\">\n            {icon || (\n              <ProgressPieIcon\n                className=\"w-5 h-5 transition-colors duration-200\"\n                style={{ color: colorConfig.active }}\n              />\n            )}\n          </div>\n          <div className=\"flex flex-col\">\n            <h3 className=\"text-[15px] sm:text-base font-semibold text-white tracking-tight leading-snug\">\n              {title}\n            </h3>\n            <p className=\"text-xs sm:text-[13px] text-zinc-400 font-normal leading-normal\">\n              {subtitle}\n            </p>\n          </div>\n        </div>\n\n        <button\n          type=\"button\"\n          onClick={onMoreClick}\n          aria-label=\"More options\"\n          className=\"rounded-lg p-1.5 text-zinc-400 hover:text-white hover:bg-white/6 transition-colors cursor-pointer outline-none focus-visible:ring-1 focus-visible:ring-white/20\"\n        >\n          <MoreHorizontal className=\"w-4 h-4\" />\n        </button>\n      </div>\n\n      <div className=\"flex flex-col gap-4\">\n        {activeMetrics.map((metric) => (\n          <SegmentedProgressItem\n            key={metric.id}\n            metric={metric}\n            segments={segments}\n            interactive={interactive}\n            onChange={(newVal) => handleMetricChange(metric.id, newVal)}\n          />\n        ))}\n      </div>\n\n      {footerText && (\n        <div\n          role={footerAction || footerHref ? \"button\" : undefined}\n          tabIndex={footerAction || footerHref ? 0 : undefined}\n          onClick={footerAction}\n          style={{\n            backgroundColor: colorConfig.badgeBg,\n            borderColor: colorConfig.badgeBorder,\n            color: colorConfig.badgeText,\n          }}\n          className={cn(\n            \"group relative flex items-center justify-between rounded-xl px-4 py-3 border transition-colors duration-200\",\n            (footerAction || footerHref) &&\n              \"cursor-pointer hover:brightness-110\",\n          )}\n        >\n          {footerHref ? (\n            <a\n              href={footerHref}\n              className=\"flex items-center justify-between w-full text-xs sm:text-[13px] font-medium leading-relaxed\"\n            >\n              <span>{footerText}</span>\n              <ArrowRight className=\"w-4 h-4 shrink-0 transition-transform duration-200 group-hover:translate-x-1\" />\n            </a>\n          ) : (\n            <>\n              <span className=\"text-xs sm:text-[13px] font-medium leading-relaxed\">\n                {footerText}\n              </span>\n              <ArrowRight className=\"w-4 h-4 shrink-0 transition-transform duration-200 group-hover:translate-x-1\" />\n            </>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default SegmentedProgress;\n",
      "type": "registry:ui",
      "target": "@ui/segmented-progress.tsx"
    }
  ],
  "type": "registry:ui",
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "Segmented Progress"
}
