{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"store-button","title":"Store Button","description":"Link to your app on the App Store, Google Play, Galaxy Store or AppGallery with a badge that shows the store's mark and name, in black or as an outline. Search terms: app store badge, store badge, download on the App Store, get it on Google Play, mobile app download.","dependencies":[],"registryDependencies":["@siteplane/base","@siteplane/button","@siteplane/social-button"],"files":[{"path":"packages/ui/src/components/store-button/store-button.tsx","content":"\"use client\";\n\nimport type * as React from \"react\";\n\nimport { useSiteplaneMessages } from \"../../system/locale/messages-context\";\nimport { useResolvedControlSize } from \"../../system/provider/siteplane-ui-provider\";\nimport { Button } from \"../button\";\nimport { AppleLogo } from \"../social-button/social-logos\";\nimport { AppGalleryLogo, GalaxyStoreLogo, GooglePlayLogo } from \"./store-logos\";\n\nimport \"./store-button.css\";\n\n/** The mobile app stores a StoreButton links to. */\nexport type StoreButtonStore =\n  \"app-store\" | \"app-gallery\" | \"galaxy-store\" | \"google-play\";\n\n/**\n * The sizes of the Button that a StoreButton is: md is 40 px high with a\n * 24 px mark, lg 44 px with everything 10 % larger and xl 48 px with\n * everything 20 % larger. There is no sm: 40 px is the smallest App Store\n * badge Apple allows on screen.\n */\nexport type StoreButtonSize = \"md\" | \"lg\" | \"xl\";\n\n/**\n * `solid` is the black badge, black in light and dark themes, with the store's\n * mark in its colours. `outline` is the badge without its fill: text, mark and\n * ring in the text colour.\n */\nexport type StoreButtonAppearance = \"outline\" | \"solid\";\n\n// The store's name, and the message of the store's own wording, the caption\n// over the name. Together they are the link's name.\nconst stores = {\n  \"app-gallery\": { caption: \"appGallery\", name: \"AppGallery\" },\n  \"app-store\": { caption: \"appStore\", name: \"App Store\" },\n  \"galaxy-store\": { caption: \"galaxyStore\", name: \"Galaxy Store\" },\n  \"google-play\": { caption: \"googlePlay\", name: \"Google Play\" },\n} as const satisfies Record<\n  StoreButtonStore,\n  {\n    caption: \"appGallery\" | \"appStore\" | \"galaxyStore\" | \"googlePlay\";\n    name: string;\n  }\n>;\n\nexport type StoreButtonProps = Omit<React.ComponentProps<\"a\">, \"children\"> & {\n  /** The store, which picks the mark, the caption and the store's name. */\n  store: StoreButtonStore;\n  /**\n   * md (40 px), lg (44 px) or xl (48 px). The control size of the\n   * SiteplaneUIProvider by default, and md where that is sm.\n   */\n  size?: StoreButtonSize | undefined;\n  /** `solid` (the default) or `outline`. */\n  appearance?: StoreButtonAppearance | undefined;\n};\n\n// The mark of a store. It is decoration: the link's name says the store.\nfunction logoFor(\n  store: StoreButtonStore,\n  colorful: boolean,\n): React.ReactElement {\n  const logo = {\n    \"aria-hidden\": true,\n    className: \"sp-store-button-logo\",\n    \"data-slot\": \"store-button-logo\",\n  } as const;\n  switch (store) {\n    case \"app-gallery\":\n      return <AppGalleryLogo {...logo} colorful={colorful} />;\n    case \"app-store\":\n      return <AppleLogo {...logo} />;\n    case \"galaxy-store\":\n      return <GalaxyStoreLogo {...logo} colorful={colorful} />;\n    case \"google-play\":\n      return <GooglePlayLogo {...logo} colorful={colorful} />;\n  }\n}\n\n/**\n * A link to an app in a mobile app store: the kit Button rendered as an\n * anchor, with the store's mark before two lines of text, the store's caption\n * over its name. The solid badge is the inverse Button held black in both\n * themes; the outline badge is the outline Button with its ring in the text\n * colour (store-button.css). Size, radius, focus, hover, press and cursor are\n * the Button's: it takes the provider's control size, as every Button does,\n * but never goes below md, 40 px, the smallest App Store badge Apple allows on\n * screen (Lukas, 4 November 2026; it was fixed at md). The link is named with\n * the store's wording, such as \"Get it on Google Play\", unless `aria-label`\n * names it. Anchor props pass through, such as `href`, `target`, `rel` and\n * `onClick`.\n */\nexport function StoreButton({\n  \"aria-label\": ariaLabel,\n  appearance = \"solid\",\n  className,\n  size: sizeProp,\n  store,\n  ...props\n}: StoreButtonProps): React.ReactElement {\n  const messages = useSiteplaneMessages(\"storeButton\");\n  const resolved = useResolvedControlSize(sizeProp);\n  const size = resolved === \"sm\" ? \"md\" : resolved;\n  const caption = messages[stores[store].caption];\n  const { name } = stores[store];\n  return (\n    <Button\n      className={className}\n      data-appearance={appearance}\n      data-slot=\"store-button\"\n      data-store={store}\n      render={<a aria-label={ariaLabel ?? `${caption} ${name}`} {...props} />}\n      size={size}\n      variant={appearance === \"solid\" ? \"inverse\" : \"outline\"}\n    >\n      {logoFor(store, appearance === \"solid\")}\n      <span className=\"sp-store-button-text\" data-slot=\"store-button-text\">\n        <span\n          className=\"sp-store-button-caption\"\n          data-slot=\"store-button-caption\"\n        >\n          {caption}\n        </span>\n        {/* The store's name stays as it is in a translated page. */}\n        <span\n          className=\"sp-store-button-name\"\n          data-slot=\"store-button-name\"\n          translate=\"no\"\n        >\n          {name}\n        </span>\n      </span>\n    </Button>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/store-button/store-button.tsx"},{"path":"packages/ui/src/components/store-button/store-logos.tsx","content":"import type * as React from \"react\";\n\n/*\n * The marks of Google Play, Galaxy Store and AppGallery, drawn for the kit in\n * a few simple shapes on a 24 x 24 grid. They are no store's official badge\n * artwork. The App Store's mark is SocialButton's Apple logo.\n */\n\n/** The props of a store mark: an svg on a 24 x 24 grid. */\nexport type StoreLogoProps = React.SVGProps<SVGSVGElement> & {\n  /** The store's own colours; without them the mark takes the text colour. */\n  colorful?: boolean | undefined;\n};\n\n/*\n * Google Play: a play triangle cut into four pieces by two lines that meet\n * 56 % across it, in the four colours of Google's own logo. In colour the left\n * piece is the whole triangle and the other three lie on it, so no seam shows\n * between them; in one colour each piece is drawn 0.6 units in from the lines\n * it shares, so a gap of 1.2 units parts the pieces.\n */\nconst googlePlayTriangle = \"M2 0.75L22 12L2 23.25Z\";\n\nconst googlePlayPieces = [\n  {\n    color: \"#4285F4\",\n    piece: \"M2 0.75L13.2 12L2 23.25Z\",\n    inset: \"M2 1.6L12.35 12L2 22.4Z\",\n  },\n  {\n    color: \"#34A853\",\n    piece: \"M2 0.75L16.8 9.07L13.2 12Z\",\n    inset: \"M3.92 1.83L16.24 8.76L13.24 11.19Z\",\n  },\n  {\n    color: \"#FBBC04\",\n    piece: \"M16.8 9.07L22 12L16.8 14.93L13.2 12Z\",\n    inset: \"M17.36 9.39L22 12L17.36 14.61L14.15 12Z\",\n  },\n  {\n    color: \"#EA4335\",\n    piece: \"M2 23.25L13.2 12L16.8 14.93Z\",\n    inset: \"M3.92 22.17L13.24 12.81L16.24 15.24Z\",\n  },\n] as const;\n\nexport function GooglePlayLogo({\n  colorful = false,\n  ...props\n}: StoreLogoProps): React.ReactElement {\n  return (\n    <svg fill=\"none\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" {...props}>\n      {colorful ? (\n        <>\n          <path d={googlePlayTriangle} fill={googlePlayPieces[0].color} />\n          {googlePlayPieces.slice(1).map(({ color, piece }) => (\n            <path d={piece} fill={color} key={color} />\n          ))}\n        </>\n      ) : (\n        googlePlayPieces.map(({ color, inset }) => (\n          <path d={inset} fill=\"currentColor\" key={color} />\n        ))\n      )}\n    </svg>\n  );\n}\n\n/*\n * Galaxy Store: a white shopping bag, its body and the arc of its handle, on a\n * rounded tile in the average colour of the store's gradient. In one colour the\n * tile takes the text colour and the bag is cut out of it.\n */\nconst galaxyStoreTile =\n  \"M9 0h6a9 9 0 0 1 9 9v6a9 9 0 0 1-9 9H9a9 9 0 0 1-9-9V9a9 9 0 0 1 9-9Z\";\nconst galaxyStoreBag =\n  \"M8.25 9a3.75 3.75 0 0 1 7.5 0h-1.5a2.25 2.25 0 0 0-4.5 0ZM7 9h10a1 1 0 0 1 1 1v5.5a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V10a1 1 0 0 1 1-1Z\";\n\nexport function GalaxyStoreLogo({\n  colorful = false,\n  ...props\n}: StoreLogoProps): React.ReactElement {\n  return (\n    <svg fill=\"none\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" {...props}>\n      {colorful ? (\n        <>\n          <path d={galaxyStoreTile} fill=\"#F44C5F\" />\n          <path d={galaxyStoreBag} fill=\"#FFFFFF\" />\n        </>\n      ) : (\n        <path\n          clipRule=\"evenodd\"\n          d={`${galaxyStoreTile}${galaxyStoreBag}`}\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n        />\n      )}\n    </svg>\n  );\n}\n\n/*\n * AppGallery: the white smile of the store's bag on a red rounded tile. In one\n * colour the tile takes the text colour and the smile is cut out of it.\n */\nconst appGalleryTile =\n  \"M6 0h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6Z\";\nconst appGallerySmile = \"M7.75 9a4.25 4.25 0 0 0 8.5 0H15a3 3 0 0 1-6 0Z\";\n\nexport function AppGalleryLogo({\n  colorful = false,\n  ...props\n}: StoreLogoProps): React.ReactElement {\n  return (\n    <svg fill=\"none\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" {...props}>\n      {colorful ? (\n        <>\n          <path d={appGalleryTile} fill=\"#CA1E30\" />\n          <path d={appGallerySmile} fill=\"#FFFFFF\" />\n        </>\n      ) : (\n        <path\n          clipRule=\"evenodd\"\n          d={`${appGalleryTile}${appGallerySmile}`}\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n        />\n      )}\n    </svg>\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/store-button/store-logos.tsx"},{"path":"packages/ui/src/components/store-button/store-button.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* A StoreButton is the kit Button as a link at the control size of its\n   * provider, never below md (Lukas, 4 November 2026; it was fixed at md): md\n   * is Button md (40 px), lg Button lg (44 px) and xl Button xl (48 px), with\n   * their padding, radius, focus, hover, press and cursor. Its content is the\n   * store's mark, 8 px before two lines of text: the store's caption, 10 px,\n   * over its name, 16 px, in the kit's typeface. md draws the mark at 24 px;\n   * lg and xl draw mark, gap and text at --sp-store-button-scale, 10 and 20 %\n   * larger, the steps from 40 to 44 and 48 px. The extra [data-chassis] ranks\n   * these rules above the Button's size rules, so the two sheets can load in\n   * any order. */\n  :root .sp-store-button[data-size=\"md\"] {\n    --sp-store-button-scale: 1;\n  }\n\n  :root .sp-store-button[data-size=\"lg\"] {\n    --sp-store-button-scale: 1.1;\n  }\n\n  :root .sp-store-button[data-size=\"xl\"] {\n    --sp-store-button-scale: 1.2;\n  }\n\n  :root .sp-store-button[data-chassis][data-size] {\n    --sp-rw-button-icon: calc(\n      var(--sp-rw-space-6) * var(--sp-store-button-scale, 1)\n    );\n    --sp-rw-button-content-gap: calc(\n      var(--sp-rw-space-2) * var(--sp-store-button-scale, 1)\n    );\n  }\n\n  /* The two lines start at one edge, and the pair centres in the Button's\n   * height. Their unitless lines scale with the text at lg and xl; the name's\n   * line is its size, so in Inter the baselines stand 16 px apart at md, at\n   * 15.5 and 31.5 px from the top, as on the stores' badges. */\n  :root .sp-store-button-text {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n  }\n\n  :root .sp-store-button-caption {\n    font-size: calc(var(--sp-rw-font-10) * var(--sp-store-button-scale, 1));\n    line-height: 1.2;\n  }\n\n  :root .sp-store-button-name {\n    font-size: calc(var(--sp-rw-font-16) * var(--sp-store-button-scale, 1));\n    font-weight: var(--sp-rw-font-weight-label);\n    line-height: 1;\n  }\n\n  /* Google Play and AppGallery set their caption in capitals. */\n  :root .sp-store-button[data-store=\"google-play\"] .sp-store-button-caption,\n  :root .sp-store-button[data-store=\"app-gallery\"] .sp-store-button-caption {\n    text-transform: uppercase;\n  }\n\n  /* A mark keeps its full colour, without the Button's 80 % icon opacity. */\n  :root .sp-store-button-logo {\n    opacity: 1;\n  }\n\n  /* The solid badge is the inverse Button, the kit's black Button, held black\n   * in Light and Dark: white text and mark on black, and the veil of hover and\n   * press in its white text, at the inverse Button's own steps. Its ring is the\n   * stores' light grey (#a6a6a6) in Light and white in Dark. */\n  :root .sp-store-button[data-chassis][data-appearance=\"solid\"] {\n    --sp-rw-button-source: oklch(0% 0 0);\n    --sp-rw-button-text: var(--sp-rw-fg-emphasis);\n    --sp-rw-button-interaction: var(--sp-rw-button-text);\n    --sp-rw-button-rim: inset 0 0 0 var(--sp-rw-border-default)\n      light-dark(oklch(72.5% 0 0), oklch(100% 0 0));\n  }\n\n  /* The outline badge is the outline Button with its ring in its text colour,\n   * the primary text, as its mark and its words. */\n  :root .sp-store-button[data-chassis][data-appearance=\"outline\"] {\n    --sp-rw-button-rim: inset 0 0 0 var(--sp-rw-border-default)\n      var(--sp-rw-button-text);\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/store-button/store-button.css"},{"path":"packages/ui/src/components/store-button/index.ts","content":"export * from \"./store-button\";\n","type":"registry:ui","target":"~/src/siteplane/components/store-button/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/store-button.md","categories":["Actions"],"meta":{"avoidWhen":["Avoid for a link that does not open the app's page in that store; use Button or a link.","Avoid where a store's guidelines require its official badge; use the badge artwork the store provides."],"description":"Link to your app on the App Store, Google Play, Galaxy Store or AppGallery with a badge that shows the store's mark and name, in black or as an outline.","knownLimitations":["The marks are simplified drawings, not the stores' official badge artwork."],"providerNotes":["The caption over the store's name is the storeButton message of SiteplaneLocaleProvider, English by default, in the store's own wording; the store's name stays as it is."],"releaseStatus":"preview","searchTerms":["app store badge","store badge","download on the App Store","get it on Google Play","mobile app download","Galaxy Store","AppGallery"],"useWhen":["Use on a landing page, a footer or a download page to send visitors to your mobile app in each store that lists it.","Use the outline appearance where a row of black badges would weigh too much, such as in a light footer."]}}