{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"weight-press-button","type":"registry:component","title":"Weight Press Button","description":"A button whose press you can see in its type: hover and the label thickens as the button lifts; press and it goes bold under your finger while the button sinks and its shadow collapses to the contact line, then springs back.","dependencies":[],"registryDependencies":[],"files":[{"path":"components/viberdy/weight-press-button.tsx","content":"\"use client\";\n\nimport { useState, useSyncExternalStore } from \"react\";\nimport type { CSSProperties, KeyboardEvent as ReactKeyboardEvent, ReactNode } from \"react\";\n\n/**\n * WeightPressButton — a button whose press you can see in its type.\n *\n * Hover and the label thickens a step and the button lifts a pixel as its\n * ambient shadow widens; press and the label goes bold under your finger\n * while the button sinks and the shadow collapses to its contact line, then\n * it springs back past rest. The width never changes (the label is laid\n * out at its boldest). Solid, light and outline. CSS only.\n *\n * Part of the Modern Minimal kit: a paper-white room (#f5f5f2, surfaces\n * #ffffff, ink #141412) and one light, dawn #ffa64d, only ever as light.\n * Two-layer shadows from one light above; scroll binds values directly,\n * with no easing. Inter Tight through var(--font-inter-tight) for display\n * over Geist. Respects prefers-reduced-motion. No dependencies beyond React.\n * Paste it as its own file: it repeats the kit's small helpers, which would\n * clash in one module. Load Inter Tight with next/font (variable:\n * \"--font-inter-tight\") on a parent, or from Google Fonts or @fontsource.\n */\n\nconst DISPLAY = 'var(--font-inter-tight, \"Inter Tight\", var(--font-sans, \"Geist\"), ui-sans-serif, system-ui, sans-serif)';\n\nconst EASE = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** A paper-white room and one light. */\nconst PAPER = {\n  ground: \"#f5f5f2\",\n  surface: \"#ffffff\",\n  well: \"#ecebe6\",\n  solid: \"#141412\",\n  ink: \"rgba(20,20,18,0.92)\",\n  ink2: \"rgba(20,20,18,0.6)\",\n  ink3: \"rgba(20,20,18,0.38)\",\n  line: \"rgba(20,20,18,0.08)\",\n  line2: \"rgba(20,20,18,0.14)\",\n  dawn: \"#ffa64d\",\n  night: \"#0e0f11\",\n} as const;\n\n/** Light from above: a 1px contact shadow and a soft ambient one, both falling the same way. */\nconst SHADOW = \"0 1px 2px rgba(20,20,18,0.06), 0 24px 48px -12px rgba(20,20,18,0.1)\";\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\nfunction subscribeMotion(cb: () => void) {\n  const m = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  m.addEventListener(\"change\", cb);\n  return () => m.removeEventListener(\"change\", cb);\n}\n\n/** True when the visitor asked for reduced motion. False on the server. */\nfunction useReducedMotion(): boolean {\n  return useSyncExternalStore(\n    subscribeMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\n// Each part carries only the CSS it needs, so any one of them works on its own.\n// revert-layer keeps an element's own rounded corners when a host page's focus rule flattens them.\nconst FOCUS_CSS = \".mm-scope :focus-visible,.mm-scope:focus-visible{outline:2px solid var(--mm-ring,#141412)!important;outline-offset:3px;border-radius:revert-layer}\";\n\nconst MM_BUTTON_INK = {\n  solid: { bg: PAPER.solid, fg: \"#f5f5f2\", border: \"transparent\", shadow: true },\n  light: { bg: PAPER.surface, fg: PAPER.solid, border: \"rgba(20,20,18,0.08)\", shadow: true },\n  outline: { bg: \"transparent\", fg: PAPER.solid, border: \"rgba(20,20,18,0.22)\", shadow: false },\n} as const;\n\nexport type MmButtonVariant = keyof typeof MM_BUTTON_INK;\n\n/**\n * WeightPressButton — a button whose press you can see in its type.\n *\n * A pill in Inter Tight. Hover and the label thickens a step and the button\n * lifts a pixel as its ambient shadow widens; press and the label goes bold\n * under your finger while the button sinks, the ambient shadow collapsing to\n * the contact one, then springs back past rest on release. Both shadows fall\n * from one light above. Solid, light and outline; a link when given an href.\n * CSS transitions on a variable weight, no animation library.\n */\nexport function WeightPressButton({\n  children,\n  href,\n  onClick,\n  type = \"button\",\n  variant = \"solid\",\n  size = \"lg\",\n  disabled = false,\n  ariaLabel,\n  ariaPressed,\n  className = \"\",\n}: {\n  children: ReactNode;\n  href?: string;\n  onClick?: () => void;\n  type?: \"button\" | \"submit\";\n  variant?: MmButtonVariant;\n  size?: \"lg\" | \"md\" | \"sm\";\n  disabled?: boolean;\n  ariaLabel?: string;\n  ariaPressed?: boolean;\n  className?: string;\n}) {\n  const ink = MM_BUTTON_INK[variant] ?? MM_BUTTON_INK.solid;\n  const [state, setState] = useState<\"rest\" | \"hover\" | \"down\">(\"rest\");\n  const reduced = useReducedMotion();\n  const weight = state === \"down\" ? 680 : state === \"hover\" ? 560 : 500;\n  const shadow = !ink.shadow\n    ? \"none\"\n    : state === \"down\"\n      ? \"0 1px 1px rgba(20,20,18,0.1), 0 6px 12px -8px rgba(20,20,18,0.14)\"\n      : state === \"hover\"\n        ? \"0 1px 2px rgba(20,20,18,0.06), 0 30px 56px -14px rgba(20,20,18,0.16)\"\n        : SHADOW;\n  const pad = size === \"sm\" ? \"h-9 px-4 text-[13px]\" : size === \"md\" ? \"h-11 px-5 text-[14px]\" : \"h-[52px] px-7 text-[15px]\";\n  const common = {\n    \"aria-label\": ariaLabel,\n    className: \"mm-scope relative inline-flex select-none items-center justify-center rounded-full no-underline outline-none \" + pad + (disabled ? \" pointer-events-none opacity-40 \" : \" \") + className,\n    style: {\n      background: ink.bg,\n      color: ink.fg,\n      border: `1px solid ${ink.border}`,\n      boxShadow: shadow,\n      fontFamily: DISPLAY,\n      fontWeight: weight,\n      letterSpacing: \"-0.01em\",\n      transform: state === \"down\" ? \"translateY(1px) scale(0.985)\" : state === \"hover\" ? \"translateY(-1px)\" : \"none\",\n      transition: reduced\n        ? \"none\"\n        : state === \"rest\"\n          ? `transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 260ms ${EASE}, font-weight 220ms ${EASE}`\n          : `transform 160ms ${EASE}, box-shadow 160ms ${EASE}, font-weight 160ms ${EASE}`,\n    } as CSSProperties,\n    onPointerEnter: () => setState(\"hover\"),\n    onPointerLeave: () => setState(\"rest\"),\n    onPointerDown: () => setState(\"down\"),\n    onPointerUp: () => setState(\"hover\"),\n    onPointerCancel: () => setState(\"rest\"),\n    onFocus: () => setState((s) => (s === \"rest\" ? \"hover\" : s)),\n    onBlur: () => setState(\"rest\"),\n    onKeyDown: (e: ReactKeyboardEvent) => {\n      if (e.key === \"Enter\" || (e.key === \" \" && !href)) setState(\"down\");\n    },\n    onKeyUp: () => setState(\"hover\"),\n  };\n  // The label is laid out at the boldest weight too, invisibly, so the button never changes width.\n  const label = (\n    <span className=\"relative inline-grid\">\n      <span aria-hidden className=\"invisible col-start-1 row-start-1 whitespace-nowrap\" style={{ fontWeight: 680 }}>\n        {children}\n      </span>\n      <span className=\"col-start-1 row-start-1 whitespace-nowrap text-center\">{children}</span>\n    </span>\n  );\n  if (href) {\n    return (\n      <>\n        <style>{FOCUS_CSS}</style>\n        <a href={safeHref(href)} onClick={onClick} {...common}>\n          {label}\n        </a>\n      </>\n    );\n  }\n  return (\n    <>\n      <style>{FOCUS_CSS}</style>\n      <button type={type} onClick={onClick} disabled={disabled} aria-pressed={ariaPressed} {...common}>\n        {label}\n      </button>\n    </>\n  );\n}\n","type":"registry:component","target":"components/viberdy/weight-press-button.tsx"}],"meta":{"tier":"free","tags":["button","variable font","press","shadow","spring","css"],"href":"https://viberdy.dev/library/weight-press-button","aiPrompt":"Build a React + Tailwind button, `WeightPressButton({ children, href, onClick, type, variant, size, disabled, ariaLabel, ariaPressed, className })`.\n\nA pill in Inter Tight at -0.01em. States: rest (weight 500, the two-layer shadow), hover (560, translateY(-1px), the ambient shadow widened to 0 30px 56px -14px at 16%), pressed (680, translateY(1px) scale(0.985), the ambient collapsed to 0 6px 12px -8px and a darker contact). Into hover and press in 160ms; back to rest with a 420ms cubic-bezier(0.34, 1.56, 0.64, 1) spring. The label renders twice in one grid cell, the hidden copy at 680, so the width never changes. Inks: solid (#141412 on paper), light (white with a hairline and the shadow), outline (a 22% hairline, no shadow).\n\nModern Minimal grammar: a paper-white room, ground #f5f5f2 (surfaces #ffffff that lift, a recessed well #ecebe6), ink #141412 at 92/60/38% (never grey-400 body copy), hairlines at 8% and 14%; one light, dawn #ffa64d, only ever as light (the lamp's glow, a lit state), never as a fill. Shadows come from one light above in two layers: a contact 0 1px 2px rgba(20,20,18,0.06) and an ambient 0 24px 48px -12px rgba(20,20,18,0.1), both falling the same way; a pressed thing loses the ambient, a lifted one widens it. Type: Inter Tight through var(--font-inter-tight, \"Inter Tight\", ...) for display at 600 with -0.035em tracking and 0.98 leading (tracking tightens only at display sizes), Geist for reading at 16-17px/1.55, Geist Mono uppercase at 11-12px for readouts. Radii 12px controls (pills for buttons), 20px cards, 28px media. Motion: scroll binds values directly with no easing (a value moves exactly with the scroll, like scrubbing film); discrete hover and press states take 150-260ms with a spring back past rest; ambient loops 40s or more. Respect prefers-reduced-motion (ambient scenes hold still; scroll-bound values still follow the scroll, which the visitor drives). No dependencies beyond React.","license":"https://viberdy.dev/license"}}