{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "weather-card",
  "type": "registry:ui",
  "title": "Weather Card",
  "author": "Rishabh Bose <https://theb0se.vercel.app/>",
  "description": "Dark-themed weather card featuring interactive metric tabs, a custom Catmull-Rom smooth SVG area chart with hover cursor guides, and an 8-day forecast selector with sliding selection physics.",
  "dependencies": [
    "framer-motion",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/ui/weather-card.tsx",
      "content": "\"use client\";\n\nimport React, { useState, useId, useMemo, useRef, useEffect } from \"react\";\nimport {\n  motion,\n  AnimatePresence,\n  useSpring,\n  useTransform,\n  useReducedMotion,\n  type MotionValue,\n} from \"framer-motion\";\nimport {\n  Cloud,\n  CloudRain,\n  CloudSun,\n  CloudLightning,\n  Sun,\n  Moon,\n  Wind as WindIcon,\n  Droplets,\n  Sparkles,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ------------------------------------------------------------------ */\n/* Types & Interfaces                                                 */\n/* ------------------------------------------------------------------ */\n\nexport type TemperatureUnit = \"F\" | \"C\";\nexport type WeatherMetricTab = \"temperature\" | \"precipitation\" | \"wind\";\n\nexport type WeatherCondition =\n  | \"clear-night\"\n  | \"sunny\"\n  | \"partly-cloudy\"\n  | \"cloudy\"\n  | \"rain\"\n  | \"sun-rain\"\n  | \"thunderstorm\"\n  | \"windy\";\n\nexport interface HourlyDataPoint {\n  time: string; // e.g. \"2 AM\"\n  temperature: number; // in Fahrenheit\n  precipitation: number; // in %\n  wind: number; // in mph\n}\n\nexport interface DailyForecast {\n  day: string; // e.g. \"Wed\"\n  dateLabel: string; // e.g. \"Wednesday 1:00 AM\"\n  condition: WeatherCondition;\n  conditionLabel: string; // e.g. \"Clear night\"\n  highTemp: number; // in °F\n  lowTemp: number; // in °F\n  currentTemp: number; // in °F\n  precipitation: number; // %\n  humidity: number; // %\n  windSpeed: number; // mph\n  hourly: HourlyDataPoint[];\n}\n\nexport interface WeatherCardProps {\n  location?: string;\n  initialUnit?: TemperatureUnit;\n  daily?: DailyForecast[];\n  className?: string;\n}\n\n/* ------------------------------------------------------------------ */\n/* Unit Conversion Helpers                                            */\n/* ------------------------------------------------------------------ */\n\nexport function toCelsius(fahrenheit: number): number {\n  return Math.round(((fahrenheit - 32) * 5) / 9);\n}\n\nexport function formatTemp(fahrenheit: number, unit: TemperatureUnit): number {\n  return unit === \"C\" ? toCelsius(fahrenheit) : Math.round(fahrenheit);\n}\n\nexport function formatWind(mph: number, unit: TemperatureUnit): string {\n  if (unit === \"C\") {\n    const kmh = Math.round(mph * 1.60934);\n    return `${kmh} km/h`;\n  }\n  return `${mph} mph`;\n}\n\n/* ------------------------------------------------------------------ */\n/* Catmull-Rom / Monotone Smooth SVG Spline Path Generator            */\n/* ------------------------------------------------------------------ */\n\nexport function buildSmoothPath(\n  points: { x: number; y: number }[],\n  height: number,\n  closeArea = false\n): string {\n  if (points.length === 0) return \"\";\n  if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;\n\n  let d = `M ${points[0].x.toFixed(2)} ${points[0].y.toFixed(2)}`;\n\n  for (let i = 0; i < points.length - 1; i++) {\n    const p0 = points[Math.max(0, i - 1)];\n    const p1 = points[i];\n    const p2 = points[i + 1];\n    const p3 = points[Math.min(points.length - 1, i + 2)];\n\n    // Curvature tension coefficient\n    const tension = 0.2;\n    const cp1x = p1.x + (p2.x - p0.x) * tension;\n    const cp1y = p1.y + (p2.y - p0.y) * tension;\n    const cp2x = p2.x - (p3.x - p1.x) * tension;\n    const cp2y = p2.y - (p3.y - p1.y) * tension;\n\n    d += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(\n      2\n    )} ${cp2y.toFixed(2)}, ${p2.x.toFixed(2)} ${p2.y.toFixed(2)}`;\n  }\n\n  if (closeArea) {\n    const last = points[points.length - 1];\n    const first = points[0];\n    d += ` L ${last.x.toFixed(2)} ${height.toFixed(2)}`;\n    d += ` L ${first.x.toFixed(2)} ${height.toFixed(2)} Z`;\n  }\n\n  return d;\n}\n\n/* ------------------------------------------------------------------ */\n/* Default Mock Data (8-Day Weather Model)                            */\n/* ------------------------------------------------------------------ */\n\nexport const DEFAULT_HOURLY_BASE: HourlyDataPoint[] = [\n  { time: \"2 AM\", temperature: 67, precipitation: 0, wind: 8 },\n  { time: \"5 AM\", temperature: 64, precipitation: 0, wind: 7 },\n  { time: \"8 AM\", temperature: 70, precipitation: 10, wind: 9 },\n  { time: \"11 AM\", temperature: 78, precipitation: 0, wind: 11 },\n  { time: \"2 PM\", temperature: 82, precipitation: 0, wind: 14 },\n  { time: \"5 PM\", temperature: 79, precipitation: 5, wind: 12 },\n  { time: \"8 PM\", temperature: 72, precipitation: 25, wind: 10 },\n  { time: \"11 PM\", temperature: 68, precipitation: 5, wind: 8 },\n];\n\nexport const DEFAULT_DAILY_FORECAST: DailyForecast[] = [\n  {\n    day: \"Wed\",\n    dateLabel: \"Wednesday 1:00 AM\",\n    condition: \"clear-night\",\n    conditionLabel: \"Clear night\",\n    highTemp: 82,\n    lowTemp: 61,\n    currentTemp: 67,\n    precipitation: 0,\n    humidity: 67,\n    windSpeed: 8,\n    hourly: [\n      { time: \"2 AM\", temperature: 67, precipitation: 0, wind: 8 },\n      { time: \"5 AM\", temperature: 64, precipitation: 0, wind: 7 },\n      { time: \"8 AM\", temperature: 70, precipitation: 10, wind: 9 },\n      { time: \"11 AM\", temperature: 78, precipitation: 0, wind: 11 },\n      { time: \"2 PM\", temperature: 82, precipitation: 0, wind: 14 },\n      { time: \"5 PM\", temperature: 79, precipitation: 5, wind: 12 },\n      { time: \"8 PM\", temperature: 72, precipitation: 25, wind: 10 },\n      { time: \"11 PM\", temperature: 68, precipitation: 5, wind: 8 },\n    ],\n  },\n  {\n    day: \"Thu\",\n    dateLabel: \"Thursday 1:00 AM\",\n    condition: \"partly-cloudy\",\n    conditionLabel: \"Partly cloudy\",\n    highTemp: 79,\n    lowTemp: 59,\n    currentTemp: 65,\n    precipitation: 15,\n    humidity: 62,\n    windSpeed: 10,\n    hourly: [\n      { time: \"2 AM\", temperature: 65, precipitation: 10, wind: 8 },\n      { time: \"5 AM\", temperature: 62, precipitation: 10, wind: 9 },\n      { time: \"8 AM\", temperature: 68, precipitation: 20, wind: 11 },\n      { time: \"11 AM\", temperature: 75, precipitation: 15, wind: 12 },\n      { time: \"2 PM\", temperature: 79, precipitation: 10, wind: 13 },\n      { time: \"5 PM\", temperature: 76, precipitation: 15, wind: 11 },\n      { time: \"8 PM\", temperature: 70, precipitation: 20, wind: 9 },\n      { time: \"11 PM\", temperature: 66, precipitation: 10, wind: 8 },\n    ],\n  },\n  {\n    day: \"Fri\",\n    dateLabel: \"Friday 1:00 AM\",\n    condition: \"cloudy\",\n    conditionLabel: \"Mostly cloudy\",\n    highTemp: 74,\n    lowTemp: 58,\n    currentTemp: 63,\n    precipitation: 40,\n    humidity: 78,\n    windSpeed: 12,\n    hourly: [\n      { time: \"2 AM\", temperature: 63, precipitation: 30, wind: 10 },\n      { time: \"5 AM\", temperature: 60, precipitation: 45, wind: 12 },\n      { time: \"8 AM\", temperature: 65, precipitation: 60, wind: 14 },\n      { time: \"11 AM\", temperature: 71, precipitation: 50, wind: 15 },\n      { time: \"2 PM\", temperature: 74, precipitation: 35, wind: 14 },\n      { time: \"5 PM\", temperature: 71, precipitation: 40, wind: 12 },\n      { time: \"8 PM\", temperature: 66, precipitation: 30, wind: 11 },\n      { time: \"11 PM\", temperature: 64, precipitation: 25, wind: 10 },\n    ],\n  },\n  {\n    day: \"Sat\",\n    dateLabel: \"Saturday 1:00 AM\",\n    condition: \"thunderstorm\",\n    conditionLabel: \"Scattered thunderstorms\",\n    highTemp: 71,\n    lowTemp: 54,\n    currentTemp: 60,\n    precipitation: 80,\n    humidity: 89,\n    windSpeed: 16,\n    hourly: [\n      { time: \"2 AM\", temperature: 60, precipitation: 65, wind: 14 },\n      { time: \"5 AM\", temperature: 57, precipitation: 85, wind: 16 },\n      { time: \"8 AM\", temperature: 62, precipitation: 90, wind: 18 },\n      { time: \"11 AM\", temperature: 68, precipitation: 75, wind: 19 },\n      { time: \"2 PM\", temperature: 71, precipitation: 80, wind: 20 },\n      { time: \"5 PM\", temperature: 67, precipitation: 70, wind: 17 },\n      { time: \"8 PM\", temperature: 63, precipitation: 50, wind: 15 },\n      { time: \"11 PM\", temperature: 59, precipitation: 35, wind: 12 },\n    ],\n  },\n  {\n    day: \"Sun\",\n    dateLabel: \"Sunday 1:00 AM\",\n    condition: \"rain\",\n    conditionLabel: \"Showers & light rain\",\n    highTemp: 69,\n    lowTemp: 52,\n    currentTemp: 58,\n    precipitation: 65,\n    humidity: 82,\n    windSpeed: 11,\n    hourly: [\n      { time: \"2 AM\", temperature: 58, precipitation: 50, wind: 9 },\n      { time: \"5 AM\", temperature: 55, precipitation: 65, wind: 11 },\n      { time: \"8 AM\", temperature: 61, precipitation: 75, wind: 13 },\n      { time: \"11 AM\", temperature: 66, precipitation: 60, wind: 14 },\n      { time: \"2 PM\", temperature: 69, precipitation: 45, wind: 12 },\n      { time: \"5 PM\", temperature: 65, precipitation: 40, wind: 11 },\n      { time: \"8 PM\", temperature: 61, precipitation: 30, wind: 9 },\n      { time: \"11 PM\", temperature: 57, precipitation: 20, wind: 8 },\n    ],\n  },\n  {\n    day: \"Mon\",\n    dateLabel: \"Monday 1:00 AM\",\n    condition: \"cloudy\",\n    conditionLabel: \"Overcast\",\n    highTemp: 76,\n    lowTemp: 57,\n    currentTemp: 64,\n    precipitation: 20,\n    humidity: 71,\n    windSpeed: 9,\n    hourly: [\n      { time: \"2 AM\", temperature: 64, precipitation: 20, wind: 8 },\n      { time: \"5 AM\", temperature: 61, precipitation: 25, wind: 8 },\n      { time: \"8 AM\", temperature: 67, precipitation: 20, wind: 9 },\n      { time: \"11 AM\", temperature: 73, precipitation: 15, wind: 11 },\n      { time: \"2 PM\", temperature: 76, precipitation: 10, wind: 12 },\n      { time: \"5 PM\", temperature: 72, precipitation: 15, wind: 10 },\n      { time: \"8 PM\", temperature: 68, precipitation: 20, wind: 8 },\n      { time: \"11 PM\", temperature: 65, precipitation: 15, wind: 7 },\n    ],\n  },\n  {\n    day: \"Tue\",\n    dateLabel: \"Tuesday 1:00 AM\",\n    condition: \"sun-rain\",\n    conditionLabel: \"Passing sun showers\",\n    highTemp: 80,\n    lowTemp: 60,\n    currentTemp: 66,\n    precipitation: 30,\n    humidity: 68,\n    windSpeed: 10,\n    hourly: [\n      { time: \"2 AM\", temperature: 66, precipitation: 15, wind: 8 },\n      { time: \"5 AM\", temperature: 63, precipitation: 20, wind: 8 },\n      { time: \"8 AM\", temperature: 70, precipitation: 35, wind: 10 },\n      { time: \"11 AM\", temperature: 77, precipitation: 40, wind: 12 },\n      { time: \"2 PM\", temperature: 80, precipitation: 25, wind: 13 },\n      { time: \"5 PM\", temperature: 76, precipitation: 20, wind: 11 },\n      { time: \"8 PM\", temperature: 71, precipitation: 15, wind: 9 },\n      { time: \"11 PM\", temperature: 67, precipitation: 10, wind: 8 },\n    ],\n  },\n  {\n    day: \"Wed\",\n    dateLabel: \"Next Wednesday 1:00 AM\",\n    condition: \"sunny\",\n    conditionLabel: \"Sunny & clear\",\n    highTemp: 84,\n    lowTemp: 63,\n    currentTemp: 70,\n    precipitation: 0,\n    humidity: 55,\n    windSpeed: 8,\n    hourly: [\n      { time: \"2 AM\", temperature: 70, precipitation: 0, wind: 7 },\n      { time: \"5 AM\", temperature: 66, precipitation: 0, wind: 6 },\n      { time: \"8 AM\", temperature: 73, precipitation: 0, wind: 8 },\n      { time: \"11 AM\", temperature: 80, precipitation: 0, wind: 10 },\n      { time: \"2 PM\", temperature: 84, precipitation: 0, wind: 11 },\n      { time: \"5 PM\", temperature: 81, precipitation: 0, wind: 9 },\n      { time: \"8 PM\", temperature: 75, precipitation: 0, wind: 8 },\n      { time: \"11 PM\", temperature: 71, precipitation: 0, wind: 7 },\n    ],\n  },\n];\n\n/* ------------------------------------------------------------------ */\n/* Subcomponent: High-Fidelity Weather Icon Representation             */\n/* ------------------------------------------------------------------ */\n\nexport function WeatherIcon({\n  condition,\n  className,\n  size = 28,\n}: {\n  condition: WeatherCondition;\n  className?: string;\n  size?: number;\n}) {\n  switch (condition) {\n    case \"clear-night\":\n      return (\n        <div\n          className={cn(\n            \"relative flex items-center justify-center select-none text-amber-300\",\n            className\n          )}\n          style={{ width: size, height: size }}\n        >\n          <Moon\n            style={{ width: size * 0.85, height: size * 0.85 }}\n            className=\"fill-amber-300/20 stroke-amber-300 drop-shadow-[0_0_12px_rgba(252,211,77,0.4)]\"\n            strokeWidth={1.8}\n          />\n          <Sparkles\n            style={{ width: size * 0.42, height: size * 0.42 }}\n            className=\"absolute -top-1 -right-1 text-amber-200 fill-amber-200 animate-pulse\"\n          />\n        </div>\n      );\n\n    case \"sunny\":\n      return (\n        <div\n          className={cn(\n            \"relative flex items-center justify-center select-none text-amber-400\",\n            className\n          )}\n          style={{ width: size, height: size }}\n        >\n          <Sun\n            style={{ width: size * 0.9, height: size * 0.9 }}\n            className=\"fill-amber-400/20 stroke-amber-400 drop-shadow-[0_0_14px_rgba(251,191,36,0.5)]\"\n            strokeWidth={1.8}\n          />\n        </div>\n      );\n\n    case \"partly-cloudy\":\n      return (\n        <div\n          className={cn(\n            \"relative flex items-center justify-center select-none text-sky-200\",\n            className\n          )}\n          style={{ width: size, height: size }}\n        >\n          <CloudSun\n            style={{ width: size * 0.9, height: size * 0.9 }}\n            className=\"stroke-sky-200 drop-shadow-[0_0_8px_rgba(56,189,248,0.3)]\"\n            strokeWidth={1.8}\n          />\n        </div>\n      );\n\n    case \"cloudy\":\n      return (\n        <div\n          className={cn(\n            \"relative flex items-center justify-center select-none text-neutral-300\",\n            className\n          )}\n          style={{ width: size, height: size }}\n        >\n          <Cloud\n            style={{ width: size * 0.9, height: size * 0.9 }}\n            className=\"fill-neutral-300/15 stroke-neutral-300 drop-shadow-[0_0_8px_rgba(255,255,255,0.15)]\"\n            strokeWidth={1.8}\n          />\n        </div>\n      );\n\n    case \"rain\":\n      return (\n        <div\n          className={cn(\n            \"relative flex items-center justify-center select-none text-sky-400\",\n            className\n          )}\n          style={{ width: size, height: size }}\n        >\n          <CloudRain\n            style={{ width: size * 0.9, height: size * 0.9 }}\n            className=\"fill-sky-400/10 stroke-sky-400 drop-shadow-[0_0_10px_rgba(56,189,248,0.4)]\"\n            strokeWidth={1.8}\n          />\n        </div>\n      );\n\n    case \"sun-rain\":\n      return (\n        <div\n          className={cn(\n            \"relative flex items-center justify-center select-none text-amber-300\",\n            className\n          )}\n          style={{ width: size, height: size }}\n        >\n          <CloudSun\n            style={{ width: size * 0.88, height: size * 0.88 }}\n            className=\"stroke-amber-300\"\n            strokeWidth={1.8}\n          />\n          <Droplets\n            style={{ width: size * 0.38, height: size * 0.38 }}\n            className=\"absolute -bottom-0.5 right-0 text-sky-400 fill-sky-400\"\n          />\n        </div>\n      );\n\n    case \"thunderstorm\":\n      return (\n        <div\n          className={cn(\n            \"relative flex items-center justify-center select-none text-purple-300\",\n            className\n          )}\n          style={{ width: size, height: size }}\n        >\n          <CloudLightning\n            style={{ width: size * 0.9, height: size * 0.9 }}\n            className=\"stroke-purple-300 drop-shadow-[0_0_12px_rgba(192,132,252,0.5)]\"\n            strokeWidth={1.8}\n          />\n        </div>\n      );\n\n    case \"windy\":\n    default:\n      return (\n        <div\n          className={cn(\n            \"relative flex items-center justify-center select-none text-teal-300\",\n            className\n          )}\n          style={{ width: size, height: size }}\n        >\n          <WindIcon\n            style={{ width: size * 0.9, height: size * 0.9 }}\n            className=\"stroke-teal-300\"\n            strokeWidth={1.8}\n          />\n        </div>\n      );\n  }\n}\n\n/* ------------------------------------------------------------------ */\n/* Subcomponent: Animated Rolling Temperature Number                  */\n/* ------------------------------------------------------------------ */\n\nexport function AnimatedTemperature({ value }: { value: number }) {\n  const shouldReduceMotion = useReducedMotion();\n  const spring = useSpring(value, { stiffness: 120, damping: 18 });\n  const display = useTransform(spring, (latest) => Math.round(latest));\n  const [currentVal, setCurrentVal] = useState(value);\n\n  useEffect(() => {\n    spring.set(value);\n  }, [value, spring]);\n\n  useEffect(() => {\n    const unsubscribe = display.on(\"change\", (latest) => {\n      setCurrentVal(latest);\n    });\n    return () => unsubscribe();\n  }, [display]);\n\n  if (shouldReduceMotion) {\n    return <span>{value}</span>;\n  }\n\n  return <span>{currentVal}</span>;\n}\n\n/* ------------------------------------------------------------------ */\n/* Subcomponent: Weather Header                                       */\n/* ------------------------------------------------------------------ */\n\nexport interface WeatherHeaderProps {\n  location: string;\n  dayData: DailyForecast;\n  unit: TemperatureUnit;\n  onToggleUnit: (newUnit: TemperatureUnit) => void;\n}\n\nexport function WeatherHeader({\n  location,\n  dayData,\n  unit,\n  onToggleUnit,\n}: WeatherHeaderProps) {\n  const currentTemp = formatTemp(dayData.currentTemp, unit);\n\n  return (\n    <div className=\"flex flex-col sm:flex-row sm:items-start justify-between gap-4 pb-4\">\n      {/* Left side: Weather icon, big temperature, unit toggle, 3-line stat block */}\n      <div className=\"flex items-center flex-wrap sm:flex-nowrap gap-4 sm:gap-5\">\n        {/* Large 64px weather illustration */}\n        <AnimatePresence mode=\"wait\">\n          <motion.div\n            key={dayData.condition}\n            initial={{ opacity: 0, scale: 0.88 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.88 }}\n            transition={{ duration: 0.25 }}\n            className=\"flex-shrink-0\"\n          >\n            <WeatherIcon condition={dayData.condition} size={64} />\n          </motion.div>\n        </AnimatePresence>\n\n        {/* Big Temperature & Unit Toggle */}\n        <div className=\"flex items-start\">\n          <span className=\"text-5xl sm:text-6xl font-semibold tracking-tight text-white font-sans tabular-nums\">\n            <AnimatedTemperature value={currentTemp} />\n          </span>\n\n          {/* Unit Toggle Buttons: °F | °C */}\n          <div\n            role=\"group\"\n            aria-label=\"Temperature unit selection\"\n            className=\"ml-2 flex items-center text-sm font-medium mt-1 select-none\"\n          >\n            <button\n              type=\"button\"\n              aria-pressed={unit === \"F\"}\n              onClick={() => onToggleUnit(\"F\")}\n              className={cn(\n                \"transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-amber-400 rounded\",\n                unit === \"F\"\n                  ? \"text-white font-semibold\"\n                  : \"text-neutral-500 hover:text-neutral-300\"\n              )}\n            >\n              °F\n            </button>\n            <span className=\"mx-1 text-neutral-600 font-light select-none\">|</span>\n            <button\n              type=\"button\"\n              aria-pressed={unit === \"C\"}\n              onClick={() => onToggleUnit(\"C\")}\n              className={cn(\n                \"transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-amber-400 rounded\",\n                unit === \"C\"\n                  ? \"text-white font-semibold\"\n                  : \"text-neutral-500 hover:text-neutral-300\"\n              )}\n            >\n              °C\n            </button>\n          </div>\n        </div>\n\n        {/* 3-Line Stat Block */}\n        <div className=\"text-xs text-neutral-400 space-y-0.5 border-l border-white/10 pl-4 py-0.5 select-none\">\n          <p>\n            Precipitation:{\" \"}\n            <span className=\"text-neutral-200 font-medium\">\n              {dayData.precipitation}%\n            </span>\n          </p>\n          <p>\n            Humidity:{\" \"}\n            <span className=\"text-neutral-200 font-medium\">\n              {dayData.humidity}%\n            </span>\n          </p>\n          <p>\n            Wind:{\" \"}\n            <span className=\"text-neutral-200 font-medium\">\n              {formatWind(dayData.windSpeed, unit)}\n            </span>\n          </p>\n        </div>\n      </div>\n\n      {/* Right side: City and Date info */}\n      <AnimatePresence mode=\"wait\">\n        <motion.div\n          key={dayData.day}\n          initial={{ opacity: 0, y: -4 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={{ opacity: 0, y: 4 }}\n          transition={{ duration: 0.2 }}\n          className=\"text-left sm:text-right flex-shrink-0\"\n        >\n          <h2 className=\"text-xl sm:text-2xl font-medium tracking-tight text-white\">\n            {location}\n          </h2>\n          <p className=\"text-xs text-neutral-400 mt-0.5\">{dayData.dateLabel}</p>\n          <p className=\"text-xs text-neutral-400 capitalize\">\n            {dayData.conditionLabel}\n          </p>\n        </motion.div>\n      </AnimatePresence>\n    </div>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Subcomponent: Weather Metric Tabs                                  */\n/* ------------------------------------------------------------------ */\n\nexport interface WeatherTabsProps {\n  activeTab: WeatherMetricTab;\n  onSelectTab: (tab: WeatherMetricTab) => void;\n}\n\nexport function WeatherTabs({ activeTab, onSelectTab }: WeatherTabsProps) {\n  const tabs: { id: WeatherMetricTab; label: string }[] = [\n    { id: \"temperature\", label: \"Temperature\" },\n    { id: \"precipitation\", label: \"Precipitation\" },\n    { id: \"wind\", label: \"Wind\" },\n  ];\n\n  return (\n    <div\n      role=\"tablist\"\n      aria-label=\"Weather metrics\"\n      className=\"flex items-center gap-6 sm:gap-8 pt-3 pb-2 border-t border-white/10 select-none\"\n    >\n      {tabs.map((tab) => {\n        const isActive = activeTab === tab.id;\n        return (\n          <button\n            key={tab.id}\n            role=\"tab\"\n            aria-selected={isActive}\n            tabIndex={isActive ? 0 : -1}\n            onClick={() => onSelectTab(tab.id)}\n            className={cn(\n              \"relative pb-2 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-amber-400 rounded-t\",\n              isActive ? \"text-white\" : \"text-neutral-400 hover:text-neutral-200\"\n            )}\n          >\n            {tab.label}\n            {isActive && (\n              <motion.div\n                layoutId=\"weather-active-tab-underline\"\n                transition={{ type: \"spring\", stiffness: 380, damping: 30 }}\n                className=\"absolute bottom-0 inset-x-0 h-[2px] bg-amber-400 shadow-[0_0_8px_rgba(251,191,36,0.6)]\"\n              />\n            )}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Subcomponent: Smooth Area Chart with Spring Cursor Guide           */\n/* ------------------------------------------------------------------ */\n\nexport interface WeatherChartProps {\n  hourly: HourlyDataPoint[];\n  metric: WeatherMetricTab;\n  unit: TemperatureUnit;\n}\n\nexport function WeatherChart({ hourly, metric, unit }: WeatherChartProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const rawId = useId();\n  const gradientId = `chart-area-grad-${rawId.replace(/:/g, \"\")}`;\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  // Hover state\n  const [hoverIndex, setHoverIndex] = useState<number | null>(null);\n\n  // Compute numerical series values\n  const seriesValues = useMemo(() => {\n    return hourly.map((p) => {\n      if (metric === \"temperature\") return formatTemp(p.temperature, unit);\n      if (metric === \"precipitation\") return p.precipitation;\n      return p.wind;\n    });\n  }, [hourly, metric, unit]);\n\n  // Label formatting function\n  const formatValueLabel = (val: number, isFirst: boolean) => {\n    if (metric === \"temperature\") return `${val}°`;\n    if (metric === \"precipitation\") return `${val}%`;\n    return `${val} mph`;\n  };\n\n  // Dimensions\n  const svgWidth = 600;\n  const svgHeight = 150;\n  const paddingX = 36;\n  const paddingTop = 30;\n  const paddingBottom = 26;\n\n  const minVal = Math.min(...seriesValues);\n  const maxVal = Math.max(...seriesValues);\n  const range = Math.max(1, maxVal - minVal);\n\n  // Coordinate mapping for each data point\n  const points = useMemo(() => {\n    const count = seriesValues.length;\n    const stepX = (svgWidth - paddingX * 2) / (count - 1);\n\n    return seriesValues.map((val, i) => {\n      const x = paddingX + i * stepX;\n      // Invert Y so highest value is near paddingTop\n      const normalized = (val - minVal) / range;\n      const y =\n        svgHeight -\n        paddingBottom -\n        normalized * (svgHeight - paddingTop - paddingBottom);\n      return { x, y, value: val, time: hourly[i].time };\n    });\n  }, [seriesValues, hourly, minVal, range]);\n\n  // Line path & Area path\n  const linePath = useMemo(() => buildSmoothPath(points, svgHeight, false), [points]);\n  const areaPath = useMemo(() => buildSmoothPath(points, svgHeight, true), [points]);\n\n  // Pointer move handler to track nearest data point\n  const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    const rect = containerRef.current?.getBoundingClientRect();\n    if (!rect) return;\n    const clientX = e.clientX - rect.left;\n    const ratio = Math.max(0, Math.min(1, clientX / rect.width));\n    const rawIndex = ratio * (points.length - 1);\n    const nearest = Math.round(rawIndex);\n    setHoverIndex(nearest);\n  };\n\n  const handlePointerLeave = () => {\n    setHoverIndex(null);\n  };\n\n  const activePoint = hoverIndex !== null ? points[hoverIndex] : null;\n\n  return (\n    <div\n      ref={containerRef}\n      onPointerMove={handlePointerMove}\n      onPointerLeave={handlePointerLeave}\n      className=\"relative w-full h-[160px] select-none my-2 cursor-crosshair overflow-visible\"\n    >\n      <svg\n        viewBox={`0 0 ${svgWidth} ${svgHeight}`}\n        className=\"w-full h-full overflow-visible\"\n        preserveAspectRatio=\"none\"\n      >\n        <defs>\n          <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n            <stop offset=\"0%\" stopColor=\"#0ea5e9\" stopOpacity=\"0.28\" />\n            <stop offset=\"60%\" stopColor=\"#0ea5e9\" stopOpacity=\"0.08\" />\n            <stop offset=\"100%\" stopColor=\"#0ea5e9\" stopOpacity=\"0.0\" />\n          </linearGradient>\n        </defs>\n\n        {/* Shaded Area Fill */}\n        <motion.path\n          key={`area-${metric}`}\n          d={areaPath}\n          fill={`url(#${gradientId})`}\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={{ duration: shouldReduceMotion ? 0 : 0.6, delay: 0.15 }}\n        />\n\n        {/* Sky-400 Smooth Curve Line */}\n        <motion.path\n          key={`line-${metric}`}\n          d={linePath}\n          fill=\"none\"\n          stroke=\"#0ea5e9\"\n          strokeWidth=\"2.2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          initial={{ pathLength: shouldReduceMotion ? 1 : 0 }}\n          animate={{ pathLength: 1 }}\n          transition={{ duration: shouldReduceMotion ? 0 : 0.75, ease: \"easeOut\" }}\n        />\n\n        {/* Vertical Guide Line on Hover */}\n        {activePoint && (\n          <g>\n            <line\n              x1={activePoint.x}\n              y1={paddingTop - 12}\n              x2={activePoint.x}\n              y2={svgHeight - 4}\n              stroke=\"#0ea5e9\"\n              strokeWidth=\"1.2\"\n              strokeDasharray=\"3 3\"\n              opacity=\"0.6\"\n            />\n            {/* Glowing circle at data point */}\n            <circle\n              cx={activePoint.x}\n              cy={activePoint.y}\n              r=\"4.5\"\n              fill=\"#ffffff\"\n              stroke=\"#0ea5e9\"\n              strokeWidth=\"2\"\n              className=\"drop-shadow-[0_0_8px_rgba(14,165,233,0.9)]\"\n            />\n          </g>\n        )}\n      </svg>\n\n      {/* Floating Dynamic Tooltip following active point */}\n      <AnimatePresence>\n        {activePoint && (\n          <motion.div\n            initial={{ opacity: 0, y: -6, scale: 0.95 }}\n            animate={{ opacity: 1, y: 0, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.95 }}\n            transition={{ duration: 0.15 }}\n            style={{\n              left: `${(activePoint.x / svgWidth) * 100}%`,\n              top: `${Math.max(6, (activePoint.y / svgHeight) * 100 - 32)}%`,\n            }}\n            className=\"pointer-events-none absolute -translate-x-1/2 -translate-y-full z-30 flex flex-col items-center\"\n          >\n            <div className=\"rounded-lg border border-sky-400/40 bg-[#121214]/95 px-2.5 py-1 text-center shadow-lg backdrop-blur-md\">\n              <span className=\"text-[10px] text-neutral-400 font-mono block\">\n                {activePoint.time}\n              </span>\n              <span className=\"text-xs font-bold text-white tracking-tight\">\n                {formatValueLabel(activePoint.value, false)}\n              </span>\n            </div>\n            {/* Tiny triangle caret */}\n            <div className=\"h-1.5 w-1.5 rotate-45 border-r border-b border-sky-400/40 bg-[#121214] -mt-1\" />\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      {/* Value Labels Above Data Points */}\n      <div className=\"pointer-events-none absolute inset-0\">\n        {points.map((pt, i) => {\n          const isFirst = i === 0;\n          const leftPercent = (pt.x / svgWidth) * 100;\n          const topPercent = (pt.y / svgHeight) * 100;\n\n          return (\n            <motion.div\n              key={`${metric}-val-${i}`}\n              initial={{ opacity: 0, y: 4 }}\n              animate={{ opacity: 1, y: 0 }}\n              transition={{\n                duration: shouldReduceMotion ? 0 : 0.3,\n                delay: i * 0.03,\n              }}\n              style={{\n                left: `${leftPercent}%`,\n                top: `${topPercent}%`,\n              }}\n              className=\"absolute -translate-x-1/2 -translate-y-6 text-center select-none\"\n            >\n              <span\n                className={cn(\n                  \"text-[11px] tabular-nums\",\n                  isFirst\n                    ? \"font-bold text-white drop-shadow-sm\"\n                    : \"text-neutral-400\"\n                )}\n              >\n                {formatValueLabel(pt.value, isFirst)}\n              </span>\n            </motion.div>\n          );\n        })}\n      </div>\n\n      {/* X-Axis Labels Beneath (2 AM, 5 AM, etc.) */}\n      <div className=\"pointer-events-none absolute bottom-0 inset-x-0 flex justify-between px-3 text-[11px] text-neutral-400 font-medium select-none\">\n        {hourly.map((p, i) => (\n          <span\n            key={`time-${i}`}\n            className={cn(\n              \"tabular-nums text-center\",\n              hoverIndex === i ? \"text-sky-300 font-semibold\" : \"text-neutral-400\"\n            )}\n          >\n            {p.time}\n          </span>\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Subcomponent: 8-Day Forecast Row                                   */\n/* ------------------------------------------------------------------ */\n\nexport interface WeatherForecastRowProps {\n  daily: DailyForecast[];\n  selectedIndex: number;\n  unit: TemperatureUnit;\n  onSelectDay: (index: number) => void;\n}\n\nexport function WeatherForecastRow({\n  daily,\n  selectedIndex,\n  unit,\n  onSelectDay,\n}: WeatherForecastRowProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <div\n      role=\"listbox\"\n      aria-label=\"8-day weather forecast\"\n      className=\"mt-4 pt-4 border-t border-white/10 grid grid-cols-8 gap-1.5 sm:gap-2 overflow-x-auto pb-1 scrollbar-none\"\n    >\n      {daily.map((item, idx) => {\n        const isSelected = selectedIndex === idx;\n        const high = formatTemp(item.highTemp, unit);\n        const low = formatTemp(item.lowTemp, unit);\n\n        return (\n          <motion.div\n            key={`${item.day}-${idx}`}\n            role=\"option\"\n            aria-selected={isSelected}\n            tabIndex={0}\n            onClick={() => onSelectDay(idx)}\n            onKeyDown={(e) => {\n              if (e.key === \"Enter\" || e.key === \" \") {\n                e.preventDefault();\n                onSelectDay(idx);\n              }\n            }}\n            initial={{ opacity: 0, y: 8 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{\n              duration: shouldReduceMotion ? 0 : 0.35,\n              delay: idx * 0.04,\n            }}\n            whileHover={shouldReduceMotion ? undefined : { y: -2 }}\n            whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}\n            className={cn(\n              \"relative flex flex-col items-center justify-between rounded-2xl py-3 px-1 min-w-[56px] sm:min-w-0 cursor-pointer select-none transition-colors\",\n              \"focus:outline-none focus-visible:ring-1 focus-visible:ring-amber-400\"\n            )}\n          >\n            {/* Sliding Shared Selection Pill Background */}\n            {isSelected && (\n              <motion.div\n                layoutId=\"forecast-day-highlight\"\n                transition={{ type: \"spring\", stiffness: 380, damping: 28 }}\n                className=\"absolute inset-0 rounded-2xl bg-white/10 border border-white/15 shadow-inner\"\n              />\n            )}\n\n            {/* Day label */}\n            <span\n              className={cn(\n                \"relative z-10 text-xs font-medium transition-colors\",\n                isSelected ? \"text-white font-semibold\" : \"text-neutral-400\"\n              )}\n            >\n              {item.day}\n            </span>\n\n            {/* Condition Icon */}\n            <div className=\"relative z-10 my-2\">\n              <WeatherIcon condition={item.condition} size={28} />\n            </div>\n\n            {/* High & Low Temperatures */}\n            <div className=\"relative z-10 flex items-center gap-1 text-xs tabular-nums\">\n              <span className=\"font-semibold text-white\">{high}°</span>\n              <span className=\"text-neutral-500\">{low}°</span>\n            </div>\n          </motion.div>\n        );\n      })}\n    </div>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Main Component: WeatherCard                                        */\n/* ------------------------------------------------------------------ */\n\nexport function WeatherCard({\n  location = \"San Francisco, CA\",\n  initialUnit = \"F\",\n  daily = DEFAULT_DAILY_FORECAST,\n  className,\n}: WeatherCardProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  // Selected Day & Metric Tab & Temperature Unit State\n  const [selectedDayIndex, setSelectedDayIndex] = useState(0);\n  const [activeTab, setActiveTab] = useState<WeatherMetricTab>(\"temperature\");\n  const [unit, setUnit] = useState<TemperatureUnit>(initialUnit);\n\n  const activeDayData = daily[selectedDayIndex] ?? daily[0];\n\n  return (\n    <motion.div\n      initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 12 }}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{ duration: 0.5, ease: \"easeOut\" }}\n      className={cn(\n        \"relative w-full max-w-[630px] mx-auto rounded-3xl border border-white/10 bg-[#181818] p-5 sm:p-6 shadow-[inset_0_1px_0_rgba(255,255,255,0.06),0_20px_40px_-15px_rgba(0,0,0,0.7)] text-white font-sans antialiased overflow-hidden\",\n        className\n      )}\n    >\n      {/* Subtle Dark Ambient Backdrop Glow */}\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute -top-28 -right-28 h-64 w-64 rounded-full bg-sky-500/10 blur-3xl\"\n      />\n\n      {/* 1. Header Row */}\n      <WeatherHeader\n        location={location}\n        dayData={activeDayData}\n        unit={unit}\n        onToggleUnit={setUnit}\n      />\n\n      {/* 2. Tabs Row */}\n      <WeatherTabs activeTab={activeTab} onSelectTab={setActiveTab} />\n\n      {/* 3. Smooth Area Chart */}\n      <WeatherChart\n        hourly={activeDayData.hourly}\n        metric={activeTab}\n        unit={unit}\n      />\n\n      {/* 4. 8-Day Forecast Row */}\n      <WeatherForecastRow\n        daily={daily}\n        selectedIndex={selectedDayIndex}\n        unit={unit}\n        onSelectDay={setSelectedDayIndex}\n      />\n    </motion.div>\n  );\n}\n\nexport default WeatherCard;\n",
      "type": "registry:ui",
      "target": "components/ui/weather-card.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport async function copyToClipboard(text: string): Promise<boolean> {\n  if (typeof window === \"undefined\") return false;\n\n  try {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(text);\n      return true;\n    }\n\n    const textArea = document.createElement(\"textarea\");\n    textArea.value = text;\n    textArea.style.position = \"fixed\";\n    textArea.style.left = \"-999999px\";\n    textArea.style.top = \"-999999px\";\n    document.body.appendChild(textArea);\n    textArea.focus();\n    textArea.select();\n    const succeeded = document.execCommand(\"copy\");\n    textArea.remove();\n    return succeeded;\n  } catch {\n    return false;\n  }\n}\n",
      "type": "registry:lib",
      "target": "lib/utils.ts"
    }
  ]
}
