{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A BreakpointIndicator component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nconst STORAGE_KEY = \"smoothui:breakpoint-indicator:visible\";\nconst DEFAULT_HOTKEY = \"b\";\nconst DEFAULT_POSITION: BreakpointIndicatorPosition = \"bottom-left\";\nconst BASE_BREAKPOINT_NAME = \"base\";\nconst RULER_MIN_PERCENT = 0;\nconst RULER_MAX_PERCENT = 100;\n\nconst DEFAULT_BREAKPOINTS: Breakpoint[] = [\n  { min: 640, name: \"sm\" },\n  { min: 768, name: \"md\" },\n  { min: 1024, name: \"lg\" },\n  { min: 1280, name: \"xl\" },\n  { min: 1536, name: \"2xl\" },\n];\n\nexport interface Breakpoint {\n  min: number;\n  name: string;\n}\n\nexport type BreakpointIndicatorPosition =\n  | \"bottom-left\"\n  | \"bottom-right\"\n  | \"top-left\"\n  | \"top-right\";\n\nexport interface BreakpointIndicatorProps {\n  breakpoints?: Breakpoint[];\n  className?: string;\n  enabled?: boolean;\n  fixed?: boolean;\n  hotkey?: string;\n  position?: BreakpointIndicatorPosition;\n  showRuler?: boolean;\n  showSize?: boolean;\n}\n\nconst POSITION_CLASSES: Record<BreakpointIndicatorPosition, string> = {\n  \"bottom-left\": \"bottom-3 left-3\",\n  \"bottom-right\": \"bottom-3 right-3\",\n  \"top-left\": \"top-3 left-3\",\n  \"top-right\": \"top-3 right-3\",\n};\n\nconst readStoredVisibility = (): boolean => {\n  if (typeof window === \"undefined\") {\n    return true;\n  }\n\n  try {\n    const stored = window.localStorage.getItem(STORAGE_KEY);\n    return stored === null ? true : stored === \"true\";\n  } catch {\n    return true;\n  }\n};\n\nconst getActiveBreakpoint = (\n  breakpoints: Breakpoint[],\n  width: number\n): { active: Breakpoint; next: Breakpoint | null } => {\n  const sorted = [...breakpoints].sort((a, b) => a.min - b.min);\n  let active: Breakpoint = { min: 0, name: BASE_BREAKPOINT_NAME };\n  let next: Breakpoint | null = sorted[0] ?? null;\n\n  for (let index = 0; index < sorted.length; index += 1) {\n    const candidate = sorted[index];\n    if (width >= candidate.min) {\n      active = candidate;\n      next = sorted[index + 1] ?? null;\n    }\n  }\n\n  return { active, next };\n};\n\nconst clampPercent = (value: number): number =>\n  Math.min(RULER_MAX_PERCENT, Math.max(RULER_MIN_PERCENT, value));\n\nexport default function BreakpointIndicator({\n  breakpoints = DEFAULT_BREAKPOINTS,\n  className,\n  enabled = process.env.NODE_ENV !== \"production\",\n  fixed = true,\n  hotkey = DEFAULT_HOTKEY,\n  position = DEFAULT_POSITION,\n  showRuler = false,\n  showSize = true,\n}: BreakpointIndicatorProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [width, setWidth] = useState(0);\n  const [height, setHeight] = useState(0);\n  const [visible, setVisible] = useState<boolean>(readStoredVisibility);\n\n  useEffect(() => {\n    if (!enabled || typeof document === \"undefined\") {\n      return;\n    }\n\n    let frame = 0;\n    const measure = () => {\n      frame = 0;\n      setWidth(document.documentElement.clientWidth);\n      setHeight(document.documentElement.clientHeight);\n    };\n\n    const observer = new ResizeObserver(() => {\n      if (frame) {\n        return;\n      }\n      frame = window.requestAnimationFrame(measure);\n    });\n\n    observer.observe(document.documentElement);\n    measure();\n\n    return () => {\n      observer.disconnect();\n      if (frame) {\n        window.cancelAnimationFrame(frame);\n      }\n    };\n  }, [enabled]);\n\n  useEffect(() => {\n    if (!enabled) {\n      return;\n    }\n\n    const normalizedHotkey = hotkey.toLowerCase();\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (\n        (event.metaKey || event.ctrlKey) &&\n        event.key.toLowerCase() === normalizedHotkey\n      ) {\n        event.preventDefault();\n        setVisible((prev) => !prev);\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [enabled, hotkey]);\n\n  useEffect(() => {\n    if (!enabled) {\n      return;\n    }\n\n    try {\n      window.localStorage.setItem(STORAGE_KEY, String(visible));\n    } catch {\n      // localStorage unavailable (private mode, blocked storage, etc.)\n    }\n  }, [enabled, visible]);\n\n  if (!(enabled && visible)) {\n    return null;\n  }\n\n  const { active, next } = getActiveBreakpoint(breakpoints, width);\n  const rulerPercent = next\n    ? clampPercent(((width - active.min) / (next.min - active.min)) * 100)\n    : RULER_MAX_PERCENT;\n  const remainingPx = next ? Math.max(0, next.min - width) : 0;\n\n  return (\n    <div\n      aria-label={`Current breakpoint ${active.name}, viewport ${width} by ${height} pixels`}\n      aria-live=\"polite\"\n      className={cn(\n        \"z-50 flex items-center gap-2 rounded-full border border-foreground/10 bg-background/95 px-3 py-1.5 font-mono text-foreground text-xs shadow-lg backdrop-blur\",\n        fixed ? \"fixed\" : \"absolute\",\n        POSITION_CLASSES[position],\n        className\n      )}\n      role=\"status\"\n    >\n      <div className=\"relative h-4 overflow-hidden\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            animate={\n              shouldReduceMotion ? { opacity: 1, y: 0 } : { opacity: 1, y: 0 }\n            }\n            className=\"inline-block font-semibold uppercase tracking-wide\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, y: -8 }\n            }\n            initial={\n              shouldReduceMotion ? { opacity: 1, y: 0 } : { opacity: 0, y: 8 }\n            }\n            key={active.name}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { bounce: 0.1, duration: 0.25, type: \"spring\" }\n            }\n          >\n            {active.name}\n          </motion.span>\n        </AnimatePresence>\n      </div>\n\n      {showSize ? (\n        <span className=\"text-foreground/60\">\n          {width}×{height}\n        </span>\n      ) : null}\n\n      {showRuler ? (\n        <div aria-hidden=\"true\" className=\"flex items-center gap-1.5\">\n          <div className=\"h-1 w-16 overflow-hidden rounded-full bg-foreground/10\">\n            <div\n              className=\"h-full bg-foreground/60\"\n              style={{ width: `${rulerPercent}%` }}\n            />\n          </div>\n          {next ? (\n            <span className=\"text-foreground/40\">{remainingPx}px</span>\n          ) : null}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/breakpoint-indicator/index.tsx","type":"registry:ui"}],"name":"breakpoint-indicator","registryDependencies":[],"title":"Breakpoint Indicator","type":"registry:ui"}