{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"task-widget","type":"registry:ui","title":"Task Widget","description":"An iOS-like glass widget with a live clock, weather line and tactile task cards that frost toward the tail of the list.","dependencies":["motion","lucide-react","figma-squircle","react-use-measure"],"registryDependencies":["utils"],"files":[{"path":"registry/ui/task-widget.tsx","type":"registry:ui","content":"\"use client\";\n\n// Adapted from Calamansi UI by fujiDevv (MIT License).\n// https://github.com/fujiDevv/calamansi-ui\n// Design inspired by Jay Dwivedi (https://sprrrint.com/jaydwivedi).\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  useSyncExternalStore,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from \"motion/react\";\nimport { Check, Coffee, Layers, Link2, Moon, PenTool, Sun, Users } from \"lucide-react\";\nimport { getSvgPath } from \"figma-squircle\";\nimport useMeasure from \"react-use-measure\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TaskItem = {\n  id: string | number;\n  title: string;\n  completed?: boolean;\n  tag?: string;\n  icon?: ReactNode;\n};\n\nexport type TaskWidgetVariant =\n  | \"calamansi\"\n  | \"slate\"\n  | \"citrus\"\n  | \"black\"\n  | \"dark\";\n\n/** The shell's corner: the widget's own large radius, or the kit's squircle. */\nexport type TaskWidgetCorner = \"rounded\" | \"squircle\";\n\nexport type TaskWidgetProps = {\n  /** Array of task items (controlled). */\n  tasks?: TaskItem[];\n  /** Default task items (uncontrolled). */\n  defaultTasks?: TaskItem[];\n  /** Callback fired when a task checkbox is toggled. */\n  onTaskToggle?: (taskId: string | number, completed: boolean) => void;\n  /** Section title above task list. Default: \"Today\" */\n  title?: string;\n  /** Weather display options. */\n  weather?: {\n    condition?: string;\n    temperature?: string;\n    icon?: ReactNode;\n  };\n  /** Whether to show the live clock. Default: true */\n  showClock?: boolean;\n  /** Clock time format. Default: \"12h\" */\n  timeFormat?: \"12h\" | \"24h\";\n  /** Visual glass tint variant. Default: \"calamansi\" */\n  variant?: TaskWidgetVariant;\n  /**\n   * Shell corner. Default: \"rounded\" — the widget's own radius, which is the\n   * shape it shipped with. \"squircle\" swaps it for the kit's curve.\n   */\n  corner?: TaskWidgetCorner;\n  /** Extra class names merged onto outer container. */\n  className?: string;\n};\n\nconst DARK_QUERY = \"(prefers-color-scheme: dark)\";\nconst subscribeDark = (cb: () => void) => {\n  const media = window.matchMedia(DARK_QUERY);\n  media.addEventListener(\"change\", cb);\n  return () => media.removeEventListener(\"change\", cb);\n};\n\nconst DEFAULT_TASKS: TaskItem[] = [\n  {\n    id: 1,\n    title: \"Buy a T-shirt\",\n    completed: true,\n    icon: <Coffee className=\"size-5\" />,\n  },\n  {\n    id: 2,\n    title: \"Unsubscribe Figma\",\n    completed: false,\n    icon: <Layers className=\"size-5\" />,\n  },\n  {\n    id: 3,\n    title: \"Meeting with Sarah\",\n    completed: false,\n    icon: <Users className=\"size-5\" />,\n  },\n  {\n    id: 4,\n    title: \"Review PRs\",\n    completed: false,\n    icon: <Link2 className=\"size-5\" />,\n  },\n  {\n    id: 5,\n    title: \"Update Portfolio\",\n    completed: false,\n    icon: <PenTool className=\"size-5\" />,\n  },\n];\n\nconst VARIANT_STYLES: Record<\n  TaskWidgetVariant,\n  {\n    bg: string;\n    accent: string;\n    dotColor: string;\n  }\n> = {\n  calamansi: {\n    bg: \"from-[#8fa37d] via-[#5c7a67] to-[#39564a] dark:from-[#1b281f] dark:via-[#16221a] dark:to-[#0e1611]\",\n    accent: \"text-[#b4e84c] dark:text-[#b4e84c]\",\n    dotColor: \"#b4e84c\",\n  },\n  slate: {\n    bg: \"from-[#a79cb7] via-[#687396] to-[#4a5a7f] dark:from-[#1e1b4b] dark:via-[#1e293b] dark:to-[#0f172a]\",\n    accent: \"text-amber-300 dark:text-amber-200\",\n    dotColor: \"#f4f4f5\",\n  },\n  citrus: {\n    bg: \"from-[#d69f7e] via-[#b87152] to-[#7d4128] dark:from-[#2e170c] dark:via-[#22120b] dark:to-[#140a06]\",\n    accent: \"text-[#ff9e3d] dark:text-[#ff9e3d]\",\n    dotColor: \"#ff9e3d\",\n  },\n  black: {\n    bg: \"from-[#27272a] via-[#18181b] to-[#09090b] dark:from-[#18181b] dark:via-[#09090b] dark:to-[#000000]\",\n    accent: \"text-zinc-200 dark:text-zinc-300\",\n    dotColor: \"#e4e4e7\",\n  },\n  dark: {\n    bg: \"from-[#27272a] via-[#18181b] to-[#09090b] dark:from-[#18181b] dark:via-[#09090b] dark:to-[#000000]\",\n    accent: \"text-zinc-200 dark:text-zinc-300\",\n    dotColor: \"#e4e4e7\",\n  },\n};\n\n/**\n * The widget's own radius, the shape it shipped with. The inset is not here — it\n * sits on the content layer, so both corners pad the widget identically.\n */\nconst ROUNDED_SHELL = [\n  \"overflow-hidden rounded-[36px] border-4 border-white/80\",\n  \"sm:rounded-[64px] sm:border-[5px] lg:rounded-[88px] lg:border-[6px]\",\n  \"dark:border-white/10 bg-gradient-to-br transition-colors duration-500\",\n].join(\" \");\n\n/** Its cast shadow, plus the two inset highlights that give the glass its depth. */\nconst ROUNDED_SHADOW =\n  \"0 32px 64px -16px rgba(0, 0, 0, 0.35), inset 0 -4px 20px 8px rgba(255, 255, 255, 0.4), inset 0 -10px 4px rgba(0, 0, 0, 0.2)\";\n\n/** On the squircle the surface is a clipped layer: the cast shadow rides the wrapper as a filter, the insets move inside. */\nconst SQUIRCLE_SHADOW = \"drop-shadow(0 26px 44px rgba(0, 0, 0, 0.32))\";\nconst SQUIRCLE_INSETS =\n  \"inset 0 -4px 20px 8px rgba(255, 255, 255, 0.4), inset 0 -10px 4px rgba(0, 0, 0, 0.2)\";\n\n/** The bottom indicator's spring — the weight its `transition` named all along. */\nconst INDICATOR_SPRING = { stiffness: 400, damping: 35 };\n\n/**\n * An iOS-inspired glassmorphism widget with live time, weather, and tactile interactive task cards.\n * The corner is its own large radius by default, or the kit's squircle with\n * `corner=\"squircle\"`.\n * Design inspired by Jay Dwivedi (https://sprrrint.com/jaydwivedi).\n */\nexport function TaskWidget({\n  tasks: controlledTasks,\n  defaultTasks = DEFAULT_TASKS,\n  onTaskToggle,\n  title = \"Today\",\n  weather,\n  showClock = true,\n  timeFormat = \"12h\",\n  variant = \"calamansi\",\n  corner = \"rounded\",\n  className,\n}: TaskWidgetProps) {\n  const [internalTasks, setInternalTasks] = useState<TaskItem[]>(defaultTasks);\n  const [activeTaskId, setActiveTaskId] = useState<string | number | null>(\n    null,\n  );\n  const activeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const [currentTime, setCurrentTime] = useState<{\n    time: string;\n    ampm: string;\n  }>({\n    time: \"9:41\",\n    ampm: \"AM\",\n  });\n  const [canScroll, setCanScroll] = useState(false);\n  const [isScrolled, setIsScrolled] = useState(false);\n\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const shouldReduceMotion = useReducedMotion();\n  const filterId = useId().replace(/[^a-zA-Z0-9]/g, \"\");\n  const blurFilterId = `${filterId}blur`;\n\n  /*\n    The deck's scroll progress is a motion value rather than state. As state it\n    changed on every scroll frame and re-rendered the whole deck for it — six cards\n    and their grain filters reconciled dozens of times a second, on the main thread,\n    at exactly the moment the compositor is busy re-sampling a backdrop blur. A\n    motion value writes the one style that actually moves, and it lets the bottom\n    indicator run the spring it has always asked for: the width used to come from a\n    plain style, so that spring never had an animation to apply to.\n  */\n  const scrollProgress = useMotionValue(0);\n  const smoothProgress = useSpring(scrollProgress, INDICATOR_SPRING);\n  const indicatorWidth = useTransform(\n    shouldReduceMotion ? scrollProgress : smoothProgress,\n    (progress) => `${Math.round(45 + progress * 55)}%`,\n  );\n\n  const activeTasks = controlledTasks ?? internalTasks;\n  const currentVariant = VARIANT_STYLES[variant] ?? VARIANT_STYLES.calamansi;\n\n  // Live clock\n  useEffect(() => {\n    function updateClock() {\n      const now = new Date();\n      let hours = now.getHours();\n      const minutes = now.getMinutes();\n      const ampm = hours >= 12 ? \"PM\" : \"AM\";\n\n      if (timeFormat === \"12h\") {\n        hours = hours % 12;\n        hours = hours ? hours : 12;\n      }\n\n      const formattedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;\n      setCurrentTime({\n        time: `${hours}:${formattedMinutes}`,\n        ampm: timeFormat === \"12h\" ? ampm : \"\",\n      });\n    }\n\n    updateClock();\n    const timer = setInterval(updateClock, 1000);\n    return () => clearInterval(timer);\n  }, [timeFormat]);\n\n  // Dark mode detection for the ambient fallback (sun / moon)\n  const isDarkMode = useSyncExternalStore(subscribeDark, () => window.matchMedia(DARK_QUERY).matches, () => false);\n\n  // Check scroll overflow on mount and updates\n  useEffect(() => {\n    const el = scrollRef.current;\n    if (!el) return;\n    const checkScroll = () => {\n      const maxScroll = el.scrollHeight - el.clientHeight;\n      setCanScroll(maxScroll > 10);\n      setIsScrolled(el.scrollTop > 12);\n    };\n    checkScroll();\n    const raf = requestAnimationFrame(checkScroll);\n    return () => cancelAnimationFrame(raf);\n  }, [activeTasks]);\n\n  // Track scroll position for bottom indicator thumb and blur visibility\n  const handleScroll = useCallback(() => {\n    const el = scrollRef.current;\n    if (!el) return;\n    const maxScroll = el.scrollHeight - el.clientHeight;\n    setCanScroll(maxScroll > 10);\n    setIsScrolled(el.scrollTop > 12);\n\n    if (maxScroll <= 0) {\n      scrollProgress.set(0);\n      return;\n    }\n    const progress = Math.min(1, Math.max(0, el.scrollTop / maxScroll));\n    scrollProgress.set(progress);\n  }, [scrollProgress]);\n\n  // Clear active task highlight timeout on unmount\n  useEffect(() => {\n    return () => {\n      if (activeTimeoutRef.current) {\n        clearTimeout(activeTimeoutRef.current);\n      }\n    };\n  }, []);\n\n  const toggleTask = (taskId: string | number) => {\n    setActiveTaskId(taskId);\n\n    // Auto-remove the white highlight after 2.5 seconds\n    if (activeTimeoutRef.current) {\n      clearTimeout(activeTimeoutRef.current);\n    }\n    activeTimeoutRef.current = setTimeout(() => {\n      setActiveTaskId((current) => (current === taskId ? null : current));\n    }, 2500);\n\n    if (onTaskToggle) {\n      const task = activeTasks.find((t) => t.id === taskId);\n      onTaskToggle(taskId, !task?.completed);\n    }\n    if (!controlledTasks) {\n      setInternalTasks((prev) =>\n        prev.map((task) =>\n          task.id === taskId ? { ...task, completed: !task.completed } : task,\n        ),\n      );\n    }\n  };\n\n  const handleKeyDown = (e: KeyboardEvent, taskId: string | number) => {\n    if (e.key === \"Enter\" || e.key === \" \") {\n      e.preventDefault();\n      toggleTask(taskId);\n    }\n  };\n\n  const completedCount = activeTasks.filter((t) => t.completed).length;\n  const squircle = corner === \"squircle\";\n\n  /*\n    The grain and the ambient dots ride on the surface in both shapes: inside the\n    squircle layer when the corner is the kit's curve, straight on the shell when\n    it is the widget's own radius — where the shell is the wrapper itself.\n  */\n  const surface = (\n    <>\n      {/* SVG Grain Noise Filter Overlay */}\n      <svg className=\"pointer-events-none absolute inset-0 size-full opacity-15 mix-blend-overlay\">\n        <filter id={filterId}>\n          <feTurbulence\n            type=\"fractalNoise\"\n            baseFrequency=\"0.85\"\n            numOctaves=\"3\"\n            stitchTiles=\"stitch\"\n          />\n        </filter>\n        <rect width=\"100%\" height=\"100%\" filter={`url(#${filterId})`} />\n      </svg>\n\n      {/* Top Ambient Matrix Dots */}\n      <div\n        className=\"pointer-events-none absolute left-6 top-6 size-5 opacity-40 sm:left-8 sm:top-8\"\n        style={{\n          backgroundImage: `radial-gradient(circle, ${currentVariant.dotColor} 2px, transparent 2.5px)`,\n          backgroundSize: \"8px 8px\",\n        }}\n      />\n    </>\n  );\n\n  return (\n    <div\n      className={cn(\n        \"relative flex w-full max-w-[780px] flex-col\",\n        !squircle && ROUNDED_SHELL,\n        !squircle && currentVariant.bg,\n        className,\n      )}\n      /* a clip takes a box shadow with it, so on the squircle the cast shadow is a filter */\n      style={\n        squircle\n          ? { filter: SQUIRCLE_SHADOW }\n          : { boxShadow: ROUNDED_SHADOW }\n      }\n    >\n      {squircle ? (\n        <Squircle\n          className={cn(\n            \"bg-gradient-to-br transition-colors duration-500\",\n            currentVariant.bg,\n          )}\n          border=\"text-white/80 dark:text-white/10\"\n          borderWidth={4}\n          /* the widget casts the shadow above, so the kit's lift would double it */\n          lift={false}\n        >\n          {/* the glass lip, drawn on the layer so it takes the squircle */}\n          <span\n            aria-hidden=\"true\"\n            className=\"pointer-events-none absolute inset-0 block\"\n            style={{ boxShadow: SQUIRCLE_INSETS }}\n          />\n          {surface}\n        </Squircle>\n      ) : (\n        surface\n      )}\n\n      {/*\n        Main Content Layout. The shell's inset lives here rather than on the shell:\n        in the squircle mode the surface is a layer rather than the layout box, and\n        padding a parent with no painted background would simply be the same box.\n      */}\n      <div className=\"relative flex flex-col justify-between p-5 sm:p-8 lg:p-10\">\n        <div className=\"relative z-10 flex flex-col gap-6 md:flex-row md:items-center md:justify-between md:gap-8\">\n          {/* Left Column: Weather and Live Clock */}\n          <div className=\"flex flex-col justify-end select-none pt-2 sm:pt-6 md:pt-10\">\n            {/* Weather Row */}\n            <div className=\"flex items-center gap-2 sm:gap-2.5\">\n              {weather?.icon ??\n                (isDarkMode ? (\n                  <Moon className=\"size-5 text-slate-100 drop-shadow-sm sm:size-6\" />\n                ) : (\n                  <Sun\n                    className={cn(\n                      \"size-5 drop-shadow-sm sm:size-6\",\n                      currentVariant.accent,\n                    )}\n                  />\n                ))}\n              <span className=\"text-xs font-bold tracking-wider text-white/90 uppercase sm:text-base\">\n                {weather?.condition ?? (isDarkMode ? \"Starry\" : \"Sunny\")}\n              </span>\n              {weather?.temperature && (\n                <span className=\"text-xs font-semibold text-white/70 sm:text-base\">\n                  {weather.temperature}\n                </span>\n              )}\n            </div>\n\n            {/* Large Clock Display */}\n            {showClock && (\n              <div className=\"mt-1 flex items-baseline gap-1.5\">\n                <h1 className=\"text-5xl font-bold tracking-tight text-white drop-shadow-md sm:text-7xl lg:text-8xl tabular-nums\">\n                  {currentTime.time}\n                </h1>\n                {currentTime.ampm && (\n                  <span className=\"text-xl font-bold tracking-tight text-white/90 sm:text-3xl\">\n                    {currentTime.ampm}\n                  </span>\n                )}\n              </div>\n            )}\n          </div>\n\n          {/* Right Column: Task Deck */}\n          {/*\n          min-w-0 is load-bearing: the column is flex-basis 0, so without it the deck is pinned to its\n          min-content width and a wider clock (switching 12h to 24h) overflows the row, pushing the\n          deck past the card's overflow-hidden edge.\n        */}\n          <div className=\"flex min-w-0 flex-1 flex-col w-full md:max-w-[420px]\">\n            {/* Section Header */}\n            <div className=\"mb-2.5 flex items-center justify-between px-1.5 select-none sm:mb-3 sm:px-2\">\n              <h2 className=\"text-xs font-bold tracking-wider text-white/60 uppercase sm:text-sm\">\n                {title}\n              </h2>\n              <span className=\"rounded-full bg-white/15 px-2.5 py-0.5 text-[11px] font-semibold text-white/80 backdrop-blur-xs\">\n                {completedCount}/{activeTasks.length}\n              </span>\n            </div>\n\n            {/* Scrollable Tasks Container with Overflow Blur */}\n            <div className=\"relative overflow-visible\">\n              <div\n                ref={scrollRef}\n                onScroll={handleScroll}\n                className=\"flex max-h-[250px] flex-col gap-2.5 overflow-y-auto p-1 pr-1.5 scroll-smooth [scrollbar-width:thin] [scrollbar-color:rgba(255,255,255,0.25)_transparent] sm:max-h-[280px] sm:gap-3 sm:pr-2\"\n              >\n                {activeTasks.map((task, index) => {\n                  const isActive = activeTaskId === task.id;\n                  const isChecked = Boolean(task.completed);\n                  /*\n                    The deck frosts toward its tail. `veilDepth` is how far past\n                    the fold a card sits — 0 on the first veiled one, 1 by the\n                    third — so the blur, the mask and the grain all ride a single\n                    ramp. Two neighbouring cards stepping between two fixed\n                    strengths is a seam, and the eye finds a seam long before it\n                    reads the frost itself.\n                  */\n                  const veilDepth = canScroll\n                    ? Math.min(1, Math.max(0, (index - 1) / 2))\n                    : 0;\n                  const hasItemBlur = canScroll && index >= 1;\n                  const veilBlur = `blur(${10 + 6 * veilDepth}px)`;\n                  /* where the veil gives out, measured up from the card's bottom */\n                  const veilFade = Math.round(65 * (1 - veilDepth));\n                  const veilMask =\n                    veilFade > 0\n                      ? `linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,0.85) ${Math.round(\n                          veilFade * 0.46,\n                        )}%, rgba(0,0,0,0) ${veilFade}%)`\n                      : \"linear-gradient(to top, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 100%)\";\n                  const grainOpacity = 0.25 + 0.3 * veilDepth;\n                  const itemFilterId = `${blurFilterId}-${String(task.id).replace(/[^a-zA-Z0-9]/g, \"\")}`;\n\n                  return (\n                    <div\n                      key={task.id}\n                      role=\"checkbox\"\n                      aria-checked={isChecked}\n                      tabIndex={0}\n                      onClick={() => toggleTask(task.id)}\n                      onKeyDown={(e) => handleKeyDown(e, task.id)}\n                      className={cn(\n                        \"group relative flex h-16 w-full shrink-0 cursor-pointer items-center justify-between gap-3 overflow-hidden rounded-2xl px-4 transition-all duration-300 sm:h-20 sm:gap-3.5 sm:rounded-3xl sm:px-6\",\n                        \"border backdrop-blur-md outline-hidden focus-visible:ring-2 focus-visible:ring-white/80\",\n                        isActive\n                          ? \"border-white/70 bg-white/80 text-foreground shadow-lg dark:border-white/20 dark:bg-white/15 dark:text-white\"\n                          : \"border-white/30 bg-white/45 text-foreground/80 opacity-90 hover:border-white/60 hover:bg-white/65 hover:opacity-100 dark:border-white/10 dark:bg-white/5 dark:text-white/80 dark:hover:bg-white/10\",\n                      )}\n                      style={{\n                        boxShadow: isActive\n                          ? \"inset 0 2px 2px rgba(255, 255, 255, 0.8), 0 10px 24px -4px rgba(0, 0, 0, 0.12)\"\n                          : \"inset 0 1px 1px rgba(255, 255, 255, 0.3)\",\n                      }}\n                    >\n                      {/* Custom Tactile Checkbox */}\n                      <div\n                        className={cn(\n                          \"relative z-0 flex size-8 shrink-0 items-center justify-center rounded-xl border-2 transition-all duration-200 sm:size-9\",\n                          isChecked\n                            ? \"border-primary bg-primary text-primary-foreground shadow-xs dark:border-primary dark:bg-primary\"\n                            : \"border-foreground/40 bg-transparent dark:border-white/40\",\n                        )}\n                      >\n                        {isChecked && (\n                          <motion.div\n                            initial={\n                              shouldReduceMotion\n                                ? false\n                                : { scale: 0.5, opacity: 0 }\n                            }\n                            animate={{ scale: 1, opacity: 1 }}\n                            transition={{\n                              type: \"spring\",\n                              stiffness: 500,\n                              damping: 30,\n                            }}\n                          >\n                            <Check className=\"size-4 stroke-[3.5] sm:size-5\" />\n                          </motion.div>\n                        )}\n                      </div>\n\n                      {/* Task Title */}\n                      <span\n                        className={cn(\n                          \"relative z-0 flex-1 truncate text-sm font-semibold tracking-tight transition-colors duration-200 sm:text-lg\",\n                          isChecked &&\n                            \"text-foreground/40 line-through decoration-foreground/30 dark:text-white/40 dark:decoration-white/30\",\n                        )}\n                      >\n                        {task.title}\n                      </span>\n\n                      {/* Task Icon / Slot */}\n                      {task.icon && (\n                        <div\n                          className={cn(\n                            \"relative z-0 shrink-0 transition-opacity duration-200\",\n                            isChecked\n                              ? \"opacity-30\"\n                              : \"opacity-75 group-hover:opacity-100\",\n                          )}\n                        >\n                          {task.icon}\n                        </div>\n                      )}\n\n                      {/* In-Item Grainy Glass Blur Effect (Contained strictly inside item) */}\n                      {hasItemBlur && (\n                        <motion.div\n                          aria-hidden=\"true\"\n                          initial={false}\n                          animate={\n                            shouldReduceMotion\n                              ? { opacity: !canScroll || isScrolled ? 0 : 1 }\n                              : {\n                                  opacity: !canScroll || isScrolled ? 0 : 1,\n                                  y:\n                                    !canScroll || isScrolled\n                                      ? 6 + 4 * veilDepth\n                                      : 0,\n                                }\n                          }\n                          transition={{\n                            duration: 0.35,\n                            ease: [0.16, 1, 0.3, 1],\n                          }}\n                          className=\"pointer-events-none absolute inset-0 z-10 overflow-hidden rounded-2xl sm:rounded-3xl\"\n                          style={{\n                            backdropFilter: veilBlur,\n                            WebkitBackdropFilter: veilBlur,\n                            maskImage: veilMask,\n                            WebkitMaskImage: veilMask,\n                          }}\n                        >\n                          {/*\n                            Tactile sandblasted grain. The noise is greyed, then its\n                            own alpha is pushed through a steep ramp so a pixel is\n                            either grain or clear — letting every pixel sit\n                            half-transparent over the glass is what made this read as\n                            a fog of dirt rather than a frosted surface, and the ramp\n                            keeps the speckle crisp enough to see in either theme.\n                          */}\n                          <svg\n                            className=\"pointer-events-none absolute inset-0 size-full mix-blend-overlay\"\n                            style={{ opacity: grainOpacity }}\n                          >\n                            <filter id={itemFilterId}>\n                              {/*\n                              A seed per card. feTurbulence defaults to 0, and every\n                              card here is the same width, so one seed tiles the\n                              identical speckle behind each frosted one — a repeat the\n                              eye catches well before it reads grain.\n                            */}\n                              <feTurbulence\n                                type=\"fractalNoise\"\n                                baseFrequency=\"0.8\"\n                                numOctaves=\"2\"\n                                stitchTiles=\"stitch\"\n                                seed={index + 1}\n                              />\n                              <feColorMatrix type=\"saturate\" values=\"0\" />\n                              <feColorMatrix\n                                type=\"matrix\"\n                                values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 3 -1\"\n                              />\n                            </filter>\n                            <rect\n                              width=\"100%\"\n                              height=\"100%\"\n                              filter={`url(#${itemFilterId})`}\n                            />\n                          </svg>\n\n                          {/* Ambient Glass Tint Layer */}\n                          <div\n                            className={cn(\n                              \"size-full\",\n                              veilDepth >= 1\n                                ? \"bg-gradient-to-t from-black/40 via-white/10 to-white/5 dark:from-black/60 dark:via-white/5\"\n                                : \"bg-gradient-to-t from-black/25 via-white/10 to-transparent dark:from-black/45 dark:via-white/5\",\n                            )}\n                          />\n                        </motion.div>\n                      )}\n                    </div>\n                  );\n                })}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Bottom Symmetrical Home Indicator Bar */}\n        <div className=\"relative z-10 mt-5 flex justify-center sm:mt-6\">\n          <div className=\"h-1.5 w-32 rounded-full bg-white/20 p-0.5 backdrop-blur-xs sm:w-36\">\n            <motion.div\n              className=\"h-full rounded-full bg-white shadow-xs\"\n              style={{ width: indicatorWidth, margin: \"0 auto\" }}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/* ─── Squircle surface (inlined from Calamansi's lib/squircle) ───────────────\n   A superellipse painted on its own layer, inset to the parent, clipped with a\n   figma-squircle path measured from the layout box. */\nfunction Squircle({\n  className,\n  radius = 28,\n  share,\n  lift = true,\n  filter,\n  border,\n  borderWidth = 1,\n  children,\n}: {\n  className?: string;\n  radius?: number;\n  share?: number;\n  lift?: boolean;\n  filter?: string;\n  border?: string;\n  borderWidth?: number;\n  children?: ReactNode;\n}) {\n  const [ref, bounds] = useMeasure({ offsetSize: true });\n  const corner = share && bounds.height > 0 ? Math.min(radius, bounds.height * share) : radius;\n  const fallback = share ? `min(${radius}px, ${share * 100}%)` : radius;\n  const path = useMemo(\n    () =>\n      bounds.width > 0 && bounds.height > 0\n        ? getSvgPath({ width: bounds.width, height: bounds.height, cornerRadius: corner, cornerSmoothing: 1 })\n        : null,\n    [bounds.width, bounds.height, corner],\n  );\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"pointer-events-none absolute inset-0 block\"\n      style={{ filter: filter ?? (lift ? \"drop-shadow(0 14px 28px rgba(0, 0, 0, 0.24))\" : undefined) }}\n    >\n      <span\n        ref={ref}\n        style={path ? { clipPath: `path('${path}')` } : { borderRadius: fallback }}\n        className={cn(\"relative block size-full overflow-hidden\", className)}\n      >\n        {children}\n        {border &&\n          (path ? (\n            <svg aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 size-full\" fill=\"none\">\n              {/* only the inner half of the stroke survives the clip */}\n              <path d={path} className={border} stroke=\"currentColor\" strokeWidth={borderWidth * 2} />\n            </svg>\n          ) : (\n            <span\n              aria-hidden=\"true\"\n              className={cn(\"pointer-events-none absolute inset-0 block\", border)}\n              style={{ borderRadius: fallback, boxShadow: `inset 0 0 0 ${borderWidth}px currentColor` }}\n            />\n          ))}\n      </span>\n    </span>\n  );\n}\n"}]}