{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"newsletter-section","type":"registry:block","title":"Newsletter Section","description":"Newsletter signup section with an email field, light validation and a submit button that steps through idle, loading, success and error from your onSubscribe promise, a form that morphs into a confirmation with a draw-in check, a privacy note, optional reader avatars for social proof, and card or inline layouts.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/newsletter-section.tsx","type":"registry:component","target":"@components/motion/newsletter-section.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/blocks/newsletter-section\n\nimport { Mail, Send } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type FormEvent,\n  type ReactNode,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { AvatarGroup, type AvatarGroupItem } from \"@/components/motion/avatar-group\";\nimport { type ButtonState, StatefulButton } from \"@/components/motion/button\";\nimport { Input } from \"@/components/motion/input\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface NewsletterSocialProof {\n  /** Avatars shown in an overlapping stack. */\n  avatars: AvatarGroupItem[];\n  /** Copy beside the stack, e.g. \"Join 4,200 readers\". */\n  label: ReactNode;\n  /** Avatars before a \"+N\" chip. Default 4. */\n  max?: number;\n}\n\nexport interface NewsletterSectionProps {\n  /** \"card\" is a centered bordered panel; \"inline\" puts copy and form side by side. */\n  variant?: \"card\" | \"inline\";\n  eyebrow?: ReactNode;\n  heading?: ReactNode;\n  description?: ReactNode;\n  placeholder?: string;\n  buttonLabel?: string;\n  /** Resolve to confirm; reject (or throw) to show the error state. */\n  onSubscribe?: (email: string) => Promise<void> | void;\n  /** Small print under the form. Pass null to hide. */\n  privacyNote?: ReactNode;\n  socialProof?: NewsletterSocialProof;\n  successTitle?: ReactNode;\n  /** Receives the subscribed address. */\n  successMessage?: ReactNode | ((email: string) => ReactNode);\n  invalidEmailMessage?: string;\n  errorMessage?: string;\n  className?: string;\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/;\n\n/** Light check used before `onSubscribe`; the server stays the source of truth. */\nexport function isValidNewsletterEmail(value: string) {\n  return EMAIL_PATTERN.test(value.trim());\n}\n\nconst SWAP_ENTER = { duration: 0.32, ease: EASE_OUT } as const;\nconst SWAP_EXIT = { duration: 0.16, ease: EASE_OUT } as const;\nconst ERROR_RESET_MS = 2400;\n\nfunction SuccessCheck({ reduce }: { reduce: boolean }) {\n  return (\n    <motion.span\n      initial={{ scale: reduce ? 1 : 0.6, opacity: 0 }}\n      animate={{ scale: 1, opacity: 1 }}\n      transition={reduce ? SWAP_ENTER : SPRING_SWAP}\n      className=\"grid size-11 shrink-0 place-items-center rounded-full bg-success/15 text-success\"\n    >\n      <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\" className=\"size-5\">\n        <motion.path\n          d=\"M5 12.5l4.5 4.5L19 7.5\"\n          stroke=\"currentColor\"\n          strokeWidth={2.5}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          initial={{ pathLength: reduce ? 1 : 0 }}\n          animate={{ pathLength: 1 }}\n          transition={{ duration: 0.4, ease: EASE_OUT, delay: reduce ? 0 : 0.12 }}\n        />\n      </svg>\n    </motion.span>\n  );\n}\n\n/**\n * Newsletter signup section: heading, an email field with idle/loading/\n * success/error states driven by the `onSubscribe` promise (with a light\n * client-side email check), a privacy note and optional social-proof avatars.\n * On success the form morphs into a confirmation with a draw-in check. Two\n * layouts: a centered \"card\" and a side-by-side \"inline\" band that stacks in\n * narrow containers.\n */\nexport function NewsletterSection({\n  variant = \"card\",\n  eyebrow,\n  heading = \"Get the weekly digest\",\n  description = \"One short email every Friday with new components, motion notes and the occasional deep dive.\",\n  placeholder = \"you@example.com\",\n  buttonLabel = \"Subscribe\",\n  onSubscribe,\n  privacyNote = \"No spam. Unsubscribe with one click.\",\n  socialProof,\n  successTitle = \"You're on the list\",\n  successMessage = (email: string) => (\n    <>\n      We sent a confirmation to <span className=\"font-medium text-foreground\">{email}</span>.\n    </>\n  ),\n  invalidEmailMessage = \"Enter a valid email address.\",\n  errorMessage = \"Something went wrong. Please try again.\",\n  className,\n}: NewsletterSectionProps) {\n  const reduce = useReducedMotion() ?? false;\n  const id = useId();\n  const [email, setEmail] = useState(\"\");\n  const [state, setState] = useState<ButtonState>(\"idle\");\n  const [error, setError] = useState<string | null>(null);\n  const [subscribed, setSubscribed] = useState(\"\");\n  const timer = useRef<number | undefined>(undefined);\n  const successRef = useRef<HTMLDivElement>(null);\n\n  useEffect(\n    () => () => {\n      if (timer.current) window.clearTimeout(timer.current);\n    },\n    [],\n  );\n\n  const done = state === \"success\";\n\n  // Move focus to the confirmation so keyboard and screen reader users land\n  // on the result instead of a removed form.\n  useEffect(() => {\n    if (done) successRef.current?.focus({ preventScroll: true });\n  }, [done]);\n\n  const submit = async (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    if (state === \"loading\" || done) return;\n    if (!isValidNewsletterEmail(email)) {\n      setError(invalidEmailMessage);\n      return;\n    }\n    const value = email.trim();\n    setError(null);\n    setState(\"loading\");\n    try {\n      await onSubscribe?.(value);\n      setSubscribed(value);\n      setState(\"success\");\n    } catch {\n      setState(\"error\");\n      setError(errorMessage);\n      if (timer.current) window.clearTimeout(timer.current);\n      timer.current = window.setTimeout(() => setState(\"idle\"), ERROR_RESET_MS);\n    }\n  };\n\n  const inline = variant === \"inline\";\n  const swapInitial = reduce ? { opacity: 0 } : { opacity: 0, y: 10, filter: \"blur(4px)\" };\n  const swapExit = reduce\n    ? { opacity: 0, transition: SWAP_EXIT }\n    : { opacity: 0, y: -8, filter: \"blur(4px)\", transition: SWAP_EXIT };\n\n  const form = (\n    <motion.form\n      key=\"form\"\n      noValidate\n      onSubmit={submit}\n      initial={swapInitial}\n      animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n      exit={swapExit}\n      transition={SWAP_ENTER}\n      className=\"flex w-full flex-col gap-2\"\n    >\n      <div className=\"flex w-full flex-col gap-2 @md:flex-row @md:items-start\">\n        <Input\n          id={`${id}-email`}\n          type=\"email\"\n          inputMode=\"email\"\n          autoComplete=\"email\"\n          aria-label=\"Email address\"\n          placeholder={placeholder}\n          value={email}\n          onChange={(next) => {\n            setEmail(next);\n            if (error && isValidNewsletterEmail(next)) setError(null);\n          }}\n          error={error ?? undefined}\n          leftIcon={<Mail aria-hidden />}\n          className=\"min-w-0 flex-1\"\n          classNames={{ field: \"bg-background\", errorMessage: \"text-left\" }}\n        />\n        <StatefulButton\n          type=\"submit\"\n          state={state}\n          loadingText=\"Subscribing\"\n          successText=\"Subscribed\"\n          errorText=\"Try again\"\n          icon={<Send aria-hidden className=\"size-3.5\" />}\n          className=\"h-11 shrink-0\"\n        >\n          {buttonLabel}\n        </StatefulButton>\n      </div>\n      {privacyNote ? (\n        <p className={cn(\"text-xs text-muted-foreground\", inline ? \"text-left\" : \"text-center @md:text-left\")}>\n          {privacyNote}\n        </p>\n      ) : null}\n    </motion.form>\n  );\n\n  const confirmation = (\n    <motion.div\n      key=\"success\"\n      ref={successRef}\n      tabIndex={-1}\n      role=\"status\"\n      initial={swapInitial}\n      animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n      transition={SWAP_ENTER}\n      className=\"flex w-full items-center gap-3 rounded-2xl border border-border bg-background p-3 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n    >\n      <SuccessCheck reduce={reduce} />\n      <div className=\"min-w-0\">\n        <p className=\"text-sm font-medium text-foreground\">{successTitle}</p>\n        <p className=\"text-sm text-muted-foreground\">\n          {typeof successMessage === \"function\" ? successMessage(subscribed) : successMessage}\n        </p>\n      </div>\n    </motion.div>\n  );\n\n  const copy = (\n    <div className={cn(\"flex flex-col gap-3\", !inline && \"items-center text-center\")}>\n      {eyebrow ? (\n        <p className=\"text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground\">{eyebrow}</p>\n      ) : null}\n      {heading ? (\n        <h2 className=\"text-balance text-2xl font-semibold leading-tight tracking-tight @3xl:text-3xl\">\n          {heading}\n        </h2>\n      ) : null}\n      {description ? (\n        <p className={cn(\"text-pretty text-sm leading-6 text-muted-foreground\", !inline && \"max-w-md\")}>\n          {description}\n        </p>\n      ) : null}\n    </div>\n  );\n\n  const proof = socialProof ? (\n    <div className={cn(\"flex items-center gap-3\", !inline && \"justify-center\")}>\n      <AvatarGroup items={socialProof.avatars} max={socialProof.max ?? 4} size=\"sm\" label=\"Subscribers\" ringClassName={inline ? undefined : \"ring-card\"} />\n      <p className=\"text-xs font-medium text-muted-foreground\">{socialProof.label}</p>\n    </div>\n  ) : null;\n\n  // The form/confirmation swap changes height, so the slot morphs its layout\n  // on purpose (the section must grow or shrink with the content).\n  const slot = (\n    <motion.div\n      layout={reduce ? false : \"size\"}\n      transition={{ layout: SPRING_LAYOUT }}\n      className=\"w-full\"\n    >\n      <AnimatePresence mode=\"wait\" initial={false}>\n        {done ? confirmation : form}\n      </AnimatePresence>\n    </motion.div>\n  );\n\n  return (\n    <section className={cn(\"@container w-full text-foreground\", className)}>\n      {inline ? (\n        <div className=\"grid items-center gap-6 border-y border-border py-8 @2xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] @2xl:gap-10\">\n          <div className=\"flex flex-col gap-4\">\n            {copy}\n            {proof}\n          </div>\n          {slot}\n        </div>\n      ) : (\n        <div className=\"relative isolate mx-auto flex max-w-2xl flex-col items-center gap-6 overflow-hidden rounded-3xl border border-border bg-card px-5 py-10 text-card-foreground shadow-sm @md:px-10\">\n          {/* Decorative dot grid fading out from the top. */}\n          <div\n            aria-hidden\n            className=\"pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(var(--border-strong)_1px,transparent_1px)] bg-[size:18px_18px] [mask-image:radial-gradient(ellipse_70%_60%_at_50%_0%,black,transparent)]\"\n          />\n          {copy}\n          <div className=\"w-full max-w-md\">{slot}</div>\n          {proof}\n        </div>\n      )}\n    </section>\n  );\n}\n"},{"path":"components/motion/avatar-group.tsx","type":"registry:component","target":"@components/motion/avatar-group.tsx","content":"\"use client\";\n\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n  MorphPopover,\n  MorphPopoverContent,\n  MorphPopoverTrigger,\n} from \"@/components/motion/popover-morph\";\nimport { Tooltip } from \"@/components/motion/tooltip\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_MOUSE } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AvatarStatus = \"online\" | \"away\" | \"busy\" | \"offline\";\n\nexport interface AvatarGroupItem {\n  id: string;\n  name: string;\n  /** Image URL. Falls back to initials on a colored disc. */\n  src?: string;\n  /** Background for the initials fallback (any CSS color). */\n  color?: string;\n  status?: AvatarStatus;\n  /** Shows a pulsing ring around the avatar. */\n  typing?: boolean;\n}\n\nexport type AvatarGroupSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface AvatarGroupProps {\n  items: AvatarGroupItem[];\n  /** Avatars shown before the rest collapse into a \"+N\" chip. Default 4. */\n  max?: number;\n  size?: AvatarGroupSize;\n  /** Makes avatars buttons; also used for rows in the overflow list. */\n  onSelect?: (item: AvatarGroupItem) => void;\n  /** Accessible label for the group. Default \"Collaborators\". */\n  label?: string;\n  className?: string;\n  /** Ring color separating overlapped avatars; match the surface behind. */\n  ringClassName?: string;\n}\n\nconst SIZES: Record<\n  AvatarGroupSize,\n  { box: number; overlap: number; fan: number; text: string; dot: number }\n> = {\n  sm: { box: 28, overlap: 6, fan: 6, text: \"text-[10px]\", dot: 8 },\n  md: { box: 36, overlap: 8, fan: 8, text: \"text-xs\", dot: 10 },\n  lg: { box: 44, overlap: 10, fan: 10, text: \"text-sm\", dot: 12 },\n};\n\nconst STATUS_CLASS: Record<AvatarStatus, string> = {\n  online: \"bg-emerald-500\",\n  away: \"bg-amber-400\",\n  busy: \"bg-rose-500\",\n  offline: \"bg-muted-foreground/60\",\n};\n\nconst STATUS_LABEL: Record<AvatarStatus, string> = {\n  online: \"online\",\n  away: \"away\",\n  busy: \"busy\",\n  offline: \"offline\",\n};\n\nconst CHART_VARS = [1, 2, 3, 4, 5].map((n) => `var(--chart-${n})`);\n\n/** Deterministic fallback color from the name, so SSR and client agree. */\nfunction colorFor(item: AvatarGroupItem) {\n  if (item.color) return item.color;\n  let hash = 0;\n  for (const ch of item.name) hash = (hash * 31 + ch.charCodeAt(0)) | 0;\n  return CHART_VARS[Math.abs(hash) % CHART_VARS.length];\n}\n\nfunction initials(name: string) {\n  const parts = name.trim().split(/\\s+/);\n  return ((parts[0]?.[0] ?? \"\") + (parts[1]?.[0] ?? \"\")).toUpperCase();\n}\n\nfunction describe(item: AvatarGroupItem) {\n  const bits = [item.name];\n  if (item.status) bits.push(STATUS_LABEL[item.status]);\n  if (item.typing) bits.push(\"typing\");\n  return bits.join(\", \");\n}\n\n/**\n * Overlapping avatar stack that fans apart on hover or keyboard focus.\n * Each avatar carries a name tooltip, an optional presence dot and an\n * \"is typing\" pulse; avatars past `max` collapse into a \"+N\" chip that opens\n * the full list in a popover.\n */\nexport function AvatarGroup({\n  items,\n  max = 4,\n  size = \"md\",\n  onSelect,\n  label = \"Collaborators\",\n  className,\n  ringClassName = \"ring-background\",\n}: AvatarGroupProps) {\n  const reduce = useReducedMotion() ?? false;\n  const canHover = useHoverCapable();\n  const [hovered, setHovered] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const [listOpen, setListOpen] = useState(false);\n  const s = SIZES[size];\n\n  const shown = items.slice(0, max);\n  const hidden = items.slice(max);\n  const expanded = (hovered || focused || listOpen) && !reduce;\n\n  return (\n    // Hover/focus only drive the decorative fan-out; every control inside is a\n    // real button.\n    <fieldset\n      className={cn(\"m-0 inline-flex min-w-0 items-center border-0 p-0\", className)}\n      onPointerEnter={(e) => {\n        if (e.pointerType === \"mouse\" && canHover) setHovered(true);\n      }}\n      onPointerLeave={() => setHovered(false)}\n      onFocus={() => setFocused(true)}\n      onBlur={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {\n          setFocused(false);\n        }\n      }}\n    >\n      <legend className=\"sr-only\">{label}</legend>\n      {shown.map((item, i) => (\n        <motion.div\n          key={item.id}\n          className=\"relative\"\n          style={{ marginLeft: i === 0 ? 0 : -s.overlap, zIndex: shown.length - i }}\n          initial={false}\n          animate={{ x: expanded ? i * s.fan : 0 }}\n          transition={SPRING_LAYOUT}\n        >\n          <Tooltip content={item.name} side=\"top\">\n            <Avatar\n              item={item}\n              size={size}\n              ringClassName={ringClassName}\n              onSelect={onSelect}\n              reduce={reduce}\n              lift={canHover && !reduce}\n            />\n          </Tooltip>\n        </motion.div>\n      ))}\n      {hidden.length > 0 ? (\n        <motion.div\n          className=\"relative\"\n          style={{ marginLeft: -s.overlap, zIndex: 0 }}\n          initial={false}\n          animate={{ x: expanded ? shown.length * s.fan : 0 }}\n          transition={SPRING_LAYOUT}\n        >\n          <MorphPopover open={listOpen} onOpenChange={setListOpen}>\n            <MorphPopoverTrigger>\n              <button\n                type=\"button\"\n                aria-label={`Show ${hidden.length} more`}\n                className={cn(\n                  \"grid place-items-center rounded-full bg-muted font-semibold text-foreground ring-2 outline-none transition-colors hover:bg-muted/80 focus-visible:ring-primary\",\n                  ringClassName,\n                  s.text,\n                )}\n                style={{ width: s.box, height: s.box }}\n              >\n                +{hidden.length}\n              </button>\n            </MorphPopoverTrigger>\n            <MorphPopoverContent align=\"start\" className=\"w-56 p-1.5\">\n              <ul\n                aria-label={`All ${items.length} ${label.toLowerCase()}`}\n                className=\"max-h-64 overflow-y-auto [scrollbar-color:var(--border-strong)_transparent] [scrollbar-width:thin]\"\n              >\n                {items.map((item) => {\n                  const row = (\n                    <>\n                      <Avatar\n                        item={item}\n                        size=\"sm\"\n                        ringClassName=\"ring-background\"\n                        reduce={reduce}\n                        decorative\n                      />\n                      <span className=\"min-w-0 flex-1 truncate text-sm text-foreground\">\n                        {item.name}\n                      </span>\n                      {item.typing ? (\n                        <span className=\"text-xs text-muted-foreground\">\n                          typing…\n                        </span>\n                      ) : item.status ? (\n                        <span className=\"text-xs capitalize text-muted-foreground\">\n                          {STATUS_LABEL[item.status]}\n                        </span>\n                      ) : null}\n                    </>\n                  );\n                  return (\n                    <li key={item.id}>\n                      {onSelect ? (\n                        <button\n                          type=\"button\"\n                          onClick={() => {\n                            onSelect(item);\n                            setListOpen(false);\n                          }}\n                          className=\"flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left outline-none transition-colors hover:bg-muted focus-visible:bg-muted\"\n                        >\n                          {row}\n                        </button>\n                      ) : (\n                        <div className=\"flex items-center gap-2.5 rounded-lg px-2 py-1.5\">\n                          {row}\n                        </div>\n                      )}\n                    </li>\n                  );\n                })}\n              </ul>\n            </MorphPopoverContent>\n          </MorphPopover>\n        </motion.div>\n      ) : null}\n    </fieldset>\n  );\n}\n\n// Typing pulse: a slow breathe so it reads as \"activity\" rather than an alert.\nconst TYPING_PULSE = {\n  duration: 1.4,\n  repeat: Number.POSITIVE_INFINITY,\n  ease: EASE_OUT,\n} as const;\n\nfunction Avatar({\n  item,\n  size,\n  ringClassName,\n  onSelect,\n  reduce,\n  lift = false,\n  decorative = false,\n}: {\n  item: AvatarGroupItem;\n  size: AvatarGroupSize;\n  ringClassName: string;\n  onSelect?: (item: AvatarGroupItem) => void;\n  reduce: boolean;\n  lift?: boolean;\n  decorative?: boolean;\n}) {\n  const s = SIZES[size];\n  const color = colorFor(item);\n  const face = (\n    <>\n      {item.typing ? (\n        reduce ? (\n          <span\n            aria-hidden\n            className=\"pointer-events-none absolute -inset-1 rounded-full border-2 border-primary/50\"\n          />\n        ) : (\n          <motion.span\n            aria-hidden\n            className=\"pointer-events-none absolute -inset-1 rounded-full border-2 border-primary\"\n            initial={{ opacity: 0.7, scale: 0.95 }}\n            animate={{ opacity: 0, scale: 1.25 }}\n            transition={TYPING_PULSE}\n          />\n        )\n      ) : null}\n      <span\n        className={cn(\n          \"relative grid size-full place-items-center overflow-hidden rounded-full font-semibold text-white ring-2\",\n          ringClassName,\n          s.text,\n        )}\n        style={{ background: item.src ? undefined : color }}\n      >\n        {item.src ? (\n          // biome-ignore lint/performance/noImgElement: library component, no framework image loader assumed.\n          <img src={item.src} alt=\"\" className=\"size-full object-cover\" draggable={false} />\n        ) : (\n          initials(item.name)\n        )}\n      </span>\n      {item.status ? (\n        <span\n          aria-hidden\n          className={cn(\n            \"absolute bottom-0 right-0 rounded-full ring-2\",\n            ringClassName,\n            STATUS_CLASS[item.status],\n          )}\n          style={{ width: s.dot, height: s.dot }}\n        />\n      ) : null}\n    </>\n  );\n\n  const box = { width: s.box, height: s.box };\n  const hover = lift ? { y: -3 } : undefined;\n\n  if (decorative) {\n    return (\n      <span aria-hidden className=\"relative inline-block shrink-0\" style={box}>\n        {face}\n      </span>\n    );\n  }\n\n  if (onSelect) {\n    return (\n      <motion.button\n        type=\"button\"\n        aria-label={describe(item)}\n        onClick={() => onSelect(item)}\n        whileHover={hover}\n        transition={SPRING_LAYOUT}\n        className=\"relative block shrink-0 rounded-full outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n        style={box}\n      >\n        {face}\n      </motion.button>\n    );\n  }\n\n  return (\n    <motion.span\n      role=\"img\"\n      aria-label={describe(item)}\n      whileHover={hover}\n      transition={SPRING_LAYOUT}\n      className=\"relative block shrink-0\"\n      style={box}\n    >\n      {face}\n    </motion.span>\n  );\n}\n\n/* -------------------------------------------------------------------------- */\n/*                                Live cursors                                */\n/* -------------------------------------------------------------------------- */\n\nexport interface LiveCursor {\n  id: string;\n  name: string;\n  /** Any CSS color. */\n  color: string;\n  /** Horizontal position, percent of the container width (0–100). */\n  x: number;\n  /** Vertical position, percent of the container height (0–100). */\n  y: number;\n}\n\nexport interface LiveCursorsProps {\n  cursors: LiveCursor[];\n  className?: string;\n}\n\n/**\n * Collaboration cursors layered over a `relative` container. Positions arrive\n * as container-relative percentages (e.g. from a presence channel) and each\n * cursor springs toward its latest point, smoothing out sparse updates.\n */\nexport function LiveCursors({ cursors, className }: LiveCursorsProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const [size, setSize] = useState({ width: 0, height: 0 });\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    const observer = new ResizeObserver(([entry]) => {\n      const { width, height } = entry.contentRect;\n      setSize((prev) =>\n        prev.width === width && prev.height === height\n          ? prev\n          : { width, height },\n      );\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    <div\n      ref={ref}\n      aria-hidden\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n    >\n      {size.width > 0\n        ? cursors.map((cursor) => (\n            <CursorMark\n              key={cursor.id}\n              cursor={cursor}\n              width={size.width}\n              height={size.height}\n            />\n          ))\n        : null}\n    </div>\n  );\n}\n\nfunction CursorMark({\n  cursor,\n  width,\n  height,\n}: {\n  cursor: LiveCursor;\n  width: number;\n  height: number;\n}) {\n  const reduce = useReducedMotion() ?? false;\n  const toPx = useCallback(\n    (pct: number, extent: number) => (Math.min(Math.max(pct, 0), 100) / 100) * extent,\n    [],\n  );\n  const targetX = useMotionValue(toPx(cursor.x, width));\n  const targetY = useMotionValue(toPx(cursor.y, height));\n  const springX = useSpring(targetX, SPRING_MOUSE);\n  const springY = useSpring(targetY, SPRING_MOUSE);\n\n  useEffect(() => {\n    targetX.set(toPx(cursor.x, width));\n    targetY.set(toPx(cursor.y, height));\n  }, [cursor.x, cursor.y, width, height, targetX, targetY, toPx]);\n\n  return (\n    <motion.div\n      className=\"absolute left-0 top-0\"\n      style={{ x: reduce ? targetX : springX, y: reduce ? targetY : springY }}\n    >\n      <svg\n        aria-hidden=\"true\"\n        width=\"18\"\n        height=\"18\"\n        viewBox=\"0 0 18 18\"\n        fill=\"none\"\n        className=\"block drop-shadow-sm\"\n      >\n        <path\n          d=\"M2 1.5 L15.5 8.2 L9.4 9.6 L6.6 15.5 Z\"\n          fill={cursor.color}\n          stroke=\"white\"\n          strokeWidth=\"1.4\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n      <span\n        className=\"ml-3 -mt-0.5 inline-block whitespace-nowrap rounded-full px-2 py-0.5 text-[11px] font-medium text-white shadow-sm\"\n        style={{ background: cursor.color }}\n      >\n        {cursor.name}\n      </span>\n    </motion.div>\n  );\n}\n"},{"path":"components/motion/button/index.tsx","type":"registry:component","target":"@components/motion/button/index.tsx","content":"export type {\n  ButtonLinkProps,\n  ButtonProps,\n  ButtonSize,\n  ButtonVariant,\n} from \"./base\";\nexport { Button, ButtonLink } from \"./base\";\nexport type { MagneticButtonProps } from \"./magnetic\";\nexport { MagneticButton } from \"./magnetic\";\nexport type { ButtonState, StatefulButtonProps } from \"./stateful\";\nexport { StatefulButton } from \"./stateful\";\n"},{"path":"components/motion/input.tsx","type":"registry:component","target":"@components/motion/input.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type InputHTMLAttributes,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type InputClassNames = {\n  root?: string;\n  label?: string;\n  field?: string;\n  input?: string;\n  leftIcon?: string;\n  rightIcon?: string;\n  successIcon?: string;\n  errorMessage?: string;\n};\n\nexport interface InputProps extends Omit<\n  InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> {\n  label?: string;\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  /** Truthy error triggers a shake, red border and (if a string) a message. */\n  error?: string | boolean;\n  success?: boolean;\n  leftIcon?: ReactNode;\n  rightIcon?: ReactNode;\n  className?: string;\n  classNames?: InputClassNames;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n  {\n    label,\n    value: valueProp,\n    defaultValue,\n    onChange,\n    onFocus,\n    onBlur,\n    error,\n    success,\n    leftIcon,\n    rightIcon,\n    className,\n    classNames,\n    disabled,\n    id: idProp,\n    type,\n    ...rest\n  },\n  ref,\n) {\n  const reactId = useId();\n  const id = idProp ?? reactId;\n  const reduce = useReducedMotion();\n\n  const controlled = valueProp !== undefined;\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const value = controlled ? (valueProp ?? \"\") : internal;\n\n  const [focused, setFocused] = useState(false);\n\n  const fieldRef = useRef<HTMLDivElement>(null);\n\n  const hasError = Boolean(error);\n  const errorMessage = typeof error === \"string\" ? error : null;\n\n  // Right edge shows the success check, otherwise the caller's right icon.\n  const rightSlot = success ? null : rightIcon;\n\n  // Shake the field when an error appears.\n  useEffect(() => {\n    if (!fieldRef.current || reduce || !hasError) return;\n    animate(\n      fieldRef.current,\n      { x: [0, -6, 6, -4, 4, -2, 0] },\n      { duration: 0.45 },\n    );\n  }, [hasError, reduce]);\n\n  const handleChange = (next: string) => {\n    if (!controlled) setInternal(next);\n    onChange?.(next);\n  };\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-1.5\", className, classNames?.root)}\n    >\n      {label ? (\n        <label\n          htmlFor={id}\n          className={cn(\n            \"px-1 text-sm font-medium text-foreground\",\n            classNames?.label,\n          )}\n        >\n          {label}\n        </label>\n      ) : null}\n\n      <div\n        ref={fieldRef}\n        data-state={\n          hasError\n            ? \"error\"\n            : success\n              ? \"success\"\n              : focused\n                ? \"focused\"\n                : \"idle\"\n        }\n        className={cn(\n          \"relative h-11 overflow-hidden rounded-full border transition-colors duration-200\",\n          \"border-border\",\n          focused && !hasError && \"border-foreground/40 ring-2 ring-ring/40\",\n          hasError && \"border-destructive ring-2 ring-destructive/25\",\n          disabled && \"opacity-60\",\n          classNames?.field,\n        )}\n      >\n        {leftIcon ? (\n          <span\n            className={cn(\n              \"pointer-events-none absolute left-3 top-1/2 flex -translate-y-1/2 items-center text-muted-foreground [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.leftIcon,\n            )}\n          >\n            {leftIcon}\n          </span>\n        ) : null}\n\n        <input\n          ref={ref}\n          id={id}\n          type={type}\n          value={value}\n          disabled={disabled}\n          aria-invalid={hasError || undefined}\n          aria-describedby={errorMessage ? `${id}-error` : undefined}\n          {...rest}\n          onChange={(e) => handleChange(e.target.value)}\n          onFocus={(event) => {\n            setFocused(true);\n            onFocus?.(event);\n          }}\n          onBlur={(event) => {\n            setFocused(false);\n            onBlur?.(event);\n          }}\n          className={cn(\n            \"peer h-full w-full bg-transparent text-base leading-6 text-foreground caret-foreground outline-none\",\n            \"placeholder:text-muted-foreground/60\",\n            leftIcon ? \"pl-10\" : \"pl-3.5\",\n            rightSlot || success ? \"pr-10\" : \"pr-3.5\",\n            disabled && \"cursor-not-allowed\",\n            classNames?.input,\n          )}\n        />\n\n        {success ? (\n          <motion.svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            className={cn(\n              \"absolute right-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-(--color-success)\",\n              classNames?.successIcon,\n            )}\n          >\n            <motion.path\n              d=\"M5 12.5l4.5 4.5L19 7.5\"\n              stroke=\"currentColor\"\n              strokeWidth={2.5}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n              animate={{ pathLength: 1 }}\n              transition={{ duration: 0.35, ease: \"easeOut\" }}\n            />\n          </motion.svg>\n        ) : rightSlot ? (\n          <span\n            className={cn(\n              \"absolute right-0 top-0 flex h-full items-center text-muted-foreground [&_button]:grid [&_button]:size-11 [&_button]:place-items-center [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.rightIcon,\n            )}\n          >\n            {rightSlot}\n          </span>\n        ) : null}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {errorMessage ? (\n          <motion.p\n            id={`${id}-error`}\n            role=\"alert\"\n            initial={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            transition={{ duration: 0.2 }}\n            className={cn(\n              \"px-1 text-xs text-destructive\",\n              classNames?.errorMessage,\n            )}\n          >\n            {errorMessage}\n          </motion.p>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n});\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"components/motion/popover-morph.tsx","type":"registry:component","target":"@components/motion/popover-morph.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  cloneElement,\n  createContext,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { usePopoverPortalPosition } from \"@/components/motion/popover-position\";\nimport { EASE_OUT, SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype Side = \"top\" | \"bottom\";\ntype Align = \"start\" | \"end\";\n\ntype MorphContextValue = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  toggle: () => void;\n  triggerId: string;\n  contentId: string;\n  /** The element the panel measures against — see `registerTrigger`. */\n  triggerRef: React.MutableRefObject<HTMLElement | null>;\n  registerTrigger: (node: HTMLElement | null) => void;\n  contentRef: React.MutableRefObject<HTMLDivElement | null>;\n};\n\nconst MorphContext = createContext<MorphContextValue | null>(null);\n\nfunction useMorphContext(component: string) {\n  const ctx = useContext(MorphContext);\n  if (!ctx) throw new Error(`${component} must be used within <MorphPopover>`);\n  return ctx;\n}\n\nexport interface MorphPopoverProps {\n  children: ReactNode;\n  /** Controlled open state. */\n  open?: boolean;\n  /** Uncontrolled initial open state. */\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  className?: string;\n}\n\n/**\n * A popover whose panel morphs open from the trigger corner: it's laid out at\n * full size but clipped to the corner nearest the trigger, then unclips as one\n * piece. Closes on outside pointer / Escape. Controlled or uncontrolled.\n */\nexport function MorphPopover({\n  children,\n  open: controlledOpen,\n  defaultOpen = false,\n  onOpenChange,\n  className,\n}: MorphPopoverProps) {\n  const baseId = useId();\n  const [root, setRoot] = useState<HTMLDivElement | null>(null);\n  const [trigger, setTrigger] = useState<HTMLElement | null>(null);\n  const contentRef = useRef<HTMLDivElement | null>(null);\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const controlled = controlledOpen !== undefined;\n  const open = controlled ? controlledOpen : internalOpen;\n\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (!controlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlled, onOpenChange],\n  );\n  const toggle = useCallback(() => setOpen(!open), [setOpen, open]);\n\n  // A trigger normally registers itself through MorphPopoverTrigger. It can't\n  // when something else already clones the element — a Tooltip wrapping the\n  // button, say — and an unregistered trigger leaves the panel with nothing to\n  // measure against, so it renders permanently invisible. The root boxes the\n  // trigger exactly (the content portals out of it), so it stands in until a\n  // real trigger registers, and stands in again if that one unmounts. Both are\n  // state, so a trigger arriving while the panel is open re-anchors it.\n  const anchorRef = useMemo<React.MutableRefObject<HTMLElement | null>>(\n    () => ({ current: trigger ?? root }),\n    [root, trigger],\n  );\n\n  // The panel is a `role=\"dialog\"` and goes inert the moment it closes, so\n  // focus cannot be left sitting inside it: a dismissal hands it back to the\n  // trigger, the way the ARIA dialog pattern asks. A pointer dismissal takes\n  // the focus onward itself when it lands on something focusable — this only\n  // catches the case where it would otherwise be stranded. When no trigger has\n  // registered, the root anchor stands in only if it can actually hold focus;\n  // there is nowhere better than where the keyboard already is, so leave it.\n  const close = useCallback(() => {\n    setOpen(false);\n    const focused = document.activeElement;\n    const inPanel =\n      focused instanceof HTMLElement && contentRef.current?.contains(focused);\n    if (!inPanel) return;\n    const restore = trigger ?? (root && root.tabIndex >= 0 ? root : null);\n    restore?.focus();\n  }, [root, setOpen, trigger]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && close();\n    const onPointer = (e: PointerEvent) => {\n      const target = e.target as Node;\n      if (\n        root &&\n        !root.contains(target) &&\n        !contentRef.current?.contains(target)\n      )\n        close();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    window.addEventListener(\"pointerdown\", onPointer);\n    return () => {\n      window.removeEventListener(\"keydown\", onKey);\n      window.removeEventListener(\"pointerdown\", onPointer);\n    };\n  }, [open, root, close]);\n\n  const ctx = useMemo<MorphContextValue>(\n    () => ({\n      open,\n      setOpen,\n      toggle,\n      triggerId: `${baseId}-trigger`,\n      contentId: `${baseId}-content`,\n      triggerRef: anchorRef,\n      registerTrigger: setTrigger,\n      contentRef,\n    }),\n    [open, setOpen, toggle, baseId, anchorRef],\n  );\n\n  return (\n    <MorphContext.Provider value={ctx}>\n      <div ref={setRoot} className={cn(\"relative inline-flex\", className)}>\n        {children}\n      </div>\n    </MorphContext.Provider>\n  );\n}\n\nexport interface MorphPopoverTriggerProps {\n  children: ReactElement;\n}\n\nfunction mergeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref && typeof ref === \"object\")\n        (ref as React.MutableRefObject<T | null>).current = node;\n    }\n  };\n}\n\n/** Wraps a single element, toggling the popover on click. */\nexport function MorphPopoverTrigger({ children }: MorphPopoverTriggerProps) {\n  const ctx = useMorphContext(\"MorphPopoverTrigger\");\n  if (!isValidElement(children)) return children;\n\n  const child = children as ReactElement<Record<string, unknown>>;\n  const childOnClick = child.props.onClick as\n    | ((e: unknown) => void)\n    | undefined;\n  const childRef = (child.props as { ref?: Ref<HTMLElement> }).ref;\n\n  return cloneElement(child, {\n    id: ctx.triggerId,\n    ref: mergeRefs(childRef, ctx.registerTrigger),\n    onClick: (e: unknown) => {\n      childOnClick?.(e);\n      ctx.toggle();\n    },\n    \"aria-haspopup\": \"dialog\",\n    \"aria-expanded\": ctx.open,\n    \"aria-controls\": ctx.open ? ctx.contentId : undefined,\n  });\n}\n\nconst originFor = (side: Side, align: Align) =>\n  `${side === \"bottom\" ? \"top\" : \"bottom\"} ${align === \"end\" ? \"right\" : \"left\"}`;\n\n// A clip that hides everything but the corner nearest the trigger, so the\n// panel appears to grow out of it. inset(top right bottom left).\nfunction clipHidden(side: Side, align: Align, radius: number) {\n  const top = side === \"bottom\" ? \"0%\" : \"92%\";\n  const bottom = side === \"bottom\" ? \"92%\" : \"0%\";\n  const right = align === \"end\" ? \"0%\" : \"92%\";\n  const left = align === \"end\" ? \"92%\" : \"0%\";\n  return `inset(${top} ${right} ${bottom} ${left} round ${radius}px)`;\n}\nconst clipShown = (radius: number) => `inset(0% 0% 0% 0% round ${radius}px)`;\n\n// Preserve the original spring character on the wrapper, but tween the complex\n// clip-path so it cannot snap when the spring resolves its final distance.\nconst MORPH_CLIP_TRANSITION = { duration: 0.32, ease: EASE_OUT } as const;\n\nexport interface MorphPopoverContentProps {\n  children: ReactNode;\n  side?: Side;\n  align?: Align;\n  /** Gap between trigger and panel, in px. Default 8. */\n  sideOffset?: number;\n  /** Panel corner radius, in px. Default 16. */\n  radius?: number;\n  className?: string;\n}\n\nexport function MorphPopoverContent({\n  children,\n  side = \"bottom\",\n  align = \"end\",\n  sideOffset = 8,\n  radius = 16,\n  className,\n}: MorphPopoverContentProps) {\n  const ctx = useMorphContext(\"MorphPopoverContent\");\n  const reduce = useReducedMotion() ?? false;\n  const [portalReady, setPortalReady] = useState(false);\n  const layout = usePopoverPortalPosition(\n    ctx.triggerRef,\n    ctx.contentRef,\n    portalReady && ctx.open,\n  );\n\n  useEffect(() => setPortalReady(true), []);\n  const left = layout\n    ? align === \"end\"\n      ? layout.trigger.left + layout.trigger.width - layout.content.width\n      : layout.trigger.left\n    : 0;\n  const top = layout\n    ? side === \"bottom\"\n      ? layout.trigger.top + layout.trigger.height + sideOffset\n      : layout.trigger.top - layout.content.height - sideOffset\n    : 0;\n\n  // Both directions travel between the exact same hidden/show states. Exit\n  // targets \"hidden\" directly instead of introducing separate choreography.\n  const wrap = reduce\n    ? undefined\n    : {\n        hidden: { opacity: 0, scale: 0.96, transition: SPRING_PANEL },\n        show: { opacity: 1, scale: 1, transition: SPRING_PANEL },\n      };\n  const clip = reduce\n    ? undefined\n    : {\n        hidden: {\n          clipPath: clipHidden(side, align, radius),\n          transition: MORPH_CLIP_TRANSITION,\n        },\n        show: {\n          clipPath: clipShown(radius),\n          transition: MORPH_CLIP_TRANSITION,\n        },\n      };\n\n  // Keep the server and first client render identical, then mount the portal.\n  if (!portalReady) return null;\n\n  return createPortal(\n    <AnimatePresence>\n      {ctx.open ? (\n        <motion.div\n          data-morph-popover-portal=\"\"\n          // Wrapper carries the shadow as a drop-shadow filter, which hugs the\n          // clipped shape below (box-shadow would just get clipped away).\n          variants={wrap}\n          initial={reduce ? { opacity: 0 } : \"hidden\"}\n          animate={reduce ? { opacity: 1 } : \"show\"}\n          exit={reduce ? { opacity: 0 } : \"hidden\"}\n          transition={reduce ? { duration: 0.12 } : undefined}\n          style={{\n            left,\n            top,\n            visibility: layout ? \"visible\" : \"hidden\",\n            transformOrigin: originFor(side, align),\n          }}\n          className=\"fixed z-[9999] [filter:drop-shadow(0_10px_18px_rgba(0,0,0,0.14))]\"\n        >\n          <motion.div\n            ref={ctx.contentRef}\n            id={ctx.contentId}\n            role=\"dialog\"\n            aria-labelledby={ctx.triggerId}\n            variants={clip}\n            style={{ borderRadius: radius }}\n            className={cn(\n              \"overflow-hidden border border-border bg-background\",\n              className,\n            )}\n          >\n            {children}\n          </motion.div>\n        </motion.div>\n      ) : null}\n    </AnimatePresence>,\n    document.body,\n  );\n}\n"},{"path":"components/motion/tooltip.tsx","type":"registry:component","target":"@components/motion/tooltip.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  cloneElement,\n  isValidElement,\n  type PointerEvent,\n  type ReactElement,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { useDismiss } from \"@/lib/hooks/use-dismiss\";\nimport { useHoverGesture } from \"@/lib/hooks/use-hover-gesture\";\nimport { useTapGesture } from \"@/lib/hooks/use-tap-gesture\";\nimport { cn } from \"@/lib/utils\";\n\ntype Side = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport interface TooltipProps {\n  content: ReactNode;\n  children: ReactElement;\n  side?: Side;\n  /** Delay before showing (ms). Default 120. */\n  delay?: number;\n  className?: string;\n  /** Classes for the outer wrapper span. Use to fix baseline / fill parent. */\n  wrapperClassName?: string;\n}\n\n// Gap between trigger and tooltip, in px.\nconst GAP = 8;\n\n// Centering transform for the fixed-positioned anchor point, per side.\nconst anchorTransform: Record<Side, string> = {\n  top: \"translate(-50%, -100%)\",\n  bottom: \"translate(-50%, 0)\",\n  left: \"translate(-100%, -50%)\",\n  right: \"translate(0, -50%)\",\n};\n\nconst transformOrigin: Record<Side, string> = {\n  top: \"center bottom\",\n  bottom: \"center top\",\n  left: \"right center\",\n  right: \"left center\",\n};\n\n// Offset is in the direction *away* from the trigger — content originates near\n// the trigger and rises into resting position.\nconst offsetFrom: Record<Side, { x?: number; y?: number }> = {\n  top: { y: 8 },\n  bottom: { y: -8 },\n  left: { x: 8 },\n  right: { x: -8 },\n};\n\nfunction buildVariants(side: Side): Variants {\n  const o = offsetFrom[side];\n  return {\n    initial: {\n      opacity: 0,\n      scale: 0.9,\n      filter: \"blur(5px)\",\n      x: o.x ?? 0,\n      y: o.y ?? 0,\n    },\n    animate: {\n      opacity: 1,\n      scale: 1,\n      filter: \"blur(0px)\",\n      x: 0,\n      y: 0,\n      transition: {\n        type: \"spring\",\n        stiffness: 380,\n        damping: 30,\n        mass: 0.7,\n        opacity: { duration: 0.14, ease: EASE_OUT },\n        filter: { duration: 0.18, ease: EASE_OUT },\n      },\n    },\n    exit: {\n      opacity: 0,\n      scale: 0.94,\n      filter: \"blur(3px)\",\n      x: (o.x ?? 0) * 0.6,\n      y: (o.y ?? 0) * 0.6,\n      transition: { duration: 0.12, ease: EASE_OUT },\n    },\n  };\n}\n\nconst REDUCED_VARIANTS: Variants = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1, transition: { duration: 0.14, ease: EASE_OUT } },\n  exit: { opacity: 0, transition: { duration: 0.1, ease: EASE_OUT } },\n};\n\n// Once any tooltip has just closed, neighbouring tooltips open without the\n// initial delay — moving along a toolbar feels instant after the first one.\nconst WARM_WINDOW_MS = 300;\nlet lastHiddenAt = 0;\n\nexport function Tooltip({\n  content,\n  children,\n  side = \"top\",\n  delay = 120,\n  className,\n  wrapperClassName,\n}: TooltipProps) {\n  const [open, setOpen] = useState(false);\n  const [coords, setCoords] = useState<{ top: number; left: number } | null>(\n    null,\n  );\n  const id = useId();\n  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const anchorRef = useRef<HTMLSpanElement>(null);\n  const hover = useHoverGesture();\n  const reduce = useReducedMotion();\n\n  // Anchor point in viewport coords, on the edge of the trigger facing `side`.\n  // Position:fixed means these viewport coords place the tooltip directly, so\n  // it escapes every ancestor's stacking context and overflow.\n  const place = useCallback(() => {\n    const el = anchorRef.current;\n    if (!el) return;\n    const r = el.getBoundingClientRect();\n    const cx = r.left + r.width / 2;\n    const cy = r.top + r.height / 2;\n    const point: Record<Side, { top: number; left: number }> = {\n      top: { top: r.top - GAP, left: cx },\n      bottom: { top: r.bottom + GAP, left: cx },\n      left: { top: cy, left: r.left - GAP },\n      right: { top: cy, left: r.right + GAP },\n    };\n    setCoords(point[side]);\n  }, [side]);\n\n  const show = useCallback(() => {\n    if (timer.current) clearTimeout(timer.current);\n    const warm = Date.now() - lastHiddenAt < WARM_WINDOW_MS;\n    timer.current = setTimeout(\n      () => {\n        place();\n        setOpen(true);\n      },\n      warm ? 0 : delay,\n    );\n  }, [delay, place]);\n\n  const hide = useCallback(() => {\n    if (timer.current) {\n      clearTimeout(timer.current);\n      timer.current = null;\n    }\n    if (open) lastHiddenAt = Date.now();\n    setOpen(false);\n  }, [open]);\n\n  // A finger never hovers, and Safari does not focus a button on tap either, so\n  // the label is only reachable if the tap itself opens the tooltip. A click\n  // carries no pointerType, so the pointerdown that preceded it is what says\n  // whether this was a tap; keyboard activation arrives with no pointerdown at\n  // all, and focus has already shown the label there.\n  const tap = useTapGesture<boolean>();\n\n  const toggleOnTap = useCallback(() => {\n    const gesture = tap.take();\n    if (!gesture || gesture.pointerType === \"mouse\") return;\n    if (gesture.state) {\n      hide();\n      return;\n    }\n    if (timer.current) clearTimeout(timer.current);\n    place();\n    setOpen(true);\n  }, [hide, place, tap]);\n\n  // ...and closed again by the next tap that lands somewhere else. The label\n  // covers nothing interactive, so that tap passes through to what it hit.\n  useDismiss(open, hide, anchorRef);\n\n  // Keep the tooltip pinned to the trigger while it's open and the page scrolls\n  // or resizes (fixed coords are viewport-relative).\n  useEffect(() => {\n    if (!open) return;\n    const onMove = () => place();\n    window.addEventListener(\"scroll\", onMove, true);\n    window.addEventListener(\"resize\", onMove);\n    return () => {\n      window.removeEventListener(\"scroll\", onMove, true);\n      window.removeEventListener(\"resize\", onMove);\n    };\n  }, [open, place]);\n\n  const variants = useMemo(\n    () => (reduce ? REDUCED_VARIANTS : buildVariants(side)),\n    [reduce, side],\n  );\n\n  if (!isValidElement(children)) return children;\n\n  // The label describes the trigger, so it has to name the trigger itself.\n  // Everything else the tooltip needs is read off the anchor below instead of\n  // cloned on: a handler written onto the child is the child's handler as far\n  // as that child can tell, and a component that owns its activation —\n  // hard-wiring onClick and spreading the rest of its props over it, as\n  // ThemeToggle does — then runs the tooltip's instead of its own. Composing\n  // with `props.onClick` cannot save it either, because a component element's\n  // props hold nothing the component does internally.\n  const trigger = cloneElement(children as ReactElement<Record<string, unknown>>, {\n    \"aria-describedby\": id,\n  });\n\n  return (\n    <>\n      {/* biome-ignore lint/a11y/noStaticElementInteractions: the anchor is not a\n          control — it observes the trigger it wraps. Every event listed reaches\n          it on its own (pointerdown/click/keydown/pointercancel bubble, focus\n          and blur arrive as focusin/focusout, and enter/leave are derived from\n          pointerover/pointerout along a path the anchor is on), so the trigger\n          keeps every handler it came with. */}\n      <span\n        ref={anchorRef}\n        className={cn(\"relative inline-flex align-middle\", wrapperClassName)}\n        // Pointer events, not the mouse pair: a tap fires compatibility\n        // mouseenter/mouseleave that carry no pointerType, which raced the tap\n        // path into opening and closing the same label.\n        onPointerEnter={(event: PointerEvent) => {\n          if (hover.enter(event)) show();\n        }}\n        onPointerLeave={(event: PointerEvent) => {\n          if (hover.leave(event)) hide();\n        }}\n        onFocus={show}\n        onBlur={hide}\n        onPointerDown={(event: PointerEvent) => tap.start(event, open)}\n        // A gesture the platform took away sends no click, and a key press\n        // starts an activation that never had a pointer behind it. Either way\n        // the record has to go, or the next click reads a finger that has long\n        // since lifted.\n        onPointerCancel={tap.drop}\n        onKeyDown={tap.drop}\n        onClick={toggleOnTap}\n      >\n        {trigger}\n      </span>\n      {typeof document !== \"undefined\"\n        ? createPortal(\n            <AnimatePresence>\n              {open && coords ? (\n                <span\n                  aria-hidden\n                  className=\"pointer-events-none fixed z-[9999]\"\n                  style={{\n                    top: coords.top,\n                    left: coords.left,\n                    transform: anchorTransform[side],\n                  }}\n                >\n                  <motion.span\n                    id={id}\n                    role=\"tooltip\"\n                    variants={variants}\n                    initial=\"initial\"\n                    animate=\"animate\"\n                    exit=\"exit\"\n                    style={{ transformOrigin: transformOrigin[side] }}\n                    className={cn(\n                      \"block whitespace-nowrap rounded-lg border border-border bg-background px-2.5 py-1 text-xs font-medium text-foreground shadow-lg\",\n                      className,\n                    )}\n                  >\n                    {content}\n                  </motion.span>\n                </span>\n              ) : null}\n            </AnimatePresence>,\n            document.body,\n          )\n        : null}\n    </>\n  );\n}\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"registry:hook","target":"@lib/hooks/use-hover-capable.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"components/motion/button/base.tsx","type":"registry:component","target":"@components/motion/button/base.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { TOUCH_TARGET_CLASS } from \"@/lib/touch\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\nexport interface ButtonProps extends Omit<\n  HTMLMotionProps<\"button\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  /** Spawn a Material-style ripple from the press point. Off by default. */\n  ripple?: boolean;\n  children?: ReactNode;\n}\n\nexport interface ButtonLinkProps extends Omit<\n  HTMLMotionProps<\"a\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  children?: ReactNode;\n}\n\ntype Ripple = { id: number; x: number; y: number; size: number };\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n  secondary: \"border border-border bg-card text-foreground hover:border-border\",\n  ghost: \"text-muted-foreground hover:text-foreground hover:bg-primary/5\",\n  outline:\n    \"border border-border bg-transparent text-foreground hover:bg-primary/5\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  function Button(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      ripple = false,\n      className,\n      children,\n      onPointerDown,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n    const [ripples, setRipples] = useState<Ripple[]>([]);\n    const nextId = useRef(0);\n\n    const handlePointerDown = useCallback(\n      (event: PointerEvent<HTMLButtonElement>) => {\n        if (ripple && !reduce) {\n          const rect = event.currentTarget.getBoundingClientRect();\n          const size = Math.max(rect.width, rect.height) * 2;\n          const id = nextId.current++;\n          setRipples((prev) => [\n            ...prev,\n            {\n              id,\n              x: event.clientX - rect.left,\n              y: event.clientY - rect.top,\n              size,\n            },\n          ]);\n        }\n        onPointerDown?.(event);\n      },\n      [ripple, reduce, onPointerDown],\n    );\n\n    return (\n      <motion.button\n        ref={ref}\n        type=\"button\"\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        onPointerDown={handlePointerDown}\n        className={cn(\n          \"relative inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          // The ripple layer clips itself, so on a touch screen the button can\n          // stop clipping and let its enlarged hit area overhang.\n          ripple && \"overflow-hidden pointer-coarse:overflow-visible\",\n          TOUCH_TARGET_CLASS,\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {ripple && !reduce ? (\n          <span className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]\">\n            <AnimatePresence>\n              {ripples.map((r) => (\n                <motion.span\n                  key={r.id}\n                  className=\"absolute rounded-full bg-current\"\n                  style={{\n                    left: r.x,\n                    top: r.y,\n                    width: r.size,\n                    height: r.size,\n                    x: \"-50%\",\n                    y: \"-50%\",\n                  }}\n                  initial={{ scale: 0.05, opacity: 0.3 }}\n                  animate={{ scale: 1, opacity: 0 }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: 1.6, ease: EASE_OUT }}\n                  onAnimationComplete={() =>\n                    setRipples((prev) => prev.filter((x) => x.id !== r.id))\n                  }\n                />\n              ))}\n            </AnimatePresence>\n          </span>\n        ) : null}\n        {children}\n      </motion.button>\n    );\n  },\n);\n\nexport const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(\n  function ButtonLink(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      className,\n      children,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n\n    return (\n      <motion.a\n        ref={ref}\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        className={cn(\n          \"relative inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          TOUCH_TARGET_CLASS,\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </motion.a>\n    );\n  },\n);\n"},{"path":"components/motion/button/magnetic.tsx","type":"registry:component","target":"@components/motion/button/magnetic.tsx","content":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Magnetic } from \"../magnetic\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport interface MagneticButtonProps extends ButtonProps {\n  /** Magnetic pull strength. Default 0.25. */\n  strength?: number;\n  /** Class applied to the magnetic wrapper. */\n  magneticClassName?: string;\n}\n\nexport const MagneticButton = forwardRef<HTMLButtonElement, MagneticButtonProps>(function MagneticButton(\n  { strength = 0.25, magneticClassName, children, ...rest },\n  ref,\n) {\n  return (\n    <Magnetic strength={strength} className={magneticClassName}>\n      <Button ref={ref} {...rest}>\n        {children}\n      </Button>\n    </Magnetic>\n  );\n});\n"},{"path":"components/motion/button/stateful.tsx","type":"registry:component","target":"@components/motion/button/stateful.tsx","content":"\"use client\";\n\nimport { Check, Loader2, X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type ReactNode,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport type ButtonState = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nexport interface StatefulButtonProps extends Omit<ButtonProps, \"children\"> {\n  state?: ButtonState;\n  children: ReactNode;\n  loadingText?: ReactNode;\n  successText?: ReactNode;\n  errorText?: ReactNode;\n  icon?: ReactNode;\n}\n\nconst CASCADE_STAGGER = 0.025;\nconst ROLL_BLUR = \"blur(6px)\";\n\nconst CASCADE_LETTER_VARIANTS: Variants = {\n  initial: { opacity: 0, y: \"105%\", filter: ROLL_BLUR },\n  animate: (delay: number = 0) => ({\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: { ...SPRING_SWAP, delay },\n  }),\n  exit: (delay: number = 0) => ({\n    opacity: 0,\n    y: \"-105%\",\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT, delay: delay * 0.5 },\n  }),\n};\n\nconst ICON_VARIANTS: Variants = {\n  // Width collapses too, so the icon adds/removes its own space smoothly\n  // instead of popping the row width in a single frame.\n  initial: { opacity: 0, width: 0, scale: 0.7, filter: ROLL_BLUR },\n  animate: {\n    opacity: 1,\n    width: \"1.5rem\",\n    scale: 1,\n    filter: \"blur(0px)\",\n    transition: SPRING_SWAP,\n  },\n  exit: {\n    opacity: 0,\n    width: 0,\n    scale: 0.7,\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT },\n  },\n};\n\nfunction IconSlot({ keyId, children }: { keyId: string; children: ReactNode }) {\n  const reduce = useReducedMotion();\n  return (\n    <motion.span\n      key={keyId}\n      variants={ICON_VARIANTS}\n      initial={reduce ? { opacity: 0 } : \"initial\"}\n      animate={reduce ? { opacity: 1 } : \"animate\"}\n      exit={reduce ? { opacity: 0 } : \"exit\"}\n      transition={reduce ? { duration: 0.15 } : undefined}\n      className=\"inline-grid shrink-0 place-items-center overflow-hidden\"\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nfunction TextSlot({\n  value,\n  children,\n}: {\n  value: string;\n  children: ReactNode;\n}) {\n  const reduce = useReducedMotion();\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const [width, setWidth] = useState<number>();\n  const label = typeof children === \"string\" ? children : null;\n  const cascade = label !== null && !reduce;\n\n  // Measure strings with the same per-letter layout as the cascade. Measuring\n  // the whole string preserves kerning, which can make it narrower than the\n  // inline-block letters and clip the final glyph during the width animation.\n  useLayoutEffect(() => {\n    const nextWidth = measureRef.current?.offsetWidth;\n    if (!nextWidth) return;\n    setWidth((current) => (current === nextWidth ? current : nextWidth));\n  });\n\n  return (\n    <motion.span\n      initial={false}\n      animate={{ width }}\n      transition={reduce ? { duration: 0 } : SPRING_SWAP}\n      className=\"relative inline-block overflow-hidden whitespace-nowrap align-bottom\"\n    >\n      <span\n        ref={measureRef}\n        aria-hidden\n        className=\"invisible inline-block whitespace-nowrap\"\n      >\n        {cascade\n          ? label.split(\"\").map((char, index) => (\n              <span\n                // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                key={index}\n                className=\"inline-block whitespace-pre\"\n              >\n                {char}\n              </span>\n            ))\n          : children}\n      </span>\n\n      {cascade ? (\n        <>\n          <span className=\"sr-only\">{label}</span>\n          <AnimatePresence initial={false}>\n            <motion.span\n              key={`cascade-${value}`}\n              aria-hidden\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              className=\"absolute left-0 top-0 inline-block whitespace-pre\"\n            >\n              {label.split(\"\").map((char, index) => (\n                <motion.span\n                  // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                  key={index}\n                  custom={index * CASCADE_STAGGER}\n                  variants={CASCADE_LETTER_VARIANTS}\n                  className=\"inline-block whitespace-pre will-change-[opacity,filter,transform]\"\n                >\n                  {char}\n                </motion.span>\n              ))}\n            </motion.span>\n          </AnimatePresence>\n        </>\n      ) : (\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={`text-${value}`}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14, filter: ROLL_BLUR }}\n            animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -14, filter: ROLL_BLUR }}\n            transition={reduce ? { duration: 0.15 } : SPRING_SWAP}\n            className=\"absolute left-0 top-0 inline-block will-change-[opacity,filter,transform]\"\n          >\n            {children}\n          </motion.span>\n        </AnimatePresence>\n      )}\n    </motion.span>\n  );\n}\n\nexport const StatefulButton = forwardRef<HTMLButtonElement, StatefulButtonProps>(function StatefulButton(\n  {\n    state = \"idle\",\n    children,\n    loadingText = \"Loading\",\n    successText = \"Done\",\n    errorText = \"Try again\",\n    icon,\n    disabled,\n    ...rest\n  },\n  ref,\n) {\n  const isBusy = state === \"loading\";\n  const stateText =\n    state === \"loading\"\n      ? loadingText\n      : state === \"success\"\n        ? successText\n        : state === \"error\"\n        ? errorText\n        : children;\n  const textKey =\n    typeof stateText === \"string\" ? `${state}-${stateText}` : state;\n\n  return (\n    <Button ref={ref} disabled={disabled || isBusy} aria-busy={isBusy} whileHover={undefined} {...rest}>\n      <span\n        aria-live=\"polite\"\n        className=\"relative inline-flex items-center justify-center overflow-hidden\"\n      >\n        <AnimatePresence initial={false}>\n          {state === \"loading\" ? (\n            <IconSlot keyId=\"loading-icon\">\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n            </IconSlot>\n          ) : null}\n          {state === \"success\" ? (\n            <IconSlot keyId=\"success-icon\">\n              <Check className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n          {state === \"error\" ? (\n            <IconSlot keyId=\"error-icon\">\n              <X className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n        </AnimatePresence>\n\n        <TextSlot value={textKey}>{stateText}</TextSlot>\n\n        <AnimatePresence initial={false}>\n          {state === \"idle\" && icon ? (\n            <IconSlot keyId=\"idle-icon\">{icon}</IconSlot>\n          ) : null}\n        </AnimatePresence>\n      </span>\n    </Button>\n  );\n});\n"},{"path":"components/motion/popover-position.ts","type":"registry:component","target":"@components/motion/popover-position.ts","content":"\"use client\";\n\nimport {\n  type MutableRefObject,\n  useCallback,\n  useLayoutEffect,\n  useState,\n} from \"react\";\n\nexport type PortalLayout = {\n  trigger: {\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  };\n  content: {\n    width: number;\n    height: number;\n  };\n};\n\nfunction sameLayout(a: PortalLayout | null, b: PortalLayout) {\n  return (\n    a?.trigger.left === b.trigger.left &&\n    a.trigger.top === b.trigger.top &&\n    a.trigger.width === b.trigger.width &&\n    a.trigger.height === b.trigger.height &&\n    a.content.width === b.content.width &&\n    a.content.height === b.content.height\n  );\n}\n\n/** Measures a trigger and portalled panel in viewport coordinates. */\nexport function usePopoverPortalPosition<\n  TriggerElement extends HTMLElement,\n  ContentElement extends HTMLElement,\n>(\n  triggerRef: MutableRefObject<TriggerElement | null>,\n  contentRef: MutableRefObject<ContentElement | null>,\n  active: boolean,\n) {\n  const [layout, setLayout] = useState<PortalLayout | null>(null);\n\n  const update = useCallback(() => {\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    if (!trigger || !content) return;\n\n    const rect = trigger.getBoundingClientRect();\n    const next: PortalLayout = {\n      trigger: {\n        left: rect.left,\n        top: rect.top,\n        width: rect.width,\n        height: rect.height,\n      },\n      content: {\n        width: content.offsetWidth,\n        height: content.offsetHeight,\n      },\n    };\n    setLayout((current) => (sameLayout(current, next) ? current : next));\n  }, [contentRef, triggerRef]);\n\n  useLayoutEffect(() => {\n    update();\n    if (!active) return;\n\n    const trigger = triggerRef.current;\n    const content = contentRef.current;\n    const observer = new ResizeObserver(update);\n    if (trigger) observer.observe(trigger);\n    if (content) observer.observe(content);\n\n    window.addEventListener(\"scroll\", update, true);\n    window.addEventListener(\"resize\", update);\n    return () => {\n      observer.disconnect();\n      window.removeEventListener(\"scroll\", update, true);\n      window.removeEventListener(\"resize\", update);\n    };\n  }, [active, contentRef, triggerRef, update]);\n\n  return layout;\n}\n"},{"path":"lib/hooks/use-dismiss.ts","type":"registry:hook","target":"@lib/hooks/use-dismiss.ts","content":"\"use client\";\n\nimport { type RefObject, useEffect } from \"react\";\n\n/**\n * What the dismissing gesture does to the control it landed on.\n *\n * `\"pass-through\"` is the platform norm (native popover light-dismiss): the\n * tap closes the overlay *and* activates whatever was under it. Use\n * `\"consume\"` where the open overlay sits over or beside controls that would\n * be costly to trigger by accident — the dismissal then swallows the\n * activation too, so the gesture only closes.\n */\nexport type DismissBehavior = \"pass-through\" | \"consume\";\n\nexport interface DismissOptions {\n  /** Default `\"pass-through\"`. */\n  behavior?: DismissBehavior;\n  /** Dismiss on Escape as well. Default true. */\n  escape?: boolean;\n  /** Return true for an outside target that should *not* dismiss. Must be stable. */\n  ignore?: (target: Element) => boolean;\n}\n\n/**\n * What every currently open dismiss scope counts as inside itself. A consumed\n * dismissal reads this to tell a stray gesture from one that belongs to an\n * overlay in front of it: overlays have no shared z-order to consult, but the\n * one the gesture landed in has said as much by registering it.\n */\nconst openScopes = new Set<(target: Element) => boolean>();\n\nfunction claimedByAnotherScope(\n  self: (target: Element) => boolean,\n  target: Element,\n) {\n  for (const scope of openScopes) {\n    if (scope !== self && scope(target)) return true;\n  }\n  return false;\n}\n\n// preventDefault on pointerdown does not suppress the click that follows, so\n// consuming a gesture means swallowing that click itself. The swallower\n// deliberately outlives the effect that installed it — the dismissal it\n// belongs to has already unmounted or re-rendered by the time the click lands.\n// It releases on that click, or on the next gesture if the pointer is dragged\n// away and no click ever arrives, so it can never eat a later one. A keydown\n// releases it too: a gesture that ends with neither a click nor a cancel would\n// otherwise leave it armed, and the click Enter synthesizes on some focused\n// control is not the one this dismissal was owed.\nfunction consumeActivation(source: Event) {\n  const swallow = (event: MouseEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    release();\n  };\n  const restart = (event: Event) => {\n    if (event !== source) release();\n  };\n  const release = () => {\n    window.removeEventListener(\"click\", swallow, true);\n    window.removeEventListener(\"pointerdown\", restart, true);\n    window.removeEventListener(\"pointercancel\", restart, true);\n    window.removeEventListener(\"keydown\", release, true);\n  };\n  window.addEventListener(\"click\", swallow, true);\n  window.addEventListener(\"pointerdown\", restart, true);\n  window.addEventListener(\"pointercancel\", restart, true);\n  window.addEventListener(\"keydown\", release, true);\n}\n\n/**\n * Close an open overlay on Escape or a pointerdown outside `ref`. Pass `null`\n * for `ref` when what counts as inside isn't one element, and say so with\n * `ignore` instead.\n *\n * The pointerdown listener is capture-phase: a bubble-phase one is blinded by\n * any handler in between that stops propagation, and an overlay cannot know\n * what it is layered over. `onDismiss` and `ignore` must be stable (wrap in\n * useCallback) so the listeners aren't re-bound every render while open.\n */\nexport function useDismiss(\n  open: boolean,\n  onDismiss: () => void,\n  ref: RefObject<HTMLElement | null> | null,\n  {\n    behavior = \"pass-through\",\n    escape: dismissOnEscape = true,\n    ignore,\n  }: DismissOptions = {},\n) {\n  useEffect(() => {\n    if (!open) return;\n    const inside = (target: Element) =>\n      Boolean(ref?.current?.contains(target)) || Boolean(ignore?.(target));\n    const onKey = (event: KeyboardEvent) => {\n      if (dismissOnEscape && event.key === \"Escape\") onDismiss();\n    };\n    const onPointer = (event: PointerEvent) => {\n      const target = event.target as Element | null;\n      if (!target || inside(target)) return;\n      // Outside this overlay, but inside one that is also open: the gesture is\n      // that overlay's to answer, and swallowing its click from behind would\n      // cost the user the control they actually aimed at.\n      if (behavior === \"consume\" && !claimedByAnotherScope(inside, target)) {\n        consumeActivation(event);\n      }\n      onDismiss();\n    };\n    openScopes.add(inside);\n    window.addEventListener(\"keydown\", onKey);\n    window.addEventListener(\"pointerdown\", onPointer, true);\n    return () => {\n      openScopes.delete(inside);\n      window.removeEventListener(\"keydown\", onKey);\n      window.removeEventListener(\"pointerdown\", onPointer, true);\n    };\n  }, [open, onDismiss, ref, behavior, dismissOnEscape, ignore]);\n}\n"},{"path":"lib/hooks/use-hover-gesture.ts","type":"registry:hook","target":"@lib/hooks/use-hover-gesture.ts","content":"\"use client\";\n\nimport { useMemo, useRef } from \"react\";\nimport { isHoveringPointer } from \"@/lib/touch\";\n\ninterface BoundaryEvent {\n  pointerId: number;\n  pointerType: string;\n  buttons: number;\n}\n\nexport interface HoverGesture {\n  /** True when this enter starts a hover: the pointer arrived resting, not pressing. */\n  enter: (event: BoundaryEvent) => boolean;\n  /** True when this leave ends a hover that entered as one. */\n  leave: (event: BoundaryEvent) => boolean;\n}\n\n/**\n * Pairs a surface's enter with its leave, per pointer.\n *\n * `isHoveringPointer` answers the question the *enter* asks — is this pointer\n * resting on the surface or pressing it — and both boundary cases go wrong if\n * the leave is asked the same question again:\n *\n * - A pen with no hover never rests. It arrives in contact, taps, and the spec\n *   then requires its boundary events after `pointerup`, so the leave carries\n *   `buttons: 0` and reads as a mouse gliding off. Hover teardown then undid\n *   the tap — the panel the pen had just opened closed under it.\n * - A mouse pressed on the surface and dragged off leaves with `buttons: 1`.\n *   Skipping teardown there strands the surface open: the release happens\n *   outside, and no second leave ever comes.\n *\n * So the state a hover holds is released by the pointer that took it, whatever\n * the buttons say at the boundary, and a pointer that arrived in contact never\n * took it in the first place. Contact is the exception tracked here, not\n * hover: a leave from a pointer this surface never saw enter — mounted under\n * the cursor, say — still counts, since the alternative is state with no way\n * out.\n */\nexport function useHoverGesture(): HoverGesture {\n  const contact = useRef(new Set<number>());\n\n  return useMemo(\n    () => ({\n      enter: (event) => {\n        if (isHoveringPointer(event)) {\n          contact.current.delete(event.pointerId);\n          return true;\n        }\n        contact.current.add(event.pointerId);\n        return false;\n      },\n      leave: (event) => {\n        const arrivedInContact = contact.current.delete(event.pointerId);\n        return !arrivedInContact && event.pointerType !== \"touch\";\n      },\n    }),\n    [],\n  );\n}\n"},{"path":"lib/hooks/use-tap-gesture.ts","type":"registry:hook","target":"@lib/hooks/use-tap-gesture.ts","content":"\"use client\";\n\nimport { useMemo, useRef } from \"react\";\n\n/** What a pointerdown recorded, read back by the click that ends its gesture. */\nexport interface TapRecord<S> {\n  /** Which input started the gesture. */\n  pointerType: string;\n  /** What the surface was showing when it started. */\n  state: S;\n}\n\nexport interface TapGesture<S> {\n  /** Record the gesture a pointerdown starts, with the state it starts in. */\n  start: (event: { pointerType: string }, state: S) => void;\n  /** Read the record and clear it. `null` when no pointer is behind this click. */\n  take: () => TapRecord<S> | null;\n  /** Drop the record: this gesture will never spend it on a click. */\n  drop: () => void;\n}\n\n/**\n * The pointer gesture behind a click, recorded where the click cannot report\n * it. A `click` carries no `pointerType` in the engines that matter, so the\n * `pointerdown` before it is the only thing that says which input activated\n * the control — and whether one did at all, since keyboard activation\n * synthesizes a click with no pointer behind it.\n *\n * State goes in with the record because a click reports that no better: a\n * browser that focuses a control on contact can open the very panel the tap\n * was meant to open, and reading \"is it open\" at click time then undoes it.\n * What the gesture started against is what it acts on.\n *\n * The record is spent by one click and dropped by everything else, because a\n * record that outlives its gesture is worse than none:\n *\n * - A scroll or an OS gesture takes the touch away — `pointercancel`, no click\n *   ever — and the finger would sit in the record until some later click.\n * - That later click is often `Enter` on a keyboard, which arrives with no\n *   pointerdown of its own and would inherit the abandoned finger. A keydown\n *   is the start of a keyboard activation and never part of a tap, so it drops\n *   the record too.\n *\n * Both ends have to be wired by the surface: `drop` on `onPointerCancel` and\n * on `onKeyDown`.\n */\nexport function useTapGesture<S>(): TapGesture<S> {\n  const record = useRef<TapRecord<S> | null>(null);\n\n  return useMemo(\n    () => ({\n      start: (event, state) => {\n        record.current = { pointerType: event.pointerType, state };\n      },\n      take: () => {\n        const spent = record.current;\n        record.current = null;\n        return spent;\n      },\n      drop: () => {\n        record.current = null;\n      },\n    }),\n    [],\n  );\n}\n"},{"path":"lib/touch.ts","type":"registry:lib","target":"@lib/touch.ts","content":"// Shared touch primitives. iOS and iPadOS run their own gestures on top of the\n// page — the long-press selection callout and the selection it drags in with\n// it — and they win: once the platform claims a touch it cancels ours\n// mid-gesture, so a press-and-hold or a drag simply dies. Surfaces that own\n// their gesture have to opt out.\n//\n// What the two classes below cover, precisely:\n// - `-webkit-touch-callout: none` stops iOS's long-press callout. WebKit-only:\n//   it is not a property other engines have, so it is inert everywhere else.\n// - `user-select: none` stops the long-press selection on every engine,\n//   Android included, and stops a drag from painting a selection under the\n//   cursor. It is inherited, so it reaches every descendant — which is why the\n//   two classes differ only in whether they apply it unconditionally.\n// What neither covers:\n// - Chrome for Android's long-press menu on a link or an image. No CSS\n//   suppresses it; a gesture surface that wraps one needs its own\n//   `onContextMenu` with `preventDefault()`.\n// - The native drag of an `<img>` or `<a>` descendant. `-webkit-user-drag` is\n//   not inherited and plain divs and buttons are not drag sources, so setting\n//   it on the surface does nothing — the child itself needs `draggable={false}`.\n\n/**\n * Classes for a surface that *is* the control: a thumb, a drum, a stage, a\n * handle, a hold button. Selection is suppressed on every input, because a\n * drag that highlights the control's own label is wrong on a mouse too.\n * Compose with `touch-none` when the surface also owns the scroll axis — leave\n * it off when the page must still scroll from there.\n */\nexport const TOUCH_GESTURE_CLASS = \"select-none [-webkit-touch-callout:none]\";\n\n/**\n * The same opt-out for a gesture surface that wraps content the consumer owns:\n * a scroller, a context-menu trigger, a sheet header, a list row. Selection is\n * suppressed only where the platform runs its own press gestures — a coarse\n * pointer — so a mouse user can still select and copy that content. If the\n * gesture itself would paint a selection under the cursor, add `select-none`\n * for the duration of the gesture rather than reaching for\n * `TOUCH_GESTURE_CLASS`.\n *\n * `pointer: coarse` describes the *primary* pointer and nothing else, so a\n * hybrid machine reads it wrong in both directions: a tablet with a mouse\n * plugged in keeps touch as primary and loses mouse selection, and a laptop\n * with a touchscreen keeps the mouse as primary and leaves selection live\n * under a finger. No media query can answer per interaction — the query is\n * about the device, and the question is about the gesture in progress. The\n * default stays here because it is right on the machines that are one thing or\n * the other, and losing a selection is a nuisance; where the miss costs a\n * *gesture* instead, the surface pairs it with `holdSelection` on the press.\n */\nexport const TOUCH_GESTURE_CONTENT_CLASS =\n  \"[-webkit-touch-callout:none] pointer-coarse:select-none\";\n\n/**\n * Grow a small control's *hit area* to at least 44×44px (WCAG 2.5.5, Apple\n * HIG) on a coarse pointer, without changing what it looks like or the space\n * it takes in layout. An invisible `::after` box, centered on the control and\n * `max(100%, 2.75rem)` on each axis, catches the taps: a 32px icon button\n * reaches 6px past each edge, a wide 40px button only grows vertically, and a\n * control already 44px or larger is left as it is. Hit-testing a pseudo\n * element lands on its owner, so handlers, focus and `:active` all behave.\n *\n * Mouse and trackpad rendering is untouched — nothing here applies outside\n * `pointer: coarse`. Requirements on the element it goes on:\n * - It must be the containing block: add `relative` (and keep it mergeable, so\n *   a consumer's `absolute` still wins through `cn`).\n * - It must not clip itself (`overflow-hidden`), or the overhang is clipped\n *   away along with its hit area.\n * - It must render children: `<input>` and other void elements have no\n *   `::after`. Put it on the wrapper or label instead.\n * - It must not already use `::after` for something visible.\n *\n * Centered 44px means two 32px buttons with a 4px gap overlap by 8px; the\n * later one wins that strip, which still ends well short of the earlier\n * button's visual center. Below a ~32px pitch the later neighbour starts\n * eating into that center — use `TOUCH_TARGET_Y_CLASS` for such dense rows.\n *\n * The overhang is real geometry: inside a scroll container it counts as\n * scrollable overflow, so a control flush against a scroller's edge can add a\n * few pixels of scroll on touch screens. Give such a scroller a little padding\n * on that side.\n */\nexport const TOUCH_TARGET_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:top-1/2 pointer-coarse:after:left-1/2 pointer-coarse:after:size-[max(100%,2.75rem)] pointer-coarse:after:-translate-x-1/2 pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * `TOUCH_TARGET_CLASS` on the vertical axis only, for controls packed tighter\n * than ~32px apart in a row (pagination dots, dense swatches), where growing\n * sideways would hand most of each control to its neighbour. Same rules.\n */\nexport const TOUCH_TARGET_Y_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:top-1/2 pointer-coarse:after:h-[max(100%,2.75rem)] pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * Suppress selection on `element` for as long as a gesture is running on it,\n * whatever the primary pointer of the machine happens to be. Returns the\n * release. Inline, so it wins over the class above and is gone again the\n * moment the gesture ends.\n *\n * For the press gestures a native selection would otherwise steal — a\n * long-press that opens a menu. Elsewhere prefer the classes: a surface that\n * takes selection away for the whole session is a surface whose text nobody\n * can copy.\n */\nexport function holdSelection(element: HTMLElement) {\n  element.style.setProperty(\"user-select\", \"none\");\n  element.style.setProperty(\"-webkit-user-select\", \"none\");\n  return () => {\n    element.style.removeProperty(\"user-select\");\n    element.style.removeProperty(\"-webkit-user-select\");\n  };\n}\n\n/**\n * Pointer capture, best effort. WebKit throws `NotFoundError` when the pointer\n * is already gone by the time the handler runs — routine on iOS, where the\n * system can claim the touch first — and an uncaught throw takes the rest of\n * the handler, the gesture included, down with it. Touch pointers carry\n * implicit capture anyway, so losing it is never fatal.\n */\nexport function capturePointer(element: Element, pointerId: number) {\n  try {\n    element.setPointerCapture(pointerId);\n  } catch {\n    // Pointer is no longer active — implicit capture still applies on touch.\n  }\n}\n\n/** Release a capture taken with `capturePointer`, ignoring a stale pointer. */\nexport function releasePointer(element: Element, pointerId: number) {\n  try {\n    if (element.hasPointerCapture(pointerId)) {\n      element.releasePointerCapture(pointerId);\n    }\n  } catch {\n    // Capture was already dropped by the browser.\n  }\n}\n\n/**\n * Whether this event came from a pointer that is *hovering*: not a touch, and\n * not currently pressed. Which input the user is holding right now is not\n * something a device capability can answer — a touchscreen laptop hovers and\n * taps, and iPadOS reports a fine hovering pointer for a finger — so both\n * paths stay live and each handler branches on the event it was given.\n *\n * A pen resting on the glass is making contact, not hovering: `buttons` is the\n * tell, and it sends a pen tap down the same route a finger takes.\n *\n * This answers what an *enter* asks. A leave is the other half of a pair and\n * has to be read against the enter that started it — `useHoverGesture` in\n * `lib/hooks/use-hover-gesture` does that, and hover surfaces should use it\n * rather than asking this question twice.\n */\nexport const isHoveringPointer = (event: {\n  pointerType: string;\n  buttons: number;\n}) => event.pointerType !== \"touch\" && event.buttons === 0;\n"},{"path":"components/motion/magnetic.tsx","type":"registry:component","target":"@components/motion/magnetic.tsx","content":"\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { SPRING_MOUSE } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MagneticProps {\n  children: ReactNode;\n  strength?: number;\n  className?: string;\n}\n\nexport function Magnetic({ children, strength = 0.35, className }: MagneticProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  // Decorative cursor-follow: skip on touch (phantom hover) and reduced motion.\n  const enabled = !reduce && canHover;\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const sx = useSpring(x, SPRING_MOUSE);\n  const sy = useSpring(y, SPRING_MOUSE);\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = ref.current;\n    if (!el || !enabled) return;\n    const rect = el.getBoundingClientRect();\n    x.set((e.clientX - rect.left - rect.width / 2) * strength);\n    y.set((e.clientY - rect.top - rect.height / 2) * strength);\n  };\n\n  const onLeave = () => {\n    x.set(0);\n    y.set(0);\n  };\n\n  return (\n    <motion.div\n      ref={ref}\n      onMouseMove={onMove}\n      onMouseLeave={onLeave}\n      style={{ x: sx, y: sy }}\n      className={cn(\"inline-block\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"}]}