{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "weight-widget",
  "type": "registry:component",
  "title": "Weight Widget",
  "description": "A tactile, sliding scale component designed for precise weight input with a premium haptic feel.",
  "dependencies": [
    "motion",
    "next-themes"
  ],
  "files": [
    {
      "path": "components/watermelon/weight-widget.tsx",
      "type": "registry:component",
      "content": "'use client';\n\nimport React, { useEffect, useState, useMemo } from 'react';\nimport {\n  motion,\n  useMotionValue,\n  useSpring,\n  useTransform,\n  type PanInfo,\n  MotionValue,\n} from 'motion/react';\nimport { useTheme } from 'next-themes';\n\ninterface WeightWidgetProps {\n  initialValue?: number;\n  min?: number;\n  max?: number;\n  onChange?: (value: number) => void;\n}\n\nexport const WeightWidget: React.FC<WeightWidgetProps> = ({\n  initialValue = 25,\n  min = 0,\n  max = 100,\n  onChange,\n}) => {\n  const { resolvedTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n  const pixelsPerUnit = 80;\n\n  const x = useMotionValue(-initialValue * pixelsPerUnit);\n  const springConfig = { bounce: 0.45 };\n  const springX = useSpring(x, springConfig);\n\n  const [displayValue, setDisplayValue] = useState(initialValue);\n\n  useEffect(() => {\n    requestAnimationFrame(() => setMounted(true));\n  }, []);\n\n  useEffect(() => {\n    const unsubscribe = springX.on('change', (latest) => {\n      const val = Math.abs(latest / pixelsPerUnit);\n      const roundedVal = Math.round(val);\n      if (roundedVal !== displayValue) {\n        setDisplayValue(roundedVal);\n        if (onChange) onChange(roundedVal);\n      }\n    });\n    return () => unsubscribe();\n  }, [springX, pixelsPerUnit, onChange, displayValue]);\n\n  const dragStartX = React.useRef(x.get());\n\n  const handlePanStart = () => {\n    dragStartX.current = x.get();\n  };\n\n  const handlePan = (_: any, info: PanInfo) => {\n    // Restrict visual drag movement to roughly one number space so it doesn't run away\n    const maxOffset = pixelsPerUnit;\n    const boundedOffset = Math.max(\n      -maxOffset,\n      Math.min(maxOffset, info.offset.x * 0.6),\n    );\n    const newX = dragStartX.current + boundedOffset;\n\n    const minX = -max * pixelsPerUnit;\n    const maxX = -min * pixelsPerUnit;\n    x.set(Math.max(minX, Math.min(maxX, newX)));\n  };\n\n  const handlePanEnd = (_: any, info: PanInfo) => {\n    const baseValue = Math.round(dragStartX.current / -pixelsPerUnit);\n    let direction = 0;\n\n    if (info.offset.x < -20 || info.velocity.x < -100) direction = 1;\n    else if (info.offset.x > 20 || info.velocity.x > 100) direction = -1;\n\n    const targetValue = Math.max(min, Math.min(max, baseValue + direction));\n    x.set(-targetValue * pixelsPerUnit);\n  };\n\n  const visibleRange = useMemo(() => {\n    const items = [];\n    const buffer = 5;\n    for (\n      let i = Math.max(min, displayValue - buffer);\n      i <= Math.min(max, displayValue + buffer);\n      i += 0.5\n    ) {\n      items.push(i);\n    }\n    return items;\n  }, [min, max, displayValue]);\n\n  if (!mounted) return null;\n\n  const isDark = resolvedTheme === 'dark';\n\n  return (\n    <div className=\"relative flex h-[220px] w-[220px] touch-none flex-col items-center overflow-hidden rounded-[28px] border-2 border-[#F0F0F0] bg-white font-sans shadow-lg transition-colors duration-300 select-none sm:h-[260px] sm:w-[260px] sm:rounded-[36px] dark:border-[#1E1E21] dark:bg-[#121214]\">\n      <div className=\"mt-5 text-base font-semibold tracking-wide text-[#94A3B8] capitalize transition-colors sm:mt-6 sm:text-xl dark:text-[#475569]\">\n        Weight\n      </div>\n\n      <div className=\"relative flex w-full flex-1 items-start justify-center\">\n        {/* Sliding Numbers Layer */}\n        <motion.div\n          onPanStart={handlePanStart}\n          onPan={handlePan}\n          onPanEnd={handlePanEnd}\n          className=\"absolute flex h-full w-full cursor-grab items-start active:cursor-grabbing\"\n          style={{ x: springX, left: '50%' }}\n        >\n          {visibleRange.map((i) => (\n            <DialItem\n              key={i}\n              value={i}\n              pixelsPerUnit={pixelsPerUnit}\n              scrollX={springX}\n              isDark={isDark}\n            />\n          ))}\n        </motion.div>\n\n        {/* Static Indicator */}\n        <div className=\"pointer-events-none absolute bottom-0 z-20 mb-1 flex flex-col items-center sm:mb-0\">\n          <div className=\"mb-1.5 h-[5px] w-[5px] rounded-full bg-black transition-colors sm:h-[6.5px] sm:w-[6.5px] dark:bg-white\" />\n          <svg\n            className=\"h-6 w-2 text-black transition-colors sm:h-9 sm:w-[10px] dark:text-white\"\n            viewBox=\"0 0 10 36\"\n            fill=\"none\"\n            preserveAspectRatio=\"none\"\n          >\n            <path\n              d=\"M 5 2 L 9 36 L 1 36 Z\"\n              fill=\"currentColor\"\n              stroke=\"currentColor\"\n              strokeWidth=\"2\"\n              strokeLinejoin=\"round\"\n            />\n          </svg>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst DialItem: React.FC<{\n  value: number;\n  pixelsPerUnit: number;\n  scrollX: MotionValue<number>;\n  isDark: boolean;\n}> = ({ value, pixelsPerUnit, scrollX, isDark }) => {\n  const isHalf = value % 1 !== 0;\n  const itemX = value * pixelsPerUnit;\n  const distance = useTransform(scrollX, (s: number) => Math.abs(s + itemX));\n\n  const opacity = useTransform(\n    distance,\n    [0, pixelsPerUnit * 2, pixelsPerUnit * 3],\n    [1, 0.4, 0],\n  );\n\n  const color = useTransform(\n    distance,\n    [0, pixelsPerUnit],\n    isDark ? ['#F8FAFC', '#334155'] : ['#25262B', '#CBD5E1'],\n  );\n\n  const scale = useTransform(distance, [0, pixelsPerUnit * 2], [1, 0.85]);\n\n  // Use a quadratic curve for true circular appearance instead of linear\n  const yOffset = useTransform(\n    distance,\n    [\n      0,\n      pixelsPerUnit * 0.5,\n      pixelsPerUnit,\n      pixelsPerUnit * 1.5,\n      pixelsPerUnit * 2,\n      pixelsPerUnit * 2.5,\n      pixelsPerUnit * 3,\n    ],\n    [0, 2, 7, 17, 32, 54, 88],\n  );\n\n  const rotate = useTransform(scrollX, (s: number) => {\n    const d = s + itemX;\n    return (d / pixelsPerUnit) * 12;\n  });\n\n  return (\n    <motion.div\n      className=\"absolute top-0 flex flex-col items-center\"\n      style={{\n        left: itemX,\n        x: '-50%',\n        opacity,\n        scale,\n        y: yOffset,\n        rotate,\n        transformOrigin: 'center 140px', // Adjusted for smaller card\n      }}\n    >\n      <motion.span\n        className={`text-[56px] font-bold tracking-tight sm:text-[68px] ${isHalf ? 'invisible' : ''}`}\n        style={{ color }}\n      >\n        {Math.floor(value)}\n      </motion.span>\n\n      <div className=\"mt-2 flex flex-col items-center sm:mt-4\">\n        <div\n          className={`h-5 w-[2.5px] rounded-full transition-colors sm:h-7 sm:w-[3px] ${isDark ? 'bg-[#2D2D30]' : 'bg-[#D6D5E1]'}`}\n        />\n      </div>\n    </motion.div>\n  );\n};\n"
    }
  ]
}
