{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"chrome-pill-button","type":"registry:component","title":"Chrome Pill Button","description":"A pill of polished chrome that reflects a lit room: its horizon, softbox glint and bezel light turn toward the pointer, and a press squashes it and drops a ring into the surface.","dependencies":[],"registryDependencies":[],"files":[{"path":"components/viberdy/chrome-pill-button.tsx","content":"\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport type { CSSProperties, PointerEvent as ReactPointerEvent, ReactNode } from \"react\";\n\n/**\n * ChromePillButton — a pill of polished chrome that reflects a lit room.\n *\n * A bright sky, a hard dark horizon riding low under the label, a lit floor\n * and a softbox glint. The room turns toward the pointer (the horizon\n * rises and falls, the glint slides, the bezel's light swings round), eased\n * in CSS through registered custom properties, so moving the pointer costs\n * no renders. A press squashes the pill and drops a ring into the surface.\n * \"bezel\" is the quiet variant: graphite in a chrome rim. Renders a link\n * when given an href, else a button.\n *\n * Part of the Liquid Chrome kit: a graphite room, a four-band chrome ramp\n * (#08090b, #4b4f58, #c9ced6, #ffffff) multiplied by a finish (chrome, gold,\n * rose, cobalt or any #rrggbb), foil colour only as a thin film. Fonts come\n * from CSS variables with Archivo and Geist fallbacks. Respects\n * prefers-reduced-motion. No dependencies beyond React. Paste it as its own\n * file: it repeats the kit's small helpers, which would clash in one module.\n */\n\nconst SANS = 'var(--font-sans, \"Geist\", \"Inter\", ui-sans-serif, system-ui, sans-serif)';\n\nconst GROUND = \"#0c0d10\";\n\nconst RAISED = \"#1b1c21\";\n\nconst INK = \"rgba(244,245,247,0.94)\";\n\nconst EASE = \"cubic-bezier(0.22, 1, 0.36, 1)\";\n\nconst HEX = /^#[0-9a-fA-F]{6}$/;\n\n/** The reflected room, darkest to hottest. Every chrome surface in the kit reflects these four bands. */\nconst RAMP = [\"#08090b\", \"#4b4f58\", \"#c9ced6\", \"#ffffff\"] as const;\n\n/** Metal finishes: the colour the room is multiplied by. */\nexport const LC_FINISHES = {\n  chrome: \"#ffffff\",\n  gold: \"#f2cf92\",\n  rose: \"#f1bcb4\",\n  cobalt: \"#b9cbff\",\n} as const;\n\nfunction own<T extends object>(map: T, key: string): key is Extract<keyof T, string> {\n  return Object.prototype.hasOwnProperty.call(map, key);\n}\n\n/** A named finish or a #rrggbb tint; anything else falls back to chrome. */\nfunction finishHex(value: string): string {\n  if (own(LC_FINISHES, value)) return LC_FINISHES[value];\n  return HEX.test(value) ? value : LC_FINISHES.chrome;\n}\n\nfunction rgbOf(hex: string): [number, number, number] {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\n/** A ramp colour multiplied by the finish, as rgb(). */\nfunction tinted(hex: string, tint: string): string {\n  const a = rgbOf(hex);\n  const b = rgbOf(tint);\n  return `rgb(${Math.round((a[0] * b[0]) / 255)} ${Math.round((a[1] * b[1]) / 255)} ${Math.round((a[2] * b[2]) / 255)})`;\n}\n\n/** The finished ramp as the CSS variables every chrome gradient and the focus ring read. */\nfunction finishVars(tint: string): CSSProperties {\n  return {\n    \"--lc-0\": tinted(RAMP[0], tint),\n    \"--lc-1\": tinted(RAMP[1], tint),\n    \"--lc-2\": tinted(RAMP[2], tint),\n    \"--lc-3\": tinted(RAMP[3], tint),\n  } as CSSProperties;\n}\n\n/**\n * Chrome as a CSS gradient: bright sky, a hard dark horizon, a lit floor.\n * `--lc-h` moves the horizon (0-100), so a surface can turn in the room.\n */\nconst CHROME_BG =\n  \"linear-gradient(180deg, var(--lc-3) 0%, var(--lc-2) calc(var(--lc-h, 50) * 1% - 16%), var(--lc-1) calc(var(--lc-h, 50) * 1% - 2%), var(--lc-0) calc(var(--lc-h, 50) * 1%), var(--lc-1) calc(var(--lc-h, 50) * 1% + 14%), var(--lc-2) calc(var(--lc-h, 50) * 1% + 34%), var(--lc-3) 100%)\";\n\n/** Links: strip tabs and newlines, refuse backslashes, allow http(s), mailto, tel and same-site paths. */\nfunction safeHref(raw: string): string {\n  const v = raw.replace(/[\\t\\n\\r]/g, \"\").trim();\n  if (!v || v.includes(\"\\\\\")) return \"#\";\n  if (/^(https?:|mailto:|tel:)/i.test(v)) return v;\n  if (/^[/#?]/.test(v) && !/^\\/\\//.test(v)) return v;\n  return \"#\";\n}\n\n// Each part carries only the CSS it needs, so any one of them works on its own.\n// Nothing is interpolated into these sheets; colours arrive as CSS variables.\n// revert-layer keeps an element's own rounded corners when a host page's focus rule flattens them.\nconst FOCUS_CSS = \".lc-scope :focus-visible,.lc-scope:focus-visible{outline:2px solid var(--lc-2);outline-offset:3px;border-radius:revert-layer}\";\n\n// Registered, so the reflection eases in CSS without a render per pointer move.\nconst PILL_CSS =\n  \"@property --lc-h{syntax:'<number>';inherits:true;initial-value:50}\" +\n  \"@property --lc-mx{syntax:'<percentage>';inherits:true;initial-value:32%}\" +\n  \"@property --lc-ang{syntax:'<angle>';inherits:true;initial-value:200deg}\" +\n  \".lc-pill{transition:--lc-h 220ms \" + EASE + \",--lc-mx 220ms \" + EASE + \",--lc-ang 260ms \" + EASE + \",transform 380ms cubic-bezier(0.34,1.56,0.64,1)}\" +\n  \".lc-pill:active{transform:scale(0.965,0.94);transition-duration:220ms,220ms,260ms,130ms}\" +\n  \"@keyframes lc-drop{from{transform:translate(-50%,-50%) scale(0.2);opacity:0.9}to{transform:translate(-50%,-50%) scale(1);opacity:0}}\" +\n  \"@media (prefers-reduced-motion: reduce){.lc-pill{transition:none}.lc-pill:active{transform:none}.lc-drop{display:none}}\";\n\nexport type ChromePillButtonProps = {\n  children: ReactNode;\n  /** Renders a link when set, else a button. */\n  href?: string;\n  onClick?: () => void;\n  /** \"chrome\": a solid chrome pill (one per view). \"bezel\": a graphite pill in a chrome rim. */\n  variant?: \"chrome\" | \"bezel\";\n  finish?: string;\n  size?: \"md\" | \"lg\";\n  type?: \"button\" | \"submit\";\n  disabled?: boolean;\n  className?: string;\n};\n\n/**\n * A pill of polished chrome that reflects a lit room: a bright sky, a hard\n * dark horizon and a lit floor, with a softbox glint across the top. The\n * room turns toward the pointer (the horizon rises and falls, the glint\n * slides, the rim's light swings round), so the pill reads as metal you are\n * tilting. A press squashes it and drops a ring into the surface.\n */\nexport function ChromePillButton({\n  children,\n  href,\n  onClick,\n  variant = \"chrome\",\n  finish = \"chrome\",\n  size = \"md\",\n  type = \"button\",\n  disabled,\n  className,\n}: ChromePillButtonProps) {\n  const tint = finishHex(finish);\n  const ref = useRef<HTMLElement | null>(null);\n  const [drops, setDrops] = useState<{ id: number; x: number; y: number }[]>([]);\n  const dropId = useRef(0);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    let raf = 0;\n    let px = 0;\n    let py = 0;\n    const apply = () => {\n      raf = 0;\n      const r = el.getBoundingClientRect();\n      const cx = r.left + r.width / 2;\n      const cy = r.top + r.height / 2;\n      // Falls off over a few hundred pixels: near the pill the room turns fully.\n      const fx = Math.max(-1, Math.min(1, (px - cx) / Math.max(r.width, 240)));\n      const fy = Math.max(-1, Math.min(1, (py - cy) / 220));\n      el.style.setProperty(\"--lc-mx\", (50 + fx * 42).toFixed(1) + \"%\");\n      // The horizon rides low on a pill, so the label always sits on bright metal.\n      el.style.setProperty(\"--lc-h\", (68 - fy * 9).toFixed(1));\n      el.style.setProperty(\"--lc-ang\", ((Math.atan2(py - cy, px - cx) * 180) / Math.PI + 90).toFixed(1) + \"deg\");\n    };\n    const onMove = (e: PointerEvent) => {\n      px = e.clientX;\n      py = e.clientY;\n      if (!raf) raf = requestAnimationFrame(apply);\n    };\n    let listening = false;\n    const io = new IntersectionObserver((entries) => {\n      const on = entries[entries.length - 1].isIntersecting;\n      if (on && !listening) window.addEventListener(\"pointermove\", onMove, { passive: true });\n      if (!on && listening) window.removeEventListener(\"pointermove\", onMove);\n      listening = on;\n    });\n    io.observe(el);\n    return () => {\n      io.disconnect();\n      cancelAnimationFrame(raf);\n      window.removeEventListener(\"pointermove\", onMove);\n    };\n  }, []);\n\n  const onDown = (e: ReactPointerEvent) => {\n    const r = e.currentTarget.getBoundingClientRect();\n    const id = ++dropId.current;\n    setDrops((d) => [...d.slice(-2), { id, x: e.clientX - r.left, y: e.clientY - r.top }]);\n  };\n\n  const chrome = variant === \"chrome\";\n  const h = size === \"lg\" ? 52 : 44;\n  const style = {\n    ...finishVars(tint),\n    \"--lc-h\": 68,\n    height: h,\n    padding: size === \"lg\" ? \"0 30px\" : \"0 22px\",\n    fontFamily: SANS,\n    fontSize: size === \"lg\" ? 16 : 15,\n    fontWeight: 600,\n    letterSpacing: \"-0.01em\",\n    color: chrome ? \"rgba(12,13,16,0.9)\" : INK,\n    textShadow: chrome ? \"0 1px 0 rgba(255,255,255,0.5)\" : \"none\",\n    background: chrome\n      ? `radial-gradient(38% 70% at var(--lc-mx) 0%, rgba(255,255,255,0.95), rgba(255,255,255,0) 70%), ${CHROME_BG}`\n      : `radial-gradient(60% 120% at var(--lc-mx) 0%, rgba(255,255,255,0.12), rgba(255,255,255,0) 70%), linear-gradient(180deg, ${RAISED}, ${GROUND})`,\n    boxShadow: chrome\n      ? \"inset 0 1px 0 rgba(255,255,255,0.9), inset 0 -1px 1px rgba(0,0,0,0.45), 0 1px 0 rgba(255,255,255,0.08), 0 10px 28px -10px rgba(0,0,0,0.9)\"\n      : \"inset 0 1px 0 rgba(255,255,255,0.1), 0 10px 28px -12px rgba(0,0,0,0.9)\",\n    opacity: disabled ? 0.5 : 1,\n    cursor: disabled ? \"not-allowed\" : \"pointer\",\n  } as CSSProperties;\n  const inner = (\n    <>\n      {/* The bezel: a chrome ring whose light swings toward the pointer. */}\n      <span\n        aria-hidden\n        className=\"pointer-events-none absolute -inset-[2px] rounded-full\"\n        style={{\n          padding: 2,\n          background: \"conic-gradient(from var(--lc-ang), var(--lc-3), var(--lc-1) 18%, var(--lc-0) 34%, var(--lc-2) 52%, var(--lc-3) 62%, var(--lc-1) 80%, var(--lc-3))\",\n          WebkitMask: \"linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)\",\n          mask: \"linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0)\",\n        }}\n      />\n      <span aria-hidden className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-full\">\n        {drops.map((d) => (\n          <span\n            key={d.id}\n            className=\"lc-drop absolute rounded-full\"\n            onAnimationEnd={() => setDrops((all) => all.filter((x) => x.id !== d.id))}\n            style={{\n              left: d.x,\n              top: d.y,\n              width: h * 3,\n              height: h * 3,\n              boxShadow: chrome ? \"inset 0 0 0 2px rgba(255,255,255,0.9), 0 0 0 1px rgba(0,0,0,0.25)\" : \"inset 0 0 0 1.5px var(--lc-2)\",\n              animation: \"lc-drop 620ms \" + EASE + \" forwards\",\n            }}\n          />\n        ))}\n      </span>\n      <span className=\"relative inline-flex items-center gap-2\">{children}</span>\n    </>\n  );\n  const cls =\n    \"lc-scope lc-pill relative inline-flex select-none items-center justify-center whitespace-nowrap rounded-full\" +\n    (className ? \" \" + className : \"\");\n  return (\n    <>\n      <style>{PILL_CSS + FOCUS_CSS}</style>\n      {href && !disabled ? (\n        <a ref={(n) => { ref.current = n; }} href={safeHref(href)} onClick={onClick} onPointerDown={onDown} className={cls} style={style}>\n          {inner}\n        </a>\n      ) : (\n        <button\n          ref={(n) => { ref.current = n; }}\n          type={type}\n          onClick={onClick}\n          onPointerDown={disabled ? undefined : onDown}\n          disabled={disabled}\n          className={cls}\n          style={style}\n        >\n          {inner}\n        </button>\n      )}\n    </>\n  );\n}\n","type":"registry:component","target":"components/viberdy/chrome-pill-button.tsx"}],"meta":{"tier":"free","tags":["button","chrome","metallic","pointer","press","css","y2k"],"href":"https://viberdy.dev/library/chrome-pill-button","aiPrompt":"Build a React + Tailwind button, `ChromePillButton({ children, href, onClick, variant, finish, size, type, disabled })`: a pill of polished chrome.\n\nRegister --lc-h (<number>), --lc-mx (<percentage>) and --lc-ang (<angle>) with @property and transition them over 220-260ms, so the reflection eases in CSS. The chrome variant's background is a radial softbox glint at var(--lc-mx) 0% over a vertical room gradient: --lc-3 at the top, --lc-2, --lc-1, --lc-0 at the horizon (var(--lc-h) percent, default 68 so the label sits on bright metal), then --lc-1, --lc-2 and --lc-3 below, with an inset top highlight and a soft drop shadow. The label is dark ink with a 1px light text-shadow (engraved). A bezel ring is a conic gradient from var(--lc-ang) masked to a 2px ring (mask-composite exclude). The bezel variant is graphite with the same ring and light text.\n\nWhile the button is on screen (IntersectionObserver), listen to window pointermove and, once per frame, set --lc-mx from the pointer's x offset, --lc-h from its y offset (68 minus up to 9) and --lc-ang to the angle toward it. A press scales it to (0.965, 0.94) in 130ms and springs back (cubic-bezier(0.34, 1.56, 0.64, 1)), and drops a ring at the press point that grows and fades over 620ms. Render an anchor with a sanitised href, else a button. Reduced motion: no squash, no rings, no easing.\n\nLiquid Chrome grammar: a graphite room (ground #0c0d10, panels #141519 and #1b1c21, hairlines white 7% and 16% drawn as inset box-shadows or SVG strokes, never border colours), ink #f4f5f7 at 94/62/40%. Chrome is a reflected room, never a flat grey gradient: four bands (#08090b, #4b4f58, #c9ced6, #ffffff) arranged as a bright sky over a hard dark horizon over a lit floor, multiplied by a finish (chrome #ffffff, gold #f2cf92, rose #f1bcb4, cobalt #b9cbff, or any #rrggbb validated against /^#[0-9a-fA-F]{6}$/) and handed to CSS as --lc-0 to --lc-3 with the horizon position in --lc-h. Colour appears only as a thin film on Newton's series (silver #d8dce2, champagne #eadfc4, magenta #d68fc7, cyan #8fd6d1, green #a8d68f, gold #d6b98f, violet #b48fd6, unevenly spaced), never a hue-rotate rainbow. Chrome goes only on heavy, curved forms (display type, pills, discs), never on hairlines. Display type is Archivo at 800 with font-stretch 125% and -0.02em tracking through var(--font-display, \"Archivo\", ...); UI in Geist; captions in Geist Mono, uppercase. Pills and 26px radii. The room eases toward the pointer in about 220ms, ambient drift runs 40s or slower, presses squash about 4% in 130ms. Keyframes and a focus ring (a .lc-scope rule with border-radius: revert-layer) live in a small static <style> the component renders itself; nothing is interpolated into it. Respect prefers-reduced-motion. No dependencies beyond React.","license":"https://viberdy.dev/license"}}